Files
web-terminal/ios/IntegrationTests/GitFixture.swift
Yaojia Wang ddab77b337 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.
2026-07-30 16:58:22 +02:00

156 lines
7.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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)
}