test: migrate consumers to inbox and owned-run APIs
This commit is contained in:
@@ -2,6 +2,7 @@ import { createUserMessage, MessageId , createMessage } from '@deepseek-ai/dsh-l
|
||||
import { Context } from 'cordis'
|
||||
import type { Terminal } from '@earendil-works/pi-tui'
|
||||
import AgentRegistry, {
|
||||
Inbox,
|
||||
type Agent,
|
||||
type AgentCancelCause,
|
||||
type AgentOptions,
|
||||
@@ -190,6 +191,7 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
id: sessionId,
|
||||
options: options.agentOptions ?? { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
||||
session,
|
||||
inbox: new Inbox(session),
|
||||
status: options.status ?? 'idle',
|
||||
ctx,
|
||||
sent,
|
||||
|
||||
@@ -387,7 +387,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
})
|
||||
harness.session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: { kind: 'aborted' },
|
||||
reason: { kind: 'aborted', reason: { kind: 'user' } },
|
||||
})
|
||||
})
|
||||
await checkpoint('retry-cancelled', harness.terminal, { includeScrollback: true })
|
||||
@@ -407,8 +407,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
turn: 1,
|
||||
reason: {
|
||||
kind: 'error',
|
||||
step: 3,
|
||||
failure: { message: 'provider still unavailable', code: 'SERVER', status: 503 },
|
||||
error: { message: 'provider still unavailable', code: 'SERVER', status: 503 },
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -587,7 +586,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: { kind: 'error', step: 1, message: `Unsafe turn error ${CONTROL_PROBE}` },
|
||||
reason: { kind: 'error', error: `Unsafe turn error ${CONTROL_PROBE}` },
|
||||
})
|
||||
},
|
||||
}, { columns: 100, rows: 34 })
|
||||
@@ -771,7 +770,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
harness.session.append('step/end', { turn: 1, step: 1 })
|
||||
harness.session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: { kind: 'error', step: 1, message: 'provider stream failed after partial output' },
|
||||
reason: { kind: 'error', error: 'provider stream failed after partial output' },
|
||||
})
|
||||
harness.session.append('turn/start', { turn: 2 })
|
||||
harness.session.append('turn/end', {
|
||||
@@ -779,7 +778,10 @@ describe('TUI terminal-state snapshots', () => {
|
||||
reason: { kind: 'interrupted' },
|
||||
})
|
||||
harness.session.append('turn/start', { turn: 3 })
|
||||
harness.session.append('turn/end', { turn: 3, 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.
|
||||
|
||||
@@ -5,17 +5,14 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { CombinedAutocompleteProvider, visibleWidth, type Terminal } from '@earendil-works/pi-tui'
|
||||
import AgentRegistry, {
|
||||
agentEvents, assembleContextFor, InboxItemId, type Agent, type InboxItem,
|
||||
type InboxPlacement,
|
||||
agentEvents, assembleContextFor, Inbox, type Agent,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage,
|
||||
createToolResultMessage,
|
||||
ReasoningEffortId,
|
||||
type LlmCallConfig,
|
||||
type LlmModelReasoningInfo,
|
||||
MessageId,
|
||||
createMessage,
|
||||
freezeMessage,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { GOAL_CHANGE_VERSION, GoalId, renderGoalChange, type GoalSnapshotChangeMeta } from '@deepseek-ai/dsh-goal'
|
||||
import CommandService, { type CommandInvocation } from '@deepseek-ai/dsh-commands'
|
||||
@@ -54,13 +51,6 @@ const UNUSED_TOOL_OUTPUT: ToolDefinition['output'] = {
|
||||
render: () => [],
|
||||
}
|
||||
|
||||
let nextInboxItem = 0
|
||||
|
||||
/** Wrap one test message in the production inbox occurrence envelope. */
|
||||
function inboxItem(message: InboxItem['message'], placement: InboxPlacement): InboxItem {
|
||||
return { id: InboxItemId(`tui-item-${nextInboxItem++}`), message, placement }
|
||||
}
|
||||
|
||||
class FakeTerminal implements Terminal {
|
||||
columns = 88
|
||||
rows = 32
|
||||
@@ -461,9 +451,9 @@ describe('goodbye message and /resume', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ kind: 'aborted' }, 'cancelled'],
|
||||
[{ kind: 'error', step: 1, message: 'failed' }, 'error'],
|
||||
[{ kind: 'disposed' }, 'disposed'],
|
||||
[{ kind: 'aborted', reason: { kind: 'user' } }, 'cancelled'],
|
||||
[{ kind: 'error', error: new Error('failed') }, 'error'],
|
||||
[{ kind: 'aborted', reason: { kind: 'disposed' } }, 'cancelled'],
|
||||
[{ kind: 'max-tokens' }, 'max tokens'],
|
||||
[{ kind: 'interrupted' }, 'interrupted'],
|
||||
[{ kind: 'future-result' } as unknown as TurnEndReason, 'unknown result'],
|
||||
@@ -1342,7 +1332,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, reason: { kind: 'aborted' } })
|
||||
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, reason: { kind: 'completed' } })
|
||||
result.session.append('turn/start', { turn: 3 })
|
||||
@@ -1660,16 +1653,16 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
const submitSteering = (text: string): void => {
|
||||
result.terminal.send(text)
|
||||
result.terminal.send('\r')
|
||||
const message = result.agent.steeredOptions.at(-1)
|
||||
if (message !== undefined) {
|
||||
result.agent.inbox.splice('next-step', result.agent.inbox.nextStep.length, 0, [message])
|
||||
}
|
||||
}
|
||||
const drainSteering = (text: string): void => {
|
||||
const id = result.agent.steeredIds.shift()
|
||||
if (id !== undefined) {
|
||||
result.ctx.emit('agent/inbox/dequeue', result.agent, inboxItem(freezeMessage({
|
||||
id,
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text }],
|
||||
source: { kind: 'user' },
|
||||
}), 'steering'))
|
||||
const index = result.agent.inbox.nextStep.findIndex(message => message.id === id)
|
||||
if (index >= 0) result.agent.inbox.splice('next-step', index, 1, [], 'admitted')
|
||||
}
|
||||
result.session.append('steering/message', {
|
||||
turn: 1,
|
||||
@@ -1680,18 +1673,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
}, { surfaceOp: 'append' })
|
||||
}
|
||||
|
||||
// A steering queue for a different agent never touches this status line.
|
||||
const other = { ...result.agent, id: SessionId('other') } as Agent
|
||||
result.terminal.output = ''
|
||||
result.ctx.emit('agent/inbox/enqueue', other, inboxItem(freezeMessage({
|
||||
id: MessageId('stub'),
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'elsewhere' }],
|
||||
source: { kind: 'user' },
|
||||
}), 'queued'))
|
||||
await tick()
|
||||
expect(result.terminal.output).not.toContain('queued')
|
||||
|
||||
// Two steering messages queue while the turn runs.
|
||||
submitSteering('first')
|
||||
result.terminal.output = ''
|
||||
@@ -1753,34 +1734,15 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
submitSteering('fourth')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('2 queued')
|
||||
const discarded = result.agent.steeredIds.splice(0).map(id => freezeMessage({
|
||||
id,
|
||||
role: 'user' as const,
|
||||
content: [{ type: 'text' as const, text: 'discarded' }],
|
||||
source: { kind: 'user' as const },
|
||||
}))
|
||||
// Another agent's dequeue/discard, and ones naming no pending id, leave
|
||||
// the badge alone.
|
||||
result.ctx.emit('agent/inbox/dequeue', other, inboxItem(discarded[0]!, 'steering'))
|
||||
result.ctx.emit('agent/inbox/dequeue', result.agent, inboxItem(freezeMessage({
|
||||
id: MessageId('never-queued'),
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'x' }],
|
||||
source: { kind: 'user' },
|
||||
}), 'steering'))
|
||||
result.ctx.emit('agent/inbox/discard', other, discarded.map(message => inboxItem(message, 'steering')))
|
||||
result.ctx.emit('agent/inbox/discard', result.agent, [
|
||||
inboxItem(freezeMessage({
|
||||
id: MessageId('never-queued'),
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'x' }],
|
||||
source: { kind: 'user' },
|
||||
}), 'steering'),
|
||||
])
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('2 queued')
|
||||
result.terminal.output = ''
|
||||
result.ctx.emit('agent/inbox/discard', result.agent, discarded.map(message => inboxItem(message, 'steering')))
|
||||
result.agent.steeredIds.splice(0)
|
||||
result.agent.inbox.splice(
|
||||
'next-step',
|
||||
0,
|
||||
result.agent.inbox.nextStep.length,
|
||||
[],
|
||||
'canceled',
|
||||
)
|
||||
await tick()
|
||||
expect(result.terminal.output).not.toContain('queued')
|
||||
|
||||
@@ -2096,12 +2058,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
it('tracks steering drains without a running status line', async () => {
|
||||
const result = await setup()
|
||||
const source = { kind: 'user' as const }
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, inboxItem(freezeMessage({
|
||||
id: MessageId('stub'),
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'early' }],
|
||||
source,
|
||||
}), 'steering'))
|
||||
result.session.append('steering/message', {
|
||||
turn: 1,
|
||||
message: createUserMessage({
|
||||
@@ -2609,21 +2565,25 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
// Idle: the snapshot rides the prompt's admission (additionalContexts on
|
||||
// the allow decision), not a separate pre-admission inject.
|
||||
expect(result.agent.injected).toHaveLength(0)
|
||||
const submitted = result.agent.sentMessages[0]!
|
||||
const decision = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', result.agent.sentMessages[0]!,
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [submitted],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [submitted] }),
|
||||
)
|
||||
expect(decision.kind).toBe('allow')
|
||||
expect(decision.kind === 'allow' && decision.additionalContexts?.[0]?.source)
|
||||
expect(decision.kind === 'allow'
|
||||
&& decision.messages.find(message => message.source.kind === 'session-reference')?.source)
|
||||
.toMatchObject({ kind: 'session-reference', references: [{ sessionId: 'source-session' }] })
|
||||
|
||||
// The one-shot wrapper detached itself at admission: replaying the
|
||||
// waterfall attaches nothing a second time.
|
||||
const replay = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', result.agent.sentMessages[0]!,
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [submitted],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [submitted] }),
|
||||
)
|
||||
expect(replay.kind === 'allow' && replay.additionalContexts).toBeUndefined()
|
||||
expect(replay.kind === 'allow' && replay.messages).toEqual([submitted])
|
||||
|
||||
const mention = formatSessionReferenceMention({ sessionId: sourceId, label: 'Source chat' })
|
||||
result.agent.status = 'running'
|
||||
@@ -2662,31 +2622,32 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
// Running each prompt's admission waterfall detaches its wrapper.
|
||||
for (const sent of result.agent.sentMessages) {
|
||||
await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', sent,
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [sent],
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const, messages: [sent] }),
|
||||
)
|
||||
}
|
||||
// Both wrappers now gone: a discard naming either prompt's content finds
|
||||
// no armed listener, and an unrelated admission is untouched. The leak
|
||||
// regression: a listener installed after its cleanup already ran would
|
||||
// survive every future cleanup.
|
||||
result.ctx.emit('agent/inbox/discard', result.agent, [inboxItem(result.agent.sentMessages[0]!, 'queued')])
|
||||
const unrelatedMessage = createUserMessage({
|
||||
content: [{ type: 'text', text: 'unrelated' }],
|
||||
source: { kind: 'user' },
|
||||
})
|
||||
const unrelated = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', createUserMessage({
|
||||
content: [{ type: 'text', text: 'unrelated' }],
|
||||
source: { kind: 'user' },
|
||||
}),
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [unrelatedMessage],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [unrelatedMessage] }),
|
||||
)
|
||||
expect(unrelated.kind === 'allow' && unrelated.additionalContexts).toBeUndefined()
|
||||
expect(unrelated.kind === 'allow' && unrelated.messages).toEqual([unrelatedMessage])
|
||||
// Replaying either sent prompt attaches nothing: the one-shot wrappers
|
||||
// are gone, not merely spent.
|
||||
for (const sent of result.agent.sentMessages) {
|
||||
const replay = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', sent,
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [sent],
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const, messages: [sent] }),
|
||||
)
|
||||
expect(replay.kind === 'allow' && replay.additionalContexts).toBeUndefined()
|
||||
expect(replay.kind === 'allow' && replay.messages).toEqual([sent])
|
||||
}
|
||||
await dispose(result)
|
||||
})
|
||||
@@ -2704,20 +2665,11 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
// Real send() publishes its already identified snapshot, then an enqueue
|
||||
// listener may synchronously cancel and discard it before followup()
|
||||
// returns that id. This stub reproduces that ordering.
|
||||
const foreign = { ...result.agent, id: SessionId('foreign') } as unknown as Agent
|
||||
result.agent.followup = (input) => {
|
||||
result.agent.sent.push(input.content)
|
||||
result.agent.sentMessages.push(input)
|
||||
const message = freezeMessage({
|
||||
id: input.id,
|
||||
role: 'user' as const,
|
||||
content: structuredClone(input.content),
|
||||
source: structuredClone(input.source),
|
||||
})
|
||||
result.ctx.emit('agent/inbox/enqueue', foreign, inboxItem(message, 'queued'))
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, inboxItem(message, 'queued'))
|
||||
result.ctx.emit('agent/inbox/discard', result.agent, [inboxItem(message, 'queued')])
|
||||
return message.id
|
||||
result.agent.inbox.splice('next-turn', 0, 0, [input])
|
||||
result.agent.inbox.splice('next-turn', 0, 1, [], 'canceled')
|
||||
}
|
||||
|
||||
result.terminal.send('@sync-source')
|
||||
@@ -2731,10 +2683,11 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
// returned the existing id: replaying the prompt's admission attaches no
|
||||
// stranded snapshot, and nothing leaks for the TUI lifetime.
|
||||
const replay = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', result.agent.sentMessages[0]!,
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [result.agent.sentMessages[0]!],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [result.agent.sentMessages[0]!] }),
|
||||
)
|
||||
expect(replay.kind === 'allow' && replay.additionalContexts).toBeUndefined()
|
||||
expect(replay.kind === 'allow' && replay.messages).toEqual([result.agent.sentMessages[0]!])
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
@@ -2762,22 +2715,25 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
await vi.waitFor(() => { expect(result.agent.sent).toHaveLength(1) })
|
||||
|
||||
const blocked = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', result.agent.sentMessages[0]!,
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [result.agent.sentMessages[0]!],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [result.agent.sentMessages[0]!] }),
|
||||
)
|
||||
expect(blocked.kind).toBe('block')
|
||||
// Nothing entered history and nothing waits for a later prompt: a fresh
|
||||
// unrelated admission sees no leftover contexts.
|
||||
expect(result.agent.injected).toHaveLength(0)
|
||||
blockPrompts = false
|
||||
const unrelatedMessage = createUserMessage({
|
||||
content: [{ type: 'text', text: 'unrelated' }],
|
||||
source: { kind: 'user' },
|
||||
})
|
||||
const unrelated = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', createUserMessage({
|
||||
content: [{ type: 'text', text: 'unrelated' }],
|
||||
source: { kind: 'user' },
|
||||
}),
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [unrelatedMessage],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [unrelatedMessage] }),
|
||||
)
|
||||
expect(unrelated.kind === 'allow' && unrelated.additionalContexts).toBeUndefined()
|
||||
expect(unrelated.kind === 'allow' && unrelated.messages).toEqual([unrelatedMessage])
|
||||
|
||||
// Second referenced prompt, this time dropped by a broad cancel before
|
||||
// any admission runs: the discard listener releases the wrapper.
|
||||
@@ -2788,31 +2744,33 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
result.terminal.send('\r')
|
||||
await vi.waitFor(() => { expect(result.agent.sent).toHaveLength(2) })
|
||||
// A different prompt passing the still-armed wrapper delegates untouched.
|
||||
const differentMessage = createUserMessage({
|
||||
content: [{ type: 'text', text: 'different prompt' }],
|
||||
source: { kind: 'user' },
|
||||
})
|
||||
const passthrough = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', createUserMessage({
|
||||
content: [{ type: 'text', text: 'different prompt' }],
|
||||
source: { kind: 'user' },
|
||||
}),
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [differentMessage],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [differentMessage] }),
|
||||
)
|
||||
expect(passthrough.kind === 'allow' && passthrough.additionalContexts).toBeUndefined()
|
||||
// A foreign agent's discard leaves the wrapper armed.
|
||||
const foreign = { ...result.agent, id: SessionId('foreign') } as unknown as Agent
|
||||
result.ctx.emit('agent/inbox/discard', foreign, [inboxItem(result.agent.sentMessages.at(-1)!, 'queued')])
|
||||
// An unrelated discard for this agent also leaves the wrapper armed.
|
||||
result.ctx.emit('agent/inbox/discard', result.agent, [inboxItem(createUserMessage({
|
||||
expect(passthrough.kind === 'allow' && passthrough.messages).toEqual([differentMessage])
|
||||
// Canceling the exact pending message releases its wrapper.
|
||||
const canceled = result.agent.sentMessages.at(-1)!
|
||||
result.agent.inbox.splice('next-turn', 0, 0, [canceled])
|
||||
result.agent.inbox.splice('next-turn', 0, 1, [], 'canceled')
|
||||
const unrelatedDiscard = createUserMessage({
|
||||
content: [{ type: 'text', text: 'unrelated discard' }],
|
||||
source: { kind: 'user' },
|
||||
}), 'queued')])
|
||||
result.ctx.emit('agent/inbox/discard', result.agent, [inboxItem(result.agent.sentMessages.at(-1)!, 'queued')])
|
||||
})
|
||||
result.agent.inbox.splice('next-turn', 0, 0, [unrelatedDiscard])
|
||||
result.agent.inbox.splice('next-turn', 0, 1, [], 'canceled')
|
||||
await tick()
|
||||
// Idempotent: a repeat discard after cleanup is a no-op.
|
||||
result.ctx.emit('agent/inbox/discard', result.agent, [inboxItem(result.agent.sentMessages.at(-1)!, 'queued')])
|
||||
const afterDiscard = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', result.agent.sentMessages.at(-1)!,
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [canceled],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [canceled] }),
|
||||
)
|
||||
expect(afterDiscard.kind === 'allow' && afterDiscard.additionalContexts).toBeUndefined()
|
||||
expect(afterDiscard.kind === 'allow' && afterDiscard.messages).toEqual([canceled])
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
@@ -2963,11 +2921,14 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(result.agent.sent).toEqual([[
|
||||
{ type: 'text', text: '@evil\\x1b\\x07\\x9b\\x0as' },
|
||||
]])
|
||||
const submitted = result.agent.sentMessages[0]!
|
||||
const decision = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/prompt-submit', result.agent.sentMessages[0]!,
|
||||
new AbortController().signal, () => Promise.resolve({ kind: 'allow' as const }),
|
||||
'agent/prompt-submit', [submitted],
|
||||
new AbortController().signal,
|
||||
() => Promise.resolve({ kind: 'allow' as const, messages: [submitted] }),
|
||||
)
|
||||
expect(decision.kind === 'allow' && decision.additionalContexts?.[0]?.source)
|
||||
expect(decision.kind === 'allow'
|
||||
&& decision.messages.find(message => message.source.kind === 'session-reference')?.source)
|
||||
.toMatchObject({ references: [{ sessionId: unsafeId }] })
|
||||
await dispose(result)
|
||||
})
|
||||
@@ -3738,11 +3699,14 @@ 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, reason: { kind: 'error', step: 1, message: 'live failure' } })
|
||||
events.session.append('turn/end', { turn: 1, reason: { kind: 'error', error: 'live failure' } })
|
||||
events.session.append('turn/start', { turn: 2 })
|
||||
events.session.append('turn/end', { turn: 2, reason: { kind: 'error', step: 1, message: 'durable failure' } })
|
||||
events.session.append('turn/end', { turn: 2, reason: { kind: 'error', error: 'durable failure' } })
|
||||
events.session.append('turn/start', { turn: 3 })
|
||||
events.session.append('turn/end', { turn: 3, reason: { kind: 'aborted' } })
|
||||
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, reason: { kind: 'max-tokens' } })
|
||||
events.session.append('turn/start', { turn: 5 })
|
||||
@@ -3750,10 +3714,13 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
events.session.append('turn/start', { turn: 6 })
|
||||
events.session.append('turn/end', {
|
||||
turn: 6,
|
||||
reason: { kind: 'error', step: 1, failure: { message: 'structured provider failure', code: 'SERVER' } },
|
||||
reason: { kind: 'error', error: { message: 'structured provider failure', code: 'SERVER' } },
|
||||
})
|
||||
events.session.append('turn/start', { turn: 8 })
|
||||
events.session.append('turn/end', { turn: 8, 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, reason: { kind: 'plugin-policy' } as never })
|
||||
@@ -4935,7 +4902,7 @@ describe('terminal mounting', () => {
|
||||
ctx.provide('tools', { get: () => undefined } as never)
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', ctx,
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
@@ -4960,7 +4927,7 @@ describe('terminal mounting', () => {
|
||||
ctx.provide('tools', { get: () => undefined } as never)
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', ctx,
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
@@ -4995,14 +4962,14 @@ describe('terminal mounting', () => {
|
||||
|
||||
const otherSession = ctx.sessions.create(SessionId('other-session'))
|
||||
ctx.agents.register({
|
||||
id: otherSession.id, options: {}, session: otherSession, status: 'idle', ctx,
|
||||
id: otherSession.id, options: {}, session: otherSession, inbox: new Inbox(otherSession), status: 'idle', ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
expect(terminal.started).toBe(0)
|
||||
|
||||
const session = ctx.sessions.create(SessionId('late-session'))
|
||||
const agent = {
|
||||
id: session.id, options: {}, session, status: 'idle', ctx,
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
} as Agent
|
||||
ctx.agents.register(agent)
|
||||
@@ -5033,7 +5000,7 @@ describe('terminal mounting', () => {
|
||||
|
||||
const session = ctx.sessions.create(SessionId('main-session'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', ctx,
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'idle', ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
await tick()
|
||||
@@ -5077,7 +5044,7 @@ describe('terminal mounting', () => {
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'running', ctx,
|
||||
id: session.id, options: {}, session, inbox: new Inbox(session), status: 'running', ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
|
||||
Reference in New Issue
Block a user