test(ios): close T-iOS-32's end-to-end half — real worktree lifecycle against a real server

T-iOS-32's acceptance is "builder 测试 + 端到端一次". The builder half was green,
but grep for projects/worktree across the whole test tree returned nothing — no
test had ever created or removed a real git worktree against a real server.

Six tests on a throwaway git fixture: create/remove/prune verified against both
the filesystem and git's own `worktree list --porcelain` + .git/worktrees/, and
asserting the values the SERVER derives rather than echoing our input (branch
feature/e2e-worktree becomes directory feature-e2e-worktree; the raw branch name
must never appear in the path). Includes a differential leg for the guarded-route
rule — the same request without Origin is 403 with zero side effects, and with it
is 200 — which needs raw URLSession, since APIClient structurally cannot emit an
Origin-less guarded request.

GET /sessions gets its own HOME-isolated server: history.ts reads
os.homedir()/.claude/projects, so against the shared harness the assertions would
land on the developer's real Claude history, and in CI the directory is absent so
it degrades to [] and proves nothing.

Integration tests 26 -> 32, independently re-run.
This commit is contained in:
Yaojia Wang
2026-07-30 16:58:22 +02:00
parent 5cc755b0b6
commit ddab77b337
10 changed files with 1577 additions and 23 deletions

View File

