1117 lines · 34.4 KB · TypeScript
RawDownload
import { scopeThreadRef } from "@t3tools/client-runtime";import {  CheckpointRef,  DEFAULT_MODEL_BY_PROVIDER,  EnvironmentId,  EventId,  MessageId,  ProjectId,  ThreadId,  TurnId,  type OrchestrationEvent,  type OrchestrationReadModel,} from "@t3tools/contracts";import { describe, expect, it } from "vitest"; import {  applyOrchestrationEvent,  applyOrchestrationEvents,  selectEnvironmentState,  selectProjectsAcrossEnvironments,  setThreadBranch,  selectThreadsAcrossEnvironments,  syncServerReadModel,  type AppState,  type EnvironmentState,} from "./store";import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type Thread } from "./types"; const localEnvironmentId = EnvironmentId.makeUnsafe("environment-local");const remoteEnvironmentId = EnvironmentId.makeUnsafe("environment-remote"); function withActiveEnvironmentState(  environmentState: EnvironmentState,  overrides: Partial<AppState & EnvironmentState> = {},): AppState {  const {    activeEnvironmentId: overrideActiveEnvironmentId,    environmentStateById: overrideEnvironmentStateById,    ...environmentOverrides  } = overrides;  const activeEnvironmentId = overrideActiveEnvironmentId ?? localEnvironmentId;  const mergedEnvironmentState = {    ...environmentState,    ...environmentOverrides,  };  const environmentStateById =    overrideEnvironmentStateById ??    (activeEnvironmentId      ? {          [activeEnvironmentId]: mergedEnvironmentState,        }      : {});   return {    activeEnvironmentId,    environmentStateById,  };} function makeThread(overrides: Partial<Thread> = {}): Thread {  return {    id: ThreadId.makeUnsafe("thread-1"),    environmentId: localEnvironmentId,    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 projectId = ProjectId.makeUnsafe("project-1");  const project = {    id: projectId,    environmentId: thread.environmentId,    name: "Project",    cwd: "/tmp/project",    defaultModelSelection: {      provider: "codex" as const,      model: "gpt-5-codex",    },    createdAt: "2026-02-13T00:00:00.000Z",    updatedAt: "2026-02-13T00:00:00.000Z",    scripts: [],  };  const threadIdsByProjectId: EnvironmentState["threadIdsByProjectId"] = {    [thread.projectId]: [thread.id],  };  const environmentState = {    projectIds: [projectId],    projectById: {      [projectId]: project,    },    threadIds: [thread.id],    threadIdsByProjectId,    threadShellById: {      [thread.id]: {        id: thread.id,        environmentId: thread.environmentId,        codexThreadId: thread.codexThreadId,        projectId: thread.projectId,        title: thread.title,        modelSelection: thread.modelSelection,        runtimeMode: thread.runtimeMode,        interactionMode: thread.interactionMode,        error: thread.error,        createdAt: thread.createdAt,        archivedAt: thread.archivedAt,        updatedAt: thread.updatedAt,        branch: thread.branch,        worktreePath: thread.worktreePath,      },    },    threadSessionById: {      [thread.id]: thread.session,    },    threadTurnStateById: {      [thread.id]: {        latestTurn: thread.latestTurn,        ...(thread.pendingSourceProposedPlan          ? { pendingSourceProposedPlan: thread.pendingSourceProposedPlan }          : {}),      },    },    messageIdsByThreadId: {      [thread.id]: thread.messages.map((message) => message.id),    },    messageByThreadId: {      [thread.id]: Object.fromEntries(        thread.messages.map((message) => [message.id, message] as const),      ) as EnvironmentState["messageByThreadId"][ThreadId],    },    activityIdsByThreadId: {      [thread.id]: thread.activities.map((activity) => activity.id),    },    activityByThreadId: {      [thread.id]: Object.fromEntries(        thread.activities.map((activity) => [activity.id, activity] as const),      ) as EnvironmentState["activityByThreadId"][ThreadId],    },    proposedPlanIdsByThreadId: {      [thread.id]: thread.proposedPlans.map((plan) => plan.id),    },    proposedPlanByThreadId: {      [thread.id]: Object.fromEntries(        thread.proposedPlans.map((plan) => [plan.id, plan] as const),      ) as EnvironmentState["proposedPlanByThreadId"][ThreadId],    },    turnDiffIdsByThreadId: {      [thread.id]: thread.turnDiffSummaries.map((summary) => summary.turnId),    },    turnDiffSummaryByThreadId: {      [thread.id]: Object.fromEntries(        thread.turnDiffSummaries.map((summary) => [summary.turnId, summary] as const),      ) as EnvironmentState["turnDiffSummaryByThreadId"][ThreadId],    },    sidebarThreadSummaryById: {},    bootstrapComplete: true,  };  return withActiveEnvironmentState(environmentState, {    activeEnvironmentId: thread.environmentId,  });} function makeEmptyState(overrides: Partial<AppState & EnvironmentState> = {}): AppState {  const environmentState: EnvironmentState = {    projectIds: [],    projectById: {},    threadIds: [],    threadIdsByProjectId: {},    threadShellById: {},    threadSessionById: {},    threadTurnStateById: {},    messageIdsByThreadId: {},    messageByThreadId: {},    activityIdsByThreadId: {},    activityByThreadId: {},    proposedPlanIdsByThreadId: {},    proposedPlanByThreadId: {},    turnDiffIdsByThreadId: {},    turnDiffSummaryByThreadId: {},    sidebarThreadSummaryById: {},    bootstrapComplete: true,  };  return withActiveEnvironmentState(environmentState, overrides);} function localEnvironmentStateOf(state: AppState): EnvironmentState {  return selectEnvironmentState(state, localEnvironmentId);} function environmentStateOf(state: AppState, environmentId: EnvironmentId): EnvironmentState {  return selectEnvironmentState(state, environmentId);} function projectsOf(state: AppState) {  return selectProjectsAcrossEnvironments(state);} function threadsOf(state: AppState) {  return selectThreadsAcrossEnvironments(state);} 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("setThreadBranch", () => {  it("updates only the scoped thread environment", () => {    const sharedThreadId = ThreadId.makeUnsafe("thread-shared");    const localThread = makeThread({      id: sharedThreadId,      environmentId: localEnvironmentId,      branch: "local-branch",    });    const remoteThread = makeThread({      id: sharedThreadId,      environmentId: remoteEnvironmentId,      branch: "remote-branch",    });    const state: AppState = {      activeEnvironmentId: localEnvironmentId,      environmentStateById: {        [localEnvironmentId]: environmentStateOf(makeState(localThread), localEnvironmentId),        [remoteEnvironmentId]: environmentStateOf(makeState(remoteThread), remoteEnvironmentId),      },    };     const next = setThreadBranch(      state,      scopeThreadRef(remoteEnvironmentId, sharedThreadId),      "remote-next",      "/tmp/remote-worktree",    );     expect(      environmentStateOf(next, localEnvironmentId).threadShellById[sharedThreadId]?.branch,    ).toBe("local-branch");    expect(      environmentStateOf(next, remoteEnvironmentId).threadShellById[sharedThreadId]?.branch,    ).toBe("remote-next");    expect(      environmentStateOf(next, remoteEnvironmentId).threadShellById[sharedThreadId]?.worktreePath,    ).toBe("/tmp/remote-worktree");  });}); describe("store read model sync", () => {  it("marks bootstrap complete after snapshot sync", () => {    const initialState = withActiveEnvironmentState(      localEnvironmentStateOf(makeState(makeThread())),      {        bootstrapComplete: false,      },    );     const next = syncServerReadModel(      initialState,      makeReadModel(makeReadModelThread({})),      localEnvironmentId,    );     expect(localEnvironmentStateOf(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, localEnvironmentId);     expect(threadsOf(next)[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, localEnvironmentId);     expect(threadsOf(next)[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, localEnvironmentId);     expect(projectsOf(next)[0]?.updatedAt).toBe("2026-02-27T00:00:00.000Z");    expect(threadsOf(next)[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,        }),      ),      localEnvironmentId,    );     expect(threadsOf(next)[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 = makeEmptyState({      projectIds: [project2, project1],      projectById: {        [project2]: {          id: project2,          environmentId: localEnvironmentId,          name: "Project 2",          cwd: "/tmp/project-2",          defaultModelSelection: {            provider: "codex",            model: DEFAULT_MODEL_BY_PROVIDER.codex,          },          createdAt: "2026-02-27T00:00:00.000Z",          updatedAt: "2026-02-27T00:00:00.000Z",          scripts: [],        },        [project1]: {          id: project1,          environmentId: localEnvironmentId,          name: "Project 1",          cwd: "/tmp/project-1",          defaultModelSelection: {            provider: "codex",            model: DEFAULT_MODEL_BY_PROVIDER.codex,          },          createdAt: "2026-02-27T00:00:00.000Z",          updatedAt: "2026-02-27T00:00:00.000Z",          scripts: [],        },      },    });    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",        }),      ],

Lines 1–500 of 1117.