/** * The browser half, without a browser. * * `client/client.js` is a plain script that hands a factory to the platform's * module loader, so it can be exercised in Node by stubbing `window`, `react`, * and the one primitives package it imports. That is worth doing rather than * leaving the page to be eyeballed in a running app, because the failures this * file exists to catch are structural and invisible until someone opens Plugins: * * - the configuration page is registered into `plugins.bundle.config` under * the bundle's package name, which is the seat the Plugins page draws as a * section on the plugin's own card. The 0.1.5 seat (`settings.plugin.item`, * bound through `settingsScope`) does not exist in 0.1.7, so a page left in * it registers against a slot nobody declares and simply never appears; the * sibling seat `plugins.row.config` renders one click deeper instead, on a * single row's page, which is not where someone opening the card looks. * - the right-sidebar tab needs a TYPE registered before its body means * anything: the keyed body slot is dispatched under the definition's `id`. * - the field list here and the `Config` schema in index.js must name the same * settings, or a setting is silently uneditable. * * Two things are stubbed rather than real, and the tests that depend on them say * so. `SettingsFormModel` and the field controls come from * `@deepseek-ai/dsh-client-ui-primitives`, whose bundle imports CSS and is * therefore not loadable in bare Node; the stub records what this plugin hands * the framework and answers the same questions the real one does. What the * framework is trusted for — staging, save ordering, revision fencing, the * rendered chrome — is covered by the harness's own suite, not here. */ import { strict as assert } from 'node:assert' import { test } from 'node:test' import { SETTING_KEYS } from '../index.js' /** A React good enough to run one component: element, state, effect, memo. */ function makeReact() { const hooks = [] let cursor = 0 return { createElement(type, props, ...children) { const merged = { ...(props ?? {}) } if (children.length > 0) merged.children = children.length === 1 ? children[0] : children return { type, props: merged } }, useState(initial) { const at = cursor cursor += 1 if (!(at in hooks)) hooks[at] = typeof initial === 'function' ? initial() : initial return [ hooks[at], (next) => { hooks[at] = typeof next === 'function' ? next(hooks[at]) : next }, ] }, useEffect(fn) { fn() }, useMemo(fn) { return fn() }, /** Prepare for one more render, keeping hook state. */ __render() { cursor = 0 }, } } /** * Expand a function element the way React would. * * The card renders its controls as `h(Field, …)`, and those elements are only * real nodes once React calls them — so a walk that stops at `type === 'input'` * finds nothing without this. */ function expand(node) { if (node === null || typeof node !== 'object') return node return typeof node.type === 'function' ? expand(node.type(node.props)) : node } /** Depth-first search over a rendered tree, expanding function components. */ function find(node, predicate, found = []) { const current = expand(node) if (current === null || typeof current !== 'object') return found if (predicate(current)) found.push(current) const children = current.props?.children if (Array.isArray(children)) children.forEach((child) => find(child, predicate, found)) else find(children, predicate, found) return found } /** * Stand-ins for the framework's settings-form kit. * * The model records the specs and secret writers this plugin hands it, and * answers `shell`/`field`/`plan` the way the real one does; the controls render * plain nodes carrying the props they were given. */ function makePrimitives(record) { class SettingsFormModel { constructor(scope, specs, secrets = []) { this.scope = scope this.specs = specs this.secrets = secrets this.staged = new Map() record.model = this } bind(project) { this.project = project // Faithful to the framework: `bind` is not a subscription setter, it // builds the snapshot store from ONE immediate projection. A stub that // only stored the projection would let a controller read a field it has // not assigned yet — which is exactly the bug this reproduces. project() return { getSnapshot: () => project(), subscribe: () => () => {} } } shell() { return { available: this.scope.getSnapshot().status === 'ready', writable: this.scope.getSnapshot().writable, dirty: this.staged.size > 0, invalid: [...this.staged].some(([name, text]) => { if (this.secrets.some((entry) => entry.field === name)) return false return this.specs.find((entry) => entry.field === name).parse(text) === undefined }), saving: false, failed: false, } } field(name) { if (this.secrets.some((entry) => entry.field === name)) { return { text: this.staged.get(name) ?? '', overridden: false, invalid: false } } const spec = this.specs.find((entry) => entry.field === name) const text = this.staged.has(name) ? this.staged.get(name) : spec.format(this.scope.getSnapshot().value?.[name]) const write = spec.parse(text) const user = this.scope.getSnapshot().user return { text, overridden: this.staged.has(name) ? write?.kind === 'set' : Object.hasOwn(user ?? {}, name), invalid: write === undefined, } } actions() { return { edit: (name, text) => { this.staged.set(name, text) record.edits.push({ name, text }) }, resetField: (name) => this.staged.set(name, undefined), save: () => { record.saved = true }, discard: () => this.staged.clear(), } } dispose() { record.disposed = true } /** What a save would write, built from this plugin's own spec objects. */ plan() { const plan = [] for (const [name, text] of this.staged) { if (this.secrets.some((entry) => entry.field === name)) { plan.push({ field: name, run: () => this.secrets.find((s) => s.field === name).write(String(text)) }) continue } const write = this.specs.find((entry) => entry.field === name).parse(String(text)) if (write === undefined) plan.push({ field: name }) else if (write.kind === 'clear') plan.push({ field: name, op: { op: 'unset', path: [name] } }) else plan.push({ field: name, op: { op: 'set', path: [name], value: write.value } }) } return plan } } return { SettingsForm: (props) => ({ type: 'form', props }), SettingsValueField: (props) => ({ type: 'input', props }), SettingsSecretField: (props) => ({ type: 'input', props: { ...props, type: 'password' } }), SettingsFormModel, settingsNumberField: (field) => ({ field, format: (value) => (value === undefined || value === null ? '' : String(value)), parse: (text) => String(text).trim() === '' ? { kind: 'clear' } : Number.isFinite(Number(text)) ? { kind: 'set', value: Number(text) } : undefined, }), settingsTextField: (field) => ({ field, format: (value) => (value === undefined || value === null ? '' : String(value)), parse: (text) => (String(text).trim() === '' ? { kind: 'clear' } : { kind: 'set', value: text }), }), } } // Load the browser bundle once, the way the platform loader would. const loaded = [] globalThis.window = { __ModuleLoader__: { load: (entry) => loaded.push(entry) } } await import('../client/client.js') const entry = loaded[0] /** * Instantiate the bundle, mount it on a stubbed context, and expose what it * registered. * * @param {object} options `services` mounts extra optional dependencies, * `served` is what the (stubbed) describe mirror reports, and * `withConfigForms: false` models a deployment with no settings provider. */ function mount(options = {}) { const { services = {}, served = ['dsh-recall'], withConfigForms = true } = options const record = { edits: [], saved: false, disposed: false, model: undefined } const react = makeReact() const primitives = makePrimitives(record) const factory = entry.factory((name) => { if (name === 'react') return react if (name === '@deepseek-ai/dsh-client-ui-primitives') return primitives throw new Error(`unexpected import: ${name}`) }) const registered = [] const tabTypes = [] const whileServedCalls = [] const slots = { inject: (name, fn) => fn(), register: (meta, component) => { registered.push({ meta, component }) return () => {} }, } const form = { calls: [], snapshot: { status: 'ready', value: {}, base: {}, user: {}, writable: true, revision: 9 }, getSnapshot() { return this.snapshot }, subscribe: () => () => {}, async mutate(ops, revision) { this.calls.push({ ops, revision }) return true }, } const describe = { getSnapshot: () => ({ status: 'ready', view: { writable: true, namespaces: served.map((ns) => ({ ns, secrets: [{ path: ['embeddingApiKey'], set: true }] })), }, }), subscribe: () => () => {}, } const configForms = { requested: undefined, get(id) { this.requested = id return form }, describe: () => describe, whileServed: (namespaces, register) => { whileServedCalls.push({ namespaces }) const live = namespaces.some((ns) => served.includes(ns)) const off = live ? register(new Set(served)) : undefined return () => off?.() }, } const sidebarRightTabs = services.sidebarRightTabs ?? { register: (definition) => { tabTypes.push(definition) return () => {} }, } const ctx = { slots, effect: (fn) => { const dispose = fn() if (typeof dispose === 'function') record.disposables = [...(record.disposables ?? []), dispose] }, inject: (deps, callback) => { const available = { ...services, sidebarRightTabs, ...(withConfigForms ? { configForms } : {}), } const missing = deps.filter((dep) => available[dep] === undefined) if (missing.length > 0) return callback({ ...ctx, ...Object.fromEntries(deps.map((dep) => [dep, available[dep]])) }) }, } factory.apply(ctx) const byName = new Map(registered.map((item) => [item.meta.name, item])) /** Render one registered slot with the props its occupant would receive. */ const renderSlot = (name, props = {}) => { const item = byName.get(name) assert.ok(item !== undefined, `nothing registered for ${name}`) react.__render() const element = item.component(props) if (element === null) return null return element.type(element.props) } /** * The settings page's element plus the props it would receive. * * `hooks` is resolved into `use` here exactly as the slot machinery * does, so the component under test sees the props a real render would give it. */ const card = (view) => { const item = byName.get('plugins.bundle.config') assert.ok(item !== undefined, 'no settings page was registered') const props = { ...item.meta.inject(), view } delete props.hooks props.useRecallSettings = (selector) => selector(record.model.project()) return { item, props, element: item.component(props) } } return { react, module: factory, record, form, registered, tabTypes, whileServedCalls, renderSlot, card, configForms, } } /** The rendered page's control nodes, keyed by their HTML id. */ function controlsOf(element) { const form = expand(element) const controls = new Map() for (const node of find(form, (candidate) => candidate.type === 'input')) controls.set(node.props.id, node) return controls } test('the bundle registers under the loader with the plugin id', () => { assert.equal(loaded.length, 1) assert.equal(entry.id, 'dsh-recall') assert.equal(typeof entry.factory, 'function') }) test('the client module claims the slot registry in its inject list', () => { const { module } = mount() assert.equal(module.name, 'dsh-recall') assert.deepEqual(module.inject, ['slots']) }) test('the configuration page claims the seat the plugin card draws', () => { const { registered } = mount() const page = registered.find((item) => item.meta.name === 'plugins.bundle.config') assert.ok(page !== undefined, 'the page must register into plugins.bundle.config') // Keyed by the BUNDLE's package name, which is what the Plugins page // dispatches this seat with (`entryKey: pkg.name`). The sibling seat, // `plugins.row.config`, would be keyed `#` and would render // one click deeper, on the row's own page. assert.equal(page.meta.key, 'dsh-recall') assert.equal(typeof page.meta.inject, 'function') }) test('the page edits the settings entry the Host keys this plugin under', () => { const { configForms, whileServedCalls } = mount() assert.equal(configForms.requested, 'dsh-recall') assert.deepEqual(whileServedCalls, [{ namespaces: ['dsh-recall'] }]) }) test('no page is registered for a namespace the Host does not serve', () => { // A control that writes nowhere is worse than no control: the page would open // onto fields nothing would accept. const { registered } = mount({ served: ['some-other-plugin'] }) assert.equal( registered.some((item) => item.meta.name === 'plugins.bundle.config'), false, ) }) test('the chip and the sidebar tab survive a deployment with no settings provider', () => { // The page is reached through `ctx.inject`, not a top-level `inject`, so a // profile without a settings provider loses the page and keeps the rest. const { registered, tabTypes } = mount({ withConfigForms: false }) assert.deepEqual(registered.map((item) => item.meta.name).sort(), [ 'conversation.session.header.utilities', 'sidebar.right.pane.tab', ]) assert.equal(tabTypes.length, 1, 'the tab type is independent of settings') }) test('the header chip is an additive list entry carrying its own id', () => { // The header's far-right corner is a SINGLE cell, and the product already // occupies it with its right-sidebar expand control — the client catalog // reports that key as `replaceRisk: "shadows-shipped-ui"`. A second // registration at the same priority is a hard error inside the registry, // thrown while the OTHER plugin's `slots.inject()` callback runs: the // renderer catches it, stops that controller for the rest of the fiber's // life, and rethrows it uncaught. So the chip belongs in a list cell, where // a unique id makes it additive. const { registered } = mount() const chip = registered.find((item) => item.meta.name === 'conversation.session.header.utilities') assert.ok(chip !== undefined) assert.equal(chip.meta.id, 'dsh-recall', 'a list cell is addressed by its id') assert.equal(typeof chip.meta.order, 'number', 'the order places the cell within the list') assert.equal(chip.meta.key, undefined, 'a list cell takes an id, not a key') assert.deepEqual( registered.filter((item) => item.meta.name === 'conversation.session.header.corner'), [], 'the corner is a single cell the product owns; a registration there shadows shipped UI', ) }) test('the sidebar tab registers a type AND a body under the same id', () => { // The body slot is dispatched under the id of the type in force for a tab's // kind. A body with no type is a page nothing can open. const { registered, tabTypes } = mount() const body = registered.find((item) => item.meta.name === 'sidebar.right.pane.tab') assert.ok(body !== undefined) assert.equal(body.meta.key, 'dsh-recall') assert.equal(tabTypes.length, 1, 'exactly one tab type') assert.equal(tabTypes[0].id, 'dsh-recall', "the body's key must be the definition's id") assert.equal(tabTypes[0].kind, 'dsh-recall') assert.equal(tabTypes[0].priority, 'extension', 'a type from outside the product sits in that band') assert.equal(typeof tabTypes[0].title, 'function') assert.equal(tabTypes[0].title(), 'Recall') // A page type recognises no address, so the guide entry is the only way in. assert.equal(tabTypes[0].guide.length, 1) assert.equal(tabTypes[0].guide[0].title(), 'Recall') assert.match(tabTypes[0].guide[0].description(), /kept out/) }) test('the browser half contributes three seats, none of them the retired 0.1.5 one', () => { const { registered } = mount() assert.deepEqual(registered.map((item) => item.meta.name).sort(), [ 'conversation.session.header.utilities', 'plugins.bundle.config', 'sidebar.right.pane.tab', ]) }) test('the page asks for nothing to be edited before the user types', () => { const { card } = mount() const { element } = card('page') const state = find(element, (node) => node.type === 'form')[0] assert.equal(state.props.state.dirty, false) assert.equal(state.props.state.invalid, false) assert.equal(state.props.state.writable, true) assert.equal(typeof state.props.onSave, 'function') assert.equal(typeof state.props.onDiscard, 'function') assert.equal(state.props.labels.save, 'Save') }) test('the summary view is the one-liner the Plugins page falls back to', () => { const { card } = mount() // The page asks for `summary` to fill a row with no description of its own, // so it must be text, and it must describe the plugin rather than the form. const summary = card('summary').element assert.equal(typeof summary, 'string') assert.match(summary, /Local embedding endpoint/) // And the page view is the form, not that string. assert.notEqual(typeof card('page').element, 'string') }) test('every declared setting renders a control, and the API key renders as a secret', () => { const { card } = mount() const controls = controlsOf(card('page').element) assert.deepEqual( [...controls.keys()].sort(), SETTING_KEYS.map((key) => `dsh-recall-${key}`).sort(), 'every setting in the host schema must be editable here, and nothing else', ) const secret = controls.get('dsh-recall-embeddingApiKey') assert.equal(secret.props.type, 'password') assert.equal(secret.props.text, '', 'a redacted secret renders blank') // The page learns whether a key is stored, never the key itself. assert.equal(secret.props.configured, true) assert.match(secret.props.stateLabel, /stored/) }) test('the field list and the schema cannot drift apart', () => { const { record } = mount() assert.deepEqual( record.model.specs.map((spec) => spec.field).sort(), SETTING_KEYS.filter((key) => key !== 'embeddingApiKey').sort(), 'every non-secret setting needs a conversion spec', ) assert.deepEqual(record.model.secrets.map((spec) => spec.field), ['embeddingApiKey']) }) test('a list setting round-trips as comma separated text', () => { const { record } = mount() const spec = record.model.specs.find((entry) => entry.field === 'exclude') assert.equal(spec.format(['dist/', 'vendor/']), 'dist/, vendor/') assert.deepEqual(spec.parse('dist/, vendor/, tmp/'), { kind: 'set', value: ['dist/', 'vendor/', 'tmp/'] }) assert.deepEqual(spec.parse(''), { kind: 'clear' }, 'emptying the control reverts to the schema default') }) test('a boolean setting accepts the obvious spellings and refuses the rest', () => { const { record } = mount() const spec = record.model.specs.find((entry) => entry.field === 'rerankEnabled') assert.equal(spec.format(true), 'true') assert.equal(spec.format(false), 'false') assert.deepEqual(spec.parse('TRUE'), { kind: 'set', value: true }) assert.deepEqual(spec.parse('off'), { kind: 'set', value: false }) assert.deepEqual(spec.parse(''), { kind: 'clear' }) // Anything else blocks the save rather than guessing which way the user meant. assert.equal(spec.parse('maybe'), undefined) }) test('an invalid draft is reported, so the framework can block the save', () => { const { record } = mount() record.model.actions().edit('embeddingConcurrency', 'lots') assert.equal(record.model.field('embeddingConcurrency').invalid, true) assert.equal(record.model.shell().invalid, true) }) test('an edit is staged in the model, never written to the document on the spot', () => { const { record, form } = mount() record.model.actions().edit('embeddingConcurrency', '4') assert.deepEqual(record.edits, [{ name: 'embeddingConcurrency', text: '4' }]) assert.deepEqual(form.calls, [], 'only a save writes') }) test('the API key is written through the document, fenced by the current revision', async () => { const { record, form } = mount() const secret = record.model.secrets[0] assert.equal(secret.field, 'embeddingApiKey') await secret.write('sk-secret') assert.deepEqual(form.calls, [ { ops: [{ op: 'set', path: ['embeddingApiKey'], value: 'sk-secret' }], revision: 9 }, ]) }) test('a blank secret is never staged, so opening the page cannot delete the key', () => { const { record } = mount() assert.deepEqual(record.model.plan(), [], 'nothing typed means nothing to write') }) test('a typed secret is planned as a write outside the section, not as a section op', () => { const { record } = mount() record.model.actions().edit('embeddingApiKey', 'sk-secret') const plan = record.model.plan() assert.equal(plan.length, 1) assert.equal(plan[0].field, 'embeddingApiKey') assert.equal(plan[0].op, undefined, 'a secret rides its own writer, because the wire never returns it') }) test('a later revision is carried by the next write rather than the one staged from', async () => { const { record, form } = mount() form.snapshot = { ...form.snapshot, revision: 12 } await record.model.secrets[0].write('sk-newer') assert.equal(form.calls[0].revision, 12, 'every write carries the revision it was staged against') }) test('the form subscription is released with the plugin', () => { const { record } = mount() for (const dispose of record.disposables ?? []) dispose() // The controller's disposer runs the model's, so a reload leaks no listener. assert.equal(record.disposed, true) }) /** A projection value shaped like the host unit's wire view. */ function usageView(overrides = {}) { return { searches: 3, indexes: 0, failed: 0, excerpts: 9, files: 5, weak: 0, returnedBytes: 12_000, sourceBytes: 480_000, avoidedBytes: 468_000, avoidedTokens: 117_000, compression: 40, rereadTokens: 0, avoidedUsd: 0.0008, lastFocus: '.', ...overrides, } } test('the header chip renders nothing when the conversation never used it', () => { const { renderSlot } = mount() // The utilities list draws nothing for an entry that renders nothing, so // this is the state that must cost no space at all. assert.equal(renderSlot('conversation.session.header.utilities', { useProjection: () => undefined }), null) assert.equal(renderSlot('conversation.session.header.utilities', { useProjection: () => usageView({ searches: 0, excerpts: 0 }) }), null) // And a host without the projection service at all. assert.equal(renderSlot('conversation.session.header.utilities', {}), null) }) test('the header chip leads with what was kept out, not what it cost', () => { const { renderSlot } = mount() const chip = renderSlot('conversation.session.header.utilities', { useProjection: () => usageView() }) assert.ok(chip !== null) const text = String(chip.props.children) assert.match(text, /Recall 3/) assert.match(text, /40/) assert.match(text, /kept out/) // Money is deliberately absent from the chip: a fraction of a penny reads as // the tool being useless rather than as the arithmetic being honest. assert.ok(!text.includes('$'), `chip showed a price: ${text}`) }) test('the chip tooltip carries the figures the chip has no room for', () => { const { renderSlot } = mount() const chip = renderSlot('conversation.session.header.utilities', { useProjection: () => usageView({ rereadTokens: 500_000 }) }) const detail = chip.props.title assert.match(detail, /3 searches/) assert.match(detail, /9 excerpts from 5 files/) assert.match(detail, /would have been read/) assert.match(detail, /estimate/i, 'the comparison must be labelled as one') }) test('the sidebar panel explains an unused conversation instead of showing zeroes', () => { const { renderSlot } = mount() const empty = renderSlot('sidebar.right.pane.tab', { useProjection: () => usageView({ searches: 0, indexes: 0, excerpts: 0 }) }) const text = JSON.stringify(empty) assert.match(text, /has not used recall yet/) assert.ok(!/\b0 B\b/.test(text), 'zeroes would read as "this did nothing"') }) test('the sidebar panel says so when the projection is absent', () => { const { renderSlot } = mount() const missing = renderSlot('sidebar.right.pane.tab', {}) assert.match(JSON.stringify(missing), /not available for this conversation/) }) test('the sidebar panel reports the comparison and the compounding', () => { const { renderSlot } = mount() const panel = renderSlot('sidebar.right.pane.tab', { useProjection: () => usageView({ rereadTokens: 1_200_000 }) }) const text = JSON.stringify(panel) assert.match(text, /Kept out of this conversation/) assert.match(text, /Smaller than reading the files/) assert.match(text, /Re-read since/) assert.match(text, /1,200,000 tokens/) assert.match(text, /upper bound/, 'the panel must say what the figure is not') })