blob: 8a25c6d40313cb1cae7983a21e75e7923ea9dad7 [file]
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import type { LlmConnection, SessionEvent, SessionHeader, StoredMessage } from '@maka/core';
import { PermissionEngine } from '../permission-engine.js';
import type {
RuntimeCommitSink,
ToolOutcomeCommit,
ToolPreparedCommit,
} from '../runtime-commit-sink.js';
import { ToolRuntime, type MakaTool } from '../tool-runtime.js';
describe('ToolRuntime durable boundary', () => {
it('does not invoke the tool or publish a result when T1 fails', async () => {
let implementationCalls = 0;
const harness = makeHarness({
commitToolPrepared: async () => {
throw new Error('T1 unavailable');
},
commitToolOutcome: async () => {
throw new Error('must not reach T2');
},
});
await assert.rejects(
harness.execute(
tool(() => {
implementationCalls += 1;
return { ok: true };
}),
),
/T1 unavailable/,
);
assert.equal(implementationCalls, 0);
assert.equal(
harness.events.some((event) => event.type === 'tool_result'),
false,
);
assert.equal(
harness.messages.some((message) => message.type === 'tool_result'),
false,
);
});
it('does not invoke a tool when another local dispatcher already owns its operation', async () => {
let implementationCalls = 0;
const harness = makeHarness({
commitToolPrepared: async () => ({ created: false, runtimeEventSeq: 1 }),
commitToolOutcome: async () => {
throw new Error('must not reach T2');
},
});
await assert.rejects(
harness.execute(
tool(() => {
implementationCalls += 1;
return { ok: true };
}),
),
/already claimed/,
);
assert.equal(implementationCalls, 0);
assert.deepEqual(
harness.events.map((event) => event.type),
['tool_start'],
);
assert.deepEqual(
harness.messages.map((message) => message.type),
['tool_call'],
);
});
it('does not cross T1 after the captured Run loses ownership', async () => {
let runReads = 0;
let preparedCalls = 0;
let implementationCalls = 0;
const harness = makeHarness(
{
commitToolPrepared: async () => {
preparedCalls += 1;
return { created: true, runtimeEventSeq: 1 };
},
commitToolOutcome: async () => {
throw new Error('must not reach T2');
},
},
undefined,
() => (++runReads === 1 ? 'run-1' : undefined),
);
await assert.rejects(
harness.execute(
tool(() => {
implementationCalls += 1;
return { ok: true };
}),
),
/lost Run ownership/,
);
assert.equal(preparedCalls, 0);
assert.equal(implementationCalls, 0);
});
it('does not cross T1 when durable dispatch is already aborted', async () => {
let preparedCalls = 0;
let implementationCalls = 0;
const controller = new AbortController();
controller.abort(new Error('stop before start'));
const harness = makeHarness({
commitToolPrepared: async () => {
preparedCalls += 1;
return { created: true, runtimeEventSeq: 1 };
},
commitToolOutcome: async () => {
throw new Error('must not reach T2');
},
});
await assert.rejects(
harness.execute(
tool(() => {
implementationCalls += 1;
return { ok: true };
}),
controller.signal,
),
/stop before start/,
);
assert.equal(preparedCalls, 0);
assert.equal(implementationCalls, 0);
});
it('commits T1 before implementation and T2 before publishing the result', async () => {
const order: string[] = [];
const prepared: ToolPreparedCommit[] = [];
const outcomes: ToolOutcomeCommit[] = [];
const harness = makeHarness(
{
commitToolPrepared: async (input) => {
prepared.push(input);
order.push('t1');
return { created: true, runtimeEventSeq: 1 };
},
commitToolOutcome: async (input) => {
outcomes.push(input);
order.push('t2');
return { created: true, runtimeEventSeq: 2 };
},
},
order,
);
const result = await harness.execute(
tool(() => {
order.push('impl');
return { ok: true, text: 'done' };
}),
);
assert.deepEqual(result, { ok: true, text: 'done' });
assert.deepEqual(order, ['t1', 'impl', 't2', 'published-result']);
assert.equal(prepared[0]?.runtimeEvent.content?.kind, 'function_call');
assert.equal(
prepared[0]?.dispatchRuntimeEvent.actions?.toolDispatch?.protocol,
't1_after_preflight_v1',
);
assert.equal(prepared[0]?.dispatchRuntimeEvent.content, undefined);
assert.equal(outcomes[0]?.runtimeEvent.content?.kind, 'function_response');
assert.equal(prepared[0]?.operationId, outcomes[0]?.operationId);
assert.equal(prepared[0]?.runtimeEvent.refs?.operationId, prepared[0]?.operationId);
assert.equal(prepared[0]?.dispatchRuntimeEvent.refs?.operationId, prepared[0]?.operationId);
assert.equal(outcomes[0]?.runtimeEvent.refs?.operationId, prepared[0]?.operationId);
});
it('wraps business-domain kind values as canonical JSON tool results', async () => {
const outcomes: ToolOutcomeCommit[] = [];
const harness = makeHarness({
commitToolPrepared: async () => ({ created: true, runtimeEventSeq: 1 }),
commitToolOutcome: async (input) => {
outcomes.push(input);
return { created: true, runtimeEventSeq: 2 };
},
});
const output = {
kind: 'plan_submitted',
proposal: { proposalId: 'proposal-1' },
storeVersion: 1,
};
assert.deepEqual(await harness.execute(tool(() => output)), output);
const response = outcomes[0]?.runtimeEvent.content;
assert.equal(response?.kind, 'function_response');
assert.deepEqual(response?.kind === 'function_response' ? response.result : undefined, {
kind: 'json',
value: output,
});
const message = harness.messages.find((candidate) => candidate.type === 'tool_result');
assert.deepEqual(message?.type === 'tool_result' ? message.content : undefined, {
kind: 'json',
value: output,
});
});
it('does not create a prepared journal operation when permission is denied', async () => {
let preparedCalls = 0;
let implementationCalls = 0;
const harness = makeHarness({
commitToolPrepared: async () => {
preparedCalls += 1;
return { created: true, runtimeEventSeq: 1 };
},
commitToolOutcome: async () => {
throw new Error('must not reach T2');
},
});
const execution = harness.execute({
...tool(() => {
implementationCalls += 1;
return { ok: true };
}),
name: 'Bash',
permissionRequired: true,
});
while (!harness.events.some((event) => event.type === 'permission_request')) {
await Promise.resolve();
}
const request = harness.events.find((event) => event.type === 'permission_request');
if (!request || request.type !== 'permission_request')
throw new Error('expected permission request');
harness.permissionEngine.recordResponse('turn-1', {
requestId: request.requestId,
decision: 'deny',
});
await execution;
assert.equal(preparedCalls, 0);
assert.equal(implementationCalls, 0);
});
it('does not publish an implementation result when T2 fails', async () => {
let implementationCalls = 0;
const harness = makeHarness({
commitToolPrepared: async () => ({ created: true, runtimeEventSeq: 1 }),
commitToolOutcome: async () => {
throw new Error('T2 unavailable');
},
});
await assert.rejects(
harness.execute(
tool(() => {
implementationCalls += 1;
return { ok: true };
}),
),
/T2 unavailable/,
);
assert.equal(implementationCalls, 1);
assert.equal(
harness.events.some((event) => event.type === 'tool_result'),
false,
);
assert.equal(
harness.messages.some((message) => message.type === 'tool_result'),
false,
);
});
it('commits a normalized error outcome before returning a thrown tool failure to the model', async () => {
const outcomes: ToolOutcomeCommit[] = [];
const harness = makeHarness({
commitToolPrepared: async () => ({ created: true, runtimeEventSeq: 1 }),
commitToolOutcome: async (input) => {
outcomes.push(input);
return { created: true, runtimeEventSeq: 2 };
},
});
await harness.execute(
tool(() => {
throw new Error('tool exploded');
}),
);
const response = outcomes[0]?.runtimeEvent.content;
assert.equal(response?.kind, 'function_response');
assert.equal(response?.kind === 'function_response' && response.isError, true);
assert.equal(
harness.events.some((event) => event.type === 'tool_result' && event.isError),
true,
);
});
});
function makeHarness(
sink: RuntimeCommitSink,
order?: string[],
getCurrentRunId: () => string | undefined = () => 'run-1',
) {
const messages: StoredMessage[] = [];
const events: SessionEvent[] = [];
const permissionEngine = new PermissionEngine({ newId: nextId(), now: () => 1 });
permissionEngine.beginTurn('turn-1');
const runtime = new ToolRuntime({
sessionId: 'session-1',
header: header(),
connection: connection(),
modelId: 'model-1',
appendMessage: async (message) => {
messages.push(message);
},
permissionEngine,
newId: nextId(),
now: nextNow(),
getPermissionPauseTarget: () => null,
getCurrentRunId,
runtimeCommitSink: sink,
});
return {
messages,
events,
permissionEngine,
execute: async (target: MakaTool, abortSignal: AbortSignal = new AbortController().signal) =>
(
await runtime.settleToolCall({
tool: target,
turnId: 'turn-1',
toolCallId: 'provider-call-1',
input: {},
abortSignal,
eventSink: {
push: (event) => {
events.push(event);
if (event.type === 'tool_result') order?.push('published-result');
},
pushAndWaitUntilConsumed: async (event) => {
events.push(event);
if (event.type === 'tool_result') order?.push('published-result');
},
},
})
).result,
};
}
function tool(impl: MakaTool['impl']): MakaTool {
return {
name: 'Read',
description: 'read',
parameters: {},
permissionRequired: false,
recoveryMode: 'replay_safe',
impl,
};
}
function header(): SessionHeader {
return {
id: 'session-1',
workspaceRoot: '/workspace/repo',
cwd: '/workspace/repo',
createdAt: 1,
lastUsedAt: 1,
name: 'test',
titleIsManual: false,
isFlagged: false,
labels: [],
isArchived: false,
status: 'active',
statusUpdatedAt: 1,
hasUnread: false,
backend: 'ai-sdk',
llmConnectionSlug: 'connection-1',
connectionLocked: true,
model: 'model-1',
permissionMode: 'ask',
schemaVersion: 1,
};
}
function connection(): LlmConnection {
return {
slug: 'connection-1',
name: 'test',
providerType: 'openai',
defaultModel: 'model-1',
enabled: true,
createdAt: 1,
updatedAt: 1,
};
}
function nextId(): () => string {
let value = 0;
return () => `id-${++value}`;
}
function nextNow(): () => number {
let value = 0;
return () => ++value;
}