import APIClient import Foundation import HostRegistry import TestSupport import Testing import WireProtocol @testable import WebTerm /// T-iOS-32(C2)· 历史会话恢复的**发射端**:`ProjectsViewModel.requestResumeClaude` /// 必须走与"在此仓库开新会话"完全相同的 `attach(null, cwd)` 缝,只把 bootstrap 换成 /// `claude --resume \r`,并在铸造 request 之前拦下不可信的 cwd / 会话 id。 @MainActor @Suite("ProjectResumeLaunch") struct ProjectResumeLaunchTests { private nonisolated static let base = "http://192.168.1.5:3000" private func makeViewModel() throws -> ProjectsViewModel { let baseURL = try #require(URL(string: Self.base)) let endpoint = try #require(HostEndpoint(baseURL: baseURL)) let host = HostRegistry.Host(id: UUID(), name: "书房 Mac", endpoint: endpoint) return ProjectsViewModel(host: host, http: FakeHTTPTransport()) } @Test("合法 cwd + 合法 id → OpenRequest 携带 `claude --resume \\r`") func resumeBuildsBootstrapInput() throws { let viewModel = try makeViewModel() let sessionId = "3f2b1c4d-0000-4000-8000-000000000001" viewModel.requestResumeClaude(cwd: "/repos/web-terminal", sessionId: sessionId) let request = try #require(viewModel.openRequest) #expect(request.cwd == "/repos/web-terminal") #expect(request.bootstrapInput == "claude --resume \(sessionId)\r") #expect(viewModel.openErrorMessage == nil) } @Test("相对 cwd → 拒绝铸造(与 requestOpenClaude 同款纪律)") func relativeCwdIsRejected() throws { let viewModel = try makeViewModel() viewModel.requestResumeClaude(cwd: "repos/web-terminal", sessionId: "abc") #expect(viewModel.openRequest == nil) #expect(viewModel.openErrorMessage != nil) } @Test("危险会话 id → 拒绝铸造(绝不把它送进 PTY 命令行)") func injectionIdIsRejected() throws { let viewModel = try makeViewModel() viewModel.requestResumeClaude( cwd: "/repos/web-terminal", sessionId: "abc; rm -rf ~" ) #expect(viewModel.openRequest == nil) #expect(viewModel.openErrorMessage == ResumeCopy.notResumable) } @Test("普通开会话仍是 `claude\\r`(恢复路径没有改动既有行为)") func plainOpenIsUnchanged() throws { let viewModel = try makeViewModel() viewModel.requestOpenClaude(cwd: "/repos/web-terminal") #expect(viewModel.openRequest?.bootstrapInput == ProjectLaunch.claudeBootstrapInput) } } /// C2 · PR 链接白名单(`PrStatus.url` 是服务器字节 —— 交给 `openURL` 等于让远端 /// 决定要唤起哪个 App)。 @Suite("PrLink") struct PrLinkTests { @Test("https + github.com(含子域)→ 可点") func githubHttpsIsAllowed() { #expect(PrLink.url(from: "https://github.com/o/r/pull/7") != nil) #expect(PrLink.url(from: "https://www.github.com/o/r/pull/7") != nil) } @Test( "其余一律拒绝:http、自定义 scheme、非 github 主机、伪造后缀、nil", arguments: [ "http://github.com/o/r/pull/7", "javascript:alert(1)", "webterm://join?id=1", "https://evil.com/o/r/pull/7", "https://github.com.evil.com/o/r/pull/7", "not a url at all", "", ] ) func everythingElseIsRejected(raw: String) { #expect(PrLink.url(from: raw) == nil, "\(raw) 不应被接受") } @Test("nil URL(gh 降级时没有 url 字段)→ nil") func nilIsRejected() { #expect(PrLink.url(from: nil) == nil) } }