import * as Path from "effect/Path";import * as AcpError from "./errors.ts";import * as Effect from "effect/Effect";import * as Deferred from "effect/Deferred";import * as Fiber from "effect/Fiber";import * as Queue from "effect/Queue";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import * as Ref from "effect/Ref";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { it, assert } from "@effect/vitest";import * as NodeServices from "@effect/platform-node/NodeServices"; import * as AcpSchema from "./_generated/schema.gen.ts";import * as AcpProtocol from "./protocol.ts";import { encodeJsonl, jsonRpcNotification, jsonRpcRequest, jsonRpcResponse,} from "./_internal/shared.ts";import { makeInMemoryStdio, makeTerminationError, makeChildStdio } from "./_internal/stdio.ts"; const SessionCancelNotification = jsonRpcNotification( "session/cancel", AcpSchema.CancelNotification,);const SessionUpdateNotification = jsonRpcNotification( "session/update", AcpSchema.SessionNotification,);const ElicitationCompleteNotification = jsonRpcNotification( "session/elicitation/complete", AcpSchema.ElicitationCompleteNotification,);const RequestPermissionRequest = jsonRpcRequest( "session/request_permission", AcpSchema.RequestPermissionRequest,);const RequestPermissionResponse = jsonRpcResponse(AcpSchema.RequestPermissionResponse);const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String }));const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean }));const decodeSessionCancelNotification = Schema.decodeEffect( Schema.fromJsonString(SessionCancelNotification),);const decodeExtRequest = Schema.decodeEffect(Schema.fromJsonString(ExtRequest));const decodeRequestPermissionResponse = Schema.decodeEffect( Schema.fromJsonString(RequestPermissionResponse),);const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.UnknownFromJsonString);const encoder = new TextEncoder();const mockPeerPath = Effect.map(Effect.service(Path.Path), (path) => path.join(import.meta.dirname, "../test/fixtures/acp-mock-peer.ts"),);const mockPeerArgs = (path: string) => [path]; const makeHandle = (env?: Record<string, string>) => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const path = yield* Path.Path; const command = ChildProcess.make(process.execPath, mockPeerArgs(yield* mockPeerPath), { cwd: path.join(import.meta.dirname, ".."), ...(env ? { env: { ...process.env, ...env } } : {}), }); return yield* spawner.spawn(command); }); it.layer(NodeServices.layer)("effect-acp protocol", (it) => { it.effect( "emits exact JSON-RPC notifications and decodes inbound session/update and elicitation completion", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const notifications = yield* Deferred.make<ReadonlyArray<AcpProtocol.AcpIncomingNotification>>(); yield* transport.incoming.pipe( Stream.take(2), Stream.runCollect, Effect.flatMap((notificationChunk) => Deferred.succeed(notifications, notificationChunk)), Effect.forkScoped, ); yield* transport.notify("session/cancel", { sessionId: "session-1" }); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeSessionCancelNotification(outbound), { jsonrpc: "2.0", method: "session/cancel", params: { sessionId: "session-1", }, }); yield* Queue.offer( input, yield* encodeJsonl(SessionUpdateNotification, { jsonrpc: "2.0", method: "session/update", params: { sessionId: "session-1", update: { sessionUpdate: "plan", entries: [ { content: "Inspect repository", priority: "high", status: "in_progress", }, ], }, }, }), ); yield* Queue.offer( input, yield* encodeJsonl(ElicitationCompleteNotification, { jsonrpc: "2.0", method: "session/elicitation/complete", params: { elicitationId: "elicitation-1", }, }), ); const [update, completion] = yield* Deferred.await(notifications); assert.equal(update?._tag, "SessionUpdate"); assert.equal(completion?._tag, "ElicitationComplete"); }), ); it.effect("keeps invalid core notification values only in the schema cause", () => Effect.gen(function* () { const secret = "acp-core-notification-secret-sentinel"; const { stdio, input } = yield* makeInMemoryStdio(); const termination = yield* Deferred.make<AcpError.AcpError>(); yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid), }); yield* Queue.offer( input, encoder.encode( `${encodeUnknownJsonString({ jsonrpc: "2.0", method: "session/update", params: { sessionId: { secret }, update: { sessionUpdate: "plan", entries: [], }, }, })}\n`, ), ); const error = yield* Deferred.await(termination); assert.instanceOf(error, AcpError.AcpProtocolParseError); const parseError = error as AcpError.AcpProtocolParseError; const { cause, ...directDiagnostics } = parseError; assert.equal(parseError.operation, "decode-notification-payload"); assert.equal(parseError.method, "session/update"); assert.isAbove(parseError.issueCount ?? 0, 0); assert.include(parseError.issueKinds ?? [], "Pointer"); assert.isAbove(parseError.maximumPathDepth ?? 0, 0); assert.isTrue(Schema.isSchemaError(cause)); assert.notInclude(parseError.message, secret); assert.notInclude(encodeUnknownJsonString(directDiagnostics), secret); }), ); it.effect("logs outgoing notifications when logOutgoing is enabled", () => Effect.gen(function* () { const { stdio } = yield* makeInMemoryStdio(); const events: Array<AcpProtocol.AcpProtocolLogEvent> = []; const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), logOutgoing: true, logger: (event) => Effect.sync(() => { events.push(event); }), }); yield* transport.notify("session/cancel", { sessionId: "session-1" }); assert.deepEqual(events, [ { direction: "outgoing", stage: "decoded", payload: { _tag: "Request", id: "", tag: "session/cancel", payload: { sessionId: "session-1", }, headers: [], }, }, { direction: "outgoing", stage: "raw", payload: '{"jsonrpc":"2.0","method":"session/cancel","params":{"sessionId":"session-1"},"id":"","headers":[]}\n', }, ]); }), ); it.effect("logs decode failures without copying the cause or wire payload", () => Effect.gen(function* () { const secret = "acp-wire-secret-sentinel"; const { stdio, input } = yield* makeInMemoryStdio(); const events: Array<AcpProtocol.AcpProtocolLogEvent> = []; const termination = yield* Deferred.make<AcpError.AcpError>(); yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), logIncoming: true, logger: (event) => Effect.sync(() => { events.push(event); }), onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid), }); yield* Queue.offer(input, encoder.encode(`{"secret":"${secret}"\n`)); yield* Deferred.await(termination); const event = events.find(({ stage }) => stage === "decode_failed"); assert.deepEqual(event, { direction: "incoming", stage: "decode_failed", payload: { operation: "decode-wire-message", }, }); assert.notInclude(encodeUnknownJsonString(event), secret); }), ); it.effect("fails notification encoding through the declared ACP error channel", () => Effect.gen(function* () { const { stdio } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const bigintError = yield* transport.notify("x/test", 1n).pipe(Effect.flip); assert.instanceOf(bigintError, AcpError.AcpProtocolParseError); assert.equal(bigintError.operation, "encode-message"); assert.equal(bigintError.method, "x/test"); assert.instanceOf(bigintError.cause, TypeError); assert.equal( bigintError.message, "ACP protocol operation 'encode-message' failed for method 'x/test'.", ); const circular: Record<string, unknown> = {}; circular.self = circular; const circularError = yield* transport.notify("x/test", circular).pipe(Effect.flip); assert.instanceOf(circularError, AcpError.AcpProtocolParseError); assert.equal(circularError.operation, "encode-message"); assert.equal(circularError.method, "x/test"); assert.instanceOf(circularError.cause, TypeError); const requestError = yield* transport.request("x/request", 1n).pipe( Effect.match({ onFailure: (error) => error, onSuccess: () => assert.fail("Expected request encoding to fail"), }), ); assert.instanceOf(requestError, AcpError.AcpProtocolParseError); assert.deepInclude(requestError, { operation: "encode-message", method: "x/request", requestId: "1", }); }), ); it.effect("supports generic extension requests over the patched transport", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const response = yield* transport .request("x/test", { hello: "world" }) .pipe(Effect.forkScoped); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeExtRequest(outbound), { jsonrpc: "2.0", id: 1, method: "x/test", params: { hello: "world", }, headers: [], }); yield* Queue.offer( input, yield* encodeJsonl(ExtResponse, { jsonrpc: "2.0", id: 1, result: { ok: true, }, }), ); const resolved = yield* Fiber.join(response); assert.deepEqual(resolved, { ok: true }); }), ); it.effect("correlates extension response errors with the originating request", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const response = yield* transport .request("x/private", { hello: "world" }) .pipe(Effect.forkScoped); yield* Queue.take(output); yield* Queue.offer( input, encoder.encode( `${encodeUnknownJsonString({ jsonrpc: "2.0", id: 1, error: { _tag: "Cause", code: -32602, message: "Invalid params", data: [ { _tag: "Fail", error: { code: -32602, message: "Invalid params", data: { field: "hello" }, }, }, ], }, })}\n`, ), ); const error = yield* Fiber.join(response).pipe( Effect.match({ onFailure: (error) => error, onSuccess: () => assert.fail("Expected extension request to fail"), }), ); assert.instanceOf(error, AcpError.AcpRequestError); assert.deepInclude(error, { code: -32602, errorMessage: "Invalid params", method: "x/private", requestId: "1", operation: "receive-response", }); }), ); it.effect("preserves zero-valued ids for inbound core client requests", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(["session/request_permission"]), }); const inboundRequest = yield* Deferred.make<unknown>(); yield* transport.serverProtocol .run((_clientId, message) => Deferred.succeed(inboundRequest, message).pipe(Effect.asVoid)) .pipe(Effect.forkScoped); yield* Queue.offer( input, yield* encodeJsonl(RequestPermissionRequest, { jsonrpc: "2.0", id: 0, method: "session/request_permission", params: { sessionId: "session-1", toolCall: { toolCallId: "tool-1", title: "Allow mock action", }, options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }], }, headers: [], }), ); const message = yield* Deferred.await(inboundRequest); assert.deepEqual(message, { _tag: "Request", id: "0", tag: "session/request_permission", payload: { sessionId: "session-1", toolCall: { toolCallId: "tool-1", title: "Allow mock action", }, options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }], }, headers: [], }); yield* transport.serverProtocol.send(0, { _tag: "Exit", requestId: "0", exit: { _tag: "Success", value: { outcome: { outcome: "selected", optionId: "allow", }, }, }, }); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeRequestPermissionResponse(outbound), { jsonrpc: "2.0", id: 0, result: { outcome: { outcome: "selected", optionId: "allow", }, }, }); }), ); it.effect("cleans up interrupted extension requests before a late response arrives", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const lateResponse = yield* Deferred.make<unknown>(); yield* transport.clientProtocol .run(0, (message) => Deferred.succeed(lateResponse, message).pipe(Effect.asVoid)) .pipe(Effect.forkScoped); const response = yield* transport .request("x/test", { hello: "world" }) .pipe(Effect.forkScoped); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeExtRequest(outbound), { jsonrpc: "2.0", id: 1, method: "x/test", params: { hello: "world", }, headers: [], }); yield* Fiber.interrupt(response); yield* Queue.offer( input, yield* encodeJsonl(ExtResponse, { jsonrpc: "2.0", id: 1, result: { ok: true, }, }), ); const message = yield* Deferred.await(lateResponse); assert.deepEqual(message, {Lines 1–500 of 652. Load more
import * as Path from "effect/Path"; import * as AcpError from "./errors.ts"; import * as Effect from "effect/Effect"; import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as Ref from "effect/Ref"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { it, assert } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as AcpSchema from "./_generated/schema.gen.ts"; import * as AcpProtocol from "./protocol.ts"; import { encodeJsonl, jsonRpcNotification, jsonRpcRequest, jsonRpcResponse, } from "./_internal/shared.ts"; import { makeInMemoryStdio, makeTerminationError, makeChildStdio } from "./_internal/stdio.ts"; const SessionCancelNotification = jsonRpcNotification( "session/cancel", AcpSchema.CancelNotification, ); const SessionUpdateNotification = jsonRpcNotification( "session/update", AcpSchema.SessionNotification, ); const ElicitationCompleteNotification = jsonRpcNotification( "session/elicitation/complete", AcpSchema.ElicitationCompleteNotification, ); const RequestPermissionRequest = jsonRpcRequest( "session/request_permission", AcpSchema.RequestPermissionRequest, ); const RequestPermissionResponse = jsonRpcResponse(AcpSchema.RequestPermissionResponse); const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String })); const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean })); const decodeSessionCancelNotification = Schema.decodeEffect( Schema.fromJsonString(SessionCancelNotification), ); const decodeExtRequest = Schema.decodeEffect(Schema.fromJsonString(ExtRequest)); const decodeRequestPermissionResponse = Schema.decodeEffect( Schema.fromJsonString(RequestPermissionResponse), ); const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); const encoder = new TextEncoder(); const mockPeerPath = Effect.map(Effect.service(Path.Path), (path) => path.join(import.meta.dirname, "../test/fixtures/acp-mock-peer.ts"), ); const mockPeerArgs = (path: string) => [path]; const makeHandle = (env?: Record<string, string>) => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const path = yield* Path.Path; const command = ChildProcess.make(process.execPath, mockPeerArgs(yield* mockPeerPath), { cwd: path.join(import.meta.dirname, ".."), ...(env ? { env: { ...process.env, ...env } } : {}), }); return yield* spawner.spawn(command); }); it.layer(NodeServices.layer)("effect-acp protocol", (it) => { it.effect( "emits exact JSON-RPC notifications and decodes inbound session/update and elicitation completion", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const notifications = yield* Deferred.make<ReadonlyArray<AcpProtocol.AcpIncomingNotification>>(); yield* transport.incoming.pipe( Stream.take(2), Stream.runCollect, Effect.flatMap((notificationChunk) => Deferred.succeed(notifications, notificationChunk)), Effect.forkScoped, ); yield* transport.notify("session/cancel", { sessionId: "session-1" }); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeSessionCancelNotification(outbound), { jsonrpc: "2.0", method: "session/cancel", params: { sessionId: "session-1", }, }); yield* Queue.offer( input, yield* encodeJsonl(SessionUpdateNotification, { jsonrpc: "2.0", method: "session/update", params: { sessionId: "session-1", update: { sessionUpdate: "plan", entries: [ { content: "Inspect repository", priority: "high", status: "in_progress", }, ], }, }, }), ); yield* Queue.offer( input, yield* encodeJsonl(ElicitationCompleteNotification, { jsonrpc: "2.0", method: "session/elicitation/complete", params: { elicitationId: "elicitation-1", }, }), ); const [update, completion] = yield* Deferred.await(notifications); assert.equal(update?._tag, "SessionUpdate"); assert.equal(completion?._tag, "ElicitationComplete"); }), ); it.effect("keeps invalid core notification values only in the schema cause", () => Effect.gen(function* () { const secret = "acp-core-notification-secret-sentinel"; const { stdio, input } = yield* makeInMemoryStdio(); const termination = yield* Deferred.make<AcpError.AcpError>(); yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid), }); yield* Queue.offer( input, encoder.encode( `${encodeUnknownJsonString({ jsonrpc: "2.0", method: "session/update", params: { sessionId: { secret }, update: { sessionUpdate: "plan", entries: [], }, }, })}\n`, ), ); const error = yield* Deferred.await(termination); assert.instanceOf(error, AcpError.AcpProtocolParseError); const parseError = error as AcpError.AcpProtocolParseError; const { cause, ...directDiagnostics } = parseError; assert.equal(parseError.operation, "decode-notification-payload"); assert.equal(parseError.method, "session/update"); assert.isAbove(parseError.issueCount ?? 0, 0); assert.include(parseError.issueKinds ?? [], "Pointer"); assert.isAbove(parseError.maximumPathDepth ?? 0, 0); assert.isTrue(Schema.isSchemaError(cause)); assert.notInclude(parseError.message, secret); assert.notInclude(encodeUnknownJsonString(directDiagnostics), secret); }), ); it.effect("logs outgoing notifications when logOutgoing is enabled", () => Effect.gen(function* () { const { stdio } = yield* makeInMemoryStdio(); const events: Array<AcpProtocol.AcpProtocolLogEvent> = []; const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), logOutgoing: true, logger: (event) => Effect.sync(() => { events.push(event); }), }); yield* transport.notify("session/cancel", { sessionId: "session-1" }); assert.deepEqual(events, [ { direction: "outgoing", stage: "decoded", payload: { _tag: "Request", id: "", tag: "session/cancel", payload: { sessionId: "session-1", }, headers: [], }, }, { direction: "outgoing", stage: "raw", payload: '{"jsonrpc":"2.0","method":"session/cancel","params":{"sessionId":"session-1"},"id":"","headers":[]}\n', }, ]); }), ); it.effect("logs decode failures without copying the cause or wire payload", () => Effect.gen(function* () { const secret = "acp-wire-secret-sentinel"; const { stdio, input } = yield* makeInMemoryStdio(); const events: Array<AcpProtocol.AcpProtocolLogEvent> = []; const termination = yield* Deferred.make<AcpError.AcpError>(); yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), logIncoming: true, logger: (event) => Effect.sync(() => { events.push(event); }), onTermination: (error) => Deferred.succeed(termination, error).pipe(Effect.asVoid), }); yield* Queue.offer(input, encoder.encode(`{"secret":"${secret}"\n`)); yield* Deferred.await(termination); const event = events.find(({ stage }) => stage === "decode_failed"); assert.deepEqual(event, { direction: "incoming", stage: "decode_failed", payload: { operation: "decode-wire-message", }, }); assert.notInclude(encodeUnknownJsonString(event), secret); }), ); it.effect("fails notification encoding through the declared ACP error channel", () => Effect.gen(function* () { const { stdio } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const bigintError = yield* transport.notify("x/test", 1n).pipe(Effect.flip); assert.instanceOf(bigintError, AcpError.AcpProtocolParseError); assert.equal(bigintError.operation, "encode-message"); assert.equal(bigintError.method, "x/test"); assert.instanceOf(bigintError.cause, TypeError); assert.equal( bigintError.message, "ACP protocol operation 'encode-message' failed for method 'x/test'.", ); const circular: Record<string, unknown> = {}; circular.self = circular; const circularError = yield* transport.notify("x/test", circular).pipe(Effect.flip); assert.instanceOf(circularError, AcpError.AcpProtocolParseError); assert.equal(circularError.operation, "encode-message"); assert.equal(circularError.method, "x/test"); assert.instanceOf(circularError.cause, TypeError); const requestError = yield* transport.request("x/request", 1n).pipe( Effect.match({ onFailure: (error) => error, onSuccess: () => assert.fail("Expected request encoding to fail"), }), ); assert.instanceOf(requestError, AcpError.AcpProtocolParseError); assert.deepInclude(requestError, { operation: "encode-message", method: "x/request", requestId: "1", }); }), ); it.effect("supports generic extension requests over the patched transport", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const response = yield* transport .request("x/test", { hello: "world" }) .pipe(Effect.forkScoped); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeExtRequest(outbound), { jsonrpc: "2.0", id: 1, method: "x/test", params: { hello: "world", }, headers: [], }); yield* Queue.offer( input, yield* encodeJsonl(ExtResponse, { jsonrpc: "2.0", id: 1, result: { ok: true, }, }), ); const resolved = yield* Fiber.join(response); assert.deepEqual(resolved, { ok: true }); }), ); it.effect("correlates extension response errors with the originating request", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const response = yield* transport .request("x/private", { hello: "world" }) .pipe(Effect.forkScoped); yield* Queue.take(output); yield* Queue.offer( input, encoder.encode( `${encodeUnknownJsonString({ jsonrpc: "2.0", id: 1, error: { _tag: "Cause", code: -32602, message: "Invalid params", data: [ { _tag: "Fail", error: { code: -32602, message: "Invalid params", data: { field: "hello" }, }, }, ], }, })}\n`, ), ); const error = yield* Fiber.join(response).pipe( Effect.match({ onFailure: (error) => error, onSuccess: () => assert.fail("Expected extension request to fail"), }), ); assert.instanceOf(error, AcpError.AcpRequestError); assert.deepInclude(error, { code: -32602, errorMessage: "Invalid params", method: "x/private", requestId: "1", operation: "receive-response", }); }), ); it.effect("preserves zero-valued ids for inbound core client requests", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(["session/request_permission"]), }); const inboundRequest = yield* Deferred.make<unknown>(); yield* transport.serverProtocol .run((_clientId, message) => Deferred.succeed(inboundRequest, message).pipe(Effect.asVoid)) .pipe(Effect.forkScoped); yield* Queue.offer( input, yield* encodeJsonl(RequestPermissionRequest, { jsonrpc: "2.0", id: 0, method: "session/request_permission", params: { sessionId: "session-1", toolCall: { toolCallId: "tool-1", title: "Allow mock action", }, options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }], }, headers: [], }), ); const message = yield* Deferred.await(inboundRequest); assert.deepEqual(message, { _tag: "Request", id: "0", tag: "session/request_permission", payload: { sessionId: "session-1", toolCall: { toolCallId: "tool-1", title: "Allow mock action", }, options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }], }, headers: [], }); yield* transport.serverProtocol.send(0, { _tag: "Exit", requestId: "0", exit: { _tag: "Success", value: { outcome: { outcome: "selected", optionId: "allow", }, }, }, }); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeRequestPermissionResponse(outbound), { jsonrpc: "2.0", id: 0, result: { outcome: { outcome: "selected", optionId: "allow", }, }, }); }), ); it.effect("cleans up interrupted extension requests before a late response arrives", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), }); const lateResponse = yield* Deferred.make<unknown>(); yield* transport.clientProtocol .run(0, (message) => Deferred.succeed(lateResponse, message).pipe(Effect.asVoid)) .pipe(Effect.forkScoped); const response = yield* transport .request("x/test", { hello: "world" }) .pipe(Effect.forkScoped); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeExtRequest(outbound), { jsonrpc: "2.0", id: 1, method: "x/test", params: { hello: "world", }, headers: [], }); yield* Fiber.interrupt(response); yield* Queue.offer( input, yield* encodeJsonl(ExtResponse, { jsonrpc: "2.0", id: 1, result: { ok: true, }, }), ); const message = yield* Deferred.await(lateResponse); assert.deepEqual(message, {