|
| 1 | +// runtime-e2e/resume_plan_step_fields/test.mjs |
| 2 | +// Real-stack assertion: resumePlan surfaces the step/confirm-mode HITL fields |
| 3 | +// the orchestrator emits when it gates the next step (step_result, next_step, |
| 4 | +// next_step_name, total_steps) plus workflow_id on every resume path and |
| 5 | +// message on terminal paths. Pre-fix, the transformer dropped all of them and |
| 6 | +// the ResumePlanResponse doc marked them "never populated". |
| 7 | +// Per CLAUDE.md HARD RULE #0 — this test MUST hit a real agent, no mocks. |
| 8 | +// |
| 9 | +// Drives the real confirm-mode loop end-to-end: |
| 10 | +// generatePlan(executionMode: 'confirm') → executePlan (WCP gates step 0, |
| 11 | +// awaiting_approval) → resumePlan(approved) per step → terminal resume. |
| 12 | +// |
| 13 | +// Requires an Enterprise stack (confirm/step mode is license-gated). |
| 14 | +// Run (after `source /tmp/axonflow-e2e-env.sh` from the enterprise setup |
| 15 | +// script; npm run build first): |
| 16 | +// |
| 17 | +// AXONFLOW_AGENT_URL=http://localhost:8080 node runtime-e2e/resume_plan_step_fields/test.mjs |
| 18 | + |
| 19 | +import { AxonFlow } from '../../dist/esm/index.js'; |
| 20 | + |
| 21 | +const endpoint = process.env.AXONFLOW_AGENT_URL || 'http://localhost:8080'; |
| 22 | +const clientId = process.env.AXONFLOW_CLIENT_ID; |
| 23 | +const clientSecret = process.env.AXONFLOW_CLIENT_SECRET; |
| 24 | +const userToken = process.env.AXONFLOW_USER_TOKEN || ''; |
| 25 | +if (!clientId || !clientSecret) { |
| 26 | + console.error('AXONFLOW_CLIENT_ID + AXONFLOW_CLIENT_SECRET must be set; see ../README.md'); |
| 27 | + process.exit(2); |
| 28 | +} |
| 29 | + |
| 30 | +const client = new AxonFlow({ clientId, clientSecret, endpoint, timeout: 120000 }); |
| 31 | + |
| 32 | +let failures = 0; |
| 33 | +const check = (name, ok, detail = '') => { |
| 34 | + console.log(`${ok ? 'PASS' : 'FAIL'} [${name}] ${detail}`); |
| 35 | + if (!ok) failures++; |
| 36 | +}; |
| 37 | + |
| 38 | +try { |
| 39 | + // 1. Generate a multi-step plan in confirm mode (every step gated). |
| 40 | + const plan = await client.generatePlan( |
| 41 | + 'Plan a two-step research task: gather sources, then summarize findings', |
| 42 | + 'research', |
| 43 | + userToken, |
| 44 | + { executionMode: 'confirm' } |
| 45 | + ); |
| 46 | + check('generate-plan-confirm-mode', Boolean(plan.planId), `planId=${plan.planId}`); |
| 47 | + |
| 48 | + // 2. Execute — confirm mode gates the first step instead of running it. |
| 49 | + const exec = await client.executePlan(plan.planId, userToken); |
| 50 | + check( |
| 51 | + 'execute-plan-awaiting-approval', |
| 52 | + exec.status === 'awaiting_approval', |
| 53 | + `status=${exec.status}` |
| 54 | + ); |
| 55 | + |
| 56 | + // 3. Resume loop. Every non-terminal resume must carry ALL step-mode HITL |
| 57 | + // fields; the terminal resume must carry workflowId + message and none |
| 58 | + // of the step-gating fields. |
| 59 | + let sawStepGate = false; |
| 60 | + let resume = null; |
| 61 | + for (let i = 0; i < 25; i++) { |
| 62 | + resume = await client.resumePlan(plan.planId, true); |
| 63 | + if (resume.status !== 'awaiting_approval') { |
| 64 | + break; |
| 65 | + } |
| 66 | + sawStepGate = true; |
| 67 | + check( |
| 68 | + `step-gate-${i}-workflow-id`, |
| 69 | + typeof resume.workflowId === 'string' && resume.workflowId.length > 0, |
| 70 | + `workflowId=${resume.workflowId}` |
| 71 | + ); |
| 72 | + check( |
| 73 | + `step-gate-${i}-step-result`, |
| 74 | + resume.stepResult !== undefined && resume.stepResult !== null, |
| 75 | + `stepResult=${JSON.stringify(resume.stepResult).slice(0, 80)}` |
| 76 | + ); |
| 77 | + check( |
| 78 | + `step-gate-${i}-next-step`, |
| 79 | + Number.isInteger(resume.nextStep), |
| 80 | + `nextStep=${resume.nextStep}` |
| 81 | + ); |
| 82 | + check( |
| 83 | + `step-gate-${i}-next-step-name`, |
| 84 | + typeof resume.nextStepName === 'string' && resume.nextStepName.length > 0, |
| 85 | + `nextStepName=${resume.nextStepName}` |
| 86 | + ); |
| 87 | + check( |
| 88 | + `step-gate-${i}-total-steps`, |
| 89 | + Number.isInteger(resume.totalSteps) && resume.totalSteps >= 2, |
| 90 | + `totalSteps=${resume.totalSteps}` |
| 91 | + ); |
| 92 | + } |
| 93 | + |
| 94 | + // The generated plan must have gated at least one intermediate step — |
| 95 | + // otherwise the step-mode wire path was never exercised. |
| 96 | + check('at-least-one-step-gate', sawStepGate, `finalStatus=${resume?.status}`); |
| 97 | + |
| 98 | + // 4. Terminal resume: workflow_id + message on the wire, no step gating. |
| 99 | + check('terminal-status-completed', resume?.status === 'completed', `status=${resume?.status}`); |
| 100 | + check( |
| 101 | + 'terminal-workflow-id', |
| 102 | + typeof resume?.workflowId === 'string' && resume.workflowId.length > 0, |
| 103 | + `workflowId=${resume?.workflowId}` |
| 104 | + ); |
| 105 | + check( |
| 106 | + 'terminal-message', |
| 107 | + typeof resume?.message === 'string' && resume.message.length > 0, |
| 108 | + `message=${resume?.message}` |
| 109 | + ); |
| 110 | + check( |
| 111 | + 'terminal-no-next-step', |
| 112 | + resume?.nextStep === undefined && |
| 113 | + resume?.nextStepName === undefined && |
| 114 | + resume?.totalSteps === undefined, |
| 115 | + `nextStep=${resume?.nextStep} nextStepName=${resume?.nextStepName} totalSteps=${resume?.totalSteps}` |
| 116 | + ); |
| 117 | +} catch (e) { |
| 118 | + check('resume-plan-confirm-loop', false, e.message); |
| 119 | +} |
| 120 | + |
| 121 | +if (failures > 0) { |
| 122 | + console.log(`RESULT: FAIL (${failures})`); |
| 123 | + process.exit(1); |
| 124 | +} |
| 125 | +console.log('RESULT: PASS'); |
0 commit comments