Test the probe and the card
The probe tests use an injected fetch, so they cover the shapes that matter without a network: a matching width, a mismatch, an HTTP error, an unreachable host, a timeout, and the key being sent only when there is one. The client tests evaluate the bundle the way the real module loader does — inside `window.__ModuleLoader__.load` — with a fake `require` that answers `react` and throws on anything else. That throw is the point: it is the check that the card 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 expanding its element tree, and its handlers are driven directly to prove that a changed field reaches `set`, a cleared field reaches `unset`, a number field is sent as a number, a list round-trips through comma-separated text, and a read-only namespace disables everything. The status test moves with the behaviour: status may now run a read-only `--version`, so the assertion is that it executes nothing else.
This commit is contained in:
1 parent
4bb19af2ac
commit
0028349d53
3 files changed
+441
-3
No files matched your search
@@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
})
|
||||||
+6
-3
@@ -422,10 +422,13 @@ test('a workspace-write session with stateDir inside the workspace proceeds', as
|
|||||||
assert.deepEqual(state.shellRequests[0].sandboxPolicy, { mode: 'workspace-write', workspaceRoot: workspace })
|
assert.deepEqual(state.shellRequests[0].sandboxPolicy, { mode: 'workspace-write', workspaceRoot: workspace })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('configure and status never touch the shell, so they work under any sandbox', async () => {
|
test('configure never touches the shell, and status only runs a read-only version check', async () => {
|
||||||
const { state } = harness({}, { sandboxMode: 'workspace-write' })
|
const { state } = harness({}, { sandboxMode: 'workspace-write' })
|
||||||
const tool = state.tools.get('slopo')
|
const tool = state.tools.get('slopo')
|
||||||
await tool.execute({ action: 'status' }, execContext('/some/workspace'))
|
|
||||||
await tool.execute({ action: 'configure', config: { embeddingModel: 'x' } }, execContext('/some/workspace'))
|
await tool.execute({ action: 'configure', config: { embeddingModel: 'x' } }, execContext('/some/workspace'))
|
||||||
assert.deepEqual(state.shellRequests, [])
|
assert.deepEqual(state.shellRequests, [], 'configure must not execute anything')
|
||||||
|
await tool.execute({ action: 'status' }, execContext('/some/workspace'))
|
||||||
|
for (const request of state.shellRequests) {
|
||||||
|
assert.match(request.command, /--version/, 'status may only run a read-only version check')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { test } from 'node:test'
|
||||||
|
|
||||||
|
import { bareModelName, embeddingUrl, probeEmbeddingEndpoint } from '../lib/probe.js'
|
||||||
|
|
||||||
|
/** A fetch stand-in returning one canned response. */
|
||||||
|
function fakeFetch({ ok = true, status = 200, body = {}, throws, onCall } = {}) {
|
||||||
|
return async (url, options) => {
|
||||||
|
if (onCall !== undefined) onCall(url, options)
|
||||||
|
if (throws !== undefined) throw throws
|
||||||
|
return {
|
||||||
|
ok,
|
||||||
|
status,
|
||||||
|
async text() {
|
||||||
|
return typeof body === 'string' ? body : JSON.stringify(body)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('bareModelName strips a known provider prefix', () => {
|
||||||
|
assert.equal(bareModelName('ollama/unclemusclez/jina-embeddings-v2-base-code:latest'), 'unclemusclez/jina-embeddings-v2-base-code:latest')
|
||||||
|
assert.equal(bareModelName('openai/text-embedding-3-small'), 'text-embedding-3-small')
|
||||||
|
assert.equal(bareModelName('hosted_vllm/meta-llama/Llama-3-8B'), 'meta-llama/Llama-3-8B')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bareModelName leaves a model that merely contains a slash alone', () => {
|
||||||
|
// An unknown first segment is far more likely to be part of the model name
|
||||||
|
// than a provider prefix, and mangling it would make the probe lie.
|
||||||
|
assert.equal(bareModelName('unclemusclez/jina-embeddings-v2-base-code:latest'), 'unclemusclez/jina-embeddings-v2-base-code:latest')
|
||||||
|
assert.equal(bareModelName('text-embedding-3-small'), 'text-embedding-3-small')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('embeddingUrl tolerates a base that already carries the version segment', () => {
|
||||||
|
assert.equal(embeddingUrl('http://host:11434'), 'http://host:11434/v1/embeddings')
|
||||||
|
assert.equal(embeddingUrl('http://host:11434/'), 'http://host:11434/v1/embeddings')
|
||||||
|
assert.equal(embeddingUrl('http://host:11434/v1'), 'http://host:11434/v1/embeddings')
|
||||||
|
assert.equal(embeddingUrl(' http://host:11434/v2/ '), 'http://host:11434/v2/embeddings')
|
||||||
|
assert.equal(embeddingUrl(''), '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('probeEmbeddingEndpoint reports a matching dimension as ok', async () => {
|
||||||
|
let seen
|
||||||
|
const result = await probeEmbeddingEndpoint({
|
||||||
|
apiBase: 'http://host:11434',
|
||||||
|
model: 'ollama/unclemusclez/jina:latest',
|
||||||
|
dimensions: 768,
|
||||||
|
fetchImpl: fakeFetch({
|
||||||
|
body: { data: [{ embedding: new Array(768).fill(0.1) }] },
|
||||||
|
onCall: (url, options) => {
|
||||||
|
seen = { url, options }
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
assert.equal(result.ok, true)
|
||||||
|
assert.equal(result.dimensions, 768)
|
||||||
|
assert.equal(result.expected, 768)
|
||||||
|
assert.equal(result.url, 'http://host:11434/v1/embeddings')
|
||||||
|
// The prefix is LiteLLM's; the endpoint must receive the bare model name.
|
||||||
|
assert.equal(JSON.parse(seen.options.body).model, 'unclemusclez/jina:latest')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('probeEmbeddingEndpoint catches a dimension mismatch, which is what slopo freezes', async () => {
|
||||||
|
const result = await probeEmbeddingEndpoint({
|
||||||
|
apiBase: 'http://host:11434',
|
||||||
|
model: 'ollama/m:latest',
|
||||||
|
dimensions: 1024,
|
||||||
|
fetchImpl: fakeFetch({ body: { data: [{ embedding: new Array(768).fill(0) }] } }),
|
||||||
|
})
|
||||||
|
assert.equal(result.ok, false)
|
||||||
|
assert.equal(result.dimensions, 768)
|
||||||
|
assert.equal(result.expected, 1024)
|
||||||
|
assert.match(result.error, /returns 768 dimensions but embeddingDimensions is 1024/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('probeEmbeddingEndpoint reports an HTTP error instead of throwing', async () => {
|
||||||
|
const result = await probeEmbeddingEndpoint({
|
||||||
|
apiBase: 'http://host:11434',
|
||||||
|
model: 'ollama/m:latest',
|
||||||
|
dimensions: 768,
|
||||||
|
fetchImpl: fakeFetch({ ok: false, status: 404, body: 'model not found' }),
|
||||||
|
})
|
||||||
|
assert.equal(result.ok, false)
|
||||||
|
assert.equal(result.status, 404)
|
||||||
|
assert.match(result.error, /model not found/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('probeEmbeddingEndpoint reports an unreachable endpoint instead of throwing', async () => {
|
||||||
|
const result = await probeEmbeddingEndpoint({
|
||||||
|
apiBase: 'http://host:11434',
|
||||||
|
model: 'ollama/m:latest',
|
||||||
|
dimensions: 768,
|
||||||
|
fetchImpl: fakeFetch({ throws: new Error('connect ECONNREFUSED') }),
|
||||||
|
})
|
||||||
|
assert.equal(result.ok, false)
|
||||||
|
assert.equal(result.reachable, false)
|
||||||
|
assert.match(result.error, /ECONNREFUSED/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('probeEmbeddingEndpoint reports a timeout as a timeout', async () => {
|
||||||
|
const abort = new Error('aborted')
|
||||||
|
abort.name = 'AbortError'
|
||||||
|
const result = await probeEmbeddingEndpoint({
|
||||||
|
apiBase: 'http://host:11434',
|
||||||
|
model: 'm',
|
||||||
|
dimensions: 768,
|
||||||
|
timeoutMs: 25,
|
||||||
|
fetchImpl: fakeFetch({ throws: abort }),
|
||||||
|
})
|
||||||
|
assert.equal(result.ok, false)
|
||||||
|
assert.match(result.error, /no response within 25ms/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('probeEmbeddingEndpoint skips cleanly with nothing to probe', async () => {
|
||||||
|
const result = await probeEmbeddingEndpoint({ apiBase: '', model: 'm', dimensions: 768 })
|
||||||
|
assert.equal(result.checked, false)
|
||||||
|
assert.match(result.skipped, /no embeddingApiBase/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('probeEmbeddingEndpoint sends the key as a bearer token only when there is one', async () => {
|
||||||
|
const seen = []
|
||||||
|
const call = (apiKey) =>
|
||||||
|
probeEmbeddingEndpoint({
|
||||||
|
apiBase: 'http://host',
|
||||||
|
model: 'm',
|
||||||
|
dimensions: 1,
|
||||||
|
apiKey,
|
||||||
|
fetchImpl: fakeFetch({
|
||||||
|
body: { data: [{ embedding: [1] }] },
|
||||||
|
onCall: (_url, options) => seen.push(options.headers),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
await call('secret-key')
|
||||||
|
await call('')
|
||||||
|
assert.equal(seen[0].authorization, 'Bearer secret-key')
|
||||||
|
assert.equal(Object.hasOwn(seen[1], 'authorization'), false)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user