test(session): align turn-end contract artifacts
This commit is contained in:
@@ -201,7 +201,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
|| event.type === 'step/start' || event.type === 'turn/end').map(event => event.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(agent.session.events.find(event => event.type === 'turn/end')?.data)
|
||||
.toEqual({ turn: 1, step: 0, reason: { kind: 'completed' } })
|
||||
.toEqual({ turn: 1, reason: { kind: 'completed' } })
|
||||
expect(agent.inbox.nextTurn).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -357,7 +357,7 @@ describe('plugin exceptions are contained', () => {
|
||||
send(agent, 'first')
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(agent.session.events.findLast(event => event.type === 'turn/end')).toMatchObject({
|
||||
data: { step: 1, reason: { kind: 'error', error: { message: 'broken continuation plugin', code: 'UNKNOWN' } } },
|
||||
data: { reason: { kind: 'error', error: { message: 'broken continuation plugin', code: 'UNKNOWN' } } },
|
||||
})
|
||||
|
||||
// the loop is still alive: a second send works normally
|
||||
@@ -601,7 +601,7 @@ describe('a finish-error stream chunk ends the turn as error, not completed', ()
|
||||
|
||||
const events = [...agent.session.events]
|
||||
const turnEnd = events.find(event => event.type === 'turn/end')
|
||||
expect(turnEnd).toMatchObject({ data: { step: 1, reason: { kind: 'error', error: failure } } })
|
||||
expect(turnEnd).toMatchObject({ data: { reason: { kind: 'error', error: failure } } })
|
||||
// A failed step must not synthesize an assistant message.
|
||||
expect(events.some(event => event.type === 'assistant/message')).toBe(false)
|
||||
})
|
||||
@@ -781,7 +781,7 @@ describe('turn and step boundary recovery', () => {
|
||||
errors: 1,
|
||||
})
|
||||
expect(agent.session.events.findLast(event => event.type === 'turn/end')).toMatchObject({
|
||||
data: { step: 0, reason: { kind: 'error', error: { message: 'reject step-start before commit', code: 'UNKNOWN' } } },
|
||||
data: { reason: { kind: 'error', error: { message: 'reject step-start before commit', code: 'UNKNOWN' } } },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('thrown-value propagation', () => {
|
||||
expect(starts).toHaveLength(0)
|
||||
expect(ends).toHaveLength(0)
|
||||
expect(messages).toHaveLength(0)
|
||||
expect(agent.inbox.nextTurn).toHaveLength(1)
|
||||
expect(agent.inbox.nextTurn).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('preserves non-Error throws from the agent/request waterfall', async () => {
|
||||
|
||||
@@ -565,14 +565,15 @@ describe('agent loop', () => {
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(0)
|
||||
expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(1)
|
||||
expect(agent.session.events.filter(event => event.type === 'turn/end')).toHaveLength(1)
|
||||
expect(agent.inbox.nextStep).toHaveLength(1)
|
||||
|
||||
send(agent, 'resume')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(1)
|
||||
expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(2)
|
||||
expect(JSON.stringify(adapter.requests[0]?.messages)).toContain('pending steering')
|
||||
})
|
||||
|
||||
@@ -867,11 +868,11 @@ describe('agent loop', () => {
|
||||
|
||||
send(agent, 'first')
|
||||
await waitForIdle(ctx, agent)
|
||||
// The first proposal failed before opening a turn or calling the model.
|
||||
// The first proposal failed inside a balanced turn without calling the model.
|
||||
expect(errors.map(error => error.message)).toEqual(['boom in pre-step'])
|
||||
expect(adapter.requests.length).toBe(0)
|
||||
expect(agent.session.events.some(event => event.type === 'turn/start')).toBe(false)
|
||||
expect(agent.session.events.some(event => event.type === 'turn/end')).toBe(false)
|
||||
expect(agent.session.events.some(event => event.type === 'turn/start')).toBe(true)
|
||||
expect(agent.session.events.some(event => event.type === 'turn/end')).toBe(true)
|
||||
|
||||
// The loop survived: a second prompt runs a normal completed turn.
|
||||
send(agent, 'second')
|
||||
|
||||
@@ -360,7 +360,6 @@ describe('request stability across the loop', () => {
|
||||
|
||||
expect(agent.session.events.findLast(event => event.type === 'turn/end')).toMatchObject({
|
||||
data: {
|
||||
step: 1,
|
||||
reason: failure instanceof LlmError
|
||||
? { kind: 'error', error: failure.failure }
|
||||
: { kind: 'error', error: { message: failure.message, code: 'UNKNOWN' } },
|
||||
|
||||
@@ -44,7 +44,7 @@ async function persistSession(sessionId: SessionId): Promise<string> {
|
||||
// the model merely to construct this lifecycle fixture.
|
||||
const seed: SessionEvent[] = [
|
||||
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
|
||||
{ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, step: 0, reason: { kind: 'completed' } } },
|
||||
{ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
]
|
||||
const session = ctx.sessions.create(sessionId, { seed })
|
||||
await ctx.sessions.flush(session)
|
||||
@@ -198,7 +198,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
|
||||
await expect(ctx.agents.resume({ resumeSessionId: sessionId }))
|
||||
.rejects.toThrow(/live turn is open/)
|
||||
|
||||
first.session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
|
||||
first.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
await ctx.sessions.flush(first.session)
|
||||
const loaded = await ctx.sessionPersistence.load(sessionId)
|
||||
expect(loaded.events.map(event => event.type)).toEqual(['turn/start', 'turn/end'])
|
||||
@@ -548,7 +548,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
|
||||
// materializes the fork (header + seed) on disk.
|
||||
const seed: SessionEvent[] = [
|
||||
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
|
||||
{ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, step: 0, reason: { kind: 'completed' } } },
|
||||
{ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
]
|
||||
const adapter1 = new MockAdapter([textResponse('a')])
|
||||
const { ctx: ctx1, root } = await persistentHarness(adapter1)
|
||||
|
||||
@@ -683,7 +683,7 @@ describe('tool-call scheduler: failure quiescence', () => {
|
||||
expect(turnEndBeforeDrain).toBeUndefined()
|
||||
expect(gated.pending()).toEqual([])
|
||||
expect(events(agent).findLast(event => event.type === 'turn/end')).toMatchObject({
|
||||
data: { step: 1, reason: { kind: 'error', error: { message: schedulerError.message, code: 'UNKNOWN' } } },
|
||||
data: { reason: { kind: 'error', error: { message: schedulerError.message, code: 'UNKNOWN' } } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('loop-level canonical tool order', () => {
|
||||
expect(Object.isFrozen(adapter.requests[0])).toBe(true)
|
||||
})
|
||||
|
||||
it('fails before opening a turn when toolOrder names an unregistered tool', async () => {
|
||||
it('closes a no-step turn when toolOrder names an unregistered tool', async () => {
|
||||
const adapter = new MockAdapter([textResponse('never sent')])
|
||||
const ctx = await harness(adapter, ['ghost', TOOL_ORDER_REST])
|
||||
registerNamed(ctx, 'alpha')
|
||||
@@ -102,8 +102,8 @@ describe('loop-level canonical tool order', () => {
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(foldRequestHeader(agent.session.events)).toBeUndefined()
|
||||
expect(agent.session.events.some(e => e.type === 'turn/start')).toBe(false)
|
||||
expect(agent.session.events.some(e => e.type === 'turn/end')).toBe(false)
|
||||
expect(agent.session.events.some(e => e.type === 'turn/start')).toBe(true)
|
||||
expect(agent.session.events.some(e => e.type === 'turn/end')).toBe(true)
|
||||
expect(agent.session.events.some(e => e.type === 'step/start')).toBe(false)
|
||||
expect(agent.session.events.some(e => e.type === 'step/end')).toBe(false)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user