blob: 953e92a4387fade8e2b68cf06263d938ff3e7b4a [file]
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
decodePlanQueryResult,
decodeRequestFrame,
decodeResponseFrame,
HOST_OPERATION_SPECS,
PLAN_PAGE_MAX_ITEMS,
} from '../protocol/index.js';
const proposal = {
planId: 'plan-1',
proposalId: 'proposal-1',
sessionId: 'session-1',
turnId: 'turn-1',
revision: 1,
title: 'Host-owned Plan',
steps: [{ id: 'step-1', title: 'Own state', description: 'Persist the Plan in the Host' }],
status: 'pending_approval' as const,
submittedAt: 1,
};
test('Plan protocol declares bounded snapshot queries and stable controls', () => {
assert.equal(HOST_OPERATION_SPECS['plan.query'].mode, 'query');
assert.equal(HOST_OPERATION_SPECS['plan.control'].mode, 'control');
assert.equal(HOST_OPERATION_SPECS['plan.turn.start'].mode, 'command');
assert.deepEqual(
decodeRequestFrame({
requestId: 'request-1',
operation: 'plan.control',
input: {
kind: 'approve_proposal',
sessionId: 'session-1',
proposalId: 'proposal-1',
expectedRevision: 1,
expectedStoreVersion: 1,
operationId: 'approve-1',
},
}),
{
requestId: 'request-1',
operation: 'plan.control',
input: {
kind: 'approve_proposal',
sessionId: 'session-1',
proposalId: 'proposal-1',
expectedRevision: 1,
expectedStoreVersion: 1,
operationId: 'approve-1',
},
},
);
assert.deepEqual(
decodeResponseFrame({
requestId: 'request-1',
operation: 'plan.control',
ok: true,
result: {
sessionId: 'session-1',
storeVersion: 2,
eventType: 'plan_approved',
proposalId: 'proposal-1',
executionId: 'execution-1',
},
}),
{
requestId: 'request-1',
operation: 'plan.control',
ok: true,
result: {
sessionId: 'session-1',
storeVersion: 2,
eventType: 'plan_approved',
proposalId: 'proposal-1',
executionId: 'execution-1',
},
},
);
assert.throws(() =>
HOST_OPERATION_SPECS['plan.control'].assertOutputForInput?.(
{
kind: 'approve_proposal',
sessionId: 'session-1',
proposalId: 'proposal-1',
expectedRevision: 1,
expectedStoreVersion: 1,
operationId: 'approve-1',
},
{
sessionId: 'session-1',
storeVersion: 2,
eventType: 'plan_approved',
proposalId: 'proposal-1',
executionId: null,
},
),
);
});
test('Plan Turn start closes approve and resume into one correlated command', () => {
const input = {
kind: 'approve_proposal' as const,
sessionId: 'session-1',
proposalId: 'proposal-1',
expectedRevision: 1,
expectedStoreVersion: 2,
turnId: 'turn-2',
};
assert.deepEqual(
decodeRequestFrame({ requestId: 'request-1', operation: 'plan.turn.start', input }),
{ requestId: 'request-1', operation: 'plan.turn.start', input },
);
const result = {
plan: {
sessionId: 'session-1',
storeVersion: 3,
eventType: 'plan_approved' as const,
proposalId: 'proposal-1',
executionId: 'execution-1',
},
turn: {
sessionId: 'session-1',
turnId: 'turn-2',
runId: 'run-1',
status: 'running' as const,
},
};
assert.deepEqual(
decodeResponseFrame({
requestId: 'request-1',
operation: 'plan.turn.start',
ok: true,
result,
}),
{ requestId: 'request-1', operation: 'plan.turn.start', ok: true, result },
);
assert.throws(() =>
HOST_OPERATION_SPECS['plan.turn.start'].assertOutputForInput?.(input, {
...result,
turn: { ...result.turn, turnId: 'turn-other' },
}),
);
});
test('Plan pages reject open shapes, oversized pages, and malformed nested state', () => {
const page = {
kind: 'page',
sessionId: 'session-1',
storeVersion: 1,
latestProposalId: 'proposal-1',
activeExecutionId: null,
items: [{ kind: 'proposal', proposal }],
nextCursor: null,
};
assert.deepEqual(decodePlanQueryResult(page), page);
const legacyPage = {
...page,
items: [
{
kind: 'proposal' as const,
proposal: { ...proposal, legacyProjection: { truncated: true as const } },
},
],
};
assert.deepEqual(decodePlanQueryResult(legacyPage), legacyPage);
assert.throws(() => decodePlanQueryResult({ ...page, extra: true }));
assert.throws(() =>
decodePlanQueryResult({
...page,
items: Array.from({ length: PLAN_PAGE_MAX_ITEMS + 1 }, () => ({
kind: 'proposal',
proposal,
})),
}),
);
assert.throws(() =>
decodePlanQueryResult({
...page,
items: [{ kind: 'proposal', proposal: { ...proposal, unknown: true } }],
}),
);
assert.throws(() =>
decodePlanQueryResult({
...page,
items: [
{
kind: 'proposal',
proposal: { ...proposal, legacyProjection: { truncated: false } },
},
],
}),
);
assert.throws(() =>
decodePlanQueryResult({
...page,
items: [
{
kind: 'proposal',
proposal: {
...proposal,
steps: [{ ...proposal.steps[0], id: 'step one' }],
},
},
],
}),
);
assert.throws(() =>
decodeRequestFrame({
requestId: 'request-2',
operation: 'plan.control',
input: {
kind: 'resume_execution',
sessionId: 'session-1',
executionId: 'execution-1',
},
}),
);
});