Files
dsh-context/tests/host.test.mjs
T
bowenliang123 d29dcf89fa refactor: drop the mirrored occupancy fold, read the official contextPressure projection
token-meter is a core service that owns estimation and replay (its docs:
'the singleton ctx.tokenMeter directly owns estimation and replay'), and the
published package registers three projections on the shared
SessionProjectionMap — tokenUsage / contextPressure / contextBreakdown.
The host was duplicating the contextPressure fold (pressureTokens +
sampledSurfaceTokens + occupancyWindow, sampleUsage on assistant/message and
usage chunks) and the client was rendering from that mirror.

Host: the occupancy mirror leaves the fold entirely (assistant/chunk is now
structurally ignored — same reference, no change feed). stateVersion 1 -> 2
so cached rows from the old fold are discarded. The strict wire schema no
longer accepts occupancy; the shared type keeps it optional as legacy for
downstream consumers.

Client: the headline reads the official token-meter contextPressure
projection via the useProjection seat (the same key the chat's context ring
reads); absent key/value degrades to the existing derived fallback (last
request prompt + surface movement), so the UI renders identically in every
deployment. UI functionality is unchanged — same headline, same percentages,
same composition anchoring.

diff-fold.mjs is repurposed: the plugin no longer has an occupancy fold to
compare, but its heuristic composition (current.system/tools + surface total)
must stay equal to the official contextBreakdown projection — verified
MATCH on three real session logs.

Tests: host asserts occupancy is absent + chunk reference stability; client
feeds the official projection through the stub and asserts the same UI text.
2026-08-17 01:21:31 +08:00

265 lines
17 KiB
JavaScript

