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,155 @@
//
// GitFixture.swift T-iOS-32 ** git **
//
// `POST /projects/worktree` ****
// `<dirname(repo)>/<basename(repo)>-worktrees/`
// src/http/worktrees.ts:146-160,224-256
// = / repo ****
// worktree `destroy()`
//
// worktreeBase
// <root>/repo HEAD
// <root>/repo-worktrees/<sanitized> worktree
//
// **git ** `worktree list --porcelain`
// HTTP "" ""
import Darwin
import Foundation
/// `git worktree list --porcelain`
struct RegisteredWorktree: Sendable, Equatable {
let path: String
let branch: String?
/// git ""`prunable <reason>`
let prunable: Bool
}
/// git 退 `HarnessError.setup`
/// ****""
@discardableResult
func runGit(_ arguments: [String], in directory: URL) throws -> String {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
process.arguments = arguments
process.currentDirectoryURL = directory
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
var environment = ProcessInfo.processInfo.environment
environment["GIT_TERMINAL_PROMPT"] = "0" //
process.environment = environment
try process.run()
// EOF wait
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
let text = String(decoding: data, as: UTF8.self)
guard process.terminationStatus == 0 else {
throw HarnessError.setup(
"git \(arguments.joined(separator: " ")) 退出码 \(process.terminationStatus): \(text)")
}
return text
}
/// git + `destroy()` `defer`
struct GitFixture: Sendable {
/// ** symlink**macOS $TMPDIR /var/ /private/var/
/// realpath
let root: URL
/// `<root>/repo`
let repo: URL
/// `worktreeRoot` worktree
/// `<dirname(repo)>/<basename(repo)>-worktrees`src/http/worktrees.ts:151-152
var worktreeBase: URL { root.appending(path: "\(repo.lastPathComponent)-worktrees") }
/// `-c` git gpg
private static let identity = [
"-c", "user.email=fixture@example.invalid",
"-c", "user.name=WebTerm Fixture",
"-c", "commit.gpgsign=false",
]
/// + `git worktree add` unborn HEAD
static func make() throws -> GitFixture {
let manager = FileManager.default
let raw = manager.temporaryDirectory
.appending(path: "webterm-worktree-e2e-\(UUID().uuidString)")
try manager.createDirectory(at: raw, withIntermediateDirectories: true)
// macOS $TMPDIR /var/ /private/var/
// worktree git
let root = URL(fileURLWithPath: canonicalPath(raw.path), isDirectory: true)
let repo = root.appending(path: "repo")
try manager.createDirectory(at: repo, withIntermediateDirectories: true)
try runGit(["-c", "init.defaultBranch=main", "init", "-q"], in: repo)
try runGit(identity + ["commit", "-q", "--allow-empty", "-m", "fixture"], in: repo)
return GitFixture(root: root, repo: repo)
}
/// repo worktree `root`
///
func destroy() {
try? FileManager.default.removeItem(at: root)
}
// git
/// `git worktree list --porcelain` worktree
func registeredWorktrees() throws -> [RegisteredWorktree] {
let porcelain = try runGit(["worktree", "list", "--porcelain"], in: repo)
var out: [RegisteredWorktree] = []
var path: String?
var branch: String?
var prunable = false
func flush() {
guard let path else { return }
out.append(RegisteredWorktree(path: path, branch: branch, prunable: prunable))
(branch, prunable) = (nil, false)
}
for rawLine in porcelain.split(separator: "\n", omittingEmptySubsequences: false) {
let line = String(rawLine)
if line.isEmpty {
flush()
path = nil
continue
}
if line.hasPrefix("worktree ") {
path = String(line.dropFirst("worktree ".count))
} else if line.hasPrefix("branch refs/heads/") {
branch = String(line.dropFirst("branch refs/heads/".count))
} else if line == "prunable" || line.hasPrefix("prunable ") {
prunable = true
}
}
flush()
return out
}
/// symlink
func registeredWorktree(atCanonicalPath target: String) throws -> RegisteredWorktree? {
let wanted = canonicalPath(target)
return try registeredWorktrees().first { canonicalPath($0.path) == wanted }
}
/// `.git/worktrees/` prune
func administrativeEntries() -> [String] {
let dir = repo.appending(path: ".git/worktrees")
let names = try? FileManager.default.contentsOfDirectory(atPath: dir.path)
return (names ?? []).sorted()
}
}
/// = `realpath(3)`****
///
/// `URL.resolvingSymlinksInPath()` RED
/// /private **** /private
/// `/var/folders/` git `worktree list`
/// `/private/var/folders/`****
/// `/private` ""
/// `realpath(3)`
/// ""
func canonicalPath(_ path: String) -> String {
guard let resolved = realpath(path, nil) else { return path }
defer { free(resolved) }
return String(cString: resolved)
}