4369 lines · 146.1 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:offimport * as NodeFS from "node:fs";import * as NodeOS from "node:os";import * as NodePath from "node:path";import * as NodeChildProcess from "node:child_process"; import {  OrchestrationReadModel,  ProviderDriverKind,  ProviderRuntimeEvent,  ProviderSession,  ProviderInstanceId,} from "@t3tools/contracts";import {  ApprovalRequestId,  CommandId,  DEFAULT_PROVIDER_INTERACTION_MODE,  EventId,  MessageId,  type OrchestrationCommand,  ProjectId,  ProviderItemId,  RuntimeRequestId,  type ServerSettings,  ThreadId,  TurnId,} from "@t3tools/contracts";import * as Clock from "effect/Clock";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as ManagedRuntime from "effect/ManagedRuntime";import * as Option from "effect/Option";import * as PubSub from "effect/PubSub";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import * as Tracer from "effect/Tracer";import { it as effectIt } from "@effect/vitest";import { afterEach, describe, expect, it } from "vite-plus/test"; import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";import {  ProviderService,  type ProviderServiceShape,} from "../../provider/Services/ProviderService.ts";import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts";import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts";import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";import * as VcsProcess from "../../vcs/VcsProcess.ts";import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";import * as ThreadBackgroundLiveness from "../ThreadBackgroundLiveness.ts";import * as ThreadPlanProgress from "../ThreadPlanProgress.ts";import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts";import { DEFAULT_THREAD_TITLE } from "../threadTitles.ts";import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts";import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import * as NodeServices from "@effect/platform-node/NodeServices";import { makeSqlStatementCounter } from "../../../integration/SqlStatementCounter.integration.ts"; function makeTestServerSettingsLayer(overrides: Partial<ServerSettings> = {}) {  return ServerSettingsService.layerTest(overrides);} const asProjectId = (value: string): ProjectId => ProjectId.make(value);const asItemId = (value: string): ProviderItemId => ProviderItemId.make(value);const asEventId = (value: string): EventId => EventId.make(value);const asMessageId = (value: string): MessageId => MessageId.make(value);const asThreadId = (value: string): ThreadId => ThreadId.make(value);const asTurnId = (value: string): TurnId => TurnId.make(value); type LegacyProviderRuntimeEvent = {  readonly type: string;  readonly eventId: EventId;  readonly provider: ProviderRuntimeEvent["provider"];  readonly createdAt: string;  readonly threadId: ThreadId;  readonly turnId?: string | undefined;  readonly itemId?: string | undefined;  readonly requestId?: string | undefined;  readonly payload?: unknown | undefined;  readonly [key: string]: unknown;}; type LegacyTurnCompletedEvent = LegacyProviderRuntimeEvent & {  readonly type: "turn.completed";  readonly payload?: undefined;  readonly status: "completed" | "failed" | "interrupted" | "cancelled";  readonly errorMessage?: string | undefined;}; function isLegacyTurnCompletedEvent(  event: LegacyProviderRuntimeEvent,): event is LegacyTurnCompletedEvent {  return (    event.type === "turn.completed" &&    event.payload === undefined &&    typeof event.status === "string"  );} function createProviderServiceHarness() {  const runtimeEventPubSub = Effect.runSync(    PubSub.unbounded<{      readonly events: ReadonlyArray<ProviderRuntimeEvent>;      readonly enqueued?: Deferred.Deferred<void>;    }>(),  );  const runtimeSessions: ProviderSession[] = [];   const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;  const service: ProviderServiceShape = {    startSession: () => unsupported(),    sendTurn: () => unsupported(),    compactThread: () => unsupported(),    interruptTurn: () => unsupported(),    respondToRequest: () => unsupported(),    respondToUserInput: () => unsupported(),    stopSession: () => unsupported(),    listSessions: () => Effect.succeed([...runtimeSessions]),    getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }),    assertConversationRollbackSupported: () => unsupported(),    getInstanceInfo: (instanceId) => {      const driverKind = ProviderDriverKind.make(String(instanceId));      return Effect.succeed({        instanceId,        driverKind,        displayName: undefined,        enabled: true,        continuationIdentity: {          driverKind,          continuationKey: `${driverKind}:instance:${instanceId}`,        },      });    },    rollbackConversation: () => unsupported(),    uploadFeedback: () => unsupported(),    get streamEvents() {      return Stream.fromPubSub(runtimeEventPubSub).pipe(        Stream.flatMap(({ events, enqueued }) =>          Stream.concat(            Stream.fromIterable(events),            enqueued              ? Stream.fromEffect(Deferred.succeed(enqueued, undefined)).pipe(Stream.drain)              : Stream.empty,          ),        ),      );    },  };   const setSession = (session: ProviderSession): void => {    const existingIndex = runtimeSessions.findIndex((entry) => entry.threadId === session.threadId);    if (existingIndex >= 0) {      runtimeSessions[existingIndex] = session;      return;    }    runtimeSessions.push(session);  };   const normalizeLegacyEvent = (event: LegacyProviderRuntimeEvent): ProviderRuntimeEvent => {    if (isLegacyTurnCompletedEvent(event)) {      const normalized: Extract<ProviderRuntimeEvent, { type: "turn.completed" }> = {        ...(event as Omit<Extract<ProviderRuntimeEvent, { type: "turn.completed" }>, "payload">),        payload: {          state: event.status,          ...(typeof event.errorMessage === "string" ? { errorMessage: event.errorMessage } : {}),        },      };      return normalized;    }     return event as ProviderRuntimeEvent;  };   const emit = (event: LegacyProviderRuntimeEvent): void => {    Effect.runSync(PubSub.publish(runtimeEventPubSub, { events: [normalizeLegacyEvent(event)] }));  };   const emitAndWaitForEnqueue = Effect.fnUntraced(function* (    events: ReadonlyArray<LegacyProviderRuntimeEvent>,  ) {    const enqueued = yield* Deferred.make<void>();    yield* PubSub.publish(runtimeEventPubSub, {      events: events.map(normalizeLegacyEvent),      enqueued,    });    yield* Deferred.await(enqueued);  });   return {    service,    emit,    emitAndWaitForEnqueue,    setSession,  };} type ProviderRuntimeTestReadModel = OrchestrationReadModel;type ProviderRuntimeTestThread = ProviderRuntimeTestReadModel["threads"][number];type ProviderRuntimeTestMessage = ProviderRuntimeTestThread["messages"][number];type ProviderRuntimeTestProposedPlan = ProviderRuntimeTestThread["proposedPlans"][number];type ProviderRuntimeTestActivity = ProviderRuntimeTestThread["activities"][number];type ProviderRuntimeTestCheckpoint = ProviderRuntimeTestThread["checkpoints"][number]; async function waitForThread(  readModel: () => Promise<ProviderRuntimeTestReadModel>,  predicate: (thread: ProviderRuntimeTestThread) => boolean,  timeoutMs = 2000,  threadId: ThreadId = asThreadId("thread-1"),) {  const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;  const poll = async (): Promise<ProviderRuntimeTestThread> => {    const snapshot = await readModel();    const thread = snapshot.threads.find((entry) => entry.id === threadId);    if (thread && predicate(thread)) {      return thread;    }    if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {      throw new Error("Timed out waiting for thread state");    }    await Effect.runPromise(Effect.yieldNow);    return poll();  };  return poll();} describe("ProviderRuntimeIngestion", () => {  let runtime: ManagedRuntime.ManagedRuntime<    OrchestrationEngineService | ProviderRuntimeIngestionService | ProjectionSnapshotQuery,    unknown  > | null = null;  let scope: Scope.Closeable | null = null;  const tempDirs: string[] = [];   function makeTempDir(prefix: string): string {    const dir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), prefix));    tempDirs.push(dir);    return dir;  }   afterEach(async () => {    if (scope) {      await Effect.runPromise(Scope.close(scope, Exit.void));    }    scope = null;    if (runtime) {      await runtime.dispose();    }    runtime = null;    for (const dir of tempDirs.splice(0)) {      NodeFS.rmSync(dir, { recursive: true, force: true });    }  });   async function createHarness(options?: {    serverSettings?: Partial<ServerSettings>;    threadTitle?: string;    workspaceSubdirectory?: string;  }) {    const repositoryRoot = makeTempDir("t3-provider-project-");    NodeChildProcess.execFileSync("git", ["init", "--initial-branch=main"], {      cwd: repositoryRoot,      stdio: "ignore",    });    const workspaceRoot = NodePath.join(repositoryRoot, options?.workspaceSubdirectory ?? "");    NodeFS.mkdirSync(workspaceRoot, { recursive: true });    const provider = createProviderServiceHarness();    const sqlCounter = makeSqlStatementCounter();    const orchestrationLayer = OrchestrationEngineLive.pipe(      Layer.provide(OrchestrationProjectionSnapshotQueryLive),      Layer.provide(OrchestrationProjectionPipelineLive),      Layer.provide(OrchestrationEventStoreLive),      Layer.provide(OrchestrationCommandReceiptRepositoryLive),      Layer.provide(RepositoryIdentityResolver.layer),      Layer.provide(SqlitePersistenceMemory),    );    const projectionSnapshotLayer = OrchestrationProjectionSnapshotQueryLive.pipe(      Layer.provide(RepositoryIdentityResolver.layer),      Layer.provide(SqlitePersistenceMemory),    );    const ingestionProjectionSnapshotLayer = Layer.effect(      ProjectionSnapshotQuery,      Effect.gen(function* () {        const query = yield* ProjectionSnapshotQuery;        return ProjectionSnapshotQuery.of({          ...query,          getThreadDetailById: () =>            Effect.die("provider runtime ingestion must not hydrate thread detail"),        });      }),    ).pipe(Layer.provide(projectionSnapshotLayer));    const layer = ProviderRuntimeIngestionLive.pipe(      Layer.provideMerge(orchestrationLayer),      Layer.provideMerge(ingestionProjectionSnapshotLayer),      // Single shared liveness instance across ingestion (writer), the      // engine, and the snapshot query (reader).      Layer.provideMerge(ThreadBackgroundLiveness.layer),      Layer.provideMerge(ThreadPlanProgress.layer),      Layer.provideMerge(SqlitePersistenceMemory),      Layer.provideMerge(Layer.succeed(ProviderService, provider.service)),      Layer.provideMerge(makeTestServerSettingsLayer(options?.serverSettings)),      Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer))),      Layer.provideMerge(VcsProcess.layer),      Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),      Layer.provideMerge(NodeServices.layer),      Layer.provideMerge(Layer.succeed(Tracer.Tracer, sqlCounter.tracer)),    );    const testRuntime = ManagedRuntime.make(layer);    runtime = testRuntime;    const engine = await testRuntime.runPromise(Effect.service(OrchestrationEngineService));    const snapshotQuery = await testRuntime.runPromise(Effect.service(ProjectionSnapshotQuery));    const ingestion = await testRuntime.runPromise(Effect.service(ProviderRuntimeIngestionService));    scope = await Effect.runPromise(Scope.make("sequential"));    await testRuntime.runPromise(ingestion.start().pipe(Scope.provide(scope)));    const drain = () => testRuntime.runPromise(ingestion.drain);    const dispatch = (command: OrchestrationCommand) =>      testRuntime.runPromise(engine.dispatch(command));    const emitAndDrain = (events: ReadonlyArray<LegacyProviderRuntimeEvent>) =>      testRuntime.runPromise(        provider.emitAndWaitForEnqueue(events).pipe(Effect.andThen(ingestion.drain)),      );     const createdAt = "2026-01-01T00:00:00.000Z";    await dispatch({      type: "project.create",      commandId: CommandId.make("cmd-provider-project-create"),      projectId: asProjectId("project-1"),      title: "Provider Project",      workspaceRoot,      defaultModelSelection: {        instanceId: ProviderInstanceId.make("codex"),        model: "gpt-5-codex",      },      createdAt,    });    await dispatch({      type: "thread.create",      commandId: CommandId.make("cmd-thread-create"),      threadId: ThreadId.make("thread-1"),      projectId: asProjectId("project-1"),      title: options?.threadTitle ?? "Thread",      modelSelection: {        instanceId: ProviderInstanceId.make("codex"),        model: "gpt-5-codex",      },      interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,      runtimeMode: "approval-required",      branch: null,      worktreePath: null,      createdAt,    });    await dispatch({      type: "thread.session.set",      commandId: CommandId.make("cmd-session-seed"),      threadId: ThreadId.make("thread-1"),      session: {        threadId: ThreadId.make("thread-1"),        status: "ready",        providerName: "codex",        runtimeMode: "approval-required",        activeTurnId: null,        updatedAt: createdAt,        lastError: null,      },      createdAt,    });    provider.setSession({      provider: ProviderDriverKind.make("codex"),      status: "ready",      runtimeMode: "approval-required",      threadId: ThreadId.make("thread-1"),      createdAt,      updatedAt: createdAt,    });     return {      engine,      dispatch,      readModel: () => testRuntime.runPromise(snapshotQuery.getSnapshot()),      readThreadShell: () =>        testRuntime.runPromise(          snapshotQuery            .getThreadShellById(asThreadId("thread-1"))            .pipe(Effect.map(Option.getOrThrow)),        ),      emit: provider.emit,      emitAndDrain,      sqlCount: sqlCounter.count,      setProviderSession: provider.setSession,      drain,    };  }   it("maps turn started/completed events into thread session updates", async () => {    const harness = await createHarness();    const now = "2026-01-01T00:00:00.000Z";     harness.emit({      type: "turn.started",      eventId: asEventId("evt-turn-started"),      provider: ProviderDriverKind.make("codex"),      threadId: asThreadId("thread-1"),      createdAt: now,      turnId: asTurnId("turn-1"),    });     await waitForThread(      harness.readModel,      (thread) => thread.session?.status === "running" && thread.session?.activeTurnId === "turn-1",    );     harness.emit({      type: "turn.completed",      eventId: asEventId("evt-turn-completed"),      provider: ProviderDriverKind.make("codex"),      threadId: asThreadId("thread-1"),      createdAt: "2026-01-01T00:00:00.000Z",      turnId: asTurnId("turn-1"),      payload: {        state: "failed",        errorMessage: "turn failed",      },    });     const thread = await waitForThread(      harness.readModel,      (entry) =>        entry.session?.status === "error" &&        entry.session?.activeTurnId === null &&        entry.session?.lastError === "turn failed",    );    expect(thread.session?.status).toBe("error");    expect(thread.session?.lastError).toBe("turn failed");  });   it.each([    { delivery: "buffered", enableLegacyTokenStreaming: false },    { delivery: "streamed", enableLegacyTokenStreaming: true },  ])("settles OpenCode aborted turns and saves $delivery assistant text", async (settings) => {    const harness = await createHarness({      serverSettings: { enableLegacyTokenStreaming: settings.enableLegacyTokenStreaming },    });    const threadId = asThreadId("thread-1");    const turnId = asTurnId("opencode-aborted-turn");    const base = {      provider: ProviderDriverKind.make("opencode"),      threadId,      turnId,      createdAt: "2026-01-01T00:00:01.000Z",    };    harness.emit({ ...base, type: "turn.started", eventId: asEventId("opencode-started") });    harness.emit({      ...base,      type: "content.delta",      eventId: asEventId("opencode-partial-text"),      itemId: asItemId("opencode-text-part"),      payload: { streamKind: "assistant_text", delta: "Work before the stop." },    });    harness.emit({      ...base,      type: "turn.aborted",      eventId: asEventId("opencode-aborted"),      createdAt: "2026-01-01T00:00:02.000Z",      payload: { reason: "Interrupted by user." },    });     await harness.drain();    const thread = (await harness.readModel()).threads.find((entry) => entry.id === threadId);    expect(thread?.session).toMatchObject({      status: "interrupted",      activeTurnId: null,      lastError: null,    });    expect(thread?.latestTurn).toMatchObject({      turnId,      state: "interrupted",      completedAt: "2026-01-01T00:00:02.000Z",    });    expect(thread?.messages).toEqual([      expect.objectContaining({        role: "assistant",        turnId,        text: "Work before the stop.",        streaming: false,      }),    ]);  });   it.each(["turn.completed", "turn.aborted"] as const)(    "finalizes old buffered text on late %s without stopping the newer turn",    async (terminalType) => {      const harness = await createHarness({

Lines 1–500 of 4369.