test(session): align turn-end contract artifacts

This commit is contained in:
_Kerman
2026-08-04 14:09:52 +08:00
parent d4fa26023d
commit e874910a76
186 changed files with 408 additions and 414 deletions

View File

@@ -221,7 +221,7 @@ describe('new-session default', () => {
})
const legacy = freshSession('legacy-source')
legacy.append('turn/start', { turn: 1 })
legacy.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
legacy.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
const resumed = ctx.sessions.create(SessionId('legacy-resumed'), { seed: legacy.events })
expect(ctx.permission.current(resumed.events)).toBe('workspace-write')
expect(resumed.events.slice(-3).map(event => event.type)).toEqual([

View File

@@ -407,7 +407,7 @@ describe('TUI terminal-state snapshots', () => {
],
})
session.append('step/end', { turn: 1, step: 1 })
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
session.append('turn/start', {
turn: 2,
})
@@ -454,7 +454,7 @@ describe('TUI terminal-state snapshots', () => {
},
}),
}, { surfaceOp: 'append' })
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
harness.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
await checkpoint('retry-recovered', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
@@ -473,7 +473,7 @@ describe('TUI terminal-state snapshots', () => {
delayMs: 1_000,
failure: { message: 'temporary transport failure', code: 'TRANSPORT' },
})
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'aborted', reason: { kind: 'user' } },
harness.session.append('turn/end', { turn: 1, reason: { kind: 'aborted', reason: { kind: 'user' } },
})
})
await checkpoint('retry-cancelled', harness.terminal, { includeScrollback: true })
@@ -489,7 +489,7 @@ describe('TUI terminal-state snapshots', () => {
step: 3,
chunk: { type: 'text-delta', index: 0, text: 'discarded terminal partial output' },
})
harness.session.append('turn/end', { turn: 1, step: 3, reason: { kind: 'error', error: { message: 'provider still unavailable', code: 'SERVER', status: 503 },
harness.session.append('turn/end', { turn: 1, reason: { kind: 'error', error: { message: 'provider still unavailable', code: 'SERVER', status: 503 },
},
})
})
@@ -645,7 +645,7 @@ describe('TUI terminal-state snapshots', () => {
harness.session.append('step/start', { turn: 1, step: 2 })
appendAssistant(harness.session, [{ type: 'text', text: 'The renderer is sound; no refactor needed.' }], undefined, { turn: 1, step: 2 })
harness.session.append('step/end', { turn: 1, step: 2 })
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
harness.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
})
// collapsed -> expanded -> hidden: one Assistant header, no tool card.
await renderAfter(harness, () => { harness.terminal.send('\x0f') })
@@ -672,7 +672,7 @@ describe('TUI terminal-state snapshots', () => {
])
appendToolResult(harness.session, 'details-1', [{ type: 'text', text: 'all tests pass' }])
harness.session.append('step/end', { turn: 1, step: 1 })
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
harness.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
})
await renderAfter(harness, () => {
harness.terminal.send('/details hidden reasoning off')
@@ -733,7 +733,7 @@ describe('TUI terminal-state snapshots', () => {
todos: [{ content: `Unsafe todo ${CONTROL_PROBE}`, status: 'in_progress' }],
})
session.append('step/end', { turn: 1, step: 1 })
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: { message: `Unsafe turn error ${CONTROL_PROBE}`, code: 'UNKNOWN' } },
session.append('turn/end', { turn: 1, reason: { kind: 'error', error: { message: `Unsafe turn error ${CONTROL_PROBE}`, code: 'UNKNOWN' } },
})
},
}, { columns: 100, rows: 34 })
@@ -905,18 +905,18 @@ describe('TUI terminal-state snapshots', () => {
harness.terminal.send('\r')
agentEvents(harness.ctx, harness.agent).emit('agent/error', 1, 1, new Error('provider stream failed after partial output'))
harness.session.append('step/end', { turn: 1, step: 1 })
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'provider stream failed after partial output', code: 'UNKNOWN' } },
harness.session.append('turn/end', { turn: 1, reason: { kind: 'error', error: { message: 'provider stream failed after partial output', code: 'UNKNOWN' } },
})
harness.session.append('turn/start', { turn: 2 })
harness.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'interrupted' },
harness.session.append('turn/end', { turn: 2, reason: { kind: 'interrupted' },
})
harness.session.append('turn/start', { turn: 3 })
harness.session.append('turn/end', { turn: 3, step: 0, reason: { kind: 'aborted', reason: { kind: 'disposed' } },
harness.session.append('turn/end', { turn: 3, reason: { kind: 'aborted', reason: { kind: 'disposed' } },
})
harness.session.append('turn/start', { turn: 4 })
// A merge-extensible turn-end kind unknown to the TUI still surfaces its
// name so the agent never stops without a visible reason.
harness.session.append('turn/end', { turn: 4, step: 0, reason: { kind: 'plugin-policy' } as never })
harness.session.append('turn/end', { turn: 4, reason: { kind: 'plugin-policy' } as never })
})
await checkpoint('errors-and-help', harness.terminal, { includeScrollback: true })
@@ -967,7 +967,7 @@ describe('TUI terminal-state snapshots', () => {
}),
}, surfaceOp: 'append' },
{ type: 'step/end', seq: 5, time: Date.parse(`${day}T00:00:06Z`), data: { turn: 1, step: 1 } },
{ type: 'turn/end', seq: 6, time: Date.parse(`${day}T00:00:07Z`), data: { turn: 1, step: 1, reason: { kind: 'completed' } } },
{ type: 'turn/end', seq: 6, time: Date.parse(`${day}T00:00:07Z`), data: { turn: 1, reason: { kind: 'completed' } } },
{ type: 'session/title', seq: 7, time: Date.parse(`${day}T00:00:08Z`), data: { title, messageSeqs: [1], source: { kind: 'fallback' } } },
],
})

