900 lines · 26.5 KB · TypeScript
RawDownload
import {  CommandId,  EventId,  ProjectId,  ThreadId,  type OrchestrationEvent,} from "@t3tools/contracts";import { Effect } from "effect";import { describe, expect, it } from "vitest"; import { createEmptyReadModel, projectEvent } from "./projector.ts"; function makeEvent(input: {  sequence: number;  type: OrchestrationEvent["type"];  occurredAt: string;  aggregateKind: OrchestrationEvent["aggregateKind"];  aggregateId: string;  commandId: string | null;  payload: unknown;}): OrchestrationEvent {  return {    sequence: input.sequence,    eventId: EventId.makeUnsafe(`event-${input.sequence}`),    type: input.type,    aggregateKind: input.aggregateKind,    aggregateId:      input.aggregateKind === "project"        ? ProjectId.makeUnsafe(input.aggregateId)        : ThreadId.makeUnsafe(input.aggregateId),    occurredAt: input.occurredAt,    commandId: input.commandId === null ? null : CommandId.makeUnsafe(input.commandId),    causationEventId: null,    correlationId: null,    metadata: {},    payload: input.payload as never,  } as OrchestrationEvent;} describe("orchestration projector", () => {  it("applies thread.created events", async () => {    const now = new Date().toISOString();    const model = createEmptyReadModel(now);     const next = await Effect.runPromise(      projectEvent(        model,        makeEvent({          sequence: 1,          type: "thread.created",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: now,          commandId: "cmd-thread-create",          payload: {            threadId: "thread-1",            projectId: "project-1",            title: "demo",            model: "gpt-5-codex",            runtimeMode: "full-access",            branch: null,            worktreePath: null,            createdAt: now,            updatedAt: now,          },        }),      ),    );     expect(next.snapshotSequence).toBe(1);    expect(next.threads).toEqual([      {        id: "thread-1",        projectId: "project-1",        title: "demo",        model: "gpt-5-codex",        runtimeMode: "full-access",        interactionMode: "default",        branch: null,        worktreePath: null,        latestTurn: null,        createdAt: now,        updatedAt: now,        deletedAt: null,        messages: [],        proposedPlans: [],        activities: [],        workUnits: [],        checkpoints: [],        session: null,      },    ]);  });   it("fails when event payload cannot be decoded by runtime schema", async () => {    const now = new Date().toISOString();    const model = createEmptyReadModel(now);     await expect(      Effect.runPromise(        projectEvent(          model,          makeEvent({            sequence: 1,            type: "thread.created",            aggregateKind: "thread",            aggregateId: "thread-1",            occurredAt: now,            commandId: "cmd-invalid",            payload: {              // missing required threadId              projectId: "project-1",              title: "demo",              model: "gpt-5-codex",              branch: null,              worktreePath: null,              createdAt: now,              updatedAt: now,            },          }),        ),      ),    ).rejects.toBeDefined();  });   it("applies thread.work-unit-upserted events", async () => {    const now = new Date().toISOString();    const model = createEmptyReadModel(now);     const afterCreate = await Effect.runPromise(      projectEvent(        model,        makeEvent({          sequence: 1,          type: "thread.created",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: now,          commandId: "cmd-thread-create",          payload: {            threadId: "thread-1",            projectId: "project-1",            title: "demo",            model: "gpt-5-codex",            runtimeMode: "full-access",            branch: null,            worktreePath: null,            createdAt: now,            updatedAt: now,          },        }),      ),    );     const next = await Effect.runPromise(      projectEvent(        afterCreate,        makeEvent({          sequence: 2,          type: "thread.work-unit-upserted",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: now,          commandId: "cmd-work-unit-upsert",          payload: {            threadId: "thread-1",            workUnit: {              id: "wu:thread-1:turn:turn-1:root",              turnId: "turn-1",              parentWorkUnitId: null,              kind: "primary_agent",              state: "running",              title: "Primary agent",              detail: null,              spawnedByActivityId: null,              providerRefs: {                providerTurnId: "provider-turn-1",              },              startedAt: now,              updatedAt: now,              completedAt: null,            },          },        }),      ),    );     expect(next.threads[0]?.workUnits).toEqual([      {        id: "wu:thread-1:turn:turn-1:root",        turnId: "turn-1",        parentWorkUnitId: null,        kind: "primary_agent",        state: "running",        title: "Primary agent",        detail: null,        spawnedByActivityId: null,        providerRefs: {          providerTurnId: "provider-turn-1",        },        startedAt: now,        updatedAt: now,        completedAt: null,      },    ]);  });   it("keeps projector forward-compatible for unhandled event types", async () => {    const now = new Date().toISOString();    const model = createEmptyReadModel(now);     const next = await Effect.runPromise(      projectEvent(        model,        makeEvent({          sequence: 7,          type: "thread.turn-start-requested",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: "2026-01-01T00:00:00.000Z",          commandId: "cmd-unhandled",          payload: {            threadId: "thread-1",            messageId: "message-1",            runtimeMode: "approval-required",            createdAt: "2026-01-01T00:00:00.000Z",          },        }),      ),    );     expect(next.snapshotSequence).toBe(7);    expect(next.updatedAt).toBe("2026-01-01T00:00:00.000Z");    expect(next.threads).toEqual([]);  });   it("tracks latest turn id from session lifecycle events", async () => {    const createdAt = "2026-02-23T08:00:00.000Z";    const startedAt = "2026-02-23T08:00:05.000Z";    const model = createEmptyReadModel(createdAt);     const afterCreate = await Effect.runPromise(      projectEvent(        model,        makeEvent({          sequence: 1,          type: "thread.created",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: createdAt,          commandId: "cmd-create",          payload: {            threadId: "thread-1",            projectId: "project-1",            title: "demo",            model: "gpt-5.3-codex",            runtimeMode: "full-access",            branch: null,            worktreePath: null,            createdAt,            updatedAt: createdAt,          },        }),      ),    );     const afterRunning = await Effect.runPromise(      projectEvent(        afterCreate,        makeEvent({          sequence: 2,          type: "thread.session-set",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: startedAt,          commandId: "cmd-running",          payload: {            threadId: "thread-1",            session: {              threadId: "thread-1",              status: "running",              providerName: "codex",              providerSessionId: "session-1",              providerThreadId: "provider-thread-1",              runtimeMode: "approval-required",              activeTurnId: "turn-1",              lastError: null,              updatedAt: startedAt,            },          },        }),      ),    );     const thread = afterRunning.threads[0];    expect(thread?.latestTurn?.turnId).toBe("turn-1");    expect(thread?.session?.status).toBe("running");  });   it("updates canonical thread runtime mode from thread.runtime-mode-set", async () => {    const createdAt = "2026-02-23T08:00:00.000Z";    const updatedAt = "2026-02-23T08:00:05.000Z";    const model = createEmptyReadModel(createdAt);     const afterCreate = await Effect.runPromise(      projectEvent(        model,        makeEvent({          sequence: 1,          type: "thread.created",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: createdAt,          commandId: "cmd-create",          payload: {            threadId: "thread-1",            projectId: "project-1",            title: "demo",            model: "gpt-5.3-codex",            runtimeMode: "full-access",            branch: null,            worktreePath: null,            createdAt,            updatedAt: createdAt,          },        }),      ),    );     const afterUpdate = await Effect.runPromise(      projectEvent(        afterCreate,        makeEvent({          sequence: 2,          type: "thread.runtime-mode-set",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: updatedAt,          commandId: "cmd-runtime-mode-set",          payload: {            threadId: "thread-1",            runtimeMode: "approval-required",            updatedAt,          },        }),      ),    );     expect(afterUpdate.threads[0]?.runtimeMode).toBe("approval-required");    expect(afterUpdate.threads[0]?.updatedAt).toBe(updatedAt);  });   it("marks assistant messages completed with non-streaming updates", async () => {    const createdAt = "2026-02-23T09:00:00.000Z";    const deltaAt = "2026-02-23T09:00:01.000Z";    const completeAt = "2026-02-23T09:00:03.500Z";    const model = createEmptyReadModel(createdAt);     const afterCreate = await Effect.runPromise(      projectEvent(        model,        makeEvent({          sequence: 1,          type: "thread.created",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: createdAt,          commandId: "cmd-create",          payload: {            threadId: "thread-1",            projectId: "project-1",            title: "demo",            model: "gpt-5.3-codex",            runtimeMode: "full-access",            branch: null,            worktreePath: null,            createdAt,            updatedAt: createdAt,          },        }),      ),    );     const afterDelta = await Effect.runPromise(      projectEvent(        afterCreate,        makeEvent({          sequence: 2,          type: "thread.message-sent",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: deltaAt,          commandId: "cmd-delta",          payload: {            threadId: "thread-1",            messageId: "assistant:msg-1",            role: "assistant",            text: "hello",            turnId: "turn-1",            streaming: true,            createdAt: deltaAt,            updatedAt: deltaAt,          },        }),      ),    );     const afterComplete = await Effect.runPromise(      projectEvent(        afterDelta,        makeEvent({          sequence: 3,          type: "thread.message-sent",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: completeAt,          commandId: "cmd-complete",          payload: {            threadId: "thread-1",            messageId: "assistant:msg-1",            role: "assistant",            text: "",            turnId: "turn-1",            streaming: false,            createdAt: completeAt,            updatedAt: completeAt,          },        }),      ),    );     const message = afterComplete.threads[0]?.messages[0];    expect(message?.id).toBe("assistant:msg-1");    expect(message?.text).toBe("hello");    expect(message?.streaming).toBe(false);    expect(message?.updatedAt).toBe(completeAt);  });   it("prunes reverted turn messages from in-memory thread snapshot", async () => {    const createdAt = "2026-02-23T10:00:00.000Z";    const model = createEmptyReadModel(createdAt);     const afterCreate = await Effect.runPromise(      projectEvent(        model,        makeEvent({          sequence: 1,          type: "thread.created",          aggregateKind: "thread",          aggregateId: "thread-1",          occurredAt: createdAt,          commandId: "cmd-create",          payload: {            threadId: "thread-1",            projectId: "project-1",            title: "demo",            model: "gpt-5.3-codex",            runtimeMode: "full-access",            branch: null,            worktreePath: null,            createdAt,            updatedAt: createdAt,          },        }),      ),    );     const events: ReadonlyArray<OrchestrationEvent> = [      makeEvent({        sequence: 2,        type: "thread.message-sent",        aggregateKind: "thread",        aggregateId: "thread-1",        occurredAt: "2026-02-23T10:00:01.000Z",        commandId: "cmd-user-1",        payload: {          threadId: "thread-1",          messageId: "user-msg-1",          role: "user",          text: "First edit",          turnId: null,          streaming: false,          createdAt: "2026-02-23T10:00:01.000Z",          updatedAt: "2026-02-23T10:00:01.000Z",        },      }),      makeEvent({        sequence: 3,        type: "thread.message-sent",        aggregateKind: "thread",        aggregateId: "thread-1",        occurredAt: "2026-02-23T10:00:02.000Z",        commandId: "cmd-assistant-1",        payload: {          threadId: "thread-1",          messageId: "assistant-msg-1",          role: "assistant",          text: "Updated README to v2.\n",          turnId: "turn-1",          streaming: false,

Lines 1–500 of 900.