Skip to content
Merged
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
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"types": "./dist/src/utils/transcript-records.d.ts",
"import": "./dist/src/utils/transcript-records.js"
},
"./goalWire": {
"types": "./dist/src/goals/goal-wire.d.ts",
"import": "./dist/src/goals/goal-wire.js"
},
"./package.json": "./package.json",
"./dist/*": "./dist/*",
"./src/*": "./src/*"
Expand Down
125 changes: 125 additions & 0 deletions packages/core/src/goals/goal-legacy-projection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it } from 'vitest';
import type {
GoalRecord,
GoalStateCause,
GoalStateRecordPayloadV2,
} from './goal-protocol.js';
import { projectGoalStateToLegacy } from './goal-legacy-projection.js';

const GOAL: GoalRecord = {
goalId: 'goal-1',
revision: 2,
objective: 'ship it',
status: 'active',
evidenceCursor: { recordId: 'state-1' },
turnCount: 4,
activeTimeMs: 2000,
createdAt: 100,
updatedAt: 200,
lastReason: 'continuing',
};

function payload(
cause: GoalStateCause,
status: GoalRecord['status'] = 'active',
goal: GoalRecord | null = { ...GOAL, status },
): GoalStateRecordPayloadV2 {
return {
v: 2,
cause,
snapshot: { v: 2, activity: 'idle', goal },
};
}

