test(goal): close the per-file coverage gaps for the goal surface

- projection.spec covers applyGoalProjection's non-message early return and
  the foreign-change-kind arm directly (the pure-function posture the unit
  requires — a throwing apply would tear down the registry drive).
- New ui-goal browser-plugin spec exercises the plugin body over a real
  cordis Context with fake faces: dock registration (id/order), verbs
  reading the CAS ref from the current projected value, the
  no-current-goal short circuit, RPC-error mapping, HMR disposal, the
  GoalDock adapter's three projection states, and the inert node half.
This commit is contained in:
imccyu
2026-07-28 23:41:19 +08:00
parent da4e54b515
commit 1c43ff3061
6 changed files with 196 additions and 13 deletions

View File

@@ -100,6 +100,10 @@ export function applyGoalProjection(state: GoalProjection | null, event: Session
const source = event.data.source
if (source.kind !== 'goal' || source.round !== 0) return state
const change = source.change
// Session-log data is a durable boundary: the static type promises the kind,
// but a foreign or corrupted change record must degrade to same-reference,
// never feed the zod parse in the registry drive.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- durable-boundary guard
if (change === undefined || change.kind !== 'goal/change') return state
if (change.operation === 'clear') return null
return {

View File

@@ -46,7 +46,7 @@ function liveAgent(ctx: Context, session: Session): Agent {
},
cancel() {},
whenIdle() { return Promise.resolve() },
} as Agent
}
ctx.agents.register(agent)
return agent
}
@@ -63,7 +63,7 @@ async function harness(withGoal: boolean): Promise<Bench> {
ctx,
session,
agent,
tailValues: () => ctx.sessionProjections.snapshot(session).values as Record<string, unknown>,
tailValues: () => ctx.sessionProjections.snapshot(session).values,
tailAsOfSeq: () => ctx.sessionProjections.snapshot(session).asOfSeq,
}
}
@@ -142,6 +142,18 @@ describe('goal projection unit', () => {
// Same-reference return: the registry's Object.is gate sees no change.
expect(applyGoalProjection(state, malformed)).toBe(state)
expect(applyGoalProjection(null, malformed)).toBeNull()
// A non-message event (the registry drives EVERY committed event through
// apply): early same-reference return.
const turnStart = { type: 'turn/start', seq: 3, time: 4, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } } as never
expect(applyGoalProjection(state, turnStart)).toBe(state)
// A round-zero goal source whose change carries a foreign kind: same posture.
const foreignKind = { type: 'user/message', seq: 2, time: 3, data: createUserMessage({
content: [{ type: 'text', text: 'foreign' }],
source: { kind: 'goal', goalId: 'g1', revision: 1, round: 0, change: { kind: 'not-a-goal-change' } } as never,
}) } as never
expect(applyGoalProjection(state, foreignKind)).toBe(state)
})
it('has no goal key when the goal service is not composed', async () => {