/** * 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', '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), '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) })