Files
dsh-slopo/test/client.test.js
T
dsh-slopo agent 22be1e819a Test the scope defaults, the backlog gate and the database reader
The gate tests use a canned shell runner, and two of them build a real
slopo-shaped SQLite file to prove the warm-database hole is closed: the summary
line says zero new units while the database says 8,607 unembedded, and the gate
must believe the database.

Those fixtures insert in one transaction. Row-by-row they were 8,607 individual
fsyncs, which took two minutes and timed the suite out - worth knowing before
writing another one.
2026-09-22 13:23:49 +01:00

299 lines
10 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 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.
*
* The card is then rendered by calling the registered component and expanding
* the elements it returns, and its handlers are driven directly. That is not a
* substitute for looking at it in a browser, but it does check the wiring the
* card cannot work without.
*/
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { test } from 'node:test'
const SOURCE = readFileSync(new URL('../client/client.js', import.meta.url), 'utf8')
/** Minimal React: enough for createElement plus the three hooks the card uses. */
function makeFakeReact() {
const states = []
let cursor = 0
return {
reset() {
cursor = 0
},
api: {
createElement: (type, props, ...children) => ({ type, props: props ?? {}, children }),
useState(initial) {
const index = cursor
cursor += 1
if (!(index in states)) states[index] = typeof initial === 'function' ? initial() : initial
return [
states[index],
(next) => {
states[index] = typeof next === 'function' ? next(states[index]) : next
},
]
},
useEffect() {},
useMemo(factory) {
return factory()
},
},
}
}
/** Evaluate the bundle against a fake loader and return its exports. */
function loadBundle() {
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 react = makeFakeReact()
const exported = loaded.factory((name) => {
if (name === 'react') return react.api
throw new Error(`the client bundle required "${name}", which is not in the platform seed table`)
})
return { id: loaded.id, exported, react }
}
/**
* Resolve function components by calling them, until a host element or null.
*
* This is the whole "renderer": the slot hands back an element whose type is the
* card component, and function-typed children are expanded the same way.
*/
function expand(node, depth = 0) {
if (depth > 50) 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
}
function textOf(node) {
if (typeof node === 'string') return node
if (node === null || node === undefined || typeof node !== 'object') return ''
if (Array.isArray(node)) return node.map(textOf).join('')
return (node.children ?? []).map(textOf).join('')
}
/** A SettingsScope stand-in recording the writes it was asked to make. */
function fakeScope(snapshot) {
const calls = { set: [], unset: [] }
return {
calls,
getSnapshot: () => snapshot,
subscribe: () => () => {},
set: async (field, value) => {
calls.set.push([field, value])
},
unset: async (field) => {
calls.unset.push(field)
},
}
}
const READY = {
status: 'ready',
value: { embeddingModel: '', embeddingDimensions: 0 },
user: {},
base: {},
revision: 1,
writable: true,
mode: 'host',
}
/** Register the card the way the host would, and hand back its component. */
function registered() {
const { id, exported, react } = loadBundle()
let registration
let render
const ctx = {
inject(deps, callback) {
assert.deepEqual(deps, ['settingsScope'])
callback({
settingsScope: { bind: () => fakeScope(READY) },
slots: {
inject(_name, produce) {
produce()
},
register(options, component) {
registration = options
render = component
},
},
})
},
}
exported.apply(ctx)
const draw = (scope) => expand(render({ scope }))
return { id, exported, react, registration, render, draw }
}
const inputFor = (tree, label) =>
findAll(tree, (node) => node.type === 'input' && node.props['aria-label'] === label)[0]
const buttonStartingWith = (tree, prefix) =>
findAll(tree, (node) => node.type === 'button' && textOf(node).startsWith(prefix))[0]
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 react.
assert.doesNotThrow(() => loadBundle())
})
test('applying registers exactly one settings.plugin.item card, keyed on the namespace', () => {
const { registration, exported } = registered()
assert.equal(exported.name, 'dsh-slopo')
assert.deepEqual(exported.inject, ['slots'])
assert.equal(registration.name, 'settings.plugin.item')
// The key must equal the Host namespace, or the tab never dispatches the card.
assert.equal(registration.key, 'dsh-slopo')
})
test('the card renders a control for every setting the Host accepts', () => {
const { draw } = registered()
const tree = draw(fakeScope(READY))
const labels = findAll(tree, (node) => node.type === 'label').map(textOf)
for (const expected of [
'Embedding model',
'Embedding dimensions',
'Embedding API base',
'Embedding API key',
'Extra embedding parameters',
'slopo executable',
'Source directory',
'Extra exclude patterns',
'State directory',
'Ignore file',
'Foreground timeout',
]) {
assert.ok(
labels.some((label) => label.includes(expected)),
`no control labelled "${expected}"`,
)
}
})
test('the API key is rendered as a password field', () => {
const { draw } = registered()
const tree = draw(fakeScope(READY))
assert.equal(inputFor(tree, 'Embedding API key').props.type, 'password')
})
test('the card renders nothing when the namespace is unavailable', () => {
const { draw } = registered()
assert.equal(draw(fakeScope({ ...READY, status: 'unavailable' })), null)
})
test('editing a field marks the card dirty and saving writes it through set()', async () => {
const { draw, react } = registered()
const scope = fakeScope(READY)
inputFor(draw(scope), 'Embedding model').props.onChange({ target: { value: 'ollama/example:latest' } })
// Re-render the way a state update would.
react.reset()
const tree = draw(scope)
const save = buttonStartingWith(tree, 'Save')
assert.equal(save.props.disabled, false, 'a changed field must enable Save')
await save.props.onClick()
assert.deepEqual(scope.calls.set, [['embeddingModel', 'ollama/example:latest']])
assert.deepEqual(scope.calls.unset, [])
})
test('a whole number field is sent as a number, not the string the input holds', async () => {
const { draw, react } = registered()
const scope = fakeScope(READY)
inputFor(draw(scope), 'Embedding dimensions').props.onChange({ target: { value: '768' } })
react.reset()
await buttonStartingWith(draw(scope), 'Save').props.onClick()
assert.deepEqual(scope.calls.set, [['embeddingDimensions', 768]])
})
test('clearing a field is an unset, so it re-inherits rather than becoming empty', async () => {
const { draw, react } = registered()
const scope = fakeScope({ ...READY, value: { embeddingModel: 'ollama/old:latest', embeddingDimensions: 768 } })
inputFor(draw(scope), 'Embedding model').props.onChange({ target: { value: '' } })
react.reset()
await buttonStartingWith(draw(scope), 'Save').props.onClick()
assert.deepEqual(scope.calls.unset, ['embeddingModel'])
assert.deepEqual(scope.calls.set, [], 'a cleared field must not be written as an empty string')
})
test('an exclude list round-trips through comma-separated text', async () => {
const { draw, react } = registered()
const scope = fakeScope({ ...READY, value: { embeddingModel: 'm', embeddingDimensions: 1 } })
inputFor(draw(scope), 'Extra exclude patterns').props.onChange({ target: { value: '**/test/**, *.min.js' } })
react.reset()
await buttonStartingWith(draw(scope), 'Save').props.onClick()
assert.deepEqual(scope.calls.set, [['sourceDirExclude', ['**/test/**', '*.min.js']]])
})
test('a boolean field writes a real boolean', async () => {
const { draw, react } = registered()
const scope = fakeScope(READY)
const checkbox = findAll(
draw(scope),
(node) => node.type === 'input' && node.props['aria-label'] === 'Install slopo with uv when missing',
)[0]
checkbox.props.onChange({ target: { checked: true } })
react.reset()
await buttonStartingWith(draw(scope), 'Save').props.onClick()
assert.deepEqual(scope.calls.set, [['autoInstall', true]])
})
test('an overridden field is marked, and Reset clears every override', async () => {
const { draw } = registered()
const scope = fakeScope({
...READY,
value: { embeddingModel: 'ollama/x:latest', embeddingDimensions: 768 },
user: { embeddingModel: 'ollama/x:latest', embeddingDimensions: 768 },
})
const tree = draw(scope)
const labels = findAll(tree, (node) => node.type === 'label').map(textOf)
assert.ok(labels.some((label) => label.includes('overridden')), 'a user-set field must be marked')
const reset = buttonStartingWith(tree, 'Reset')
assert.equal(reset.props.disabled, false)
await reset.props.onClick()
assert.deepEqual(scope.calls.unset.sort(), ['embeddingDimensions', 'embeddingModel'])
})
test('a read-only namespace disables every control', () => {
const { draw } = registered()
const tree = draw(fakeScope({ ...READY, writable: false, mode: 'memory' }))
const inputs = findAll(tree, (node) => node.type === 'input')
assert.ok(inputs.length > 0)
for (const input of inputs) assert.equal(input.props.disabled, true)
})