init
This commit is contained in:
commit
0964819cdc
34 files changed
+7912
No files matched your search
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* 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` and
|
||||
* `require('react')`. That is worth doing rather than leaving the card to be
|
||||
* eyeballed in a running app, because the two things most likely to be wrong are
|
||||
* both structural and both invisible until someone opens Settings:
|
||||
*
|
||||
* - the root element must be an `<li>`, because the tab renders every card
|
||||
* into a `<ul>` and a `<div>` never receives the list styling;
|
||||
* - the card must start collapsed and disclose on click, because that is the
|
||||
* framework's card contract and the thing that was wrong the first time.
|
||||
*/
|
||||
|
||||
import { strict as assert } from 'node:assert'
|
||||
import { test } from 'node:test'
|
||||
|
||||
/** 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 each control as `h(Field, …)`, and those elements are only
|
||||
* real DOM 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
|
||||
}
|
||||
|
||||
/** The input carrying a given accessible label. */
|
||||
function inputFor(tree, label) {
|
||||
return find(tree, (node) => node.type === 'input' && node.props['aria-label'] === label)[0]
|
||||
}
|
||||
|
||||
/** The save button, whatever its current label. */
|
||||
function saveButton(tree) {
|
||||
return find(tree, (node) => node.type === 'button' && String(node.props.children).startsWith('Save'))[0]
|
||||
}
|
||||
|
||||
/** A settings scope that records writes instead of performing them. */
|
||||
function makeScope(options = {}) {
|
||||
const { value = {}, user = null, status = 'ready', writable = true } = options
|
||||
const calls = []
|
||||
const snapshot = { status, value, user, writable }
|
||||
return {
|
||||
calls,
|
||||
getSnapshot: () => snapshot,
|
||||
subscribe: () => () => {},
|
||||
set: async (key, next) => {
|
||||
calls.push({ op: 'set', key, value: next })
|
||||
},
|
||||
unset: async (key) => {
|
||||
calls.push({ op: 'unset', key })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 module with a stubbed `react`. */
|
||||
function makeModule() {
|
||||
const react = makeReact()
|
||||
const factory = entry.factory((name) => {
|
||||
if (name === 'react') return react
|
||||
throw new Error(`unexpected import: ${name}`)
|
||||
})
|
||||
return { react, module: factory }
|
||||
}
|
||||
|
||||
/** Register every slot the browser half contributes, and render any of them. */
|
||||
function mount(scope) {
|
||||
const { react, module } = makeModule()
|
||||
const registered = []
|
||||
const slots = {
|
||||
inject: (name, fn) => fn(),
|
||||
register: (meta, component) => {
|
||||
registered.push({ meta, component })
|
||||
return () => {}
|
||||
},
|
||||
}
|
||||
const ctx = {
|
||||
slots,
|
||||
inject: (deps, cb) =>
|
||||
cb({
|
||||
settingsScope: {
|
||||
bind: (options) => {
|
||||
assert.equal(options.namespace, 'dsh-recall')
|
||||
return scope
|
||||
},
|
||||
},
|
||||
slots,
|
||||
}),
|
||||
}
|
||||
module.apply(ctx)
|
||||
|
||||
const byName = new Map(registered.map((entry) => [entry.meta.name, entry]))
|
||||
|
||||
/** Render one registered slot with the props its occupant would receive. */
|
||||
const renderSlot = (name, props = {}) => {
|
||||
const entry = byName.get(name)
|
||||
assert.ok(entry !== undefined, `nothing registered for ${name}`)
|
||||
react.__render()
|
||||
const element = entry.component(props)
|
||||
if (element === null) return null
|
||||
return element.type(element.props)
|
||||
}
|
||||
|
||||
/** Render the settings card, at its current state. */
|
||||
const render = () => {
|
||||
react.__render()
|
||||
const element = byName.get('settings.plugin.item').component({ scope })
|
||||
return element.type(element.props)
|
||||
}
|
||||
|
||||
return { react, module, meta: byName.get('settings.plugin.item').meta, registered, render, renderSlot }
|
||||
}
|
||||
|
||||
/** Open the card, and hand back a function returning the CURRENT body. */
|
||||
function openCard(render) {
|
||||
render().props.children[0].props.onClick()
|
||||
// Call this after any edit: React re-renders then, and the Save button the
|
||||
// user actually clicks is the one created by that later render.
|
||||
return () => render().props.children[1]
|
||||
}
|
||||
|
||||
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 slots and reports the host namespace', () => {
|
||||
const { module } = makeModule()
|
||||
assert.equal(module.name, 'dsh-recall')
|
||||
assert.deepEqual(module.inject, ['slots'])
|
||||
})
|
||||
|
||||
test('the card is registered under the same key the host half serves', () => {
|
||||
const { meta } = mount(makeScope())
|
||||
assert.equal(meta.name, 'settings.plugin.item')
|
||||
assert.equal(meta.key, 'dsh-recall', 'the host namespace and the card key must match, or nothing renders')
|
||||
assert.equal(typeof meta.inject, 'function')
|
||||
})
|
||||
|
||||
test('the root is an <li>, because the tab renders cards into a <ul>', () => {
|
||||
const { render } = mount(makeScope())
|
||||
assert.equal(render().type, 'li')
|
||||
})
|
||||
|
||||
test('the card starts collapsed and discloses on click', () => {
|
||||
const { render } = mount(makeScope())
|
||||
const closed = render()
|
||||
const [closedHeader, closedBody] = closed.props.children
|
||||
assert.equal(closedHeader.type, 'button')
|
||||
assert.equal(closedHeader.props['aria-expanded'], false, 'the card must start collapsed')
|
||||
assert.equal(closedBody, null, 'a collapsed card renders no body')
|
||||
|
||||
// Click the header, then render again — hook state persists across renders.
|
||||
closedHeader.props.onClick()
|
||||
const [openHeader, openBodyNode] = render().props.children
|
||||
assert.equal(openHeader.props['aria-expanded'], true)
|
||||
assert.notEqual(openBodyNode, null, 'an expanded card renders its controls')
|
||||
assert.equal(openBodyNode.type, 'div')
|
||||
})
|
||||
|
||||
test('every host setting appears as a control', () => {
|
||||
const { render } = mount(makeScope())
|
||||
const body = openCard(render)
|
||||
const labels = find(body(), (node) => node.type === 'input').map((node) => node.props['aria-label'])
|
||||
for (const label of [
|
||||
'Embedding endpoint',
|
||||
'Embedding model',
|
||||
'Embedding dimensions',
|
||||
'Embedding API key',
|
||||
'Lines per excerpt',
|
||||
'Index warm-up budget per search (ms)',
|
||||
]) {
|
||||
assert.ok(labels.includes(label), `expected a control labelled ${label}, saw ${labels.join(' | ')}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('an unavailable namespace renders nothing at all', () => {
|
||||
const { render } = mount(makeScope({ status: 'unavailable' }))
|
||||
assert.equal(render(), null)
|
||||
})
|
||||
|
||||
test('a read-only namespace says so rather than offering a dead form', () => {
|
||||
const { render } = mount(makeScope({ writable: false }))
|
||||
const body = openCard(render)
|
||||
assert.ok(find(body(), (node) => node.props?.role === 'status').length > 0)
|
||||
})
|
||||
|
||||
test('a save writes only the fields the user changed', async () => {
|
||||
const scope = makeScope({ value: { embeddingApiBase: 'http://a:11434', excerptLines: 40 } })
|
||||
const { render } = mount(scope)
|
||||
const body = openCard(render)
|
||||
|
||||
inputFor(body(), 'Lines per excerpt').props.onChange({ target: { value: '25' } })
|
||||
const button = saveButton(body())
|
||||
assert.match(String(button.props.children), /Save 1 change/)
|
||||
|
||||
await button.props.onClick()
|
||||
assert.deepEqual(scope.calls, [{ op: 'set', key: 'excerptLines', value: 25 }])
|
||||
})
|
||||
|
||||
test('clearing a non-secret field unsets it rather than writing an empty value', async () => {
|
||||
const scope = makeScope({ value: { embeddingApiBase: 'http://a:11434' } })
|
||||
const { render } = mount(scope)
|
||||
const body = openCard(render)
|
||||
|
||||
inputFor(body(), 'Embedding endpoint').props.onChange({ target: { value: '' } })
|
||||
await saveButton(body()).props.onClick()
|
||||
|
||||
assert.deepEqual(scope.calls, [{ op: 'unset', key: 'embeddingApiBase' }])
|
||||
})
|
||||
|
||||
test('a list field round-trips as a comma separated string', async () => {
|
||||
const scope = makeScope({ value: { exclude: ['dist/', 'vendor/'] } })
|
||||
const { render } = mount(scope)
|
||||
const body = openCard(render)
|
||||
|
||||
const input = inputFor(body(), 'Extra exclude patterns')
|
||||
assert.equal(input.props.value, 'dist/, vendor/')
|
||||
input.props.onChange({ target: { value: 'dist/, vendor/, tmp/' } })
|
||||
await saveButton(body()).props.onClick()
|
||||
|
||||
assert.deepEqual(scope.calls, [{ op: 'set', key: 'exclude', value: ['dist/', 'vendor/', 'tmp/'] }])
|
||||
})
|
||||
|
||||
test('the API key is never sent back to the page, and blank does not clear it', async () => {
|
||||
// The host marks this field role('secret'), so it arrives absent. A save with
|
||||
// the field untouched must not write anything for it — otherwise opening the
|
||||
// card and pressing Save would delete the key.
|
||||
const scope = makeScope({ value: { embeddingApiBase: 'http://a:11434' }, user: { embeddingApiKey: 'stored' } })
|
||||
const { render } = mount(scope)
|
||||
const body = openCard(render)
|
||||
|
||||
const keyInput = inputFor(body(), 'Embedding API key')
|
||||
assert.ok(keyInput !== undefined)
|
||||
assert.equal(keyInput.props.type, 'password')
|
||||
assert.equal(keyInput.props.value, '', 'a redacted secret must render blank')
|
||||
assert.equal(keyInput.props.placeholder, '(unchanged)')
|
||||
|
||||
// Nothing is dirty, so there is nothing to save and the button is inert.
|
||||
const button = saveButton(body())
|
||||
assert.equal(button.props.disabled, true)
|
||||
await button.props.onClick()
|
||||
assert.deepEqual(scope.calls, [], 'an untouched secret must never be written or unset')
|
||||
})
|
||||
|
||||
test('an unsaved edit is marked on the header while collapsed', () => {
|
||||
const { render } = mount(makeScope({ value: { excerptLines: 40 } }))
|
||||
const body = openCard(render)
|
||||
inputFor(body(), 'Lines per excerpt').props.onChange({ target: { value: '25' } })
|
||||
|
||||
// Collapse again and look at the header.
|
||||
render().props.children[0].props.onClick()
|
||||
const collapsed = render()
|
||||
const marked = find(collapsed.props.children[0], (node) => node.props?.children === 'unsaved')
|
||||
assert.equal(marked.length, 1, 'unsaved edits must be visible while the card is folded away')
|
||||
})
|
||||
|
||||
test('the browser half contributes four slots, one of them the header corner', () => {
|
||||
const { registered } = mount(makeScope())
|
||||
const names = registered.map((entry) => entry.meta.name).sort()
|
||||
assert.deepEqual(names, [
|
||||
'conversation.session.header.corner',
|
||||
'settings.plugin.item',
|
||||
'sidebar.right.pane.tab',
|
||||
'sidebar.right.pane.tab.title',
|
||||
])
|
||||
})
|
||||
|
||||
/** 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(makeScope())
|
||||
// The corner is laid out only while its occupant renders something, so this
|
||||
// is the state that must cost no space at all.
|
||||
assert.equal(renderSlot('conversation.session.header.corner', { useProjection: () => undefined }), null)
|
||||
assert.equal(renderSlot('conversation.session.header.corner', { useProjection: () => usageView({ searches: 0, excerpts: 0 }) }), null)
|
||||
// And a host without the projection service at all.
|
||||
assert.equal(renderSlot('conversation.session.header.corner', {}), null)
|
||||
})
|
||||
|
||||
test('the header chip leads with what was kept out, not what it cost', () => {
|
||||
const { renderSlot } = mount(makeScope())
|
||||
const chip = renderSlot('conversation.session.header.corner', { 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(makeScope())
|
||||
const chip = renderSlot('conversation.session.header.corner', { 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(makeScope())
|
||||
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(makeScope())
|
||||
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(makeScope())
|
||||
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')
|
||||
})
|
||||
|
||||
test('the tab title is registered under the same key as the panel', () => {
|
||||
const { registered } = mount(makeScope())
|
||||
const panel = registered.find((entry) => entry.meta.name === 'sidebar.right.pane.tab')
|
||||
const title = registered.find((entry) => entry.meta.name === 'sidebar.right.pane.tab.title')
|
||||
assert.equal(panel.meta.key, 'dsh-recall')
|
||||
assert.equal(title.meta.key, panel.meta.key, 'the title must bind to the panel it names')
|
||||
})
|
||||
Reference in new issue
Block a user