import * as path from "node:path";import * as os from "node:os";import { chmod, mkdtemp, readFile, writeFile } from "node:fs/promises";import { fileURLToPath } from "node:url"; import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it } from "@effect/vitest";import { Deferred, Effect, Fiber, Layer, Stream } from "effect";import { createModelSelection } from "@t3tools/shared/model"; import { ApprovalRequestId, type ProviderRuntimeEvent, ThreadId } from "@t3tools/contracts"; import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import { CursorAdapter } from "../Services/CursorAdapter.ts";import { makeCursorAdapterLive } from "./CursorAdapter.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url));const mockAgentPath = path.join(__dirname, "../../../scripts/acp-mock-agent.ts");const bunExe = "bun"; async function makeMockAgentWrapper( extraEnv?: Record<string, string>, options?: { initialDelaySeconds?: number },) { const dir = await mkdtemp(path.join(os.tmpdir(), "cursor-acp-mock-")); const wrapperPath = path.join(dir, "fake-agent.sh"); const envExports = Object.entries(extraEnv ?? {}) .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) .join("\n"); const script = `#!/bin/sh${envExports}${options?.initialDelaySeconds ? `sleep ${JSON.stringify(String(options.initialDelaySeconds))}` : ""}exec ${JSON.stringify(bunExe)} ${JSON.stringify(mockAgentPath)} "$@"`; await writeFile(wrapperPath, script, "utf8"); await chmod(wrapperPath, 0o755); return wrapperPath;} async function makeProbeWrapper( requestLogPath: string, argvLogPath: string, extraEnv?: Record<string, string>,) { const dir = await mkdtemp(path.join(os.tmpdir(), "cursor-acp-probe-")); const wrapperPath = path.join(dir, "fake-agent.sh"); const envExports = Object.entries(extraEnv ?? {}) .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) .join("\n"); const script = `#!/bin/shprintf '%s\t' "$@" >> ${JSON.stringify(argvLogPath)}printf '\n' >> ${JSON.stringify(argvLogPath)}export T3_ACP_REQUEST_LOG_PATH=${JSON.stringify(requestLogPath)}${envExports}exec ${JSON.stringify(bunExe)} ${JSON.stringify(mockAgentPath)} "$@"`; await writeFile(wrapperPath, script, "utf8"); await chmod(wrapperPath, 0o755); return wrapperPath;} async function readArgvLog(filePath: string) { const raw = await readFile(filePath, "utf8"); return raw .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0) .map((line) => line.split("\t").filter((token) => token.length > 0));} async function readJsonLines(filePath: string) { const raw = await readFile(filePath, "utf8"); return raw .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0) .map((line) => JSON.parse(line) as Record<string, unknown>);} async function waitForFileContent(filePath: string, attempts = 40) { for (let attempt = 0; attempt < attempts; attempt += 1) { try { const raw = await readFile(filePath, "utf8"); if (raw.trim().length > 0) { return raw; } } catch {} await new Promise((resolve) => setTimeout(resolve, 50)); } throw new Error(`Timed out waiting for file content at ${filePath}`);} const cursorAdapterTestLayer = it.layer( makeCursorAdapterLive().pipe( Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge( ServerConfig.layerTest(process.cwd(), { prefix: "t3code-cursor-adapter-test-", }), ), Layer.provideMerge(NodeServices.layer), ),); cursorAdapterTestLayer("CursorAdapterLive", (it) => { it.effect("starts a session and maps mock ACP prompt flow to runtime events", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const settings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-mock-thread"); const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper()); yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "default" }, }); assert.equal(session.provider, "cursor"); assert.deepStrictEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "mock-session-1", }); yield* adapter.sendTurn({ threadId, input: "hello mock", attachments: [], }); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); const types = runtimeEvents.map((e) => e.type); for (const t of [ "session.started", "session.state.changed", "thread.started", "turn.started", "turn.plan.updated", "item.started", "content.delta", "item.completed", "turn.completed", ] as const) { assert.include(types, t); } const assistantStarted = runtimeEvents.find( (event) => event.type === "item.started" && event.payload.itemType === "assistant_message", ); assert.isDefined(assistantStarted); const delta = runtimeEvents.find((e) => e.type === "content.delta"); assert.isDefined(delta); if (delta?.type === "content.delta") { assert.equal(delta.payload.delta, "hello from mock"); assert.match(String(delta.itemId), /^assistant:mock-session-1:segment:0$/); } const assistantCompleted = runtimeEvents.find( (event) => event.type === "item.completed" && event.payload.itemType === "assistant_message", ); assert.isDefined(assistantCompleted); const planUpdate = runtimeEvents.find((event) => event.type === "turn.plan.updated"); assert.isDefined(planUpdate); if (planUpdate?.type === "turn.plan.updated") { assert.deepStrictEqual(planUpdate.payload.plan, [ { step: "Inspect mock ACP state", status: "completed" }, { step: "Implement the requested change", status: "inProgress" }, ]); } yield* adapter.stopSession(threadId); }), ); it.effect("closes the ACP child process when a session stops", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const settings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-stop-session-close"); const tempDir = yield* Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "cursor-adapter-exit-log-")), ); const exitLogPath = path.join(tempDir, "exit.log"); const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper({ T3_ACP_EXIT_LOG_PATH: exitLogPath, }), ); yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "default" }, }); yield* adapter.stopSession(threadId); const exitLog = yield* Effect.promise(() => waitForFileContent(exitLogPath)); assert.include(exitLog, "SIGTERM"); }), ); it.effect( "serializes concurrent startSession calls for the same thread and closes the replaced ACP session", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const settings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-concurrent-start-session"); const tempDir = yield* Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "cursor-adapter-concurrent-exit-log-")), ); const exitLogPath = path.join(tempDir, "exit.log"); const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper( { T3_ACP_EXIT_LOG_PATH: exitLogPath, }, { initialDelaySeconds: 0.2 }, ), ); yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); const [firstSession, secondSession] = yield* Effect.all( [ adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "default" }, }), adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "default" }, }), ], { concurrency: "unbounded" }, ); assert.equal(firstSession.threadId, threadId); assert.equal(secondSession.threadId, threadId); yield* adapter.stopSession(threadId); const exitLog = yield* Effect.promise(() => waitForFileContent(exitLogPath)); assert.equal(exitLog.match(/SIGTERM/g)?.length ?? 0, 2); }), ); it.effect("rejects startSession when provider mismatches", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const result = yield* adapter .startSession({ threadId: ThreadId.make("bad-provider"), provider: "codex", cwd: process.cwd(), runtimeMode: "full-access", }) .pipe(Effect.result); assert.equal(result._tag, "Failure"); }), ); it.effect("maps app plan mode onto the ACP plan session mode", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const serverSettings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-plan-mode-probe"); const tempDir = yield* Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "cursor-acp-"))); const requestLogPath = path.join(tempDir, "requests.ndjson"); const argvLogPath = path.join(tempDir, "argv.txt"); yield* Effect.promise(() => writeFile(requestLogPath, "", "utf8")); const wrapperPath = yield* Effect.promise(() => makeProbeWrapper(requestLogPath, argvLogPath), ); yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "composer-2" }, }); yield* adapter.sendTurn({ threadId, input: "plan this change", attachments: [], interactionMode: "plan", }); yield* adapter.stopSession(threadId); const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); const modeRequest = requests .toReversed() .find( (entry) => entry.method === "session/set_mode" || (entry.method === "session/set_config_option" && (entry.params as Record<string, unknown> | undefined)?.configId === "mode"), ); assert.isDefined(modeRequest); assert.equal( (modeRequest?.params as Record<string, unknown> | undefined)?.sessionId, "mock-session-1", ); assert.include( ["architect", "plan"], String( (modeRequest?.params as Record<string, unknown> | undefined)?.modeId ?? (modeRequest?.params as Record<string, unknown> | undefined)?.value, ), ); }), ); it.effect( "applies initial model and mode configuration during startSession and skips repeating it on first send", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const serverSettings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-initial-config-probe"); const tempDir = yield* Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "cursor-acp-"))); const requestLogPath = path.join(tempDir, "requests.ndjson"); const argvLogPath = path.join(tempDir, "argv.txt"); yield* Effect.promise(() => writeFile(requestLogPath, "", "utf8")); const wrapperPath = yield* Effect.promise(() => makeProbeWrapper(requestLogPath, argvLogPath), ); yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } }, }); const modelSelection = createModelSelection("cursor", "gpt-5.4", [ { id: "reasoning", value: "xhigh" }, { id: "contextWindow", value: "1m" }, { id: "fastMode", value: true }, ]); yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection, }); yield* Effect.promise(() => waitForFileContent(requestLogPath)); const requestsAfterStart = yield* Effect.promise(() => readJsonLines(requestLogPath)); const configIdsAfterStart = requestsAfterStart.flatMap((entry) => entry.method === "session/set_config_option" && typeof (entry.params as Record<string, unknown> | undefined)?.configId === "string" ? [String((entry.params as Record<string, unknown>).configId)] : [], ); assert.deepStrictEqual(configIdsAfterStart, [ "model", "reasoning", "context", "fast", "mode", ]); yield* adapter.sendTurn({ threadId, input: "hello mock", attachments: [], modelSelection, interactionMode: "default", }); yield* adapter.stopSession(threadId); const finalRequests = yield* Effect.promise(() => readJsonLines(requestLogPath)); const finalConfigIds = finalRequests.flatMap((entry) => entry.method === "session/set_config_option" && typeof (entry.params as Record<string, unknown> | undefined)?.configId === "string" ? [String((entry.params as Record<string, unknown>).configId)] : [], ); assert.deepStrictEqual(finalConfigIds, ["model", "reasoning", "context", "fast", "mode"]); assert.equal(finalRequests.filter((entry) => entry.method === "session/prompt").length, 1); }), ); it.effect( "streams ACP tool calls and approvals on the active turn in approval-required mode", () => Effect.gen(function* () { const previousEmitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS; process.env.T3_ACP_EMIT_TOOL_CALLS = "1"; const adapter = yield* CursorAdapter; const serverSettings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-tool-call-probe"); const runtimeEvents: Array<ProviderRuntimeEvent> = []; const settledEventTypes = new Set<string>(); const settledEventsReady = yield* Deferred.make<void>(); const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper({ T3_ACP_EMIT_TOOL_CALLS: "1" }), ); yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } }, }); yield* Stream.runForEach(adapter.streamEvents, (event) => Effect.gen(function* () { runtimeEvents.push(event); if (String(event.threadId) !== String(threadId)) { return; } if (event.type === "request.opened" && event.requestId) { yield* adapter.respondToRequest( threadId, ApprovalRequestId.make(String(event.requestId)), "accept", ); } if ( event.type === "turn.completed" || (event.type === "item.completed" && event.payload.itemType === "command_execution") || event.type === "content.delta" ) { settledEventTypes.add(event.type); if (settledEventTypes.size === 3) { yield* Deferred.succeed(settledEventsReady, undefined).pipe(Effect.orDie); } } }), ).pipe(Effect.forkChild); const program = Effect.gen(function* () { yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "approval-required", modelSelection: { provider: "cursor", model: "default" }, }); const turn = yield* adapter.sendTurn({ threadId, input: "run a tool call", attachments: [], }); yield* Deferred.await(settledEventsReady); const threadEvents = runtimeEvents.filter( (event) => String(event.threadId) === String(threadId), ); assert.includeMembers( threadEvents.map((event) => event.type), [ "session.started", "session.state.changed", "thread.started", "turn.started", "request.opened", "request.resolved", "item.updated", "item.completed", "content.delta", "turn.completed", ], ); const turnEvents = threadEvents.filter( (event) => String(event.turnId) === String(turn.turnId), ); const toolUpdates = turnEvents.filter((event) => event.type === "item.updated"); // ACP updates can arrive either as distinct pending + in-progress events // or as a single coalesced in-progress update before approval resolves.Lines 1–500 of 1177. Load more
import * as path from "node:path"; import * as os from "node:os"; import { chmod, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { Deferred, Effect, Fiber, Layer, Stream } from "effect"; import { createModelSelection } from "@t3tools/shared/model"; import { ApprovalRequestId, type ProviderRuntimeEvent, ThreadId } from "@t3tools/contracts"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { CursorAdapter } from "../Services/CursorAdapter.ts"; import { makeCursorAdapterLive } from "./CursorAdapter.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const mockAgentPath = path.join(__dirname, "../../../scripts/acp-mock-agent.ts"); const bunExe = "bun"; async function makeMockAgentWrapper( extraEnv?: Record<string, string>, options?: { initialDelaySeconds?: number }, ) { const dir = await mkdtemp(path.join(os.tmpdir(), "cursor-acp-mock-")); const wrapperPath = path.join(dir, "fake-agent.sh"); const envExports = Object.entries(extraEnv ?? {}) .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) .join("\n"); const script = `#!/bin/sh ${envExports} ${options?.initialDelaySeconds ? `sleep ${JSON.stringify(String(options.initialDelaySeconds))}` : ""} exec ${JSON.stringify(bunExe)} ${JSON.stringify(mockAgentPath)} "$@" `; await writeFile(wrapperPath, script, "utf8"); await chmod(wrapperPath, 0o755); return wrapperPath; } async function makeProbeWrapper( requestLogPath: string, argvLogPath: string, extraEnv?: Record<string, string>, ) { const dir = await mkdtemp(path.join(os.tmpdir(), "cursor-acp-probe-")); const wrapperPath = path.join(dir, "fake-agent.sh"); const envExports = Object.entries(extraEnv ?? {}) .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) .join("\n"); const script = `#!/bin/sh printf '%s\t' "$@" >> ${JSON.stringify(argvLogPath)} printf '\n' >> ${JSON.stringify(argvLogPath)} export T3_ACP_REQUEST_LOG_PATH=${JSON.stringify(requestLogPath)} ${envExports} exec ${JSON.stringify(bunExe)} ${JSON.stringify(mockAgentPath)} "$@" `; await writeFile(wrapperPath, script, "utf8"); await chmod(wrapperPath, 0o755); return wrapperPath; } async function readArgvLog(filePath: string) { const raw = await readFile(filePath, "utf8"); return raw .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0) .map((line) => line.split("\t").filter((token) => token.length > 0)); } async function readJsonLines(filePath: string) { const raw = await readFile(filePath, "utf8"); return raw .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0) .map((line) => JSON.parse(line) as Record<string, unknown>); } async function waitForFileContent(filePath: string, attempts = 40) { for (let attempt = 0; attempt < attempts; attempt += 1) { try { const raw = await readFile(filePath, "utf8"); if (raw.trim().length > 0) { return raw; } } catch {} await new Promise((resolve) => setTimeout(resolve, 50)); } throw new Error(`Timed out waiting for file content at ${filePath}`); } const cursorAdapterTestLayer = it.layer( makeCursorAdapterLive().pipe( Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge( ServerConfig.layerTest(process.cwd(), { prefix: "t3code-cursor-adapter-test-", }), ), Layer.provideMerge(NodeServices.layer), ), ); cursorAdapterTestLayer("CursorAdapterLive", (it) => { it.effect("starts a session and maps mock ACP prompt flow to runtime events", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const settings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-mock-thread"); const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper()); yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "default" }, }); assert.equal(session.provider, "cursor"); assert.deepStrictEqual(session.resumeCursor, { schemaVersion: 1, sessionId: "mock-session-1", }); yield* adapter.sendTurn({ threadId, input: "hello mock", attachments: [], }); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); const types = runtimeEvents.map((e) => e.type); for (const t of [ "session.started", "session.state.changed", "thread.started", "turn.started", "turn.plan.updated", "item.started", "content.delta", "item.completed", "turn.completed", ] as const) { assert.include(types, t); } const assistantStarted = runtimeEvents.find( (event) => event.type === "item.started" && event.payload.itemType === "assistant_message", ); assert.isDefined(assistantStarted); const delta = runtimeEvents.find((e) => e.type === "content.delta"); assert.isDefined(delta); if (delta?.type === "content.delta") { assert.equal(delta.payload.delta, "hello from mock"); assert.match(String(delta.itemId), /^assistant:mock-session-1:segment:0$/); } const assistantCompleted = runtimeEvents.find( (event) => event.type === "item.completed" && event.payload.itemType === "assistant_message", ); assert.isDefined(assistantCompleted); const planUpdate = runtimeEvents.find((event) => event.type === "turn.plan.updated"); assert.isDefined(planUpdate); if (planUpdate?.type === "turn.plan.updated") { assert.deepStrictEqual(planUpdate.payload.plan, [ { step: "Inspect mock ACP state", status: "completed" }, { step: "Implement the requested change", status: "inProgress" }, ]); } yield* adapter.stopSession(threadId); }), ); it.effect("closes the ACP child process when a session stops", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const settings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-stop-session-close"); const tempDir = yield* Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "cursor-adapter-exit-log-")), ); const exitLogPath = path.join(tempDir, "exit.log"); const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper({ T3_ACP_EXIT_LOG_PATH: exitLogPath, }), ); yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "default" }, }); yield* adapter.stopSession(threadId); const exitLog = yield* Effect.promise(() => waitForFileContent(exitLogPath)); assert.include(exitLog, "SIGTERM"); }), ); it.effect( "serializes concurrent startSession calls for the same thread and closes the replaced ACP session", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const settings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-concurrent-start-session"); const tempDir = yield* Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "cursor-adapter-concurrent-exit-log-")), ); const exitLogPath = path.join(tempDir, "exit.log"); const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper( { T3_ACP_EXIT_LOG_PATH: exitLogPath, }, { initialDelaySeconds: 0.2 }, ), ); yield* settings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); const [firstSession, secondSession] = yield* Effect.all( [ adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "default" }, }), adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "default" }, }), ], { concurrency: "unbounded" }, ); assert.equal(firstSession.threadId, threadId); assert.equal(secondSession.threadId, threadId); yield* adapter.stopSession(threadId); const exitLog = yield* Effect.promise(() => waitForFileContent(exitLogPath)); assert.equal(exitLog.match(/SIGTERM/g)?.length ?? 0, 2); }), ); it.effect("rejects startSession when provider mismatches", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const result = yield* adapter .startSession({ threadId: ThreadId.make("bad-provider"), provider: "codex", cwd: process.cwd(), runtimeMode: "full-access", }) .pipe(Effect.result); assert.equal(result._tag, "Failure"); }), ); it.effect("maps app plan mode onto the ACP plan session mode", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const serverSettings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-plan-mode-probe"); const tempDir = yield* Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "cursor-acp-"))); const requestLogPath = path.join(tempDir, "requests.ndjson"); const argvLogPath = path.join(tempDir, "argv.txt"); yield* Effect.promise(() => writeFile(requestLogPath, "", "utf8")); const wrapperPath = yield* Effect.promise(() => makeProbeWrapper(requestLogPath, argvLogPath), ); yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } } }); yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection: { provider: "cursor", model: "composer-2" }, }); yield* adapter.sendTurn({ threadId, input: "plan this change", attachments: [], interactionMode: "plan", }); yield* adapter.stopSession(threadId); const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); const modeRequest = requests .toReversed() .find( (entry) => entry.method === "session/set_mode" || (entry.method === "session/set_config_option" && (entry.params as Record<string, unknown> | undefined)?.configId === "mode"), ); assert.isDefined(modeRequest); assert.equal( (modeRequest?.params as Record<string, unknown> | undefined)?.sessionId, "mock-session-1", ); assert.include( ["architect", "plan"], String( (modeRequest?.params as Record<string, unknown> | undefined)?.modeId ?? (modeRequest?.params as Record<string, unknown> | undefined)?.value, ), ); }), ); it.effect( "applies initial model and mode configuration during startSession and skips repeating it on first send", () => Effect.gen(function* () { const adapter = yield* CursorAdapter; const serverSettings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-initial-config-probe"); const tempDir = yield* Effect.promise(() => mkdtemp(path.join(os.tmpdir(), "cursor-acp-"))); const requestLogPath = path.join(tempDir, "requests.ndjson"); const argvLogPath = path.join(tempDir, "argv.txt"); yield* Effect.promise(() => writeFile(requestLogPath, "", "utf8")); const wrapperPath = yield* Effect.promise(() => makeProbeWrapper(requestLogPath, argvLogPath), ); yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } }, }); const modelSelection = createModelSelection("cursor", "gpt-5.4", [ { id: "reasoning", value: "xhigh" }, { id: "contextWindow", value: "1m" }, { id: "fastMode", value: true }, ]); yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "full-access", modelSelection, }); yield* Effect.promise(() => waitForFileContent(requestLogPath)); const requestsAfterStart = yield* Effect.promise(() => readJsonLines(requestLogPath)); const configIdsAfterStart = requestsAfterStart.flatMap((entry) => entry.method === "session/set_config_option" && typeof (entry.params as Record<string, unknown> | undefined)?.configId === "string" ? [String((entry.params as Record<string, unknown>).configId)] : [], ); assert.deepStrictEqual(configIdsAfterStart, [ "model", "reasoning", "context", "fast", "mode", ]); yield* adapter.sendTurn({ threadId, input: "hello mock", attachments: [], modelSelection, interactionMode: "default", }); yield* adapter.stopSession(threadId); const finalRequests = yield* Effect.promise(() => readJsonLines(requestLogPath)); const finalConfigIds = finalRequests.flatMap((entry) => entry.method === "session/set_config_option" && typeof (entry.params as Record<string, unknown> | undefined)?.configId === "string" ? [String((entry.params as Record<string, unknown>).configId)] : [], ); assert.deepStrictEqual(finalConfigIds, ["model", "reasoning", "context", "fast", "mode"]); assert.equal(finalRequests.filter((entry) => entry.method === "session/prompt").length, 1); }), ); it.effect( "streams ACP tool calls and approvals on the active turn in approval-required mode", () => Effect.gen(function* () { const previousEmitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS; process.env.T3_ACP_EMIT_TOOL_CALLS = "1"; const adapter = yield* CursorAdapter; const serverSettings = yield* ServerSettingsService; const threadId = ThreadId.make("cursor-tool-call-probe"); const runtimeEvents: Array<ProviderRuntimeEvent> = []; const settledEventTypes = new Set<string>(); const settledEventsReady = yield* Deferred.make<void>(); const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper({ T3_ACP_EMIT_TOOL_CALLS: "1" }), ); yield* serverSettings.updateSettings({ providers: { cursor: { binaryPath: wrapperPath } }, }); yield* Stream.runForEach(adapter.streamEvents, (event) => Effect.gen(function* () { runtimeEvents.push(event); if (String(event.threadId) !== String(threadId)) { return; } if (event.type === "request.opened" && event.requestId) { yield* adapter.respondToRequest( threadId, ApprovalRequestId.make(String(event.requestId)), "accept", ); } if ( event.type === "turn.completed" || (event.type === "item.completed" && event.payload.itemType === "command_execution") || event.type === "content.delta" ) { settledEventTypes.add(event.type); if (settledEventTypes.size === 3) { yield* Deferred.succeed(settledEventsReady, undefined).pipe(Effect.orDie); } } }), ).pipe(Effect.forkChild); const program = Effect.gen(function* () { yield* adapter.startSession({ threadId, provider: "cursor", cwd: process.cwd(), runtimeMode: "approval-required", modelSelection: { provider: "cursor", model: "default" }, }); const turn = yield* adapter.sendTurn({ threadId, input: "run a tool call", attachments: [], }); yield* Deferred.await(settledEventsReady); const threadEvents = runtimeEvents.filter( (event) => String(event.threadId) === String(threadId), ); assert.includeMembers( threadEvents.map((event) => event.type), [ "session.started", "session.state.changed", "thread.started", "turn.started", "request.opened", "request.resolved", "item.updated", "item.completed", "content.delta", "turn.completed", ], ); const turnEvents = threadEvents.filter( (event) => String(event.turnId) === String(turn.turnId), ); const toolUpdates = turnEvents.filter((event) => event.type === "item.updated"); // ACP updates can arrive either as distinct pending + in-progress events // or as a single coalesced in-progress update before approval resolves.