Merge remote-tracking branch 'origin/master' into jsonl-packed-chunk-rows
Conflicts: the four generated catalog docs (regenerated over merged sources), session index.ts exports (keep chunk-rows exports + master's SessionSurface re-export), stdio/acp demo config schema and persistence wiring (thread packChunks through master's DEFAULT_PERSISTENCE_ROOT/UI shape), stdio README config table, and the jsonl spec import line. The packed-chunk fixture also gains the provenance field master made required on assistant/message.
This commit is contained in:
@@ -188,7 +188,7 @@ describe('loadReplayScript', () => {
|
||||
it('uses the sidecar override when present, ignoring the JSONL', () => {
|
||||
writeFileSync(file, sessionJsonl([]), 'utf8')
|
||||
const overrideFile = join(dir, 'replay.override.json')
|
||||
const override: ReplayEntry[] = [{ kind: 'throw', chunks: [], message: '401', code: 'AUTH', status: 401 }]
|
||||
const override: ReplayEntry[] = [{ kind: 'throw', chunks: [], message: '401', code: 'AUTH' }]
|
||||
writeFileSync(overrideFile, JSON.stringify(override), 'utf8')
|
||||
expect(loadReplayScript({ file, overrideFile })).toEqual(override)
|
||||
})
|
||||
@@ -211,7 +211,7 @@ describe('loadReplayScript', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('installLlmReplay (through the real waterfall)', () => {
|
||||
describe('installLlmReplay (through the real LlmService)', () => {
|
||||
function writeLog(...calls: StreamChunk[][]): void {
|
||||
let seq = 1
|
||||
const events: SessionEvent[] = []
|
||||
@@ -227,7 +227,41 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
await ctx.plugin(LlmService)
|
||||
// No adapter registered for 'm' — replay must not reach it.
|
||||
installLlmReplay(ctx, { file })
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
})
|
||||
|
||||
it('registers a replay-only provider catalog when configured', async () => {
|
||||
writeLog(TEXT_CHUNKS)
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
const dispose = installLlmReplay(ctx, {
|
||||
file,
|
||||
providers: [
|
||||
{
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
models: [
|
||||
{ id: 'flash' },
|
||||
{ id: 'pro', name: 'Pro', description: 'Larger model' },
|
||||
],
|
||||
},
|
||||
{ id: 'empty' },
|
||||
],
|
||||
})
|
||||
|
||||
expect(ctx.llm.listProviders()).toEqual([
|
||||
{ id: 'deepseek', name: 'DeepSeek' },
|
||||
{ id: 'empty', name: 'empty' },
|
||||
])
|
||||
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
|
||||
{ provider: 'deepseek', id: 'flash', name: 'flash' },
|
||||
{ provider: 'deepseek', id: 'pro', name: 'Pro', description: 'Larger model' },
|
||||
])
|
||||
await expect(ctx.llm.listModels('empty')).resolves.toEqual([])
|
||||
expect(await drain(ctx.llm.stream({ provider: 'deepseek', model: 'pro', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
|
||||
dispose()
|
||||
expect(ctx.llm.listProviders()).toEqual([])
|
||||
})
|
||||
|
||||
it('serves the Nth call the Nth derived entry (positional)', async () => {
|
||||
@@ -240,16 +274,16 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
installLlmReplay(ctx, { file })
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(second)
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(second)
|
||||
})
|
||||
|
||||
it('replays a sidecar throw-entry as an LlmError with code/status, after its prefix chunks', async () => {
|
||||
it('replays a sidecar throw-entry as an LlmError with its stable code, after its prefix chunks', async () => {
|
||||
writeFileSync(file, sessionJsonl([]), 'utf8')
|
||||
const overrideFile = join(dir, 'replay.override.json')
|
||||
const partial: StreamChunk[] = [{ type: 'block-start', index: 0, blockType: 'text' }]
|
||||
writeFileSync(overrideFile, JSON.stringify([
|
||||
{ kind: 'throw', chunks: partial, message: 'unauthorized', code: 'AUTH', status: 401 },
|
||||
{ kind: 'throw', chunks: partial, message: 'unauthorized', code: 'AUTH' },
|
||||
]), 'utf8')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
@@ -257,8 +291,8 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
|
||||
const seen: StreamChunk[] = []
|
||||
await expect((async () => {
|
||||
for await (const c of ctx.llm.stream({ model: 'm', messages: [] })) seen.push(c)
|
||||
})()).rejects.toMatchObject({ message: 'unauthorized', code: 'AUTH', status: 401 })
|
||||
for await (const c of ctx.llm.stream({ provider: 'm', model: 'm', messages: [] })) seen.push(c)
|
||||
})()).rejects.toMatchObject({ message: 'unauthorized', code: 'AUTH' })
|
||||
expect(seen).toEqual(partial)
|
||||
})
|
||||
|
||||
@@ -271,7 +305,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
installLlmReplay(ctx, { file, overrideFile })
|
||||
|
||||
const controller = new AbortController()
|
||||
const iterator = ctx.llm.stream({ model: 'm', messages: [], signal: controller.signal })[Symbol.asyncIterator]()
|
||||
const iterator = ctx.llm.stream({ provider: 'm', model: 'm', messages: [], signal: controller.signal })[Symbol.asyncIterator]()
|
||||
// Deterministically consume the two pre-hang chunks (no sleep), then abort
|
||||
// and assert the next pull rejects — event-driven, per the no-sleeps rule.
|
||||
expect((await iterator.next()).value).toMatchObject({ type: 'block-start' })
|
||||
@@ -285,8 +319,8 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
installLlmReplay(ctx, { file })
|
||||
await drain(ctx.llm.stream({ model: 'm', messages: [] }))
|
||||
await expect(drain(ctx.llm.stream({ model: 'm', messages: [] }))).rejects.toThrow(/exhausted/)
|
||||
await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))
|
||||
await expect(drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).rejects.toThrow(/exhausted/)
|
||||
})
|
||||
|
||||
it('aborts mid-replay when the signal is already set', async () => {
|
||||
@@ -296,7 +330,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
installLlmReplay(ctx, { file })
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
await expect(drain(ctx.llm.stream({ model: 'm', messages: [], signal: controller.signal })))
|
||||
await expect(drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [], signal: controller.signal })))
|
||||
.rejects.toThrow('aborted')
|
||||
})
|
||||
|
||||
@@ -318,11 +352,11 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
}, { inject: ['llm'] }))
|
||||
|
||||
// While installed, replay short-circuits to the derived fixture ('hi').
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
|
||||
await fiber.dispose()
|
||||
// After dispose the listener is gone; the call reaches the real adapter.
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] })))
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] })))
|
||||
.toEqual([{ type: 'finish', reason: { kind: 'stop' } }])
|
||||
})
|
||||
|
||||
@@ -334,7 +368,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
installLlmReplay(ctx, { file, overrideFile })
|
||||
await expect(drain(ctx.llm.stream({ model: 'm', messages: [] })))
|
||||
await expect(drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] })))
|
||||
.rejects.toThrow(/llm-replay replay entry/)
|
||||
})
|
||||
|
||||
@@ -346,7 +380,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
await ctx.plugin(LlmService)
|
||||
installLlmReplay(ctx, { file, overrideFile })
|
||||
const controller = new AbortController()
|
||||
const iterator = ctx.llm.stream({ model: 'm', messages: [], signal: controller.signal })[Symbol.asyncIterator]()
|
||||
const iterator = ctx.llm.stream({ provider: 'm', model: 'm', messages: [], signal: controller.signal })[Symbol.asyncIterator]()
|
||||
// Consume the two pre-hang chunks, then start the third pull so the generator
|
||||
// is parked inside the await (signal NOT yet aborted — exercises the
|
||||
// addEventListener('abort') registration), and only THEN abort.
|
||||
@@ -363,7 +397,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
const overrideFile = join(dir, 'replay.override.json')
|
||||
const partial: StreamChunk[] = [{ type: 'block-start', index: 0, blockType: 'text' }]
|
||||
writeFileSync(overrideFile, JSON.stringify([
|
||||
{ kind: 'throw', chunks: partial, message: 'unauthorized', code: 'AUTH', status: 401 },
|
||||
{ kind: 'throw', chunks: partial, message: 'unauthorized', code: 'AUTH' },
|
||||
]), 'utf8')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
@@ -372,7 +406,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
controller.abort()
|
||||
// Already aborted: the throw-entry's prefix loop surfaces 'aborted' before
|
||||
// it can reach the recorded LlmError.
|
||||
await expect(drain(ctx.llm.stream({ model: 'm', messages: [], signal: controller.signal })))
|
||||
await expect(drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [], signal: controller.signal })))
|
||||
.rejects.toThrow('aborted')
|
||||
})
|
||||
|
||||
@@ -386,7 +420,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
// The two pre-hang chunks still flow; the abort surfaces at the await.
|
||||
const iterator = ctx.llm.stream({ model: 'm', messages: [], signal: controller.signal })[Symbol.asyncIterator]()
|
||||
const iterator = ctx.llm.stream({ provider: 'm', model: 'm', messages: [], signal: controller.signal })[Symbol.asyncIterator]()
|
||||
await iterator.next()
|
||||
await iterator.next()
|
||||
await expect(iterator.next()).rejects.toThrow('aborted')
|
||||
@@ -516,7 +550,7 @@ describe('installLlmReplay (per-session keying)', () => {
|
||||
]
|
||||
|
||||
const live = (id: string): GenerateOptions =>
|
||||
({ model: 'm', messages: [], sessionId: id as NonNullable<GenerateOptions['sessionId']> })
|
||||
({ provider: 'm', model: 'm', messages: [], sessionId: id as NonNullable<GenerateOptions['sessionId']> })
|
||||
|
||||
it('routes each live session to its own script by FIRST-CALL order', async () => {
|
||||
const parentFile = writeSession('session.jsonl', { id: 'rec-parent', createdAt: 100 }, [TEXT_CHUNKS])
|
||||
@@ -553,7 +587,7 @@ describe('installLlmReplay (per-session keying)', () => {
|
||||
await ctx.plugin(LlmService)
|
||||
installLlmReplay(ctx, { file: parentFile })
|
||||
// No sessionId at all — the legacy single-session path.
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
})
|
||||
|
||||
it('fails loud when more distinct live sessions call than were recorded', async () => {
|
||||
@@ -587,12 +621,13 @@ describe('apply (the plugin entry)', () => {
|
||||
expect(inject).toEqual(['llm'])
|
||||
})
|
||||
|
||||
it('installs replay from an explicit config.file', async () => {
|
||||
it('installs replay and its catalog from explicit config', async () => {
|
||||
writeFileSync(file, sessionJsonl(TEXT_CHUNKS.map((c, i) => chunkEvent(i + 1, 1, 1, c))), 'utf8')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
apply(ctx, { file })
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
apply(ctx, { file, providers: [{ id: 'm', models: [{ id: 'm' }] }] })
|
||||
expect(ctx.llm.listProviders()).toEqual([{ id: 'm', name: 'm' }])
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
})
|
||||
|
||||
it('falls back to $DSH_SNAPSHOT_FILE / $DSH_SNAPSHOT_OVERRIDE when config is empty', async () => {
|
||||
@@ -604,7 +639,7 @@ describe('apply (the plugin entry)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
apply(ctx)
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
})
|
||||
|
||||
it('uses only the file when no override path is configured or in the env', async () => {
|
||||
@@ -614,7 +649,7 @@ describe('apply (the plugin entry)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
apply(ctx)
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
})
|
||||
|
||||
it('throws when no fixture path is given by config or env', async () => {
|
||||
@@ -644,7 +679,7 @@ describe('apply (the plugin entry)', () => {
|
||||
await ctx.plugin(LlmService)
|
||||
apply(ctx, { file, childFiles: [childFile] })
|
||||
const live = (id: string): GenerateOptions =>
|
||||
({ model: 'm', messages: [], sessionId: id as NonNullable<GenerateOptions['sessionId']> })
|
||||
({ provider: 'm', model: 'm', messages: [], sessionId: id as NonNullable<GenerateOptions['sessionId']> })
|
||||
expect(await drain(ctx.llm.stream(live('A')))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream(live('B')))).toEqual(childSecond)
|
||||
})
|
||||
@@ -664,7 +699,7 @@ describe('apply (the plugin entry)', () => {
|
||||
await ctx.plugin(LlmService)
|
||||
apply(ctx)
|
||||
const live = (id: string): GenerateOptions =>
|
||||
({ model: 'm', messages: [], sessionId: id as NonNullable<GenerateOptions['sessionId']> })
|
||||
({ provider: 'm', model: 'm', messages: [], sessionId: id as NonNullable<GenerateOptions['sessionId']> })
|
||||
expect(await drain(ctx.llm.stream(live('A')))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream(live('B')))).toEqual(childChunks)
|
||||
})
|
||||
@@ -676,6 +711,6 @@ describe('apply (the plugin entry)', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
apply(ctx)
|
||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user