This commit is contained in:
NotExpectedYet committed 2026-09-26 05:35:29 +01:00
commit 0964819cdc
34 files changed
+7912

No files matched your search

+690
View File
@@ -0,0 +1,690 @@
import { strict as assert } from 'node:assert'
import { readFile, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { test } from 'node:test'
import { apply } from '../index.js'
import { makeContext, makeExec, makeStateDir, makeStubFetch, makeWorkspace } from './helpers.js'
/**
* Validate a value against the tool's own declared output schema.
*
* This exists because the schema is `additionalProperties: false` and the host
* enforces it before the model ever sees a result — so an internal field leaking
* into the returned object is not a cosmetic extra, it is a failed call. The
* check is deliberately written against the declared schema rather than a copy
* of it, so the two cannot drift apart.
*/
function schemaErrors(schema, value, at = '$') {
const errors = []
if (schema.oneOf !== undefined) {
if (!schema.oneOf.some((branch) => schemaErrors(branch, value, at).length === 0)) {
errors.push(`${at}: matched no oneOf branch`)
}
return errors
}
const type = schema.type
if (type === 'object') {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
errors.push(`${at}: expected an object`)
return errors
}
const properties = schema.properties ?? {}
for (const [key, sub] of Object.entries(properties)) {
if (sub.required === true && value[key] === undefined) errors.push(`${at}.${key}: required but missing`)
}
if (schema.additionalProperties === false) {
for (const key of Object.keys(value)) {
if (!(key in properties)) errors.push(`${at}.${key}: not declared, but additionalProperties is false`)
}
}
for (const [key, sub] of Object.entries(properties)) {
if (value[key] !== undefined) errors.push(...schemaErrors(sub, value[key], `${at}.${key}`))
}
return errors
}
if (type === 'array') {
if (!Array.isArray(value)) {
errors.push(`${at}: expected an array`)
return errors
}
value.forEach((item, index) => errors.push(...schemaErrors(schema.items ?? {}, item, `${at}[${index}]`)))
return errors
}
if (type === 'json') return errors
if (type === 'string' && typeof value !== 'string') errors.push(`${at}: expected a string`)
if (type === 'boolean' && typeof value !== 'boolean') errors.push(`${at}: expected a boolean`)
if (type === 'integer' && !Number.isInteger(value)) errors.push(`${at}: expected an integer`)
if (type === 'number' && typeof value !== 'number') errors.push(`${at}: expected a number`)
if (schema.enum !== undefined && !schema.enum.includes(value)) errors.push(`${at}: not one of ${schema.enum.join(', ')}`)
return errors
}
const SESSION = `import { randomBytes } from 'node:crypto'
const SESSION_TTL_MS = 1000 * 60 * 60 * 12
export interface SessionRecord {
userId: string
issuedAt: number
expiresAt: number
}
export function issueSession(userId: string): SessionRecord {
const now = Date.now()
return { userId, issuedAt: now, expiresAt: now + SESSION_TTL_MS }
}
export function isSessionExpired(record: SessionRecord, now = Date.now()): boolean {
return record.expiresAt <= now
}
export function rotateSessionToken(record: SessionRecord): string {
if (isSessionExpired(record)) throw new Error('cannot rotate an expired session')
return randomBytes(32).toString('hex')
}
export function sessionTimeRemaining(record: SessionRecord, now = Date.now()): number {
return Math.max(0, record.expiresAt - now)
}
export function describeSession(record: SessionRecord): string {
const remaining = sessionTimeRemaining(record)
return remaining === 0 ? 'expired' : 'active for ' + remaining + 'ms'
}
`
const FORECAST = `export interface ForecastDay {
date: string
minC: number
maxC: number
rainMm: number
}
export function averageTemperature(days: ForecastDay[]): number {
if (days.length === 0) return 0
return days.reduce((sum, day) => sum + (day.minC + day.maxC) / 2, 0) / days.length
}
export function totalRainfall(days: ForecastDay[]): number {
return days.reduce((sum, day) => sum + day.rainMm, 0)
}
`
const SQLITE = `import { DatabaseSync } from 'node:sqlite'
export function openDatabase(path: string): DatabaseSync {
const db = new DatabaseSync(path)
db.exec('PRAGMA journal_mode = WAL')
return db
}
`
const FILES = {
'src/auth/session.ts': SESSION,
'src/weather/forecast.ts': FORECAST,
'src/store/sqlite.ts': SQLITE,
}
function configFor(stateDir, extra = {}) {
return {
embeddingApiBase: 'http://stub',
embeddingModel: 'test-model',
embeddingDimensions: 768,
embeddingApiKey: '',
embeddingBatchSize: 32,
embeddingConcurrency: 2,
embeddingTimeoutMs: 5_000,
extensions: [],
exclude: [],
maxFiles: 500,
maxFileBytes: 1_048_576,
chunkLines: 48,
maxChunkLines: 80,
overlapLines: 8,
defaultLimit: 8,
maxLimit: 25,
perFileLimit: 2,
excerptLines: 40,
minSimilarity: 0.2,
syncBudgetMs: 20_000,
// These mirror the Config schema exactly. Cordis fills schema defaults
// before `apply`, and this object stands in for that validated config — so
// any key the code reads and this object omits arrives as `undefined`, which
// is how `AbortSignal.timeout(NaN)` came to throw inside the expander.
focus: '',
focusMarkers: [],
expansionEnabled: false,
expansionModel: '',
expansionCount: 4,
expansionTimeoutMs: 8_000,
rerankEnabled: false,
rerankModel: '',
rerankCandidates: 8,
rerankTimeoutMs: 15_000,
stateDir,
...extra,
}
}
/** Apply the plugin to a fresh workspace with a stubbed embedding endpoint. */
async function setup(t, files = FILES, extra = {}, stubOptions = {}) {
const workspace = await makeWorkspace(files)
const stateDir = await makeStateDir()
const stub = makeStubFetch(stubOptions)
const originalFetch = globalThis.fetch
globalThis.fetch = stub
t.after(async () => {
globalThis.fetch = originalFetch
await rm(workspace, { recursive: true, force: true })
await rm(stateDir, { recursive: true, force: true })
})
const { ctx, tools, dispose } = makeContext()
t.after(() => dispose())
apply(ctx, configFor(stateDir, extra))
assert.equal(tools.length, 1, 'exactly one tool should be registered')
return { tool: tools[0], workspace, stub }
}
test('the plugin registers one tool whose declared schema is closed', async (t) => {
const { tool } = await setup(t)
assert.equal(tool.name, 'recall')
assert.equal(tool.output.schema.additionalProperties, false)
// defineTool normalises the parameter DSL into a JSON Schema object.
assert.deepEqual(tool.parameters.properties.action.enum, ['search', 'index', 'status', 'configure'])
assert.equal(tool.parameters.properties.query.type, 'string')
// A long index must not run in parallel with sibling tool calls.
assert.equal(tool.isConcurrencySafe({ action: 'search' }), true)
assert.equal(tool.isConcurrencySafe({ action: 'index' }), false)
})
test('a search returns the right file, as an excerpt rather than a whole file', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute(
{ query: 'how long until a user session token expires' },
makeExec(workspace),
)
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.ok, true)
assert.equal(value.results.length > 0, true)
assert.equal(value.results[0].path, 'src/auth/session.ts')
// The excerpt must be a real slice of the file on disk, not paraphrase.
const onDisk = await readFile(join(workspace, 'src/auth/session.ts'), 'utf8')
assert.ok(onDisk.includes(value.results[0].excerpt), 'the excerpt must come from the file')
assert.ok(
value.results[0].excerpt.split('\n').length <= 40,
'the excerpt must respect the configured line cap',
)
assert.ok(
value.results[0].excerpt.length < onDisk.length,
'the point of the tool is to return less than the whole file',
)
})
test('a second search re-embeds nothing, because the index is incremental', async (t) => {
const { tool, workspace, stub } = await setup(t)
await tool.execute({ query: 'session expiry' }, makeExec(workspace))
const afterFirst = stub.calls.length
assert.ok(afterFirst > 1, 'the first search should have embedded the workspace')
await tool.execute({ query: 'rainfall totals' }, makeExec(workspace))
const second = stub.calls.slice(afterFirst)
// Exactly one request: the query vector. A warm index embeds no files.
assert.equal(second.length, 1, `expected only a query embed, saw ${second.length} requests`)
})
test('an edited file is re-embedded on the next search', async (t) => {
const { tool, workspace, stub } = await setup(t)
await tool.execute({ query: 'session expiry' }, makeExec(workspace))
const afterFirst = stub.calls.length
await writeFile(
join(workspace, 'src/weather/forecast.ts'),
`${FORECAST}\nexport function humidityPercent(days: ForecastDay[]): number {\n return days.length\n}\n`,
'utf8',
)
await tool.execute({ query: 'humidityPercent' }, makeExec(workspace))
const second = stub.calls.slice(afterFirst)
assert.ok(second.length >= 2, 'the changed file plus the query should both be embedded')
assert.deepEqual(second[second.length - 1].body.input, ['humidityPercent'])
})
test('a spent sync budget leaves the rest pending rather than lying about it', async (t) => {
const { tool, workspace } = await setup(t, FILES, { syncBudgetMs: 0 })
// First search indexes one file, then the budget stops it.
const first = await tool.execute({ query: 'session expiry' }, makeExec(workspace))
assert.ok(first.index.pending >= 1, `expected pending work, saw ${first.index.pending}`)
// A full index action catches up.
const full = await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, full), [])
assert.equal(full.index.pending, 0)
const third = await tool.execute({ query: 'session expiry' }, makeExec(workspace))
assert.equal(third.index.pending, 0)
})
test('the index action builds the whole workspace', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.ok, true)
assert.equal(value.index.files, 3)
assert.ok(value.index.chunks >= 3)
})
test('status probes the endpoint once and reports it healthy', async (t) => {
const { tool, workspace, stub } = await setup(t)
const value = await tool.execute({ action: 'status' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.ok, true)
// Exactly one request: the probe. Status must not index or search.
assert.equal(stub.calls.length, 1, `status should probe once, saw ${stub.calls.length}`)
assert.equal(value.endpoint.ok, true)
assert.match(value.note, /endpoint answered/)
})
test('status reports an endpoint that accepts the connection and never answers', async (t) => {
// The failure mode that a plain reachability check misses, and the one that
// actually happened in development: the socket opens, no bytes ever come back.
const workspace = await makeWorkspace(FILES)
const stateDir = await makeStateDir()
const originalFetch = globalThis.fetch
globalThis.fetch = (url, init) =>
new Promise((_resolve, reject) => {
init.signal.addEventListener('abort', () => reject(init.signal.reason), { once: true })
})
t.after(async () => {
globalThis.fetch = originalFetch
await rm(workspace, { recursive: true, force: true })
await rm(stateDir, { recursive: true, force: true })
})
const { ctx, tools, dispose } = makeContext()
t.after(() => dispose())
// A short timeout keeps the probe honest about how long it will wait.
apply(ctx, configFor(stateDir, { embeddingTimeoutMs: 1_000 }))
const value = await tools[0].execute({ action: 'status' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tools[0].output.schema, value), [])
assert.equal(value.ok, true, 'status itself succeeded; it is reporting a fault, not failing')
assert.equal(value.endpoint.ok, false)
assert.equal(value.endpoint.errorCode, 'UNREACHABLE')
assert.match(value.note, /did NOT answer/)
})
test('a search without a query is refused with advice', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute({}, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'NO_QUERY')
assert.ok(value.remedy !== undefined)
})
test('a path filter restricts results to that directory', async (t) => {
const { tool, workspace } = await setup(t)
await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
const value = await tool.execute(
{ query: 'function', path: 'src/weather' },
makeExec(workspace),
)
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.ok(value.results.length > 0)
for (const result of value.results) assert.match(result.path, /^src\/weather\//)
})
test('configure reports the missing settings provider instead of pretending to save', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute(
{ action: 'configure', config: { embeddingApiBase: 'http://elsewhere:11434' } },
makeExec(workspace),
)
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'NO_SETTINGS_PROVIDER')
assert.match(value.remedy, /cordis layer/)
})
test('configure rejects an unknown setting by name', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute(
{ action: 'configure', config: { nonsense: 1 } },
makeExec(workspace),
)
assert.equal(value.errorCode, 'UNKNOWN_SETTING')
assert.match(value.note, /nonsense/)
})
test('an unreachable endpoint fails with a remedy, not a stack trace', async (t) => {
const workspace = await makeWorkspace(FILES)
const stateDir = await makeStateDir()
const originalFetch = globalThis.fetch
// A persistent 5xx, which the client retries and then reports as unreachable.
globalThis.fetch = async () => ({ ok: false, status: 500, text: async () => 'boom', json: async () => ({}) })
t.after(async () => {
globalThis.fetch = originalFetch
await rm(workspace, { recursive: true, force: true })
await rm(stateDir, { recursive: true, force: true })
})
const { ctx, tools, dispose } = makeContext()
t.after(() => dispose())
apply(ctx, configFor(stateDir))
const value = await tools[0].execute({ query: 'anything at all' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tools[0].output.schema, value), [])
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'UNREACHABLE')
assert.match(value.remedy, /embeddingApiBase/)
})
test('a workspace with no indexable code says so plainly', async (t) => {
const { tool, workspace } = await setup(t, { 'README.md': '# prose only\n' })
const value = await tool.execute({ query: 'anything' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.ok, true)
assert.deepEqual(value.results, [])
assert.match(value.note, /Nothing is indexed/)
})
test('the rendered result is compact and names the file and line range', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute({ query: 'session token rotation' }, makeExec(workspace))
const blocks = tool.output.render({ query: 'session token rotation' }, value)
assert.equal(blocks.length, 1)
assert.equal(blocks[0].type, 'text')
assert.match(blocks[0].text, /src\/auth\/session\.ts:\d+-\d+/)
// It must not dump the whole file into the conversation.
assert.ok(blocks[0].text.length < SESSION.length * 3)
})
test('an empty result tells the model to prefer grep', async (t) => {
const { tool, workspace } = await setup(t)
await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
const value = await tool.execute(
{ query: 'zzzz-nothing-matches-this-zzzz', path: 'src/store' },
makeExec(workspace),
)
const blocks = tool.output.render({ query: 'zzzz' }, value)
if (value.results.length === 0) {
assert.match(blocks[0].text, /grep/)
} else {
assert.equal(value.weak, true)
assert.match(blocks[0].text, /No strong match/)
}
})
test('focus narrows the index to one directory, and paths stay workspace-relative', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute(
{ action: 'index', focus: 'src/auth', run_in_background: false },
makeExec(workspace),
)
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.focus.path, 'src/auth')
assert.equal(value.focus.source, 'explicit')
assert.equal(value.index.files, 1, 'only src/auth should be indexed')
const search = await tool.execute({ query: 'session expiry', focus: 'src/auth' }, makeExec(workspace))
assert.ok(search.results.length > 0)
for (const result of search.results) {
// The critical invariant: a result path must be usable with `read`, which
// resolves against the workspace. A focus-relative path here would read the
// wrong file.
assert.match(result.path, /^src\/auth\//, `path was ${result.path}`)
const onDisk = await readFile(join(workspace, result.path), 'utf8')
assert.ok(onDisk.includes(result.excerpt), `${result.path} should resolve from the workspace root`)
}
})
test('each focus keeps its own index', async (t) => {
const { tool, workspace } = await setup(t)
const auth = await tool.execute({ action: 'index', focus: 'src/auth', run_in_background: false }, makeExec(workspace))
const weather = await tool.execute({ action: 'index', focus: 'src/weather', run_in_background: false }, makeExec(workspace))
assert.equal(auth.index.files, 1)
assert.equal(weather.index.files, 1)
assert.notEqual(auth.index.database, weather.index.database, 'focuses must not share an index file')
assert.notEqual(auth.index.database, (await tool.execute({ action: 'status' }, makeExec(workspace))).index.database)
})
test('a search in one focus cannot see another focus', async (t) => {
const { tool, workspace } = await setup(t)
await tool.execute({ action: 'index', focus: 'src/weather', run_in_background: false }, makeExec(workspace))
// A tolerant query that would match the auth module if it were in scope.
const value = await tool.execute({ query: 'issueSession session expiry', focus: 'src/weather' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
for (const result of value.results) assert.match(result.path, /^src\/weather\//)
})
test('an unusable focus is reported and falls back, never silently obeyed', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute(
{ action: 'index', focus: 'src/does-not-exist', run_in_background: false },
makeExec(workspace),
)
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.focus.path, '.', 'must fall back to the workspace')
assert.match(value.focus.problem, /not a directory/)
assert.equal(value.index.files, 3, 'the fallback index covers the whole workspace')
})
test('status reports which focus is in effect, and why', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute({ action: 'status' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.focus.path, '.')
// This fixture has no AGENTS.md, so resolution reaches the final fallback.
assert.equal(value.focus.source, 'workspace')
assert.match(value.note, /scoped to the whole workspace/)
})
test('a session at the workspace root keeps the whole workspace, even with nested scopes', async (t) => {
// Inheritance and discovery are different questions. The root's own AGENTS.md
// is what contains this session; the controller below is merely available.
// Auto-adopting it would hide the other two thirds of the workspace.
const { tool, workspace } = await setup(t, {
'AGENTS.md': '# repo instructions\n',
'src/auth/CONTROLLER.md': '# the auth controller\n',
...FILES,
})
const value = await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.equal(value.focus.path, '.')
assert.equal(value.focus.source, 'marker')
assert.equal(value.focus.marker, 'AGENTS.md')
assert.equal(value.index.files, 3)
})
test('discovered scopes are listed, so a caller can name one', async (t) => {
const { tool, workspace } = await setup(t, {
'src/auth/CONTROLLER.md': '# the auth controller\n',
'src/weather/CONTROLLER.md': '# the weather controller\n',
...FILES,
})
const value = await tool.execute({ action: 'status' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.deepEqual(value.scopes, [
{ path: 'src/auth', marker: 'CONTROLLER.md' },
{ path: 'src/weather', marker: 'CONTROLLER.md' },
])
assert.match(value.note, /Available scopes: src\/auth, src\/weather/)
})
test('a workspace with no markers says so rather than listing nothing', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute({ action: 'status' }, makeExec(workspace))
assert.deepEqual(value.scopes, [])
assert.match(value.note, /No focusable scopes/)
})
test('a scope found by discovery indexes only itself when named', async (t) => {
const { tool, workspace } = await setup(t, {
'src/auth/CONTROLLER.md': '# the auth controller\n',
...FILES,
})
const listed = await tool.execute({ action: 'status' }, makeExec(workspace))
const [scope] = listed.scopes
const value = await tool.execute({ action: 'index', focus: scope.path, run_in_background: false }, makeExec(workspace))
assert.equal(value.focus.source, 'explicit')
assert.equal(value.index.files, 1, 'naming a discovered scope must confine the index to it')
})
test('the rendered search header names the scope when there is one', async (t) => {
const { tool, workspace } = await setup(t)
await tool.execute({ action: 'index', focus: 'src/auth', run_in_background: false }, makeExec(workspace))
const value = await tool.execute({ query: 'session', focus: 'src/auth' }, makeExec(workspace))
const blocks = tool.output.render({ query: 'session' }, value)
assert.match(blocks[0].text, /in src\/auth/)
})
test('the tool records what it saved, for the session log to carry', async (t) => {
const { tool, workspace } = await setup(t)
await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
const value = await tool.execute({ query: 'session expiry' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
// The comparison is against the whole of each file the excerpts came from.
assert.ok(value.measure.sourceBytes > 0)
assert.ok(
value.measure.sourceBytes > value.measure.returnedBytes,
`expected a saving, got ${value.measure.returnedBytes} of ${value.measure.sourceBytes}`,
)
assert.ok(value.measure.files >= 1)
const meta = tool.output.presentationMeta({ query: 'session expiry' }, value)
assert.equal(meta.recall.action, 'search')
assert.equal(meta.recall.excerpts, value.results.length)
assert.equal(meta.recall.sourceBytes, value.measure.sourceBytes)
assert.equal(meta.recall.returnedBytes, value.measure.returnedBytes)
// Pure: replayed on projection rebuilds, so it may not vary between calls.
assert.deepEqual(tool.output.presentationMeta({ query: 'session expiry' }, value), meta)
// And JSON-safe, because this is what actually lands in the log.
assert.deepEqual(JSON.parse(JSON.stringify(meta)), meta)
})
test('an index call records differently from a search', async (t) => {
const { tool, workspace } = await setup(t)
const value = await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
const meta = tool.output.presentationMeta({ action: 'index' }, value)
assert.equal(meta.recall.action, 'index')
assert.equal(meta.recall.excerpts, undefined, 'indexing returns no excerpts to account for')
})
test('a failed search still leaves a record worth folding', async (t) => {
const workspace = await makeWorkspace(FILES)
const stateDir = await makeStateDir()
const originalFetch = globalThis.fetch
globalThis.fetch = async () => ({ ok: false, status: 500, text: async () => 'boom', json: async () => ({}) })
t.after(async () => {
globalThis.fetch = originalFetch
await rm(workspace, { recursive: true, force: true })
await rm(stateDir, { recursive: true, force: true })
})
const { ctx, tools, dispose } = makeContext()
t.after(() => dispose())
apply(ctx, configFor(stateDir))
const value = await tools[0].execute({ query: 'anything' }, makeExec(workspace))
const meta = tools[0].output.presentationMeta({ query: 'anything' }, value)
assert.equal(meta.recall.failed, 1, 'a failure should be counted, not silently dropped')
})
test('the usage projection is registered when the service is mounted', async (t) => {
const registered = []
const { ctx, tools, dispose } = makeContext({
services: { sessionProjections: { register: (unit) => registered.push(unit) } },
})
t.after(() => dispose())
apply(ctx, configFor('/tmp/dsh-recall-projection-test'))
assert.equal(tools.length, 1, 'the tool registers regardless')
assert.equal(registered.length, 1, 'exactly one projection unit')
assert.equal(registered[0].key, 'recallUsage', 'the key the client reads')
assert.equal(typeof registered[0].apply, 'function')
assert.equal(typeof registered[0].wire.view, 'function', 'the unit must be client-visible')
assert.equal(typeof registered[0].wire.viewSchema.parse, 'function')
})
test('a profile without projections still gets a working tool', async (t) => {
// The projection is an optional surface, not a dependency of retrieval: a
// composition that mounts no projection service must lose the reporting and
// nothing else.
const { ctx, tools, dispose } = makeContext()
t.after(() => dispose())
apply(ctx, configFor('/tmp/dsh-recall-no-projection'))
assert.equal(tools.length, 1)
assert.equal(tools[0].name, 'recall')
})
test('a projection that refuses to register is reported, not fatal', async (t) => {
const { ctx, tools, dispose } = makeContext({
services: {
sessionProjections: {
register: () => {
throw new Error('duplicate key')
},
},
},
})
t.after(() => dispose())
// The warning is logged rather than thrown: a conflicting unit must not take
// the whole plugin, and therefore retrieval, down with it.
assert.doesNotThrow(() => apply(ctx, configFor('/tmp/dsh-recall-bad-projection')))
assert.equal(tools.length, 1)
})
test('expansion is off by default and changes nothing', async (t) => {
const { tool, workspace, stub } = await setup(t)
await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
const value = await tool.execute({ query: 'session expiry' }, makeExec(workspace))
assert.ok(value.results.length > 0)
assert.equal(
stub.calls.filter((call) => call.url.endsWith('/api/generate')).length,
0,
'the local model must not be called unless the setting is on',
)
})
test('with expansion on and no model answering, the search still runs', async (t) => {
// The stub only answers /api/generate when a test supplies a response, so this
// is the wedged-endpoint case: expansion must degrade, not fail the search.
const { tool, workspace } = await setup(t, FILES, { expansionEnabled: true, expansionModel: 'local-small' })
await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
const value = await tool.execute({ query: 'session expiry' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.ok(value.results.length > 0, 'a failed expansion must not fail the search')
})
test('expansion asks the model once and still returns results', async (t) => {
const { tool, workspace, stub } = await setup(
t,
FILES,
{ expansionEnabled: true, expansionModel: 'local-small', expansionCount: 3 },
{ generate: 'issueSession\nsession expiry check' },
)
await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
const value = await tool.execute({ query: 'session expiry' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, value), [])
assert.ok(value.results.length > 0)
const generations = stub.calls.filter((call) => call.url.endsWith('/api/generate'))
assert.equal(generations.length, 1, 'one generation for the query, not one per variant')
assert.match(generations[0].body.prompt, /session expiry/)
assert.equal(generations[0].body.model, 'local-small', 'the configured expansion model is used')
})
test('re-ranking degrades to the fused order when the model has no opinion', async (t) => {
const { tool, workspace } = await setup(t, FILES, { rerankEnabled: true, rerankModel: 'local-small' })
await tool.execute({ action: 'index', run_in_background: false }, makeExec(workspace))
const plain = await tool.execute({ query: 'session expiry' }, makeExec(workspace))
const reranked = await tool.execute({ query: 'session expiry' }, makeExec(workspace))
assert.deepEqual(schemaErrors(tool.output.schema, reranked), [])
// No usable scores means the ranking is untouched rather than emptied.
assert.deepEqual(
reranked.results.map((result) => result.path),
plain.results.map((result) => result.path),
)
assert.ok(reranked.results.length > 0, 'no opinion must not mean no results')
})