fix(validators): the progress validator knows stopped and blocked
/trekexecute writes run status stopped (dirty-tree stop, entry-condition stop, escalation) and step status blocked (Step 0 sandbox sentinel); the validator rejected the first and warned on the second. Both are real states lib/util/session-orders.mjs already reads (ENDED_BAD), so the validator learns them. The step-status list is exported as PROGRESS_STEP_STATUSES (was an inline literal) so tests check prose against it instead of keeping a copy. docs/HANDOVER-CONTRACTS.md Handover 5 rows updated. Red: 3 of 4 new tests fail on the pre-fix code (stopped run status, PROGRESS_STEP_STATUSES export, blocked step status). Regression guard (green on arrival): every run status but completed stays resumable. progress-validator 16/16. Suite 1368 (1366/0/2). Voyage order 20261005T104513Z-1250514346, plan step 1/5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5680591ddf
commit
8a56187341
3 changed files with 58 additions and 5 deletions
|
|
@ -292,7 +292,7 @@ The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrato
|
|||
| `mode` | string | yes | `execute \| dry-run \| validate` |
|
||||
| `total_steps` | number | yes | |
|
||||
| `current_step` | number | yes | 0..total_steps |
|
||||
| `status` | string | yes | `pending \| in_progress \| completed \| failed \| partial` |
|
||||
| `status` | string | yes | `pending \| in_progress \| completed \| failed \| partial \| stopped` |
|
||||
| `session_start_sha` | string | optional | git sha at execute start |
|
||||
| `session_end_sha` | string | optional | git sha at execute end |
|
||||
| `steps` | object | yes | Map of step number → step record |
|
||||
|
|
@ -301,7 +301,7 @@ The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrato
|
|||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `status` | `completed \| in_progress \| failed \| pending \| deferred \| skipped` | |
|
||||
| `status` | `completed \| in_progress \| failed \| pending \| deferred \| skipped \| blocked` | `blocked` = Step 0 sandbox sentinel |
|
||||
| `attempts` | number | 1..N |
|
||||
| `error` | string \| null | |
|
||||
| `completed_at` | ISO string \| null | |
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import { readFileSync, existsSync } from 'node:fs';
|
|||
import { issue, fail } from '../util/result.mjs';
|
||||
|
||||
export const PROGRESS_REQUIRED_TOP = ['schema_version', 'plan', 'plan_version', 'mode', 'status', 'total_steps', 'current_step', 'steps'];
|
||||
export const PROGRESS_VALID_STATUSES = ['pending', 'in_progress', 'completed', 'failed', 'partial'];
|
||||
export const PROGRESS_VALID_STATUSES = ['pending', 'in_progress', 'completed', 'failed', 'partial', 'stopped'];
|
||||
// Step statuses /trekexecute stores. `blocked` is the Step 0 sandbox sentinel; `stopped` above
|
||||
// is the escalation / dirty-tree / entry-condition stop. Both are states other readers already
|
||||
// know (lib/util/session-orders.mjs ENDED_BAD).
|
||||
export const PROGRESS_STEP_STATUSES = ['completed', 'in_progress', 'failed', 'pending', 'deferred', 'skipped', 'blocked'];
|
||||
|
||||
export function validateProgressContent(jsonText, opts = {}) {
|
||||
let parsed;
|
||||
|
|
@ -75,7 +79,7 @@ export function validateProgressObject(parsed, opts = {}) {
|
|||
errors.push(issue('PROGRESS_STEP_SHAPE', `steps["${k}"] is not an object`));
|
||||
continue;
|
||||
}
|
||||
if (s.status !== undefined && !['completed', 'in_progress', 'failed', 'pending', 'deferred', 'skipped'].includes(s.status)) {
|
||||
if (s.status !== undefined && !PROGRESS_STEP_STATUSES.includes(s.status)) {
|
||||
warnings.push(issue('PROGRESS_STEP_BAD_STATUS', `steps["${k}"].status "${s.status}" unrecognized`));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { validateProgressObject, checkResumeReadiness } from '../../lib/validators/progress-validator.mjs';
|
||||
import * as progressValidator from '../../lib/validators/progress-validator.mjs';
|
||||
|
||||
const { validateProgressObject, checkResumeReadiness } = progressValidator;
|
||||
|
||||
function goodProgress() {
|
||||
return {
|
||||
|
|
@ -107,3 +109,50 @@ 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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue