refactor(comments): rewrap test comments onto fuller lines
Merge consecutive short comment lines in tests/** up to the 140-col limit without touching their text: comment-stripped content, comment text (whitespace normalised) and line-comment runs are all proven identical per file. This is the line-count half of the cleanup only; the content pass over tests/** is still outstanding.
This commit is contained in:
1 parent
537e53a56a
commit
7c87ce2b76
90 files changed
+273
-577
No files matched your search
+28
-62
@@ -26,13 +26,10 @@ export interface ClientSeam {
|
||||
markdownChrome: string
|
||||
/** The platform module table the shell seeds (client-bundle requires must resolve). */
|
||||
platformModules: readonly string[]
|
||||
/**
|
||||
* The faces the split timeline's on-demand detail channel rides
|
||||
/** The faces the split timeline's on-demand detail channel rides
|
||||
* (host/detail.ts + client/timelineSource.ts): the host's generic
|
||||
* Connection RPC registry, the browser caller, and the projection
|
||||
* registry's `stateOf` read. Each probe is a needle in the named source
|
||||
* file of the baseline tag.
|
||||
*/
|
||||
* registry's `stateOf` read. Each probe is a needle in the named source file of the baseline tag. */
|
||||
detailChannel: {
|
||||
/** `HostConnectionRpc` in the connection package's shared rpc source. */
|
||||
hostRpcFile: string
|
||||
@@ -44,15 +41,13 @@ export interface ClientSeam {
|
||||
registryFile: string
|
||||
registryNeedle: string
|
||||
}
|
||||
/**
|
||||
* The session-jump seam (the Context Dashboard's session cards and the
|
||||
/** The session-jump seam (the Context Dashboard's session cards and the
|
||||
* Agent network card's nodes — issue #90). Every supported line selects a
|
||||
* session through the view owner's `uiWorkspace.openSession` (the sidebar
|
||||
* row click's own verb), re-spelled `openSession(target: SessionTarget)` by
|
||||
* the 0.1.6 selection refactor, which also retired the sessions service's
|
||||
* own `open(id)` spelling — absent on every supported line, probed so a
|
||||
* resurrection names the seam instead of silently dead-ending the jump.
|
||||
*/
|
||||
* resurrection names the seam instead of silently dead-ending the jump. */
|
||||
sessionNav: {
|
||||
/** The view-owner navigation verb's source, plus this line's signature spelling. */
|
||||
workspaceFile: string
|
||||
@@ -61,14 +56,12 @@ export interface ClientSeam {
|
||||
sessionsFile: string
|
||||
sessionsOpen: boolean
|
||||
}
|
||||
/**
|
||||
* The Context Insights page's first-level panel seam: the layout's keyed
|
||||
/** The Context Insights page's first-level panel seam: the layout's keyed
|
||||
* `main` slot (the page's seat — also the conversation panel's own) and
|
||||
* the sidebar's `sidebar.panellist` list (the entry's seat), addressed by
|
||||
* one shared id. Both spellings shipped with the sidebar/layout split and
|
||||
* are present on every supported line; probed presence-as-declared so a
|
||||
* rename names the seam instead of silently orphaning the page.
|
||||
*/
|
||||
* rename names the seam instead of silently orphaning the page. */
|
||||
insightPage: {
|
||||
/** The panellist contract's declaring source, plus the slot's spelling in it. */
|
||||
panellistFile: string
|
||||
@@ -77,12 +70,10 @@ export interface ClientSeam {
|
||||
mainFile: string
|
||||
mainNeedle: string
|
||||
}
|
||||
/**
|
||||
* The right Sidebar's tab seam — every supported generation ships it. The
|
||||
/** The right Sidebar's tab seam — every supported generation ships it. The
|
||||
* plugin's registration stays an OPTIONAL deferred inject: a below-baseline
|
||||
* host (the gate's fallback composition) may lack the service entirely, and
|
||||
* the client must simply never register the tab there instead of pending.
|
||||
*/
|
||||
* the client must simply never register the tab there instead of pending. */
|
||||
sidebar: {
|
||||
/** The tab-type registry service's providing source. */
|
||||
serviceFile: string
|
||||
@@ -90,29 +81,22 @@ export interface ClientSeam {
|
||||
/** The keyed body seat's declaring source. */
|
||||
slotFile: string
|
||||
slotNeedle: string
|
||||
/**
|
||||
* The keyed chip-title seat the plugin's tab type also registers into
|
||||
/** The keyed chip-title seat the plugin's tab type also registers into
|
||||
* (the emblem-beside-label idiom the shipped files type uses). Present
|
||||
* wherever the body seat is; probed separately so a rename names the seam.
|
||||
*/
|
||||
* wherever the body seat is; probed separately so a rename names the seam. */
|
||||
titleSlotNeedle: string
|
||||
/**
|
||||
* The navigation face the plugin's file-open affordance rides
|
||||
/** The navigation face the plugin's file-open affordance rides
|
||||
* (`ctx.sidebarRight.openResource`): the controller's providing source and
|
||||
* the needle in it. Same package as the registry, so present wherever the
|
||||
* tab seam is; probed so a rename names the preview seam instead of
|
||||
* silently degrading the affordance.
|
||||
*/
|
||||
* tab seam is; probed so a rename names the preview seam instead of silently degrading the affordance. */
|
||||
nav: {
|
||||
/** The controller source (the public navigation face's implementation). */
|
||||
file: string
|
||||
/** The `openResource` verb's declaration/comments marker. */
|
||||
needle: string
|
||||
}
|
||||
/**
|
||||
* The guide-entry contract the plugin's contribution must satisfy: one
|
||||
* needle per field `SidebarRightGuideEntry` carries on this generation.
|
||||
*/
|
||||
/** The guide-entry contract the plugin's contribution must satisfy: one
|
||||
* needle per field `SidebarRightGuideEntry` carries on this generation. */
|
||||
guideEntry: {
|
||||
/** The declaring source. */
|
||||
file: string
|
||||
@@ -128,42 +112,31 @@ export interface Baseline {
|
||||
tag: string
|
||||
/** The vendored @deepseek-ai/cordis release that harness line ships. */
|
||||
cordis: string
|
||||
/**
|
||||
* The @deepseek-ai/dsh-session release that line vendors. The staged
|
||||
/** The @deepseek-ai/dsh-session release that line vendors. The staged
|
||||
* session-projection sources import runtime values from it (SessionLogOffset /
|
||||
* SessionSeq), so the compat driver must resolve the specifier to the tag's
|
||||
* own generation.
|
||||
*/
|
||||
* SessionSeq), so the compat driver must resolve the specifier to the tag's own generation. */
|
||||
session: string
|
||||
/**
|
||||
* The durable-event families THIS line's log carries that the host fold
|
||||
/** The durable-event families THIS line's log carries that the host fold
|
||||
* switches on — the probe asserts every one exists in the tag's
|
||||
* `KNOWN_SESSION_EVENT_TYPES`. Every supported line is the V4 generation,
|
||||
* so the lists match; the matrix also asserts the union covers the fold's
|
||||
* whole vocabulary, so a fold case added without a baseline list fails
|
||||
* loudly instead of going unprobed.
|
||||
*/
|
||||
* whole vocabulary, so a fold case added without a baseline list fails loudly instead of going unprobed. */
|
||||
foldEventTypes: readonly string[]
|
||||
client: ClientSeam
|
||||
/**
|
||||
* The step-boundary identity guard's host seam (src/host/stepIdentity.ts):
|
||||
/** The step-boundary identity guard's host seam (src/host/stepIdentity.ts):
|
||||
* the agent-loop file that dispatches the `agent/pre-step` waterfall and
|
||||
* appends its decision's messages, plus a shipped context plugin proving
|
||||
* the `prepend` listener option this guard rides on that line.
|
||||
*/
|
||||
* the `prepend` listener option this guard rides on that line. */
|
||||
stepGuard: {
|
||||
loopFile: string
|
||||
loopNeedles: readonly string[]
|
||||
prependProofFile: string
|
||||
}
|
||||
/**
|
||||
* The tag's settings-namespace surface. Every supported line is the V4+
|
||||
/** The tag's settings-namespace surface. Every supported line is the V4+
|
||||
* Config-form generation: configuration forms derive from each loader
|
||||
* entry's own Config schema — the `settings.register` host face is gone
|
||||
* (probed absent, so the plugin never calls it), and the browser half rides
|
||||
* the `configForms` service and the Plugins page's keyed
|
||||
* `plugins.bundle.config` seat.
|
||||
*/
|
||||
* the `configForms` service and the Plugins page's keyed `plugins.bundle.config` seat. */
|
||||
settings: {
|
||||
/** The settings service source missing the register face. */
|
||||
serviceFile: string
|
||||
@@ -179,15 +152,13 @@ export interface Baseline {
|
||||
transportFile: string
|
||||
transportPresent: boolean
|
||||
}
|
||||
/**
|
||||
* The balance capsule's host seam (src/host/balance.ts). The facts fold
|
||||
/** The balance capsule's host seam (src/host/balance.ts). The facts fold
|
||||
* reads the DeepSeek API-key provider's settings row off the Config-form
|
||||
* `describe()` projection — the provider row is the entry whose served
|
||||
* value declares the top-level `apiKeyEnv` credential ref, preferred under
|
||||
* the settings ids the generations have served it as. The retired
|
||||
* registered-section `get(ns)` read is probed absent, so a resurrection
|
||||
* names the seam instead of silently shadowing the projection.
|
||||
*/
|
||||
* names the seam instead of silently shadowing the projection. */
|
||||
balance: {
|
||||
/** The settings service source the face needles are asserted in. */
|
||||
settingsFile: string
|
||||
@@ -201,11 +172,8 @@ export interface Baseline {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One support-matrix entry. Every supported line is the V4 generation, so
|
||||
* the seams are spelled once here; a future line that diverges on a needle
|
||||
* outgrows the factory (inline its entry instead).
|
||||
*/
|
||||
/** One support-matrix entry. Every supported line is the V4 generation, so
|
||||
* the seams are spelled once here; a future line that diverges on a needle outgrows the factory (inline its entry instead). */
|
||||
function baseline(id: BaselineId, tag: string, session: string): Baseline {
|
||||
return {
|
||||
id,
|
||||
@@ -317,8 +285,7 @@ export const BASELINES: readonly Baseline[] = [
|
||||
// baseline pins `0.1.7-rc.2`, the first release of the line with a
|
||||
// complete npm dependency closure; rc.2 adds `startsSeries` to the first
|
||||
// `request/header` of a resumed series and the `developer/message`
|
||||
// tool-registry events. startsSeries is inert to the fold; registry
|
||||
// messages contribute to injected context.
|
||||
// tool-registry events. startsSeries is inert to the fold; registry messages contribute to injected context.
|
||||
// 0.1.7 enforces plugin dsh-peer compatibility at startup and install
|
||||
// (evaluatePluginCompatibility); this plugin's `>=0.1.7-rc.2` dsh peers
|
||||
// satisfy every supported line under the gate's includePrerelease check.
|
||||
@@ -333,7 +300,6 @@ export const BASELINES: readonly Baseline[] = [
|
||||
// `configForms` transport, the balance settings face, the step-boundary
|
||||
// guard, and the durable event vocabulary (still the V4 generation — the
|
||||
// session-format tree carries no v4-to-v5 migration). Cordis stays 4.0.4,
|
||||
// and the line continues to enforce the plugin dsh-peer gate this
|
||||
// plugin's `>=0.1.7-rc.2` peers satisfy.
|
||||
// and the line continues to enforce the plugin dsh-peer gate this plugin's `>=0.1.7-rc.2` peers satisfy.
|
||||
baseline('v0.2.0-rc.2', 'dsh-v0.2.0-rc.2', '0.2.0-rc.2'),
|
||||
]
|
||||
@@ -536,8 +536,7 @@ describe('layoutForest', () => {
|
||||
assert.equal(linkG.y2, (pointOf.get('g')?.y as number) - AGENT_NODE_R - 10)
|
||||
assert.equal(linkG.running, false)
|
||||
assert.equal(layout.links.find(l => l.to === 'b')?.running, true)
|
||||
// Links carry the family hue of the child's level-1 subtree: g inherits
|
||||
// a's family (0); b is its own family (1).
|
||||
// Links carry the family hue of the child's level-1 subtree: g inherits a's family (0); b is its own family (1).
|
||||
assert.equal(linkG.color, familyHue(0))
|
||||
assert.equal(layout.links.find(l => l.to === 'b')?.color, familyHue(1))
|
||||
assert.equal(layout.links.length, 3)
|
||||
@@ -619,8 +618,7 @@ describe('layoutForest', () => {
|
||||
|
||||
test('responsive: child bands interleave with later parent bands (kinship order)', () => {
|
||||
// Stage 224 → perLevel = 2. Root's children split [p1,p2] / [p3]; p1's
|
||||
// kids' band lands right after p1's own band — BEFORE p3's band, so a
|
||||
// family never straddles a stranger's row.
|
||||
// kids' band lands right after p1's own band — BEFORE p3's band, so a family never straddles a stranger's row.
|
||||
const layout = layoutForest(forestOf([
|
||||
['root'],
|
||||
['p1', 'root'],
|
||||
@@ -636,8 +634,7 @@ describe('layoutForest', () => {
|
||||
assert.equal(pointOf.get('p3')?.y, 56 + 3 * 154)
|
||||
// Kinship interleave: p1's grandchildren sit between p1's band and p3's band.
|
||||
assert.ok((pointOf.get('k1')?.y as number) < (pointOf.get('p3')?.y as number))
|
||||
// Links keep their family hue through the interleave: p1's subtree is
|
||||
// family 0, p3 is family 2.
|
||||
// Links keep their family hue through the interleave: p1's subtree is family 0, p3 is family 2.
|
||||
assert.equal(layout.links.find(l => l.to === 'k1')?.color, familyHue(0))
|
||||
assert.equal(layout.links.find(l => l.to === 'p3')?.color, familyHue(2))
|
||||
assert.equal(layout.width, 224)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Step brief (src/client/brief.ts): node merge/sort, the per-step brief
|
||||
// (response hit, turn-start detection, opener scan, mid-turn inputs), and
|
||||
// the tooltip reply previews.
|
||||
// (response hit, turn-start detection, opener scan, mid-turn inputs), and the tooltip reply previews.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Tool-call summaries (src/client/callSummary.ts): argument JSON parsing,
|
||||
// the description/file_path/path/filePath key priority, and the
|
||||
// conversation-block scanners.
|
||||
// the description/file_path/path/filePath key priority, and the conversation-block scanners.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Category parts (src/client/categories.ts): the seven-bucket parts builder,
|
||||
// the official contextBreakdown split (rounding residue on the largest
|
||||
// category, clamped), the provider-anchored reproportioning, and the Token
|
||||
// card's billed-usage split.
|
||||
// category, clamped), the provider-anchored reproportioning, and the Token card's billed-usage split.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -74,8 +74,7 @@ describe('candidates', () => {
|
||||
const { source } = setup()
|
||||
const candidates = await source.candidates({ sessionId: 'cmd-empty' }, { query: '', position: 'leading', signal: SIGNAL })
|
||||
// The localized section replaces the menu's raw source-name title row;
|
||||
// the localized label and glyph are display-only (older harness builds
|
||||
// render the bare name).
|
||||
// the localized label and glyph are display-only (older harness builds render the bare name).
|
||||
assert.deepEqual(candidates, [{
|
||||
name: 'context',
|
||||
label: DICT_EN['cmd.label'],
|
||||
|
||||
@@ -33,20 +33,17 @@ function historyEvents(): unknown[] {
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Arm a test ctx with the harness service faces the way the generation
|
||||
/** Arm a test ctx with the harness service faces the way the generation
|
||||
* composes them: the gateway remotes (the traced `remote` facade carrying
|
||||
* the session page, plus the direct `remote.session` service whose
|
||||
* undeclared reads throw) and the durable-image loader service. Arm the
|
||||
* plugin's declared inject (`watchHistoryFaces`) to resolve the page face.
|
||||
*/
|
||||
* plugin's declared inject (`watchHistoryFaces`) to resolve the page face. */
|
||||
export function baselineCtx(): { ctx: TestClientCtx; calls: { page: unknown[]; image: { sessionId: string; attachment: unknown }[] } } {
|
||||
const ctx = new TestClientCtx()
|
||||
const calls: { page: unknown[]; image: { sessionId: string; attachment: unknown }[] } = { page: [], image: [] }
|
||||
const events = historyEvents()
|
||||
|
||||
// The remote resolves the ClientResult itself; rows are
|
||||
// `{type:'event', event}` records.
|
||||
// The remote resolves the ClientResult itself; rows are `{type:'event', event}` records.
|
||||
const page = (request: unknown): Promise<unknown> => {
|
||||
calls.page.push(request)
|
||||
return Promise.resolve({ ok: true, value: { records: events.map(event => ({ type: 'event', event })) } })
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// face and its response envelope (the remote.session gateway), and the
|
||||
// MarkdownText chrome prop (required `labels`). These are the seams whose
|
||||
// drift produced the recurring client-side incidents (issues #8, #12, #26).
|
||||
//
|
||||
// The real-code complement — the ACTUAL dsh sources per tag — runs in the
|
||||
// `compat` vitest project (tests/compat/matrix.spec.ts).
|
||||
|
||||
@@ -29,8 +28,7 @@ import { baselineCtx, chatSeat, convNodes } from './baselineFaces'
|
||||
const kit = makeKit()
|
||||
|
||||
// The chrome-prop capture: the plugin must hand EVERY markdown render a
|
||||
// well-formed `labels` object (the primitives REQUIRE it). Intercept the
|
||||
// primitive.
|
||||
// well-formed `labels` object (the primitives REQUIRE it). Intercept the primitive.
|
||||
const captured = vi.hoisted(() => ({ markdownProps: undefined as Record<string, unknown> | undefined }))
|
||||
vi.mock('@deepseek-ai/dsh-client-ui-primitives', async () => {
|
||||
const React = await import('react')
|
||||
@@ -39,8 +37,7 @@ vi.mock('@deepseek-ai/dsh-client-ui-primitives', async () => {
|
||||
captured.markdownProps = props
|
||||
return React.createElement('div', null, String(props.text ?? ''))
|
||||
},
|
||||
// The rich-text copy control's glyphs and clipboard writer; the
|
||||
// Plugin Info card's settings gear.
|
||||
// The rich-text copy control's glyphs and clipboard writer; the Plugin Info card's settings gear.
|
||||
IconCopyOutlineRegular: () => React.createElement('span', null),
|
||||
IconCheckOutlineRegular: () => React.createElement('span', null),
|
||||
IconSettingsOutlineMedium: () => React.createElement('span', null),
|
||||
@@ -193,8 +190,7 @@ for (const baseline of BASELINES) {
|
||||
// Issue #42: a traced proxy can throw on the property READ itself
|
||||
// ("cannot get property "remote.session" without inject") — arming the
|
||||
// declared inject with such a hostile facade must leave the slot unset
|
||||
// (nothing escapes the callback) and the tab renders with its static
|
||||
// hint instead of crashing.
|
||||
// (nothing escapes the callback) and the tab renders with its static hint instead of crashing.
|
||||
const ctx = new TestClientCtx()
|
||||
ctx.setService('remote', {
|
||||
get session() { throw new Error('cannot get property "remote.session" without inject') },
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// AgentGraph (src/client/components/agentGraph.tsx) — the Agent network card
|
||||
// rendered for real against a faithful in-memory sessions face: tree
|
||||
// rendering with donut/ring geometry, hover inspector, click/keyboard
|
||||
// navigation, catalog refresh, live snapshot updates, and every degrade arm
|
||||
// (no service, no anchor, no stats).
|
||||
// navigation, catalog refresh, live snapshot updates, and every degrade arm (no service, no anchor, no stats).
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -433,10 +432,7 @@ describe('AgentGraph — cold-relative composition fetch', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A programmable global fetch recording its reads: POSTs resolve `value`
|
||||
* (or throw, or hold until released).
|
||||
*/
|
||||
/** A programmable global fetch recording its reads: POSTs resolve `value` (or throw, or hold until released). */
|
||||
function detailFetch(options: { reject?: boolean; head?: unknown; nullValue?: boolean; defer?: boolean } = {}) {
|
||||
const calls: string[] = []
|
||||
let release: ((value: unknown) => void) | undefined
|
||||
@@ -467,14 +463,12 @@ describe('AgentGraph — cold-relative composition fetch', () => {
|
||||
const { View, face } = makeFetchingView()
|
||||
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
|
||||
|
||||
// The cold relative fetched at mount; until the read lands it wears the
|
||||
// pressure-only fused ring (arc + free outline).
|
||||
// The cold relative fetched at mount; until the read lands it wears the pressure-only fused ring (arc + free outline).
|
||||
assert.deepEqual(rpc.calls, ['/api/dsh-context/detail:done'])
|
||||
assert.equal(query(m.container, 'g[data-agent="done"]').querySelectorAll('circle.lc-agent-seg').length, 2)
|
||||
|
||||
// A snapshot tick while the read is in flight re-attaches the SAME
|
||||
// pending read; its duplicate landing settles on the identity bail-out
|
||||
// instead of re-rendering.
|
||||
// pending read; its duplicate landing settles on the identity bail-out instead of re-rendering.
|
||||
await act(async () => {
|
||||
face.setState(family())
|
||||
})
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
// currency shows with the account's first currency as fallback, and the
|
||||
// non-zero parts of the breakdown (topped-up / granted; the pill itself
|
||||
// carries the total) ride the page's hover tip via the pill's data
|
||||
// attributes. The route read is stubbed per test; memory and storage are
|
||||
// reset between tests.
|
||||
// attributes. The route read is stubbed per test; memory and storage are reset between tests.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -85,8 +84,7 @@ describe('BalanceCapsule', () => {
|
||||
assert.equal(query(m.container, '.lc-ov-balance-value')?.textContent, '$12.50')
|
||||
// The breakdown lives in the pill's data attributes for the page's hover
|
||||
// tip (no native `title`): the pill already carries the total, so only
|
||||
// the non-zero parts ride the bubble — the topped-up line leads the
|
||||
// granted one, and the tip drops below the pill.
|
||||
// the non-zero parts ride the bubble — the topped-up line leads the granted one, and the tip drops below the pill.
|
||||
assert.equal(pill?.getAttribute('title'), null)
|
||||
assert.equal(pill?.getAttribute('data-lc-tip'), 'Topped-up balance: $10.00\nGranted balance: $2.50')
|
||||
assert.equal(pill?.getAttribute('data-lc-tip-side'), 'bottom')
|
||||
@@ -119,8 +117,7 @@ describe('BalanceCapsule', () => {
|
||||
|
||||
test('the zh locale shows CNY with localized breakdown labels', async () => {
|
||||
stubRoute({ ok: true, value: WIRE_BALANCE })
|
||||
// The kit's `t` is the harness's locale-bound translate (zh), while the
|
||||
// ctx's locale service drives the currency pick.
|
||||
// The kit's `t` is the harness's locale-bound translate (zh), while the ctx's locale service drives the currency pick.
|
||||
const Capsule = makeBalanceCapsule(asClientCtx(new TestClientCtx({ locale: 'zh' })), makeKit('zh'))
|
||||
const m = await mount(h(Capsule, {}))
|
||||
await flush()
|
||||
|
||||
@@ -46,8 +46,7 @@ afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
// The cold-start share (timelineSource.ts) is page-lifetime per session:
|
||||
// drop it so one test's cold read never leaks its state into a later test
|
||||
// that reuses the same session id.
|
||||
// drop it so one test's cold read never leaks its state into a later test that reuses the same session id.
|
||||
resetTimelineDetailStores()
|
||||
})
|
||||
|
||||
@@ -302,8 +301,7 @@ describe('ContextModal', () => {
|
||||
useContextModal: boundModalHook(sid),
|
||||
useProjection: () => undefined,
|
||||
}))
|
||||
// Relocate the mount container under the frame before opening, so the
|
||||
// measure walks the real ancestor chain.
|
||||
// Relocate the mount container under the frame before opening, so the measure walks the real ancestor chain.
|
||||
seat.appendChild(m.container)
|
||||
await act(async () => {
|
||||
modalStoreOf(sid).set(true)
|
||||
@@ -312,8 +310,7 @@ describe('ContextModal', () => {
|
||||
assert.equal(backdrop.style.left, '280px')
|
||||
assert.equal(backdrop.style.right, '360px')
|
||||
|
||||
// A sidebar rewrite while open (the right panel closed here) is followed
|
||||
// through the observer on both edges.
|
||||
// A sidebar rewrite while open (the right panel closed here) is followed through the observer on both edges.
|
||||
await act(async () => {
|
||||
frame.style.gridTemplateColumns = '56px minmax(0px, 1fr) minmax(0px, 0px)'
|
||||
})
|
||||
@@ -506,8 +503,7 @@ describe('ContextModal — the split generation', () => {
|
||||
|
||||
// The face lands mid-open (cordis replays the pending inject): the modal
|
||||
// re-renders, the fetcher builds, and the open epoch fetches by itself.
|
||||
// The face rides INSIDE the `remote` facade; the `remote.session` service
|
||||
// key stays hostile, as on the real host.
|
||||
// The face rides INSIDE the `remote` facade; the `remote.session` service key stays hostile, as on the real host.
|
||||
await act(async () => {
|
||||
ctx.setService('remote.session', { get page() { throw new Error('cannot get property "remote.session" without inject') } })
|
||||
ctx.setService('remote', {
|
||||
|
||||
@@ -22,8 +22,7 @@ import { DICT_EN } from '../../../src/client/i18n'
|
||||
import { TestClientCtx, TestLocale, asClientCtx } from '../helpers/harness'
|
||||
import { click, flush, hover, makeKit, mount, query, queryAll, silenceWindowErrors, text, unhover, until } from '../helpers/kit'
|
||||
|
||||
// pluginInfo's npm-registry probe stays inert (and '0.0.0-dev' short-circuits
|
||||
// it anyway).
|
||||
// pluginInfo's npm-registry probe stays inert (and '0.0.0-dev' short-circuits it anyway).
|
||||
vi.stubGlobal('fetch', () => Promise.resolve({ ok: false } as Response))
|
||||
|
||||
const kit = makeKit()
|
||||
@@ -32,8 +31,7 @@ afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
// The cold-start share (timelineSource.ts) is page-lifetime per session:
|
||||
// drop it so one test's cold read never leaks its failed/ready state into
|
||||
// a later test that reuses the same session id.
|
||||
// drop it so one test's cold read never leaks its failed/ready state into a later test that reuses the same session id.
|
||||
resetTimelineDetailStores()
|
||||
})
|
||||
|
||||
@@ -195,8 +193,7 @@ describe('ContextView — projection guards', () => {
|
||||
assert.ok(text(m.container).includes(DICT_EN['overview.used']))
|
||||
// The Token card's center is the chat stats line's whole-session billed
|
||||
// total off the official tokenUsage projection (100 + 200 + 0 + 50), and
|
||||
// its corner shows the line's own cache-hit rate two decimals deep
|
||||
// (200 / 300, truncated).
|
||||
// its corner shows the line's own cache-hit rate two decimals deep (200 / 300, truncated).
|
||||
const tokensCard = queryAll(m.container, '.lc-head > .lc-col-donut')[0]
|
||||
assert.equal(query(tokensCard, '.lc-donut-center b').textContent, '350')
|
||||
assert.equal(query(tokensCard, '.lc-card-rate').textContent, 'Cache Hit 66.66%')
|
||||
@@ -871,8 +868,7 @@ describe('ContextView — file activity card', () => {
|
||||
const card = queryAll(m.container, '.lc-card').find(c => text(c).includes(DICT_EN['files.title']))
|
||||
assert.ok(card !== undefined)
|
||||
const name = query(query(card, '.lc-fa-row'), '.lc-fa-file')
|
||||
// The preview affordance leads where the column exists; the system open
|
||||
// stays unreached.
|
||||
// The preview affordance leads where the column exists; the system open stays unreached.
|
||||
assert.equal(name.getAttribute('title'), DICT_EN['files.preview'])
|
||||
await click(name)
|
||||
assert.deepEqual(previewed, ['dsh-resource://file/session/sv-preview/src/a.ts'])
|
||||
@@ -1014,8 +1010,7 @@ describe('ContextView — targeted content fetch and image loading', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
// The direct `remote.session` service is hostile, as on the real host:
|
||||
// the face resolves only through the declared inject.
|
||||
// The direct `remote.session` service is hostile, as on the real host: the face resolves only through the declared inject.
|
||||
ctx.setService('remote.session', { get page() { throw new Error('cannot get property "remote.session" without inject') } })
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
const View = makeView(ctx)
|
||||
@@ -1487,8 +1482,7 @@ describe('ContextView — the split generation (slim head + detail channel)', ()
|
||||
const ctx = slimCtx(async () => ({ ok: true, value: slimDetail() }))
|
||||
const View = makeView(ctx)
|
||||
// The jump is armed BEFORE the view mounts (the chat action relay) — the
|
||||
// detail read is still pending, so the pin must wait for it (not consume
|
||||
// and clamp against an empty record list).
|
||||
// detail read is still pending, so the pin must wait for it (not consume and clamp against an empty record list).
|
||||
requestContextFocus('sv-slim-jump', 4)
|
||||
const m = await mount(h(View, { sessionId: 'sv-slim-jump', useProjection: projectionsFor(slimHead()) }))
|
||||
assert.equal(queryAll(m.container, '.lc-bar-selected').length, 0, 'nothing pins before the detail')
|
||||
@@ -1515,8 +1509,7 @@ describe('ContextView — the op-log generation (fileOps on the detail payload)'
|
||||
nodes: [],
|
||||
droppedNodes: 0,
|
||||
archive: [],
|
||||
// The op log covers the full session — the conversation window
|
||||
// join plays no role in this card on this generation.
|
||||
// The op log covers the full session — the conversation window join plays no role in this card on this generation.
|
||||
fileOps: [
|
||||
{ seq: 1, path: '/ws/README.md', kind: 'read', tool: 'read', err: false, added: 0, removed: 0, read: { start: 1, count: 12 } },
|
||||
{ seq: 2, path: '/ws/src/a.ts', kind: 'write', tool: 'edit', err: false, added: 3, removed: 1 },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// The Insights page's date-range picker (src/client/components/dateRange.tsx)
|
||||
// — the two-click range in either order, the highlighted band and edges, the
|
||||
// portaled panel's own seat and every way out of it, and the month nav plus
|
||||
// the grid's keyboard cursor.
|
||||
// portaled panel's own seat and every way out of it, and the month nav plus the grid's keyboard cursor.
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -322,8 +321,7 @@ describe('the keyboard cursor', () => {
|
||||
describe('the panel seat', () => {
|
||||
test('sits under its trigger, inside the viewport, and flips when it would fall off', async () => {
|
||||
// jsdom measures every element as 0; drive the panel's own box, then put
|
||||
// the prototype's own descriptors back (deleting them would leave every
|
||||
// later measurement NaN).
|
||||
// the prototype's own descriptors back (deleting them would leave every later measurement NaN).
|
||||
const size = { w: 240, h: 300 }
|
||||
const own = (name: string): PropertyDescriptor | undefined => Object.getOwnPropertyDescriptor(HTMLElement.prototype, name)
|
||||
const width = own('offsetWidth')
|
||||
@@ -336,8 +334,7 @@ describe('the panel seat', () => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, get: () => size.h })
|
||||
try {
|
||||
const { m } = await open()
|
||||
// The panel measures its anchor wrapper (the trigger's own box): jsdom's
|
||||
// rects are all zeros, so drive the one it reads.
|
||||
// The panel measures its anchor wrapper (the trigger's own box): jsdom's rects are all zeros, so drive the one it reads.
|
||||
const rect = { left: 800, right: 900, top: 680, bottom: 700 }
|
||||
query(m.container, '.lc-dp').getBoundingClientRect = () =>
|
||||
({ ...rect, x: rect.left, y: rect.top, width: 100, height: 20, toJSON: () => '' }) as DOMRect
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Donut (src/client/components/donut.tsx) rendered with real React:
|
||||
// proportional SVG segments around the center label, with the empty ring
|
||||
// fallback and hostile-value skips.
|
||||
// proportional SVG segments around the center label, with the empty ring fallback and hostile-value skips.
|
||||
|
||||
import { createElement as h, useState } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// ErrorBoundary (src/client/components/errorBoundary.tsx): real subtree
|
||||
// errors degrade to the styled error card; Retry resets the boundary.
|
||||
// React logs caught errors to console and replays the failed render through a
|
||||
// fake DOM event — silenced with a window-error preventer and a spy, never
|
||||
// mocked.
|
||||
// fake DOM event — silenced with a window-error preventer and a spy, never mocked.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -96,8 +96,7 @@ describe('EventList', () => {
|
||||
assert.ok(text(rows[0]).includes('Context Injection'))
|
||||
assert.equal(queryAll(rows[0], '.lc-event-tokens').length, 0)
|
||||
|
||||
// Foreign kind: EVENT_ICONS miss → '•' fallback, and the kind label
|
||||
// resolves through the locale chain to the key itself.
|
||||
// Foreign kind: EVENT_ICONS miss → '•' fallback, and the kind label resolves through the locale chain to the key itself.
|
||||
assert.equal(query(rows[1], '.lc-event-icon').textContent, '•')
|
||||
assert.equal(query(rows[1], '.lc-kind').textContent, 'kind.mystery')
|
||||
|
||||
@@ -133,10 +132,8 @@ describe('EventList', () => {
|
||||
assert.equal(labels[0].getAttribute('title'), '')
|
||||
assert.equal(labels[1].getAttribute('title'), '')
|
||||
|
||||
// Newest first: labels[0] is the prune, labels[1] the inject.
|
||||
// Force an empty-text overflow on the prune label (the
|
||||
// `el.textContent || ''` fallback arm) and a real overflow on the
|
||||
// inject label, then resize.
|
||||
// Newest first: labels[0] is the prune, labels[1] the inject. Force an empty-text overflow on the prune label (the
|
||||
// `el.textContent || ''` fallback arm) and a real overflow on the inject label, then resize.
|
||||
Object.defineProperty(labels[0], 'scrollWidth', { value: 100, configurable: true })
|
||||
Object.defineProperty(labels[1], 'scrollWidth', { value: 100, configurable: true })
|
||||
labels[0].textContent = ''
|
||||
|
||||
@@ -532,15 +532,13 @@ describe('FileCard — workspace display and system open', () => {
|
||||
onPreview: e => { previewed.push(e); return true },
|
||||
onOpen: p => { opened.push(p) },
|
||||
}))
|
||||
// The preview affordance leads: the title says so, and the click never
|
||||
// reaches the system opener.
|
||||
// The preview affordance leads: the title says so, and the click never reaches the system opener.
|
||||
const a = baseOf(rowOf(m.container, '/repo/src/a.ts'))
|
||||
assert.equal(a.getAttribute('title'), 'Preview in the sidebar')
|
||||
await click(a)
|
||||
assert.deepEqual(previewed.map(e => e.path), ['/repo/src/a.ts'])
|
||||
assert.equal(opened.length, 0)
|
||||
// A refusal (no preview type claims it, no surface mounted) still opens on
|
||||
// the system rather than becoming an inert click.
|
||||
// A refusal (no preview type claims it, no surface mounted) still opens on the system rather than becoming an inert click.
|
||||
await m.unmount()
|
||||
const m2 = await mount(h(FileCard, {
|
||||
activity: workspaceActivity(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// The activity heatmap (src/client/components/heatmap.tsx): grid math off
|
||||
// the injected today key, depth levels, day pinning, future cells, and the
|
||||
// empty/degraded paths — rendered with real React.
|
||||
// the injected today key, depth levels, day pinning, future cells, and the empty/degraded paths — rendered with real React.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -82,8 +81,7 @@ describe('Heatmap', () => {
|
||||
const spans = queryAll(m.container, 'span.lc-heat-cell')
|
||||
assert.ok(spans.length > 0)
|
||||
assert.ok(spans.some(s => s.className.includes('lc-heat-future')), 'future placeholders present')
|
||||
// Weekday labels sit on the rows they name (Sunday-first: row 1 = Mon):
|
||||
// Mon/Wed/Fri carry their names, the rest blank.
|
||||
// Weekday labels sit on the rows they name (Sunday-first: row 1 = Mon): Mon/Wed/Fri carry their names, the rest blank.
|
||||
const wds = queryAll(m.container, '.lc-heat-wd')
|
||||
assert.deepEqual(wds.map(w => w.textContent), ['', 'Mon', '', 'Wed', '', 'Fri', ''])
|
||||
await m.unmount()
|
||||
@@ -92,8 +90,7 @@ describe('Heatmap', () => {
|
||||
test('month labels mark the columns where a month begins', async () => {
|
||||
const days = { '2026-09-16': { tokens: 10, requests: 1, sessions: 1, cost: null } }
|
||||
// The default 8-week window runs Sunday 2026-07-26 → 2026-09-13: July
|
||||
// began outside it, August begins in the Jul 26 column (Sat Aug 1) and
|
||||
// September in the Aug 30 column (Tue Sep 1).
|
||||
// began outside it, August begins in the Jul 26 column (Sat Aug 1) and September in the Aug 30 column (Tue Sep 1).
|
||||
const m = await mount(h(Heatmap, { days, today: TODAY }))
|
||||
assert.deepEqual(queryAll(m.container, '.lc-heat-mon').map(s => s.textContent), ['Aug', 'Sep'])
|
||||
await m.unmount()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// images.tsx — imageRefOf narrowing (pure) plus ImageCard and its preview
|
||||
// wiring, rendered with real React. The preview itself is the platform
|
||||
// ImageLightbox; its dialog behavior is covered upstream, so only the
|
||||
// plugin-owned open/close wiring is pinned here.
|
||||
// ImageLightbox; its dialog behavior is covered upstream, so only the plugin-owned open/close wiring is pinned here.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -99,8 +99,7 @@ async function openPanel(
|
||||
...props,
|
||||
}
|
||||
const m = await mount(h(Panel, allProps))
|
||||
// The price book's first fetch resolves a microtask or two behind the
|
||||
// mount; a second act window keeps its notify inside act.
|
||||
// The price book's first fetch resolves a microtask or two behind the mount; a second act window keeps its notify inside act.
|
||||
await flush()
|
||||
return { m, Panel }
|
||||
}
|
||||
@@ -144,8 +143,7 @@ describe('OverviewPanel', () => {
|
||||
assert.ok(text(m.container).includes('Context Insights'))
|
||||
// The page chrome: the section's one scroll region carries the centered
|
||||
// content column — the page heading, then the 1:1 first row (the KPI
|
||||
// band beside the last-7-days usage chart), the aggregate stats pair,
|
||||
// and the insight/session body.
|
||||
// band beside the last-7-days usage chart), the aggregate stats pair, and the insight/session body.
|
||||
const page = query(m.container, 'section.lc-ov-page')
|
||||
assert.equal(page.getAttribute('aria-label'), 'Context Insights')
|
||||
const scrollRows = [...query(m.container, '.lc-ov-pagecontent').children].map(el => el.className.split(' ')[0])
|
||||
@@ -454,8 +452,7 @@ describe('OverviewPanel', () => {
|
||||
|
||||
test('the settings chip beside the title runs the preferences jump', async () => {
|
||||
// The harness chrome the jump drives (settingsJump.ts): the Plugins panel
|
||||
// entry first, then the bundle card's open control (the one non-switch
|
||||
// button inside the card), both click-tracked.
|
||||
// entry first, then the bundle card's open control (the one non-switch button inside the card), both click-tracked.
|
||||
const clicks: string[] = []
|
||||
const chrome = document.createElement('div')
|
||||
chrome.innerHTML = `
|
||||
@@ -472,8 +469,7 @@ describe('OverviewPanel', () => {
|
||||
const ctx = makeCtx()
|
||||
const { m } = await openPanel(ctx)
|
||||
const chip = query<HTMLButtonElement>(m.container, '.lc-ov-settings')
|
||||
// The chip rides the heading, directly after the title (the long hint
|
||||
// moved onto the hover title).
|
||||
// The chip rides the heading, directly after the title (the long hint moved onto the hover title).
|
||||
assert.equal(chip.textContent, 'Plugin Settings')
|
||||
assert.equal(chip.title, 'Open plugin settings')
|
||||
const head = query(m.container, '.lc-ov-pagehead')
|
||||
|
||||
@@ -443,8 +443,7 @@ describe('OverviewSkills', () => {
|
||||
const detail = query(m.container, '.lc-ov-skill-detail')
|
||||
assert.equal(detail.closest('.lc-ov-skill-unit'), null, 'no inline seat without the pinned row')
|
||||
assert.equal(queryAll(detail, '.lc-skilld-head').length, 1, 'the detached seat carries the head')
|
||||
// All twenty-five generated names match 'skill' — twenty rows plus the
|
||||
// overflow line counting the filtered set.
|
||||
// All twenty-five generated names match 'skill' — twenty rows plus the overflow line counting the filtered set.
|
||||
await actType(input, 'skill')
|
||||
assert.equal(queryAll(m.container, 'button.lc-ov-skill').length, 20)
|
||||
assert.ok(text(m.container).includes('5 more skills'), 'the overflow counts the filtered set')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// The Context Insights panel's aggregate Token Stats card
|
||||
// (src/client/components/overviewTokens.tsx) rendered with real React: the
|
||||
// donut centered on the range's merged billed total and split by the SAME
|
||||
// composition categories as the Context tab's Token card — the per-session
|
||||
// `billedParts` estimates folded by tokenPartsOf.
|
||||
// composition categories as the Context tab's Token card — the per-session `billedParts` estimates folded by tokenPartsOf.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// The first row's usage chart (src/client/components/overviewUsage.tsx):
|
||||
// the fixed last-7-days window off the injected today key, the two series'
|
||||
// independent maxima, bar heights, the tick rails and gridlines framed off
|
||||
// the same maxima, unpriced/zero days, and the empty and degraded paths —
|
||||
// rendered with real React.
|
||||
// the same maxima, unpriced/zero days, and the empty and degraded paths — rendered with real React.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// PluginInfo (src/client/components/pluginInfo.tsx): static metadata rows
|
||||
// plus the live npm latest-version check. fetch is stubbed per test and the
|
||||
// modules are re-imported fresh (vi.resetModules) so the baked-in version
|
||||
// define and the fetch TTL cache both start clean.
|
||||
// modules are re-imported fresh (vi.resetModules) so the baked-in version define and the fetch TTL cache both start clean.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -368,8 +368,7 @@ describe('RequestDetail brief section', () => {
|
||||
44: { kind: 'assistant', seq: 44, blocks: [{ kind: 'tool-call', name: 'write', argsRaw: '{"file_path":"a.ts"}' }] },
|
||||
}
|
||||
// One row per input would need 11 rows; instead render them as one inputs
|
||||
// list — chips are the unit under test (3 shown + overflow), so render
|
||||
// batches through separate mounts.
|
||||
// list — chips are the unit under test (3 shown + overflow), so render batches through separate mounts.
|
||||
const render = async (list: SurfaceNode[]) => {
|
||||
const m = await mount(h(RequestDetail, {
|
||||
request: req({}),
|
||||
@@ -412,8 +411,7 @@ describe('RequestDetail brief section', () => {
|
||||
assert.ok(chipText(m6, 0).includes('Skill · ponytail') && chipText(m6, 0).includes('notes inside'))
|
||||
await m6.unmount()
|
||||
|
||||
// A category outside the fold's vocabulary (host drift) degrades into the
|
||||
// user tail — a plain text row, never a throw.
|
||||
// A category outside the fold's vocabulary (host drift) degrades into the user tail — a plain text row, never a throw.
|
||||
const m7 = await render([surfaceNode({ seq: 43, cat: 'mystery' as never, text: 'survivor' })])
|
||||
assert.equal(chipText(m7, 0), 'survivor', 'hostile cat renders its text plainly')
|
||||
await m7.unmount()
|
||||
@@ -425,8 +423,7 @@ describe('RequestDetail brief section', () => {
|
||||
const m11 = await render([surfaceNode({ seq: 47, cat: 'skill', skill: 'grilling', tool: 'skill' })])
|
||||
assert.equal(chipText(m11, 0), 'Skill · grilling')
|
||||
await m11.unmount()
|
||||
// The catalog digest tags its form; a nameless, formless skill row
|
||||
// degrades to the context label.
|
||||
// The catalog digest tags its form; a nameless, formless skill row degrades to the context label.
|
||||
const m11b = await render([
|
||||
surfaceNode({ seq: 471, cat: 'skill', form: 'catalog' }),
|
||||
surfaceNode({ seq: 472, cat: 'skill' }),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// The preference card (src/client/components/settingsCard.tsx): the
|
||||
// Plugins-page seat rendered with real React against the real DICT_EN
|
||||
// strings; the select rows open the REAL Menu primitive (portaled into
|
||||
// document.body) and pick through it.
|
||||
// strings; the select rows open the REAL Menu primitive (portaled into document.body) and pick through it.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -75,8 +74,7 @@ describe('PluginConfigCard (the Plugins-page seat)', () => {
|
||||
assert.equal(selects[0].getAttribute('aria-expanded'), 'false')
|
||||
assert.equal(document.body.querySelector('[role="menu"]'), null)
|
||||
|
||||
// The insights-entry row follows the placement one; its pick writes the
|
||||
// visibility field.
|
||||
// The insights-entry row follows the placement one; its pick writes the visibility field.
|
||||
assert.ok(text(m.container).includes(DICT_EN['settings.insightsEntry']))
|
||||
assert.ok(text(selects[1]).includes(DICT_EN['insightsEntry.show']))
|
||||
await click(selects[1])
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// SliceList (src/client/components/sliceList.tsx) rendered with real React:
|
||||
// the donut legend rows — the primary line (color dot, label, bold share)
|
||||
// over the secondary quantity line.
|
||||
// the donut legend rows — the primary line (color dot, label, bold share) over the secondary quantity line.
|
||||
|
||||
import { createElement as h, useState } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// StackedBar + Legend (src/client/components/stackedBar.tsx) rendered with
|
||||
// real React through a state-holding hover harness, plus direct mounts for
|
||||
// the scale/free/reserve/tooltip branch matrix.
|
||||
// real React through a state-holding hover harness, plus direct mounts for the scale/free/reserve/tooltip branch matrix.
|
||||
|
||||
import { createElement as h, useState } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
// book (the store never reaches the network). The connector geometry is
|
||||
// pinned through measureFlow's unit tests (jsdom has no layout); the event
|
||||
// rows themselves live on the events card (contextView.spec.ts).
|
||||
// `countsOfRecords` and `toolTallyOf` derive the tallies the split
|
||||
// generation's wire head carries, pinned here.
|
||||
// `countsOfRecords` and `toolTallyOf` derive the tallies the split generation's wire head carries, pinned here.
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -100,8 +99,7 @@ describe('countsOfRecords (the inline generation derivation)', () => {
|
||||
[
|
||||
ev('inject'), ev('inject'), ev('inject'), ev('compaction'), ev('compaction'), ev('prune'), ev('model'), ev('mode'),
|
||||
// Skill loads and a `/name` invocation count by DISTINCT name; the
|
||||
// available-skills catalog digest rides an untagged inject event and
|
||||
// never lands in the tally.
|
||||
// available-skills catalog digest rides an untagged inject event and never lands in the tally.
|
||||
skillEv('grilling'), skillEv('grilling'), skillEv('ponytail'),
|
||||
{ ...ev('inject'), name: 'available-skills' },
|
||||
// Hostile shapes skip: a nameless tag and a non-string name.
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// React: the active-time donut (TTFT + generation vs tools vs overhead) and
|
||||
// the pct-led slice rows with call counts and true durations — plus the
|
||||
// bottom session-time strip (the fold's per-step spans packed by their share
|
||||
// of the active time in occurrence order, joined into the hover link by
|
||||
// KIND), and the empty and hostile-timing degrades.
|
||||
// of the active time in occurrence order, joined into the hover link by KIND), and the empty and hostile-timing degrades.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// StatsTokens (src/client/components/statsTokens.tsx) rendered with real
|
||||
// React: the donut centered on the chat stats line's whole-session billed
|
||||
// total, sliced by composition category — the prompt side shared by the
|
||||
// estimated ratios, output exact.
|
||||
// total, sliced by composition category — the prompt side shared by the estimated ratios, output exact.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
// is its share of the active time; idle time takes no track), the fixed
|
||||
// zero/quartile/full ticks, the kind-keyed hover link (every span of a kind
|
||||
// lights together while the tip answers only the strip's OWN pointer), and
|
||||
// the hostile-span sanitize — an unknown kind or a NaN/inverted instant drops
|
||||
// out whole instead of poisoning the axis.
|
||||
// the hostile-span sanitize — an unknown kind or a NaN/inverted instant drops out whole instead of poisoning the axis.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -43,8 +42,7 @@ describe('TimingStrip', () => {
|
||||
const segs = segsOf(m.container)
|
||||
assert.equal(segs.length, 5)
|
||||
// Width reads HOW LONG (5/10/5/15/5s of the 40s active total), position
|
||||
// reads the running share — the idle gaps (21s→26s, 41s→46s) take no
|
||||
// track: tools sits at 50%, not at the wall-clock 62.5%.
|
||||
// reads the running share — the idle gaps (21s→26s, 41s→46s) take no track: tools sits at 50%, not at the wall-clock 62.5%.
|
||||
assert.deepEqual(leftsOf(m.container), [0, 12.5, 37.5, 50, 87.5])
|
||||
assert.deepEqual(widthsOf(m.container), [12.5, 25, 12.5, 37.5, 12.5])
|
||||
assert.deepEqual(segs.map(s => s.style.background), [
|
||||
@@ -54,8 +52,7 @@ describe('TimingStrip', () => {
|
||||
'var(--color-teal-500)',
|
||||
'var(--color-slate-400)',
|
||||
])
|
||||
// The axis: zero, the three quartiles, and the full span of the 40s
|
||||
// ACTIVE total (not the 50s wall window).
|
||||
// The axis: zero, the three quartiles, and the full span of the 40s ACTIVE total (not the 50s wall window).
|
||||
const ticks = queryAll(m.container, '.lc-tstrip-tick')
|
||||
assert.deepEqual(ticks.map(t => t.textContent), ['0', '10.0s', '20.0s', '30.0s', '40.0s'])
|
||||
assert.deepEqual(ticks.map(t => (t as HTMLElement).style.left), ['0%', '25%', '50%', '75%', '100%'])
|
||||
@@ -77,8 +74,7 @@ describe('TimingStrip', () => {
|
||||
assert.equal(wrapper.length, 1)
|
||||
assert.ok(wrapper[0].className.includes('animate-lc-stacked-in'))
|
||||
assert.ok(wrapper[0].className.includes('motion-reduce:animate-none'))
|
||||
// Every band sits INSIDE the wrapper (their percent offsets keep the bar's own box) and carries no
|
||||
// animation of its own.
|
||||
// Every band sits INSIDE the wrapper (their percent offsets keep the bar's own box) and carries no animation of its own.
|
||||
assert.equal(wrapper[0].querySelectorAll('.lc-tstrip-seg').length, 300)
|
||||
assert.deepEqual(queryAll(m.container, '.lc-tstrip-seg').filter(s => /animate-/.test(s.className)), [])
|
||||
await m.unmount()
|
||||
@@ -114,8 +110,7 @@ describe('TimingStrip', () => {
|
||||
})
|
||||
|
||||
test('every span of the hovered KIND lights together and dims the rest, WITHOUT floating the tip (the mirror rule)', async () => {
|
||||
// A second ttft span (the next step's wait, sitting inside the wall-clock
|
||||
// idle window — it packs all the same).
|
||||
// A second ttft span (the next step's wait, sitting inside the wall-clock idle window — it packs all the same).
|
||||
const spans: TimingSpan[] = [...SPANS, { kind: 'ttft', start: 21_000, end: 26_000 }]
|
||||
const m = await mount(h(Strip, { spans, hoverKey: 'ttft' }))
|
||||
const on = queryAll(m.container, '.lc-tstrip-seg-on')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// UpgradeGate (src/client/components/upgradeGate.tsx) — the baseline-gate
|
||||
// modal rendered for real in jsdom: version rows, backdrop/card/Escape/OK/×
|
||||
// dismissal, and the per-session dismissal ledger across remounts and
|
||||
// in-place session switches.
|
||||
// dismissal, and the per-session dismissal ledger across remounts and in-place session switches.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// (exact, case-insensitive, and the model-side resolution tiers behind the
|
||||
// cross-provider fallback), the USD→CNY conversion at the fixed 1 CNY = 0.15
|
||||
// USD, the null degradations, the numOf coercion of garbage bucket fields,
|
||||
// the money/rate formatting, and the deep merge behind the stats board's
|
||||
// family-scope cost cells.
|
||||
// the money/rate formatting, and the deep merge behind the stats board's family-scope cost cells.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -148,8 +147,7 @@ describe('priceIndexOf / the model-side resolution tiers', () => {
|
||||
// The live-registry shape: azure re-lists `deepseek-v4-pro` flat (no
|
||||
// cache_read published) and its `@ai-sdk/azure` package is the only
|
||||
// self-named one — DeepSeek rides `@ai-sdk/openai-compatible`, so the
|
||||
// model-side tiers would hand the id to azure. The `deepseek-` spelling
|
||||
// prices from DeepSeek's own listing instead.
|
||||
// model-side tiers would hand the id to azure. The `deepseek-` spelling prices from DeepSeek's own listing instead.
|
||||
const AZURE_FLAT = { hit: 1.74, miss: 1.74, write: 1.74, out: 3.48 }
|
||||
const hostBook = bookOf({
|
||||
deepseek: { 'deepseek-v4-pro': PRO },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// dnaOf (src/client/dna.ts): the DNA strip's per-item decomposition — the
|
||||
// system prompt and the header epoch's tool schemas lead, the message flow
|
||||
// follows in seq order: the order the model reads the context, which is also
|
||||
// the order items joined it.
|
||||
// follows in seq order: the order the model reads the context, which is also the order items joined it.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -415,8 +415,7 @@ describe('activityOf — nested Code-Mode (PTC) calls', () => {
|
||||
})])
|
||||
assert.deepEqual(a.entries.map(e => e.path), ['/b.ts', '/a.ts'])
|
||||
assert.equal(a.entries[1].ops[0].err, true)
|
||||
// The parent's stamp locates the op; a failed program does not fail its
|
||||
// earlier successful reads.
|
||||
// The parent's stamp locates the op; a failed program does not fail its earlier successful reads.
|
||||
const ok = a.entries[0].ops[0]
|
||||
assert.equal(ok.err, false)
|
||||
assert.equal(ok.gone, 40)
|
||||
@@ -507,8 +506,7 @@ describe('activityOf — search meta attribution', () => {
|
||||
meta: { shape: 'paths', truncated: false, total: 1, paths: ['/old.ts'] },
|
||||
}),
|
||||
])
|
||||
// The searched target rows too: '/src' (the narrowed path) and the two
|
||||
// pathless patterns ride ahead of their hit files.
|
||||
// The searched target rows too: '/src' (the narrowed path) and the two pathless patterns ride ahead of their hit files.
|
||||
assert.deepEqual(a.entries.map(e => e.path), ['old', '/old.ts', 'broad', '/src', '/src/a.ts', '/src/b.ts'])
|
||||
const byPath = new Map(a.entries.map(e => [e.path, e]))
|
||||
const srcA = byPath.get('/src/a.ts')!
|
||||
@@ -681,8 +679,7 @@ describe('glyphOf', () => {
|
||||
assert.ok(g.text !== undefined, `badge text color missing for ${base}`)
|
||||
}
|
||||
// The text color contrasts with the fill: near-black on light shades (the
|
||||
// JS yellow, shell green, rust peach), white on dark ones (including the
|
||||
// kotlin lilac, just under the threshold).
|
||||
// JS yellow, shell green, rust peach), white on dark ones (including the kotlin lilac, just under the threshold).
|
||||
assert.equal(glyphOf('/x/index.js', 'text').text, '#1f2328')
|
||||
assert.equal(glyphOf('/x/deploy.sh', 'text').text, '#1f2328')
|
||||
assert.equal(glyphOf('/x/lib.rs', 'text').text, '#1f2328')
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Display formatting (src/client/format.ts): the k/M/B suffix style, byte
|
||||
// sizes, the cache-hit truncation, locale-safe time, locale-unit durations,
|
||||
// and leading slice shares.
|
||||
// sizes, the cache-hit truncation, locale-safe time, locale-unit durations, and leading slice shares.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// plugin's `apply` consumes. Every member implements the DOCUMENTED contract
|
||||
// (cordis effect/inject semantics, locale fallback chain, slot registry,
|
||||
// sessions scope) rather than a test-specific stub — registrations
|
||||
// and disposals behave the way the real harness's do, so specs assert through
|
||||
// the same seams the production runtime uses.
|
||||
// and disposals behave the way the real harness's do, so specs assert through the same seams the production runtime uses.
|
||||
|
||||
import type { ClientCtx, LocaleService, SlotRegistration, SlotsService } from '../../../src/client/services'
|
||||
import { makeTranslate } from './kit'
|
||||
@@ -63,12 +62,10 @@ export interface TestClientCtxOptions {
|
||||
services?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* A client ctx with cordis semantics: `inject` runs its callback once every
|
||||
/** A client ctx with cordis semantics: `inject` runs its callback once every
|
||||
* requested service exists (services are armed via options.services or
|
||||
* setService — arming later replays pending callbacks, like cordis), and
|
||||
* `effect` collects disposers that `dispose()` runs LIFO.
|
||||
*/
|
||||
* `effect` collects disposers that `dispose()` runs LIFO. */
|
||||
export class TestClientCtx {
|
||||
readonly slots = new TestSlots()
|
||||
readonly locale: TestLocale
|
||||
@@ -101,12 +98,10 @@ export class TestClientCtx {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cordis semantics: an inject gets its fiber handle as soon as it is
|
||||
/** Cordis semantics: an inject gets its fiber handle as soon as it is
|
||||
* declared (PENDING while a service is still missing) — disposing it either
|
||||
* cancels the pending wait or unwinds the loaded callback, and the loaded
|
||||
* disposer also belongs to the context's own dispose (LIFO). Either path
|
||||
* retires it, so the two lifetimes never run it twice.
|
||||
* disposer also belongs to the context's own dispose (LIFO). Either path retires it, so the two lifetimes never run it twice.
|
||||
*/
|
||||
inject(deps: string[] | Record<string, unknown>, cb: (ctx: TestClientCtx) => (() => void) | void): { dispose: () => Promise<void> } {
|
||||
const list = Array.isArray(deps) ? deps : Object.keys(deps)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Client test kit: real dictionaries, real React rendering, and a faithful
|
||||
// harness-context implementation (the documented cordis/locale/slots
|
||||
// contracts, not mocks of plugin code). Shared by every client spec so each
|
||||
// test file stays small and stateless.
|
||||
// contracts, not mocks of plugin code). Shared by every client spec so each test file stays small and stateless.
|
||||
|
||||
import { act, type ReactElement, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
@@ -20,11 +19,9 @@ export function surfaceNodeAt(seq: number, over: Partial<SurfaceNode> = {}): Sur
|
||||
return { seq, cat: 'user', tokens: 1, ...over }
|
||||
}
|
||||
|
||||
/**
|
||||
* The harness locale chain: active-locale dictionary → en → the key itself,
|
||||
/** The harness locale chain: active-locale dictionary → en → the key itself,
|
||||
* with `{name}` interpolation. Built over the plugin's REAL dictionaries, so
|
||||
* assertions exercise the shipped strings rather than a test copy.
|
||||
*/
|
||||
* assertions exercise the shipped strings rather than a test copy. */
|
||||
export function makeTranslate(active: 'en' | 'zh' = 'en', dicts?: Record<string, Record<string, string>>): Translate {
|
||||
const table = dicts ?? { zh: DICT_ZH, en: DICT_EN }
|
||||
return (key, params) => {
|
||||
@@ -76,13 +73,11 @@ export async function flush(): Promise<void> {
|
||||
await act(async () => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll until the predicate holds, one short act() scope per tick: timers and
|
||||
/** Poll until the predicate holds, one short act() scope per tick: timers and
|
||||
* promise chains only get to run while a scope's sleep is awaiting, so the
|
||||
* state updates they land are act-attributed (no React act() warnings) and
|
||||
* committed when the scope exits — the check between ticks sees them. Real
|
||||
* timers ride the store's own debounce; `message` labels the give-up failure.
|
||||
*/
|
||||
* timers ride the store's own debounce; `message` labels the give-up failure. */
|
||||
export async function until(fn: () => boolean, message: string): Promise<void> {
|
||||
for (let i = 0; i < 400; i++) {
|
||||
await act(async () => {
|
||||
@@ -94,13 +89,10 @@ export async function until(fn: () => boolean, message: string): Promise<void> {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Silence a deliberately-thrown render error: React 18 dev replays a failed
|
||||
/** Silence a deliberately-thrown render error: React 18 dev replays a failed
|
||||
* render through a fake DOM event, which jsdom reports as an uncaught window
|
||||
* error and vitest forwards to an uncaughtException — a user error listener
|
||||
* that prevents the default keeps the throw inside the test. Returns the
|
||||
* cleanup that restores normal error reporting.
|
||||
*/
|
||||
* that prevents the default keeps the throw inside the test. Returns the cleanup that restores normal error reporting. */
|
||||
export function silenceWindowErrors(): () => void {
|
||||
const onError = (e: Event) => { e.preventDefault() }
|
||||
window.addEventListener('error', onError)
|
||||
@@ -128,11 +120,8 @@ export async function click(el: HTMLElement): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointer hover. React 18 synthesizes onMouseEnter/onMouseLeave from the
|
||||
* BUBBLING mouseover/mouseout pair — dispatching raw mouseenter/leave (they
|
||||
* do not bubble) never reaches React's handlers.
|
||||
*/
|
||||
/** Pointer hover. React 18 synthesizes onMouseEnter/onMouseLeave from the
|
||||
* BUBBLING mouseover/mouseout pair — dispatching raw mouseenter/leave (they do not bubble) never reaches React's handlers. */
|
||||
export async function hover(el: Element): Promise<void> {
|
||||
await act(async () => {
|
||||
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
|
||||
|
||||
@@ -232,8 +232,7 @@ describe('makeContentFetcher — the per-session targeted read', () => {
|
||||
test('a hostile or shapeless declared face leaves the slot unset (issue #42)', () => {
|
||||
// The traced proxy can throw on the property READ itself, or serve a
|
||||
// null / primitive / verb-less / hostile-verb face — the declared
|
||||
// callback contains all of it, the slot stays unset, and the fetcher
|
||||
// degrades instead of taking the view down.
|
||||
// callback contains all of it, the slot stays unset, and the fetcher degrades instead of taking the view down.
|
||||
const remotes = [
|
||||
{ get session() { throw new Error('traced read failed') } },
|
||||
{ session: null },
|
||||
@@ -259,8 +258,7 @@ describe('makeContentFetcher — the per-session targeted read', () => {
|
||||
|
||||
test('the declared inject tolerates a shapeless remote service', () => {
|
||||
const ctx = new TestClientCtx()
|
||||
// Both names exist but the facade bears no session namespace: the
|
||||
// callback fires and leaves the slot unset, never throwing.
|
||||
// Both names exist but the facade bears no session namespace: the callback fires and leaves the slot unset, never throwing.
|
||||
ctx.setService('remote', {})
|
||||
ctx.setService('remote.session', {})
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Dictionaries (src/client/i18n.ts): both locales carry exactly the same
|
||||
// keys, every value is a non-empty string, and interpolation placeholders
|
||||
// agree between locales for every parametrized key.
|
||||
// keys, every value is a non-empty string, and interpolation placeholders agree between locales for every parametrized key.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -54,8 +54,7 @@ describe('ContextIcon', () => {
|
||||
// complement mask that each eroding shift is painted through.
|
||||
assert.equal(queryAll<SVGPathElement>(m.container, 'path').length, 10)
|
||||
// The inset itself: a white flood minus one black shift per axis
|
||||
// direction, 17 units each — the source's 98-unit bars less the harness
|
||||
// weight's 64 makes for two 17s.
|
||||
// direction, 17 units each — the source's 98-unit bars less the harness weight's 64 makes for two 17s.
|
||||
const [hole, thin] = queryAll<SVGMaskElement>(m.container, 'mask')
|
||||
assert.match(hole.id, /-hole$/)
|
||||
assert.match(thin.id, /-thin$/)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Client entry (src/client/index.ts): the plugin's apply() wiring asserted
|
||||
// through the faithful harness-context seams — dictionaries, slots, the
|
||||
// /context trigger source, and the deferred configForms inject — plus real
|
||||
// renders of the registered components.
|
||||
// /context trigger source, and the deferred configForms inject — plus real renders of the registered components.
|
||||
|
||||
import { createElement as h, type ReactElement } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
@@ -113,8 +113,7 @@ describe('watchInsightPage', () => {
|
||||
})
|
||||
|
||||
test('a slots face returning no disposer is tolerated across flips and dispose', () => {
|
||||
// A minimal face: inject/register land nothing unwindable (the harness
|
||||
// contract allows either to return no handle).
|
||||
// A minimal face: inject/register land nothing unwindable (the harness contract allows either to return no handle).
|
||||
const bare = {
|
||||
slots: {
|
||||
inject: (_name: string, cb: () => unknown) => { cb(); return undefined },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Plugin self-metadata (src/client/meta.ts): the build-time define fallbacks
|
||||
// and the define-armed variants. Each variant sets the globals BEFORE its own
|
||||
// dynamic import (vi.resetModules clears the module cache so the constants
|
||||
// are re-evaluated), then restores the globals.
|
||||
// dynamic import (vi.resetModules clears the module cache so the constants are re-evaluated), then restores the globals.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { afterEach, describe, test, vi } from 'vitest'
|
||||
|
||||
@@ -117,8 +117,7 @@ describe('the price store', () => {
|
||||
assert.deepEqual(getModelPricesSnap(), { book: null, failed: false })
|
||||
const seen: ModelPricesSnap[] = []
|
||||
const un = subscribeModelPrices(() => seen.push(getModelPricesSnap()))
|
||||
// A second subscription in the same tick finds the fetch already in
|
||||
// flight and never re-kicks.
|
||||
// A second subscription in the same tick finds the fetch already in flight and never re-kicks.
|
||||
const un2 = subscribeModelPrices(() => {})
|
||||
await settle()
|
||||
assert.equal(calls, 1)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// The Context Dashboard's data layer (src/client/overview.ts): the hostile-
|
||||
// snapshot row join, filters, sorts, aggregations, and relative time — every
|
||||
// guard branch with hostile fixtures.
|
||||
// snapshot row join, filters, sorts, aggregations, and relative time — every guard branch with hostile fixtures.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test, vi } from 'vitest'
|
||||
@@ -149,8 +148,7 @@ describe('rowsOfSnapshot', () => {
|
||||
const rows = rowsOfSnapshot(snap, { archivedSessionIds: ['b', 7, null] })
|
||||
assert.ok(rows !== null)
|
||||
assert.deepEqual(rows.map(r => r.id), ['a', 'c'], 'archived ids drop; non-string entries archive nothing')
|
||||
// Fail-open shapes: an absent seat, a bare record, a malformed set, and a
|
||||
// throwing accessor all keep the full list.
|
||||
// Fail-open shapes: an absent seat, a bare record, a malformed set, and a throwing accessor all keep the full list.
|
||||
assert.equal(rowsOfSnapshot(snap)?.length, 3)
|
||||
assert.equal(rowsOfSnapshot(snap, null)?.length, 3)
|
||||
assert.equal(rowsOfSnapshot(snap, 7)?.length, 3)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Placement gating (src/client/placement.ts): the mounts follow the
|
||||
// `defaultPlacement` preference — both, the tab only, or the sidebar only —
|
||||
// a flip disposes exactly what the new value drops, and the watcher's own
|
||||
// disposer unwinds everything and detaches the store.
|
||||
// a flip disposes exactly what the new value drops, and the watcher's own disposer unwinds everything and detaches the store.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -32,8 +32,7 @@ describe('resolveIcon', () => {
|
||||
})
|
||||
|
||||
test('the module-level exports resolve against the pinned generation', () => {
|
||||
// The devDep carries the supported vocabulary, so these resolved at
|
||||
// module load.
|
||||
// The devDep carries the supported vocabulary, so these resolved at module load.
|
||||
for (const icon of [IconBranch, IconPlus, IconCheck, IconCopy, IconClose, IconSettings, IconChevronDown]) {
|
||||
assert.equal(typeof icon, 'function', 'every seam resolves to a component')
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// revealScroll (src/client/revealScroll.ts) — the jump's landing reveal,
|
||||
// driven against real jsdom DOM: the nearest ancestor that ACTUALLY scrolls
|
||||
// (overflowing style alone is not enough) takes the rect-delta scroll, the
|
||||
// walk stops at body/null, hostile chains never throw.
|
||||
// (overflowing style alone is not enough) takes the rect-delta scroll, the walk stops at body/null, hostile chains never throw.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -339,8 +339,7 @@ describe('timelineOf', () => {
|
||||
|
||||
test('a slim head carrying the (empty) collections passes through by reference, markers intact', () => {
|
||||
// The host's head builder emits the four collections as empty lists, so
|
||||
// the split generation rides the cheap pass-through path like any
|
||||
// well-formed value — the markers/counts survive untouched.
|
||||
// the split generation rides the cheap pass-through path like any well-formed value — the markers/counts survive untouched.
|
||||
const head = {
|
||||
ok: true,
|
||||
current,
|
||||
|
||||
@@ -5,10 +5,8 @@ import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { createContextSettings, type SettingsScopeLike } from '../../src/client/settings'
|
||||
|
||||
/**
|
||||
* A faithful in-memory settings form (the harness configForms.get contract:
|
||||
* getSnapshot/subscribe/set), not a mock of plugin code.
|
||||
*/
|
||||
/** A faithful in-memory settings form (the harness configForms.get contract:
|
||||
* getSnapshot/subscribe/set), not a mock of plugin code. */
|
||||
class TestSettingsScope implements SettingsScopeLike {
|
||||
private snapshot: { status: string; value: unknown; writable: boolean }
|
||||
private readonly listeners = new Set<() => void>()
|
||||
@@ -145,8 +143,7 @@ describe('set', () => {
|
||||
|
||||
test('a rejected placement write degrades to all when the scope carries no valid value', async () => {
|
||||
// Fail open: the unpersisted echo must not keep hiding an entry until
|
||||
// the next reload — with nothing valid in the scope's truth, fall back
|
||||
// to `all`.
|
||||
// the next reload — with nothing valid in the scope's truth, fall back to `all`.
|
||||
const s = createContextSettings()
|
||||
const scope = new TestSettingsScope({ status: 'ready', value: {}, writable: true })
|
||||
s.attach(scope)
|
||||
@@ -158,8 +155,7 @@ describe('set', () => {
|
||||
})
|
||||
|
||||
test('a rejected placement write rolls back to the scope\'s valid truth', async () => {
|
||||
// The scope truth itself is a valid placement: the rollback restores it,
|
||||
// no forced degrade.
|
||||
// The scope truth itself is a valid placement: the rollback restores it, no forced degrade.
|
||||
const s = createContextSettings()
|
||||
const scope = new TestSettingsScope({ status: 'ready', value: { defaultPlacement: 'sidebar' }, writable: true })
|
||||
s.attach(scope)
|
||||
@@ -171,8 +167,7 @@ describe('set', () => {
|
||||
|
||||
test('a rejected insights-entry write degrades to show when the scope carries no valid value', async () => {
|
||||
// Fail open: the unpersisted echo must not keep the panel's entry hidden
|
||||
// until the next reload — with nothing valid in the scope's truth, fall
|
||||
// back to `show`.
|
||||
// until the next reload — with nothing valid in the scope's truth, fall back to `show`.
|
||||
const s = createContextSettings()
|
||||
const scope = new TestSettingsScope({ status: 'ready', value: {}, writable: true })
|
||||
s.attach(scope)
|
||||
@@ -277,8 +272,7 @@ describe('attach', () => {
|
||||
})
|
||||
|
||||
test('an invalid scope placement degrades to all instead of keeping the current one', () => {
|
||||
// Fail open on the read path too: a value the plugin cannot understand
|
||||
// must not strand a previously chosen placement.
|
||||
// Fail open on the read path too: a value the plugin cannot understand must not strand a previously chosen placement.
|
||||
const s = createContextSettings()
|
||||
s.set('defaultPlacement', 'tab')
|
||||
assert.equal(s.defaultPlacement(), 'tab')
|
||||
@@ -288,8 +282,7 @@ describe('attach', () => {
|
||||
})
|
||||
|
||||
test('an invalid scope insights entry degrades to show instead of keeping the current one', () => {
|
||||
// Fail open on the read path too: a config problem must not leave the
|
||||
// panel's entry hidden.
|
||||
// Fail open on the read path too: a config problem must not leave the panel's entry hidden.
|
||||
const s = createContextSettings()
|
||||
s.set('insightsEntry', 'hide')
|
||||
assert.equal(s.insightsEntry(), 'hide')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// openPluginSettings (src/client/settingsJump.ts): the guarded DOM jump to
|
||||
// the plugin's preferences on the Plugins main panel — panel entry first,
|
||||
// the bundle card's open control after the page settles — plus the hostile
|
||||
// degradations (missing chrome, throwing elements, a card that never lists).
|
||||
// jsdom supplies the real document.
|
||||
// degradations (missing chrome, throwing elements, a card that never lists). jsdom supplies the real document.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { afterEach, describe, test } from 'vitest'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// The right Sidebar's Context tab (src/client/sidebar.ts): the OPTIONAL
|
||||
// three-stage registration — tab type into `sidebarRightTabs`, body into the
|
||||
// keyed `sidebar.right.pane.tab` seat, chip title into
|
||||
// `sidebar.right.pane.tab.title` — and its silent degrade on every harness
|
||||
// keyed `sidebar.right.pane.tab` seat, chip title into `sidebar.right.pane.tab.title` — and its silent degrade on every harness
|
||||
// that serves no right Sidebar (the older supported lines).
|
||||
|
||||
import { createElement as h, type ReactElement } from 'react'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// The timeline source (src/client/timelineSource.ts): the detail payload
|
||||
// narrow, the Connection-RPC fetcher, the per-session detail store machine
|
||||
// (debounce / single-flight / latest-wins / refold / retry / backoff), and
|
||||
// the useTimelineSource hook merging both wire generations into the value
|
||||
// the cards render.
|
||||
// the useTimelineSource hook merging both wire generations into the value the cards render.
|
||||
|
||||
import { act, createElement as h, type ReactElement } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
@@ -735,8 +734,7 @@ describe('useTimelineSource', () => {
|
||||
return { body: { ok: true, value: detail(2, { head: slimHead(2) }) } }
|
||||
})
|
||||
const m = await mount(h(SourceProbe, { sessionId: 's1', value: undefined }))
|
||||
// The first paint waits on the cold read — then the read's own slim head
|
||||
// stands in for the never-pushed projection value.
|
||||
// The first paint waits on the cold read — then the read's own slim head stands in for the never-pushed projection value.
|
||||
assert.equal(probeRead(m.container, 'state'), 'loading')
|
||||
await until(() => probeRead(m.container, 'state') === 'ready', 'the cold read never landed')
|
||||
assert.equal(probeRead(m.container, 'model'), 'm', 'the read head renders')
|
||||
@@ -763,8 +761,7 @@ describe('useTimelineSource', () => {
|
||||
const m = await mount(h(PushProbe, null))
|
||||
await until(() => probeRead(m.container, 'state') === 'ready', 'the cold read never landed')
|
||||
assert.equal(probeRead(m.container, 'model'), 'cold-m')
|
||||
// The pushed head outruns the cold read's revision: same per-session
|
||||
// store, one trailing refetch, the pushed value renders.
|
||||
// The pushed head outruns the cold read's revision: same per-session store, one trailing refetch, the pushed value renders.
|
||||
servedRev = 2
|
||||
pushed = slimHead(2, { model: 'pushed-m' })
|
||||
await m.update(h(PushProbe, null))
|
||||
@@ -853,8 +850,7 @@ describe('useTimelineSource', () => {
|
||||
assert.equal(detailStoreOf('s1').getSnapshot().detail?.rev, 3)
|
||||
served = 4
|
||||
await m.update(h(SourceProbe, { sessionId: 's1', value: slimHead(4) }))
|
||||
// The probe's rev cell reads the HEAD's marker (4 as pushed); the store's
|
||||
// served detail proves the refetch.
|
||||
// The probe's rev cell reads the HEAD's marker (4 as pushed); the store's served detail proves the refetch.
|
||||
assert.equal(probeRead(m.container, 'rev'), '4')
|
||||
await until(() => detailStoreOf('s1').getSnapshot().detail?.rev === 4, 'the refetch never landed')
|
||||
await m.unmount()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// viewFocus (src/client/viewFocus.ts) — the chat→Context jump relay, the
|
||||
// sidebar-tab opener, and the conversation-tab activation, driven against a
|
||||
// real jsdom tab bar.
|
||||
// sidebar-tab opener, and the conversation-tab activation, driven against a real jsdom tab bar.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -42,8 +41,7 @@ describe('context focus subscription', () => {
|
||||
test('a listener unsubscribing another mid-notify skips it (later records do not re-run it)', () => {
|
||||
let late = 0
|
||||
let offLate: () => void = () => {}
|
||||
// Insertion order matters: the remover runs first and detaches the victim
|
||||
// before Set iteration reaches it.
|
||||
// Insertion order matters: the remover runs first and detaches the victim before Set iteration reaches it.
|
||||
const offRemover = subscribeContextFocus(() => { offLate() })
|
||||
offLate = subscribeContextFocus(() => { late++ })
|
||||
requestContextFocus('sv-mid', 1)
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
// data-plugin style tags, platform-module requires) only exists after tsdown
|
||||
// runs. Skipped cleanly when lib/ is absent (run `pnpm run build` first);
|
||||
// the release workflow builds before `pnpm test`, so it always runs there.
|
||||
//
|
||||
// Everything here runs against the REAL bundle in its own JSDOM with REAL
|
||||
// React and the REAL ui-primitives — only the harness services
|
||||
// (locale/slots/effects) are in-memory implementations of their documented
|
||||
// contracts.
|
||||
// (locale/slots/effects) are in-memory implementations of their documented contracts.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
@@ -48,8 +46,7 @@ describe.skipIf(staging.artifactsMissing())('bundle smoke — the built lib/clie
|
||||
// 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).
|
||||
// shell's role here (their real rendering is covered by the vitest jsdom lane).
|
||||
const inert = (name: string) => {
|
||||
const C = (): null => null
|
||||
;(C as { displayName?: string }).displayName = name
|
||||
|
||||
@@ -6,15 +6,11 @@
|
||||
// on the cordis release that line vendors, the tag's own settings-namespace
|
||||
// pattern, its durable-event vocabulary, its platform module table, and its
|
||||
// client seam sources (slots, seats, image/history faces, markdown chrome).
|
||||
// A failing probe names the SEAM — the connection point to re-fit or
|
||||
// refactor — not just "it broke somewhere".
|
||||
//
|
||||
// A failing probe names the SEAM — the connection point to re-fit or refactor — not just "it broke somewhere".
|
||||
// Preconditions (skipped cleanly when absent; the release workflow fetches
|
||||
// the baseline tags before `pnpm test`, so it always runs there):
|
||||
// - a dsh checkout with the baseline tags (env DSH_REPO, default
|
||||
// ~/dev/deepseek-harness),
|
||||
// - the built plugin (`pnpm run build` first — the matrix exercises the
|
||||
// BUILT artifacts, lib/index.js + lib/client.js).
|
||||
// - a dsh checkout with the baseline tags (env DSH_REPO, default ~/dev/deepseek-harness),
|
||||
// - the built plugin (`pnpm run build` first — the matrix exercises the BUILT artifacts, lib/index.js + lib/client.js).
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { beforeAll, describe, test } from 'vitest'
|
||||
@@ -28,8 +24,7 @@ if (reasons.length > 0) {
|
||||
|
||||
// The always-runnable part (no checkout needed): every specifier the built
|
||||
// bundle requires at runtime must be seeded by EACH baseline's platform
|
||||
// module table — a require the shell cannot answer is a guaranteed boot
|
||||
// crash on that generation.
|
||||
// module table — a require the shell cannot answer is a guaranteed boot crash on that generation.
|
||||
describe('compat matrix — the per-baseline fold vocabularies', () => {
|
||||
test('their union covers every event family the fold switches on', () => {
|
||||
const union = new Set(BASELINES.flatMap(baseline => baseline.foldEventTypes))
|
||||
@@ -54,8 +49,7 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base
|
||||
describe.each(BASELINES)('$id (tag $tag, cordis $cordis)', (baseline) => {
|
||||
let report: staging.DriverReport
|
||||
// 60s: the driver's own spawn budget. A cold stage dir (fresh clone,
|
||||
// cache miss, dependency re-pin) runs a real npm install inside the hook,
|
||||
// which the 10s default ceiling cannot cover.
|
||||
// cache miss, dependency re-pin) runs a real npm install inside the hook, which the 10s default ceiling cannot cover.
|
||||
beforeAll(() => {
|
||||
report = staging.runDriver(baseline)
|
||||
}, 60_000)
|
||||
@@ -87,8 +81,7 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base
|
||||
test('host: hostile gateway usage keeps the served view schema-valid (issue #44)', () => {
|
||||
// Negative uncached input (cached_tokens > prompt_tokens), fractional
|
||||
// counts, string buckets, and a hostile NaN must all survive the REAL
|
||||
// registry's strict wire parse — billed from sanitized buckets, never
|
||||
// frozen.
|
||||
// registry's strict wire parse — billed from sanitized buckets, never frozen.
|
||||
assert.equal(report.hostileSnapshotOk, true)
|
||||
assert.equal(report.hostilePrompt, 161)
|
||||
assert.equal(report.hostileCacheRead, 151)
|
||||
@@ -120,8 +113,7 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base
|
||||
// The balance route resolves the provider's settings row off the
|
||||
// Config-form `describe()` projection (the row matched by its own
|
||||
// apiKeyEnv-bearing section shape); the retired `get(ns)` section read
|
||||
// is asserted absent, so a moved seam names itself instead of silently
|
||||
// hiding the capsule.
|
||||
// is asserted absent, so a moved seam names itself instead of silently hiding the capsule.
|
||||
const balance = baseline.balance
|
||||
assert.equal(
|
||||
staging.dshHasString(baseline.tag, balance.settingsGetNeedle, balance.settingsFile),
|
||||
@@ -153,8 +145,7 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base
|
||||
// transport it rides: the Plugins page's `plugins.bundle.config` +
|
||||
// `configForms` on every supported line. Asserted BOTH ways (like the
|
||||
// sidebar seam) so a moved seam cannot read as "unsupported here" — a
|
||||
// slot the generation does not declare simply never receives the
|
||||
// registration.
|
||||
// slot the generation does not declare simply never receives the registration.
|
||||
const settings = baseline.settings
|
||||
assert.equal(
|
||||
settings.cardSlotFiles.some(pattern =>
|
||||
@@ -214,8 +205,7 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base
|
||||
|
||||
test('client: the session-jump seam of this generation (issue #90)', () => {
|
||||
// The view owner's navigation verb (the sidebar row click's own), with
|
||||
// the signature this line spells; and the retired sessions-service
|
||||
// `open(id)` selection verb, probed staying gone.
|
||||
// the signature this line spells; and the retired sessions-service `open(id)` selection verb, probed staying gone.
|
||||
const nav = baseline.client.sessionNav
|
||||
assert.equal(
|
||||
staging.dshHasString(baseline.tag, nav.workspaceNeedle, nav.workspaceFile),
|
||||
|
||||
+11
-27
@@ -1,12 +1,9 @@
|
||||
// Staging + probe helpers for the compat matrix (matrix.spec.ts): stage the
|
||||
// REAL harness sources at a baseline tag, boot the plugin's BUILT host entry
|
||||
// on the tag's real registry + vendored cordis, and read the tag's client
|
||||
// seams straight from its sources.
|
||||
//
|
||||
// on the tag's real registry + vendored cordis, and read the tag's client seams straight from its sources.
|
||||
// The registry probe runs as a small spawned driver INSIDE the staging dir,
|
||||
// so the bare `@deepseek-ai/cordis` import resolves to the cordis release
|
||||
// that harness line vendors (not this repo's) — one process per baseline,
|
||||
// reported as one JSON line.
|
||||
// that harness line vendors (not this repo's) — one process per baseline, reported as one JSON line.
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
@@ -106,12 +103,9 @@ export interface DriverReport {
|
||||
poisonedSnapshotThrows: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The staged host driver: runs INSIDE the baseline's staging dir, applies the
|
||||
/** The staged host driver: runs INSIDE the baseline's staging dir, applies the
|
||||
* plugin's real host entry into the tag's real registry, drives a canonical
|
||||
* log through the registry's own snapshot / checkpoint / restore paths, and
|
||||
* reports as one JSON line.
|
||||
*/
|
||||
* log through the registry's own snapshot / checkpoint / restore paths, and reports as one JSON line. */
|
||||
function driverSource(pluginUrl: string, developerMessages: boolean): string {
|
||||
return `
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
@@ -188,8 +182,7 @@ if (${developerMessages}) {
|
||||
// A mis-accounting gateway's figures (negative uncached input from
|
||||
// cached_tokens > prompt_tokens, fractional counts, string buckets, a NaN a
|
||||
// hostile direct-drive could supply) ride the durable log legally; the
|
||||
// plugin's served values must stay schema-valid through the registry's own
|
||||
// strict parse on every later delivery.
|
||||
// plugin's served values must stay schema-valid through the registry's own strict parse on every later delivery.
|
||||
const hostile = [
|
||||
ev(12, 'step/start'),
|
||||
ev(13, 'assistant/message', { turn: 1, step: 2, message: { content: [{ type: 'text', text: 'h1' }] }, usage: { inputTokens: -80, cacheReadTokens: 150.7, cacheWriteTokens: '10', outputTokens: 22.4 } }),
|
||||
@@ -294,26 +287,20 @@ export function bundleRequires(): string[] {
|
||||
return [...new Set([...source.matchAll(/require\("([^"]+)"\)/g)].map(m => m[1]))]
|
||||
}
|
||||
|
||||
/**
|
||||
* The slot registrations the client half mounts on EVERY baseline (identical
|
||||
/** The slot registrations the client half mounts on EVERY baseline (identical
|
||||
* spellings in each generation's sources). The preferences card seat and the
|
||||
* settings transport are asserted separately, per generation — the matrix
|
||||
* spec pins them to the baseline's `settings` seam (`plugins.bundle.config` +
|
||||
* `configForms` on every supported line).
|
||||
*/
|
||||
* spec pins them to the baseline's `settings` seam (`plugins.bundle.config` + `configForms` on every supported line). */
|
||||
export const SLOT_SEAMS = [
|
||||
'conversation.view',
|
||||
'conversation.chat.assistant-actions',
|
||||
'conversation.input.overlay',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* The ui-primitives icon seams the client bundle renders: every supported
|
||||
/** The ui-primitives icon seams the client bundle renders: every supported
|
||||
* line ships the `…OutlineRegular`/`…OutlineMedium` vocabulary, and a missing
|
||||
* name is `undefined` at the browser's runtime — React error #130.
|
||||
* `src/client/primitives.ts` resolves each by name; every one must exist per
|
||||
* tag.
|
||||
*/
|
||||
* `src/client/primitives.ts` resolves each by name; every one must exist per tag. */
|
||||
export const ICON_SEAMS = [
|
||||
'IconBranchOutlineRegular',
|
||||
'IconPlusOutlineRegular',
|
||||
@@ -326,13 +313,10 @@ export const ICON_SEAMS = [
|
||||
'IconSearchOutlineRegular',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* The event families the host fold switches on (src/host/fold.ts) — the UNION
|
||||
/** The event families the host fold switches on (src/host/fold.ts) — the UNION
|
||||
* over every supported generation. The per-baseline probe asserts the
|
||||
* baseline's own `foldEventTypes` subset, and a matrix test asserts this
|
||||
* union equals the baselines' union — a fold case added without a baseline
|
||||
* list fails loudly instead of going unprobed.
|
||||
*/
|
||||
* union equals the baselines' union — a fold case added without a baseline list fails loudly instead of going unprobed. */
|
||||
export const FOLD_EVENT_TYPES = [
|
||||
'request/header', 'request/context', 'step/start', 'step/end',
|
||||
'user/message', 'developer/message', 'tool/call', 'tool/result', 'assistant/message',
|
||||
|
||||
@@ -20,12 +20,9 @@ const specFile = path.normalize(fileURLToPath(import.meta.url))
|
||||
* registry); reading it yields the wrapped underlying service instance. */
|
||||
const CORDIS_ORIGINAL = Symbol.for('cordis.original')
|
||||
|
||||
/**
|
||||
* A tracked tools service shaped like dsh's ToolRuntime: a real Service
|
||||
/** A tracked tools service shaped like dsh's ToolRuntime: a real Service
|
||||
* subclass whose `register` is a PROTOTYPE method — the shape that makes
|
||||
* cordis hand out a fresh traced proxy (closing over the reader's context)
|
||||
* on every `ctx.tools` property read.
|
||||
*/
|
||||
* cordis hand out a fresh traced proxy (closing over the reader's context) on every `ctx.tools` property read. */
|
||||
class TrackedTools extends Service {
|
||||
layers = { global: { tools: new Map() } }
|
||||
constructor(ctx: Context) { super(ctx, 'tools') }
|
||||
@@ -211,8 +208,7 @@ describe('createToolAttribution', () => {
|
||||
const first = createToolAttribution(app)
|
||||
void first
|
||||
// The second hook incarnation (a reload) peels the first wrapper back to
|
||||
// the original: a registration records into the NEW hook only, and the
|
||||
// original register still runs exactly once.
|
||||
// the original: a registration records into the NEW hook only, and the original register still runs exactly once.
|
||||
const second = createToolAttribution(app)
|
||||
await app.plugin({
|
||||
name: 'provider-b',
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
// route hits no-op), the deferred inject's service gating, the per-session
|
||||
// skip ladder (malformed, invisible, live, already fully served), the
|
||||
// cold-read → coldSnapshot fold path, per-session failure isolation with
|
||||
// the migration-refusal accounting (debug detail, one summary info), and
|
||||
// abort-on-dispose.
|
||||
// the migration-refusal accounting (debug detail, one summary info), and abort-on-dispose.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -48,8 +47,7 @@ interface LogLine {
|
||||
/** Capture the pass's log lines through the logger service's exporter sink. */
|
||||
function captureLogs(ctx: Context): LogLine[] {
|
||||
const lines: LogLine[] = []
|
||||
// Lift every source's threshold to debug: the default exporter drops
|
||||
// warn/debug records before they reach the sink.
|
||||
// Lift every source's threshold to debug: the default exporter drops warn/debug records before they reach the sink.
|
||||
ctx.logger.exporter({
|
||||
levels: { root: 3, 'dsh-context': 3 },
|
||||
export(message) {
|
||||
@@ -59,12 +57,10 @@ function captureLogs(ctx: Context): LogLine[] {
|
||||
return lines
|
||||
}
|
||||
|
||||
/**
|
||||
* One format-refusal face of the persistence seam. The format edge throws
|
||||
/** One format-refusal face of the persistence seam. The format edge throws
|
||||
* `SessionFormatUnsupportedMigrationError`, but the seam translates it — with
|
||||
* the "source vN artifact remains unchanged (raw log: …)" suffix — into
|
||||
* `SessionFormatUnsupportedError` before the error escapes (issue #75): the
|
||||
* pass must classify BOTH faces as the same refusal.
|
||||
* `SessionFormatUnsupportedError` before the error escapes (issue #75): the pass must classify BOTH faces as the same refusal.
|
||||
*/
|
||||
function unsupportedFormatError(name: string): Error {
|
||||
const error = new Error(
|
||||
@@ -613,8 +609,7 @@ describe('watchActivityBackfill', () => {
|
||||
ctx.provide('connection', connectionOf(route))
|
||||
const dispose = watchActivityBackfill(ctx)
|
||||
await trigger(route)
|
||||
// The request landed while only the route half existed; the faces land
|
||||
// now and the pass must still run.
|
||||
// The request landed while only the route half existed; the faces land now and the pass must still run.
|
||||
ctx.provide('sessionQuery', { listSessions: async () => state.listed })
|
||||
ctx.provide('sessionProjectionCache', {
|
||||
cachedSnapshot: () => undefined,
|
||||
|
||||
@@ -19,11 +19,9 @@ interface CtxSpec {
|
||||
credentials?: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal host ctx double with cordis inject semantics: the callback runs
|
||||
/** A minimal host ctx double with cordis inject semantics: the callback runs
|
||||
* once its dependency list completes (here: immediately when the services
|
||||
* map has them, never otherwise), and its returned disposer is collected.
|
||||
*/
|
||||
* map has them, never otherwise), and its returned disposer is collected. */
|
||||
function ctxOf(spec: CtxSpec): { ctx: Context; captured: { path?: string; fetch?: RouteFetch }; disposers: (() => void)[] } {
|
||||
const captured: { path?: string; fetch?: RouteFetch } = {}
|
||||
const disposers: (() => void)[] = []
|
||||
|
||||
@@ -70,8 +70,7 @@ describe('makeColdReadGate', () => {
|
||||
ran++
|
||||
return 'b'
|
||||
})
|
||||
// Let the first read actually START (its check passed), then let pressure
|
||||
// rise while the second read waits behind it.
|
||||
// Let the first read actually START (its check passed), then let pressure rise while the second read waits behind it.
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
assert.ok(started, 'the front read started under the low reading')
|
||||
reading = HIGH
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
// generation (issues #26/#8 — the tab stuck "Reading the session log…"), a
|
||||
// state that fails the projection cache's plain-JSON write gate (issues
|
||||
// #5-#7, #27-#30 — session creation/titles broke host-wide), a stale-row
|
||||
// restore that must seed through stateSchema, and an init that must tolerate
|
||||
// the registry's header argument.
|
||||
//
|
||||
// restore that must seed through stateSchema, and an init that must tolerate the registry's header argument.
|
||||
// The real-code complement — the ACTUAL dsh registry sources per tag — runs
|
||||
// in the `compat` vitest project (tests/compat/matrix.spec.ts).
|
||||
|
||||
@@ -95,8 +93,7 @@ for (const [index, baseline] of BASELINES.entries()) {
|
||||
|
||||
test('the split generation (slim head) rides the same registry contract', () => {
|
||||
// The slim head is the wire value when the detail channel is live
|
||||
// (host/detail.ts): the registry drives, caches, restores, and gates it
|
||||
// identically — only the served value differs.
|
||||
// (host/detail.ts): the registry drives, caches, restores, and gates it identically — only the served value differs.
|
||||
const driver = new RegistryDriver(baseline)
|
||||
driver.register(createContextTimelineDefinition({}, () => true))
|
||||
driver.register(createContextHeadersDefinition())
|
||||
@@ -120,8 +117,7 @@ for (const [index, baseline] of BASELINES.entries()) {
|
||||
assert.equal(timeline.counts?.steps, 2, 'the counters travel on the head')
|
||||
assert.equal(timeline.nodes.length, 0, 'the collections stay off the wire')
|
||||
assert.equal(timeline.requests.length, 0)
|
||||
// The cache contract is unchanged: plain-JSON rows, and a restore that
|
||||
// refolds to the live cut.
|
||||
// The cache contract is unchanged: plain-JSON rows, and a restore that refolds to the live cut.
|
||||
assert.ok(driver.checkpointJson(session) !== undefined)
|
||||
const cold = driver.restore({}, log, 0, session.header)
|
||||
assert.deepEqual(cold.values, driver.snapshot(session).values)
|
||||
@@ -193,8 +189,7 @@ for (const [index, baseline] of BASELINES.entries()) {
|
||||
// carries a raw nonconforming gateway figure, while the durable log
|
||||
// holds every committed step. The bump makes the stale row unusable, so
|
||||
// the cold read refolds from init over the FULL log — the steps the
|
||||
// frozen feed never showed come back, billed with sanitized figures and
|
||||
// passing the real wire schema on the way out.
|
||||
// frozen feed never showed come back, billed with sanitized figures and passing the real wire schema on the way out.
|
||||
const log = [
|
||||
...fullLog(),
|
||||
assistantMessage(13, { turn: 1, step: 2, usage: { inputTokens: -80, cacheReadTokens: 150.7, outputTokens: 22.4 } }),
|
||||
@@ -223,8 +218,7 @@ for (const [index, baseline] of BASELINES.entries()) {
|
||||
const { driver, session } = bootSession(index, log)
|
||||
assert.equal(driver.viewCheckpoint(stale).contextTimeline, undefined)
|
||||
// The cold read refolds from init over the full durable log; a schema
|
||||
// violation anywhere would throw right here (restore parses each wire
|
||||
// value through the unit's real viewSchema).
|
||||
// violation anywhere would throw right here (restore parses each wire value through the unit's real viewSchema).
|
||||
const restored = driver.restore(stale, log, 0, session.header)
|
||||
const timeline = restored.values.contextTimeline as { requests: { seq: number; prompt?: number; cacheRead?: number; output?: number }[] }
|
||||
// Every step is tracked again — the hostile ones included…
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// A per-baseline driver over the session-projection registry's DOCUMENTED
|
||||
// semantics — the rules the REAL dsh registries enforce, mirrored from the
|
||||
// source at each baseline tag so the always-on suite pins the plugin's units
|
||||
// against every supported registry generation:
|
||||
//
|
||||
// source at each baseline tag so the always-on suite pins the plugin's units against every supported registry generation:
|
||||
// dsh-v0.1.7-rc.2 packages/session/session-projection/src/index.ts
|
||||
// dsh-v0.2.0-rc.2 packages/session/session-projection/src/index.ts
|
||||
//
|
||||
// The real-code matrix (tests/compat/matrix.spec.ts) runs the ACTUAL registry
|
||||
// sources per tag; this driver exists so `pnpm test` alone still detects a
|
||||
// definition that drifts from any supported generation's rules (a missing
|
||||
@@ -61,9 +58,7 @@ export interface CheckpointRow {
|
||||
|
||||
export type Checkpoint = Record<string, CheckpointRow>
|
||||
|
||||
/**
|
||||
* The registry's register-time rules: a non-negative safe integer
|
||||
* `stateVersion`, and a shared key must agree on the version.
|
||||
/** The registry's register-time rules: a non-negative safe integer `stateVersion`, and a shared key must agree on the version.
|
||||
*/
|
||||
export class RegistryViolationError extends Error {}
|
||||
|
||||
@@ -89,8 +84,7 @@ export class RegistryDriver {
|
||||
const def: ErasedDefinition = {
|
||||
key: definition.key,
|
||||
stateSchema: definition.stateSchema,
|
||||
// The registry calls init(header, inheritedEventCount); the plugin's
|
||||
// zero-argument init observes neither argument.
|
||||
// The registry calls init(header, inheritedEventCount); the plugin's zero-argument init observes neither argument.
|
||||
init: (header, inheritedEventCount) => definition.init(header, inheritedEventCount),
|
||||
apply: (state, event) => definition.apply(state, event),
|
||||
wire,
|
||||
@@ -171,13 +165,10 @@ export class RegistryDriver {
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* The projection cache's WRITE precondition (session-projection-cache
|
||||
/** The projection cache's WRITE precondition (session-projection-cache
|
||||
* src/index.ts): every row must be losslessly JSON-serializable or the
|
||||
* whole write fails with a TypeError — one undefined-valued property in
|
||||
* one unit's state once broke every session on the host (issues #5-#7,
|
||||
* #27-#30). Returns the detached JSON image.
|
||||
*/
|
||||
* one unit's state once broke every session on the host (issues #5-#7, #27-#30). Returns the detached JSON image. */
|
||||
checkpointJson(session: SessionLike): Record<string, unknown> | undefined {
|
||||
return snapshotJson(this.checkpoint(session)) as Record<string, unknown> | undefined
|
||||
}
|
||||
@@ -211,11 +202,9 @@ export class RegistryDriver {
|
||||
return floor === undefined ? undefined : Math.max(floor - 1, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cold read: seed usable rows through stateSchema.parse, refold the rest.
|
||||
/** Cold read: seed usable rows through stateSchema.parse, refold the rest.
|
||||
* `inheritedEventCount` is the fork-inherited prefix length the registry
|
||||
* hands to unit initialization (0 for a plain session).
|
||||
*/
|
||||
* hands to unit initialization (0 for a plain session). */
|
||||
restore(checkpoint: Checkpoint, events: { seq: number }[], baseSeq: number, header: unknown, inheritedEventCount = 0): ProjectionSnapshot {
|
||||
const endSeq = events.at(-1)?.seq ?? baseSeq - 1
|
||||
const values: Record<string, unknown> = {}
|
||||
|
||||
@@ -107,8 +107,7 @@ describe('volatileField', () => {
|
||||
|
||||
test('returns the field unchanged on a schemastery without the modifier (the older lines)', () => {
|
||||
const plain = z.union(['a', 'b']).default('a')
|
||||
// A schema-shaped value with no `volatile` member at all — the shape the
|
||||
// older lines' schemastery presents.
|
||||
// A schema-shaped value with no `volatile` member at all — the shape the older lines' schemastery presents.
|
||||
const legacy = { meta: plain.meta } as typeof plain
|
||||
assert.equal(volatileField(legacy), legacy)
|
||||
assert.equal(legacy.meta?.volatile, undefined)
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// on the connection/sessions faces (load-order independent), the gate's live
|
||||
// flip on unload, and the route's typed outcomes — the detail payload off the
|
||||
// live fold state, the typed `null` for a gone session or an absent unit, and
|
||||
// the failure envelope for bad payloads and hostile reads (the route must
|
||||
// never throw into the transport).
|
||||
// the failure envelope for bad payloads and hostile reads (the route must never throw into the transport).
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -25,11 +24,9 @@ interface CtxSpec {
|
||||
stateOf?: (session: unknown, key: string) => unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal host ctx double with cordis inject semantics: the callback runs
|
||||
/** A minimal host ctx double with cordis inject semantics: the callback runs
|
||||
* once its dependency list completes (here: immediately when the services
|
||||
* map has them, never otherwise), and its returned disposer is collected.
|
||||
*/
|
||||
* map has them, never otherwise), and its returned disposer is collected. */
|
||||
function ctxOf(spec: CtxSpec): { ctx: Context; captured: { path?: string; fetch?: RouteFetch }; disposers: (() => void)[] } {
|
||||
const captured: { path?: string; fetch?: RouteFetch } = {}
|
||||
const disposers: (() => void)[] = []
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// The baseline gate's fallback units (src/host/fallback.ts), driven over the
|
||||
// REAL session-projection registry: the gate's value arrives through the
|
||||
// ordinary snapshot cut no matter what the log carries, both registry
|
||||
// contract generations read their own shape off the definition, and cached
|
||||
// rows from either direction seed harmlessly.
|
||||
// contract generations read their own shape off the definition, and cached rows from either direction seed harmlessly.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
+1
-2
@@ -1,3 +1,2 @@
|
||||
// Typing for the throwaway plain-JS fixture (the runtime module mirrors this
|
||||
// exactly); keeps tsc happy without allowJs.
|
||||
// Typing for the throwaway plain-JS fixture (the runtime module mirrors this exactly); keeps tsc happy without allowJs.
|
||||
export function registerBlind(tools: { register(def?: unknown): () => void }): () => void
|
||||
+1
-2
@@ -1,3 +1,2 @@
|
||||
// Typing for the throwaway plain-JS fixture (the runtime module mirrors this
|
||||
// exactly); keeps tsc happy without allowJs.
|
||||
// Typing for the throwaway plain-JS fixture (the runtime module mirrors this exactly); keeps tsc happy without allowJs.
|
||||
export function registerTool(tools: { register(def?: unknown): () => void }): void
|
||||
@@ -216,8 +216,7 @@ describe('user/message injection records', () => {
|
||||
|
||||
test('a hostile form (truthy non-string) degrades to the default inject form', () => {
|
||||
// The harness validates a source's `kind` but not its `form`: a durable
|
||||
// number must not ride the record into the strict wire/state schemas
|
||||
// (the permanent per-session freeze class).
|
||||
// number must not ride the record into the strict wire/state schemas (the permanent per-session freeze class).
|
||||
const { state } = driveTimeline([
|
||||
userMessage(1, text('note'), { kind: 'plugin', plugin: 'dsh-x', form: 123 } as never),
|
||||
])
|
||||
@@ -431,8 +430,7 @@ describe('hostile provider usage (issue #44: stats must survive nonconforming fi
|
||||
// against the unit's strict integer schemas with no containment: one raw
|
||||
// nonconforming figure in the state fails the gate on EVERY later delivery
|
||||
// and permanently freezes the session's projection feed. The fold therefore
|
||||
// sanitizes each bucket before it touches the state, and the records stay
|
||||
// complete (billed, not dropped).
|
||||
// sanitizes each bucket before it touches the state, and the records stay complete (billed, not dropped).
|
||||
test('fractional buckets round to integers', () => {
|
||||
const { state } = driveTimeline([
|
||||
assistantMessage(1, { usage: { inputTokens: 10.4, cacheReadTokens: 20.6, outputTokens: 3.5 } }),
|
||||
@@ -776,8 +774,7 @@ describe('hostile events', () => {
|
||||
test('null and primitive content blocks price and render as nothing', () => {
|
||||
const blocks = [null, 7, 'x', { type: 'text', text: 'hello' }] as never
|
||||
// The tool/result has NO preceding tool/call — the unpaired lookup must
|
||||
// not stamp an `undefined`-valued node.tool (it would fail every
|
||||
// projection-cache write for the session).
|
||||
// not stamp an `undefined`-valued node.tool (it would fail every projection-cache write for the session).
|
||||
const { state } = driveTimeline([
|
||||
userMessage(1, blocks),
|
||||
toolResult(2, { callId: 'c1', content: blocks }),
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// book at tool/result off the armed call's arguments and the result's
|
||||
// presentation meta; Code-Mode sub-dispatches buffer under their run_code
|
||||
// root and flush at its result with parent/program stamps; the log trims to
|
||||
// maxFileOps with a coverage floor; and the detail builder serves detached
|
||||
// copies. Driven through the real projection unit.
|
||||
// maxFileOps with a coverage floor; and the detail builder serves detached copies. Driven through the real projection unit.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -90,8 +89,7 @@ describe('the file-op log — call/result pairing', () => {
|
||||
toolResult(2, { callId: 'c1', content: [{ type: 'text', text: 'no' }], error: true }),
|
||||
toolCall(3, { callId: 'c2', name: 'read', arguments: JSON.stringify({ file_path: 'b.ts' }) }),
|
||||
// A hostile envelope-only mark (the retired pre-V4 spelling): the
|
||||
// admission validation forbids `data.error` without the message mark,
|
||||
// so a row carrying it alone reads as non-error.
|
||||
// admission validation forbids `data.error` without the message mark, so a row carrying it alone reads as non-error.
|
||||
{
|
||||
type: 'tool/result', seq: 4, time: 4,
|
||||
data: {
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
// driven through the real projection unit with tiny config bounds. The
|
||||
// restored-state defenses (a projection-cache restore can carry archived
|
||||
// entries without `gone` — the state schema leaves it optional) are exercised
|
||||
// by feeding hand-crafted states through def.apply, exactly what a cache
|
||||
// restore seeds.
|
||||
// by feeding hand-crafted states through def.apply, exactly what a cache restore seeds.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -79,8 +78,7 @@ describe('trimState request/event bounds', () => {
|
||||
[1, 2, 3, 4, 5].map(i => assistantMessage(i, { turn: 1, step: i })),
|
||||
{ maxKeptTurns: 10, maxRequestSteps: 3 },
|
||||
)
|
||||
// One turn-run: the turn trim cannot cut mid-turn, so the step backstop
|
||||
// slices the newest tail.
|
||||
// One turn-run: the turn trim cannot cut mid-turn, so the step backstop slices the newest tail.
|
||||
assert.deepEqual(drive.state.requests.map(r => r.seq), [3, 4, 5])
|
||||
})
|
||||
|
||||
@@ -105,8 +103,7 @@ describe('the incremental turnRuns ledger', () => {
|
||||
assistantMessage(4, { turn: 2, step: 1 }),
|
||||
assistantMessage(5, { turn: 2, step: 2 }),
|
||||
])
|
||||
// Scan semantics: the leading turn-less record forms no run; runs open at
|
||||
// turn 1 (record 2) and turn 2 (record 4).
|
||||
// Scan semantics: the leading turn-less record forms no run; runs open at turn 1 (record 2) and turn 2 (record 4).
|
||||
assert.equal(drive.state.turnRuns, 2)
|
||||
assertStatesPlainJson(drive)
|
||||
})
|
||||
@@ -166,8 +163,7 @@ describe('trimState restored-state defenses', () => {
|
||||
const def = timelineDef({ maxArchiveNodes: 2 })
|
||||
const st = createTimelineState()
|
||||
st.requests.push(req(5, 1, 1))
|
||||
// A restored/legacy row without `gone`: `gone ?? Infinity` keeps it above
|
||||
// any oldest retained request.
|
||||
// A restored/legacy row without `gone`: `gone ?? Infinity` keeps it above any oldest retained request.
|
||||
st.archived.push({ seq: 1, cat: 'user', tokens: 9 })
|
||||
const next = def.apply(st, planMode(10, { active: true }))
|
||||
assert.deepEqual(next.archived.map(n => n.seq), [1])
|
||||
|
||||
@@ -83,8 +83,7 @@ describe('spans — the step flush', () => {
|
||||
// packs no reasoning chunks (the content is redacted): the first token
|
||||
// lands on a tool-call fragment at 952. Metering (ttftMs) opens the
|
||||
// decode window at the MARKER — anchoring the wait at the token would
|
||||
// charge the 588ms the legend's own tally reports to the wait as well,
|
||||
// and the rows would double-count it past 100%.
|
||||
// charge the 588ms the legend's own tally reports to the wait as well, and the rows would double-count it past 100%.
|
||||
const { state } = driveTimeline([
|
||||
stepStart(1, { time: 0 }),
|
||||
assistantMessage(2, {
|
||||
@@ -118,8 +117,7 @@ describe('spans — the step flush', () => {
|
||||
|
||||
test('blocks stamped wholly past the message instant drop out of the tile', () => {
|
||||
// Hostile stream: both markers sit beyond the assistant message's own
|
||||
// instant — their claimed intervals assert decode after the message
|
||||
// landed, so they paint nothing.
|
||||
// instant — their claimed intervals assert decode after the message landed, so they paint nothing.
|
||||
const { state } = driveTimeline([
|
||||
stepStart(1, { time: 0 }),
|
||||
assistantMessage(2, {
|
||||
@@ -197,15 +195,13 @@ describe('spans — the step flush', () => {
|
||||
{ kind: 'tools', start: 3_000, end: 3_500 },
|
||||
{ kind: 'other', start: 3_500, end: 4_000 },
|
||||
])
|
||||
// The totals keep the TRUE per-call sums (3_500ms over 3 calls) — only the
|
||||
// strip de-overlaps.
|
||||
// The totals keep the TRUE per-call sums (3_500ms over 3 calls) — only the strip de-overlaps.
|
||||
assert.equal(state.timing?.toolsMs, 1_000 + 1_000 + 1_500)
|
||||
assert.equal(state.timing?.toolCalls, 3)
|
||||
})
|
||||
|
||||
test('spans clamp into the step window; a window wholly outside drops', () => {
|
||||
// A run whose result arrives after the step's end instant (out-of-order
|
||||
// log) paints only up to the end…
|
||||
// A run whose result arrives after the step's end instant (out-of-order log) paints only up to the end…
|
||||
const past = driveTimeline([
|
||||
stepStart(1, { time: 0 }),
|
||||
toolCallAt(2, 'c1', 3_000),
|
||||
@@ -272,8 +268,7 @@ describe('spans — the step flush', () => {
|
||||
describe('spans — retention, wire, and schema faces', () => {
|
||||
test('the collection keeps the newest tail past the 2_000-span cap', () => {
|
||||
// 41 steps × 50 spans each (a 48-marker decode run): 2_050 spans — the
|
||||
// cap must drop the oldest step whole. Fewer, denser steps keep the fold
|
||||
// cheap enough for a CI runner's 5s test timeout.
|
||||
// cap must drop the oldest step whole. Fewer, denser steps keep the fold cheap enough for a CI runner's 5s test timeout.
|
||||
const events: TimelineEvent[] = []
|
||||
for (let i = 0; i < 41; i++) {
|
||||
const start = i * 10_000
|
||||
@@ -322,8 +317,7 @@ describe('spans — retention, wire, and schema faces', () => {
|
||||
])
|
||||
for (const state of drive.states) drive.def.stateSchema.parse(structuredClone(state))
|
||||
// A row folded before the collection existed has no `spans` key: the
|
||||
// required-key schema rejects it, so the projection cache refolds it from
|
||||
// the durable log (the stateVersion 22 → 23 bump).
|
||||
// required-key schema rejects it, so the projection cache refolds it from the durable log (the stateVersion 22 → 23 bump).
|
||||
const stale = structuredClone(drive.state) as unknown as Record<string, unknown>
|
||||
delete stale.spans
|
||||
assert.throws(() => drive.def.stateSchema.parse(stale))
|
||||
@@ -337,8 +331,7 @@ describe('spans — retention, wire, and schema faces', () => {
|
||||
toolResult(4, { callId: 'c1', content: text('ok'), time: 1_200 }),
|
||||
stepEnd(5, { time: 1_400 }),
|
||||
// A step left OPEN mid-accumulation: the intermediate states carry the
|
||||
// armed accumulator through the same gate (driveTimeline pins the
|
||||
// no-absent-members rule on every one).
|
||||
// armed accumulator through the same gate (driveTimeline pins the no-absent-members rule on every one).
|
||||
stepStart(6, { time: 2_000 }),
|
||||
assistantMessage(7, { time: 3_000, stream: [chunkRec(2_100, { type: 'text-delta', text: 'y' })] }),
|
||||
])
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// buildTimelineDetail + the detailRev ledger): the slim head carries the
|
||||
// counters and the headline anchor while the detail payload serves 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.
|
||||
// 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'
|
||||
@@ -50,8 +49,7 @@ function canonicalLog(): TimelineEvent[] {
|
||||
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.
|
||||
// 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)
|
||||
})
|
||||
@@ -59,8 +57,7 @@ describe('the detailRev ledger', () => {
|
||||
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.
|
||||
// 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' }),
|
||||
@@ -97,11 +94,9 @@ describe('buildTimelineHead', () => {
|
||||
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).
|
||||
// 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).
|
||||
// …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')
|
||||
@@ -188,8 +183,7 @@ describe('buildTimelineDetail', () => {
|
||||
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.
|
||||
// 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)
|
||||
|
||||
@@ -54,8 +54,7 @@ describe('applySurface tool/result branch', () => {
|
||||
})
|
||||
|
||||
test('an unknown source callId pairs nothing (a replay window where the tool/call event is gone)', () => {
|
||||
// Note the fold assigns the lookup miss onto the node, so this state is
|
||||
// not assertPlainJson-able.
|
||||
// Note the fold assigns the lookup miss onto the node, so this state is not assertPlainJson-able.
|
||||
const { state } = driveTimeline([toolResult(1, { callId: 'ghost', content: text('ok') })])
|
||||
assert.equal(state.surface.at(-1)?.tool, undefined)
|
||||
assert.deepEqual(state.callNames, {})
|
||||
@@ -72,8 +71,7 @@ describe('applySurface tool/result branch', () => {
|
||||
|
||||
test('a result without the source callId pairs nothing and leaves callNames untouched', () => {
|
||||
// A hostile/foreign envelope: the admission validation requires the
|
||||
// source callId, so a source-less row can only arrive hostile — it folds
|
||||
// to an unnamed node instead of throwing.
|
||||
// source callId, so a source-less row can only arrive hostile — it folds to an unnamed node instead of throwing.
|
||||
const ev: TimelineEvent = {
|
||||
type: 'tool/result', seq: 2, time: at(),
|
||||
data: { message: { role: 'tool', toolCallId: 'keep', content: text('ok') } },
|
||||
@@ -97,8 +95,7 @@ describe('applySurface tool/result branch', () => {
|
||||
test('the retired envelope-only error mark no longer flags the node', () => {
|
||||
// The pre-V4 spelling (an event-level `data.error` with no message mark)
|
||||
// is unreachable through the supported harnesses' admission validation;
|
||||
// a hostile replay carrying it degrades to an unflagged node, never a
|
||||
// throw.
|
||||
// a hostile replay carrying it degrades to an unflagged node, never a throw.
|
||||
const ev: TimelineEvent = {
|
||||
type: 'tool/result', seq: 2, time: at(),
|
||||
data: {
|
||||
@@ -287,8 +284,7 @@ describe('surfaceOp variants', () => {
|
||||
})
|
||||
|
||||
test('restored state: claim without pendingShadowEventSeq skips the rewrite', () => {
|
||||
// A checkpoint restore may carry the claim fields partially — def.apply
|
||||
// accepts any TimelineState.
|
||||
// A checkpoint restore may carry the claim fields partially — def.apply accepts any TimelineState.
|
||||
const def = timelineDef({})
|
||||
const base = driveTimeline([userMessage(1, text(big))]).state
|
||||
const restored: TimelineState = { ...base, pendingShadowedSeqs: [1] }
|
||||
|
||||
@@ -213,8 +213,7 @@ describe('timing — tool call durations', () => {
|
||||
describe('timing — the generation split (reasoning / text / tool args)', () => {
|
||||
test('the embedded stream tiles the generation window into the three buckets', () => {
|
||||
// step start 0, first marker 200 (10ms BEFORE the first token 210 — the
|
||||
// wait/generation boundary opens there), blocks reasoning→text→tool-call,
|
||||
// message at 2000.
|
||||
// wait/generation boundary opens there), blocks reasoning→text→tool-call, message at 2000.
|
||||
const stream = [
|
||||
chunkRec(200, { type: 'block-start', index: 0, blockType: 'reasoning' }),
|
||||
{ type: 'reasoning-chunks', time0: 210, index: 0, dt: [], texts: ['think'] },
|
||||
@@ -266,8 +265,7 @@ describe('timing — the generation split (reasoning / text / tool args)', () =>
|
||||
test('an unstamped call carries no split (its model time is unattributed wholesale)', () => {
|
||||
// Block markers but no token anywhere in the stream: the call prices no
|
||||
// generation time, so its spans must not reappear as generation the
|
||||
// caller never charged — and its blocks count nothing either (the tally
|
||||
// rides the same gate).
|
||||
// caller never charged — and its blocks count nothing either (the tally rides the same gate).
|
||||
const { state } = driveTimeline([
|
||||
stepStart(1, { time: 0 }),
|
||||
assistantMessage(2, {
|
||||
@@ -410,8 +408,7 @@ describe('timing — served wire view', () => {
|
||||
toolCall(5, { callId: 'c1', name: 'bash' }),
|
||||
toolResult(6, { callId: 'c1', content: text('ok') }),
|
||||
stepEnd(7, { time: 30_000 }),
|
||||
// A step left OPEN with a stamped slot: the final state carries
|
||||
// stepStart.{time,firstToken} for the gate.
|
||||
// A step left OPEN with a stamped slot: the final state carries stepStart.{time,firstToken} for the gate.
|
||||
stepStart(8, { time: 40_000 }),
|
||||
assistantAttempt(9, { stream: [chunkRec(40_100, { type: 'text-delta', text: 'x' })] })])
|
||||
// The 0.1.1+ contract validates persisted state through stateSchema.
|
||||
@@ -519,8 +516,7 @@ describe('timing — the step\'s active time (user waits priced out)', () => {
|
||||
})
|
||||
|
||||
test('a requestless step after a real one stamps nothing onto its record', () => {
|
||||
// The seq guard: the open step armed no back-pointer, so the earlier
|
||||
// step's record keeps its own stamp untouched.
|
||||
// The seq guard: the open step armed no back-pointer, so the earlier step's record keeps its own stamp untouched.
|
||||
const { state } = driveTimeline([
|
||||
...step(1, 0, 1_000, { tokenMs: 200 }),
|
||||
stepStart(4, { time: 10_000 }),
|
||||
|
||||
@@ -16,8 +16,7 @@ const bucket = (uncached: number, cacheRead: number, cacheWrite: number, output:
|
||||
describe('buildTimelineView unknown-model shape', () => {
|
||||
test('optional scalars stay ABSENT keys until known — never undefined-valued (issue #29)', () => {
|
||||
// A fold over events that name no model/provider/capacity: the served
|
||||
// view must not carry `undefined`-valued properties, which fail the
|
||||
// harness's lossless-JSON push pipeline whole.
|
||||
// view must not carry `undefined`-valued properties, which fail the harness's lossless-JSON push pipeline whole.
|
||||
const { view } = driveTimeline([
|
||||
userMessage(1, [{ type: 'text', text: 'aaaa' }]),
|
||||
assistantMessage(2, { turn: 1, step: 1 }),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Unit tests for the contextHeaders projection unit (src/host/headers.ts) —
|
||||
// the request-header EPOCH METADATA behind the timeline's envelope figures.
|
||||
// The unit is pure init/apply/view: each case drives real event envelopes
|
||||
// through the real definition (no harness plumbing).
|
||||
// The unit is pure init/apply/view: each case drives real event envelopes through the real definition (no harness plumbing).
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -66,8 +65,7 @@ describe('createContextHeadersDefinition', () => {
|
||||
assert.equal(def.apply(state, headerEvent(seq, raw) as never), state, `header ${String(raw)} is not an epoch`)
|
||||
}
|
||||
// A data-less envelope is unreachable from the harness (append validates
|
||||
// the payload) but must degrade to "not an epoch" all the same: this unit
|
||||
// has no try/catch around its fold.
|
||||
// the payload) but must degrade to "not an epoch" all the same: this unit has no try/catch around its fold.
|
||||
assert.equal(def.apply(state, { type: 'request/header', seq: 5, time: 5000 } as never), state)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Fold drivers for the host specs: run event envelopes through the REAL
|
||||
// projection units the plugin registers (no harness plumbing — the units are
|
||||
// pure init/apply/view), pinning the state's plain-JSON rule on every
|
||||
// intermediate state (see assertNoAbsentMembers).
|
||||
// pure init/apply/view), pinning the state's plain-JSON rule on every intermediate state (see assertNoAbsentMembers).
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
@@ -12,14 +11,12 @@ import type { ContextHeaders, ContextTimeline } from '../../../src/shared/types'
|
||||
import { createContextTimelineDefinition } from '../../../src/host/timeline'
|
||||
import { createContextHeadersDefinition } from '../../../src/host/headers'
|
||||
|
||||
/**
|
||||
* The fold's declared contract takes the core `SessionEvent` union; the log
|
||||
/** The fold's declared contract takes the core `SessionEvent` union; the log
|
||||
* also carries declaration-merged plugin events (compaction/*), so the fold
|
||||
* widens to TimelineEvent (src/host/fold.ts). These widened faces mirror the
|
||||
* supported registry contract (src/host/compat.ts: stateSchema + required
|
||||
* `wire`, zero-arg `init` — see ProjectionDefinition) and perform the ONE
|
||||
* documented cast inside the helper, keeping every spec call site clean.
|
||||
*/
|
||||
* documented cast inside the helper, keeping every spec call site clean. */
|
||||
export interface TimelineDefLike {
|
||||
key: string
|
||||
/** The persisted-state gate the projection cache's cold path parses rows with. */
|
||||
@@ -48,14 +45,12 @@ export function headersDef(): HeadersDefLike {
|
||||
return createContextHeadersDefinition() as unknown as HeadersDefLike
|
||||
}
|
||||
|
||||
/**
|
||||
* Lossless-JSON probe and detach, inlined with the dsh `snapshotJsonValue`
|
||||
/** Lossless-JSON probe and detach, inlined with the dsh `snapshotJsonValue`
|
||||
* semantics (the export lives in `@deepseek-ai/dsh-util-values`, and the
|
||||
* test fixtures must track no single dsh face). Returns undefined when the
|
||||
* value is not losslessly JSON-serializable: an undefined/function/symbol
|
||||
* member, a non-finite number, a non-plain object, or a cycle. Shared with the
|
||||
* compat matrix's registry driver (tests/host/compat/registryDriver.ts).
|
||||
*/
|
||||
* compat matrix's registry driver (tests/host/compat/registryDriver.ts). */
|
||||
export function snapshotJson(value: unknown, ancestors: Set<object> = new Set()): unknown {
|
||||
switch (typeof value) {
|
||||
case 'string': case 'boolean': return value
|
||||
@@ -91,11 +86,9 @@ export function snapshotJson(value: unknown, ancestors: Set<object> = new Set())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The projection-cache precondition: every state the fold produces must be
|
||||
/** The projection-cache precondition: every state the fold produces must be
|
||||
* losslessly JSON-serializable (a single undefined property fails EVERY cache
|
||||
* write for the session — see TimelineState). Returns the detached copy.
|
||||
*/
|
||||
* write for the session — see TimelineState). Returns the detached copy. */
|
||||
export function assertPlainJson<T>(state: T): T {
|
||||
const copy = snapshotJson(state)
|
||||
assert.ok(copy !== undefined, 'fold state must be losslessly JSON-serializable')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Integration tests for the Host-half plugin module (src/host/index.ts)
|
||||
// against the REAL cordis registry, session store, and session-projection
|
||||
// registry — the dsh-canonical harness: real envelopes appended to a real
|
||||
// session, folded by the registered units, read back through the snapshot
|
||||
// cut and the change feed.
|
||||
// session, folded by the registered units, read back through the snapshot cut and the change feed.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { join } from 'node:path'
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// fold reconciles — the embedded assistant stream's first token, the
|
||||
// replacement op's endpoints, and the raw chunk token test. Hostile shapes
|
||||
// are pinned beside the happy paths: a malformed record must read as
|
||||
// "nothing here", never throw (the projection registry drives the fold with
|
||||
// no error boundary of its own).
|
||||
// "nothing here", never throw (the projection registry drives the fold with no error boundary of its own).
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -43,8 +42,7 @@ describe('decodeTallyOfStream', () => {
|
||||
})
|
||||
|
||||
test('a later block closes the previous one even when its own kind is unknown', () => {
|
||||
// The unknown marker still ends the reasoning span; its own interval is
|
||||
// lost and it counts nothing.
|
||||
// The unknown marker still ends the reasoning span; its own interval is lost and it counts nothing.
|
||||
assert.deepEqual(decodeTallyOfStream([chunk(1000, 'reasoning'), chunk(1400, 'image')], 1900), {
|
||||
spans: { reasoning: 400, text: 0, toolarg: 0 },
|
||||
blocks: { reasoning: 1, text: 0, toolarg: 0 },
|
||||
@@ -83,8 +81,7 @@ describe('decodeTallyOfStream', () => {
|
||||
})
|
||||
|
||||
test('a backwards marker clamps to a zero span instead of a negative one', () => {
|
||||
// The text block's interval is negative → 0; the reasoning block is last,
|
||||
// so it owns the tail to endTime.
|
||||
// The text block's interval is negative → 0; the reasoning block is last, so it owns the tail to endTime.
|
||||
assert.deepEqual(decodeTallyOfStream([chunk(1000, 'text'), chunk(900, 'reasoning')], 950), {
|
||||
spans: { reasoning: 50, text: 0, toolarg: 0 },
|
||||
blocks: { reasoning: 1, text: 1, toolarg: 0 },
|
||||
|
||||
@@ -49,8 +49,7 @@ describe('estimateMessage', () => {
|
||||
|
||||
test('a retired wrapper block prices as generic JSON (the meter parity branch)', () => {
|
||||
// The supported log generation carries no wrapper blocks; a hostile one
|
||||
// prices through the same structural-JSON arm the meter applies to every
|
||||
// unknown block.
|
||||
// prices through the same structural-JSON arm the meter applies to every unknown block.
|
||||
const block = { type: 'tool-result', content: [{ type: 'text', text: 'ok' }] }
|
||||
assert.equal(
|
||||
estimateMessage({ content: [block] }),
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
// harness's own skill catalog (observeSession → cwd + preset; a live agent's
|
||||
// scoped registry wins; a cold session rides a scope lease; every gap falls
|
||||
// back to the global layers), the typed `null` for every unusable face (the
|
||||
// route must never throw into the transport), the per-entry narrowing, and
|
||||
// the serve bound.
|
||||
// route must never throw into the transport), the per-entry narrowing, and the serve bound.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -21,11 +20,8 @@ interface CtxSpec {
|
||||
agentPresets?: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal host ctx double with cordis inject semantics (mirrors
|
||||
* balance.spec.ts's): the callback runs once its dependency list completes,
|
||||
* and `ctx.get` serves the request-time reads.
|
||||
*/
|
||||
/** A minimal host ctx double with cordis inject semantics (mirrors
|
||||
* balance.spec.ts's): the callback runs once its dependency list completes, and `ctx.get` serves the request-time reads. */
|
||||
function ctxOf(spec: CtxSpec): { ctx: Context; captured: { path?: string; fetch?: RouteFetch } } {
|
||||
const captured: { path?: string; fetch?: RouteFetch } = {}
|
||||
const services = new Map<string, unknown>()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Unit tests for host/toolSources.ts — best-effort tool-to-plugin attribution:
|
||||
// MCP server recovery from the `mcp__` name prefix, and the pinned first-party
|
||||
// package map. (A harness-logged `plugin` field, when one appears, passes
|
||||
// through headers.ts ahead of everything here.)
|
||||
// package map. (A harness-logged `plugin` field, when one appears, passes through headers.ts ahead of everything here.)
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -29,8 +28,7 @@ describe('mcpServerOf', () => {
|
||||
|
||||
test('hash-appended overlong names resolve the server that survived truncation', () => {
|
||||
const hex64 = '0123456789abcdef'.repeat(4)
|
||||
// Overlong names get '_<64-hex>' appended; the last __ separator survives
|
||||
// unless the truncation cut into it.
|
||||
// Overlong names get '_<64-hex>' appended; the last __ separator survives unless the truncation cut into it.
|
||||
assert.equal(mcpServerOf(`mcp__github__tool_${hex64}`), 'github')
|
||||
assert.equal(mcpServerOf(`mcp__github__tool_${'z'.repeat(64)}`), 'github')
|
||||
})
|
||||
|
||||
+18
-42
@@ -1,19 +1,14 @@
|
||||
// The runtime harness-version probe (src/host/version.ts): the running-module
|
||||
// anchor and its own-closure guard, the home-mirror anchor, both probe paths
|
||||
// (manifest subpath / entry ascend), and every degradation arm — the gate's
|
||||
// fail-open promise rests on ALL of these returning undefined instead of
|
||||
// throwing.
|
||||
//
|
||||
// Fixture roots:
|
||||
// - tests/host/fixtures/version/homes/* — committed homes whose packages
|
||||
// fail-open promise rests on ALL of these returning undefined instead of throwing.
|
||||
// Fixture roots: - tests/host/fixtures/version/homes/* — committed homes whose packages
|
||||
// RESOLVE at the fixture level (no walk-up), shared with index.spec.ts.
|
||||
// - a per-run tree under os.tmpdir() — the FAILURE cases and the
|
||||
// running-tree fixtures. They must sit outside the repository: a failing
|
||||
// probe inside the repo tree would walk up into the repo's own node_modules
|
||||
// and answer with the pinned devDependencies instead of failing, and a
|
||||
// running-tree witness inside the repo would read as this package's own
|
||||
// dependency closure.
|
||||
//
|
||||
// running-tree witness inside the repo would read as this package's own dependency closure.
|
||||
// Walk-up cleanliness alone is not hermetic, though: CJS resolution consults
|
||||
// NODE_PATH after the walk-up fails, and a pnpm-managed environment (a
|
||||
// `pnpm` binary from a sibling checkout) injects that checkout's virtual
|
||||
@@ -24,11 +19,9 @@
|
||||
// package but carries no version, so both probe paths (manifest subpath via
|
||||
// the exports gate, entry ascend via the versionless own manifest) yield
|
||||
// undefined and the degradation arms stay machine-independent.
|
||||
//
|
||||
// The running anchor is injected as a URL resolver, and the plugin root as a
|
||||
// path, so both the trusted and the own-closure arms are exercised hermetically
|
||||
// (the repo's real defaults are covered by the last case of the degradation
|
||||
// block).
|
||||
// (the repo's real defaults are covered by the last case of the degradation block).
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
@@ -53,11 +46,8 @@ function ctxWithHome(homePath: unknown): Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* The running anchor's URL resolver for one fixture home under `root`: real
|
||||
* Node resolution from that home's profiles dir, as an installed plugin's own
|
||||
* module pipeline would perform it.
|
||||
*/
|
||||
/** The running anchor's URL resolver for one fixture home under `root`: real
|
||||
* Node resolution from that home's profiles dir, as an installed plugin's own module pipeline would perform it. */
|
||||
function runningResolver(home: string, root: string = HOMES): (specifier: string) => string {
|
||||
const anchor = join(root, home, 'profiles', 'running-probe.cjs')
|
||||
return specifier => pathToFileURL(createRequire(anchor).resolve(specifier)).href
|
||||
@@ -66,10 +56,8 @@ function runningResolver(home: string, root: string = HOMES): (specifier: string
|
||||
/** A running anchor that never answers, forcing the home anchor. */
|
||||
const NO_RUNNING = (): string => { throw new Error('no running module tree') }
|
||||
|
||||
/**
|
||||
* A plugin root that contains no fixture tree — the trusted-witness case.
|
||||
* It sits in os.tmpdir() beside (never above) the scratch root.
|
||||
*/
|
||||
/** A plugin root that contains no fixture tree — the trusted-witness case.
|
||||
* It sits in os.tmpdir() beside (never above) the scratch root. */
|
||||
const ELSEWHERE = join(tmpdir(), 'dsh-context-plugin-root')
|
||||
|
||||
/** A running anchor that always answers with one fixed URL. */
|
||||
@@ -77,12 +65,9 @@ const fixedUrl = (url: string) => (): string => url
|
||||
|
||||
let scratch = ''
|
||||
|
||||
/**
|
||||
* Write one scratch probe home. `manifest` is the literal package.json
|
||||
/** Write one scratch probe home. `manifest` is the literal package.json
|
||||
* content of the given package; `entry` (when set) becomes the file the
|
||||
* manifest's `exports['.']` points at, with `decoy` an optional mismatched
|
||||
* manifest placed beside it.
|
||||
*/
|
||||
* manifest's `exports['.']` points at, with `decoy` an optional mismatched manifest placed beside it. */
|
||||
function writeScratchHome(
|
||||
home: string,
|
||||
packageName: string,
|
||||
@@ -105,13 +90,10 @@ function writeScratchHome(
|
||||
|
||||
const scratchResolver = (home: string): (...segments: string[]) => string => resolverIn(join(scratch, home))
|
||||
|
||||
/**
|
||||
* A probe-package shadow: the package RESOLVES at the fixture level (so the
|
||||
/** A probe-package shadow: the package RESOLVES at the fixture level (so the
|
||||
* walk-up — and the NODE_PATH ambient installs behind it — are never
|
||||
* consulted) but answers NO version. The manifest subpath probe dies on the
|
||||
* exports gate (no './package.json' export), and the entry ascend reads the
|
||||
* package's own versionless manifest to undefined.
|
||||
*/
|
||||
* exports gate (no './package.json' export), and the entry ascend reads the package's own versionless manifest to undefined. */
|
||||
function shadow(home: string, packageName: string): void {
|
||||
writeScratchHome(home, packageName,
|
||||
JSON.stringify({ name: packageName, exports: { '.': './lib/index.js' } }),
|
||||
@@ -123,8 +105,7 @@ beforeAll(() => {
|
||||
// The CLI-row-absent fall-through lives in scratch: inside the repo tree a
|
||||
// missing CLI package could resolve to an ambient install above the repo
|
||||
// (e.g. a global copy under ~/node_modules), making the answer machine-
|
||||
// dependent; outside it the walk-up is provably clean. The shadows close
|
||||
// the remaining NODE_PATH route (see the header note).
|
||||
// dependent; outside it the walk-up is provably clean. The shadows close the remaining NODE_PATH route (see the header note).
|
||||
shadow('library-only', '@deepseek-ai/dsh')
|
||||
writeScratchHome('library-only', '@deepseek-ai/dsh-session-projection',
|
||||
JSON.stringify({ name: '@deepseek-ai/dsh-session-projection', version: '0.1.1-rc.2' }))
|
||||
@@ -135,8 +116,7 @@ beforeAll(() => {
|
||||
JSON.stringify({ name: '@deepseek-ai/dsh', version: '0.1.1-rc.4', exports: { '.': './a/b/index.js' } }),
|
||||
{ path: 'a/b/index.js', decoyManifest: JSON.stringify({ name: '@deepseek-ai/dsh-decoy', version: '9.9.9' }) })
|
||||
// Odd manifests sit as DECOYS on the ascend path: only the direct
|
||||
// manifest read (not a resolve) ever surfaces them, and the probe must
|
||||
// skip each to reach the owning package's real version.
|
||||
// manifest read (not a resolve) ever surfaces them, and the probe must skip each to reach the owning package's real version.
|
||||
writeScratchHome('entry-badjson', '@deepseek-ai/dsh',
|
||||
JSON.stringify({ name: '@deepseek-ai/dsh', version: '0.1.1-rc.5', exports: { '.': './a/index.js' } }),
|
||||
{ path: 'a/index.js', decoyManifest: 'not json{' })
|
||||
@@ -180,8 +160,7 @@ beforeAll(() => {
|
||||
writeScratchHome('running-supported', '@deepseek-ai/dsh-session',
|
||||
JSON.stringify({ name: '@deepseek-ai/dsh-session', version: '0.1.7-rc.2', exports: { '.': './lib/index.js' } }),
|
||||
{ path: 'lib/index.js' })
|
||||
// The just-dropped line: below the baseline since the floor moved to
|
||||
// 0.1.7-rc.2, so the gate must trip on it.
|
||||
// The just-dropped line: below the baseline since the floor moved to 0.1.7-rc.2, so the gate must trip on it.
|
||||
writeScratchHome('running-old', '@deepseek-ai/dsh-session',
|
||||
JSON.stringify({ name: '@deepseek-ai/dsh-session', version: '0.1.5-rc.1', exports: { '.': './lib/index.js' } }),
|
||||
{ path: 'lib/index.js' })
|
||||
@@ -205,23 +184,20 @@ describe('detectHarnessVersion — running anchor', () => {
|
||||
})
|
||||
|
||||
test('a below-baseline running tree still trips the gate over a newer mirror', () => {
|
||||
// The reverse direction must hold too: a genuinely old harness is reported
|
||||
// even when the mirror names something newer.
|
||||
// The reverse direction must hold too: a genuinely old harness is reported even when the mirror names something newer.
|
||||
assert.equal(detectHarnessVersion(ctxWithHome(homeResolver('future')), runningResolver('running-old', scratch), ELSEWHERE), '0.1.5-rc.1')
|
||||
})
|
||||
|
||||
test('discards a witness inside this package own closure for the home anchor', () => {
|
||||
// A `link:`-installed dev checkout resolves its own pinned devDependencies:
|
||||
// that closure is not the harness, so the home mirror answers instead. The
|
||||
// default package root (the real one, which contains the repo's
|
||||
// node_modules) performs the discard.
|
||||
// default package root (the real one, which contains the repo's node_modules) performs the discard.
|
||||
const ctx = ctxWithHome(homeResolver('old'))
|
||||
assert.equal(detectHarnessVersion(ctx, runningResolver('module-skips-cli')), '0.1.5-rc.1')
|
||||
})
|
||||
|
||||
test('never probes the CLI package from the running anchor', () => {
|
||||
// The fixture pins the CLI at 0.0.1 but the libraries at the baseline:
|
||||
// only a library answer may come back.
|
||||
// The fixture pins the CLI at 0.0.1 but the libraries at the baseline: only a library answer may come back.
|
||||
const ctx = ctxWithHome(scratchResolver('empty'))
|
||||
assert.equal(detectHarnessVersion(ctx, runningResolver('module-skips-cli'), ELSEWHERE), '0.1.7-rc.2')
|
||||
})
|
||||
|
||||
@@ -79,8 +79,7 @@ describe('DAY_KEY_RE', () => {
|
||||
|
||||
describe('startOfDayKey / endOfDayKey', () => {
|
||||
test('a picked range spans the whole local day', () => {
|
||||
// Endpoints, not a duration: a DST-short day is 23 hours long, so the
|
||||
// difference is not a fixed number of milliseconds.
|
||||
// Endpoints, not a duration: a DST-short day is 23 hours long, so the difference is not a fixed number of milliseconds.
|
||||
assert.equal(startOfDayKey('2026-09-16'), new Date(2026, 8, 16, 0, 0, 0, 0).getTime())
|
||||
assert.equal(endOfDayKey('2026-09-16'), new Date(2026, 8, 16, 23, 59, 59, 999).getTime())
|
||||
assert.equal(startOfDayKey('2026-03-08'), new Date(2026, 2, 8, 0, 0, 0, 0).getTime(), 'a DST-short day still opens on its own midnight')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// The shared token heuristics (src/shared/estimate.ts): the fixed-density
|
||||
// figures the host fold and the client boundary must agree on. The system
|
||||
// prompt is priced from a `system/message`'s content blocks (or a rendered
|
||||
// string, at the legacy-wire boundary) — with the harness token-meter's own
|
||||
// no-per-block-overhead rule.
|
||||
// string, at the legacy-wire boundary) — with the harness token-meter's own no-per-block-overhead rule.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// assembly consumed by the host fold and the client's inline-generation
|
||||
// fallback alike. Pins the whole producer matrix: the six built-ins, the
|
||||
// Anthropic-style editor, the search meta attribution (complete / truncated
|
||||
// / malformed / args-less), the read window vs the limit estimate, the
|
||||
// pattern-target fallback, and the zero-ops degradations.
|
||||
// / malformed / args-less), the read window vs the limit estimate, the pattern-target fallback, and the zero-ops degradations.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
@@ -89,8 +88,7 @@ describe('opsOfCall — one op per settled call', () => {
|
||||
argsRaw: JSON.stringify({ pattern: 'TODO' }),
|
||||
meta: grepMeta({ 'a.ts': 3, 'b.ts': 1 }),
|
||||
})
|
||||
// The searched path/pattern rows first (here the workspace-wide pattern),
|
||||
// then each hit file with its count.
|
||||
// The searched path/pattern rows first (here the workspace-wide pattern), then each hit file with its count.
|
||||
assert.deepEqual(ops.map(o => [o.path, o.hits]), [['TODO', undefined], ['a.ts', 3], ['b.ts', 1]])
|
||||
assert.equal(ops[0].pattern, true, 'the pathless target rows as the pattern')
|
||||
assert.ok(ops.slice(1).every(o => o.detail === 'TODO'), 'the pattern detail rides each hit op')
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// The version gate's arithmetic (src/shared/version.ts): parsing, the total
|
||||
// order (release > rc > beta > alpha at equal X.Y.Z), and the fail-open
|
||||
// baseline check.
|
||||
// order (release > rc > beta > alpha at equal X.Y.Z), and the fail-open baseline check.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
|
||||
Reference in new issue
Block a user