fix(session-projection): close migration coverage gaps

This commit is contained in:
_Kerman
2026-08-26 15:14:14 +08:00
parent e296e79b18
commit cd18de61d8
11 changed files with 112 additions and 7 deletions
@@ -1008,6 +1008,27 @@ describe('workspace context request injection', () => {
expect(workspaceContext.inject).toEqual(['sessionProjections'])
})
it('rejects a file-touch projection when the turn boundary unit is absent', async () => {
const ctx = new Context()
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(workspaceContext, { maxBytes: 65536 })
const exec = stubToolExecution({
callId: ToolCallId('missing-turn-boundary'),
name: 'read',
arguments: { file_path: 'file.txt' },
agent: stubAgent('/virtual/repo'),
signal: testToolSignal,
})
expect(() => {
ctx.emit('tools/result', exec, {
content: [{ type: 'text', text: 'ok' }],
isError: false,
value: null,
})
}).toThrow('agent-instructions requires the turnBoundary session projection')
})
it('does not inject baseline context when no filesystem provider is present', async () => {
const ctx = new Context()
try {
@@ -303,7 +303,12 @@ describe('Agent Teams projection events', () => {
teamId: TEAM,
task: task(),
}, 0)
const state = project(ROOT, [invalid])
const later = event('team/task', {
version: 1,
teamId: TEAM,
task: task(),
}, 1)
const state = project(ROOT, [invalid, later])
expect(state.failure).toMatch(/unsupported Agent Teams event version 2/)
expect(isEmptyState(state)).toBe(true)
})
@@ -126,6 +126,25 @@ async function waitRunning(ctx: Context, id: SessionId): Promise<Agent> {
}
describe('Team identity and provisioning', () => {
it('rejects missing and failed authoritative Team projections', async () => {
const first = await setup([])
const journal = teamInternals(first.ctx).journal
const stateOf = first.ctx.sessionProjections.stateOf.bind(first.ctx.sessionProjections)
const stateOfSpy = vi.spyOn(first.ctx.sessionProjections, 'stateOf').mockImplementation((session, key) => (
key === 'team' ? undefined : stateOf(session, key)
))
expect(() => journal.state(first.lead)).toThrow('Agent Teams projection is not registered')
stateOfSpy.mockRestore()
const second = await setup([])
second.lead.session.append('team/task', {
version: 2,
teamId: TeamId(second.lead.id),
} as never)
expect(() => teamInternals(second.ctx).journal.state(second.lead))
.toThrow('unsupported Agent Teams event version 2')
})
it('rejects deployment limits that are not positive safe integers', async () => {
const fields = [
'maxMembers',
+1
View File
@@ -583,6 +583,7 @@ export class GoalService extends TypertRemoteService {
runtime.pendingActivation = { seq: agent.session.seq, activation }
try {
const event = agent.session.append('goal/change', change)
/* v8 ignore next -- Session.append returns the event committed at the pre-append seq. */
if (runtime.pendingActivation.seq === event.seq) runtime.activation = activation
} finally {
runtime.pendingActivation = undefined
@@ -167,6 +167,7 @@ describe('goal projection unit', () => {
current: { ...current, roundsStarted: current.goal.maxGoalRounds + 1 },
}).success).toBe(false)
const empty = goalProjectionDefinition.init()
expect(goalProjectionDefinition.stateSchema.parse(empty)).toEqual(empty)
expect(applyGoalProjection(empty, user)).toBe(empty)
const admittedRound = {
type: 'user/message', seq: 1, time: 2,
@@ -209,6 +210,13 @@ describe('goal projection unit', () => {
// A declared goal/change record with a foreign payload kind is an owned-stream failure.
const foreignKind = { type: 'goal/change', seq: 4, time: 5, data: { kind: 'not-a-goal-change' } } as never
expect(applyGoalProjection(state, foreignKind).failure).toMatch(/invalid kind/)
const missingTimestamps = {
...state,
current: { ...current, createdAt: undefined, updatedAt: undefined },
} as never
expect(applyGoalProjection(missingTimestamps, admittedRound).failure)
.toMatch(/current goal fold lacks timestamps/)
})
it('fails host goal access when the projection retained a replay failure', async () => {
@@ -310,6 +310,7 @@ const SUBAGENT_TYPE = 'general-purpose'
/** The last open turn number in the agent's log, or 0 without an agent. */
function lastTurn(ctx: Context, agent: Agent | undefined): number {
if (!agent) return 0
/* v8 ignore next -- agent-present hook points run inside AgentLoop, which owns this projection. */
return ctx.sessionProjections.stateOf(agent.session, 'turnBoundary')?.lastTurn ?? 0
}
+1
View File
@@ -279,6 +279,7 @@ export function apply(ctx: Context, config: Config): void {
/* jscpd:ignore-start */
function lastTurn(ctx: Context, agent: Agent | undefined): number {
if (!agent) return 0
/* v8 ignore next -- agent-present hook points run inside AgentLoop, which owns this projection. */
return ctx.sessionProjections.stateOf(agent.session, 'turnBoundary')?.lastTurn ?? 0
}
@@ -265,7 +265,10 @@ export class PermissionPresetService extends Service {
}
private knobs(session: Session): KnobState {
return this.ctx.sessionProjections.stateOf(session, 'permissions') ?? EMPTY_KNOBS
const state = this.ctx.sessionProjections.stateOf(session, 'permissions')
/* v8 ignore next -- this service registers the hard-required projection before exposing reads. */
if (state === undefined) throw new Error('permission: permissions session projection is not registered')
return state
}
/**
@@ -363,6 +363,22 @@ describe('switching one session\'s composition', () => {
expect(recordedPreset(agent)).toEqual({ agentPreset: 'minimal' })
})
it('fails loudly when a composition omits the turn boundary projection', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-no-turn-boundary', 'standard')
const stateOf = ctx.sessionProjections.stateOf.bind(ctx.sessionProjections)
vi.spyOn(ctx.sessionProjections, 'stateOf').mockImplementation((session, key) => (
key === 'turnBoundary' ? undefined : stateOf(session, key)
))
const failure = await remoteFailure(ctx.agentPresets.select(agent, 'minimal'))
expect(failure).toMatchObject({
code: 'internal',
message: expect.stringContaining('select requires the turnBoundary session projection') as string,
})
})
it('serializes two concurrent switches on one session', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-race', 'standard')
@@ -89,6 +89,21 @@ describe('SessionObservationReader', () => {
await ctx.fiber.dispose()
})
it('returns a prepared observation without projections when no registry is mounted', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const meta = header('prepared-without-projections')
ctx.provide('sessionPersistence', {
borrowSession: () => Promise.resolve(preparedSource(meta)),
} as never)
using observed = await new SessionObservationReader(ctx).read(meta.id)
expect(observed.source).toBe('prepared')
expect(observed.projections).toBeUndefined()
await ctx.fiber.dispose()
})
it('reference-counts prepared leases and rejects retention after disposal', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
@@ -85,12 +85,27 @@ describe('title projection unit', () => {
const checkpoint = ctx.sessionProjections.checkpoint(session)
const row = checkpoint.titleInput
expect(row).toBeDefined()
const malformed = {
...checkpoint,
titleInput: { ...row!, val: { first: null, count: 1, lastSeq: null } },
const invalidStates = [
{ first: null, count: 1, lastSeq: null },
{ first: { seq: 1, text: 'first' }, count: 1, lastSeq: null },
{ first: { seq: 1, text: 'first' }, count: 0, lastSeq: 1 },
{ first: { seq: 2, text: 'first' }, count: 1, lastSeq: 1 },
]
for (const state of invalidStates) {
const malformed = {
...checkpoint,
titleInput: { ...row!, val: state },
}
expect(() => ctx.sessionProjections.restore(malformed, [], 0, session.header))
.toThrow(/title input state must pair its count with first and last message seqs/)
}
expect(() => ctx.sessionProjections.restore(malformed, [], 0, session.header))
.toThrow(/title input state must pair its count with first and last message seqs/)
expect(() => ctx.sessionProjections.restore({
...checkpoint,
titleInput: {
...row!,
val: { first: { seq: 1, text: 'first' }, count: 1, lastSeq: 1 },
},
}, [], 0, session.header)).not.toThrow()
})
})