feat(ios): access-token support + git-panel endpoints across the package layer
APIClient (77 -> 125 tests, coverage 92.22%): POST /auth probe with the four distinct outcomes from the frozen contract, Cookie/Accept landed at the same single header choke point that already enforces Origin-iff-G, plus the whole project-ops surface the server has had since late July and iOS consumed none of: /projects/log, /projects/pr, /projects/worktree/state, git stage/commit/push/ fetch, worktree create/remove/prune, GET /sessions, follow-up queue. HostRegistry (30 -> 73 tests, 88.12% -> 92.49%): per-host token in the Keychain under the existing SecItemShim conventions (device-only, never synchronizable), charset/length validated at the boundary, old token-less records still decode. SessionCore (93 -> 108 tests, 96.74%): the WS upgrade carries the cookie from the same point that writes Origin, and a 401 handshake is a terminal .unauthorized -- never entering the backoff loop, since retrying one wrong shared token is a brute-force generator against the server's 10/min limiter.
This commit is contained in:
@@ -156,6 +156,67 @@ struct ProjectsTests {
|
||||
#expect(projects.last?.sessions.isEmpty == true) // sessions 缺失 → []
|
||||
}
|
||||
|
||||
@Test("回归:lastActiveMs 来自 fs.stat().mtimeMs —— **带小数**必须解出来(否则真机上排序键永远为 nil)")
|
||||
func lastActiveMsDecodesFractionalStatMtime() async throws {
|
||||
// Arrange — 真实服务器值形如 1785390645813.5327(APFS 纳秒精度);
|
||||
// 旧实现 decode(Int.self) 对小数直接失败,被 try? 吞成 nil。
|
||||
let body = """
|
||||
[{"name":"a","path":"/a","isGit":true,"lastActiveMs":1785390645813.5327,\
|
||||
"lastCommitMs":1720000000000,"ahead":2,"behind":0,"sessions":[]}]
|
||||
"""
|
||||
|
||||
// Act
|
||||
let projects = try await fetchProjects(try makeFixture(), body: body)
|
||||
|
||||
// Assert
|
||||
let project = try #require(projects.first)
|
||||
#expect(project.lastActiveMs == 1_785_390_645_813)
|
||||
#expect(project.lastCommitMs == 1_720_000_000_000)
|
||||
#expect(project.ahead == 2)
|
||||
#expect(project.behind == 0)
|
||||
}
|
||||
|
||||
@Test("session ref 的 cwd(w6/G7)可选解码:有则解出,缺失 → nil")
|
||||
func sessionRefDecodesOptionalCwd() async throws {
|
||||
// Arrange
|
||||
let body = """
|
||||
[{"name":"a","path":"/a","isGit":true,"sessions":[\
|
||||
{"id":"\(Self.sessionIdString)","status":"idle","clientCount":0,"createdAt":1,\
|
||||
"exited":false,"cwd":"/a/.claude/worktrees/x"}]}]
|
||||
"""
|
||||
|
||||
// Act
|
||||
let projects = try await fetchProjects(try makeFixture(), body: body)
|
||||
|
||||
// Assert
|
||||
#expect(projects.first?.sessions.first?.cwd == "/a/.claude/worktrees/x")
|
||||
}
|
||||
|
||||
@Test("detail 的 dirtyCount / sync(w6/G1)可选解码,与 worktree/state 用同一 SyncState")
|
||||
func projectDetailDecodesDirtyCountAndSync() async throws {
|
||||
// Arrange
|
||||
let fixture = try makeFixture()
|
||||
let body = """
|
||||
{"name":"a","path":"/a","isGit":true,"dirty":true,"dirtyCount":7,\
|
||||
"sync":{"upstream":"origin/main","ahead":1,"behind":0,"lastFetchMs":1785390645813.5327},\
|
||||
"worktrees":[],"sessions":[],"hasClaudeMd":false}
|
||||
"""
|
||||
await fixture.http.queueSuccess(
|
||||
url: try routeURL("/projects/detail?path=%2Fa"), body: Data(body.utf8)
|
||||
)
|
||||
|
||||
// Act
|
||||
let detail = try await fixture.client.projectDetail(path: "/a")
|
||||
|
||||
// Assert
|
||||
#expect(detail.dirtyCount == 7)
|
||||
let sync = try #require(detail.sync)
|
||||
#expect(sync.upstream == "origin/main")
|
||||
#expect(sync.ahead == 1)
|
||||
#expect(sync.behind == 0)
|
||||
#expect(sync.lastFetchMs != nil)
|
||||
}
|
||||
|
||||
@Test("/projects 非数组 body → invalidResponseBody;非 200 → unexpectedStatus")
|
||||
func projectsRejectsNonArrayBodyAndBadStatus() async throws {
|
||||
// Act + Assert — 非数组
|
||||
|
||||
Reference in New Issue
Block a user