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 decodeExtResponse = Schema.decodeEffect(Schema.fromJsonString(ExtResponse));const decodeRequestPermissionResponse = Schema.decodeEffect( Schema.fromJsonString(RequestPermissionResponse),);const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));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 only recent raw notifications after their callbacks run", () => Effect.gen(function* () { const { stdio, input } = yield* makeInMemoryStdio(); const handled = yield* Deferred.make<void>(); let handledCount = 0; const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), onNotification: () => Effect.sync(() => ++handledCount).pipe( Effect.flatMap((count) => count === 64 ? Deferred.succeed(handled, undefined).pipe(Effect.asVoid) : Effect.void, ), ), }); const messages = Array.from({ length: 64 }, (_, index) => encodeUnknownJsonString({ jsonrpc: "2.0", method: "x/performance", params: { index }, }), ); yield* Queue.offer(input, encoder.encode(`${messages.join("\n")}\n`)); yield* Deferred.await(handled); const retained = yield* transport.incoming.pipe(Stream.take(32), Stream.runCollect); assert.equal(handledCount, 64); assert.equal(retained.length, 32); assert.deepEqual(retained[0]?.params, { index: 32 }); assert.deepEqual(retained[31]?.params, { index: 63 }); }), ); 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>(); let transformCalls = 0; yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), transformSessionUpdate: (notification) => { transformCalls += 1; return notification; }, 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.equal(transformCalls, 0); 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" }); // A notification must not carry `id` or `headers`. Grok CLI drops frames that do. assert.deepEqual(events, [ { direction: "outgoing", stage: "decoded", payload: { _tag: "Notification", tag: "session/cancel", payload: { sessionId: "session-1", }, }, }, { direction: "outgoing", stage: "raw", payload: '{"jsonrpc":"2.0","method":"session/cancel","params":{"sessionId":"session-1"}}\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(), }); // Notifications encode through Schema, so the cause is the schema failure rather // than the raw TypeError JSON.stringify throws. The ACP error shape is what callers see. 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.isDefined(bigintError.cause); 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.isDefined(circularError.cause); 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 numeric ids for inbound extension requests", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), onExtRequest: () => Effect.succeed({ ok: true }), }); yield* Queue.offer( input, yield* encodeJsonl(ExtRequest, { jsonrpc: "2.0", id: 7, method: "x/test", params: { hello: "world", }, headers: [], }), ); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeExtResponse(outbound), { jsonrpc: "2.0", id: 7, result: { ok: true, }, }); }), ); 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",Lines 1–500 of 770. 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 decodeExtResponse = Schema.decodeEffect(Schema.fromJsonString(ExtResponse)); const decodeRequestPermissionResponse = Schema.decodeEffect( Schema.fromJsonString(RequestPermissionResponse), ); const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); 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 only recent raw notifications after their callbacks run", () => Effect.gen(function* () { const { stdio, input } = yield* makeInMemoryStdio(); const handled = yield* Deferred.make<void>(); let handledCount = 0; const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), onNotification: () => Effect.sync(() => ++handledCount).pipe( Effect.flatMap((count) => count === 64 ? Deferred.succeed(handled, undefined).pipe(Effect.asVoid) : Effect.void, ), ), }); const messages = Array.from({ length: 64 }, (_, index) => encodeUnknownJsonString({ jsonrpc: "2.0", method: "x/performance", params: { index }, }), ); yield* Queue.offer(input, encoder.encode(`${messages.join("\n")}\n`)); yield* Deferred.await(handled); const retained = yield* transport.incoming.pipe(Stream.take(32), Stream.runCollect); assert.equal(handledCount, 64); assert.equal(retained.length, 32); assert.deepEqual(retained[0]?.params, { index: 32 }); assert.deepEqual(retained[31]?.params, { index: 63 }); }), ); 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>(); let transformCalls = 0; yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), transformSessionUpdate: (notification) => { transformCalls += 1; return notification; }, 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.equal(transformCalls, 0); 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" }); // A notification must not carry `id` or `headers`. Grok CLI drops frames that do. assert.deepEqual(events, [ { direction: "outgoing", stage: "decoded", payload: { _tag: "Notification", tag: "session/cancel", payload: { sessionId: "session-1", }, }, }, { direction: "outgoing", stage: "raw", payload: '{"jsonrpc":"2.0","method":"session/cancel","params":{"sessionId":"session-1"}}\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(), }); // Notifications encode through Schema, so the cause is the schema failure rather // than the raw TypeError JSON.stringify throws. The ACP error shape is what callers see. 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.isDefined(bigintError.cause); 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.isDefined(circularError.cause); 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 numeric ids for inbound extension requests", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(), onExtRequest: () => Effect.succeed({ ok: true }), }); yield* Queue.offer( input, yield* encodeJsonl(ExtRequest, { jsonrpc: "2.0", id: 7, method: "x/test", params: { hello: "world", }, headers: [], }), ); const outbound = yield* Queue.take(output); assert.deepEqual(yield* decodeExtResponse(outbound), { jsonrpc: "2.0", id: 7, result: { ok: true, }, }); }), ); 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",