| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | | import { ProjectId, ThreadId, TurnId } from "@t3tools/contracts"; |
| 1 | import { ProjectId, ThreadId, TurnId, type OrchestrationReadModel } from "@t3tools/contracts"; |
| 2 | 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 | 5 | import type { Thread } from "./types"; |
| 6 | 6 | |
| 7 | 7 | function makeThread(overrides: Partial<Thread> = {}): Thread { |
| ... | ... | @@ -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 | 85 | describe("store pure functions", () => { |
| 46 | 86 | it("markThreadUnread moves lastVisitedAt before completion for a completed thread", () => { |
| 47 | 87 | const latestTurnCompletedAt = "2026-02-25T12:30:00.000Z"; |
| ... | ... | @@ -82,3 +122,43 @@ describe("store pure functions", () => { |
| 82 | 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 | }); |