Replace the custom /dsh-context RPC channel + 2s client polling with a native contextTimeline session-projection unit: the host registers one pure init/apply/view unit on ctx.sessionProjections (framework-driven, persisted by the projection cache, streamed via session/projection push frames), and the client reads the finished value through the framework standard useProjection seat. Deletes the client stale-while-revalidate cache and the local service type stubs; grows the modern shelf registration (locale) and adds the published @deepseek-ai/* types as devDeps plus zod as the single runtime dependency (wire schema, kept external in the host bundle). Fixes tool-result node attribution to read the durable source.callId / content toolCallId instead of the never-present block.callId (tool labels had silently never rendered). Retention is tightened to trim by whole turn-runs deterministically (bounded state, no 1200<->1500 oscillation under incremental folding). Tests now drive the projection unit directly (host) and feed the view through a useProjection stub (client); all chart/render assertions preserved. Adds docs/redesign.md (review + target architecture, phases 0-3 landed) and docs/CLIENT_SURFACE.md (client-side audit).
228 lines
14 KiB
JavaScript
228 lines
14 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: provider-anchored, mirroring the official contextPressure --
|
|
// The usage sample (seq 7: input 900, no cache) is stamped BEFORE its own
|
|
// message joins the surface: sampled 53 = user 28 + inject 12 + tool 13,
|
|
// surface now 62 (adds the assistant 'Hi!' 9), so projected = 900 + 62 - 53.
|
|
assert.ok(v.occupancy, 'occupancy projection present')
|
|
assert.equal(v.occupancy.pressureTokens, 900, 'pressure is input + cache traffic')
|
|
assert.equal(v.occupancy.sampledSurfaceTokens, 53, 'sample anchored before the message joins the surface')
|
|
assert.equal(v.occupancy.surfaceTokens, 62, 'surface total is current')
|
|
assert.equal(v.occupancy.projectedTokens, 909, 'sample carried forward by the surface movement')
|
|
assert.equal(v.occupancy.contextWindow, 128000, 'occupancy window from request/context')
|
|
|
|
// -- 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')
|
|
|
|
// -- a streamed usage chunk is an occupancy sample even with no
|
|
// assistant/message (e.g. the step failed mid-stream) --
|
|
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.pressureTokens, 6000, 'chunk usage supplies the pressure sample')
|
|
assert.equal(snap3.occupancy.projectedTokens, 6000, 'no surface movement since the chunk sample')
|
|
assert.equal(snap3.occupancy.contextWindow, 100000, 'occupancy window present without any request record')
|
|
|
|
console.log('✔ host half functional test passed (projection unit, fold semantics, attribution, retention, shadow-price seqs, reference stability, determinism)')
|