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

+123
View File
@@ -0,0 +1,123 @@
/**
* Shared test scaffolding.
*
* The embedding stub is a bag-of-words hash rather than the real model, for two
* reasons: the suite must run without a network, and a deterministic vector
* makes a ranking assertion a statement about the ranking code rather than a
* statement about a model that may change under it. Whether the real model
* retrieves the right file is a different question, answered by
* `scripts/smoke.mjs` against a live endpoint.
*/
import { createHash } from 'node:crypto'
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
/** Deterministic unit vector for a piece of text. */
export function hashEmbed(text, width) {
const vector = new Float32Array(width)
for (const token of String(text).toLowerCase().split(/[^a-z0-9]+/)) {
if (token.length < 3) continue
const digest = createHash('sha1').update(token).digest()
const slot = ((digest[0] << 8) | digest[1]) % width
vector[slot] += 1
}
let sum = 0
for (let at = 0; at < width; at += 1) sum += vector[at] * vector[at]
if (sum === 0) return vector
const scale = 1 / Math.sqrt(sum)
for (let at = 0; at < width; at += 1) vector[at] *= scale
return vector
}
/** A `fetch` that answers the Ollama embedding API deterministically. */
export function makeStubFetch(options = {}) {
const { width = 768, onCall, unsupportedBatch = false, generate = undefined } = options
const calls = []
const impl = async (url, init) => {
const address = String(url)
const body = JSON.parse(init.body)
calls.push({ url: address, body })
onCall?.(address, body)
// The generation endpoint the expander and the re-ranker share. Left
// unanswered unless a test supplies a response, so a caller that expects
// the local model to be absent gets that for free.
if (address.endsWith('/api/generate')) {
if (generate === undefined) return { ok: false, status: 404, text: async () => 'no model', json: async () => ({}) }
const response = typeof generate === 'function' ? generate(body) : generate
return { ok: true, status: 200, json: async () => ({ response }) }
}
if (address.endsWith('/api/embed')) {
if (unsupportedBatch) {
return { ok: false, status: 404, text: async () => 'not found', json: async () => ({}) }
}
const inputs = body.input
return {
ok: true,
status: 200,
json: async () => ({ embeddings: inputs.map((text) => Array.from(hashEmbed(text, width))) }),
}
}
if (address.endsWith('/api/embeddings')) {
return {
ok: true,
status: 200,
json: async () => ({ embedding: Array.from(hashEmbed(body.prompt, width)) }),
}
}
return { ok: false, status: 404, text: async () => 'unknown endpoint', json: async () => ({}) }
}
impl.calls = calls
return impl
}
/**
* The smallest context shape `apply` actually touches.
*
* `services` mounts optional dependencies: `ctx.inject` invokes its callback
* only when every named dependency is present, which is how the plugin decides
* whether it can offer a settings namespace or a projection. Passing none
* exercises the composition-config path every minimal profile takes.
*/
export function makeContext(options = {}) {
const { services = {} } = options
const tools = []
const cleanups = []
const ctx = {
logger: () => ({ warn() {}, info() {}, error() {}, debug() {} }),
effect: (fn) => {
const dispose = fn()
if (typeof dispose === 'function') cleanups.push(dispose)
},
get: (name) => services[name],
inject: (deps, callback) => {
const missing = deps.filter((dep) => services[dep] === undefined)
if (missing.length > 0) return
callback({ ...ctx, ...Object.fromEntries(deps.map((dep) => [dep, services[dep]])) })
},
tools: { register: (tool) => tools.push(tool) },
}
return { ctx, tools, dispose: () => cleanups.forEach((fn) => fn()) }
}
/** An `exec` argument for one tool call. */
export function makeExec(cwd, signal) {
return { signal: signal ?? new AbortController().signal, agent: { session: { header: { cwd } } } }
}
/** Write a map of relative path to content into a fresh temporary directory. */
export async function makeWorkspace(files) {
const root = await mkdtemp(join(tmpdir(), 'recall-test-'))
for (const [path, content] of Object.entries(files)) {
const absolute = join(root, path)
await mkdir(dirname(absolute), { recursive: true })
await writeFile(absolute, content, 'utf8')
}
return root
}
/** A fresh temporary directory, for index state. */
export async function makeStateDir() {
return mkdtemp(join(tmpdir(), 'recall-state-'))
}