chore(scripts): move diff-fold out of tests/ and drop its lib/ dependency
The session-log divergence check runs under bun against src/ directly, tracking the working tree instead of the last build. Also add scripts/bundle-smoke.mjs (pnpm run test:bundle): the one check that must exercise the built bundle — module-loader handoff, CSS channel tagging, registration disposal and HMR style-tag semantics in jsdom with real React.
This commit is contained in:
1 parent
ba586e18f7
commit
f45bd7e8d2
2 files changed
+295
No files matched your search
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Bundle smoke check — the ONLY test that exercises the built artifacts
|
||||
* (lib/client.js). The vitest suite tests src/ directly and never builds;
|
||||
* this script exists because the packaging glue itself (the
|
||||
* `window.__ModuleLoader__.load({id, factory})` handoff, the CSS channel's
|
||||
* data-plugin style tag, platform-module requires) only exists after tsdown
|
||||
* runs. It is opt-in: run `pnpm run build && pnpm run test:bundle`.
|
||||
*
|
||||
* Everything here runs against the REAL bundle in jsdom with REAL React and
|
||||
* the REAL ui-primitives — only the harness services (locale/slots/effects)
|
||||
* are in-memory implementations of their documented contracts.
|
||||
*
|
||||
* Exit code 0 on success, 1 on failure.
|
||||
*/
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
let bundle
|
||||
try {
|
||||
bundle = await readFile(new URL('../lib/client.js', import.meta.url), 'utf8')
|
||||
} catch {
|
||||
console.error('lib/client.js missing — run `pnpm run build` first.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const dom = new JSDOM('<!doctype html><html><head></head><body></body></html>', { pretendToBeVisual: true })
|
||||
globalThis.window = dom.window
|
||||
globalThis.document = dom.window.document
|
||||
|
||||
// The module table: the shell seeds exactly these specifiers. The bundle must
|
||||
// require nothing else (the tsdown purity gate pins this too). The primitives
|
||||
// dist ships CSS modules Node cannot parse — the bundle only references its
|
||||
// components as React leaves, so inert stand-ins play the shell's role here
|
||||
// (their real rendering is covered by the vitest jsdom lane).
|
||||
const inert = name => {
|
||||
const C = () => null
|
||||
C.displayName = name
|
||||
return C
|
||||
}
|
||||
const moduleTable = (spec) => {
|
||||
if (spec === 'react') return React
|
||||
if (spec === 'react-dom') return ReactDOM
|
||||
if (spec === '@deepseek-ai/dsh-client-ui-primitives') {
|
||||
return new Proxy({}, { get: (_t, key) => (typeof key === 'string' ? inert(key) : undefined) })
|
||||
}
|
||||
throw new Error(`bundle required a non-platform module: ${spec}`)
|
||||
}
|
||||
|
||||
let handoff = null
|
||||
dom.window.__ModuleLoader__ = { load(h) { handoff = h } }
|
||||
new Function(bundle)()
|
||||
assert.ok(handoff !== null, 'bundle must register through __ModuleLoader__.load')
|
||||
assert.equal(handoff.id, 'dsh-context', 'handoff id is the package name')
|
||||
assert.equal(typeof handoff.factory, 'function')
|
||||
|
||||
const plugin = handoff.factory(moduleTable)
|
||||
assert.equal(plugin.name, 'dsh-context')
|
||||
assert.deepEqual(plugin.inject, ['slots', 'locale'])
|
||||
|
||||
// The CSS channel: one plugin-owned style tag, minified, officially tagged.
|
||||
const styleTag = () => dom.window.document.head.querySelector('style[data-plugin="dsh-context"]')
|
||||
assert.ok(styleTag() !== null, 'plugin-owned <style data-plugin> injected at factory execution')
|
||||
assert.equal(styleTag().dataset.pluginCss, 'dsh-context/styles.css', 'official data-plugin-css tag id')
|
||||
const css = styleTag().textContent
|
||||
for (const marker of ['.lc-root', '.lc-br-elem-row', '.lc-stacked-seg', '.lc-bar-tip-on', '.lc-stat-tip', '.lc-occupied-box-on']) {
|
||||
assert.ok(css.includes(marker), `styles carry ${marker}`)
|
||||
}
|
||||
assert.ok(!css.includes('120ms ease'), 'lightningcss minified the sheet')
|
||||
|
||||
// Faithful harness services (documented contracts, in-memory).
|
||||
const removeFrom = list => item => () => {
|
||||
const i = list.indexOf(item)
|
||||
if (i >= 0) list.splice(i, 1)
|
||||
}
|
||||
const dicts = new Map()
|
||||
const slots = []
|
||||
const sources = []
|
||||
const provides = []
|
||||
const effectDisposers = []
|
||||
const ctx = {
|
||||
get(name) {
|
||||
if (name === 'inputTriggers') return { registerSource(s) { sources.push(s); return removeFrom(sources)(s) } }
|
||||
if (name === 'sessions') {
|
||||
return {
|
||||
scope: () => undefined,
|
||||
provide(d) { provides.push(d); return removeFrom(provides)(d) },
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
inject() { /* no settingsScope composed: the settings wiring stays pending */ },
|
||||
effect(fn) { const d = fn(); effectDisposers.push(d); return d },
|
||||
locale: {
|
||||
register(ns, d) { dicts.set(ns, d); return () => dicts.delete(ns) },
|
||||
bind: ns => (key, vars) => {
|
||||
let s = dicts.get(ns)?.zh[key] ?? key
|
||||
if (vars) for (const k in vars) s = s.replace('{' + k + '}', String(vars[k]))
|
||||
return s
|
||||
},
|
||||
},
|
||||
slots: {
|
||||
inject(name, fn) { slots.push([name, fn()]) },
|
||||
register: opts => opts,
|
||||
},
|
||||
}
|
||||
plugin.apply(ctx)
|
||||
|
||||
assert.ok(dicts.get('dsh-context')?.zh && dicts.get('dsh-context')?.en, 'bilingual dictionaries registered')
|
||||
assert.equal(slots.length, 2, 'view tab + input overlay slots')
|
||||
assert.equal(slots[0][0], 'conversation.view')
|
||||
assert.equal(slots[0][1].order, 20)
|
||||
assert.equal(slots[0][1].label(), '上下文', 'tab label localized')
|
||||
assert.equal(slots[1][0], 'conversation.input.overlay')
|
||||
assert.equal(typeof slots[1][1].inject('s1').hooks.contextModal.getSnapshot, 'function', 'overlay hooks carry the modal store')
|
||||
assert.equal(sources.length, 1, '/context trigger source registered')
|
||||
assert.equal(sources[0].trigger, '/', 'trigger is the slash')
|
||||
assert.equal(provides.length, 1, 'loadOlderHistory contribution registered')
|
||||
assert.deepEqual(provides[0].props, ['loadOlderHistory'])
|
||||
|
||||
// HMR safety: fiber dispose removes every registration; the style tag rides
|
||||
// the HMR receiver (data-plugin claim), not the fiber.
|
||||
for (const d of effectDisposers) d()
|
||||
assert.equal(dicts.size, 0, 'dictionaries unregistered on dispose')
|
||||
assert.equal(sources.length, 0, 'trigger source removed on dispose')
|
||||
assert.equal(provides.length, 0, 'prop contribution removed on dispose')
|
||||
assert.ok(styleTag() !== null, 'style tag survives fiber dispose (the HMR receiver claims it)')
|
||||
|
||||
console.log('✔ bundle smoke: handoff, CSS channel, registrations, HMR safety')
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Differential fold check — proves the plugin's heuristic context composition
|
||||
* equals the official token-meter `contextBreakdown` projection on real
|
||||
* session logs: `current.system`/`current.tools`/surface total vs
|
||||
* `systemTokens`/`toolsTokens`/`messageTokens`.
|
||||
*
|
||||
* Since 0.11 the plugin no longer folds provider-anchored occupancy (the
|
||||
* client reads the official `contextPressure` projection directly), but the
|
||||
* composition numbers still use the plugin's own fixed-density pricing
|
||||
* (`pricing.ts`, a mirror of the meter's estimator) — this check keeps that
|
||||
* mirror honest against the official projection over real event logs.
|
||||
*
|
||||
* ONE deliberate divergence, accounted for below: since 0.24 the plugin
|
||||
* prices `image` blocks through the official DeepSeek docs image-token
|
||||
* calculator (real vision billing, 117-384 by pixel dims) where the meter
|
||||
* uses its generic JSON branch (~40 for the durable ref). The checker
|
||||
* replays a parallel image-delta surface so image-bearing logs still match
|
||||
* EXACTLY: expected plugin surface = messageTokens + Σ live image deltas.
|
||||
*
|
||||
* The official projection is imported from a dsh SOURCE checkout (env
|
||||
* DSH_REPO, default ~/dev/deepseek-harness). The checkout needs no install:
|
||||
* the needed token-meter/session sources are copied to a temp dir with their
|
||||
* bare imports rewritten (zod is only used for persistence schemas, stubbed;
|
||||
* canonicalHeader is identity — stored headers are already canonical).
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/diff-fold.ts <session.jsonl> [more.jsonl...]
|
||||
* zstd -dc ~/.dsh/sessions/<ws>/<sid>/session.jsonl.zstd > /tmp/s.jsonl
|
||||
*
|
||||
* Exit code 0 when every log matches, 1 otherwise.
|
||||
*/
|
||||
import { mkdtempSync, writeFileSync, readFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
// Source, not the built bundle: the check must track the working tree.
|
||||
import { createContextTimelineDefinition } from '../src/host/timeline.ts'
|
||||
import { estimateImageTokens } from '../src/shared/imageTokens.ts'
|
||||
|
||||
const DSH_REPO = process.env.DSH_REPO || `${process.env.HOME}/dev/deepseek-harness`
|
||||
|
||||
// Stage the official sources with rewritten imports (no pnpm install needed).
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-diff-fold-'))
|
||||
const TM = join(DSH_REPO, 'packages/llm/token-meter/src')
|
||||
const SESS = join(DSH_REPO, 'packages/core/session/src')
|
||||
const rewrite = (file, fn) => writeFileSync(join(dir, file), fn(String(readFileSync(file.startsWith('/') ? file : join(TM, file)))))
|
||||
rewrite('breakdown-projection.ts', s => s
|
||||
.replace("import { z } from 'zod'", "import { z } from './zod-stub.ts'")
|
||||
.replace("from '@deepseek-ai/dsh-session'", "from './session-facade.ts'"))
|
||||
rewrite('surface-projection.ts', s => s
|
||||
.replace("from '@deepseek-ai/dsh-session'", "from './surface.ts'"))
|
||||
rewrite('estimate.ts', s => s)
|
||||
rewrite('projection.ts', s => s)
|
||||
writeFileSync(join(dir, 'surface.ts'), String(readFileSync(join(SESS, 'surface.ts'))))
|
||||
// Stored headers are already canonical (the loop appends canonicalHeader
|
||||
// output), so an identity is faithful for the pricing check.
|
||||
writeFileSync(join(dir, 'session-facade.ts'), `export const canonicalHeader = (h) => h\n`)
|
||||
// zod stub: the persistence schemas are built at module top level but never
|
||||
// exercised by init/apply/view — a chainable no-op satisfies construction.
|
||||
writeFileSync(join(dir, 'zod-stub.ts'), `
|
||||
const stub: any = new Proxy(() => stub, { get: () => stub, apply: () => stub })
|
||||
export const z = stub
|
||||
`)
|
||||
const { contextBreakdownProjectionDefinition } = await import(pathToFileURL(join(dir, 'breakdown-projection.ts')).href)
|
||||
const { isSurfaceEvent, deriveEventMessage } = await import(pathToFileURL(join(dir, 'surface.ts')).href)
|
||||
|
||||
/**
|
||||
* The image-pricing divergence of one message's content: per image block
|
||||
* with known dims, corrected (official calculator) minus the meter's JSON
|
||||
* price of the whole block. Unknown dims price identically on both sides.
|
||||
*/
|
||||
function imageDeltaOf(blocks) {
|
||||
let delta = 0
|
||||
if (!Array.isArray(blocks)) return 0
|
||||
for (const block of blocks) {
|
||||
if (block === null || typeof block !== 'object') continue
|
||||
if (block.type === 'image') {
|
||||
const a = block.attachment
|
||||
if (a !== null && typeof a === 'object' && typeof a.width === 'number' && typeof a.height === 'number') {
|
||||
const corrected = estimateImageTokens(a.width, a.height)
|
||||
if (corrected !== null) delta += corrected - Math.ceil(JSON.stringify(block).length / 4)
|
||||
}
|
||||
} else if (block.type === 'tool-result') {
|
||||
delta += imageDeltaOf(block.content)
|
||||
}
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
const files = process.argv.slice(2)
|
||||
if (files.length === 0) {
|
||||
console.error('usage: bun tests/diff-fold.ts <session.jsonl> [...]')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
let failed = 0
|
||||
for (const file of files) {
|
||||
const lines = readFileSync(file, 'utf8').split('\n').filter(l => l.trim() !== '')
|
||||
const events = []
|
||||
for (const line of lines) {
|
||||
let ev
|
||||
try { ev = JSON.parse(line) } catch { continue }
|
||||
if (ev !== null && typeof ev === 'object' && typeof ev.seq === 'number') events.push(ev)
|
||||
}
|
||||
|
||||
// Official fold: token-meter's contextBreakdown projection, verbatim
|
||||
// (dsh 0.1.1 moved the client view under `wire`; read either contract).
|
||||
const officialView = contextBreakdownProjectionDefinition.wire?.view
|
||||
?? contextBreakdownProjectionDefinition.view
|
||||
let st = contextBreakdownProjectionDefinition.init()
|
||||
for (const ev of events) st = contextBreakdownProjectionDefinition.apply(st, ev)
|
||||
const breakdown = officialView(st)
|
||||
|
||||
// Plugin fold through its public projection unit (source import).
|
||||
const def = createContextTimelineDefinition({})
|
||||
let mine = def.init()
|
||||
for (const ev of events) mine = def.apply(mine, ev)
|
||||
const view = def.view(mine)
|
||||
const surfaceTotal = view.current.total - view.current.system - view.current.tools
|
||||
|
||||
// Parallel image-delta surface: replay the same surface ops, tracking the
|
||||
// per-node image divergence the plugin's corrected pricing introduces.
|
||||
let live = []
|
||||
let shadowedSeqs = null
|
||||
for (const ev of events) {
|
||||
if (ev.type === 'compaction/summary' || ev.type === 'compaction/prune') {
|
||||
shadowedSeqs = Array.isArray(ev.data?.shadowedSeqs) ? ev.data.shadowedSeqs : null
|
||||
continue
|
||||
}
|
||||
if (!isSurfaceEvent(ev)) { shadowedSeqs = null; continue }
|
||||
const msg = deriveEventMessage(ev)
|
||||
const delta = msg === null ? 0 : imageDeltaOf(msg.content)
|
||||
const op = ev.surfaceOp
|
||||
if (op !== 'append') {
|
||||
if (Array.isArray(shadowedSeqs) && shadowedSeqs.length > 0) {
|
||||
const shadowed = new Set(shadowedSeqs)
|
||||
live = live.filter(n => !shadowed.has(n.seq))
|
||||
} else {
|
||||
live = live.filter(n => n.seq < op.start || n.seq > op.end)
|
||||
}
|
||||
}
|
||||
shadowedSeqs = null
|
||||
live.push({ seq: ev.seq, delta })
|
||||
}
|
||||
const imageDelta = live.reduce((sum, n) => sum + n.delta, 0)
|
||||
|
||||
const match = view.current.system === breakdown.systemTokens
|
||||
&& view.current.tools === breakdown.toolsTokens
|
||||
&& surfaceTotal === breakdown.messageTokens + imageDelta
|
||||
if (!match) failed = 1
|
||||
console.log(
|
||||
(match ? 'MATCH ' : 'MISMATCH ')
|
||||
+ file
|
||||
+ ' official=' + String(breakdown.systemTokens) + '/' + String(breakdown.toolsTokens) + '/' + String(breakdown.messageTokens)
|
||||
+ ' plugin=' + String(view.current.system) + '/' + String(view.current.tools) + '/' + String(surfaceTotal)
|
||||
+ (imageDelta !== 0 ? ' imageDelta=' + String(imageDelta) : ''),
|
||||
)
|
||||
}
|
||||
process.exit(failed)
|
||||
Reference in new issue
Block a user