blob: 109b064b1028d5d3b74de6fb30d738d67c46c50d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { PassThrough, Readable, Writable } from 'node:stream';
import { describe, test } from 'node:test';
import type { InteractionRequest } from '@maka/core/interaction';
import type { StoredMessage } from '@maka/core/session';
import {
SESSION_CONTINUITY_SCHEMA_VERSION,
type InteractionPendingSnapshot,
type SessionCatalogProjection,
type SessionContinuitySnapshot,
type SubscriptionFrame,
} from '@maka/runtime-host/protocol';
import {
createRuntimeHostReconnectingConnection,
isRuntimeHostReconnectingConnection,
RuntimeHostOperationError,
RuntimeHostRequestInterruptedError,
RuntimeHostSubscriptionError,
type RuntimeHostConnection,
type RuntimeHostSessionSubscription,
} from '@maka/runtime-host/client';
import { runMakaAcpStdioServer } from '../acp/stdio-server.js';
describe('Maka ACP stdio server', () => {
for (const scenario of [
'recovery',
'question',
'form',
'permission',
'sandbox_boundary',
'client_capability',
] as const) {
test(`prompt through stdio handles ${scenario}`, { timeout: 5_000 }, async () => {
const stdin = new PassThrough();
let created: SessionCatalogProjection | undefined;
let root: NonNullable<SessionContinuitySnapshot['rootTurn']> | undefined;
let first: FakeSubscription | undefined;
let opens = 0;
let pending: InteractionPendingSnapshot | undefined;
const stops: unknown[] = [];
const snapshot = (projectionRevision = 1): SessionContinuitySnapshot =>
continuitySnapshot({
sessionId: created!.id,
projectionRevision,
rootTurn: root ?? null,
status: 'running',
});
const connection = {
request: async (operation: string, input: { sessionId: string; turnId: string }) => {
if (operation === 'session.create')
return (created = sessionProjection({ id: input.sessionId }));
if (operation === 'connection.catalog.query') return connectionCatalogPage();
if (operation === 'session.catalog.query') return { kind: 'session', session: created };
if (operation === 'interaction.query') return pending;
if (operation === 'turn.start') {
root = {
sessionId: input.sessionId,
turnId: input.turnId,
runId: 'run-1',
status: 'running',
};
return { kind: 'started', turn: root };
}
if (operation === 'turn.stop') {
stops.push(input);
return {};
}
assert.fail(`Unexpected operation: ${operation}`);
},
openSessionSubscription: async () => {
opens += 1;
if (opens === 1) return (first = new FakeSubscription(snapshot(), Promise.resolve([])));
const replacement = new FakeSubscription(
snapshot(3),
Promise.resolve([]),
'subscription-2',
);
replacement.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-2',
sequence: 1,
snapshot: {
...snapshot(4),
rootTurn: {
sessionId: root!.sessionId,
turnId: root!.turnId,
runId: root!.runId,
status: 'completed',
terminalEventId: 'terminal-1',
},
},
});
return replacement;
},
close: async () => undefined,
} as unknown as RuntimeHostConnection;
const harness = createHarness([], { stdin, connection });
const run = harness.run();
const response = (id: number) =>
(
harness.stdoutMessages() as Array<{
id: number;
result?: { sessionId?: string; stopReason?: string };
error?: { data?: { code?: string; kind?: string } };
}>
).find((message) => message.id === id);
const send = (id: number, method: string, params: unknown) =>
stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
try {
send(1, 'session/new', { cwd: '/workspace', mcpServers: [] });
await waitFor(() => Boolean(response(1)));
assert.ok(response(1)?.result?.sessionId);
send(2, 'session/prompt', {
sessionId: created!.id,
prompt: [{ type: 'text', text: 'Hello' }],
});
await waitFor(() => Boolean(root));
first!.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: snapshot(2),
});
if (scenario === 'recovery') {
first!.push({
kind: 'subscription.closed',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 2,
reason: 'slow_consumer',
});
await waitFor(() => Boolean(response(2)));
assert.deepEqual(response(2)?.result, { stopReason: 'end_turn' });
assert.equal(opens, 2);
assert.deepEqual(stops, []);
} else {
pending = {
schemaVersion: 1,
interactionId: 'question-1',
...root!,
revision: 1,
status: 'pending',
outcome: null,
request: unsupportedRequests[scenario],
};
first!.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 2,
snapshot: {
...snapshot(3),
interactions: {
pending: [pending],
},
},
});
if (
scenario === 'permission' ||
scenario === 'sandbox_boundary' ||
scenario === 'client_capability'
) {
const request = () =>
(harness.stdoutMessages() as Array<{ id: string; method?: string }>).find(
(message) => message.method === 'session/request_permission',
);
await waitFor(() => Boolean(request()));
stdin.write(
`${JSON.stringify({ jsonrpc: '2.0', id: request()!.id, error: { code: -32601, message: 'Unsupported client method' } })}\n`,
);
}
await waitFor(() => Boolean(response(2)));
assert.equal(response(2)?.error?.data?.code, 'unsupported_interaction');
assert.equal(response(2)?.error?.data?.kind, scenario);
assert.deepEqual(stops, [
{ sessionId: root!.sessionId, turnId: root!.turnId, runId: root!.runId },
]);
}
} finally {
stdin.end();
await run;
}
});
}
for (const { queryOutcome, recovery } of [
{ queryOutcome: 'pending', recovery: 'running' },
{ queryOutcome: 'pending', recovery: 'absent' },
{ queryOutcome: 'internal_failure', recovery: 'running' },
{ queryOutcome: 'internal_failure', recovery: 'held_empty' },
{ queryOutcome: 'internal_failure', recovery: 'terminal' },
{ queryOutcome: 'internal_failure', recovery: 'absent' },
{ queryOutcome: 'internal_failure', recovery: 'other_turn' },
{ queryOutcome: 'internal_failure', recovery: 'failed' },
{ queryOutcome: 'not_found', recovery: 'none' },
] as const) {
test(`settles outcome-unknown ACP cancellation with ${queryOutcome} query and ${recovery} recovery`, {
timeout: 5_000,
}, async (t) => {
// Drive admission retries explicitly; unrelated test load must not let
// a backoff timer expose query facts before this case releases recovery.
t.mock.timers.enable({ apis: ['setTimeout'] });
const stdin = new PassThrough();
let sessionId: string | undefined;
let session: SessionCatalogProjection | undefined;
let admitted:
| {
sessionId: string;
turnId: string;
runId: string;
status: 'running';
}
| undefined;
let rejectStart!: (error: Error) => void;
const start = new Promise<never>((_resolve, reject) => {
rejectStart = reject;
});
let releaseRecovery!: (messages: StoredMessage[]) => void;
const recoveryTranscript = new Promise<StoredMessage[]>((resolve) => {
releaseRecovery = resolve;
});
let releaseFailedRecoveryQuery!: () => void;
const failedRecoveryQuery = new Promise<void>((resolve) => {
releaseFailedRecoveryQuery = resolve;
});
let first: FakeSubscription | undefined;
let opens = 0;
let turnQueries = 0;
let queryTimeoutMs: number | undefined;
let rejectPendingQuery: ((error: Error) => void) | undefined;
const stops: unknown[] = [];
const snapshot = (
projectionRevision: number,
rootTurn: SessionContinuitySnapshot['rootTurn'],
): SessionContinuitySnapshot =>
continuitySnapshot({ sessionId: sessionId!, projectionRevision, rootTurn });
const connection = {
request: async (
operation: string,
input: { sessionId: string; turnId: string },
timeoutMs?: number,
) => {
if (operation === 'session.create') {
sessionId = input.sessionId;
return (session = sessionProjection({ id: sessionId }));
}
if (operation === 'connection.catalog.query') return connectionCatalogPage();
if (operation === 'session.catalog.query') return { kind: 'session', session };
if (operation === 'turn.start') {
admitted = {
sessionId: input.sessionId,
turnId: input.turnId,
runId: 'run-unknown-start',
status: 'running',
};
return start;
}
if (operation === 'turn.query') {
turnQueries += 1;
if (turnQueries > 1) {
if (recovery === 'held_empty') return admitted;
assert.ok(
recovery === 'absent' || recovery === 'other_turn' || recovery === 'failed',
);
if (recovery === 'failed') await failedRecoveryQuery;
throw new RuntimeHostOperationError(
'turn.query',
'not_found',
'Turn was not admitted',
);
}
if (queryOutcome !== 'pending') {
throw new RuntimeHostOperationError('turn.query', queryOutcome, 'Query failed');
}
assert.ok(timeoutMs !== undefined && timeoutMs > 0, 'admission query needs a deadline');
queryTimeoutMs = timeoutMs;
return new Promise<never>((_resolve, reject) => {
const timer = setTimeout(() => {
rejectPendingQuery?.(
new RuntimeHostRequestInterruptedError(
'turn.query',
'query',
'dispatched',
'timeout',
),
);
}, timeoutMs);
rejectPendingQuery = (error) => {
clearTimeout(timer);
rejectPendingQuery = undefined;
reject(error);
};
});
}
if (operation === 'turn.stop') {
stops.push(input);
return {};
}
assert.fail(`Unexpected operation: ${operation}`);
},
openSessionSubscription: async () => {
opens += 1;
if (opens === 1) {
first = new FakeSubscription(snapshot(1, null), Promise.resolve([]));
return first;
}
assert.ok(admitted);
if (recovery === 'failed') throw new Error('Session attachment permanently failed');
const root: SessionContinuitySnapshot['rootTurn'] =
recovery === 'absent' || recovery === 'held_empty'
? null
: recovery === 'terminal'
? { ...admitted, status: 'completed', terminalEventId: 'terminal-unknown-start' }
: recovery === 'other_turn'
? { ...admitted, turnId: 'unrelated-turn', runId: 'unrelated-run' }
: admitted;
return new FakeSubscription(
snapshot(2, root),
recovery === 'held_empty' ? recoveryTranscript : Promise.resolve([]),
'subscription-recovered',
);
},
close: async () => {
rejectPendingQuery?.(
new RuntimeHostRequestInterruptedError(
'turn.query',
'query',
'dispatched',
'connection_lost',
),
);
},
} as unknown as RuntimeHostConnection;
const harness = createHarness([], { stdin, connection });
const run = harness.run();
const response = () =>
(
harness.stdoutMessages() as Array<{
id?: number;
result?: { stopReason?: string };
}>
).find(({ id }) => id === 2);
const send = (value: unknown) => stdin.write(`${JSON.stringify(value)}\n`);
const startRecovery = () =>
first!.push({
kind: 'subscription.closed',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
reason: 'slow_consumer',
});
try {
send({
jsonrpc: '2.0',
id: 1,
method: 'session/new',
params: { cwd: '/workspace', mcpServers: [] },
});
await waitFor(() =>
harness.stdoutMessages().some((message) => (message as { id?: number }).id === 1),
);
send({
jsonrpc: '2.0',
id: 2,
method: 'session/prompt',
params: { sessionId: sessionId!, prompt: [{ type: 'text', text: 'Hello' }] },
});
await waitFor(() => Boolean(admitted && first));
send({
jsonrpc: '2.0',
method: 'session/cancel',
params: { sessionId: sessionId! },
});
await new Promise((resolve) => setImmediate(resolve));
if (recovery === 'held_empty') {
// The replacement snapshot precedes the lost start reply; hydration completes later.
startRecovery();
await waitFor(() => opens === 2);
}
rejectStart(
new RuntimeHostRequestInterruptedError(
'turn.start',
'command',
'dispatched',
'connection_lost',
),
);
await waitFor(() => turnQueries === 1);
if (recovery !== 'none') {
await new Promise((resolve) => setImmediate(resolve));
assert.equal(response(), undefined, 'cancellation must wait for the channel recovery');
if (recovery === 'held_empty') releaseRecovery([]);
else startRecovery();
}
if (queryOutcome === 'pending' && recovery === 'absent') {
await waitFor(() => opens === 2);
assert.ok(queryTimeoutMs);
// The recovered empty snapshot cannot settle a request still in flight.
// Honour the transport deadline, then advance the bounded retry delay.
for (let attempt = 0; attempt < 3 && !response(); attempt += 1) {
t.mock.timers.tick(queryTimeoutMs);
await new Promise((resolve) => setImmediate(resolve));
}
}
if (recovery === 'failed') {
await waitFor(() => turnQueries === 2);
assert.equal(response(), undefined, 'subscription failure does not establish absence');
releaseFailedRecoveryQuery();
}
await waitFor(() => Boolean(response()));
assert.equal(opens, recovery === 'none' ? 1 : 2);
assert.deepEqual(
stops,
recovery === 'running' || recovery === 'held_empty'
? [{ sessionId: admitted!.sessionId, turnId: admitted!.turnId, runId: admitted!.runId }]
: [],
);
assert.equal(
turnQueries,
recovery === 'absent' ||
recovery === 'other_turn' ||
recovery === 'held_empty' ||
recovery === 'failed'
? 2
: 1,
);
assert.deepEqual(response()?.result, { stopReason: 'cancelled' });
} finally {
releaseRecovery([]);
releaseFailedRecoveryQuery();
stdin.end();
await run;
}
});
}
test('publishes a local configuration commit before a newer external revision', {
timeout: 5_000,
}, async () => {
const stdin = new PassThrough();
let sessionId: string | undefined;
let initial: SessionCatalogProjection | undefined;
let committed: SessionCatalogProjection | undefined;
let external: SessionCatalogProjection | undefined;
let turn:
| {
sessionId: string;
turnId: string;
runId: string;
status: 'running';
}
| undefined;
const snapshot = (
projectionRevision: number,
metadataRevision: number,
rootTurn: SessionContinuitySnapshot['rootTurn'],
): SessionContinuitySnapshot =>
continuitySnapshot({ sessionId: sessionId!, projectionRevision, metadataRevision, rootTurn });
let releaseLocalProjection!: (catalog: ReturnType<typeof connectionCatalogPage>) => void;
const localProjection = new Promise<ReturnType<typeof connectionCatalogPage>>((resolve) => {
releaseLocalProjection = resolve;
});
let catalogReads = 0;
let sessionReads = 0;
let subscription: FakeSubscription | undefined;
const connection = {
request: async (operation: string, input: unknown) => {
if (operation === 'session.create') {
sessionId = (input as { sessionId: string }).sessionId;
initial = sessionProjection({ id: sessionId });
committed = sessionProjection({
id: sessionId,
revision: 2,
permissionMode: 'bypass',
});
external = sessionProjection({
id: sessionId,
revision: 3,
permissionMode: 'ask',
});
return initial;
}
if (operation === 'connection.catalog.query') {
catalogReads += 1;
return catalogReads === 2 ? localProjection : connectionCatalogPage();
}
if (operation === 'session.catalog.query') {
sessionReads += 1;
return { kind: 'session', session: sessionReads === 1 ? initial! : external! };
}
if (operation === 'session.configuration.update') {
assert.deepEqual(input, {
sessionId: sessionId!,
expectedRevision: 1,
patch: { permissionMode: 'bypass' },
});
return { kind: 'committed', session: committed! };
}
if (operation === 'turn.start') {
const request = input as { sessionId: string; turnId: string };
turn = {
sessionId: request.sessionId,
turnId: request.turnId,
runId: 'run-configuration-order',
status: 'running',
};
return { kind: 'started', turn };
}
assert.fail(`Unexpected operation: ${operation}`);
},
openSessionSubscription: async () => {
subscription = new FakeSubscription(snapshot(1, 1, null), Promise.resolve([]));
return subscription;
},
close: async () => undefined,
} as unknown as RuntimeHostConnection;
const harness = createHarness([], { stdin, connection });
const run = harness.run();
const send = (id: number, method: string, params: unknown) =>
stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
const response = (id: number) =>
(
harness.stdoutMessages() as Array<{
id?: number;
result?: { configOptions?: Array<{ id?: string; currentValue?: string }> };
}>
).find((message) => message.id === id);
const configurationUpdates = () =>
(
harness.stdoutMessages() as Array<{
method?: string;
params?: {
update?: {
sessionUpdate?: string;
configOptions?: Array<{ id?: string; currentValue?: string }>;
};
};
}>
).filter(
(message) =>
message.method === 'session/update' &&
message.params?.update?.sessionUpdate === 'config_option_update',
);
let sequence = 0;
let terminalPushed = false;
const pushSnapshot = (next: SessionContinuitySnapshot) => {
subscription!.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: ++sequence,
snapshot: next,
});
};
try {
send(1, 'session/new', { cwd: '/workspace', mcpServers: [] });
await waitFor(() => Boolean(response(1)));
send(2, 'session/prompt', {
sessionId: sessionId!,
prompt: [{ type: 'text', text: 'Attach this Session' }],
});
await waitFor(() => Boolean(turn && subscription));
send(3, 'session/set_config_option', {
sessionId: sessionId!,
configId: 'permission_mode',
value: 'bypass',
});
await waitFor(() => catalogReads === 2);
pushSnapshot(snapshot(2, 3, turn!));
await waitFor(() => subscription!.nextCalls >= 2);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(configurationUpdates(), []);
releaseLocalProjection(connectionCatalogPage());
await waitFor(() => configurationUpdates().length === 2 && Boolean(response(3)));
assert.deepEqual(
configurationUpdates().map(
({ params }) =>
params?.update?.configOptions?.find(({ id }) => id === 'permission_mode')?.currentValue,
),
['bypass', 'ask'],
);
assert.equal(
response(3)?.result?.configOptions?.find(({ id }) => id === 'permission_mode')
?.currentValue,
'bypass',
);
assert.equal(sessionReads, 2);
terminalPushed = true;
pushSnapshot(
snapshot(3, 3, {
...turn!,
status: 'completed',
terminalEventId: 'terminal-configuration-order',
}),
);
await waitFor(() => Boolean(response(2)));
} finally {
releaseLocalProjection(connectionCatalogPage());
if (subscription && turn && !terminalPushed) {
pushSnapshot(
snapshot(3, 3, {
...turn,
status: 'completed',
terminalEventId: 'terminal-configuration-order',
}),
);
}
stdin.end();
await run;
}
});
test('answers initialize without connecting a Runtime Host', async () => {
const harness = createHarness([
`${JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: 1 },
})}\n`,
]);
assert.equal(await harness.run(), 0);
assert.deepEqual(harness.stdoutMessages(), [
{
jsonrpc: '2.0',
id: 1,
result: {
protocolVersion: 1,
agentCapabilities: { sessionCapabilities: { list: {}, close: {} } },
authMethods: [],
agentInfo: { name: 'maka', title: 'Maka', version: '0.2.0' },
},
},
]);
assert.equal(harness.connectCalls(), 0);
});
test('EOF aborts a first attachment waiting for real connection recovery during hydration', async () => {
const stdin = new PassThrough();
let sessionId: string | undefined;
let subscription: FakeSubscription | undefined;
let disconnect!: () => void;
const closed = new Promise<void>((resolve) => {
disconnect = resolve;
});
let rejectTranscript!: (error: Error) => void;
const transcript = new Promise<StoredMessage[]>((_resolve, reject) => {
rejectTranscript = reject;
});
let reconnectSignal: AbortSignal | undefined;
let turnStarts = 0;
const initial = {
rootId: 'root-1',
hostEpoch: 'host-1',
connectionId: 'connection-1',
selectedProtocol: 0,
compositionId: 'maka.interactive',
compositionRevision: '1',
closed,
request: async (operation: string, input: { sessionId: string }) => {
if (operation === 'session.create') {
sessionId = input.sessionId;
return sessionProjection({ id: sessionId });
}
if (operation === 'connection.catalog.query') return connectionCatalogPage();
if (operation === 'turn.start') turnStarts += 1;
assert.fail(`Unexpected operation ${operation}`);
},
openSessionSubscription: async () => {
subscription = new FakeSubscription(
continuitySnapshot({ sessionId: sessionId!, projectionRevision: 1, rootTurn: null }),
transcript,
);
return subscription;
},
subscribeConfigurationChanges: () => () => undefined,
subscribeConnectionCatalogChanges: () => () => undefined,
subscribeProjectCatalogChanges: () => () => undefined,
subscribeSessionCatalogChanges: () => () => undefined,
subscribeScheduledTaskChanges: () => () => undefined,
close: async () => disconnect(),
} as unknown as RuntimeHostConnection;
const connection = await createRuntimeHostReconnectingConnection({
initialConnection: initial,
connect: async (signal) => {
reconnectSignal = signal;
return new Promise<RuntimeHostConnection>((_resolve, reject) => {
signal.addEventListener('abort', () => reject(signal.reason), { once: true });
});
},
backoff: { wait: async () => undefined },
});
const harness = createHarness([], { stdin, connection });
let finished = false;
const run = harness.run().then((code) => {
finished = true;
return code;
});
const send = (id: number, method: string, params: unknown) =>
stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
try {
send(1, 'session/new', { cwd: '/workspace', mcpServers: [] });
await waitFor(() =>
harness.stdoutMessages().some((message) => (message as { id?: number }).id === 1),
);
send(2, 'session/prompt', {
sessionId: sessionId!,
prompt: [{ type: 'text', text: 'attach' }],
});
await waitFor(() => Boolean(subscription && subscription.nextCalls > 0));
disconnect();
rejectTranscript(new RuntimeHostSubscriptionError('connection_closed', 'Host disconnected'));
await waitFor(() => Boolean(reconnectSignal) && subscription!.closeCalls > 0);
stdin.end();
await waitFor(() => finished);
assert.equal(await run, 0);
assert.equal(reconnectSignal?.aborted, true);
assert.equal(turnStarts, 0);
} finally {
stdin.end();
// Also releases the old implementation on a red test, without masking
// the assertion that EOF itself must complete teardown.
await connection.close();
await run;
}
});
test('returns zero after normal EOF without connecting a Runtime Host', async () => {
const harness = createHarness([]);
assert.equal(await harness.run(), 0);
assert.equal(harness.connectCalls(), 0);
});
test('returns a JSON-RPC parse error and then zero after EOF', async () => {
const harness = createHarness(['not json\n']);
assert.equal(await harness.run(), 0);
assert.deepEqual(harness.stdoutMessages(), [
{ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } },
]);
});
test('propagates a stdin transport error', async () => {
const transportError = new Error('stdin transport failed');
const stdin = Readable.from(
(async function* () {
throw transportError;
})(),
);
const harness = createHarness([], { stdin });
await assert.rejects(harness.run(), (error: unknown) => error === transportError);
});
test('serializes Session creation and configuration through the Runtime Host catalog', async () => {
const lifecycle: string[] = [];
let created: SessionCatalogProjection | undefined;
const connection = {
request: async (operation: string, input: unknown) => {
lifecycle.push(operation);
if (operation === 'session.create') {
const { sessionId } = input as { sessionId: string };
created = sessionProjection({ id: sessionId });
return created;
}
if (operation === 'connection.catalog.query') return connectionCatalogPage();
if (operation === 'session.catalog.query') {
assert.ok(created);
return { kind: 'session', session: created };
}
if (operation === 'session.configuration.update') {
assert.ok(created);
return {
kind: 'committed',
session: sessionProjection({
id: created.id,
revision: created.revision + 1,
collaborationMode: 'plan',
}),
};
}
assert.fail(`Unexpected Runtime Host operation: ${operation}`);
},
close: async () => {
lifecycle.push('connection.close');
},
} as unknown as RuntimeHostConnection;
const stdin = new PassThrough();
const harness = createHarness([], { stdin, connection });
const run = harness.run();
stdin.write(
`${JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: 1 },
})}\n`,
);
stdin.write(
`${JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'session/new',
params: { cwd: '/workspace', mcpServers: [] },
})}\n`,
);
await waitFor(() => created !== undefined);
stdin.end(
`${JSON.stringify({
jsonrpc: '2.0',
id: 3,
method: 'session/set_config_option',
params: {
sessionId: created!.id,
configId: 'collaboration_mode',
value: 'plan',
},
})}\n`,
);
assert.equal(await run, 0);
const responses = new Map(
harness
.stdoutMessages()
.map((message) => [(message as { id?: unknown }).id, message] as const),
);
const createdResponse = responses.get(2) as {
result?: { sessionId?: unknown; configOptions?: unknown[] };
};
assert.equal(createdResponse.result?.sessionId, created?.id);
assert.deepEqual(
createdResponse.result?.configOptions?.map((option) => (option as { id?: unknown }).id),
['permission_mode', 'thinking_level', 'collaboration_mode', 'orchestration_mode'],
);
const configuredResponse = responses.get(3) as {
result?: {
configOptions?: Array<{ id?: unknown; currentValue?: unknown }>;
};
};
assert.deepEqual(
configuredResponse.result?.configOptions?.find(({ id }) => id === 'collaboration_mode'),
{
type: 'select',
id: 'collaboration_mode',
name: 'Collaboration mode',
category: 'mode',
currentValue: 'plan',
options: [
{ value: 'agent', name: 'Agent' },
{ value: 'plan', name: 'Plan' },
],
},
);
assert.deepEqual(lifecycle, [
'session.create',
'connection.catalog.query',
'session.catalog.query',
'session.configuration.update',
'connection.catalog.query',
'connection.close',
]);
assert.equal('subscribe' in connection, false);
assert.ok(lifecycle.every((operation) => operation !== 'session.catalog.subscribe'));
assert.ok(
harness.stdoutMessages().every((message) => {
const record = message as { jsonrpc?: unknown };
return record.jsonrpc === '2.0';
}),
);
});
test('returns a Host connection failure from the Session request and keeps serving ACP', async () => {
const harness = createHarness(
[
`${JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: 1 },
})}\n`,
`${JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'session/list',
params: {},
})}\n`,
`${JSON.stringify({
jsonrpc: '2.0',
id: 3,
method: 'session/close',
params: { sessionId: 'missing' },
})}\n`,
],
{ connectError: new Error('Host unavailable') },
);
assert.equal(await harness.run(), 0);
const responses = new Map(
harness
.stdoutMessages()
.map((message) => [(message as { id?: unknown }).id, message] as const),
);
const connectionFailure = responses.get(2) as {
error?: { code?: unknown; data?: unknown };
};
assert.equal(connectionFailure.error?.code, -32603);
assert.deepEqual(connectionFailure.error?.data, {
source: 'runtime_host',
operation: 'connect',
code: 'connection_failed',
});
const methodFailure = responses.get(3) as {
error?: { code?: unknown; data?: unknown };
};
assert.equal(methodFailure.error?.code, -32602);
assert.deepEqual(methodFailure.error?.data, { reason: 'unknown_session' });
assert.equal(harness.connectCalls(), 1);
});
test('keeps close for an unknown Session Host-independent', async () => {
const harness = createHarness([
`${JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: 1 },
})}\n`,
`${JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'session/close',
params: { sessionId: 'missing' },
})}\n`,
]);
assert.equal(await harness.run(), 0);
const response = harness
.stdoutMessages()
.find((message) => (message as { id?: unknown }).id === 2) as {
error?: { code?: unknown; data?: unknown };
};
assert.equal(response.error?.code, -32602);
assert.deepEqual(response.error?.data, { reason: 'unknown_session' });
assert.equal(harness.connectCalls(), 0);
});
});
function createHarness(
chunks: string[],
options: {
readonly stdin?: Readable;
readonly connection?: RuntimeHostConnection;
readonly connectError?: Error;
} = {},
) {
const stdin = options.stdin ?? Readable.from(chunks.map((chunk) => Buffer.from(chunk)));
let connects = 0;
const connection =
options.connection ??
({
request: async () => ({ kind: 'unsupported_legacy_record' }),
close: async () => undefined,
} as unknown as RuntimeHostConnection);
const stdoutChunks: Buffer[] = [];
const stdout = new Writable({
write(chunk, _encoding, callback) {
stdoutChunks.push(Buffer.from(chunk));
callback();
},
});
return {
run: () =>
runMakaAcpStdioServer(
{ workspaceRoot: '/workspace', clientDataRoot: '/client-data', version: '0.2.0' },
{
stdin,
stdout,
connectRuntimeHostCliConnection: async () => {
connects += 1;
if (options.connectError) throw options.connectError;
return {
connection: {
...connection,
request: connection.request.bind(connection),
reconnecting: true,
hostEpoch: connection.hostEpoch ?? 'host-1',
openSessionSubscription:
connection.openSessionSubscription?.bind(connection) ??
(async () => {
throw new Error('Unexpected Session attachment');
}),
openSessionSubscriptionOnce: isRuntimeHostReconnectingConnection(connection)
? connection.openSessionSubscriptionOnce.bind(connection)
: (connection.openSessionSubscription?.bind(connection) ??
(async () => {
throw new Error('Unexpected Session attachment');
})),
subscribeConnectionAvailability: () => () => undefined,
},
close: () => connection.close(),
} as unknown as Awaited<
ReturnType<
typeof import('../runtime-host-cli-context.js').connectRuntimeHostCliConnection
>
>;
},
},
),
connectCalls: () => connects,
stdoutMessages: () =>
Buffer.concat(stdoutChunks)
.toString('utf8')
.trim()
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line) as unknown),
};
}
function connectionCatalogPage() {
return {
kind: 'page' as const,
revision: 1,
defaultTarget: { connectionId: 'connection-1', model: 'default' },
connectionCount: 1,
items: [
{
kind: 'connection' as const,
connectionIndex: 0,
connectionId: 'connection-1',
revision: 1,
slug: 'default',
name: 'Default',
providerType: 'openai' as const,
enabled: true,
enabledModelIdCount: 1,
modelCount: 0,
catalogEntryCount: 1,
},
{
kind: 'enabled_model_id' as const,
connectionIndex: 0,
itemIndex: 0,
modelId: 'default',
},
{
kind: 'catalog_entry' as const,
connectionIndex: 0,
itemIndex: 0,
entry: {
id: 'default',
canUseAsChatDefault: true,
isDefault: true,
supportsVision: false,
thinkingLevels: ['low', 'high'] as const,
},
},
],
nextCursor: null,
};
}
function sessionProjection(
overrides: Partial<SessionCatalogProjection> = {},
): SessionCatalogProjection {
return {
id: 'session-1',
revision: 1,
workspace: { target: { kind: 'host_path', path: '/workspace' }, hostCwd: '/workspace' },
createdAt: 1,
activityAt: 1,
name: 'Session',
isFlagged: false,
isArchived: false,
labels: [],
labelsTruncated: false,
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'default',
connectionLocked: false,
model: 'default',
permissionMode: 'ask',
collaborationMode: 'agent',
orchestrationMode: 'default',
...overrides,
};
}
function continuitySnapshot(input: {
readonly sessionId: string;
readonly projectionRevision: number;
readonly metadataRevision?: number;
readonly rootTurn: SessionContinuitySnapshot['rootTurn'];
readonly status?: 'active' | 'running';
}): SessionContinuitySnapshot {
return {
schemaVersion: SESSION_CONTINUITY_SCHEMA_VERSION,
session: {
sessionId: input.sessionId,
metadataRevision: input.metadataRevision ?? 1,
status: input.status ?? (input.rootTurn ? 'running' : 'active'),
createdAt: 1,
isArchived: false,
},
projectionRevision: input.projectionRevision,
rootTurn: input.rootTurn,
goal: null,
queue: { hostEpoch: 'host-1', queueRevision: 0, steering: [], followup: [] },
interactions: { pending: [] },
};
}
async function waitFor(predicate: () => boolean): Promise<void> {
for (let attempt = 0; attempt < 100; attempt += 1) {
if (predicate()) return;
await new Promise((resolve) => setImmediate(resolve));
}
assert.fail('condition was not reached');
}
class FakeSubscription implements RuntimeHostSessionSubscription, AsyncIterator<SubscriptionFrame> {
subscribePtyData(): () => void {
return () => undefined;
}
readonly #sessionDomainListeners = new Set<
(frame: Extract<SubscriptionFrame, { kind: 'subscription.session_domain_changed' }>) => void
>();
subscribeSessionDomainChanges(
listener: (
frame: Extract<SubscriptionFrame, { kind: 'subscription.session_domain_changed' }>,
) => void,
): () => void {
this.#sessionDomainListeners.add(listener);
return () => this.#sessionDomainListeners.delete(listener);
}
readonly hostEpoch = 'host-1';
readonly activeAssistantStreams = [];
readonly transcriptBootstrap = null;
readonly transcriptWatermark = null;
readonly subscriptionId: string;
readonly #frames: SubscriptionFrame[] = [];
readonly #waiters: Array<{
resolve(result: IteratorResult<SubscriptionFrame>): void;
reject(error: Error): void;
}> = [];
nextCalls = 0;
closeCalls = 0;
#readied = false;
#openGate: () => void = () => undefined;
readonly #readyGate = new Promise<void>((resolve) => {
this.#openGate = resolve;
});
#closed = false;
#failure: Error | undefined;
constructor(
readonly snapshot: SessionContinuitySnapshot,
private readonly transcript: Promise<StoredMessage[]>,
subscriptionId = 'subscription-1',
) {
this.subscriptionId = subscriptionId;
}
[Symbol.asyncIterator](): AsyncIterator<SubscriptionFrame> {
return this;
}
async ready(): Promise<void> {
this.#readied = true;
this.#openGate();
}
next(): Promise<IteratorResult<SubscriptionFrame>> {
this.nextCalls += 1;
// The Host holds frames until the subscriber declares readiness, so a fake
// that hands them over earlier would let an ordering bug pass.
return this.#readied ? this.#deliver() : this.#readyGate.then(() => this.#deliver());
}
#deliver(): Promise<IteratorResult<SubscriptionFrame>> {
const frame = this.#frames.shift();
if (frame) return Promise.resolve({ done: false, value: frame });
if (this.#failure) return Promise.reject(this.#failure);
if (this.#closed) return Promise.resolve({ done: true, value: undefined });
return new Promise((resolve, reject) => this.#waiters.push({ resolve, reject }));
}
push(frame: SubscriptionFrame): void {
if (frame.kind === 'subscription.session_domain_changed') {
for (const listener of this.#sessionDomainListeners) listener(frame);
}
const waiter = this.#waiters.shift();
if (waiter) waiter.resolve({ done: false, value: frame });
else this.#frames.push(frame);
}
fail(error: Error): void {
this.#failure = error;
for (const waiter of this.#waiters.splice(0)) waiter.reject(error);
}
async loadTranscript<T>(decodeMessage: (value: unknown) => T): Promise<T[]> {
return (await this.transcript).map(decodeMessage);
}
async decodeTranscriptPage(): Promise<never> {
throw new Error('Fake subscription does not expose transcript pages');
}
async loadTranscriptPage(): Promise<never> {
throw new Error('Fake subscription does not expose transcript pages');
}
async close(): Promise<void> {
this.closeCalls += 1;
this.#closed = true;
for (const waiter of this.#waiters.splice(0)) {
waiter.resolve({ done: true, value: undefined });
}
}
}
const unsupportedRequests = {
question: {
kind: 'question',
toolUseId: 'tool-1',
questions: [{ question: 'Continue?', options: [{ label: 'Yes' }] }],
},
form: {
kind: 'form',
toolUseId: 'tool-1',
message: 'Configure',
requester: { name: 'test', source: 'MCP' },
fields: [{ kind: 'string', name: 'name', label: 'Name', required: true }],
},
permission: {
kind: 'permission',
toolUseId: 'tool-1',
prompt: {
kind: 'tool_permission',
toolName: 'Bash',
category: 'shell_unsafe',
reason: 'shell_dangerous',
review: { kind: 'command', command: 'echo test', cwd: '/workspace' },
rememberForTurnAllowed: true,
},
},
sandbox_boundary: {
kind: 'sandbox_boundary',
expansion: { network: { enabled: true } },
justification: 'Network access',
},
client_capability: {
kind: 'client_capability',
toolUseId: 'tool-1',
target: {
providerId: 'provider',
contractId: 'contract',
serverId: 'server',
toolName: 'tool',
capability: 'desktop_mcp',
scope: { kind: 'mcp_tool', serverId: 'server', toolName: 'tool' },
},
},
} satisfies Record<string, InteractionRequest>;