blob: 0c923d2278f5315080fcb4e39d75181a26b1462c [file]
import { createTestToolRuntime } from './execution-boundary-test-helpers.js';
import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it } from 'node:test';
import {
createGenesisExecutionBoundary,
type LlmConnection,
type SessionEvent,
type SessionHeader,
} from '@maka/core';
import { createSqliteRuntimeStore } from '@maka/storage';
import { createSessionEventMapMemory, mapSessionEventToRuntimeEvent } from '../ai-sdk-flow.js';
import { buildRuntimeEventModelReplayPlan } from '../model-history.js';
import { buildMcpTools } from '../mcp-tools.js';
import type { InvocationContext } from '../invocation-context.js';
import { MAX_ACTIVE_SUBAGENT_TOOLS_PER_TURN, ToolRuntime, type MakaTool } from '../tool-runtime.js';
describe('ToolRuntime with real SQLite boundary', () => {
it('persists a boundary-blocked Client Capability without an orphan response', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-client-capability-reject-'));
const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'));
try {
let implementationCalls = 0;
const [clientTool] = buildMcpTools(
{
tools: () => [
{
serverId: 'desktop_computer_use',
name: 'maka_computer',
description: 'Client-owned Computer Use',
inputSchema: {
type: 'object',
properties: { action: { const: 'list_apps' } },
required: ['action'],
additionalProperties: false,
},
},
],
callTool: async () => {
implementationCalls += 1;
return { content: [{ type: 'text', text: 'ok' }] };
},
},
{ categoryHint: 'client_capability', recoveryMode: 'outcome_unknown' },
);
assert.ok(clientTool);
const runtime = createTestToolRuntime({
sessionId: 'session-1',
header: header(),
connection: connection(),
modelId: 'model-1',
appendMessage: async () => {},
readExecutionBoundary: async () => createGenesisExecutionBoundary('ask'),
newId: nextId(),
now: nextNow(),
getPermissionPauseTarget: () => null,
runId: 'run-1',
invocationId: 'invocation-1',
runtimeCommitSink: store,
});
const published: SessionEvent[] = [];
const result = await runtime.settleToolCall({
tool: clientTool,
turnId: 'turn-1',
toolCallId: 'provider-call-computer-use',
input: { action: 'list_apps' },
abortSignal: new AbortController().signal,
eventSink: {
push: (event) => published.push(event),
pushAndWaitUntilConsumed: async (event) => {
published.push(event);
},
},
});
assert.equal(implementationCalls, 0);
assert.match(JSON.stringify(result.result), /require the Bypass execution boundary/u);
const toolEvents = published.filter(
(event) => event.type === 'tool_start' || event.type === 'tool_result',
);
assert.equal(toolEvents.length, 2);
assert.equal(
toolEvents.some((event) => event.operationId !== undefined),
false,
);
const memory = createSessionEventMapMemory();
for (const event of toolEvents) {
await store.appendRuntimeEvent(
'session-1',
'run-1',
mapSessionEventToRuntimeEvent(event, invocationContext(), memory),
);
}
const runtimeEvents = await store.readRuntimeEvents('session-1', 'run-1');
assert.deepEqual(
runtimeEvents.map((event) => event.content?.kind),
['function_call', 'function_response'],
);
const replayCall = buildRuntimeEventModelReplayPlan(runtimeEvents).items.find(
(item) => item.kind === 'tool_call',
);
assert.deepEqual(replayCall?.kind === 'tool_call' ? replayCall.input : undefined, {
action: 'list_apps',
});
} finally {
store.close();
await rm(root, { recursive: true, force: true });
}
});
it('persists a subagent admission rejection without an orphan response', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-subagent-limit-'));
const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'));
const releases: Array<() => void> = [];
const pending: Promise<unknown>[] = [];
try {
let implementationsStarted = 0;
let resolveAllStarted!: () => void;
const allStarted = new Promise<void>((resolve) => {
resolveAllStarted = resolve;
});
const runtime = createTestToolRuntime({
sessionId: 'session-1',
header: header(),
connection: connection(),
modelId: 'model-1',
appendMessage: async () => {},
newId: nextId(),
now: nextNow(),
getPermissionPauseTarget: () => null,
runId: 'run-1',
invocationId: 'invocation-1',
runtimeCommitSink: store,
});
const tool: MakaTool = {
name: 'agent_probe',
description: 'probe',
parameters: {},
categoryHint: 'subagent',
impl: async () => {
implementationsStarted += 1;
if (implementationsStarted === MAX_ACTIVE_SUBAGENT_TOOLS_PER_TURN) resolveAllStarted();
await new Promise<void>((resolve) => releases.push(resolve));
return { ok: true };
},
};
const quietSink = {
push: (_event: SessionEvent) => {},
pushAndWaitUntilConsumed: async (_event: SessionEvent) => {},
};
for (let index = 0; index < MAX_ACTIVE_SUBAGENT_TOOLS_PER_TURN; index += 1) {
pending.push(
runtime.settleToolCall({
tool,
turnId: 'turn-1',
toolCallId: `provider-call-active-${index}`,
input: {},
abortSignal: new AbortController().signal,
eventSink: quietSink,
}),
);
}
await withTimeout(allStarted, 'Timed out waiting for subagent slots to fill');
const published: SessionEvent[] = [];
const rejected = await runtime.settleToolCall({
tool,
turnId: 'turn-1',
toolCallId: 'provider-call-rejected',
input: {},
abortSignal: new AbortController().signal,
eventSink: {
push: (event) => published.push(event),
pushAndWaitUntilConsumed: async (event) => {
published.push(event);
},
},
});
assert.match(JSON.stringify(rejected.result), /subagents|子代理/u);
const toolEvents = published.filter(
(event) => event.type === 'tool_start' || event.type === 'tool_result',
);
assert.equal(toolEvents.length, 2);
assert.equal(
toolEvents.some((event) => event.operationId !== undefined),
false,
);
const memory = createSessionEventMapMemory();
for (const event of toolEvents) {
await store.appendRuntimeEvent(
'session-1',
'run-1',
mapSessionEventToRuntimeEvent(event, invocationContext(), memory),
);
}
const rejectedEvents = (await store.readRuntimeEvents('session-1', 'run-1')).filter(
(event) =>
event.content?.kind === 'function_call'
? event.content.id === 'provider-call-rejected'
: event.content?.kind === 'function_response' &&
event.content.id === 'provider-call-rejected',
);
assert.deepEqual(
rejectedEvents.map((event) => event.content?.kind),
['function_call', 'function_response'],
);
} finally {
for (const release of releases) release();
await Promise.allSettled(pending);
store.close();
await rm(root, { recursive: true, force: true });
}
});
it('persists a preflight-rejected sibling beside an exclusive tool without an orphan response', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-exclusive-reject-'));
const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'));
try {
const runtime = createTestToolRuntime({
sessionId: 'session-1',
header: header(),
connection: connection(),
modelId: 'model-1',
appendMessage: async () => {},
newId: nextId(),
now: nextNow(),
getPermissionPauseTarget: () => null,
runId: 'run-1',
invocationId: 'invocation-1',
runtimeCommitSink: store,
});
const published: SessionEvent[] = [];
const eventSink = {
push: (event: SessionEvent) => published.push(event),
pushAndWaitUntilConsumed: async (event: SessionEvent) => {
published.push(event);
},
};
const exclusive: MakaTool = {
name: 'exclusive_batch',
description: 'exclusive',
parameters: {},
executionSemantics: 'exclusive_step',
impl: async () => ({ ok: true }),
};
const sibling: MakaTool = {
name: 'agent_output',
description: 'sibling',
parameters: {},
impl: async () => ({ ok: true }),
};
await runtime.settleToolCall({
tool: exclusive,
turnId: 'turn-1',
stepId: 'step-1',
toolCallId: 'provider-call-exclusive',
input: {},
abortSignal: new AbortController().signal,
eventSink,
});
const rejected = await runtime.settleToolCall({
tool: sibling,
turnId: 'turn-1',
stepId: 'step-1',
toolCallId: 'provider-call-rejected',
input: {},
abortSignal: new AbortController().signal,
eventSink,
});
// The refusal says nothing ran before it says why, and it names the tool
// that held the step — the same wording swarm-orchestration asserts.
assert.match(
JSON.stringify(rejected.result),
/Tool agent_output did not run: exclusive_batch cannot share an assistant step/i,
);
const memory = createSessionEventMapMemory();
for (const event of published) {
if (event.type !== 'tool_start' && event.type !== 'tool_result') continue;
const runtimeEvent = mapSessionEventToRuntimeEvent(event, invocationContext(), memory);
if (runtimeEvent.refs?.operationId !== undefined) continue;
await store.appendRuntimeEvent('session-1', 'run-1', runtimeEvent);
}
const rejectedEvents = (await store.readRuntimeEvents('session-1', 'run-1')).filter(
(event) =>
event.content?.kind === 'function_call'
? event.content.id === 'provider-call-rejected'
: event.content?.kind === 'function_response' &&
event.content.id === 'provider-call-rejected',
);
assert.deepEqual(
rejectedEvents.map((event) => event.content?.kind),
['function_call', 'function_response'],
);
} finally {
store.close();
await rm(root, { recursive: true, force: true });
}
});
it('persists one atomic prepared/outcome pair around the real implementation', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-'));
const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'));
try {
let implementationCalls = 0;
const runtime = createTestToolRuntime({
sessionId: 'session-1',
header: header(),
connection: connection(),
modelId: 'model-1',
appendMessage: async () => {},
newId: nextId(),
now: nextNow(),
getPermissionPauseTarget: () => null,
runId: 'run-1',
invocationId: 'invocation-1',
runtimeCommitSink: store,
});
const tool: MakaTool = {
name: 'Read',
description: 'read',
parameters: {},
recoveryMode: 'replay_safe',
impl: async () => {
implementationCalls += 1;
return { ok: true, text: 'contents' };
},
};
const published: SessionEvent[] = [];
await runtime.settleToolCall({
tool,
turnId: 'turn-1',
toolCallId: 'provider-call-1',
input: {},
abortSignal: new AbortController().signal,
eventSink: {
push: (event) => published.push(event),
pushAndWaitUntilConsumed: async (event) => {
published.push(event);
},
},
});
assert.equal(implementationCalls, 1);
const events = await store.readRuntimeEvents('session-1', 'run-1');
assert.deepEqual(
events.map((event) => event.content?.kind),
['function_call', undefined, 'function_response'],
);
const operationId = events[0]?.refs?.operationId;
assert.ok(operationId);
assert.equal((await store.readToolOperation(operationId))?.currentState, 'outcome_committed');
assert.deepEqual(
events.map((event) => event.invocationId),
['invocation-1', 'invocation-1', 'invocation-1'],
);
assert.deepEqual(
(await store.readToolJournal(operationId)).map((event) => event.state),
['prepared', 'outcome_committed'],
);
assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 3);
const context = invocationContext();
const memory = createSessionEventMapMemory();
const durableEvents = published.filter(
(event) => event.type === 'tool_start' || event.type === 'tool_result',
);
assert.equal(durableEvents.length, 2);
const mappedEvents = durableEvents.map((event) =>
mapSessionEventToRuntimeEvent(event, context, memory),
);
assert.deepEqual(
mappedEvents,
events.filter(
(event) =>
event.content?.kind === 'function_call' || event.content?.kind === 'function_response',
),
);
assert.equal((await store.readRuntimeEvents('session-1', 'run-1')).length, 3);
assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 3);
} finally {
store.close();
await rm(root, { recursive: true, force: true });
}
});
it('persists the same normalized error event that the Runtime flow later observes', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-error-'));
const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'));
try {
const runtime = createTestToolRuntime({
sessionId: 'session-1',
header: header(),
connection: connection(),
modelId: 'model-1',
appendMessage: async () => {},
newId: nextId(),
now: nextNow(),
getPermissionPauseTarget: () => null,
runId: 'run-1',
invocationId: 'invocation-1',
runtimeCommitSink: store,
});
const published: SessionEvent[] = [];
const tool: MakaTool = {
name: 'Read',
description: 'read',
parameters: {},
recoveryMode: 'replay_safe',
impl: async () => {
throw new Error('disk read failed');
},
};
await runtime.settleToolCall({
tool,
turnId: 'turn-1',
toolCallId: 'provider-call-1',
input: {},
abortSignal: new AbortController().signal,
eventSink: {
push: (event) => published.push(event),
pushAndWaitUntilConsumed: async (event) => {
published.push(event);
},
},
});
const memory = createSessionEventMapMemory();
const mappedEvents = published
.filter((item) => item.type === 'tool_start' || item.type === 'tool_result')
.map((event) => mapSessionEventToRuntimeEvent(event, invocationContext(), memory));
const events = await store.readRuntimeEvents('session-1', 'run-1');
assert.deepEqual(
mappedEvents,
events.filter(
(event) =>
event.content?.kind === 'function_call' || event.content?.kind === 'function_response',
),
);
assert.equal(events.length, 3);
assert.equal(events[2]?.content?.kind, 'function_response');
assert.equal(
events[2]?.content?.kind === 'function_response' ? events[2].content.isError : undefined,
true,
);
} finally {
store.close();
await rm(root, { recursive: true, force: true });
}
});
});
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 invocationContext(): InvocationContext {
return {
sessionId: 'session-1',
invocationId: 'invocation-1',
runId: 'run-1',
turnId: 'turn-1',
source: 'test',
startedAt: 1,
newId: nextId(),
now: () => 1,
request: {
sessionId: 'session-1',
invocationId: 'invocation-1',
runId: 'run-1',
turnId: 'turn-1',
text: 'test',
source: 'test',
},
};
}
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;
}
async function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => reject(new Error(message)), 1_000);
}),
]);
} finally {
if (timer !== undefined) clearTimeout(timer);
}
}