Files
dsh-recall/scripts/smoke.mjs
T
2026-09-26 05:35:29 +01:00

149 lines
5.4 KiB
JavaScript

/**
* Live smoke test: the real pipeline against the real embedding endpoint.
*
* Not part of `npm test`, because it needs a running Ollama and a model that is
* already pulled. It exists because the unit tests can only prove the code does
* what the test author believed; this proves the retrieval actually returns the
* right file for a question about it, which is the only claim that matters.
*
* node scripts/smoke.mjs [workspace] [query]
*/
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { chunkFile, embedText } from '../lib/chunk.js'
import { createEmbedder } from '../lib/embed.js'
import { rankChunks } from '../lib/rank.js'
import { RecallIndex } from '../lib/store.js'
import { listCodeFiles } from '../lib/walk.js'
const API_BASE = process.env.RECALL_API_BASE ?? 'http://10.50.1.2:11434'
const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest'
const DIMENSIONS = Number(process.env.RECALL_DIMENSIONS ?? 768)
const FIXTURE = {
'src/services/auth/session.ts': `import { createHash, 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 isExpired(record: SessionRecord, now = Date.now()): boolean {
return record.expiresAt <= now
}
export function rotateToken(record: SessionRecord): string {
if (isExpired(record)) throw new Error('cannot rotate an expired session')
return randomBytes(32).toString('hex')
}
export function fingerprint(token: string): string {
return createHash('sha256').update(token).digest('hex').slice(0, 16)
}
`,
'src/services/weather/forecast.ts': `export interface ForecastDay {
date: string
minC: number
maxC: number
rainMm: number
}
export function averageTemperature(days: ForecastDay[]): number {
if (days.length === 0) return 0
const total = days.reduce((sum, day) => sum + (day.minC + day.maxC) / 2, 0)
return total / days.length
}
export function totalRainfall(days: ForecastDay[]): number {
return days.reduce((sum, day) => sum + day.rainMm, 0)
}
`,
'src/store/sqlite.ts': `import { DatabaseSync } from 'node:sqlite'
export function openDatabase(path: string): DatabaseSync {
const db = new DatabaseSync(path)
db.exec('PRAGMA journal_mode = WAL')
return db
}
export function pruneRows(db: DatabaseSync, table: string, olderThan: number): number {
const statement = db.prepare(\`DELETE FROM \${table} WHERE created_at < ?\`)
const result = statement.run(olderThan)
return Number(result.changes)
}
`,
}
/** Write the fixture into a fresh temporary directory. */
async function makeWorkspace() {
const root = await mkdtemp(join(tmpdir(), 'recall-smoke-'))
for (const [path, content] of Object.entries(FIXTURE)) {
const absolute = join(root, path)
await mkdir(join(absolute, '..'), { recursive: true })
await writeFile(absolute, content, 'utf8')
}
return root
}
async function main() {
const query = process.argv[3] ?? 'how long does a user session stay valid before it expires'
const root = process.argv[2] ?? (await makeWorkspace())
const indexDir = await mkdtemp(join(tmpdir(), 'recall-index-'))
const database = join(indexDir, 'index.sqlite')
const embedder = createEmbedder({ apiBase: API_BASE, model: MODEL, dimensions: DIMENSIONS })
const index = new RecallIndex(database)
index.open({ model: MODEL, dimensions: DIMENSIONS, apiBase: API_BASE })
const started = Date.now()
const walk = await listCodeFiles(root)
let chunks = 0
for (const file of walk.files) {
const { readFile } = await import('node:fs/promises')
const content = await readFile(file.absolute, 'utf8')
const split = chunkFile({ path: file.path, content })
if (split.chunks.length === 0) continue
const vectors = await embedder.embed(split.chunks.map((chunk) => embedText(file.path, chunk)))
index.replaceFile(file.path, 'smoke', Buffer.byteLength(content), split.chunks.map((c, at) => ({ ...c, vec: vectors[at] })))
chunks += split.chunks.length
}
const indexMs = Date.now() - started
const matrix = index.matrix()
const queryVector = await embedder.embedOne(query)
const searchStart = Date.now()
const ranked = rankChunks({ matrix, queryVector, query, limit: 5 })
const searchMs = Date.now() - searchStart
console.log(`workspace : ${root}`)
console.log(`files : ${walk.files.length} chunks: ${chunks} indexed in ${indexMs}ms`)
console.log(`query : ${query}`)
console.log(`search : ${searchMs}ms batch endpoint: ${embedder.batchEnabled}`)
console.log('')
for (const [at, result] of ranked.results.entries()) {
console.log(`${at + 1}. ${result.path}:${result.startLine}-${result.endLine} similarity ${result.similarity}`)
console.log(` ${result.excerpt.split('\n')[0]}`)
}
console.log('')
const top = ranked.results[0]
const expected = 'src/services/auth/session.ts'
console.log(top?.path === expected ? `PASS top hit is ${expected}` : `FAIL top hit was ${top?.path}, expected ${expected}`)
index.close()
await rm(indexDir, { recursive: true, force: true })
if (process.argv[2] === undefined) await rm(root, { recursive: true, force: true })
}
await main()