@@ -0,0 +1,439 @@
//
// TokenPolicyDriftTests.swift F3(2) +
//
// 访cookie `webterm_auth` `[A-Za-z0-9._~+/=-]`
// 16512 ****
// 1. `SessionCore/AuthCookie.swift` Swift `Regex` WS upgrade
// 2. `HostRegistry/AccessToken.swift` `Set<Character>` + countKeychain
// 3. `APIClient/Endpoints.swift`(AccessToken.swift) `Set<Character>` + countHTTP
// 4. `src/config.ts:178` `WEBTERM_TOKEN_RE`
// 774 ****西****
//
// orchestrator `WireProtocol`
// 线IntegrationTests ****
// Package.swift " = "
//
//
// - Swift + ****
// - `src/config.ts` ****
// Swift
// - cookie `src/http/auth.ts` `AUTH_COOKIE_NAME`
//
// ""
// ****
import Foundation
import HostRegistry
import Testing
@testable import APIClient
@testable import SessionCore
// src/
enum ServerTokenPolicy {
/// `src/config.ts` `const WEBTERM_TOKEN_RE = //`
static func extractedTokenPatternSource() throws -> String {
let source = try readRepoFile("src/config.ts")
guard let line = source.split(separator: "\n").first(where: {
$0.contains("const WEBTERM_TOKEN_RE")
}) else {
throw HarnessError.setup("src/config.ts 里找不到 WEBTERM_TOKEN_RE 定义")
}
guard let open = line.firstIndex(of: "/"), let close = line.lastIndex(of: "/"),
open < close
else {
throw HarnessError.setup("WEBTERM_TOKEN_RE 那一行不是 /…/ 正则字面量: \(line)")
}
return String(line[line.index(after: open)..<close])
}
/// `src/http/auth.ts` `AUTH_COOKIE_NAME = '<name>'`
static func extractedCookieName() throws -> String {
let source = try readRepoFile("src/http/auth.ts")
guard let line = source.split(separator: "\n").first(where: {
$0.contains("export const AUTH_COOKIE_NAME")
}) else {
throw HarnessError.setup("src/http/auth.ts 里找不到 AUTH_COOKIE_NAME 定义")
}
guard let open = line.firstIndex(of: "'"), let close = line.lastIndex(of: "'"),
open < close
else {
throw HarnessError.setup("AUTH_COOKIE_NAME 那一行没有单引号字面量: \(line)")
}
return String(line[line.index(after: open)..<close])
}
/// ****
/// - JS `^`/`$` `m` = **** ICU `\A`/`\z`
/// JS `$` PCRE/ICU "" `$`
/// - `{16,512}` JS **UTF-16 **NSRegularExpression
/// UTF-16 Swift `Character`
/// **** ASCII
static func makeEvaluator() throws -> @Sendable (String) -> Bool {
let jsSource = try extractedTokenPatternSource()
var icuSource = jsSource
guard icuSource.hasPrefix("^"), icuSource.hasSuffix("$") else {
throw HarnessError.setup("WEBTERM_TOKEN_RE 不再是 ^…$ 整串锚定形状: \(jsSource)")
}
icuSource.removeFirst()
icuSource.removeLast()
let regex = try NSRegularExpression(pattern: "\\A\(icuSource)\\z")
return { candidate in
let range = NSRange(candidate.startIndex..., in: candidate)
return regex.firstMatch(in: candidate, options: [], range: range) != nil
}
}
private static func readRepoFile(_ relativePath: String) throws -> String {
let url = try locateRepoRoot().appending(path: relativePath)
guard let text = try? String(contentsOf: url, encoding: .utf8) else {
throw HarnessError.setup("读不到 \(url.path)(服务端真源缺失)")
}
return text
}
}
// +
struct TokenProbe: Sendable {
let label: String
let candidate: String
}
enum TokenCorpus {
/// 16
static let validBase = "AbCdEfGh01234567"
static func repeated(_ unit: String, count: Int) -> String {
String(repeating: unit, count: count)
}
/// emoji
/// U+2013 '-' 15/16/512/513
static let probes: [TokenProbe] = {
var out: [TokenProbe] = []
func add(_ label: String, _ candidate: String) {
out.append(TokenProbe(label: label, candidate: candidate))
}
// ASCII
add("len15(ASCII)", String(repeated("a", count: 15)))
add("len16(ASCII)", String(repeated("a", count: 16)))
add("len512(ASCII)", String(repeated("a", count: 512)))
add("len513(ASCII)", String(repeated("a", count: 513)))
add("len0(empty)", "")
//
add("charset-all-legal", "AZaz09._~+/=-" + "abc")
add("charset-dot-only", String(repeated(".", count: 16)))
add("charset-slash-only", String(repeated("/", count: 16)))
add("charset-equals-only", String(repeated("=", count: 16)))
add("charset-tilde-only", String(repeated("~", count: 16)))
add("charset-plus-only", String(repeated("+", count: 16)))
add("charset-hyphen-only", String(repeated("-", count: 16)))
//
// `_` ****`[A-Za-z0-9._~+/=-]` `.` `_`,
// "",
//
add("underscore(在集合内)", "AbCdEfGh0123456_")
// ""
add("colon", "AbCdEfGh0123456:")
add("semicolon(cookie 分隔符)", "AbCdEfGh0123456;")
add("space-interior", "AbCdEfGh 123456789")
add("percent", "AbCdEfGh0123456%")
add("backslash", "AbCdEfGh0123456\\")
add("comma", "AbCdEfGh0123456,")
add("quote-double", "AbCdEfGh0123456\"")
// CR/LF
add("CR-suffix", validBase + "\r")
add("LF-suffix", validBase + "\n")
add("CRLF-injection", validBase + "\r\nX-Evil: 1")
add("LF-interior", "AbCdEfGh\n123456789")
add("NUL-suffix", validBase + "\u{0}")
add("TAB-suffix", validBase + "\t")
// HostRegistry trim divergence
add("leading-space", " " + validBase)
add("trailing-space", validBase + " ")
add("leading-newline", "\n" + validBase)
// U+2013 EN DASH vs ASCII '-'
add("ascii-hyphen", "AbCdEfGh-1234567")
add("en-dash-U+2013", "AbCdEfGh\u{2013}1234567")
add("minus-sign-U+2212", "AbCdEfGh\u{2212}1234567")
add("non-breaking-hyphen-U+2011", "AbCdEfGh\u{2011}1234567")
// Halfwidth and Fullwidth Forms
add("fullwidth-A(U+FF21)", "\u{FF21}bCdEfGh01234567")
add("fullwidth-digit0(U+FF10)", "\u{FF10}bCdEfGh01234567")
add("fullwidth-fullstop(U+FF0E)", "AbCdEfGh0123456\u{FF0E}")
add("fullwidth-solidus(U+FF0F)", "AbCdEfGh0123456\u{FF0F}")
add("fullwidth-all", String(repeated("\u{FF21}", count: 16)))
// Swift Character=1JS =2
add("combining-acute", "A\u{0301}bCdEfGh01234567")
add("combining-only", String(repeated("a\u{0301}", count: 16)))
// Swift Character = 256 16512 JS UTF-16 = 512
// ""
add("combining-256-graphemes-512-units", String(repeated("a\u{0301}", count: 256)))
add("precomposed-e-acute", "\u{00E9}bCdEfGh01234567")
// /
add("zero-width-space(U+200B)", "AbCdEfGh\u{200B}1234567")
add("zero-width-joiner(U+200D)", "AbCdEfGh\u{200D}1234567")
add("zero-width-nbsp(U+FEFF)", "\u{FEFF}" + validBase)
add("soft-hyphen(U+00AD)", "AbCdEfGh\u{00AD}1234567")
add("nbsp(U+00A0)", "AbCdEfGh\u{00A0}1234567")
add("LTR-override(U+202E)", "AbCdEfGh\u{202E}1234567")
// emoji / Swift 1 CharacterJS 2
add("emoji-single", "AbCdEfGh\u{1F600}1234567")
add("emoji-zwj-family", "AbCdEfG\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}1234567")
add("emoji-flag", "AbCdEfGh\u{1F1E8}\u{1F1F3}123456")
add("emoji-only-16-graphemes", String(repeated("\u{1F600}", count: 16)))
add("emoji-fill-to-512-units", String(repeated("\u{1F600}", count: 256)))
// 西 / homoglyph
add("cyrillic-a(U+0430)", "\u{0430}bCdEfGh01234567")
add("greek-omicron(U+03BF)", "\u{03BF}bCdEfGh01234567")
// ASCII
add("len513-with-fullwidth-tail", String(repeated("a", count: 512)) + "\u{FF21}")
add("len511-plus-emoji", String(repeated("a", count: 511)) + "\u{1F600}")
return out
}()
}
//
struct TokenVerdicts: Equatable {
let sessionCore: Bool
let apiClient: Bool
let hostRegistry: Bool
let server: Bool
/// SessionCore / APIClient / ****
/// HostRegistry trim
/// ""padded
var coreThreeAgree: Bool {
sessionCore == apiClient && apiClient == server
}
var allFourAgree: Bool {
coreThreeAgree && hostRegistry == server
}
var summary: String {
"SessionCore=\(sessionCore) APIClient=\(apiClient) "
+ "HostRegistry=\(hostRegistry) server=\(server)"
}
}
/// `live(server:)`
/// `disagreementLabels` ****
/// Owns
struct TokenPolicySet {
let sessionCore: @Sendable (String) -> Bool
let apiClient: @Sendable (String) -> Bool
let hostRegistry: @Sendable (String) -> Bool
let server: @Sendable (String) -> Bool
/// Swift + src/
static func live(server: @escaping @Sendable (String) -> Bool) -> TokenPolicySet {
TokenPolicySet(
// internal `AuthCookie`SessionCore
// APIClient APIClientTokenPolicy
sessionCore: { SessionCore.AuthCookie.isValidToken($0) },
apiClient: { APIClientTokenPolicy.isWellFormed($0) },
hostRegistry: { AccessToken(rawValue: $0) != nil },
server: server
)
}
func verdicts(for candidate: String) -> TokenVerdicts {
TokenVerdicts(
sessionCore: sessionCore(candidate), apiClient: apiClient(candidate),
hostRegistry: hostRegistry(candidate), server: server(candidate)
)
}
/// ****
func disagreementLabels(over probes: [TokenProbe]) -> [String] {
probes.compactMap { probe in
let verdicts = self.verdicts(for: probe.candidate)
// HostRegistry trim
// trim
let agrees = hasSurroundingWhitespace(probe.candidate)
? verdicts.coreThreeAgree
: verdicts.allFourAgree
return agrees ? nil : "[\(probe.label)] \(verdicts.summary)"
}
}
}
func evaluateTokenPolicies(
_ candidate: String, server: @escaping @Sendable (String) -> Bool
) -> TokenVerdicts {
TokenPolicySet.live(server: server).verdicts(for: candidate)
}
/// HostRegistry trim
func hasSurroundingWhitespace(_ candidate: String) -> Bool {
candidate != candidate.trimmingCharacters(in: .whitespacesAndNewlines)
}
//
@Suite("F3 令牌策略漂移守卫(三份 Swift + 服务端正则)")
struct TokenPolicyDriftTests {
@Test("对抗语料逐条同判:任意两份实现分歧即红")
func allImplementationsAgreeOverAdversarialCorpus() throws {
let serverPredicate = try ServerTokenPolicy.makeEvaluator()
let disagreements = TokenPolicySet.live(server: serverPredicate)
.disagreementLabels(over: TokenCorpus.probes)
#expect(
disagreements.isEmpty,
"""
令牌策略已漂移(\(disagreements.count)/\(TokenCorpus.probes.count) 条样本分歧)。
四份实现必须同判:SessionCore/AuthCookie.swift、HostRegistry/AccessToken.swift、
APIClient/AccessToken.swift、src/config.ts:WEBTERM_TOKEN_RE。
分歧样本(只打标签,不打令牌):
\(disagreements.joined(separator: "\n"))
"""
)
// (""绿)
#expect(TokenCorpus.probes.count >= 50,
"对抗语料被削减到 \(TokenCorpus.probes.count) 条 —— 守卫失去意义")
}
/// ""
/// 使绿
/// SessionCore/HostRegistry/APIClient
/// Owns
@Test("守卫自身是响的:任一份实现哪怕只放宽一个字符,比较逻辑也必须报分歧")
func guardItselfDetectsEachSingleDrift() throws {
let serverPredicate = try ServerTokenPolicy.makeEvaluator()
let live = TokenPolicySet.live(server: serverPredicate)
// `:`**** `[A-Za-z0-9._~+/=-]`
// `_`
//
let drifted: @Sendable (String) -> Bool = { candidate in
let relaxed = candidate.replacingOccurrences(of: ":", with: ".")
return APIClientTokenPolicy.isWellFormed(relaxed)
}
let mutants: [(String, TokenPolicySet)] = [
("SessionCore", TokenPolicySet(
sessionCore: drifted, apiClient: live.apiClient,
hostRegistry: live.hostRegistry, server: live.server)),
("APIClient", TokenPolicySet(
sessionCore: live.sessionCore, apiClient: drifted,
hostRegistry: live.hostRegistry, server: live.server)),
("HostRegistry", TokenPolicySet(
sessionCore: live.sessionCore, apiClient: live.apiClient,
hostRegistry: drifted, server: live.server)),
("server", TokenPolicySet(
sessionCore: live.sessionCore, apiClient: live.apiClient,
hostRegistry: live.hostRegistry, server: drifted)),
]
for (name, mutant) in mutants {
let found = mutant.disagreementLabels(over: TokenCorpus.probes)
#expect(!found.isEmpty, "\(name) 漂移未被守卫抓到 —— 守卫失效(假绿)")
}
// ""
#expect(live.disagreementLabels(over: TokenCorpus.probes).isEmpty)
}
@Test("语料确实两侧都覆盖:既有被四份一致接受的,也有被四份一致拒绝的")
func corpusExercisesBothVerdicts() throws {
let serverPredicate = try ServerTokenPolicy.makeEvaluator()
let verdicts = TokenCorpus.probes.map {
evaluateTokenPolicies($0.candidate, server: serverPredicate).server
}
// "",绿( false ),
// 绿
#expect(verdicts.contains(true), "语料里没有任何被接受的样本 —— 上一条用例会假绿")
#expect(verdicts.contains(false), "语料里没有任何被拒绝的样本 —— 上一条用例会假绿")
}
@Test("服务端正则原文未变:字符集/长度窗口逐字节对钉")
func serverPatternIsStillTheFrozenShape() throws {
let pattern = try ServerTokenPolicy.extractedTokenPatternSource()
// §1.1 Swift
#expect(pattern == "^[A-Za-z0-9._~+/=-]{16,512}$",
"src/config.ts 的 WEBTERM_TOKEN_RE 变了(实测 \(pattern));三份 Swift 实现必须同步更新")
}
@Test("长度窗口 15/16/512/513:四份实现同判且落在 16…512")
func lengthBoundariesAreIdenticalEverywhere() throws {
let serverPredicate = try ServerTokenPolicy.makeEvaluator()
let cases: [(length: Int, expected: Bool)] = [
(15, false), (16, true), (511, true), (512, true), (513, false),
]
for (length, expected) in cases {
let candidate = String(repeating: "a", count: length)
let verdicts = evaluateTokenPolicies(candidate, server: serverPredicate)
#expect(verdicts.allFourAgree, "长度 \(length): \(verdicts.summary)")
#expect(verdicts.server == expected,
"长度 \(length) 应为 \(expected),实测 \(verdicts.summary)")
}
}
@Test("cookie 名三处 + 服务端 AUTH_COOKIE_NAME 全等")
func cookieNameIsPinnedEverywhere() throws {
let serverName = try ServerTokenPolicy.extractedCookieName()
#expect(serverName == "webterm_auth",
"src/http/auth.ts 的 AUTH_COOKIE_NAME 变了(实测 \(serverName))")
#expect(SessionCore.AuthCookie.name == serverName,
"SessionCore.AuthCookie.name=\(SessionCore.AuthCookie.name) ≠ 服务端 \(serverName)")
#expect(APIClientTokenPolicy.cookieName == serverName,
"APIClient.AuthCookie.name=\(APIClientTokenPolicy.cookieName) ≠ 服务端 \(serverName)")
}
@Test("cookie 头拼装形状三处一致:name=<token>,无分号无空格")
func cookieHeaderAssemblyIsIdentical() throws {
let token = TokenCorpus.validBase
let fromSessionCore = SessionCore.AuthCookie.headerValue(token: token)
let fromAPIClient = APIClientTokenPolicy.headerValue(for: token)
#expect(fromSessionCore == "webterm_auth=\(token)")
#expect(fromAPIClient == fromSessionCore,
"两份 cookie 头拼装分歧:APIClient=\(fromAPIClient) SessionCore=\(fromSessionCore ?? "nil")")
// /cookie
#expect(fromAPIClient.contains(";") == false)
#expect(fromAPIClient.contains("\r") == false && fromAPIClient.contains("\n") == false)
}
@Test("已知且有意的分歧:HostRegistry 先 trim 首尾空白,另两份原样拒绝")
func hostRegistryTrimDivergenceIsIntentionalAndBounded() throws {
let serverPredicate = try ServerTokenPolicy.makeEvaluator()
let padded = " \(TokenCorpus.validBase) "
let verdicts = evaluateTokenPolicies(padded, server: serverPredicate)
// , AccessToken ()
// ," trim /"
#expect(verdicts.hostRegistry, "HostRegistry 应接受首尾空白并归一化")
#expect(verdicts.sessionCore == false, "SessionCore 应原样拒绝带空白的串")
#expect(verdicts.apiClient == false, "APIClient 应原样拒绝带空白的串")
#expect(verdicts.server == false, "服务端正则应原样拒绝带空白的串")
// trim ""
let normalized = try #require(AccessToken(rawValue: padded)).rawValue
let afterTrim = evaluateTokenPolicies(normalized, server: serverPredicate)
#expect(afterTrim.allFourAgree && afterTrim.server,
"trim 后的令牌必须四份都接受,实测 \(afterTrim.summary)")
}
}