201 lines · 6.8 KB · TypeScript
RawDownload
import {  CommandId,  ProjectId,  ProviderInstanceId,  ThreadId,  type OrchestrationCommand,  type OrchestrationReadModel,  type OrchestrationThread,} from "@t3tools/contracts";import * as NodeServices from "@effect/platform-node/NodeServices";import { expect, it } from "@effect/vitest";import * as Effect from "effect/Effect"; import { decideOrchestrationCommand } from "./decider.ts";import { projectEvent } from "./projector.ts"; const NOW = "2026-01-01T00:00:00.000Z";// The Effect test clock starts at the epoch.const BEFORE_NOW = "1969-12-30T00:00:00.000Z";const SNOOZED_AT = "1969-12-31T00:00:00.000Z";const FUTURE_WAKE = "1970-01-02T00:00:00.000Z";const THREAD_ID = ThreadId.make("thread-1"); function makeReadModel(overrides: Partial<OrchestrationThread> = {}): OrchestrationReadModel {  return {    snapshotSequence: 0,    projects: [],    threads: [      {        id: THREAD_ID,        projectId: ProjectId.make("project-1"),        title: "Thread",        modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },        runtimeMode: "full-access",        interactionMode: "default",        pullRequests: [],        branch: null,        worktreePath: null,        latestTurn: null,        createdAt: NOW,        updatedAt: NOW,        archivedAt: null,        settledOverride: null,        settledAt: null,        unsettledAt: null,        activeOrderKey: null,        snoozedUntil: null,        snoozedAt: null,        pinnedAt: null,        pinOrderKey: null,        deletedAt: null,        messages: [],        proposedPlans: [],        activities: [],        checkpoints: [],        session: null,        ...overrides,      },    ],    updatedAt: NOW,  };} const reorderCommand = {  type: "thread.active.reorder",  commandId: CommandId.make("cmd-active-reorder"),  threadId: THREAD_ID,  orderKey: "m",} as const; it.layer(NodeServices.layer)("active thread ordering", (it) => {  it.effect("persists changed and repeated slots without changing thread activity timestamps", () =>    Effect.gen(function* () {      let readModel = makeReadModel({ unsettledAt: BEFORE_NOW });      for (const orderKey of ["m", "m", "g"]) {        const decided = yield* decideOrchestrationCommand({          command: { ...reorderCommand, orderKey },          readModel,        });        const events = Array.isArray(decided) ? decided : [decided];        expect(events).toHaveLength(1);        expect(events[0]).toMatchObject({          type: "thread.meta-updated",          payload: { threadId: THREAD_ID, activeOrderKey: orderKey, updatedAt: NOW },        });        for (const event of events) {          readModel = yield* projectEvent(readModel, {            ...event,            sequence: readModel.snapshotSequence + 1,          });        }        expect(readModel.threads[0]).toMatchObject({          activeOrderKey: orderKey,          updatedAt: NOW,          createdAt: NOW,          unsettledAt: BEFORE_NOW,        });      }    }),  );   for (const [label, overrides] of [    ["archived", { archivedAt: NOW }],    ["deleted", { deletedAt: NOW }],    ["pinned", { pinnedAt: NOW }],    ["settled", { settledOverride: "settled", settledAt: NOW }],  ] satisfies ReadonlyArray<readonly [string, Partial<OrchestrationThread>]>) {    it.effect(`rejects reordering a ${label} thread`, () =>      Effect.gen(function* () {        const error = yield* decideOrchestrationCommand({          command: reorderCommand,          readModel: makeReadModel(overrides),        }).pipe(Effect.flip);        expect(error._tag).toBe("OrchestrationCommandInvariantError");      }),    );  }   it.effect("reorders a running thread without affecting its session", () =>    Effect.gen(function* () {      const readModel = makeReadModel({        session: {          threadId: THREAD_ID,          status: "running",          providerName: "codex",          runtimeMode: "full-access",          activeTurnId: null,          lastError: null,          updatedAt: NOW,        },      });      const decided = yield* decideOrchestrationCommand({ command: reorderCommand, readModel });      const events = Array.isArray(decided) ? decided : [decided];      expect(events).toHaveLength(1);      for (const event of events) {        const projected = yield* projectEvent(readModel, { ...event, sequence: 1 });        expect(projected.threads[0]).toEqual({ ...readModel.threads[0], activeOrderKey: "m" });      }    }),  );   it.effect(    "changes a snoozed thread's retained slot without waking it or changing timestamps",    () =>      Effect.gen(function* () {        const readModel = makeReadModel({          activeOrderKey: "g",          snoozedAt: SNOOZED_AT,          snoozedUntil: FUTURE_WAKE,          unsettledAt: BEFORE_NOW,        });        const decided = yield* decideOrchestrationCommand({ command: reorderCommand, readModel });        const events = Array.isArray(decided) ? decided : [decided];        expect(events).toHaveLength(1);        for (const event of events) {          const projected = yield* projectEvent(readModel, { ...event, sequence: 1 });          expect(projected.threads[0]).toEqual({ ...readModel.threads[0], activeOrderKey: "m" });        }      }),  );   it.effect("keeps placement through metadata, pin and snooze, then resets it on settlement", () =>    Effect.gen(function* () {      let readModel = makeReadModel();      const steps = [        [reorderCommand, "m"],        [{ type: "thread.meta.update", title: "Renamed" }, "m"],        [{ type: "thread.pin", orderKey: "g" }, "m"],        [{ type: "thread.snooze", snoozedUntil: FUTURE_WAKE }, "m"],        [{ type: "thread.unsnooze", reason: "user" }, "m"],        [{ type: "thread.unpin" }, "m"],        [{ type: "thread.settle" }, null],        [{ type: "thread.unsettle", reason: "user" }, null],        [{ type: "thread.active.reorder", orderKey: "s" }, "s"],      ] as const;      for (const [index, [step, expectedKey]] of steps.entries()) {        const command: OrchestrationCommand = {          ...step,          commandId: CommandId.make(`lifecycle-${index}`),          threadId: THREAD_ID,        };        const decided = yield* decideOrchestrationCommand({ command, readModel });        const events = Array.isArray(decided) ? decided : [decided];        for (const event of events) {          readModel = yield* projectEvent(readModel, {            ...event,            sequence: readModel.snapshotSequence + 1,          });        }        expect(readModel.threads[0]?.activeOrderKey, command.type).toBe(expectedKey);      }      expect(readModel.threads[0]).toMatchObject({        title: "Renamed",        settledOverride: "active",        settledAt: null,        snoozedUntil: null,        pinnedAt: null,      });    }),  );});