import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import * as progressValidator from '../../lib/validators/progress-validator.mjs'; const { validateProgressObject, checkResumeReadiness } = progressValidator; function goodProgress() { return { schema_version: '1', plan: '.claude/projects/x/plan.md', plan_type: 'plan', plan_version: '1.7', started_at: '2026-04-18T12:00:00Z', updated_at: '2026-04-18T13:00:00Z', mode: 'execute', total_steps: 2, current_step: 1, status: 'in_progress', steps: { '1': { status: 'completed', attempts: 1, error: null, completed_at: '2026-04-18T12:30:00Z', commit: 'abc123', manifest_audit: 'pass' }, '2': { status: 'pending', attempts: 0, error: null, completed_at: null, commit: null, manifest_audit: null }, }, }; } test('validateProgress — happy path', () => { const r = validateProgressObject(goodProgress()); assert.equal(r.valid, true, JSON.stringify(r.errors)); }); test('validateProgress — wrong schema_version', () => { const p = goodProgress(); p.schema_version = '2'; const r = validateProgressObject(p); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROGRESS_SCHEMA_MISMATCH')); }); test('validateProgress — missing required field', () => { const p = goodProgress(); delete p.total_steps; const r = validateProgressObject(p); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROGRESS_MISSING_FIELD' && /total_steps/.test(e.message))); }); test('validateProgress — bad status', () => { const p = goodProgress(); p.status = 'maybe'; const r = validateProgressObject(p); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROGRESS_BAD_STATUS')); }); test('validateProgress — current_step out of range', () => { const p = goodProgress(); p.current_step = 99; const r = validateProgressObject(p); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROGRESS_STEP_RANGE')); }); test('validateProgress — step count mismatch is warning', () => { const p = goodProgress(); p.total_steps = 5; const r = validateProgressObject(p); assert.ok(r.warnings.find(w => w.code === 'PROGRESS_STEP_COUNT_MISMATCH')); }); test('validateProgress — iterations_remaining non-negative integer is valid', () => { const p = goodProgress(); p.iterations_remaining = 7; const r = validateProgressObject(p); assert.equal(r.valid, true, JSON.stringify(r.errors)); }); test('validateProgress — iterations_remaining absent is valid (backward-compat)', () => { const p = goodProgress(); delete p.iterations_remaining; const r = validateProgressObject(p); assert.equal(r.valid, true, JSON.stringify(r.errors)); }); test('validateProgress — iterations_remaining negative is invalid', () => { const p = goodProgress(); p.iterations_remaining = -1; const r = validateProgressObject(p); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROGRESS_ITERATIONS_REMAINING_INVALID')); }); test('validateProgress — iterations_remaining non-integer is invalid', () => { const p = goodProgress(); p.iterations_remaining = 3.5; const r = validateProgressObject(p); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROGRESS_ITERATIONS_REMAINING_INVALID')); }); test('checkResumeReadiness — completed run cannot resume', () => { const p = goodProgress(); p.status = 'completed'; const r = checkResumeReadiness(p); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROGRESS_ALREADY_DONE')); }); test('checkResumeReadiness — in-progress is resumable', () => { const r = checkResumeReadiness(goodProgress()); assert.equal(r.valid, true); }); // /trekexecute writes run status `stopped` (dirty-tree stop, entry-condition stop, // escalation) and step status `blocked` (Step 0 sandbox sentinel). Both are real // states other readers already know (lib/util/session-orders.mjs ENDED_BAD), so the // validator learns them rather than the prose mapping them away. test('validateProgress — stopped is a valid run status', () => { const p = goodProgress(); p.status = 'stopped'; const r = validateProgressObject(p); assert.deepEqual(r.errors, [], 'a stopped run (escalation, dirty tree, entry condition) must validate'); }); test('PROGRESS_STEP_STATUSES is exported and is the list the validator accepts', () => { const { PROGRESS_STEP_STATUSES } = progressValidator; assert.ok(Array.isArray(PROGRESS_STEP_STATUSES), 'the step-status list must be exported, not an inline literal'); for (const status of PROGRESS_STEP_STATUSES) { const p = goodProgress(); p.steps['1'].status = status; const r = validateProgressObject(p); assert.equal(r.warnings.find((w) => w.code === 'PROGRESS_STEP_BAD_STATUS'), undefined, `step status ${status}`); } for (const status of ['passed', 'running']) { const p = goodProgress(); p.steps['1'].status = status; const r = validateProgressObject(p); assert.ok(r.warnings.find((w) => w.code === 'PROGRESS_STEP_BAD_STATUS'), `${status} is not a stored step status`); } }); test('validateProgress — blocked is a valid step status (Step 0 sandbox sentinel)', () => { const p = goodProgress(); p.steps['1'].status = 'blocked'; const r = validateProgressObject(p); assert.deepEqual(r.warnings, [], 'the sandbox sentinel writes step status blocked'); }); // Regression guard (passes before this change): only `completed` refuses a resume. test('checkResumeReadiness — every run status but completed is resumable', () => { for (const status of ['stopped', 'failed', 'partial', 'in_progress']) { const p = goodProgress(); p.status = status; assert.equal(checkResumeReadiness(p).valid, true, `${status} must stay resumable`); } const done = goodProgress(); done.status = 'completed'; assert.equal(checkResumeReadiness(done).valid, false); });