describe('projectGoalStateToLegacy', () => {
it.each(['create', 'replace', 'edit', 'resume', 'migrated'] as const)(
'projects %s as legacy set with an active projection',
(cause) => {
Comment on lines +41 to +43

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] verifier_accept and verifier_reject are distinct GoalStateCause values handled by legacyStatusKind but are never exercised by any test — Failure scenario: a future refactor splits the turn_finished/verifier_accept/verifier_reject fall-through to give either verifier cause its own branch, and accidentally maps one to the wrong LegacyGoalStatusKind (e.g., 'set' instead of 'checking'). The existing parameterized tests only cover turn_finished and the five "set" causes, so the regression ships undetected.

Suggested change
it.each(['create', 'replace', 'edit', 'resume', 'migrated'] as const)(
'projects %s as legacy set with an active projection',
(cause) => {
it.each(['create', 'replace', 'edit', 'resume', 'migrated'] as const)(
'projects %s as legacy set with an active projection',
(cause) => {

Add verifier_accept and verifier_reject to the parameterized test, e.g. as separate cases that verify legacyStatusKind mapping produces the correct LegacyGoalStatusKind for each verifier cause × goal-status combination.

— qwen3.7-max via Qwen Code /review

const projected = projectGoalStateToLegacy(payload(cause));

expect(projected.goalStatus.kind).toBe('set');
expect(projected.activeGoal).toMatchObject({
condition: 'ship it',
iterations: 4,
setAt: 100,
lastReason: 'continuing',
});
expect(projected.goalTerminal).toBeNull();
},
);

it('projects completion as achieved and stops active_goal', () => {
const projected = projectGoalStateToLegacy(payload('complete', 'complete'));

expect(projected.goalStatus.kind).toBe('achieved');
expect(projected.activeGoal).toBeNull();
expect(projected.goalTerminal).toMatchObject({
kind: 'achieved',
condition: 'ship it',
iterations: 4,
durationMs: 2000,
});
});

it('projects clear as cleared using the prior goal objective', () => {
const projected = projectGoalStateToLegacy(
payload('clear', 'active', null),
GOAL,
);

expect(projected.goalStatus).toMatchObject({
kind: 'cleared',
condition: 'ship it',
});
expect(projected.activeGoal).toBeNull();
expect(projected.goalTerminal).toBeNull();
});

it('projects pause as a non-terminal legacy paused state', () => {
const projected = projectGoalStateToLegacy(payload('pause', 'paused'));

expect(projected.goalStatus.kind).toBe('paused');
expect(projected.activeGoal).toBeNull();
expect(projected.goalTerminal).toBeNull();
});

it.each(['blocked', 'usage_limited'] as const)(
'projects %s as a legacy stopped state',
(status) => {
const projected = projectGoalStateToLegacy(payload(status, status));

expect(projected.goalStatus.kind).toBe('aborted');
expect(projected.activeGoal).toBeNull();
expect(projected.goalTerminal).toMatchObject({
kind: 'aborted',
condition: 'ship it',
});
},
);

it('uses checking for active runtime progress without widening the union', () => {
const projected = projectGoalStateToLegacy(
payload('turn_finished', 'active'),
);

expect(projected.goalStatus.kind).toBe('checking');
expect(projected.activeGoal).not.toBeNull();
expect(projected.goalTerminal).toBeNull();
});

it('does not repeat an aborted terminal after a paused turn finishes', () => {
const projected = projectGoalStateToLegacy(
payload('turn_finished', 'paused'),
);

expect(projected.goalStatus.kind).toBe('checking');
expect(projected.activeGoal).toBeNull();
expect(projected.goalTerminal).toBeNull();
});
});
137 changes: 137 additions & 0 deletions packages/core/src/goals/goal-legacy-projection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import type { GoalRecord, GoalStateRecordPayloadV2 } from './goal-protocol.js';

export type LegacyGoalStatusKind =
| 'set'
| 'achieved'
| 'cleared'
| 'failed'
| 'aborted'
| 'paused'
| 'checking';

export interface LegacyGoalStatus {
type: 'goal_status';
kind: LegacyGoalStatusKind;
condition: string;
iterations?: number;
setAt?: number;
durationMs?: number;
lastReason?: string;
}

export interface LegacyActiveGoal {
readonly condition: string;
readonly iterations: number;
readonly setAt: number;
readonly tokensAtStart?: number;
readonly hookId?: string;
readonly lastReason?: string;
}

export interface LegacyGoalTerminal {
kind: 'achieved' | 'failed' | 'aborted';
condition: string;
iterations: number;
durationMs: number;
lastReason?: string;
}

export interface LegacyGoalProjection {
activeGoal: LegacyActiveGoal | null;
goalStatus: LegacyGoalStatus;
goalTerminal: LegacyGoalTerminal | null;
}

export function projectGoalStateToLegacy(
payload: GoalStateRecordPayloadV2,
previousGoal: GoalRecord | null = null,
): LegacyGoalProjection {
const snapshotGoal = payload.snapshot.goal;
const displayGoal = snapshotGoal ?? previousGoal;
const kind = legacyStatusKind(payload);
const goalStatus: LegacyGoalStatus = {
type: 'goal_status',
kind,
condition: displayGoal?.objective ?? '',
...(displayGoal ? { iterations: displayGoal.turnCount } : {}),
...(displayGoal ? { setAt: displayGoal.createdAt } : {}),
...(displayGoal ? { durationMs: displayGoal.activeTimeMs } : {}),
...(displayGoal?.lastReason === undefined
? {}
: { lastReason: displayGoal.lastReason }),
Comment on lines +65 to +67

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The lastReason conditional spread appears in three sites (goalStatus line 66, activeGoal line 82, goalTerminal line 97) but no test constructs a goal with lastReason: undefined to verify the field is omitted — Failure scenario: if a conditional spread is accidentally inverted or removed (e.g., { lastReason: displayGoal.lastReason } unconditionally), every test still passes because the test fixture GOAL always sets lastReason: 'continuing'. Downstream consumers would receive lastReason: undefined on goals that never had one, or silently lose it on goals that did.

Suggested test addition:

it('omits lastReason from projection when absent', () => {
  const goalWithoutReason = { ...GOAL, lastReason: undefined };
  const projected = projectGoalStateToLegacy(
    { ...SNAPSHOT, goal: goalWithoutReason },
    undefined,
    { cause: 'edit', now: 200 },
  );
  expect(projected.goalStatus).not.toHaveProperty('lastReason');
  expect(projected.activeGoal).not.toHaveProperty('lastReason');
});

— qwen3.7-max via Qwen Code /review

};
const terminalKind =
kind === 'achieved' || kind === 'failed' || kind === 'aborted'
? kind
: undefined;

return {
activeGoal:
snapshotGoal?.status === 'active'
? {
condition: snapshotGoal.objective,
iterations: snapshotGoal.turnCount,
setAt: snapshotGoal.createdAt,
...(snapshotGoal.lastReason === undefined
? {}
: { lastReason: snapshotGoal.lastReason }),
}
: null,
goalStatus,
goalTerminal:
terminalKind && displayGoal
? {
kind: terminalKind,
condition: displayGoal.objective,
iterations: displayGoal.turnCount,
durationMs: displayGoal.activeTimeMs,
...(displayGoal.lastReason === undefined
? {}
: { lastReason: displayGoal.lastReason }),
}
: null,
};
}

function legacyStatusKind(
payload: GoalStateRecordPayloadV2,
): LegacyGoalStatusKind {
switch (payload.cause) {
case 'create':
case 'replace':
case 'edit':
case 'resume':
case 'migrated':
return 'set';
case 'complete':
return 'achieved';
case 'clear':
return 'cleared';
case 'pause':
return 'paused';
case 'blocked':
case 'usage_limited':
return 'aborted';
case 'turn_finished':
case 'verifier_accept':
case 'verifier_reject':
Comment on lines +122 to +123

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] verifier_accept and verifier_reject are handled here but never tested — Failure scenario: a future change differentiates verifier_reject (e.g., returning 'aborted' directly) → the legacy projection for verifier outcomes silently changes without any test failing.

Add verifier_accept and verifier_reject to the it.each parameterization in goal-legacy-projection.test.ts alongside turn_finished, or add dedicated test cases confirming they project identically for the same snapshot statuses.

— qwen3.7-max via Qwen Code /review

return payload.snapshot.goal?.status === 'complete'
? 'achieved'
: payload.snapshot.goal?.status === 'blocked' ||
payload.snapshot.goal?.status === 'usage_limited'
? 'aborted'
: 'checking';
Comment on lines +121 to +129

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The turn_finished / verifier_accept / verifier_reject case group's inner ternary returns 'achieved' for complete, 'aborted' for blocked/usage_limited, and 'checking' otherwise — but only the 'checking' branch is exercised by tests (via turn_finished with 'active' and 'paused' goals). — Failure scenario: if the ternary logic were inverted or a status comparison used the wrong string literal, no test would catch it. A regression in verifier result projection ships undetected.

// Add parameterized tests mirroring the existing turn_finished patterns:
// payload('turn_finished', 'complete') → goalStatus.kind === 'achieved'
// payload('turn_finished', 'blocked') → goalStatus.kind === 'aborted'
// payload('verifier_accept', 'complete') → goalStatus.kind === 'achieved'
// payload('verifier_reject', 'active') → goalStatus.kind === 'checking'

— qwen3.7-max via Qwen Code /review

default:
return assertNever(payload.cause);
}
}

function assertNever(value: never): never {
throw new Error(`Unsupported Goal state cause: ${String(value)}`);
}
Loading
Loading