| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | import { ProjectId, ThreadId, TurnId } from "@t3tools/contracts"; | 1 | import { ProjectId, ThreadId, TurnId, type OrchestrationReadModel } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it } from "vitest"; | 2 | import { describe, expect, it } from "vitest"; |
| 3 | | 3 | |
| 4 | import { markThreadUnread, type AppState } from "./store"; | 4 | import { markThreadUnread, syncServerReadModel, type AppState } from "./store"; |
| 5 | import type { Thread } from "./types"; | 5 | import type { Thread } from "./types"; |
| 6 | | 6 | |
| 7 | function makeThread(overrides: Partial<Thread> = {}): Thread { | 7 | function makeThread(overrides: Partial<Thread> = {}): Thread { |
| ... | @@ -42,6 +42,46 @@ function makeState(thread: Thread): AppState { | ... | @@ -42,6 +42,46 @@ function makeState(thread: Thread): AppState { |
| 42 | }; | 42 | }; |
| 43 | } | 43 | } |
| 44 | | 44 | |
| | 45 | function makeReadModelThread(overrides: Partial<OrchestrationReadModel["threads"][number]>) { |
| | 46 | return { |
| | 47 | id: ThreadId.makeUnsafe("thread-1"), |
| | 48 | projectId: ProjectId.makeUnsafe("project-1"), |
| | 49 | title: "Thread", |
| | 50 | model: "gpt-5.3-codex", |
| | 51 | branch: null, |
| | 52 | worktreePath: null, |
| | 53 | latestTurn: null, |
| | 54 | createdAt: "2026-02-27T00:00:00.000Z", |
| | 55 | updatedAt: "2026-02-27T00:00:00.000Z", |
| | 56 | deletedAt: null, |
| | 57 | messages: [], |
| | 58 | activities: [], |
| | 59 | checkpoints: [], |
| | 60 | session: null, |
| | 61 | ...overrides, |
| | 62 | } satisfies OrchestrationReadModel["threads"][number]; |
| | 63 | } |
| | 64 | |
| | 65 | function makeReadModel(thread: OrchestrationReadModel["threads"][number]): OrchestrationReadModel { |
| | 66 | return { |
| | 67 | snapshotSequence: 1, |
| | 68 | updatedAt: "2026-02-27T00:00:00.000Z", |
| | 69 | projects: [ |
| | 70 | { |
| | 71 | id: ProjectId.makeUnsafe("project-1"), |
| | 72 | title: "Project", |
| | 73 | workspaceRoot: "/tmp/project", |
| | 74 | defaultModel: "gpt-5.3-codex", |
| | 75 | createdAt: "2026-02-27T00:00:00.000Z", |
| | 76 | updatedAt: "2026-02-27T00:00:00.000Z", |
| | 77 | deletedAt: null, |
| | 78 | scripts: [], |
| | 79 | }, |
| | 80 | ], |
| | 81 | threads: [thread], |
| | 82 | }; |
| | 83 | } |
| | 84 | |
| 45 | describe("store pure functions", () => { | 85 | describe("store pure functions", () => { |
| 46 | it("markThreadUnread moves lastVisitedAt before completion for a completed thread", () => { | 86 | it("markThreadUnread moves lastVisitedAt before completion for a completed thread", () => { |
| 47 | const latestTurnCompletedAt = "2026-02-25T12:30:00.000Z"; | 87 | const latestTurnCompletedAt = "2026-02-25T12:30:00.000Z"; |
| ... | @@ -82,3 +122,43 @@ describe("store pure functions", () => { | ... | @@ -82,3 +122,43 @@ describe("store pure functions", () => { |
| 82 | expect(next).toEqual(initialState); | 122 | expect(next).toEqual(initialState); |
| 83 | }); | 123 | }); |
| 84 | }); | 124 | }); |
| | 125 | |
| | 126 | describe("store read model sync", () => { |
| | 127 | it("preserves claude model slugs without an active session", () => { |
| | 128 | const initialState = makeState(makeThread()); |
| | 129 | const readModel = makeReadModel( |
| | 130 | makeReadModelThread({ |
| | 131 | model: "claude-opus-4-6", |
| | 132 | }), |
| | 133 | ); |
| | 134 | |
| | 135 | const next = syncServerReadModel(initialState, readModel); |
| | 136 | |
| | 137 | expect(next.threads[0]?.model).toBe("claude-opus-4-6"); |
| | 138 | }); |
| | 139 | |
| | 140 | it("resolves claude aliases when session provider is claudeCode", () => { |
| | 141 | const initialState = makeState(makeThread()); |
| | 142 | const readModel = makeReadModel( |
| | 143 | makeReadModelThread({ |
| | 144 | model: "sonnet", |
| | 145 | session: { |
| | 146 | threadId: ThreadId.makeUnsafe("thread-1"), |
| | 147 | status: "ready", |
| | 148 | providerName: "claudeCode", |
| | 149 | providerSessionId: null, |
| | 150 | providerThreadId: null, |
| | 151 | approvalPolicy: "on-request", |
| | 152 | sandboxMode: "workspace-write", |
| | 153 | activeTurnId: null, |
| | 154 | lastError: null, |
| | 155 | updatedAt: "2026-02-27T00:00:00.000Z", |
| | 156 | }, |
| | 157 | }), |
| | 158 | ); |
| | 159 | |
| | 160 | const next = syncServerReadModel(initialState, readModel); |
| | 161 | |
| | 162 | expect(next.threads[0]?.model).toBe("claude-sonnet-4-6"); |
| | 163 | }); |
| | 164 | }); |