import { DEFAULT_PROVIDER_INTERACTION_MODE, ProjectId, ProviderInstanceId, ThreadId, type OrchestrationReadModel, type OrchestrationThread, } from "@t3tools/contracts"; import * as HashMap from "effect/HashMap"; import { describe, expect, it } from "vite-plus/test"; import { createEmptyCommandReadModel, findProjectById, findThreadById, fromWireReadModel, isThreadDeleted, listThreadsByProjectId, } from "./commandReadModel.ts"; const now = "2026-01-01T00:00:00.000Z"; function makeThread( id: string, projectId: string, overrides?: Partial, ): OrchestrationThread { return { id: ThreadId.make(id), projectId: ProjectId.make(projectId), title: id, modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex", }, interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, runtimeMode: "full-access", branch: null, worktreePath: null, createdAt: now, updatedAt: now, archivedAt: null, latestTurn: null, messages: [], session: null, activities: [], proposedPlans: [], checkpoints: [], deletedAt: null, ...overrides, }; } const wireReadModel: OrchestrationReadModel = { snapshotSequence: 5, updatedAt: now, projects: [ { id: ProjectId.make("project-a"), title: "Project A", workspaceRoot: "/tmp/project-a", defaultModelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex", }, scripts: [], createdAt: now, updatedAt: now, deletedAt: null, }, ], threads: [ makeThread("thread-live", "project-a"), makeThread("thread-archived", "project-a", { archivedAt: now }), makeThread("thread-deleted", "project-a", { deletedAt: now }), makeThread("thread-other", "project-b"), ], }; describe("commandReadModel", () => { it("creates an empty model", () => { const model = createEmptyCommandReadModel(now); expect(model.snapshotSequence).toBe(0); expect(HashMap.size(model.threads)).toBe(0); expect(HashMap.size(model.projects)).toBe(0); expect(model.updatedAt).toBe(now); }); it("seeds from the wire model and drops deleted threads by default", () => { const model = fromWireReadModel(wireReadModel); expect(model.snapshotSequence).toBe(5); // deleted thread is evicted; archived + live + other retained expect(HashMap.size(model.threads)).toBe(3); expect(HashMap.has(model.threads, ThreadId.make("thread-deleted"))).toBe(false); expect(HashMap.has(model.threads, ThreadId.make("thread-archived"))).toBe(true); expect(HashMap.has(model.threads, ThreadId.make("thread-live"))).toBe(true); expect(HashMap.size(model.projects)).toBe(1); // The evicted deleted thread's id is retained so the create-twice invariant // survives a restart. expect(isThreadDeleted(model, ThreadId.make("thread-deleted"))).toBe(true); expect(isThreadDeleted(model, ThreadId.make("thread-live"))).toBe(false); expect(isThreadDeleted(model, ThreadId.make("thread-archived"))).toBe(false); }); it("retains deleted threads when dropDeletedThreads is false", () => { const model = fromWireReadModel(wireReadModel, { dropDeletedThreads: false }); expect(HashMap.size(model.threads)).toBe(4); expect(HashMap.has(model.threads, ThreadId.make("thread-deleted"))).toBe(true); }); it("finds threads and projects by id with O(1) lookups", () => { const model = fromWireReadModel(wireReadModel); expect(findThreadById(model, ThreadId.make("thread-live"))?.projectId).toBe("project-a"); expect(findThreadById(model, ThreadId.make("thread-deleted"))).toBeUndefined(); expect(findThreadById(model, ThreadId.make("missing"))).toBeUndefined(); expect(findProjectById(model, ProjectId.make("project-a"))?.title).toBe("Project A"); expect(findProjectById(model, ProjectId.make("missing"))).toBeUndefined(); }); it("lists threads by project id", () => { const model = fromWireReadModel(wireReadModel); const ids = listThreadsByProjectId(model, ProjectId.make("project-a")) .map((thread) => thread.id) .toSorted(); expect(ids).toEqual([ThreadId.make("thread-archived"), ThreadId.make("thread-live")]); expect(listThreadsByProjectId(model, ProjectId.make("project-b")).map((t) => t.id)).toEqual([ ThreadId.make("thread-other"), ]); }); });