Files
dsh-slopo/test/client.test.js
T
dsh-slopo agent 6beb4c0d39 fix: keep local home paths and addresses out of the test fixtures
Three test-only spots had baked in the machine this repository was developed
on, and the repository is publicly readable, so they shipped that machine's
shape to anyone who cloned it:

  * the end-to-end embedding backend was pinned to a private LAN address;
    it is now read from DSH_SLOPO_E2E_API_BASE, defaulting to Ollama's own
    loopback address, so the machine running the suite supplies its own;
  * the real-slopo test resolved discovery from a hardcoded root home, which
    meant it could only ever run for one user — it resolves from the running
    user's home now, which is also the fix for everyone else;
  * the harness-slot contract tests read the global npm tree by absolute
    path; they go through this repository's own node_modules symlink, which
    is already relative to the checkout.

No user-facing behaviour changes: the test files are not in the published
package, and the default configuration was already free of machine-specific
values — every address, path and key defaults to empty and is resolved per
user at runtime.
2026-09-26 07:05:33 +01:00

828 lines
31 KiB
JavaScript

/**
* Tests for the browser half.
*
* There is no browser here, so the bundle is evaluated the way the real module
* loader would — inside `window.__ModuleLoader__.load` — with a fake `require`
* that supplies React and `@deepseek-ai/dsh-client-ui-primitives` and THROWS on
* anything else. That last part is the useful one: it proves the bundle stays
* inside the platform's seed table, which is the constraint a hand-written
* client half is most likely to break.
*
* Two things are therefore stubbed, and it is worth being plain about what that
* does and does not prove:
*
* * React. The card itself calls no hooks — the shared primitives do — so the
* double only has to implement `createElement`, and component resolution is
* the tree walk below.
* * `@deepseek-ai/dsh-client-ui-primitives`. It is a real ESM package that
* imports React, `clsx`, zustand and thirty stylesheets, so it cannot be
* loaded under plain Node; the double reproduces the shipped
* `SettingsFormModel` staging semantics (read back from the package's own
* `lib/index.js`) closely enough that this card's field specs — which are
* the part this repo owns — are exercised for real, and renders the shared
* components as their documented props describe.
*
* `docs-contract` at the bottom of this file covers the other direction: that
* the slot and service names this bundle binds to are the ones the installed
* harness actually ships.
*/
import assert from 'node:assert/strict'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
const SOURCE = readFileSync(new URL('../client/client.js', import.meta.url), 'utf8')
/**
* The installed harness's packages, reached through this repository's own
* `node_modules/@deepseek-ai` symlink rather than a committed absolute path —
* the link is what makes the `@deepseek-ai/*` imports resolve during tests in
* the first place, so it is the one address that is already relative to the
* checkout.
*/
const DSH_PACKAGES = fileURLToPath(new URL('../node_modules/@deepseek-ai/', import.meta.url))
// ---------------------------------------------------------------------------
// The two doubles
// ---------------------------------------------------------------------------
/**
* The shipped `SettingsFormModel`, reduced to the reads and the one write this
* card drives. Staging, the plan, the revision fence and the "a refused save
* keeps its drafts" rule are the shipped behaviour, not an invention: they are
* the reason the card uses this model instead of writing on every keystroke.
*
* A store is a plain snapshot holder here; the shipped one is a uSES-compatible
* observable, which is all `store.set`/`getSnapshot` need to stand in for.
*/
class SettingsFormModel {
constructor(scope, specs, secrets = []) {
this.scope = scope
this.specs = new Map(specs.map((spec) => [spec.field, spec]))
this.secretSpecs = new Map(secrets.map((spec) => [spec.field, spec]))
this.staged = new Map()
this.listeners = new Set()
this.saving = false
this.failed = false
this.unsubscribe = scope.subscribe(() => this.publish())
}
bind(project) {
const store = {
snapshot: project(),
getSnapshot() {
return this.snapshot
},
set(next) {
this.snapshot = next
},
subscribe() {
return () => {}
},
}
this.listeners.add(() => store.set(project()))
return store
}
shell() {
const snapshot = this.scope.getSnapshot()
const plan = this.plan()
return {
available: snapshot.status === 'ready',
writable: snapshot.writable,
dirty: plan.length > 0,
invalid: plan.some((item) => item.run === undefined && item.op === undefined),
saving: this.saving,
failed: this.failed,
}
}
field(field) {
const staged = this.staged.get(field)
if (this.secretSpecs.has(field)) {
return { text: staged?.text ?? '', overridden: false, invalid: false }
}
const spec = this.spec(field)
if (staged === undefined) {
return { text: spec.format(this.sectionValue(field)), overridden: this.stored(field), invalid: false }
}
const write = staged.clear ? { kind: 'clear' } : spec.parse(staged.text)
return { text: staged.text, overridden: write?.kind === 'set', invalid: write === undefined }
}
actions() {
return {
edit: (field, text) => this.stage(field, { text, clear: false }),
resetField: (field) => this.stage(field, { text: this.spec(field).format(this.baseValue(field)), clear: true }),
save: () => this.save(),
discard: () => {
if (this.staged.size === 0 && !this.failed) return
this.staged.clear()
this.baseline = undefined
this.failed = false
this.publish()
},
}
}
async save() {
const plan = this.plan()
if (!plan.length || this.saving || !this.scope.getSnapshot().writable) return
if (plan.some((item) => item.run === undefined && item.op === undefined)) return
this.saving = true
this.failed = false
this.publish()
try {
const ops = plan.flatMap((item) => (item.op === undefined ? [] : [item.op]))
let landed = !ops.length || (await this.scope.mutate(ops, this.baseline?.revision))
if (!landed) {
this.failed = true
return
}
for (const item of plan) if (item.run) landed = (await item.run()) && landed
if (landed) {
this.staged.clear()
this.baseline = undefined
}
this.failed = !landed
} catch {
this.failed = true
} finally {
this.saving = false
this.publish()
}
}
plan() {
const plan = []
for (const [field, staged] of this.staged) {
const secret = this.secretSpecs.get(field)
if (secret !== undefined) {
const value = staged.text.trim()
if (value !== '') plan.push({ field, run: () => secret.write(value) })
continue
}
const spec = this.spec(field)
if (staged.clear) {
if (this.stored(field)) plan.push({ field, op: { op: 'unset', path: [field] } })
continue
}
if (staged.text === spec.format(this.sectionValue(field))) continue
const write = spec.parse(staged.text)
if (write === undefined) plan.push({ field })
else if (write.kind === 'clear') plan.push({ field, op: { op: 'unset', path: [field] } })
else plan.push({ field, op: { op: 'set', path: [field], value: write.value } })
}
return plan
}
stage(field, edit) {
this.baseline ??= this.scope.getSnapshot()
this.staged.set(field, edit)
this.failed = false
this.publish()
}
publish() {
for (const listener of this.listeners) listener()
}
dispose() {
this.unsubscribe()
this.listeners.clear()
}
spec(field) {
const spec = this.specs.get(field)
if (spec === undefined) throw new Error(`no spec for "${field}"`)
return spec
}
sectionValue(field) {
return this.scope.getSnapshot().value?.[field]
}
baseValue(field) {
return this.scope.getSnapshot().base?.[field]
}
userLayer() {
const user = this.scope.getSnapshot().user
return user !== null && typeof user === 'object' ? user : {}
}
stored(field) {
return Object.hasOwn(this.userLayer(), field)
}
}
/** The shipped number and text specs, verbatim. */
const numberField = (field) => ({
field,
format: (value) => (typeof value === 'number' ? String(value) : ''),
parse: (text) => {
const trimmed = text.trim()
if (trimmed === '') return { kind: 'clear' }
const parsed = Number(trimmed)
return Number.isFinite(parsed) ? { kind: 'set', value: parsed } : undefined
},
})
const textField = (field) => ({
field,
format: (value) => (typeof value === 'string' ? value : ''),
parse: (text) => {
const trimmed = text.trim()
return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed }
},
})
/**
* The shared components, rendered as the props they document.
*
* Every one of them is a plain function, so expanding the tree never needs a
* hook runtime: the card's own composition is what is under test.
*/
function primitivesDouble() {
const h = (type, props, ...children) => ({ type, props: props ?? {}, children })
const SettingsForm = (props) =>
h(
'form',
{
'data-labels': JSON.stringify(props.labels),
// The real form disables Save until something is staged, nothing is
// invalid, no write is in flight, and the document accepts writes.
'data-can-save': String(props.state.dirty && !props.state.invalid && !props.state.saving && props.state.writable),
onSave: props.onSave,
onDiscard: props.onDiscard,
},
h('button', { type: 'button', disabled: !(props.state.dirty && !props.state.invalid && props.state.writable), onClick: props.onSave }, props.labels.save),
props.children,
)
const SettingsValueField = (props) =>
h(
'div',
{ 'data-field': props.id },
h('label', { htmlFor: props.id }, props.label),
h('input', {
id: props.id,
type: 'text',
value: props.text,
disabled: props.disabled,
'aria-label': props.label,
'data-invalid': String(props.invalid),
'data-overridden': String(props.overridden),
onChange: (event) => props.onEdit(event.target.value),
}),
props.overridden ? h('button', { type: 'button', disabled: props.disabled, onClick: props.onReset }, props.resetLabel) : null,
h('div', { 'data-hint': true }, props.invalid ? props.invalidLabel : props.hint),
)
const SettingsSecretField = (props) =>
h(
'div',
{ 'data-field': props.id },
h('label', { htmlFor: props.id }, props.label),
h('input', {
id: props.id,
type: 'password',
value: props.text,
disabled: props.disabled,
'aria-label': props.label,
autoComplete: 'off',
onChange: (event) => props.onEdit(event.target.value),
}),
h('div', { 'data-state': true }, props.stateLabel),
h('div', { 'data-configured': String(props.configured) }),
)
// The real primitive is a labelled checkbox: the label WRAPS the control.
const Checkbox = (props) =>
h(
'label',
{},
h('input', {
type: 'checkbox',
checked: Boolean(props.checked),
disabled: props.disabled,
'aria-label': props.label,
onChange: (event) => props.onChange(event.target.checked),
}),
props.label,
)
return { SettingsForm, SettingsFormModel, SettingsSecretField, SettingsValueField, Checkbox, settingsNumberField: numberField, settingsTextField: textField }
}
/**
* Minimal React: `createElement` is the only hook-free seam the card needs.
*
* Children land in `props.children` as well as on the wrapper, because that is
* where a real element carries them and what the shared components read.
*/
const fakeReact = {
createElement: (type, props, ...children) => {
const merged = { ...(props ?? {}) }
if (children.length > 0) merged.children = children.length === 1 ? children[0] : children
return { type, props: merged, children }
},
}
// ---------------------------------------------------------------------------
// Harness
// ---------------------------------------------------------------------------
/** Evaluate the bundle against a fake loader and return its exports. */
function loadBundle(requireImpl) {
let loaded
const window = { __ModuleLoader__: { load: (spec) => { loaded = spec } } }
new Function('window', SOURCE)(window)
assert.ok(loaded !== undefined, 'the bundle must call window.__ModuleLoader__.load')
const primitives = primitivesDouble()
const exported = loaded.factory(
requireImpl ??
((name) => {
if (name === 'react') return fakeReact
if (name === 'react/jsx-runtime') return { jsx: fakeReact.createElement, jsxs: fakeReact.createElement, Fragment: 'Fragment' }
if (name === '@deepseek-ai/dsh-client-ui-primitives') return primitives
throw new Error(`the client bundle required "${name}", which is not in the platform seed table`)
}),
)
return { id: loaded.id, exported, primitives }
}
const READY = {
status: 'ready',
value: {
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
embeddingApiBase: 'http://127.0.0.1:11434',
embeddingParamsJson: '{}',
binary: '',
autoInstall: true,
sourceDir: '',
sourceDirExclude: ['**/dist/**'],
useDefaultExcludes: true,
maxIndexUnits: 5000,
ignoreFile: '',
stateDir: '',
embeddingRequestDelay: 0,
timeoutMs: 600_000,
},
base: {},
user: {},
revision: 7,
writable: true,
mode: 'host',
}
/** A settings scope stand-in recording the mutations it was asked to make. */
function fakeScope(snapshot = READY, { landed = true } = {}) {
const calls = { mutate: [], subscriptions: 0 }
return {
calls,
getSnapshot: () => snapshot,
subscribe(listener) {
calls.subscriptions += 1
return () => {}
},
async mutate(ops, revision) {
calls.mutate.push({ ops, revision })
return landed
},
}
}
/**
* Register the card the way the host would, and hand back everything a test
* needs to drive it: the registration options, the injected face, and a render
* function.
*/
function registered({ snapshot = READY, served = true, secrets = [], landed = true } = {}) {
const { id, exported, primitives } = loadBundle()
const scope = fakeScope(snapshot, { landed })
const effects = []
const injects = []
const whileServed = []
const dictionaries = new Map()
let registration
let component
const configForms = {
get: (ns) => {
assert.equal(ns, 'dsh-slopo')
return scope
},
whileServed: (namespaces, register) => {
whileServed.push(namespaces)
const off = served ? register(new Set(namespaces)) : undefined
return () => off?.()
},
describe: () => ({
getSnapshot: () => ({ view: { namespaces: [{ ns: 'dsh-slopo', secrets }] } }),
}),
}
/** A `ctx.inject` child: an effect scope plus the slots registry. */
const childEffect = (effect, _label) => {
effects.push(effect())
}
const ctx = {
effect: childEffect,
locale: {
register(ns, dicts) {
dictionaries.set(ns, dicts.en)
return () => {}
},
bind: (ns) => {
const dict = dictionaries.get(ns) ?? {}
return (key) => dict[key] ?? key
},
},
inject(deps, callback) {
injects.push(deps)
callback({
configForms,
effect: childEffect,
slots: {
inject(_name, produce) {
produce()
return () => {}
},
register(options, render) {
registration = options
component = render
},
},
})
},
}
exported.apply(ctx)
const t = ctx.locale.bind('dsh-slopo')
const injectFace = () => ({ t: (key) => t(key), ...registration.inject() })
const render = (extra = {}) => {
const face = { ...injectFace(), ...extra }
const state = face.hooks.slopoCard.getSnapshot()
return component({ ...face, view: 'page', useSlopoCard: (select) => select(state) })
}
return { id, exported, primitives, scope, effects, injects, whileServed, registration: () => registration, render, t }
}
// ---------------------------------------------------------------------------
// Tree helpers
// ---------------------------------------------------------------------------
/** Resolve function components by calling them, until a host element or null. */
function expand(node, depth = 0) {
if (depth > 60) throw new Error('the component tree did not settle')
if (node === null || node === undefined) return null
if (typeof node !== 'object') return node
if (Array.isArray(node)) return node
if (typeof node.type === 'function') return expand(node.type(node.props), depth + 1)
return node
}
/** Depth-first walk over a rendered tree, expanding components as it goes. */
function findAll(node, predicate, out = []) {
if (node === null || node === undefined) return out
if (Array.isArray(node)) {
for (const child of node) findAll(child, predicate, out)
return out
}
if (typeof node !== 'object') return out
if (typeof node.type === 'function') {
findAll(expand(node), predicate, out)
return out
}
if (predicate(node)) out.push(node)
for (const child of node.children ?? []) findAll(child, predicate, out)
return out
}
const inputFor = (tree, label) =>
findAll(tree, (node) => node.type === 'input' && node.props['aria-label'] === label)[0]
const saveButton = (tree) => findAll(tree, (node) => node.type === 'button' && node.children.includes('Save'))[0]
// ---------------------------------------------------------------------------
// Bundle identity and the seed table
// ---------------------------------------------------------------------------
test('the bundle id matches the package name the loader is told about', () => {
const { id } = loadBundle()
assert.equal(id, 'dsh-slopo')
})
test('the client bundle requires nothing outside the platform seed table', () => {
// loadBundle() throws on any specifier other than the two seed words.
assert.doesNotThrow(() => loadBundle())
})
test('the bundle exports the package name and the services it needs', () => {
const { exported } = loadBundle()
assert.equal(exported.name, 'dsh-slopo')
// `locale` is required because the card's copy rides a dictionary; `slots`
// because the registration is a slot.
assert.deepEqual(exported.inject, ['slots', 'locale'])
})
// ---------------------------------------------------------------------------
// The registration contract — the part that regressed
// ---------------------------------------------------------------------------
test('the card registers into plugins.bundle.config, keyed by the bundle package name', () => {
const { registration, exported } = registered()
assert.equal(exported.name, 'dsh-slopo')
assert.equal(registration().name, 'plugins.bundle.config')
// The key must equal the bundle's package name: the Plugins page dispatches
// this slot by `pkg.name`, and nothing else.
assert.equal(registration().key, 'dsh-slopo')
// `locale` is what injects the card's `t`.
assert.equal(registration().locale, 'dsh-slopo')
})
test('the card no longer binds the pre-0.1.7 settings surface', () => {
// `settings.plugin.item` and the `settingsScope` service were removed from the
// harness; a bundle that still names them renders nothing at all, and does it
// silently. The header comment names them on purpose, so the scan is over the
// code: strip comments first, then look.
const code = SOURCE.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
assert.doesNotMatch(code, /settingsScope/)
assert.doesNotMatch(code, /settings\.plugin\.item/)
assert.doesNotMatch(code, /installSection/)
})
test('the card asks for configForms, and does it optionally', () => {
const { injects } = registered()
assert.deepEqual(injects, [['configForms']])
})
test('the registration stands only while the Host serves the namespace', () => {
const served = registered({ served: true })
assert.deepEqual(served.whileServed, [['dsh-slopo']])
assert.ok(served.registration() !== undefined)
// A deployment that does not serve the entry leaves no card behind.
const unserved = registered({ served: false })
assert.equal(unserved.registration(), undefined)
})
test('the injected face carries the card store and the form actions', () => {
const { registration } = registered()
const face = registration().inject()
assert.ok(face.hooks.slopoCard !== undefined, 'the card reads its state through a hook')
for (const action of ['edit', 'resetField', 'save', 'discard']) {
assert.equal(typeof face[action], 'function', `missing form action "${action}"`)
}
})
test('teardown releases the form subscription', () => {
const { scope, effects } = registered()
assert.equal(scope.calls.subscriptions, 1)
// The second effect is the whileServed one; the first disposes the form.
assert.equal(typeof effects[0], 'function')
effects[0]()
// Disposing twice is not the card's business, but the disposer must exist and
// must not throw before the form's own disposer has a chance to run.
assert.doesNotThrow(() => effects[1]())
})
// ---------------------------------------------------------------------------
// The card's controls
// ---------------------------------------------------------------------------
test('the card renders every setting the Host schema declares', () => {
const { render } = registered()
const tree = render()
const labels = findAll(tree, (node) => node.type === 'label').map((node) => node.children.join(''))
for (const expected of [
'Embedding model',
'Embedding dimensions',
'Embedding API base',
'Embedding API key',
'Extra embedding parameters',
'slopo executable',
'Install slopo with uv when missing',
'Source directory',
'Extra exclude patterns',
'Exclude node_modules and build output',
'Refuse to embed more than this many units',
'Ignore file',
'State directory',
'Delay between embedding requests',
'Foreground timeout',
]) {
assert.ok(labels.some((label) => label.includes(expected)), `no control labelled "${expected}"`)
}
})
test('the API key is a write-only password control that reports only whether one is stored', () => {
const { render } = registered({ secrets: [{ path: ['embeddingApiKey'], set: true }] })
const tree = render()
const input = inputFor(tree, 'Embedding API key')
assert.equal(input.props.type, 'password')
assert.equal(input.props.value, '', 'a secret never rides back into the control')
assert.match(findAll(tree, (node) => node.props['data-state'] === true).map((node) => node.children.join('')).join(''), /A key is stored/)
})
test('a deployment with no key says so', () => {
const { render } = registered({ secrets: [{ path: ['embeddingApiKey'], set: false }] })
const tree = render()
assert.equal(findAll(tree, (node) => node.props['data-configured'] === 'true').length, 0)
assert.match(findAll(tree, (node) => node.props['data-state'] === true).map((node) => node.children.join('')).join(''), /No key is stored/)
})
test('the current value of every ordinary field is rendered', () => {
const { render } = registered()
const tree = render()
assert.equal(inputFor(tree, 'Embedding model').props.value, 'ollama/example:latest')
assert.equal(inputFor(tree, 'Embedding dimensions').props.value, '768')
assert.equal(inputFor(tree, 'Extra exclude patterns').props.value, '**/dist/**')
assert.equal(inputFor(tree, 'Install slopo with uv when missing').props.checked, true)
})
test('a read-only deployment disables every control', () => {
const { render } = registered({ snapshot: { ...READY, writable: false, mode: 'memory' } })
const tree = render()
const controls = findAll(tree, (node) => node.type === 'input')
assert.ok(controls.length >= 15)
for (const control of controls) assert.equal(control.props.disabled, true, control.props['aria-label'])
assert.equal(saveButton(tree).props.disabled, true)
})
test('the card hands an unavailable namespace to the shared form instead of hiding', () => {
// The form is what swaps in the "not served" line, from `state.available`.
// Hiding here would leave the Plugins page with a configured-but-empty card.
const { render } = registered({ snapshot: { ...READY, status: 'unavailable' } })
const form = expand(render())
assert.equal(JSON.parse(form.props['data-labels']).unavailable.startsWith('This plugin is not loaded'), true)
})
// ---------------------------------------------------------------------------
// Saving: the field specs this repo owns
// ---------------------------------------------------------------------------
/** Stage one edit, then click Save the way the form does. */
async function stageAndSave(render, label, next) {
const input = inputFor(render(), label)
input.props.onChange({ target: { value: next } })
const form = render()
const save = saveButton(form)
assert.equal(save.props.disabled, false, 'a staged edit must enable Save')
await save.props.onClick()
}
test('Save sends one fenced mutation, at the revision the drafts were read from', async () => {
const { render, scope } = registered()
await stageAndSave(render, 'Embedding model', 'ollama/other:latest')
assert.equal(scope.calls.mutate.length, 1)
assert.deepEqual(scope.calls.mutate[0].ops, [
{ op: 'set', path: ['embeddingModel'], value: 'ollama/other:latest' },
])
assert.equal(scope.calls.mutate[0].revision, 7)
})
test('a whole number field is sent as a number, not the string the input holds', async () => {
const { render, scope } = registered()
await stageAndSave(render, 'Embedding dimensions', '1024')
assert.deepEqual(scope.calls.mutate[0].ops, [{ op: 'set', path: ['embeddingDimensions'], value: 1024 }])
})
test('a number that is not a number blocks the save instead of being dropped', async () => {
const { render, scope } = registered()
const tree = render()
inputFor(tree, 'Embedding dimensions').props.onChange({ target: { value: 'not-a-number' } })
const form = render()
assert.equal(saveButton(form).props.disabled, true, 'an invalid draft must disable Save')
await saveButton(form).props.onClick()
assert.deepEqual(scope.calls.mutate, [])
// …and the field says why.
assert.equal(inputFor(form, 'Embedding dimensions').props['data-invalid'], 'true')
})
test('an exclude list round-trips through comma-separated text', async () => {
const { render, scope } = registered()
await stageAndSave(render, 'Extra exclude patterns', '**/test/**, *.min.js')
assert.deepEqual(scope.calls.mutate[0].ops, [
{ op: 'set', path: ['sourceDirExclude'], value: ['**/test/**', '*.min.js'] },
])
})
test('a switch writes a real boolean', async () => {
const { render, scope } = registered()
const tree = render()
inputFor(tree, 'Install slopo with uv when missing').props.onChange({ target: { checked: false } })
await saveButton(render()).props.onClick()
assert.deepEqual(scope.calls.mutate[0].ops, [{ op: 'set', path: ['autoInstall'], value: false }])
})
test('clearing a field is an unset, so it re-inherits rather than becoming empty', async () => {
const { render, scope } = registered({
snapshot: { ...READY, user: { embeddingModel: 'ollama/example:latest' } },
})
await stageAndSave(render, 'Embedding model', '')
assert.deepEqual(scope.calls.mutate[0].ops, [{ op: 'unset', path: ['embeddingModel'] }])
})
test('an overridden field is marked and offers its own reset', async () => {
const { render, scope } = registered({
snapshot: {
...READY,
base: { embeddingDimensions: 0 },
user: { embeddingDimensions: 768 },
},
})
const tree = render()
const field = inputFor(tree, 'Embedding dimensions')
assert.equal(field.props['data-overridden'], 'true')
const reset = findAll(tree, (node) => node.type === 'button' && node.children.includes('Reset to default'))[0]
assert.ok(reset !== undefined, 'an overridden field must offer a reset')
reset.props.onClick()
await saveButton(render()).props.onClick()
assert.deepEqual(scope.calls.mutate[0].ops, [{ op: 'unset', path: ['embeddingDimensions'] }])
})
test('a secret draft is written one way, and a blank one writes nothing', async () => {
const { render, scope } = registered()
inputFor(render(), 'Embedding API key').props.onChange({ target: { value: 'sk-secret' } })
await saveButton(render()).props.onClick()
assert.deepEqual(scope.calls.mutate[0].ops, [{ op: 'set', path: ['embeddingApiKey'], value: 'sk-secret' }])
const blank = registered()
inputFor(blank.render(), 'Embedding API key').props.onChange({ target: { value: ' ' } })
await saveButton(blank.render()).props.onClick()
assert.deepEqual(blank.scope.calls.mutate, [], 'a blank secret draft must not clear the stored key')
})
test('a refused save keeps its drafts so they can be corrected', async () => {
const { render, scope } = registered({ landed: false })
const tree = render()
inputFor(tree, 'Embedding model').props.onChange({ target: { value: 'ollama/try-again:latest' } })
await saveButton(render()).props.onClick()
assert.equal(scope.calls.mutate.length, 1)
// Still dirty: nothing was accepted, so nothing is thrown away.
assert.equal(saveButton(render()).props.disabled, false)
assert.equal(inputFor(render(), 'Embedding model').props.value, 'ollama/try-again:latest')
})
// ---------------------------------------------------------------------------
// Contract against the installed harness
// ---------------------------------------------------------------------------
const harnessPresent = existsSync(DSH_PACKAGES)
test(
'the slot this card binds to is declared by the installed harness',
{ skip: harnessPresent ? false : 'no global dsh installation to read the slot catalog from' },
() => {
const catalog = readFileSync(join(DSH_PACKAGES, 'dsh-cordis-client-runner/lib/client.js'), 'utf8')
assert.match(catalog, /key: "plugins\.bundle\.config"/, 'plugins.bundle.config must exist in the slot catalog')
assert.doesNotMatch(catalog, /key: "settings\.plugin\.item"/, 'settings.plugin.item must be gone')
},
)
test(
'every primitive the card requires is exported by the installed package',
{ skip: harnessPresent ? false : 'no global dsh installation to read the primitives package from' },
() => {
const bundle = readFileSync(join(DSH_PACKAGES, 'dsh-client-ui-primitives/lib/index.js'), 'utf8')
const exported = new Set(
(bundle.match(/export \{([^}]*)\};/g) ?? [])
.flatMap((line) => line.slice(8, -2).split(','))
.map((name) => name.trim().split(/\s+as\s+/).pop()),
)
for (const name of [
'Checkbox',
'SettingsForm',
'SettingsFormModel',
'SettingsSecretField',
'SettingsValueField',
'settingsNumberField',
'settingsTextField',
]) {
assert.ok(exported.has(name), `@deepseek-ai/dsh-client-ui-primitives does not export "${name}"`)
}
// The card reads the entry's secret slots off the shared describe mirror.
const settings = readFileSync(join(DSH_PACKAGES, 'dsh-client-ui-settings/lib/client.js'), 'utf8')
assert.match(settings, /super\(ctx, "configForms"\)/, 'configForms must be the settings domain service')
},
)
test(
'the installed plugin-manager declares plugins.bundle.config as a keyed child of the Plugins page',
{ skip: harnessPresent ? false : 'no global dsh installation to read the plugin manager from' },
() => {
const manager = readFileSync(join(DSH_PACKAGES, 'dsh-client-ui-plugin-manager/lib/client.js'), 'utf8')
assert.match(manager, /"plugins\.bundle\.config": \{\s*kind: "keyed"/)
// …and dispatches it by the bundle's package name.
assert.match(manager, /renderSlot\("plugins\.bundle\.config", \{ view: "page" \}, \{ entryKey: pkg\.name \}\)/)
},
)