816 lines · 23.8 KB · TypeScript
RawDownload
import {  CheckpointRef,  DEFAULT_MODEL_BY_PROVIDER,  EventId,  MessageId,  ProjectId,  ThreadId,  TurnId,  type OrchestrationEvent,  type OrchestrationReadModel,} from "@t3tools/contracts";import { describe, expect, it } from "vitest"; import {  applyOrchestrationEvent,  applyOrchestrationEvents,  syncServerReadModel,  type AppState,} from "./store";import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type Thread } from "./types"; function makeThread(overrides: Partial<Thread> = {}): Thread {  return {    id: ThreadId.makeUnsafe("thread-1"),    codexThreadId: null,    projectId: ProjectId.makeUnsafe("project-1"),    title: "Thread",    modelSelection: {      provider: "codex",      model: "gpt-5-codex",    },    runtimeMode: DEFAULT_RUNTIME_MODE,    interactionMode: DEFAULT_INTERACTION_MODE,    session: null,    messages: [],    turnDiffSummaries: [],    activities: [],    proposedPlans: [],    error: null,    createdAt: "2026-02-13T00:00:00.000Z",    archivedAt: null,    latestTurn: null,    branch: null,    worktreePath: null,    ...overrides,  };} function makeState(thread: Thread): AppState {  const threadIdsByProjectId: AppState["threadIdsByProjectId"] = {    [thread.projectId]: [thread.id],  };  return {    projects: [      {        id: ProjectId.makeUnsafe("project-1"),        name: "Project",        cwd: "/tmp/project",        defaultModelSelection: {          provider: "codex",          model: "gpt-5-codex",        },        scripts: [],      },    ],    threads: [thread],    sidebarThreadsById: {},    threadIdsByProjectId,    bootstrapComplete: true,  };} function makeEvent<T extends OrchestrationEvent["type"]>(  type: T,  payload: Extract<OrchestrationEvent, { type: T }>["payload"],  overrides: Partial<Extract<OrchestrationEvent, { type: T }>> = {},): Extract<OrchestrationEvent, { type: T }> {  const sequence = overrides.sequence ?? 1;  return {    sequence,    eventId: EventId.makeUnsafe(`event-${sequence}`),    aggregateKind: "thread",    aggregateId:      "threadId" in payload        ? payload.threadId        : "projectId" in payload          ? payload.projectId          : ProjectId.makeUnsafe("project-1"),    occurredAt: "2026-02-27T00:00:00.000Z",    commandId: null,    causationEventId: null,    correlationId: null,    metadata: {},    type,    payload,    ...overrides,  } as Extract<OrchestrationEvent, { type: T }>;} function makeReadModelThread(overrides: Partial<OrchestrationReadModel["threads"][number]>) {  return {    id: ThreadId.makeUnsafe("thread-1"),    projectId: ProjectId.makeUnsafe("project-1"),    title: "Thread",    modelSelection: {      provider: "codex",      model: "gpt-5.3-codex",    },    runtimeMode: DEFAULT_RUNTIME_MODE,    interactionMode: DEFAULT_INTERACTION_MODE,    branch: null,    worktreePath: null,    latestTurn: null,    createdAt: "2026-02-27T00:00:00.000Z",    updatedAt: "2026-02-27T00:00:00.000Z",    archivedAt: null,    deletedAt: null,    messages: [],    activities: [],    proposedPlans: [],    checkpoints: [],    session: null,    ...overrides,  } satisfies OrchestrationReadModel["threads"][number];} function makeReadModel(thread: OrchestrationReadModel["threads"][number]): OrchestrationReadModel {  return {    snapshotSequence: 1,    updatedAt: "2026-02-27T00:00:00.000Z",    projects: [      {        id: ProjectId.makeUnsafe("project-1"),        title: "Project",        workspaceRoot: "/tmp/project",        defaultModelSelection: {          provider: "codex",          model: "gpt-5.3-codex",        },        createdAt: "2026-02-27T00:00:00.000Z",        updatedAt: "2026-02-27T00:00:00.000Z",        deletedAt: null,        scripts: [],      },    ],    threads: [thread],  };} function makeReadModelProject(  overrides: Partial<OrchestrationReadModel["projects"][number]>,): OrchestrationReadModel["projects"][number] {  return {    id: ProjectId.makeUnsafe("project-1"),    title: "Project",    workspaceRoot: "/tmp/project",    defaultModelSelection: {      provider: "codex",      model: "gpt-5.3-codex",    },    createdAt: "2026-02-27T00:00:00.000Z",    updatedAt: "2026-02-27T00:00:00.000Z",    deletedAt: null,    scripts: [],    ...overrides,  };} describe("store read model sync", () => {  it("marks bootstrap complete after snapshot sync", () => {    const initialState: AppState = {      ...makeState(makeThread()),      bootstrapComplete: false,    };     const next = syncServerReadModel(initialState, makeReadModel(makeReadModelThread({})));     expect(next.bootstrapComplete).toBe(true);  });   it("preserves claude model slugs without an active session", () => {    const initialState = makeState(makeThread());    const readModel = makeReadModel(      makeReadModelThread({        modelSelection: {          provider: "claudeAgent",          model: "claude-opus-4-6",        },      }),    );     const next = syncServerReadModel(initialState, readModel);     expect(next.threads[0]?.modelSelection.model).toBe("claude-opus-4-6");  });   it("resolves claude aliases when session provider is claudeAgent", () => {    const initialState = makeState(makeThread());    const readModel = makeReadModel(      makeReadModelThread({        modelSelection: {          provider: "claudeAgent",          model: "sonnet",        },        session: {          threadId: ThreadId.makeUnsafe("thread-1"),          status: "ready",          providerName: "claudeAgent",          runtimeMode: "approval-required",          activeTurnId: null,          lastError: null,          updatedAt: "2026-02-27T00:00:00.000Z",        },      }),    );     const next = syncServerReadModel(initialState, readModel);     expect(next.threads[0]?.modelSelection.model).toBe("claude-sonnet-4-6");  });   it("preserves project and thread updatedAt timestamps from the read model", () => {    const initialState = makeState(makeThread());    const readModel = makeReadModel(      makeReadModelThread({        updatedAt: "2026-02-27T00:05:00.000Z",      }),    );     const next = syncServerReadModel(initialState, readModel);     expect(next.projects[0]?.updatedAt).toBe("2026-02-27T00:00:00.000Z");    expect(next.threads[0]?.updatedAt).toBe("2026-02-27T00:05:00.000Z");  });   it("maps archivedAt from the read model", () => {    const initialState = makeState(makeThread());    const archivedAt = "2026-02-28T00:00:00.000Z";    const next = syncServerReadModel(      initialState,      makeReadModel(        makeReadModelThread({          archivedAt,        }),      ),    );     expect(next.threads[0]?.archivedAt).toBe(archivedAt);  });   it("replaces projects using snapshot order during recovery", () => {    const project1 = ProjectId.makeUnsafe("project-1");    const project2 = ProjectId.makeUnsafe("project-2");    const project3 = ProjectId.makeUnsafe("project-3");    const initialState: AppState = {      projects: [        {          id: project2,          name: "Project 2",          cwd: "/tmp/project-2",          defaultModelSelection: {            provider: "codex",            model: DEFAULT_MODEL_BY_PROVIDER.codex,          },          scripts: [],        },        {          id: project1,          name: "Project 1",          cwd: "/tmp/project-1",          defaultModelSelection: {            provider: "codex",            model: DEFAULT_MODEL_BY_PROVIDER.codex,          },          scripts: [],        },      ],      threads: [],      sidebarThreadsById: {},      threadIdsByProjectId: {},      bootstrapComplete: true,    };    const readModel: OrchestrationReadModel = {      snapshotSequence: 2,      updatedAt: "2026-02-27T00:00:00.000Z",      projects: [        makeReadModelProject({          id: project1,          title: "Project 1",          workspaceRoot: "/tmp/project-1",        }),        makeReadModelProject({          id: project2,          title: "Project 2",          workspaceRoot: "/tmp/project-2",        }),        makeReadModelProject({          id: project3,          title: "Project 3",          workspaceRoot: "/tmp/project-3",        }),      ],      threads: [],    };     const next = syncServerReadModel(initialState, readModel);     expect(next.projects.map((project) => project.id)).toEqual([project1, project2, project3]);  });}); describe("incremental orchestration updates", () => {  it("does not mark bootstrap complete for incremental events", () => {    const state: AppState = {      ...makeState(makeThread()),      bootstrapComplete: false,    };     const next = applyOrchestrationEvent(      state,      makeEvent("thread.meta-updated", {        threadId: ThreadId.makeUnsafe("thread-1"),        title: "Updated title",        updatedAt: "2026-02-27T00:00:01.000Z",      }),    );     expect(next.bootstrapComplete).toBe(false);  });   it("preserves state identity for no-op project and thread deletes", () => {    const thread = makeThread();    const state = makeState(thread);     const nextAfterProjectDelete = applyOrchestrationEvent(      state,      makeEvent("project.deleted", {        projectId: ProjectId.makeUnsafe("project-missing"),        deletedAt: "2026-02-27T00:00:01.000Z",      }),    );    const nextAfterThreadDelete = applyOrchestrationEvent(      state,      makeEvent("thread.deleted", {        threadId: ThreadId.makeUnsafe("thread-missing"),        deletedAt: "2026-02-27T00:00:01.000Z",      }),    );     expect(nextAfterProjectDelete).toBe(state);    expect(nextAfterThreadDelete).toBe(state);  });   it("reuses an existing project row when project.created arrives with a new id for the same cwd", () => {    const originalProjectId = ProjectId.makeUnsafe("project-1");    const recreatedProjectId = ProjectId.makeUnsafe("project-2");    const state: AppState = {      projects: [        {          id: originalProjectId,          name: "Project",          cwd: "/tmp/project",          defaultModelSelection: {            provider: "codex",            model: DEFAULT_MODEL_BY_PROVIDER.codex,          },          scripts: [],        },      ],      threads: [],      sidebarThreadsById: {},      threadIdsByProjectId: {},      bootstrapComplete: true,    };     const next = applyOrchestrationEvent(      state,      makeEvent("project.created", {        projectId: recreatedProjectId,        title: "Project Recreated",        workspaceRoot: "/tmp/project",        defaultModelSelection: {          provider: "codex",          model: DEFAULT_MODEL_BY_PROVIDER.codex,        },        scripts: [],        createdAt: "2026-02-27T00:00:01.000Z",        updatedAt: "2026-02-27T00:00:01.000Z",      }),    );     expect(next.projects).toHaveLength(1);    expect(next.projects[0]?.id).toBe(recreatedProjectId);    expect(next.projects[0]?.cwd).toBe("/tmp/project");    expect(next.projects[0]?.name).toBe("Project Recreated");  });   it("removes stale project index entries when thread.created recreates a thread under a new project", () => {    const originalProjectId = ProjectId.makeUnsafe("project-1");    const recreatedProjectId = ProjectId.makeUnsafe("project-2");    const threadId = ThreadId.makeUnsafe("thread-1");    const thread = makeThread({      id: threadId,      projectId: originalProjectId,    });    const state: AppState = {      projects: [        {          id: originalProjectId,          name: "Project 1",          cwd: "/tmp/project-1",          defaultModelSelection: {            provider: "codex",            model: DEFAULT_MODEL_BY_PROVIDER.codex,          },          scripts: [],        },        {          id: recreatedProjectId,          name: "Project 2",          cwd: "/tmp/project-2",          defaultModelSelection: {            provider: "codex",            model: DEFAULT_MODEL_BY_PROVIDER.codex,          },          scripts: [],        },      ],      threads: [thread],      sidebarThreadsById: {},      threadIdsByProjectId: {        [originalProjectId]: [threadId],      },      bootstrapComplete: true,    };     const next = applyOrchestrationEvent(      state,      makeEvent("thread.created", {        threadId,        projectId: recreatedProjectId,        title: "Recovered thread",        modelSelection: {          provider: "codex",          model: DEFAULT_MODEL_BY_PROVIDER.codex,        },        runtimeMode: DEFAULT_RUNTIME_MODE,        interactionMode: DEFAULT_INTERACTION_MODE,        branch: null,        worktreePath: null,        createdAt: "2026-02-27T00:00:01.000Z",        updatedAt: "2026-02-27T00:00:01.000Z",      }),    );     expect(next.threads).toHaveLength(1);    expect(next.threads[0]?.projectId).toBe(recreatedProjectId);    expect(next.threadIdsByProjectId[originalProjectId]).toBeUndefined();    expect(next.threadIdsByProjectId[recreatedProjectId]).toEqual([threadId]);  });   it("updates only the affected thread for message events", () => {    const thread1 = makeThread({      id: ThreadId.makeUnsafe("thread-1"),      messages: [        {          id: MessageId.makeUnsafe("message-1"),          role: "assistant",          text: "hello",          turnId: TurnId.makeUnsafe("turn-1"),          createdAt: "2026-02-27T00:00:00.000Z",          completedAt: "2026-02-27T00:00:00.000Z",          streaming: false,        },      ],    });    const thread2 = makeThread({ id: ThreadId.makeUnsafe("thread-2") });    const state: AppState = {      ...makeState(thread1),      threads: [thread1, thread2],    };     const next = applyOrchestrationEvent(      state,      makeEvent("thread.message-sent", {        threadId: thread1.id,        messageId: MessageId.makeUnsafe("message-1"),        role: "assistant",        text: " world",        turnId: TurnId.makeUnsafe("turn-1"),        streaming: true,        createdAt: "2026-02-27T00:00:01.000Z",        updatedAt: "2026-02-27T00:00:01.000Z",      }),    );     expect(next.threads[0]?.messages[0]?.text).toBe("hello world");    expect(next.threads[0]?.latestTurn?.state).toBe("running");    expect(next.threads[1]).toBe(thread2);  });

Lines 1–500 of 816.