blob: c7e18c185a90ad11397d6dfa1fa1206defcd70f0 [file]
import assert from 'node:assert/strict';
import { test } from 'node:test';
import type { RuntimeEvent } from '@maka/core/runtime-event';
import type { FlowInput } from '../agent-flow.js';
import type { InvocationContext } from '../invocation-context.js';
import { createLocalContinuationSafetyInspector } from '../continuation-safety.js';
import {
RuntimeContinuationPlanner,
buildSafeBoundaryContinuationPlan,
} from '../runtime-resume.js';
import { RuntimeRunner } from '../runtime-runner.js';
test('local continuation safety inspector returns current authoritative workspace facts', async () => {
const inspect = createLocalContinuationSafetyInspector({
readSessionCwd: async () => '/workspace/repo-link',
resolveWorkspaceIdentity: async () => ({
workspaceIdentity: 'workspace:v1:123e4567-e89b-42d3-a456-426614174000',
canonicalPath: '/workspace/repo',
}),
listAvailableToolNames: async () => ['Write', 'Read', 'Read'],
hasPendingBackgroundOperations: async () => false,
});
assert.deepEqual(await inspect('session-1'), {
workspaceIdentity: 'workspace:v1:123e4567-e89b-42d3-a456-426614174000',
workspacePath: '/workspace/repo',
backgroundOperationsSettled: true,
availableToolNames: ['Read', 'Write'],
});
});
test('RuntimeRunner continues from replay context without synthesizing another user event', async () => {
const sourceEvents = [
event({
id: 'source-user',
role: 'user',
author: 'user',
content: { kind: 'text', text: 'run tests' },
}),
event({
id: 'source-call',
role: 'model',
author: 'agent',
content: { kind: 'function_call', id: 'tool-1', name: 'Bash', args: { command: 'npm test' } },
}),
event({
id: 'source-result',
role: 'tool',
author: 'tool',
content: { kind: 'function_response', id: 'tool-1', name: 'Bash', result: { exitCode: 0 } },
}),
];
const plan = buildSafeBoundaryContinuationPlan(sourceEvents, {
ledgerReadable: true,
terminalRepairSucceeded: true,
sourceCwd: '/workspace/repo',
currentCwd: '/workspace/repo',
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: ['Bash'],
continuationIdentity: {
invocationId: 'invocation-2',
runId: 'run-2',
turnId: 'turn-2',
},
});
assert.equal(plan.disposition, 'continue');
assert.ok(plan.continuation);
let capturedContext: InvocationContext | undefined;
let capturedInput: FlowInput | undefined;
const committedStartEvents: RuntimeEvent[] = [];
const runner = new RuntimeRunner({
commitContinuationStart: async (candidate) => {
committedStartEvents.push(candidate);
},
flow: {
async *run(context, input) {
assert.deepEqual(
committedStartEvents.map((candidate) => candidate.id),
['continuation-start'],
);
capturedContext = context;
capturedInput = input;
yield event({
id: 'continued-complete',
invocationId: context.invocationId,
runId: context.runId,
turnId: context.turnId,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
});
},
},
providers: { newId: () => 'continuation-start', now: () => 20 },
});
const result = await runner.resume(plan.continuation, { source: 'test' });
assert.equal(result.invocationId, 'invocation-2');
assert.equal(result.runId, 'run-2');
assert.equal(result.turnId, 'turn-2');
assert.deepEqual(
result.events.map((candidate) => candidate.id),
['continuation-start', 'continued-complete'],
);
assert.deepEqual(result.events[0]?.refs, {
sourceInvocationId: 'invocation-1',
sourceRunId: 'run-1',
sourceTurnId: 'turn-1',
sourceRuntimeEventHighWater: 3,
});
assert.deepEqual(committedStartEvents, [result.events[0]]);
assert.equal(capturedContext?.request.continuation?.sourceRunId, 'run-1');
assert.deepEqual(capturedInput?.runtimeContext, sourceEvents);
assert.equal(capturedInput?.continuation?.sourceRuntimeEventHighWater, 3);
});
test('RuntimeRunner preserves the immediate source segment when replay includes continuation ancestors', async () => {
const ancestorEvents = [
event({
id: 'ancestor-user',
invocationId: 'ancestor-invocation',
runId: 'ancestor-run',
turnId: 'ancestor-turn',
role: 'user',
author: 'user',
content: { kind: 'text', text: 'original request' },
}),
event({
id: 'ancestor-terminal',
invocationId: 'ancestor-invocation',
runId: 'ancestor-run',
turnId: 'ancestor-turn',
role: 'system',
author: 'system',
status: 'failed',
actions: { endInvocation: true },
}),
];
const sourceRuntimeContext = [
event({
id: 'source-continuation-start',
role: 'system',
author: 'system',
actions: { stateDelta: { continuation: true } },
}),
event({
id: 'source-terminal',
role: 'system',
author: 'system',
status: 'failed',
actions: { endInvocation: true },
}),
];
const runtimeContext = [...ancestorEvents, ...sourceRuntimeContext];
let capturedInput: FlowInput | undefined;
const runner = new RuntimeRunner({
commitContinuationStart: async () => {},
flow: {
async *run(context, input) {
capturedInput = input;
yield event({
id: 'continued-text',
invocationId: context.invocationId,
runId: context.runId,
turnId: context.turnId,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'continued' },
});
yield event({
id: 'continued-terminal',
invocationId: context.invocationId,
runId: context.runId,
turnId: context.turnId,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
});
},
},
providers: { newId: () => 'new-event', now: () => 20 },
});
const result = await runner.resume(
{
sessionId: 'session-1',
invocationId: 'invocation-2',
runId: 'run-2',
turnId: 'turn-2',
sourceInvocationId: 'invocation-1',
sourceRunId: 'run-1',
sourceTurnId: 'turn-1',
sourceRuntimeEventHighWater: sourceRuntimeContext.length,
sourceRuntimeContext,
runtimeContext,
safetySnapshot: {
workspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
},
},
{ source: 'test' },
);
assert.equal(result.status, 'completed');
assert.deepEqual(capturedInput?.runtimeContext, runtimeContext);
assert.equal('sourceRuntimeContext' in (capturedInput?.continuation ?? {}), false);
});
test('RuntimeContinuationPlanner reads the durable source boundary and allocates fresh identities', async () => {
const sourceEvents = [
event({
id: 'source-user',
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue' },
}),
event({
id: 'source-terminal',
role: 'system',
author: 'system',
status: 'failed',
actions: { endInvocation: true },
}),
];
const ids = ['invocation-2', 'run-2', 'turn-2'];
const planner = new RuntimeContinuationPlanner({
readSourceRun: async () => ({ cwd: '/workspace/repo', status: 'failed' }),
readRuntimeEvents: async () => sourceEvents,
newId: () => ids.shift() ?? 'unexpected-id',
});
const plan = await planner.plan({
sessionId: 'session-1',
sourceRunId: 'run-1',
currentCwd: '/workspace/repo',
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
assert.equal(plan.disposition, 'continue');
assert.deepEqual(plan.continuation, {
sessionId: 'session-1',
invocationId: 'invocation-2',
runId: 'run-2',
turnId: 'turn-2',
sourceInvocationId: 'invocation-1',
sourceRunId: 'run-1',
sourceTurnId: 'turn-1',
sourceRuntimeEventHighWater: 2,
runtimeContext: sourceEvents,
safetySnapshot: {
workspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
},
});
});
test('RuntimeRunner rejects a continuation envelope whose high-water is behind its replay context', async () => {
const runner = new RuntimeRunner({
flow: {
async *run() {
throw new Error('flow must not start');
},
},
});
await assert.rejects(
runner.resume(
{
sessionId: 'session-1',
invocationId: 'invocation-2',
runId: 'run-2',
turnId: 'turn-2',
sourceInvocationId: 'invocation-1',
sourceRunId: 'run-1',
sourceTurnId: 'turn-1',
sourceRuntimeEventHighWater: 0,
runtimeContext: [
event({
id: 'source-user',
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue' },
}),
],
safetySnapshot: {
workspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
},
},
{ source: 'test' },
),
/high-water/i,
);
});
test('RuntimeContinuationPlanner parks with a stable reason when the ledger cannot be read', async () => {
const planner = new RuntimeContinuationPlanner({
readSourceRun: async () => ({ cwd: '/workspace/repo', status: 'failed' }),
readRuntimeEvents: async () => {
throw new Error('corrupt ledger');
},
newId: () => 'unused',
});
const plan = await planner.plan({
sessionId: 'session-1',
sourceRunId: 'run-1',
currentCwd: '/workspace/repo',
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
assert.equal(plan.disposition, 'park');
assert.deepEqual(plan.rejectionReasons, ['runtime_ledger_unreadable']);
});
test('RuntimeContinuationPlanner derives terminal repair from durable run and event facts', async () => {
const planner = new RuntimeContinuationPlanner({
readSourceRun: async () => ({ cwd: '/workspace/repo', status: 'running' }),
readRuntimeEvents: async () => [
event({
id: 'source-user',
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue' },
}),
],
newId: () => 'fresh-id',
});
const plan = await planner.plan({
sessionId: 'session-1',
sourceRunId: 'run-1',
currentCwd: '/workspace/repo',
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
assert.equal(plan.disposition, 'park');
assert.deepEqual(plan.rejectionReasons, ['terminal_repair_failed']);
});
test('RuntimeContinuationPlanner parks when the terminal run header disagrees with the ledger fact', async () => {
const planner = new RuntimeContinuationPlanner({
readSourceRun: async () => ({ cwd: '/workspace/repo', status: 'completed' }),
readRuntimeEvents: async () => [
event({
id: 'source-user',
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue' },
}),
event({
id: 'source-terminal',
role: 'system',
author: 'system',
status: 'failed',
actions: { endInvocation: true },
}),
],
newId: () => 'fresh-id',
});
const plan = await planner.plan({
sessionId: 'session-1',
sourceRunId: 'run-1',
currentCwd: '/workspace/repo',
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
assert.equal(plan.disposition, 'park');
assert.deepEqual(plan.rejectionReasons, ['terminal_repair_failed']);
});
test('RuntimeContinuationPlanner rejects a ledger returned for another source run', async () => {
const planner = new RuntimeContinuationPlanner({
readSourceRun: async () => ({ cwd: '/workspace/repo', status: 'failed' }),
readRuntimeEvents: async () => [
event({
id: 'wrong-user',
runId: 'run-other',
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue' },
}),
event({
id: 'wrong-terminal',
runId: 'run-other',
role: 'system',
author: 'system',
status: 'failed',
actions: { endInvocation: true },
}),
],
newId: () => 'fresh-id',
});
const plan = await planner.plan({
sessionId: 'session-1',
sourceRunId: 'run-1',
currentCwd: '/workspace/repo',
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
assert.equal(plan.disposition, 'park');
assert.deepEqual(plan.rejectionReasons, ['runtime_identity_mismatch']);
});
function event(overrides: Partial<RuntimeEvent>): RuntimeEvent {
return {
id: 'event',
invocationId: 'invocation-1',
runId: 'run-1',
sessionId: 'session-1',
turnId: 'turn-1',
ts: 1,
partial: false,
role: 'model',
author: 'agent',
...overrides,
};
}