2026-10-12 17:44:56 +02:00
|
|
|
import test from 'node:test';
|
|
|
|
|
import assert from 'node:assert/strict';
|
2026-10-12 21:22:11 +02:00
|
|
|
import { LatestLane, AutomaticIndexGate, emptySearchState, semanticAvailable, SEMANTIC_SETTLE_MS, scopedFilters } from '../src/search-state.ts';
|
2026-10-12 17:44:56 +02:00
|
|
|
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/);
|
|
|
|
|
});
|
2026-10-12 20:59:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
test('old committed semantic generation stays available during rebuild, activation readiness gates replacement', () => {
|
|
|
|
|
assert.equal(semanticAvailable(null), false);
|
|
|
|
|
assert.equal(semanticAvailable({ready: true, generation: null, embedding: true}), false);
|
|
|
|
|
assert.equal(semanticAvailable({ready: true, generation: 1, embedding: false}), true);
|
|
|
|
|
assert.equal(semanticAvailable({ready: true, generation: 1, embedding: true}), true);
|
|
|
|
|
assert.equal(semanticAvailable({ready: false, generation: 2, embedding: true}), false);
|
|
|
|
|
assert.equal(semanticAvailable({ready: true, generation: 2, embedding: false}), true);
|
|
|
|
|
assert.equal(SEMANTIC_SETTLE_MS, 120);
|
|
|
|
|
});
|
2026-10-12 21:22:11 +02:00
|
|
|
|
|
|
|
|
test('scope snapshots freeze all filters independently of later control changes', () => {
|
|
|
|
|
const controls = { extension: 'rs', folder: '/project' };
|
|
|
|
|
const all = scopedFilters('all', controls);
|
|
|
|
|
const code = scopedFilters('code', controls);
|
|
|
|
|
controls.extension = 'pdf';
|
|
|
|
|
assert.equal(all.code_only, false);
|
|
|
|
|
assert.equal(code.code_only, true);
|
|
|
|
|
assert.equal(code.extension, 'rs');
|
|
|
|
|
assert.equal(code.folder, '/project');
|
|
|
|
|
assert.throws(() => { code.code_only = false; }, TypeError);
|
|
|
|
|
assert.throws(() => scopedFilters('unknown', {}), /scope/);
|
|
|
|
|
});
|
|
|
|
|
test('same-query scope change discards old lane output and runs latest scoped request', async () => {
|
|
|
|
|
let revision = 1; let release; const seen = []; const accepted = [];
|
|
|
|
|
const lane = new LatestLane(async request => {
|
|
|
|
|
seen.push(request.filters.code_only);
|
|
|
|
|
if (request.id === 1) await new Promise(resolve => { release = resolve; });
|
|
|
|
|
if (request.id === revision) accepted.push(request);
|
|
|
|
|
}, request => request.id === revision, () => assert.fail('Unexpected failure'));
|
|
|
|
|
lane.submit({id: 1, query: 'parse configuration', filters: scopedFilters('all', {})});
|
|
|
|
|
revision = 2;
|
|
|
|
|
lane.submit({id: 2, query: 'parse configuration', filters: scopedFilters('code', {extension: 'rs'})});
|
|
|
|
|
release(); await settle();
|
|
|
|
|
assert.deepEqual(seen, [false, true]);
|
|
|
|
|
assert.equal(accepted.length, 1);
|
|
|
|
|
assert.equal(accepted[0].filters.code_only, true);
|
|
|
|
|
assert.equal(accepted[0].filters.extension, 'rs');
|
|
|
|
|
});
|