test: align consumers with owned-run semantics

This commit is contained in:
_Kerman
2026-07-30 17:45:58 +08:00
parent d1dc303bc6
commit 4e5266daa4
26 changed files with 117 additions and 120 deletions

View File

@@ -144,15 +144,8 @@ function alwaysConfig(backoff: BackoffConfig = {}): AlwaysRetryPolicyConfig {
}
}
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
return new Promise((resolve) => {
const dispose = ctx.on('agent/status', (subject, status) => {
if (subject === agent && status === 'idle') {
dispose()
resolve()
}
})
})
function waitForIdle(_ctx: Context, agent: Agent): Promise<void> {
return agent.whenIdle()
}
function waitForRetry(ctx: Context, agent: Agent, retryNumber: number): Promise<Extract<SessionEvent, { type: 'llm/retry' }>> {
@@ -175,7 +168,7 @@ afterEach(async () => {
})
describe('provider-routed retry policy', () => {
it('records the scheduled delay before opening a fresh request attempt', async () => {
it('records the scheduled delay before retrying the request', async () => {
vi.useFakeTimers()
const adapter = new ScriptedAdapter([
new LlmError('busy', 'RATE_LIMIT', { status: 429 }),
@@ -214,7 +207,7 @@ describe('provider-routed retry policy', () => {
expect(adapter.requests).toHaveLength(2)
expect(agent.session.events.filter(item => item.type === 'step/start').map(item => item.data))
.toEqual([{ turn: 1, step: 1 }, { turn: 2, step: 1 }])
.toEqual([{ turn: 1, step: 1 }])
expect(agent.session.deriveMessages().at(-1)).toEqual({
id: expect.any(String) as unknown,
role: 'assistant',
@@ -251,7 +244,7 @@ describe('provider-routed retry policy', () => {
expect(agent.session.events.filter(event => event.type === 'assistant/message').map(event => ({
turn: event.data.turn,
step: event.data.step,
}))).toEqual([{ turn: 2, step: 1 }])
}))).toEqual([{ turn: 1, step: 1 }])
expect(agent.session.deriveMessages().at(-1)).toMatchObject({
role: 'assistant',
content: [{ type: 'text', text: 'recovered' }],
@@ -291,7 +284,7 @@ describe('provider-routed retry policy', () => {
expect(agent.session.events.filter(event => event.type === 'assistant/message').map(event => ({
turn: event.data.turn,
step: event.data.step,
}))).toEqual([{ turn: 2, step: 1 }])
}))).toEqual([{ turn: 1, step: 1 }])
expect(agent.session.events.some(event => event.type === 'tool/call')).toBe(false)
expect(toolExecutions).toBe(0)
expect(agent.session.deriveMessages().at(-1)).toMatchObject({
@@ -534,9 +527,9 @@ describe('provider-routed retry policy', () => {
backoff: { initialDelayMs: 1, maxDelayMs: 1 },
}),
}, (ctx) => {
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => ({
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => ({
...await next(),
provider: turn === 1 ? 'mock' : 'other',
provider: adapter.requests.length === 0 ? 'mock' : 'other',
}))
}))
const agent = context.agentLoop.create(SessionId('retry-provider-budgets'), {

View File

@@ -55,14 +55,8 @@ async function harness(
return ctx
}
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
return new Promise((resolve) => {
const dispose = ctx.on('agent/status', (subject, status) => {
if (subject !== agent || status !== 'idle') return
dispose()
resolve()
})
})
function waitForIdle(_ctx: Context, agent: Agent): Promise<void> {
return agent.whenIdle()
}
function sendAndWait(ctx: Context, agent: Agent): Promise<void> {
@@ -109,7 +103,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
expect(server?.requests).toHaveLength(1)
expect(agent.session.events.filter(event => event.type === 'step/start')
.map(event => [event.data.turn, event.data.step]))
.toEqual([[1, 1], [2, 1]])
.toEqual([[1, 1]])
expect(agent.session.events.filter(event => event.type === 'llm/retry').map(event => event.data.failure.code))
.toEqual(['TRANSPORT'])
expect(finalAssistantText(agent)).toBe('connected after retry')
@@ -141,7 +135,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
)).toHaveLength(failedChunkCount)
expect(agent.session.events.filter(event => event.type === 'assistant/message')
.map(event => [event.data.turn, event.data.step]))
.toEqual([[2, 1]])
.toEqual([[1, 1]])
expect(agent.session.events.filter(event => event.type === 'llm/retry').map(event => event.data.failure.code))
.toEqual(['TRANSPORT'])
expect(finalAssistantText(agent)).toBe('recovered response')
@@ -166,7 +160,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
.toEqual(['EMPTY_RESPONSE'])
expect(agent.session.events.filter(event => event.type === 'assistant/message')
.map(event => [event.data.turn, event.data.step]))
.toEqual([[2, 1]])
.toEqual([[1, 1]])
expect(agent.session.events.at(-1)).toMatchObject({
type: 'turn/end',
data: { reason: { kind: 'completed' } },
@@ -234,7 +228,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
await sendAndWait(context, agent)
expect(server.requests).toHaveLength(3)
expect(agent.session.events.filter(event => event.type === 'step/start')).toHaveLength(3)
expect(agent.session.events.filter(event => event.type === 'step/start')).toHaveLength(1)
expect(agent.session.events.filter(event => event.type === 'llm/retry')).toHaveLength(2)
expect(agent.session.events.at(-1)).toMatchObject({
type: 'turn/end',

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm/README.md
README.md: d343449d1530bf70a3a8c57f883894e29c42d18f
README.zh.md: 4dc4a0ca06378116d05fdb4b9b048738930511fd
README.md: dc7499a6854fe9a45c1297aa2a1a67aea92eaf6f
README.zh.md: 28f281908c2fde708f491194b441a32a8dcba8a5

View File

@@ -262,13 +262,16 @@ describe('LlmService', () => {
messages: [],
}))
expect(chunks.at(-1)).toMatchObject({
const finish = chunks.at(-1)
expect(finish).toMatchObject({
type: 'finish',
reason: {
kind: 'error',
failure: { code: 'NO_ADAPTER', message: expect.stringContaining('no adapter registered') },
failure: { code: 'NO_ADAPTER' },
},
})
if (finish?.type !== 'finish' || finish.reason.kind !== 'error') throw new Error('expected error finish')
expect(finish.reason.failure.message).toContain('no adapter registered')
})
it.each(['done', 'value'] as const)('normalizes a throwing IteratorResult.%s getter', async (field) => {