| /* |
| * 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 { 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 controls preserve request and result correlation', () => { |
| 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); |
| 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, |
| 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', |
| }, |
| }), |
| ); |
| }); |