// Tests for backend/search-handler.js — the CMD_SEARCH local-first +
// background-federate contract (extracted from index.js so it's Node-testable).
import test from 'node:test'
import assert from 'node:assert/strict'
import handlerMod from '../backend/search-handler.js'
const { createSearchHandler, MAX_QUERY_CHARS, MAX_SEARCH_LIMIT } = handlerMod
const tick = () => new Promise((r) => setTimeout(r, 0))
const fakePI = (rows = []) => ({
search: async () => rows.slice(),
stats: async () => ({ docs: rows.length }),
})
test('returns local results synchronously with first-paint shape; no event when not federating', async () => {
const rows = [{ docId: 'a', driveKey: 'd1' }]
const events = []
const h = createSearchHandler({
getPersonalIndex: () => fakePI(rows),
getQueryPlanner: () => ({ planAndSearch: async () => ({ results: [] }) }),
emit: (e) => events.push(e),
})
const res = await h({ query: 'x' }) // no federated flag
assert.deepEqual(res.results, rows)
assert.equal(res.phase, 'first-paint')
assert.equal(res.federating, false)
assert.equal(res.stats.docs, 1)
assert.equal(typeof res.queryId, 'number')
await tick()
assert.equal(events.length, 0) // background path never ran
})
test('federated:true emits exactly one enriched event with the planner results + matching queryId', async () => {
const rows = [{ docId: 'a' }]
const fedRows = [{ docId: 'a', tier: 'self' }, { docId: 'b', tier: 'followed' }]
const events = []
const h = createSearchHandler({
getPersonalIndex: () => fakePI(rows),
getQueryPlanner: () => ({
planAndSearch: async () => ({
results: fedRows,
verifyBudgetExhausted: false,
digestHit: true,
fallbackPull: false,
partial: true,
provenance: { digestHit: true, fallbackPull: false, partial: true, plannedPeers: 2, pulledPeers: 1, digestSkipped: 1 }
})
}),
emit: (e) => events.push(e),
})
const res = await h({ query: 'x', federated: true })
assert.equal(res.federating, true)
assert.deepEqual(res.results, rows) // first paint is still local-only
await tick()
assert.equal(events.length, 1)
assert.equal(events[0].phase, 'enriched')
assert.deepEqual(events[0].results, fedRows)
assert.equal(events[0].queryId, res.queryId)
assert.equal(events[0].verifyBudgetExhausted, false)
assert.equal(events[0].digestHit, true)
assert.equal(events[0].fallbackPull, false)
assert.equal(events[0].partial, true)
assert.deepEqual(events[0].provenance, { digestHit: true, fallbackPull: false, partial: true, plannedPeers: 2, pulledPeers: 1, digestSkipped: 1 })
})
test('federating is false when no planner exists, even with federated:true', async () => {
const events = []
const h = createSearchHandler({
getPersonalIndex: () => fakePI([]),
getQueryPlanner: () => null,
emit: (e) => events.push(e),
})
const res = await h({ query: 'x', federated: true })
assert.equal(res.federating, false)
await tick()
assert.equal(events.length, 0)
})
test('no personalIndex yields an empty first-paint result', async () => {
const h = createSearchHandler({
getPersonalIndex: () => null,
getQueryPlanner: () => null,
emit: () => {},
})
const res = await h({ query: 'x', federated: true })
assert.deepEqual(res.results, [])
assert.equal(res.federating, false)
assert.equal(res.phase, 'first-paint')
assert.equal(res.stats.docs, 0)
})
test('normalizes query text and clamps limit before local and federated search', async () => {
const calls = []
const plannerCalls = []
const pi = {
search: async (query, opts) => { calls.push({ query, opts }); return [] },
stats: async () => ({ docs: 0 }),
}
const planner = {
planAndSearch: async (query, opts) => { plannerCalls.push({ query, opts }); return { results: [] } },
}
const h = createSearchHandler({
getPersonalIndex: () => pi,
getQueryPlanner: () => planner,
emit: () => {},
})
await h({ query: ` ${'x'.repeat(MAX_QUERY_CHARS + 40)} `, limit: 100000, federated: true })
await tick()
assert.equal(calls[0].query.length, MAX_QUERY_CHARS)
assert.equal(calls[0].opts.limit, MAX_SEARCH_LIMIT)
assert.equal(plannerCalls[0].query.length, MAX_QUERY_CHARS)
assert.equal(plannerCalls[0].opts.limit, MAX_SEARCH_LIMIT)
})
test('a stale federation from a superseded query is dropped; only the newest emits', async () => {
const events = []
const deferreds = []
const planner = { planAndSearch: () => new Promise((resolve) => deferreds.push(resolve)) }
const h = createSearchHandler({
getPersonalIndex: () => fakePI([]),
getQueryPlanner: () => planner,
emit: (e) => events.push(e),
})
const r1 = await h({ query: 'a', federated: true }) // queryId 1, federation pending
const r2 = await h({ query: 'b', federated: true }) // queryId 2, federation pending
assert.notEqual(r1.queryId, r2.queryId)
deferreds[0]({ results: [{ docId: 'stale' }], verifyBudgetExhausted: false }) // resolve the OLD query
await tick()
assert.equal(events.length, 0) // suppressed — superseded
deferreds[1]({ results: [{ docId: 'fresh' }], verifyBudgetExhausted: false }) // resolve the CURRENT query
await tick()
assert.equal(events.length, 1)
assert.equal(events[0].queryId, r2.queryId)
assert.deepEqual(events[0].results, [{ docId: 'fresh' }])
})