import test from 'node:test'; import assert from 'node:assert/strict'; import { LatestLane, AutomaticIndexGate, emptySearchState } from '../src/search-state.ts'; const settle = () => new Promise(resolve => setImmediate(resolve)); test('coalesces queued keystrokes and discards stale responses', async () => { let revision = 1; let release; const executed = []; const accepted = []; const lane = new LatestLane(async value => { executed.push(value); if (value === 1) await new Promise(resolve => { release = resolve; }); if (value === revision) accepted.push(value); }, value => value === revision, () => assert.fail('Unexpected failure')); lane.submit(1); revision = 2; lane.submit(2); revision = 3; lane.submit(3); release(); await settle(); assert.deepEqual(executed, [1, 3]); assert.deepEqual(accepted, [3]); }); test('Escape invalidates both active and queued work', async () => { let revision = 1; let release; const accepted = []; const lane = new LatestLane(async value => { await new Promise(resolve => { release = resolve; }); if (value === revision) accepted.push(value); }, value => value === revision, () => assert.fail('Unexpected failure')); lane.submit(1); lane.submit(1); revision++; lane.clear(); release(); await settle(); assert.deepEqual(accepted, []); }); test('busy retries are bounded and stop when superseded', async () => { let attempts = 0; let errors = 0; const lane = new LatestLane(async () => { attempts++; throw new Error('worker busy'); }, () => true, () => errors++, async () => {}); lane.submit(1); await settle(); assert.equal(attempts, 2); assert.equal(errors, 1); let revision = 1; let release; const executed = []; const superseded = new LatestLane(async value => { executed.push(value); if (value === 1) throw new Error('busy'); }, value => value === revision, () => assert.fail('Unexpected failure'), () => new Promise(resolve => { release = resolve; })); superseded.submit(1); await settle(); revision = 2; superseded.submit(2); release(); await settle(); assert.deepEqual(executed, [1, 2]); }); test('automatic rebuild failures wait for a changed revision', () => { const gate = new AutomaticIndexGate(); assert.equal(gate.claim('generation1:pending4:folder1'), true); assert.equal(gate.claim('generation1:pending4:folder1'), false); assert.equal(gate.claim('generation1:pending4:folder2'), true); }); test('empty keyword results stay provisional while semantic setup or search is pending', () => { for (const phase of ['preparing', 'indexing', 'pending']) { const state = emptySearchState('lexical', phase); assert.equal(state.title, 'No keyword matches yet.'); assert.match(state.detail, /preparing|indexing|Searching by meaning/); assert.doesNotMatch(state.title, /No matching files|No matches for this search/); } }); test('only a completed hybrid search gives a final no-match message', () => { const complete = emptySearchState('hybrid', 'complete'); assert.equal(complete.title, 'No matches for this search.'); assert.match(complete.detail, /filters/); const rebuilding = emptySearchState('hybrid', 'indexing'); assert.equal(rebuilding.title, 'No matches in the current index yet.'); assert.match(rebuilding.detail, /may change/); for (const phase of ['preparing', 'indexing', 'pending', 'unavailable', 'no-content']) { assert.notEqual(emptySearchState('hybrid', phase).title, complete.title); } }); test('no document content and unavailable semantics have actionable distinct guidance', () => { assert.match(emptySearchState('lexical', 'no-content').detail, /readable documents/); assert.match(emptySearchState('lexical', 'unavailable').detail, /retry the search/); });