From 7a5bc9973c5b48b70a7c787c59faf49afbf38015 Mon Sep 17 00:00:00 2001 From: Totoro Date: Fri, 4 Sep 2026 14:45:41 +0800 Subject: [PATCH 1/2] test(runtime): reproduce invalid goal evaluation flags RED: rebuilt runtime and ran goal-evaluator plus goal-continuation suites. Invalid flag types are accepted as judgments and corrupt goal settlement or stall accounting. Generated-by: Codex --- .../src/__tests__/goal-continuation.test.ts | 32 ++++++++++++++++ .../src/__tests__/goal-evaluator.test.ts | 38 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/packages/runtime/src/__tests__/goal-continuation.test.ts b/packages/runtime/src/__tests__/goal-continuation.test.ts index e961dc208d..bbd77c7f1e 100644 --- a/packages/runtime/src/__tests__/goal-continuation.test.ts +++ b/packages/runtime/src/__tests__/goal-continuation.test.ts @@ -671,6 +671,38 @@ describe('GoalContinuationCoordinator settlement', () => { assert.equal(admitted.length, 1); }); + for (const field of ['met', 'impossible', 'progress', 'waiting']) { + test(`invalid ${field} cannot settle a Goal or change its stall counter`, async (t) => { + const { manager, coordinator, deps, admitted } = setup({ + evaluations: [{ progress: false }], + }); + t.after(() => coordinator.dispose()); + manager.create(SESSION, 'ship', { blockCap: 2 }); + await settleExternal(coordinator, SESSION, { kind: 'completed', turnId: 'turn-1' }); + await waitFor(() => admitted.length === 1); + assert.equal(manager.get(SESSION)?.consecutiveNoProgress, 1); + + // Exercise the raw evaluator response through the real continuation path. + deps.evaluator.evaluate = async () => + JSON.stringify({ + met: false, + impossible: false, + progress: false, + waiting: false, + [field]: 'false', + }); + const owned = admitted[0]!; + owned.completion.resolve({ kind: 'completed', turnId: owned.turnId }); + await waitFor( + () => manager.get(SESSION)?.status !== 'active' || manager.get(SESSION)?.iterations === 2, + ); + + assert.equal(manager.get(SESSION)?.status, 'active'); + assert.equal(manager.get(SESSION)?.consecutiveNoProgress, 1); + await waitFor(() => admitted.length === 2); + }); + } + test('context failure pauses the exact Goal with a visible reason', async () => { const { manager, coordinator, deps, admitted } = setup(); deps.getRecentContext = async () => { diff --git a/packages/runtime/src/__tests__/goal-evaluator.test.ts b/packages/runtime/src/__tests__/goal-evaluator.test.ts index abcfc7dbcd..ae2d0eecab 100644 --- a/packages/runtime/src/__tests__/goal-evaluator.test.ts +++ b/packages/runtime/src/__tests__/goal-evaluator.test.ts @@ -46,6 +46,44 @@ describe('parseGoalEvaluation', () => { assert.equal(r.reason, 'No reason provided'); }); + for (const field of ['met', 'impossible', 'progress', 'waiting']) { + for (const value of ['false', 'true', '', 0, 1, null, []]) { + test(`rejects ${field}=${JSON.stringify(value)} as a neutral evaluator failure`, () => { + const r = parseGoalEvaluation( + JSON.stringify({ + met: false, + impossible: false, + progress: false, + waiting: false, + [field]: value, + }), + ); + assert.equal(r.evaluatorFailed, true); + assert.equal(r.met, false); + assert.equal(r.impossible, false); + assert.equal(r.progress, false); + assert.equal(r.waiting, false); + }); + } + } + + test('rejects the whole judgment when a true verdict accompanies an invalid field', () => { + const r = parseGoalEvaluation('{"met":true,"progress":"false"}'); + assert.equal(r.evaluatorFailed, true); + assert.equal(r.met, false); + }); + + test('accepts boolean false as a real no-progress judgment', () => { + const r = parseGoalEvaluation( + '{"met":false,"impossible":false,"progress":false,"waiting":false}', + ); + assert.equal(r.evaluatorFailed, false); + assert.equal(r.met, false); + assert.equal(r.impossible, false); + assert.equal(r.progress, false); + assert.equal(r.waiting, false); + }); + test('unparseable output → neutral evaluator failure (not real no-progress)', () => { const r = parseGoalEvaluation('I cannot determine this'); assert.equal(r.met, false); From a67fa7bb84eccd7528eb9e3ceeab31b82bfa8d7c Mon Sep 17 00:00:00 2001 From: Totoro Date: Fri, 4 Sep 2026 14:46:38 +0800 Subject: [PATCH 2/2] fix(runtime): reject non-boolean goal evaluation flags Treat invalid flag types as a neutral evaluator failure while preserving missing-field defaults. GREEN: 101/101 evaluator and continuation tests pass. Evaluator coverage: 95.77% lines, 92.11% branches, 90% functions. Generated-by: Codex --- packages/runtime/src/goal-evaluator.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/goal-evaluator.ts b/packages/runtime/src/goal-evaluator.ts index bfce6faa06..3f7f1b1c25 100644 --- a/packages/runtime/src/goal-evaluator.ts +++ b/packages/runtime/src/goal-evaluator.ts @@ -150,11 +150,17 @@ export function parseGoalEvaluation(raw: string): GoalEvaluation { if (!jsonMatch) return fallback; try { const parsed = JSON.parse(jsonMatch[0]) as Record; + // Missing flags keep their defaults; invalid flags are not a real judgment. + for (const field of ['met', 'impossible', 'progress', 'waiting']) { + if (parsed[field] !== undefined && typeof parsed[field] !== 'boolean') { + return { ...fallback, reason: `Evaluator returned a non-boolean ${field}` }; + } + } return { - met: Boolean(parsed.met), - impossible: Boolean(parsed.impossible), - progress: Boolean(parsed.progress), - waiting: Boolean(parsed.waiting), + met: parsed.met === true, + impossible: parsed.impossible === true, + progress: parsed.progress === true, + waiting: parsed.waiting === true, evaluatorFailed: false, reason: typeof parsed.reason === 'string' && parsed.reason.trim()