Watch: notification switches, a model list and Ask before acting on new threads, and Ask before acting on a thread

This commit is contained in:
gib committed 2026-11-02 12:10:48 -05:00
1 parent 3cc7e40852
commit 6bc9cac6ca
14 files changed
+623 -81

No files matched your search

+61
View File
@@ -155,6 +155,8 @@ private let feedItemJSON = """
"canStop": true }
""")
#expect(detail.canStop)
// An older gateway leaves out askFirst, so the switch stays hidden.
#expect(detail.askFirst == nil)
#expect(detail.messages.first?.role == .assistant)
#expect(detail.messages.first?.createdAt == Date(timeIntervalSince1970: 1_793_526_000))
}
@@ -168,6 +170,35 @@ private let feedItemJSON = """
let host = try #require(response.hosts.first)
#expect(host.projects.count == 2)
#expect(host.launchable.map(\.id) == ["a"])
#expect(host.providers.isEmpty)
#expect(host.projects.first?.defaultModel == nil)
}
@Test func phase2FromDocs() throws {
let projects = try decode(ProjectsResponse.self, """
{ "hosts": [{ "id": "...", "label": "Mac mini", "status": "online", "error": null,
"projects": [{ "id": "a", "title": "wristcode", "model": "claude-opus-5-5", "instanceId": "claudeAgent" }],
"providers": [{ "instanceId": "claudeAgent", "name": "Claude", "canAskFirst": true,
"models": [{ "slug": "claude-opus-5-5", "name": "Claude Opus 5.5" }] }] }] }
""")
let host = try #require(projects.hosts.first)
#expect(host.projects.first?.defaultModel == ModelRef(instanceId: "claudeAgent", slug: "claude-opus-5-5"))
#expect(host.offers(ModelRef(instanceId: "claudeAgent", slug: "claude-opus-5-5")))
#expect(!host.offers(ModelRef(instanceId: "codex", slug: "claude-opus-5-5")))
let detail = try decode(ThreadDetail.self, #"{ "item": \#(feedItemJSON), "messages": [], "canStop": true, "askFirst": false }"#)
#expect(detail.askFirst == false)
let prefs = try decode(NotificationPrefs.self, #"{ "needsMe": true, "finished": true, "failed": true, "subagents": false }"#)
#expect(NotificationPrefs.Key.allCases.map { prefs[$0] } == [true, true, true, false])
let request = try encodeObject(NewThreadRequest(
hostId: "h", projectId: "p", text: "t",
model: ModelRef(instanceId: "claudeAgent", slug: "claude-sonnet-5-5"), askFirst: true))
#expect((request["model"] as? [String: String]) == ["instanceId": "claudeAgent", "slug": "claude-sonnet-5-5"])
#expect(request["askFirst"] as? Bool == true)
let plain = try encodeObject(NewThreadRequest(hostId: "h", projectId: "p", text: "t"))
#expect(plain["model"] == nil && plain["askFirst"] == nil)
}
@Test func smallResponses() throws {
@@ -285,6 +316,36 @@ private let feedItemJSON = """
== ProjectRef(hostId: "on", projectId: "a"))
}
@Test func newThreadAgentAndOptions() {
let claude = AgentProvider(
instanceId: "claudeAgent", name: "Claude", canAskFirst: true,
models: [.init(slug: "opus", name: "Claude Opus"), .init(slug: "sonnet", name: "Claude Sonnet")])
let other = AgentProvider(instanceId: "other", name: "Other", canAskFirst: false, models: [.init(slug: "o1", name: "O1")])
let project = ProjectsResponse.Project(id: "p", title: "p", model: "opus", instanceId: "claudeAgent")
let host = ProjectsResponse.Host(id: "h", label: "H", status: .online, error: nil, projects: [project], providers: [claude, other])
let sonnet = ModelRef(instanceId: "claudeAgent", slug: "sonnet")
// The last pick wins while the host still offers it, else the project's default.
#expect(NewThreadView.agent(on: host, project: project, last: sonnet) == sonnet)
#expect(NewThreadView.agent(on: host, project: project, last: ModelRef(instanceId: "claudeAgent", slug: "gone"))
== project.defaultModel)
#expect(NewThreadView.agent(on: host, project: project, last: nil) == project.defaultModel)
// The default model is left out so it keeps its options. A provider that cannot ask sends false.
let byDefault = NewThreadView.options(project: project, agent: project.defaultModel, provider: claude, askFirst: true)
#expect(byDefault.model == nil && byDefault.askFirst == true)
let picked = NewThreadView.options(project: project, agent: sonnet, provider: claude, askFirst: false)
#expect(picked.model == sonnet && picked.askFirst == false)
let o1 = ModelRef(instanceId: "other", slug: "o1")
#expect(NewThreadView.options(project: project, agent: o1, provider: other, askFirst: true).askFirst == false)
// An older gateway lists no providers, so neither is sent.
let old = NewThreadView.options(project: project, agent: nil, provider: nil, askFirst: true)
#expect(old.model == nil && old.askFirst == nil)
#expect(AgentList.shortName("Claude Opus 5.5", provider: "Claude") == "Opus 5.5")
#expect(AgentList.shortName("GPT-5.5", provider: "Codex") == "GPT-5.5")
}
@Test func summaryFromFeed() throws {
let feed = DemoFixtures.previewFeed()
let summary = Summary(feed)