View File

@@ -281,7 +281,7 @@ describe('goodbye message and /resume', () => {
}),
}, surfaceOp: 'append' },
{ type: 'step/end', seq: 5, time: time + 5, data: { turn: 1, step: 1 } },
{ type: 'turn/end', seq: 6, time: time + 6, data: { turn: 1, step: 1, reason } },
{ type: 'turn/end', seq: 6, time: time + 6, data: { turn: 1, reason } },
{ type: 'session/title', seq: 7, time: time + 7, data: { title, messageSeqs: [1], source: { kind: 'fallback' } } },
]
/** Derive the selector's batch title read from a fake per-session readSession. */
@@ -1681,10 +1681,10 @@ describe('pi-tui chat lifecycle and transcript', () => {
}), { surfaceOp: 'append' })
appendAssistant(result.session, [])
result.session.append('step/end', { turn: 1, step: 1 })
result.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'aborted', reason: { kind: 'user' } },
result.session.append('turn/end', { turn: 1, reason: { kind: 'aborted', reason: { kind: 'user' } },
})
result.session.append('turn/start', { turn: 2 })
result.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'completed' } })
result.session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
result.session.append('turn/start', { turn: 3 })
result.session.append('step/start', { turn: 3, step: 1 })
result.session.append('assistant/chunk', {
@@ -2172,7 +2172,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
session.append('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'done' } })
clock += 1_000
session.append('step/end', { turn: 1, step: 1 })
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
},
})
result.agent.status = 'running'
@@ -4384,25 +4384,25 @@ describe('pi-tui chat lifecycle and transcript', () => {
agentEvents(events.ctx, unrelatedAgent).emit('agent/disposed')
agentEvents(events.ctx, events.agent).emit('agent/error', 1, 1, new Error('live failure'))
events.session.append('step/end', { turn: 1, step: 1 })
events.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'live failure', code: 'UNKNOWN' } } })
events.session.append('turn/end', { turn: 1, reason: { kind: 'error', error: { message: 'live failure', code: 'UNKNOWN' } } })
events.session.append('turn/start', { turn: 2 })
events.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'error', error: { message: 'durable failure', code: 'UNKNOWN' } } })
events.session.append('turn/end', { turn: 2, reason: { kind: 'error', error: { message: 'durable failure', code: 'UNKNOWN' } } })
events.session.append('turn/start', { turn: 3 })
events.session.append('turn/end', { turn: 3, step: 0, reason: { kind: 'aborted', reason: { kind: 'user' } },
events.session.append('turn/end', { turn: 3, reason: { kind: 'aborted', reason: { kind: 'user' } },
})
events.session.append('turn/start', { turn: 4 })
events.session.append('turn/end', { turn: 4, step: 0, reason: { kind: 'max-tokens' } })
events.session.append('turn/end', { turn: 4, reason: { kind: 'max-tokens' } })
events.session.append('turn/start', { turn: 5 })
events.session.append('turn/end', { turn: 5, step: 0, reason: { kind: 'interrupted' } })
events.session.append('turn/end', { turn: 5, reason: { kind: 'interrupted' } })
events.session.append('turn/start', { turn: 6 })
events.session.append('turn/end', { turn: 6, step: 0, reason: { kind: 'error', error: { message: 'structured provider failure', code: 'SERVER' } },
events.session.append('turn/end', { turn: 6, reason: { kind: 'error', error: { message: 'structured provider failure', code: 'SERVER' } },
})
events.session.append('turn/start', { turn: 8 })
events.session.append('turn/end', { turn: 8, step: 0, reason: { kind: 'aborted', reason: { kind: 'disposed' } },
events.session.append('turn/end', { turn: 8, reason: { kind: 'aborted', reason: { kind: 'disposed' } },
})
events.session.append('turn/start', { turn: 9 })
// Merge-extensible reason kind unknown to the TUI still names the stop.
events.session.append('turn/end', { turn: 9, step: 0, reason: { kind: 'plugin-policy' } as never })
events.session.append('turn/end', { turn: 9, reason: { kind: 'plugin-policy' } as never })
agentEvents(events.ctx, events.agent).emit('agent/disposed')
await tick()
expect(events.terminal.output).toContain('live failure')
@@ -5612,7 +5612,7 @@ describe('tool cards and surface replay', () => {
session.append('step/start', { turn: 1, step: 2 })
appendAssistant(session, [{ type: 'text', text: 'second step text' }], undefined, { turn: 1, step: 2 })
session.append('step/end', { turn: 1, step: 2 })
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
}
it('folds a turn to one Assistant header in hidden mode and restores headers on cycle', async () => {
@@ -5664,14 +5664,14 @@ describe('tool cards and surface replay', () => {
result.session.append('step/start', { turn: 1, step: 2 })
appendAssistant(result.session, [{ type: 'text', text: 'late turn-one text' }], undefined, { turn: 1, step: 2 })
result.session.append('step/end', { turn: 1, step: 2 })
result.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'completed' } })
result.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
// Turn 2 keeps its own header.
result.session.append('turn/start', { turn: 2 })
appendUser(result.session, 'next turn')
result.session.append('step/start', { turn: 2, step: 1 })
appendAssistant(result.session, [{ type: 'text', text: 'turn-two text' }], undefined, { turn: 2, step: 1 })
result.session.append('step/end', { turn: 2, step: 1 })
result.session.append('turn/end', { turn: 2, step: 1, reason: { kind: 'completed' } })
result.session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
await tick()
result.terminal.send('\x0f')

View File

@@ -38,7 +38,7 @@ describe('approval invariants', () => {
await ctx.plugin(InvariantService)
await ctx.plugin(ApprovalInvariant)
expect(() => session.append('approval/decided', { id, outcome: 'cancelled' })).not.toThrow()
session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
})
it('adopts a bare session first observed through publication', async () => {
@@ -77,7 +77,7 @@ describe('approval invariants', () => {
await ctx.plugin(SessionStore)
const session = ctx.sessions.create()
startTurn(session)
session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'completed' } })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
session.append('approval/asked', {
id: ApprovalRequestId('ask-replay'), toolName: 'bash',
})