666 lines · 21.7 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:offimport * as NodePath from "node:path";import * as NodeOS from "node:os";import * as NodeURL from "node:url";import * as NodeFS from "node:fs"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Option from "effect/Option";import * as TestClock from "effect/testing/TestClock";import * as Stream from "effect/Stream";import { describe, expect } from "vite-plus/test"; import * as AcpSessionRuntime from "./AcpSessionRuntime.ts";import type * as EffectAcpProtocol from "effect-acp/protocol"; const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));const mockAgentPath = NodePath.join(__dirname, "../../../scripts/acp-mock-agent.ts");const mockAgentCommand = "node";const mockAgentArgs = [mockAgentPath]; describe("AcpSessionRuntime", () => {  it.effect("merges custom initialize client capabilities into the ACP handshake", () => {    const requestEvents: Array<AcpSessionRuntime.AcpSessionRequestLogEvent> = [];    return Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       const initializeStarted = requestEvents.find(        (event) => event.method === "initialize" && event.status === "started",      );      expect(initializeStarted?.payload).toMatchObject({        protocolVersion: 1,        clientCapabilities: {          fs: { readTextFile: false, writeTextFile: false },          terminal: false,          _meta: { parameterizedModelPicker: true },        },      });    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,          },          cwd: process.cwd(),          clientCapabilities: {            _meta: {              parameterizedModelPicker: true,            },          },          clientInfo: { name: "t3-test", version: "0.0.0" },          authMethodId: "test",          requestLogger: (event) =>            Effect.sync(() => {              requestEvents.push(event);            }),        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    );  });   it.effect("starts a session, prompts, and emits normalized events against the mock agent", () =>    Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      const started = yield* runtime.start();       expect(started.initializeResult).toMatchObject({ protocolVersion: 1 });      expect(started.sessionId).toBe("mock-session-1");       const promptResult = yield* runtime.prompt({        prompt: [{ type: "text", text: "hi" }],      });      expect(promptResult).toMatchObject({ stopReason: "end_turn" });       const notes = Array.from(yield* Stream.runCollect(Stream.take(runtime.getEvents(), 4)));      expect(notes).toHaveLength(4);      expect(notes.map((note) => note._tag)).toEqual([        "PlanUpdated",        "AssistantItemStarted",        "ContentDelta",        "AssistantItemCompleted",      ]);      const planUpdate = notes.find((note) => note._tag === "PlanUpdated");      expect(planUpdate?._tag).toBe("PlanUpdated");      if (planUpdate?._tag === "PlanUpdated") {        expect(planUpdate.payload.plan).toHaveLength(2);      }      const assistantStart = notes[1];      const assistantDelta = notes[2];      if (        assistantStart?._tag === "AssistantItemStarted" &&        assistantDelta?._tag === "ContentDelta"      ) {        expect(assistantDelta.itemId).toBe(assistantStart.itemId);      }    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          authMethodId: "test",        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    ),  );   it.effect("keeps assistant item IDs unique when a provider session restarts", () => {    const collectFirstAssistantItemId = Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      const started = yield* runtime.start();      expect(started.sessionId).toBe("mock-session-1");       yield* runtime.prompt({        prompt: [{ type: "text", text: "hi" }],      });       const events = Array.from(yield* Stream.runCollect(Stream.take(runtime.getEvents(), 4)));      const assistantStart = events.find((event) => event._tag === "AssistantItemStarted");      expect(assistantStart?._tag).toBe("AssistantItemStarted");      return assistantStart?._tag === "AssistantItemStarted" ? assistantStart.itemId : "";    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          authMethodId: "test",        }),      ),      Effect.scoped,    );     return Effect.gen(function* () {      const beforeRestart = yield* collectFirstAssistantItemId;      const afterRestart = yield* collectFirstAssistantItemId;       expect(afterRestart).not.toBe(beforeRestart);    }).pipe(Effect.provide(NodeServices.layer));  });   it.effect("drops session updates emitted for a child ACP session", () =>    Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       const promptResult = yield* runtime.prompt({        prompt: [{ type: "text", text: "hi" }],      });      expect(promptResult).toMatchObject({ stopReason: "end_turn" });       const notes = Array.from(yield* Stream.runCollect(Stream.take(runtime.getEvents(), 4)));      expect(notes.map((note) => note._tag)).toEqual([        "AssistantItemStarted",        "ContentDelta",        "ContentDelta",        "AssistantItemCompleted",      ]);      expect(        notes          .filter((note) => note._tag === "ContentDelta")          .map((note) => note.text)          .join(""),      ).toBe("root before child root after child");      expect(notes.some((note) => note._tag === "ToolCallUpdated")).toBe(false);    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,            env: {              T3_ACP_EMIT_FOREIGN_SESSION_UPDATES: "1",            },          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          authMethodId: "test",        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    ),  );   it.effect("supports successive standard ACP prompts", () =>    Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       const firstPromptResult = yield* runtime.prompt({        prompt: [{ type: "text", text: "first" }],      });      const secondPromptResult = yield* runtime.prompt({        prompt: [{ type: "text", text: "second" }],      });       expect(firstPromptResult).toMatchObject({ stopReason: "end_turn" });      expect(secondPromptResult).toMatchObject({ stopReason: "end_turn" });    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          authMethodId: "test",        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    ),  );   it.effect("releases a fully silent prompt when session/cancel is requested", () =>    Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       const promptFiber = yield* runtime        .prompt({          prompt: [{ type: "text", text: "hang forever" }],        })        .pipe(Effect.forkChild({ startImmediately: true }));       yield* TestClock.adjust("500 millis");      yield* runtime.cancel;       const firstPromptResult = yield* Fiber.join(promptFiber);      expect(firstPromptResult).toMatchObject({ stopReason: "cancelled" });       const secondPromptResult = yield* runtime.prompt({        prompt: [{ type: "text", text: "second" }],      });      expect(secondPromptResult).toMatchObject({ stopReason: "end_turn" });    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,            env: {              T3_ACP_HANG_FIRST_PROMPT_FOREVER: "1",            },          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          authMethodId: "test",        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    ),  );   it.effect("segments assistant text around ACP tool calls", () =>    Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       const promptResult = yield* runtime.prompt({        prompt: [{ type: "text", text: "hi" }],      });      expect(promptResult).toMatchObject({ stopReason: "end_turn" });       const notes = Array.from(yield* Stream.runCollect(Stream.take(runtime.getEvents(), 7)));      expect(notes.map((note) => note._tag)).toEqual([        "AssistantItemStarted",        "ContentDelta",        "AssistantItemCompleted",        "ToolCallUpdated",        "ToolCallUpdated",        "AssistantItemStarted",        "ContentDelta",      ]);       const firstStarted = notes[0];      const firstDelta = notes[1];      const firstCompleted = notes[2];      const secondStarted = notes[5];      const secondDelta = notes[6];      expect(firstStarted?._tag).toBe("AssistantItemStarted");      expect(firstCompleted?._tag).toBe("AssistantItemCompleted");      expect(secondStarted?._tag).toBe("AssistantItemStarted");      if (        firstStarted?._tag === "AssistantItemStarted" &&        firstDelta?._tag === "ContentDelta" &&        firstCompleted?._tag === "AssistantItemCompleted" &&        secondStarted?._tag === "AssistantItemStarted" &&        secondDelta?._tag === "ContentDelta"      ) {        expect(firstDelta.itemId).toBe(firstStarted.itemId);        expect(firstCompleted.itemId).toBe(firstStarted.itemId);        expect(secondStarted.itemId).not.toBe(firstStarted.itemId);        expect(secondDelta.itemId).toBe(secondStarted.itemId);      }    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,            env: {              T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS: "1",            },          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          authMethodId: "test",        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    ),  );   it.effect("emits status-only tool updates through completion", () =>    Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       const promptResult = yield* runtime.prompt({        prompt: [{ type: "text", text: "hi" }],      });      expect(promptResult).toMatchObject({ stopReason: "end_turn" });       const notes = Array.from(yield* Stream.runCollect(Stream.take(runtime.getEvents(), 3)));      expect(notes.map((note) => note._tag)).toEqual([        "ToolCallUpdated",        "ToolCallUpdated",        "ToolCallUpdated",      ]);      const toolCalls = notes.flatMap((note) =>        note._tag === "ToolCallUpdated" ? [note.toolCall] : [],      );      expect(toolCalls.map((toolCall) => toolCall.status)).toEqual([        "pending",        "inProgress",        "completed",      ]);      for (const toolCall of toolCalls) {        expect(toolCall.title).toBe("Read file");      }    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,            env: {              T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1",            },          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          authMethodId: "test",        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    ),  );   it.effect("logs ACP requests from the shared runtime", () => {    const requestEvents: Array<AcpSessionRuntime.AcpSessionRequestLogEvent> = [];    return Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       yield* runtime.setModel("composer-2");      yield* runtime.prompt({        prompt: [{ type: "text", text: "hi" }],      });       expect(        requestEvents.some(          (event) => event.method === "session/set_config_option" && event.status === "started",        ),      ).toBe(true);      expect(        requestEvents.some(          (event) => event.method === "session/set_config_option" && event.status === "succeeded",        ),      ).toBe(true);      expect(        requestEvents.some(          (event) => event.method === "session/prompt" && event.status === "started",        ),      ).toBe(true);      expect(        requestEvents.some(          (event) => event.method === "session/prompt" && event.status === "succeeded",        ),      ).toBe(true);    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          authMethodId: "test",          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          requestLogger: (event) =>            Effect.sync(() => {              requestEvents.push(event);            }),        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    );  });   it.effect("skips no-op session config writes when the requested value is already active", () => {    const requestEvents: Array<AcpSessionRuntime.AcpSessionRequestLogEvent> = [];    return Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       yield* runtime.setConfigOption("model", "default");      yield* runtime.setMode("ask");       expect(        requestEvents.some(          (event) => event.method === "session/set_config_option" && event.status === "started",        ),      ).toBe(false);    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          authMethodId: "test",          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          requestLogger: (event) =>            Effect.sync(() => {              requestEvents.push(event);            }),        }),      ),      Effect.scoped,      Effect.provide(NodeServices.layer),    );  });   it.effect("emits low-level ACP protocol logs for raw and decoded messages", () => {    const protocolEvents: Array<EffectAcpProtocol.AcpProtocolLogEvent> = [];    return Effect.gen(function* () {      const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;      yield* runtime.start();       yield* runtime.prompt({        prompt: [{ type: "text", text: "hi" }],      });       expect(        protocolEvents.some((event) => event.direction === "outgoing" && event.stage === "raw"),      ).toBe(true);      expect(        protocolEvents.some((event) => event.direction === "outgoing" && event.stage === "decoded"),      ).toBe(true);      expect(        protocolEvents.some((event) => event.direction === "incoming" && event.stage === "raw"),      ).toBe(true);      expect(        protocolEvents.some((event) => event.direction === "incoming" && event.stage === "decoded"),      ).toBe(true);    }).pipe(      Effect.provide(        AcpSessionRuntime.layer({          authMethodId: "test",          spawn: {            command: mockAgentCommand,            args: mockAgentArgs,          },          cwd: process.cwd(),          clientInfo: { name: "t3-test", version: "0.0.0" },          protocolLogging: {            logIncoming: true,

Lines 1–500 of 666.