mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
fix(session-projection): close migration coverage gaps
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user