Browse Source

test: close the coverage gaps the gate found

surface.replaceGeneration gets its direct-access test (the getter folds
a pending delta itself, not only via a nodes read); the reconstruction
theorem drives maxTokens and stop through the config waterfall; the
compact-basic envelope drops a dead conditional — config.maxTokens is
required and validated positive, so this backend's envelope always
carries the cap (the return type's optionality exists for overriding
subclasses).
Tianyi Cui 2 months ago
parent
commit
c58e5adee7

+ 4 - 5
packages/compact/compact-basic/src/index.ts

@@ -325,11 +325,10 @@ export class BasicCompactService extends CompactService {
       throw new Error('summarization produced no text summary content')
     }
 
-    return {
-      summary,
-      model: options.model,
-      ...options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {},
-    }
+    // config.maxTokens is required and validated positive, so this backend's
+    // envelope always carries the cap; the return type's optionality exists
+    // for overriding subclasses whose summarizer has none.
+    return { summary, model: options.model, maxTokens: this.config.maxTokens }
   }
 
   // ---- Core API (implements the abstract contract) ----

+ 1 - 1
packages/core/agent-loop/tests/request-reconstruction.spec.ts

@@ -253,7 +253,7 @@ describe('request stability across the loop', () => {
     send(agent, 'go')
     await waitForIdle(ctx, agent)
     ctx.systemPrompt.section({ name: 'extra', order: 2, text: 'now with guidance' })
-    ctx.on('agent/request', async (_agent, _turn, _step, config, _next) => ({ ...config, temperature: 0.5 }))
+    ctx.on('agent/request', async (_agent, _turn, _step, config, _next) => ({ ...config, temperature: 0.5, maxTokens: 99, stop: ['<END>'] }))
     send(agent, 'again')
     await waitForIdle(ctx, agent)
 

+ 23 - 0
packages/core/session/tests/surface.spec.ts

@@ -334,3 +334,26 @@ describe('surface type guards', () => {
     expect(isSurfaceEvent(markerless)).toBe(false)
   })
 })
+
+describe('SurfaceManager.replaceGeneration', () => {
+  it('folds the pending log delta on access and counts replaces and invalidations', () => {
+    const s = new Session(SessionId('gen'))
+    s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
+    s.append('user/message', { content: [{ type: 'text', text: 'one' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
+    s.append('user/message', { content: [{ type: 'text', text: 'two' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
+    // Read the generation FIRST — before nodes — so the getter itself folds
+    // the pending delta rather than piggybacking on a nodes read.
+    expect(s.surface.replaceGeneration).toBe(0)
+
+    const nodes = s.surface.nodes
+    s.append('context/message', {
+      content: [{ type: 'text', text: 'summary' }], source: { kind: 'plugin', plugin: 'compact' },
+    }, { surfaceOp: { op: 'replace', start: nodes[0]!.seq, end: nodes[1]!.seq }, sourceEventSeqs: [nodes[0]!.seq, nodes[1]!.seq] })
+    expect(s.surface.replaceGeneration).toBe(1)
+
+    // invalidate() is a rewrite too: the generation moves forward (and the
+    // refold re-counts the replace), never backwards.
+    s.surface.invalidate()
+    expect(s.surface.replaceGeneration).toBeGreaterThan(1)
+  })
+})