/** * The browser half: the two keys it binds, and the fields it stages. * * This file exists because of a bug that no host-side test could catch. Marking * the schema's fields `volatile()` makes the Host *serve* the namespace, but the * Plugins page draws only what a browser half registers — so the plugin showed no * settings card at all, and every host-side test passed. * * The wiring is therefore the thing worth testing, and the two keys are the trap: * the settings DOCUMENT is keyed by the profile entry id (`facet-policy`) while * the CARD is keyed by the package name (`dsh-facet`). Every sibling plugin * happens to name those identically, which is exactly why this drifted. A card * bound to the wrong id renders, saves, and writes nowhere. * * `client/client.js` is a `window.__ModuleLoader__` module rather than an ES * module — that is how the harness loads a browser half without a bundler — so * the test stands up the two globals it reads and drives it from there. */ import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import { test } from 'node:test' import { Config } from '../index.js' /** Load the browser half with `window` and `require` faked, as the seed would. */ async function loadClient() { const source = await readFile(new URL('../client/client.js', import.meta.url), 'utf8') /** Every spec a card staged, in construction order. */ const staged = [] const primitives = { Modal: 'Modal', SettingsForm: 'SettingsForm', SettingsValueField: 'SettingsValueField', SegmentedControl: 'SegmentedControl', /** Stands in for the real model: records the specs and answers with the value. */ SettingsFormModel: class { constructor(scope, specs) { this.scope = scope this.specs = specs staged.push(...specs) } bind(project) { this.project = project return { project } } shell() { return { available: true, writable: true, dirty: false, invalid: false, saving: false, failed: false } } field(name) { const spec = this.specs.find((candidate) => candidate.field === name) return { text: spec?.format(this.scope.value?.[name]) ?? '', overridden: false, invalid: false } } actions() { return { edit: () => {}, resetField: () => {}, save: () => {}, discard: () => {} } } dispose() {} }, } const require = (id) => { if (id === 'react') { return { createElement: (type, props, ...children) => ({ type, props, children }), // Single-pass stand-ins: the tests render a component once and read the // tree, so a hook only has to answer, not to re-render. useState: (initial) => [typeof initial === 'function' ? initial() : initial, () => {}], useSyncExternalStore: (_subscribe, getSnapshot) => getSnapshot(), } } if (id === '@deepseek-ai/dsh-client-ui-primitives') return primitives throw new Error(`client.js required an unexpected module: ${id}`) } let registered const loader = { load: (value) => (registered = value) } // eslint-disable-next-line no-new-func -- the file is a ModuleLoader script, not an ES module. new Function('window', 'require', source)({ __ModuleLoader__: loader }, require) assert.notEqual(registered, undefined, 'client.js never called window.__ModuleLoader__.load') return { module: registered.factory(require), staged, primitives } } /** Drive `apply` against a fake slot/configForms surface and report what it did. */ function drive(mod, { served = true, value = {} } = {}) { const seen = { formNs: undefined, watched: undefined, registrations: [] } const disposers = [] let currentSlot const ctx = { inject: (names, scope) => { const scoped = { configForms: { get: (ns) => { seen.formNs = ns return { value, getSnapshot: () => ({ value, writable: true }), subscribe: () => () => {}, mutate: () => true } }, whileServed: (namespaces, register) => { seen.watched = namespaces // The real `whileServed` registers only while the Host serves the // namespace; `served` is what the Host's `settings.describe` answers. if (!served) return () => {} return register(new Set(namespaces)) }, }, slots: { inject: (name, register) => { currentSlot = name register() currentSlot = undefined }, register: (options, component) => { seen.registrations.push({ slot: currentSlot, options, component }) return () => {} }, }, effect: (fn) => { const dispose = fn() if (typeof dispose === 'function') disposers.push(dispose) return dispose }, } scope(scoped) }, } mod.apply(ctx) return { seen, disposers } } /** The one registration a test cares about, by the slot it went into. */ function registrationFor(seen, slot) { const found = seen.registrations.filter((entry) => entry.slot === slot) assert.equal(found.length, 1, `expected exactly one registration into ${slot}`) return found[0] } test('the module is the one the Plugins page loads, and asks for the slots it registers into', async () => { const { module: mod } = await loadClient() assert.equal(mod.name, 'dsh-facet') assert.deepEqual(mod.inject, ['slots']) assert.equal(typeof mod.apply, 'function') }) test('the form binds to the profile entry id while the card registers under the package name', async () => { const { module: mod } = await loadClient() const { seen } = drive(mod) const card = registrationFor(seen, 'plugins.bundle.config') // These two are deliberately different strings. `facet-policy` is the `id` of // this bundle's row in cordis.patch.yml — what `Config` is filed under. The // card is dispatched by the BUNDLE's package name. assert.equal(seen.formNs, 'facet-policy') assert.equal(card.options.key, 'dsh-facet') assert.notEqual(seen.formNs, card.options.key, 'if these ever coincide, the trap this test guards has moved') assert.equal(card.slot, 'plugins.bundle.config') assert.deepEqual(seen.watched, ['facet-policy']) }) test('the context button rides the composer tool row, gated on the same namespace', async () => { const { module: mod } = await loadClient() const { seen } = drive(mod) const button = registrationFor(seen, 'conversation.input.left') // An id, not a key: `conversation.input.left` is a list, and the row shows one // entry per id. assert.equal(button.options.id, 'facet-context') assert.equal(typeof button.options.inject, 'function') // It has to receive the settings form, because the panel writes through the // very same document the plugin card does. assert.equal(typeof button.options.inject().form, 'object') }) test('the host row the form binds to is the one cordis.patch.yml actually declares', async () => { const patch = await readFile(new URL('../cordis.patch.yml', import.meta.url), 'utf8') const { module: mod } = await loadClient() const { seen } = drive(mod) const card = registrationFor(seen, 'plugins.bundle.config') // A rename in the patch without a rename here is a card that writes nowhere, // so the id is read out of the patch rather than trusted to the test. assert.match(patch, new RegExp(`id:\\s*${seen.formNs}\\b`)) assert.equal((await import('../package.json', { with: { type: 'json' } })).default.name, card.options.key) }) test('nothing is registered while the Host does not serve the namespace', async () => { const { module: mod } = await loadClient() const { seen } = drive(mod, { served: false }) assert.equal(seen.formNs, 'facet-policy', 'the form is still bound') // Neither the card nor the button: each would be a control that writes nowhere. assert.deepEqual(seen.registrations, []) }) test('the card stages exactly the deployment fields the host marks volatile', async () => { const { module: mod, staged } = await loadClient() drive(mod) const volatileFields = Object.entries(Config.dict) .filter(([, schema]) => schema.meta.volatile === true) .map(([key]) => key) // `scopes` is volatile too, but it is not a deployment field and must not be a // control on the card: it is per-session state, written by the context panel // and keyed by session id. Staging it here would render a dictionary blob a // user could not sensibly edit, and would put the panel's store on the one // surface that edits the deployment. assert.ok(volatileFields.includes('scopes'), 'scopes must be volatile or the panel cannot write it') const declared = volatileFields.filter((key) => key !== 'scopes') // A field served but never staged is invisible in the UI; a field staged but // never served makes `whileServed` withhold the whole card. assert.deepEqual( staged.map((spec) => spec.field).sort(), declared.sort(), 'the card and the host schema have drifted apart', ) }) test('the choice field refuses a value the schema would reject, and clears on empty', async () => { const { module: mod, staged } = await loadClient() drive(mod) const mode = staged.find((spec) => spec.field === 'mode') assert.equal(mode.parse('deny').value, 'deny') assert.equal(mode.parse(' DENY ').value, 'deny') assert.deepEqual(mode.parse(''), { kind: 'clear' }) // Undefined marks the draft invalid, which blocks the save rather than // coercing a typo into a mode the Host would reject anyway. assert.equal(mode.parse('maybe'), undefined) }) test('the list field clears rather than storing an empty array that shadows the default', async () => { const { module: mod, staged } = await loadClient() drive(mod) const roots = staged.find((spec) => spec.field === 'extraRoots') assert.deepEqual(roots.parse('a, b\nc').value, ['a', 'b', 'c']) assert.deepEqual(roots.parse(' '), { kind: 'clear' }) assert.equal(roots.format(['a', 'b']), 'a, b') assert.equal(roots.format(undefined), '') }) test('the card renders both views without reaching for anything it was not given', async () => { const { module: mod } = await loadClient() const { seen } = drive(mod) const { component: card } = registrationFor(seen, 'plugins.bundle.config') assert.equal(typeof card, 'function') assert.equal(card({ view: 'summary' }), 'How a call outside the session\u2019s own folder is handled, and which paths are always in scope.') const tree = card({ view: 'page', useFacetCard: (select) => select({ available: true, writable: true, dirty: false, invalid: false, saving: false, failed: false, mode: { text: 'deny', overridden: false, invalid: false }, extraRoots: { text: 'src/shared', overridden: true, invalid: false }, }), edit: () => {}, resetField: () => {}, save: () => {}, discard: () => {}, }) // The form frame, with the segmented control reading the staged mode. Children // ride the `children` prop, which is how the platform's own cards pass them. assert.equal(tree.type, 'SettingsForm') const controls = tree.props.children.children assert.equal(controls[0].type, 'div') assert.equal(controls[1].type, 'SettingsValueField') assert.equal(controls[0].children[1].props.value, 'deny') assert.equal(controls[1].props.text, 'src/shared') }) test('the client half loads the packages whose slots it registers into', async () => { const pkg = (await import('../package.json', { with: { type: 'json' } })).default const inject = pkg.dsh.client.inject // Registering into a slot is a dependency on the package that declares it. // `slots.inject` waits for a slot rather than creating one, so a half that // names a slot belonging to a package the boot graph has not loaded yet // registers nothing at all — and silently, because nothing errors. assert.ok(inject.includes('@deepseek-ai/dsh-client-ui-plugin-manager'), 'registers into plugins.bundle.config') assert.ok(inject.includes('@deepseek-ai/dsh-client-ui-conversation'), 'registers into conversation.input.left') assert.ok(inject.includes('@deepseek-ai/dsh-client-ui-settings'), 'reads configForms') }) // ── the context button and panel ───────────────────────────────────────────── test('the context button appears for a facet session and nowhere else', async () => { const { module: mod } = await loadClient() const { seen } = drive(mod) const { component: button, options } = registrationFor(seen, 'conversation.input.left') const { form } = options.inject() const render = (preset) => button({ sessionId: 'session-1', useSessions: (select) => select({ byId: { 'session-1': { projectionValues: { agentPreset: preset } } } }), locked: false, form, }) assert.notEqual(render('facet'), null) // Every other preset in the deployment is untouched: a control for a plugin // that is not running would be worse than no control. assert.equal(render('standard'), null) assert.equal(render(undefined), null) }) test('declaredFor normalises whatever the document holds', async () => { const { module: mod } = await loadClient() assert.deepEqual(mod.declaredFor({ value: { scopes: { a: { focus: 'src/x', extras: ['p'] } } } }, 'a'), { focus: 'src/x', extras: ['p'], }) // A session with no entry, a document with no scopes, and junk in either all // answer with the same empty declaration rather than throwing in a render. const empty = { focus: '', extras: [] } assert.deepEqual(mod.declaredFor({ value: {} }, 'a'), empty) assert.deepEqual(mod.declaredFor({}, 'a'), empty) assert.deepEqual(mod.declaredFor({ value: { scopes: { a: 'nonsense' } } }, 'a'), empty) assert.deepEqual(mod.declaredFor({ value: { scopes: { a: { focus: 7, extras: ['ok', 3, null] } } } }, 'a'), { focus: '', extras: ['ok'], }) }) test('writeDeclaration writes one whole declaration, and removes an empty one', async () => { const { module: mod } = await loadClient() const calls = [] const form = { mutate: (ops) => (calls.push(ops), true) } mod.writeDeclaration(form, 'session-1', { focus: ' src/x ', extras: ['p', ' ', 'q'] }) mod.writeDeclaration(form, 'session-1', { focus: '', extras: [] }) // One path, one write: a queued edit can never half-apply. assert.deepEqual(calls[0], [{ op: 'set', path: ['scopes', 'session-1'], value: { focus: 'src/x', extras: ['p', 'q'] } }]) // Both fields empty means "the session directory", which is the schema default // — so it is removed rather than stored as an empty override. assert.deepEqual(calls[1], [{ op: 'unset', path: ['scopes', 'session-1'] }]) }) test('the panel renders the scope, the guidance, and a way to add and remove', async () => { const { module: mod } = await loadClient() const written = [] const form = { getSnapshot: () => ({ status: 'ready', writable: true, revision: 4, value: { mode: 'deny', scopes: { 'session-1': { focus: 'src/room', extras: ['CONTROLLER.md'] } } }, }), subscribe: () => () => {}, mutate: (ops) => (written.push(ops), true), } const tree = mod.FacetContextPanel({ form, sessionId: 'session-1', mode: 'deny', onClose: () => {} }) assert.equal(tree.type, 'Modal') // Walk the tree for the text and controls a user would see. const texts = [] const buttons = [] const inputs = [] const walk = (node) => { if (typeof node === 'string') return texts.push(node) if (node === null || node === undefined || typeof node !== 'object') return if (Array.isArray(node)) return node.forEach(walk) if (node.type === 'button') buttons.push(node) if (node.type === 'input') inputs.push(node) // Children arrive either as createElement arguments or, as the card does it, // through the `children` prop. if (Array.isArray(node.children)) node.children.forEach(walk) walk(node.props?.children) } walk(tree) const labelOf = (node) => (node.children ?? [])[0] const prose = texts.join('\n') assert.match(prose, /What this conversation may see/) // The two tabs the preset picker cannot show for a custom preset live here. assert.match(prose, /Mode details/) assert.match(prose, /How to use/) // The honest bit about this deployment, so the panel does not promise a // question no approval channel can carry. assert.match(prose, /no approval channel/) assert.match(prose, /danger-full-access/) // The declared scope is on screen, and the existing extra has its own control. assert.equal(inputs[0].props.value, 'src/room') assert.equal(buttons.filter((button) => labelOf(button) === 'Remove').length, 1) assert.ok(buttons.some((button) => labelOf(button) === 'Add')) assert.ok(buttons.some((button) => labelOf(button) === 'Clear')) assert.ok(buttons.some((button) => labelOf(button) === 'Set')) })