Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/runtime/src/__tests__/goal-continuation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
38 changes: 38 additions & 0 deletions packages/runtime/src/__tests__/goal-evaluator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 10 additions & 4 deletions packages/runtime/src/goal-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,17 @@ export function parseGoalEvaluation(raw: string): GoalEvaluation {
if (!jsonMatch) return fallback;
try {
const parsed = JSON.parse(jsonMatch[0]) as Record<string, unknown>;
// 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()
Expand Down