Enforce 100% per-file test coverage on packages/*/src
vitest coverage (v8 provider) with per-file 100% thresholds for statements, branches, functions, and lines. Scope: our runtime source only — types-only files, vendor/ (upstream code), and examples/ (exercised by the demo smoke test) are excluded. yarn test:coverage runs the gate. 59 tests added to close every gap: llm generate-waterfall and adapter disposal; assembler edge protocol (duplicate block-start, stragglers after block-end, id fallback, usage omission, invariant violation); the whole Inbox surface incl. the wakeup-overwrite race; LoopAgent disposed-state throws and double-stop idempotence; config-driven agent creation; loop backstop catches (throwing turn-start/turn-end listeners, non-Error throws, non-JSON tool arguments); system-prompt dynamic sections and disposer paths; tools errorMessage fallbacks and the full schema-DSL emission matrix. Genuinely unreachable defensive guards carry /* v8 ignore */ comments with stated reasons rather than deletion (132 tests total).
This commit is contained in:
@@ -131,6 +131,7 @@ export class BlockAssembler {
|
||||
const ready: ContentBlock[] = []
|
||||
while (this.flushed < this.order.length) {
|
||||
const index = this.order[this.flushed]
|
||||
/* v8 ignore next 3 -- noUncheckedIndexedAccess guard: loop condition guarantees index exists in a non-empty array */
|
||||
if (index === undefined) break
|
||||
const partial = this.mustGet(index)
|
||||
if (!partial.block) break
|
||||
@@ -150,6 +151,7 @@ export class BlockAssembler {
|
||||
const remaining: ContentBlock[] = []
|
||||
while (this.flushed < this.order.length) {
|
||||
const index = this.order[this.flushed]
|
||||
/* v8 ignore next 3 -- noUncheckedIndexedAccess guard: loop condition guarantees index exists */
|
||||
if (index === undefined) break
|
||||
remaining.push(this.assemble(this.mustGet(index), index))
|
||||
this.flushed += 1
|
||||
|
||||
@@ -43,4 +43,111 @@ describe('BlockAssembler', () => {
|
||||
expect(assembler.blocks()).toEqual([{ type: 'text', text: 'implicit' }])
|
||||
expect(assembler.finish).toEqual({ kind: 'stop' })
|
||||
})
|
||||
|
||||
it('returns undefined usage when no usage chunk was received', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
assembler.push({ type: 'text-delta', index: 0, text: 'no usage' })
|
||||
expect(assembler.usage).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reuses an existing partial when ensure() is called with a tracked index', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
// block-start creates the partial; block-end calls ensure() on the same index
|
||||
assembler.push({ type: 'block-start', index: 0, blockType: 'text' })
|
||||
// push a delta first to guarantee the partial exists
|
||||
assembler.push({ type: 'text-delta', index: 0, text: 'hi' })
|
||||
// block-end's ensure() must find the existing partial (the second branch path)
|
||||
const block = assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
|
||||
expect(block).toEqual({ type: 'text', text: 'hi' })
|
||||
})
|
||||
|
||||
it('throws from assemble() when a partial has an unhandled blockType', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
// Directly push a block-end for an image block whose block-start never
|
||||
// called ensure — but the image block-type flows through normally.
|
||||
// What we really need is a partial whose blockType is not text/reasoning/tool-call.
|
||||
// We can achieve this via a block-start for 'image' followed by blocks().
|
||||
assembler.push({ type: 'block-start', index: 0, blockType: 'image' } as unknown as StreamChunk)
|
||||
expect(() => assembler.blocks()).toThrow('cannot assemble incomplete block of type "image"')
|
||||
})
|
||||
|
||||
it('mustGet throws when an index is missing from the partials map (invariant violation)', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
// Force the invariant violation: manually corrupt the data structures.
|
||||
/* eslint-disable */
|
||||
const hack = assembler as any
|
||||
hack.order.push(99)
|
||||
/* eslint-enable */
|
||||
expect(() => assembler.blocks()).toThrow('BlockAssembler invariant violated')
|
||||
})
|
||||
|
||||
it('assembles open blocks at end of stream via flushRemaining', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
assembler.push({ type: 'text-delta', index: 0, text: 'open' })
|
||||
assembler.push({ type: 'reasoning-delta', index: 1, text: 'thinking' })
|
||||
|
||||
// flushReady returns nothing because index 0 is incomplete and blocking
|
||||
const ready = assembler.flushReady()
|
||||
expect(ready).toEqual([])
|
||||
|
||||
// flushRemaining assembles everything still open
|
||||
const remaining = assembler.flushRemaining()
|
||||
expect(remaining).toEqual([
|
||||
{ type: 'text', text: 'open' },
|
||||
{ type: 'reasoning', text: 'thinking' },
|
||||
])
|
||||
|
||||
// blocks() now matches the flushed view
|
||||
expect(assembler.blocks()).toEqual(remaining)
|
||||
})
|
||||
|
||||
it('result() omits usage key when no usage was received', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
assembler.push({ type: 'text-delta', index: 0, text: 'msg' })
|
||||
const result = assembler.result()
|
||||
expect(result.message).toBeDefined()
|
||||
expect(result.finish).toEqual({ kind: 'stop' })
|
||||
// usage should NOT be present on the object at all
|
||||
expect('usage' in result).toBe(false)
|
||||
})
|
||||
|
||||
it('ignores duplicate block-start for the same index', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
assembler.push({ type: 'block-start', index: 0, blockType: 'text' })
|
||||
assembler.push({ type: 'text-delta', index: 0, text: 'one' })
|
||||
// duplicate block-start — should be no-op (false branch of has check)
|
||||
assembler.push({ type: 'block-start', index: 0, blockType: 'text' })
|
||||
assembler.push({ type: 'text-delta', index: 0, text: ' two' })
|
||||
assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'one two' } })
|
||||
expect(assembler.blocks()).toEqual([{ type: 'text', text: 'one two' }])
|
||||
})
|
||||
|
||||
it('ignores tool-call-delta stragglers after block-end', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
assembler.push({ type: 'block-start', index: 0, blockType: 'tool-call' })
|
||||
assembler.push({ type: 'tool-call-delta', index: 0, id: 'c1', name: 'echo', argumentsDelta: '{}' })
|
||||
assembler.push({ type: 'block-end', index: 0, block: { type: 'tool-call', id: 'c1', name: 'echo', arguments: '{}' } })
|
||||
// straggler after block-end — partial.block is set, so early return
|
||||
assembler.push({ type: 'tool-call-delta', index: 0, id: 'c1', name: 'evil', argumentsDelta: 'oops' })
|
||||
expect(assembler.blocks()).toEqual([{ type: 'tool-call', id: 'c1', name: 'echo', arguments: '{}' }])
|
||||
})
|
||||
|
||||
it('assembles tool-call with generated id fallback when no id provided', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
assembler.push({ type: 'tool-call-delta', index: 0, argumentsDelta: '{}' } as StreamChunk)
|
||||
// No id and no name provided — uses fallback id `call-{index}` and empty name
|
||||
const blocks = assembler.blocks()
|
||||
expect(blocks).toEqual([
|
||||
{ type: 'tool-call', id: 'call-0', name: '', arguments: '{}' },
|
||||
])
|
||||
})
|
||||
|
||||
it('includes usage in result() when usage was received', () => {
|
||||
const assembler = new BlockAssembler()
|
||||
assembler.push({ type: 'text-delta', index: 0, text: 'msg' })
|
||||
assembler.push({ type: 'usage', usage: { inputTokens: 5, outputTokens: 3 } })
|
||||
const result = assembler.result()
|
||||
expect(result.usage).toEqual({ inputTokens: 5, outputTokens: 3 })
|
||||
expect('usage' in result).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import LlmService, { GenerateOptions, LlmAdapter, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import LlmService, { GenerateOptions, LlmAdapter, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class ScriptedAdapter extends LlmAdapter {
|
||||
constructor(private script: StreamChunk[]) {
|
||||
@@ -70,4 +70,58 @@ describe('LlmService', () => {
|
||||
expect(chunks).toHaveLength(4)
|
||||
expect(chunks[0]).toMatchObject({ index: 99 })
|
||||
})
|
||||
|
||||
it('lets llm/generate waterfall listeners intercept and transform the result', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
ctx.llm.registerAdapter(['test-model'], new ScriptedAdapter(SCRIPT))
|
||||
|
||||
ctx.on('llm/generate', async function (_options, next) {
|
||||
const result = await next()
|
||||
return { ...result, finish: { kind: 'max-tokens' } as const }
|
||||
})
|
||||
|
||||
const result = await ctx.llm.generate({ model: 'test-model', messages: [] })
|
||||
expect(result.finish).toEqual({ kind: 'max-tokens' })
|
||||
expect(result.message.content).toEqual([{ type: 'text', text: 'hi' }])
|
||||
})
|
||||
|
||||
it('creates LlmError with a code for programmatic handling', () => {
|
||||
const err = new LlmError('something went wrong', 'CUSTOM_CODE')
|
||||
expect(err).toBeInstanceOf(Error)
|
||||
expect(err.name).toBe('LlmError')
|
||||
expect(err.message).toBe('something went wrong')
|
||||
expect(err.code).toBe('CUSTOM_CODE')
|
||||
})
|
||||
|
||||
it('disposes adapter registration on adapter-change event emission', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
|
||||
const changes: string[][] = []
|
||||
ctx.on('llm/adapter-change', () => {
|
||||
changes.push([...ctx.llm.models()])
|
||||
})
|
||||
|
||||
const dispose = ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
|
||||
expect(changes).toEqual([['m1']])
|
||||
|
||||
dispose()
|
||||
expect(changes).toEqual([['m1'], []])
|
||||
expect(ctx.llm.models()).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects duplicate adapter registration with DUPLICATE_ADAPTER code', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
|
||||
try {
|
||||
ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
|
||||
expect.fail('expected error')
|
||||
} catch (error: unknown) {
|
||||
expect(error).toBeInstanceOf(LlmError)
|
||||
expect((error as LlmError).message).toContain('already registered')
|
||||
expect((error as LlmError).code).toBe('DUPLICATE_ADAPTER')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user