Watch: waiting-count complication and multi-select questions

A WidgetKit extension (accessoryCircular, Rectangular, Inline, Corner) shows
GET /v1/summary. The device token moves to the App Group's Keychain access
group on launch so the extension can read it without re-pairing; the app
reloads the timeline when its feed counts change and on notifications.
Multi-select questions toggle options and send an array of values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
gibandClaude Opus 5.5 committed 2026-11-01 11:06:38 -05:00
1 parent d4d67295c6
commit 0994fd7d5a
18 files changed
+800 -23

No files matched your search

+67 -1
View File
@@ -69,6 +69,33 @@ private let feedItemJSON = """
#expect(request.quickOptions == nil)
#expect(request.summary == "Hash the codes?")
#expect(request.questions.first?.allowCustomAnswer == true)
// A gateway older than multi-select leaves the flag out, which means single-select.
#expect(request.questions.first?.multiSelect == false)
}
@Test func multiSelectQuestion() throws {
let question = try decode(PendingRequest.Question.self, """
{ "id": "q1", "header": "Checks", "question": "Which checks?",
"options": [{ "label": "Unit", "value": "unit" }, { "label": "Lint", "value": "lint" }],
"allowCustomAnswer": false, "multiSelect": true }
""")
#expect(question.multiSelect)
#expect(question.options.map(\.value) == ["unit", "lint"])
}
@Test func summaryFromDocs() throws {
let summary = try decode(Summary.self, """
{ "needsYou": 1, "working": 3, "hostsOffline": 0,
"next": { "hostId": "h", "threadId": "t", "title": "Receipt OCR", "hostLabel": "Mac mini" } }
""")
#expect(summary.needsYou == 1)
#expect(summary.working == 3)
#expect(summary.next?.title == "Receipt OCR")
#expect(summary.next?.route == .thread(hostId: "h", threadId: "t"))
let idle = try decode(Summary.self, #"{ "needsYou": 0, "working": 0, "hostsOffline": 2, "next": null }"#)
#expect(idle.next == nil)
#expect(idle.hostsOffline == 2)
}
@Test func unknownEnumValuesStillDecode() throws {
@@ -127,11 +154,19 @@ private let feedItemJSON = """
}
@Test func respondWithAnswers() throws {
let object = try encodeObject(RespondRequest.answers(requestId: "r1", answers: ["q1": "yes"]))
let object = try encodeObject(RespondRequest.answers(requestId: "r1", answers: ["q1": .text("yes")]))
#expect(object["answers"] as? [String: String] == ["q1": "yes"])
#expect(object["decision"] == nil)
}
@Test func respondWithMultiSelectAnswers() throws {
let object = try encodeObject(RespondRequest.answers(
requestId: "r1", answers: ["checks": .values(["unit", "lint"]), "note": .text("after lunch")]))
let answers = try #require(object["answers"] as? [String: Any])
#expect(answers["checks"] as? [String] == ["unit", "lint"])
#expect(answers["note"] as? String == "after lunch")
}
@Test func pushRegistration() throws {
let object = try encodeObject(PushRegistration(apnsToken: "ab", apnsEnvironment: .sandbox))
#expect(object["apnsEnvironment"] as? String == "sandbox")
@@ -185,6 +220,25 @@ private let feedItemJSON = """
== ProjectRef(hostId: "on", projectId: "a"))
}
@Test func summaryFromFeed() throws {
let feed = DemoFixtures.previewFeed()
let summary = Summary(feed)
#expect(summary.needsYou == feed.needsYou.count)
#expect(summary.working == feed.working.count)
#expect(summary.hostsOffline == 1)
#expect(summary.next?.threadId == feed.needsYou.first?.threadId)
#expect(Summary(Feed(needsYou: [], working: [], recent: [], hosts: [])).next == nil)
}
@Test func routeURLs() {
let thread = Route.thread(hostId: "mac mini", threadId: "t/1")
#expect(thread.url.absoluteString == "wristcode://thread/mac%20mini/t%2F1")
#expect(Route(url: thread.url) == thread)
#expect(Route(url: Route.newThread.url) == .newThread)
#expect(Route(url: URL(string: "wristcode://thread/only-host")!) == nil)
#expect(Route(url: URL(string: "https://thread/h/t")!) == nil)
}
@Test func notificationCommands() {
let info: [AnyHashable: Any] = ["hostId": "h", "threadId": "t", "requestId": "r"]
#expect(NotificationCommand(actionIdentifier: "allow", userInfo: info, text: nil)
@@ -223,4 +277,16 @@ private let feedItemJSON = """
request: .decision(requestId: item.request!.id, decision: .accept))
}
}
@Test func multiSelectAnswerMovesThreadToWorking() async throws {
let gateway = DemoGateway()
let item = try #require(try await gateway.feed().needsYou.first { $0.request?.questions.first?.multiSelect == true })
let request = try #require(item.request)
let question = try #require(request.questions.first)
try await gateway.respond(
hostId: item.hostId, threadId: item.threadId,
request: .answers(requestId: request.id, answers: [question.id: .values(["unit", "lint"])]))
#expect(try await gateway.summary().working == 3)
#expect(try await gateway.feed().working.contains { $0.id == item.id })
}
}