fold.ts held the persisted state shape, the retention bounds, the untrusted-usage sanitizer with its accumulators, the surface projection, and the served-value builders at 1234 lines. The state, the metering accumulators, and the wire builders move into flat siblings; applyTimeline and the surface projection stay together because the clone contract they enforce spans both. The metering extraction also unifies BilledUsage with the shared CostBucketTotals: one four-bucket vocabulary, so the accumulator input is spelled `uncached` like the totals it feeds. The persisted and wire shapes are unchanged (both already carried `uncached`), so no stateVersion bump and no cached-row invalidation. Production callers import the owning sibling; fold.ts keeps re-exporting the pre-split surface for the specs.
243 lines
12 KiB
TypeScript
243 lines
12 KiB
TypeScript
// The split wire generation (src/host/foldWire.ts buildTimelineHead / buildTimelineDetail + the fold's detailRev ledger): the slim head carries the
|
||
// counters and headline anchor, the detail payload the heavy collections, and the revision bumps exactly on detail-mutating folds. Also
|
||
// pins the equivalence: inline view = head + detail, so the two generations can never drift apart.
|
||
|
||
import assert from 'node:assert/strict'
|
||
import { describe, test } from 'vitest'
|
||
import { buildTimelineDetail, buildTimelineHead, skillNameOf } from '../../src/host/fold'
|
||
import type { TimelineEvent } from '../../src/host/fold'
|
||
import { resolveBounds } from '../../src/host/config'
|
||
import {
|
||
assistantMessage,
|
||
compaction,
|
||
header,
|
||
planMode,
|
||
requestContext,
|
||
stepEnd,
|
||
stepStart,
|
||
systemMessage,
|
||
toolCall,
|
||
toolResult,
|
||
userMessage,
|
||
} from './helpers/events'
|
||
import { assertPlainJson, driveTimeline, timelineDef } from './helpers/projection'
|
||
|
||
/** A session log touching every envelope family the fold serves (mirrors the compat driver's canonical log). */
|
||
function canonicalLog(): TimelineEvent[] {
|
||
return [
|
||
header(1, {
|
||
tools: [{ name: 'bash', description: 'run a command' }],
|
||
model: 'deepseek-v4-flash',
|
||
provider: 'deepseek',
|
||
}),
|
||
systemMessage(2),
|
||
requestContext(3, { contextWindow: 128000 }),
|
||
stepStart(4),
|
||
userMessage(5, [{ type: 'text', text: 'hello there' }], { kind: 'user' }),
|
||
assistantMessage(6, { turn: 1, step: 0, usage: { inputTokens: 10, outputTokens: 5, cacheReadTokens: 3 } }),
|
||
toolCall(7, { callId: 'c1', name: 'bash' }),
|
||
toolResult(8, { callId: 'c1', content: [{ type: 'text', text: 'ok' }] }),
|
||
stepEnd(9),
|
||
assistantMessage(10, { turn: 1, step: 1, usage: { inputTokens: 20, outputTokens: 8 } }),
|
||
compaction(11, 'summary', { shadowedTokenCount: 12, shadowedSeqs: [5] }),
|
||
planMode(12, { active: true }),
|
||
]
|
||
}
|
||
|
||
describe('the detailRev ledger', () => {
|
||
test('bumps exactly on the detail-mutating folds of the canonical log', () => {
|
||
// Mutations: userMessage(5), assistantMessage(6), toolResult(8),
|
||
// stepEnd(9, the strip's span flush), assistantMessage(10), compaction(11), planMode(12) — seven in total.
|
||
const { state } = driveTimeline(canonicalLog())
|
||
assert.equal(state.detailRev, 7)
|
||
})
|
||
|
||
test('the working-slot and envelope folds do NOT bump (nothing to refetch)', () => {
|
||
const { def, state } = driveTimeline(canonicalLog())
|
||
// step/start (arms the step slot), tool/call (arms the call ledger),
|
||
// request/context (capacity metadata), and a header with an UNCHANGED model all leave the detail collections as-is.
|
||
const follow: TimelineEvent[] = [
|
||
requestContext(13, { contextWindow: 256000 }),
|
||
header(14, { model: 'deepseek-v4-flash', provider: 'deepseek', reason: 'change' }),
|
||
stepStart(15),
|
||
toolCall(16, { callId: 'c2', name: 'bash' }),
|
||
]
|
||
let st = state
|
||
for (const ev of follow) st = def.apply(st, ev)
|
||
assert.equal(st.detailRev, 7, 'no detail mutation, no bump')
|
||
// The capacity metadata and the header DID fold (state changed) — only the rev stayed.
|
||
assert.notEqual(st, state)
|
||
})
|
||
|
||
test('a real model switch bumps (the events collection gains a row)', () => {
|
||
const { def, state } = driveTimeline(canonicalLog())
|
||
const switched = def.apply(state, header(13, { model: 'deepseek-v4-pro', provider: 'deepseek', reason: 'change' }))
|
||
assert.equal(switched.detailRev, 8)
|
||
})
|
||
|
||
test('the rev is plain JSON and survives the state gate', () => {
|
||
const { states } = driveTimeline(canonicalLog())
|
||
for (const state of states) assertPlainJson(state)
|
||
})
|
||
})
|
||
|
||
describe('buildTimelineHead', () => {
|
||
test('serves the counters, the headline anchor, and the revision — with empty collections', () => {
|
||
const { state } = driveTimeline(canonicalLog())
|
||
const head = buildTimelineHead(state)
|
||
assert.equal(head.ok, true)
|
||
assert.equal(head.model, 'deepseek-v4-flash')
|
||
assert.equal(head.provider, 'deepseek')
|
||
assert.equal(head.contextWindow, 128000)
|
||
assert.ok(head.current.total > 0)
|
||
// The retained records tally: one turn, two steps, the compaction event.
|
||
assert.deepEqual(head.counts, { turns: 1, steps: 2, injects: 0, compactions: 1, prunes: 0, skills: 0 })
|
||
// The whole-session tally rides the head too: the canonical log's one user message (injections excluded).
|
||
assert.equal(head.humanInputs, 1)
|
||
// …and both its textual assistant replies (a step counts as an answer only when the message carried text).
|
||
assert.equal(head.answers, 2)
|
||
// The card preview rides the head: the canonical log's own user message.
|
||
assert.equal(head.lastUser, 'hello there')
|
||
// The headline anchor is the newest retained request's billing summary.
|
||
const lastReq = state.requests.at(-1)!
|
||
assert.deepEqual(head.last, { seq: lastReq.seq, total: lastReq.total, prompt: 20 })
|
||
assert.equal(head.detailRev, 7)
|
||
// The collections stay empty on the head — the detail channel serves them.
|
||
assert.deepEqual(head.requests, [])
|
||
assert.deepEqual(head.events, [])
|
||
assert.deepEqual(head.nodes, [])
|
||
assert.equal(head.droppedNodes, 0)
|
||
assert.deepEqual(head.archive, [])
|
||
assertPlainJson(head)
|
||
})
|
||
|
||
test('the counters tally injects/prunes, and a turn-less record folds as turn 0', () => {
|
||
const { state } = driveTimeline([
|
||
userMessage(1, [{ type: 'text', text: 'ctx' }], { kind: 'plugin', form: 'context', plugin: 'dsh-test' }),
|
||
compaction(2, 'prune', { shadowedTokenCount: 3 }),
|
||
assistantMessage(3, {}), // no turn: the replay shape folds as turn 0
|
||
])
|
||
const head = buildTimelineHead(state)
|
||
assert.deepEqual(head.counts, { turns: 1, steps: 1, injects: 1, compactions: 0, prunes: 1, skills: 0 })
|
||
})
|
||
|
||
test('the skills counter tallies DISTINCT loaded-skill names — the catalog digest never counts', () => { const { state } = driveTimeline([
|
||
// A `/name` invocation and a `skill`-tool load both load a skill; the
|
||
// available-skills catalog digest is the directory listing, not a load.
|
||
userMessage(1, [{ type: 'text', text: 'run' }], { kind: 'skill-invocation', form: 'skill', name: 'grilling' }),
|
||
toolCall(2, { callId: 'c1', name: 'skill' }),
|
||
toolResult(3, { callId: 'c1', content: [{ type: 'text', text: '<skill_content name="grilling">instructions</skill_content>' }] }),
|
||
userMessage(4, [{ type: 'text', text: 'catalog' }], { kind: 'skill-catalog', form: 'catalog' }),
|
||
userMessage(5, [{ type: 'text', text: 'again' }], { kind: 'skill-invocation', form: 'skill', name: 'ponytail' }),
|
||
userMessage(6, [{ type: 'text', text: 'once more' }], { kind: 'skill-invocation', form: 'skill', name: 'grilling' }),
|
||
])
|
||
const head = buildTimelineHead(state)
|
||
// 'grilling' loaded twice counts once; the catalog digest rides an
|
||
// untagged inject event — five injects, two distinct skills.
|
||
assert.deepEqual(head.counts, { turns: 0, steps: 0, injects: 5, compactions: 0, prunes: 0, skills: 2 })
|
||
assertPlainJson(head)
|
||
})
|
||
|
||
test('skillNameOf narrows hostile messages to no name (the activity fold’s reuse)', () => {
|
||
assert.equal(skillNameOf(null), '')
|
||
assert.equal(skillNameOf('text'), '')
|
||
assert.equal(skillNameOf(42), '')
|
||
assert.equal(skillNameOf({}), '')
|
||
assert.equal(skillNameOf({ content: [{ type: 'text', text: '<skill_content name="tdd">…' }] }), 'tdd')
|
||
})
|
||
|
||
test('a fresh state serves zeroed counters and no last/detailRev payload keys beyond the marker', () => {
|
||
const head = buildTimelineHead(timelineDef().init())
|
||
assert.deepEqual(head.counts, { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0, skills: 0 })
|
||
assert.equal(head.humanInputs, 0, 'the tally defaults to zero on the wire')
|
||
assert.equal(head.answers, 0, 'the answers tally defaults to zero on the wire too')
|
||
assert.equal(head.lastUser, undefined, 'no user message yet — the preview stays absent')
|
||
assert.ok(!('lastUser' in head), 'no own key on the wire')
|
||
assert.equal(head.last, undefined, 'no request yet — the anchor stays absent')
|
||
assert.equal(head.detailRev, 0)
|
||
assert.equal(head.current.total, 0)
|
||
})
|
||
|
||
test('the newest request without a usage sample anchors without prompt', () => {
|
||
const { state } = driveTimeline([
|
||
userMessage(1, [{ type: 'text', text: 'hi' }], { kind: 'user' }),
|
||
assistantMessage(2, { turn: 1, step: 1 }),
|
||
])
|
||
const head = buildTimelineHead(state)
|
||
assert.deepEqual(head.last, { seq: 2, total: state.requests[0].total })
|
||
assert.ok(head.last !== undefined && !('prompt' in head.last), 'no undefined-valued prompt key')
|
||
})
|
||
})
|
||
|
||
describe('buildTimelineDetail', () => {
|
||
test('serves the same collections the inline view serves, plus the revision and the head', () => {
|
||
const { state, view } = driveTimeline(canonicalLog())
|
||
const detail = buildTimelineDetail(state, resolveBounds({}))
|
||
assert.equal(detail.rev, 7)
|
||
assert.deepEqual(detail.requests, view.requests)
|
||
assert.deepEqual(detail.events, view.events)
|
||
assert.deepEqual(detail.nodes, view.nodes)
|
||
assert.equal(detail.droppedNodes, view.droppedNodes)
|
||
assert.deepEqual(detail.archive, view.archive)
|
||
assert.equal(detail.surfaceFloor, view.surfaceFloor)
|
||
assert.equal(detail.archiveFloor, view.archiveFloor)
|
||
// The slim head rides at the SAME cut: the composition the Agent network's cold-node ring fetch renders from.
|
||
assert.ok(detail.head !== undefined)
|
||
assert.deepEqual(detail.head.current, view.current)
|
||
assert.deepEqual(detail.head.counts, buildTimelineHead(state).counts)
|
||
assertPlainJson(detail)
|
||
// The detail never aliases the persisted state.
|
||
assert.notEqual(detail.requests[0], state.requests[0])
|
||
assert.notEqual(detail.archive, state.archived)
|
||
})
|
||
|
||
test('the served window and floors follow the retention bounds (same rule as the inline view)', () => {
|
||
const events: TimelineEvent[] = [
|
||
userMessage(1, [{ type: 'text', text: 'injected' }], { kind: 'plugin', form: 'context', plugin: 'dsh-test' }),
|
||
userMessage(2, [{ type: 'text', text: 'x1' }]),
|
||
userMessage(3, [{ type: 'text', text: 'x2' }]),
|
||
userMessage(4, [{ type: 'text', text: 'x3' }]),
|
||
userMessage(5, [{ type: 'text', text: 'x4' }]),
|
||
]
|
||
const { state } = driveTimeline(events)
|
||
const detail = buildTimelineDetail(state, resolveBounds({ maxNodes: 2 }))
|
||
assert.deepEqual(detail.nodes.map(n => n.seq), [1, 4, 5], 'the out-of-window inject is pinned ahead of the tail')
|
||
assert.equal(detail.droppedNodes, 2)
|
||
assert.equal(detail.surfaceFloor, 3)
|
||
assert.equal(detail.rev, 5)
|
||
})
|
||
})
|
||
|
||
describe('the split wire generation (definition slim flag)', () => {
|
||
test('the slim view validates against the wire schema and carries the marker', () => {
|
||
const slim = timelineDef({}, true)
|
||
const { state } = driveTimeline(canonicalLog())
|
||
const view = slim.wire.view(state)
|
||
assert.equal(slim.wire.viewSchema.safeParse(view).success, true, 'the slim head validates')
|
||
assert.equal(typeof view.detailRev, 'number', 'the split marker is present')
|
||
assert.ok(view.counts !== undefined)
|
||
assert.deepEqual(view.nodes, [], 'collections stay off the wire value')
|
||
})
|
||
|
||
test('the inline view carries the collections and NO split markers', () => {
|
||
const inline = timelineDef({}, false)
|
||
const { state } = driveTimeline(canonicalLog())
|
||
const view = inline.wire.view(state)
|
||
assert.equal(inline.wire.viewSchema.safeParse(view).success, true, 'the inline value validates on the same schema')
|
||
assert.equal(view.detailRev, undefined)
|
||
assert.equal(view.counts, undefined)
|
||
assert.ok(view.nodes.length > 0)
|
||
assert.ok(view.requests.length > 0)
|
||
})
|
||
|
||
test('equivalence: the inline view is exactly the head plus the detail (minus the markers)', () => {
|
||
const { state } = driveTimeline(canonicalLog())
|
||
const head = buildTimelineHead(state)
|
||
const detail = buildTimelineDetail(state, resolveBounds({}))
|
||
const inline = timelineDef({}, false).wire.view(state)
|
||
const { counts: _counts, last: _last, detailRev: _rev, ...headScalars } = head
|
||
const { rev: _detailRev, head: _detailHead, ...collections } = detail
|
||
assert.deepEqual(inline, { ...headScalars, ...collections })
|
||
})
|
||
})
|