#!/usr/bin/env node
/**
* Functional smoke test for the packaged host half (v0.9): mounts the plugin
* on a fake ctx, captures the `contextTimeline` session-projection unit it
* registers, drives the unit's pure `init`/`apply`/`view` over synthetic
* event logs, and asserts the snapshot shape the browser receives.
*
* The harness framework normally drives `apply` per committed `session/event`
* and persists the state via the projection cache; those parts are not this
* plugin's concern. What we own and verify here: the fold semantics, the wire
* view shape, reference-stability of the apply contract, and the retention /
* attribution rules.
*/
import assert from 'node:assert/strict'
import { apply } from '../lib/index.js'
let def = null
const disposers = []
const fakeCtx = {
inject(list, cb) { cb(this) }, // ctx.inject(['sessionProjections'], ...)
effect(fn) { disposers.push(fn()); return () => {} },
sessionProjections: {
register(d) { def = d; return () => {} },
},
}
apply(fakeCtx)
assert.ok(def !== null, 'contextTimeline projection unit registered')
assert.equal(def.key, 'contextTimeline')
assert.equal(typeof def.init, 'function')
assert.equal(typeof def.apply, 'function')
assert.equal(typeof def.view, 'function')
/** Fold a full event log through the unit (the framework's watermark would
* restrict this to the delta; determinism here is the same output). */
const drive = (events) => {
let st = def.init()
for (const ev of events) st = def.apply(st, ev)
return def.view(st)
}
// ---- synthetic log: header, context, user+inject, tool call/result, step ----
const live = { events: [
{ seq: 1, type: 'request/header', time: 1000, data: {
header: { system: 'You are a harness agent.', tools: [{ name: 'bash', description: 'run a command' }], config: { model: 'deepseek-v4', provider: 'deepseek' } },
} },
{ seq: 2, type: 'request/context', time: 1000, data: { contextWindow: 128000 } },
{ seq: 3, type: 'user/message', time: 2000, data: { content: [{ type: 'text', text: 'Hello there, a fairly long user message that should cost more than one token!' }] } },
{ seq: 4, type: 'user/message', time: 3000, data: { source: { kind: 'plugin', form: 'notice', plugin: 'dsh-agent-presets', summary: 'Skill injected (code-review)' }, content: [{ type: 'text', text: 'injected text' }] } },
{ seq: 5, type: 'tool/call', time: 4000, data: { callId: 'c1', name: 'bash', arguments: '{}' } },
// Real tool/result envelope: the durable message carries source.kind='tool' + callId
{ seq: 6, type: 'tool/result', time: 4100, data: { callId: 'c1', message: { source: { kind: 'tool', callId: 'c1' }, content: [{ type: 'tool-result', toolCallId: 'c1', content: [{ type: 'text', text: 'ok' }] }] } } },
{ seq: 7, type: 'assistant/message', time: 5000, data: { turn: 1, step: 1, usage: { inputTokens: 900, outputTokens: 40 }, message: { content: [{ type: 'text', text: 'Hi!' }] } } },
{ seq: 8, type: 'compaction/summary', time: 6000, data: { shadowedTokenCount: 5000, shadowedSeqs: [3, 4, 5, 6] } },
] }
const v = drive(live.events)
assert.equal(v.ok, true)
assert.equal(v.model, 'deepseek-v4')
assert.equal(v.provider, 'deepseek')
assert.equal(v.contextWindow, 128000)
assert.ok(v.current.system > 0, 'system prompt tokens')
assert.ok(v.current.tools > 0, 'tool schema tokens')
assert.ok(v.current.user > 0, 'user message tokens')
assert.ok(v.current.inject > 0, 'injection tokens')
assert.ok(v.current.tool > 0, 'tool result tokens')
assert.ok(v.current.assistant > 0, 'assistant tokens')
assert.equal(v.requests.length, 1)
assert.equal(v.requests[0].prompt, 900)
assert.equal(v.requests[0].output, 40)
assert.ok(v.events.some(e => e.kind === 'inject' && e.form === 'notice'), 'injection event recorded')
assert.ok(v.events.some(e => e.kind === 'compaction'), 'compaction event recorded')
assert.ok(v.nodes.length >= 4, 'surface nodes folded')
assert.equal(v.nodes[0].time, 2000, 'nodes carry their event timestamp')
// Message projection must price CONTENT: assistant replies and tool results
// carry their message body (data.message), not a flat envelope price.
const asstNode = v.nodes.find(n => n.seq === 7)
assert.equal(asstNode.tokens, 9, "assistant 'Hi!' prices its text content (1 + 4 + 4)")
const toolNode = v.nodes.find(n => n.seq === 6)
assert.equal(toolNode.tokens, 13, 'tool result prices its nested content (5 + 4 + 4)')
assert.equal(toolNode.tool, 'bash', 'tool result node names its tool (via source.callId)')
assert.equal(asstNode.text, 'Hi!', 'assistant node carries its text preview')
// -- occupancy is NOT folded by the host anymore (R3): the client reads the
// official token-meter `contextPressure` projection (token-meter owns
// estimation and replay); the timeline wire carries only the heuristic
// composition + per-request provider usage (record.prompt/output) --
assert.equal(v.occupancy, undefined, 'the host no longer mirrors contextPressure inside contextTimeline')
assert.equal(v.requests[0].prompt, 900, 'provider usage still rides the request records (usage sample input + cache)')
// -- events are attributed to the requests around them (turn/step + range) --
const injectEv = v.events.find(e => e.kind === 'inject')
assert.ok(injectEv, 'injection event present')
assert.equal(injectEv.turn, 1, 'inject before step 1\'s call lands on Turn 1')
assert.equal(injectEv.step, 1, 'inject lands on Step 1')
assert.equal(injectEv.fromTurn, undefined, 'no request before the first inject')
const compactEv = v.events.find(e => e.kind === 'compaction' && e.seq === 8)
assert.ok(compactEv, 'compaction event present')
assert.equal(compactEv.turn, undefined, 'compaction with no following request yet stays unlabeled')
assert.equal(compactEv.step, undefined, 'no step for a trailing event')
assert.equal(compactEv.fromTurn, 1, 'trailing event still knows the request before it')
assert.equal(compactEv.fromStep, 1, 'trailing event keeps its from-step')
// -- determinism + reference-stability of the projection contract --
// Unrelated events must return the SAME state reference (Object.is gates the
// framework change feed); view() must not mutate persisted state.
let base = def.init()
for (const ev of live.events) base = def.apply(base, ev)
assert.ok(base.events.length > 0, 'folded state carries events')
assert.equal(def.apply(base, { type: 'todo/write', seq: 99, time: 0, data: { todos: [] } }), base,
'unrelated events keep the same reference (no change notification)')
assert.equal(def.apply(base, { type: 'totally/unknown', seq: 100, time: 0, data: {} }), base,
'unknown event vocabulary is safely ignored')
const emptyView = def.view(def.init())
assert.equal(emptyView.ok, true)
assert.equal(emptyView.requests.length, 0)
assert.equal(emptyView.events.length, 0)
assert.equal(emptyView.nodes.length, 0)
assert.equal(emptyView.droppedNodes, 0)
assert.equal(emptyView.current.total, 0)
assert.ok(!('turn' in base.events[0] || 'step' in base.events[0]), 'view() attributions stay on copies, never on persisted state')
// -- append a same-turn step: the compaction now spans Step 1 -> Step 2 --
live.events.push({ seq: 9, type: 'assistant/message', time: 7000, data: { turn: 1, step: 2, message: { content: [{ type: 'text', text: 'more' }] } } })
const after = drive(live.events)
assert.equal(after.requests.length, 2, 'new request folded')
assert.equal(after.requests[1].turn, 1)
const compactAfter = after.events.find(e => e.kind === 'compaction' && e.seq === 8)
assert.equal(compactAfter.turn, 1, 'compaction inside a turn lands on the same turn')
assert.equal(compactAfter.step, 2, 'compaction lands on the step whose request follows it')
assert.equal(compactAfter.fromTurn, 1, 'same-turn range keeps the previous request\'s turn')
assert.equal(compactAfter.fromStep, 1, 'same-turn range keeps the previous step')
// -- a cross-turn compaction: prev turn 1 step 2 -> next turn 2 step 1 --
live.events.push({ seq: 10, type: 'compaction/summary', time: 7500, data: { shadowedTokenCount: 3000, shadowedSeqs: [3, 4] } })
live.events.push({ seq: 11, type: 'assistant/message', time: 8000, data: { turn: 2, step: 1, message: { content: [{ type: 'text', text: 'more2' }] } } })
const cross = drive(live.events)
const compactCross = cross.events.find(e => e.kind === 'compaction' && e.seq === 10)
assert.equal(compactCross.turn, 2, 'between-turn compaction lands on the next turn')
assert.equal(compactCross.step, 1, 'lands on the next turn\'s first step')
assert.equal(compactCross.fromTurn, 1, 'cross-turn range remembers the previous turn')
assert.equal(compactCross.fromStep, 2, 'cross-turn range remembers the previous step')
// -- an empty-content assistant message (usage-only step) prices 0 tokens --
live.events.push({ seq: 12, type: 'assistant/message', time: 9000, data: { turn: 2, step: 2, message: { content: [] } } })
const emptyMsg = drive(live.events)
const emptyNode = emptyMsg.nodes.find(n => n.seq === 12)
assert.ok(emptyNode, 'usage-only message is still a surface node')
assert.equal(emptyNode.tokens, 0, 'empty assistant message prices 0, like dsh deriveEventMessage')
// A LATER step in the same turn must not re-attribute the compaction.
const compactStill = emptyMsg.events.find(e => e.kind === 'compaction' && e.seq === 8)
assert.equal(compactStill.turn, 1, 'compaction stays on Turn 1')
assert.equal(compactStill.step, 2, 'compaction stays on Step 2 (first request after it)')
// Repeat the same log: the fold is a deterministim replay, so a fresh fold
// must reproduce the identical wire value (what the projection cache relies on).
const again = drive(live.events)
assert.deepEqual(again, emptyMsg, 'deterministic replay across fresh folds')
// -- the shadow price covers the SEQ list, not the declared range end: a
// prune replacement node whose seq lies beyond the range must still be
// removed (the producer's shadowedSeqs include it) --
const shadow = drive([
{ seq: 1, type: 'user/message', time: 1000, data: { content: [{ type: 'text', text: 'a'.repeat(40) }] } },
// two tool results, 80-char text each: 20 + 4 (text) + 4 (tool-result) + 4 (role) = 32
{ seq: 2, type: 'tool/result', time: 2000, data: { message: { source: { kind: 'tool', callId: 'c1' }, content: [{ type: 'tool-result', toolCallId: 'c1', content: [{ type: 'text', text: 'b'.repeat(80) }] }] } } },
{ seq: 3, type: 'tool/result', time: 3000, data: { message: { source: { kind: 'tool', callId: 'c2' }, content: [{ type: 'tool-result', toolCallId: 'c2', content: [{ type: 'text', text: 'c'.repeat(80) }] }] } } },
// metering event: range says [2,2] but the shadowed seqs cover 2 AND 3
{ seq: 4, type: 'compaction/prune', time: 4000, data: { shadowedRange: { start: 2, end: 2 }, shadowedSeqs: [2, 3], shadowedTokenCount: 64 } },
// replacement: 'd' -> 1 + 4 (text) + 4 (tool-result) + 4 (role) = 13
{ seq: 5, type: 'tool/result', time: 5000, surfaceOp: { op: 'replace', start: 2, end: 2 }, data: { message: { source: { kind: 'tool', callId: 'c3' }, content: [{ type: 'tool-result', toolCallId: 'c3', content: [{ type: 'text', text: 'd' }] }] } } },
])
assert.equal(shadow.current.tool, 13, 'shadowed seqs beyond the range end are removed too (only the replacement survives)')
assert.equal(shadow.current.user, 18, 'unrelated nodes keep their price (40 chars: 10 + 4 + 4)')
assert.equal(shadow.current.total, 18 + 13, 'total reflects the seq-based removal')
// no tool/call events in this log, so no call name is registered — the tool
// label stays empty (never a crash)
assert.equal(shadow.nodes.find(n => n.seq === 5).tool, undefined, 'no registered call name -> no tool label')
// -- tool names also resolve from the content block's toolCallId when the
// source is absent on a (legacy) envelope --
const legacyTool = drive([
{ seq: 1, type: 'user/message', time: 1000, data: { content: [{ type: 'text', text: 'x'.repeat(40) }] } },
{ seq: 2, type: 'tool/result', time: 2000, data: { message: { content: [{ type: 'tool-result', toolCallId: 't9', content: [{ type: 'text', text: 'y' }] }] } } },
])
assert.equal(legacyTool.nodes.find(n => n.seq === 2).tool, undefined,
'no source and no registered call name -> no tool label (never crashes)')
// -- turn-based retention: long sessions trim by whole turns, never mid-turn --
// 400 turns x 4 steps = 1600 requests exceeds the step bound; the fold must
// keep the newest 300 WHOLE turns (turns 101..400, 1200 requests).
const many = []
let seq = 1000
for (let turn = 1; turn <= 400; turn++) {
for (let step = 0; step < 4; step++) {
many.push({
seq: seq++, type: 'assistant/message', time: seq * 1000,
data: { turn, step, message: { content: [{ type: 'text', text: 'x' }] } },
})
}
}
const long = drive(many)
assert.equal(long.requests.length, 1200, 'kept 300 turns x 4 steps after trimming')
assert.equal(long.requests[0].turn, 101, 'trim starts at a whole turn boundary')
assert.equal(long.requests[0].step, 0, 'trim starts at the first step of that turn')
const keptTurns = new Set(long.requests.map(r => r.turn))
assert.equal(keptTurns.size, 300, 'all 300 kept turns are present')
assert.ok([...keptTurns].every(t => t >= 101 && t <= 400), 'kept turns are the newest ones')
assert.equal(long.requests.filter(r => r.turn === 101).length, 4, 'first kept turn is complete')
assert.equal(long.requests.filter(r => r.turn === 400).length, 4, 'last turn is complete')
// -- streamed chunks are structurally ignored (same reference): token-level
// events carry no state change for the timeline since the occupancy mirror
// left the fold (R3) --
const chunkEvent = { seq: 3, type: 'assistant/chunk', time: 3000, data: { turn: 1, step: 1, chunk: { type: 'usage', usage: { inputTokens: 5000 } } } }
assert.equal(def.apply(base, chunkEvent), base, 'usage chunks keep the same reference (no change feed)')
const snap3 = drive([
{ seq: 1, type: 'request/context', time: 1000, data: { contextWindow: 100000 } },
{ seq: 2, type: 'user/message', time: 2000, data: { content: [{ type: 'text', text: 'a'.repeat(40) }] } },
{ seq: 3, type: 'assistant/chunk', time: 3000, data: { turn: 1, step: 1, chunk: { type: 'usage', usage: { inputTokens: 5000, cacheReadTokens: 1000, outputTokens: 0 } } } },
])
assert.equal(snap3.occupancy, undefined, 'chunk usage never re-enters the timeline')
assert.equal(snap3.contextWindow, 100000, 'route capacity still folds from request/context')
// -- entry config: bounded slices are honored (config -> bounds threading) --
let cfgDef = null
apply({ ...fakeCtx, sessionProjections: { register(d) { cfgDef = d; return () => {} } } }, { maxNodes: 2, maxKeptTurns: 1 })
const bounded = (() => {
let st = cfgDef.init()
for (const ev of live.events) st = cfgDef.apply(st, ev)
return cfgDef.view(st)
})()
assert.ok(bounded.nodes.length <= 2, 'maxNodes bounds the served surface slice')
assert.ok(bounded.requests.length <= 8, 'maxKeptTurns bounds the retained history (400-turn fixture trimmed by whole turns)')
assert.equal(typeof cfgDef.schema.safeParse(bounded).success, 'boolean', 'custom-bounds view still passes the unit schema')
// -- model-switch semantics (R6): the only durable signal is a request
// header differing from the previous one (reason 'change'); route/capacity
// metadata (request/context) must NOT fire a switch event by itself, and a
// provider-only change with the same model is not a switch either --
const switchLog = drive([
{ seq: 1, type: 'request/header', time: 1000, data: { reason: 'initial', header: { system: 's', config: { model: 'model-a', provider: 'deepseek' } } } },
{ seq: 2, type: 'request/header', time: 2000, data: { reason: 'change', header: { system: 's', config: { model: 'model-b', provider: 'deepseek' } } } },
{ seq: 3, type: 'request/context', time: 2000, data: { model: 'model-b', provider: 'deepseek', contextWindow: 64000 } },
{ seq: 4, type: 'request/header', time: 3000, data: { reason: 'change', header: { system: 's', config: { model: 'model-b', provider: 'openai' } } } },
{ seq: 5, type: 'request/header', time: 4000, data: { reason: 'change', header: { system: 's', config: { model: 'model-b', provider: 'openai' } } } },
])
const switchEvents = switchLog.events.filter(e => e.kind === 'model')
assert.equal(switchEvents.length, 1, 'exactly one model-switch event')
assert.equal(switchEvents[0].from, 'model-a', 'switch records the previous model')
assert.equal(switchEvents[0].to, 'model-b', 'switch records the new model')
assert.equal(switchLog.model, 'model-b', 'current model follows the header')
assert.equal(switchLog.provider, 'openai', 'provider-only change updates the route, not a switch')
assert.equal(switchLog.contextWindow, 64000, 'request/context supplies route capacity')
// Usage mapping (R5): prompt-side = input + cacheRead + cacheWrite (billed
// input), output = outputTokens; reasoningTokens stay inside outputTokens.
const usageLog = drive([
{ seq: 1, type: 'assistant/message', time: 1000, data: { turn: 1, step: 1, usage: { inputTokens: 800, cacheReadTokens: 150, cacheWriteTokens: 50, outputTokens: 30, reasoningTokens: 10 }, message: { content: [] } } },
])
assert.equal(usageLog.requests[0].prompt, 1000, 'prompt side sums the disjoint input buckets')
assert.equal(usageLog.requests[0].output, 30, 'output is outputTokens (reasoning already inside)')
console.log('✔ host half functional test passed (projection unit, fold semantics, attribution, retention, shadow-price seqs, reference stability, determinism, config bounds, model switch, usage mapping)')