vonargo/mosAIc
1
1// test/subject-rail.test.js — the rail's pure core: overlap ranking, candidate indexing2// (views + okf concept tiles), the rescore-only rerank invariant, escaped HTML.3import { test } from 'node:test';4import assert from 'node:assert/strict';5import { tokens, overlapScore, candidateIndex, rankBySubject, applyRerank, railHtml, railRowHtml, RAIL_K } from '../js/subject-rail.js';6import { TASKS } from '../js/demo.js';7 8const VIEWS = [9 { id: 'oauth', title: 'OAuth flow', heading: 'OAuth 2.0 flow', tesserae: [{ type: 'markdown', title: 'Token exchange', body: 'x' }] },10 { id: 'tokens', title: 'Access tokens', heading: 'Access tokens explained', tesserae: [{ type: 'markdown', title: 'Token lifetime', body: 'x' }] },11 { id: 'recipes', title: 'Pasta recipes', heading: 'Weeknight pasta', tesserae: [{ type: 'markdown', title: 'Carbonara', body: 'x' }] },12 { id: 'kb', title: 'Concepts', tesserae: [13 { type: 'markdown', title: 'OAuth scopes', body: 'scope grants', okf: { tags: ['oauth', 'auth'], description: 'scopes' } },14 { type: 'markdown', title: 'Béchamel', body: 'butter flour milk', okf: { tags: ['cooking'], description: 'sauce' } },15 ] },16];17 18test('tokens: lowercase 3+ alnum runs, stopwords dropped', () => {19 const t = tokens('The OAuth 2.0 Flow and the token');20 assert.ok(t.has('oauth') && t.has('flow') && t.has('token'));21 assert.ok(!t.has('the') && !t.has('and') && !t.has('2'));22});23 24test('overlapScore: 0 on disjoint/empty; symmetric; clamped', () => {25 assert.equal(overlapScore(tokens('alpha beta'), tokens('gamma delta')), 0);26 assert.equal(overlapScore(new Set(), tokens('alpha')), 0);27 const a = tokens('oauth token flow'), b = tokens('token flow lifetime');28 assert.ok(overlapScore(a, b) > 0 && overlapScore(a, b) <= 1);29 assert.equal(overlapScore(a, b), overlapScore(b, a));30});31 32test('candidateIndex: other views + okf concept tiles (ref viewId::i); active excluded; non-okf tiles are not candidates', () => {33 const c = candidateIndex(VIEWS, 'oauth');34 const refs = c.map(x => x.ref);35 assert.ok(!refs.includes('oauth'));36 assert.ok(refs.includes('tokens') && refs.includes('recipes') && refs.includes('kb'));37 assert.ok(refs.includes('kb::0') && refs.includes('kb::1')); // concepts38 assert.equal(c.find(x => x.ref === 'kb::0').kind, 'concept');39 assert.ok(!refs.includes('tokens::0')); // plain tile ≠ candidate40});41 42test('rankBySubject v2: related ranks above unrelated; zero-score dropped; sorted desc; field weight shows', () => {43 const { scorer, ranked } = rankBySubject(VIEWS[0], VIEWS);44 assert.equal(scorer, 'lexical v2');45 const refs = ranked.map(r => r.ref);46 assert.ok(refs.indexOf('tokens') >= 0, 'token-overlapping view ranks');47 assert.ok(refs.indexOf('kb::0') >= 0, 'oauth concept ranks');48 assert.equal(refs[0], 'kb::0', 'title+tag match outranks a body-ish match (field weighting)');49 assert.ok(!refs.includes('recipes'), 'pasta does not rank for OAuth'); // zero-score dropped50 for (let i = 1; i < ranked.length; i++) assert.ok(ranked[i - 1].score >= ranked[i].score);51});52 53// ── scorer v2 specifics (field weights · IDF · the Unicode tokenizer) ──54test('tokenizer v2: 2-letter acronyms live, Cyrillic tokenizes, compound tech tokens survive whole', () => {55 const t = tokens('The AI UI runs on node.js with OAuth2 — Настройка памяти');56 assert.ok(t.has('ai') && t.has('ui'), 'acronyms no longer invisible');57 assert.ok(t.has('node.js'), 'compound tech token kept whole');58 assert.ok(t.has('oauth2'), 'alnum tech token kept');59 assert.ok(t.has('настройка') && t.has('памяти'), 'Cyrillic tokenizes instead of vanishing');60 assert.ok(!t.has('the') && !t.has('on'), 'stopwords (incl. 2-letter) still dropped');61});62 63test('field weighting: a TITLE match outranks the same term buried in a BODY', () => {64 const active = { id: 'a', title: 'Provenance', tesserae: [] };65 const views = [active,66 { id: 'in-title', title: 'Provenance ledger', tesserae: [] },67 { id: 'in-body', title: 'Notes', subtitle: 'provenance provenance provenance', tesserae: [] },68 ];69 const { ranked } = rankBySubject(active, views);70 assert.equal(ranked[0].ref, 'in-title');71});72 73test('IDF: a rare shared term outranks a term every candidate carries', () => {74 const active = { id: 'a', title: 'verdigris surface', tesserae: [] };75 const views = [active,76 { id: 'rare', title: 'verdigris surface', tesserae: [] }, // shares rare + common77 { id: 'common1', title: 'surface one', tesserae: [] }, // 'surface' is everywhere78 { id: 'common2', title: 'surface two', tesserae: [] },79 { id: 'common3', title: 'surface three', tesserae: [] },80 ];81 const { ranked } = rankBySubject(active, views);82 assert.equal(ranked[0].ref, 'rare');83 assert.ok(ranked[0].score > ranked[1].score * 1.5, 'the rare-term match wins decisively, not marginally');84});85 86test('applyRerank: rescore-only — unknown refs ignored, no rows added/removed, clamped, resorted', () => {87 const ranked = [{ kind: 'view', ref: 'a', title: 'A', score: 0.9 }, { kind: 'view', ref: 'b', title: 'B', score: 0.5 }];88 const out = applyRerank(ranked, [{ ref: 'b', score: 5 }, { ref: 'ghost', score: 1 }, { ref: 'a', score: 0.1 }]);89 assert.equal(out.length, 2); // no minted rows90 assert.equal(out[0].ref, 'b'); // clamped to 1 → first91 assert.equal(out[0].score, 1);92 assert.equal(out[1].score, 0.1);93 assert.ok(!out.some(r => r.ref === 'ghost'));94});95 96test('the demo showcase lights the rail: every multi-view example ranks ≥1 related row for its first view', () => {97 for (const task of TASKS) {98 const views = task.overlay.views || [];99 if (views.length < 2) continue;100 const { ranked } = rankBySubject(views[0], views);101 assert.ok(ranked.length >= 1, `example "${task.id}" leaves the rail dark`);102 }103});104 105test('railHtml: caps at RAIL_K with a +N more toggle; escapes hostile titles; empty → hidden', () => {106 const many = { scorer: 'overlap', ranked: Array.from({ length: RAIL_K + 3 }, (_, i) => ({ kind: 'view', ref: 'v' + i, title: 'V' + i, score: 1 - i * 0.05 })) };107 const html = railHtml(many, {});108 assert.match(html, /+3 more/);109 assert.equal((html.match(/srail-row/g) || []).length, RAIL_K);110 assert.match(railHtml(many, { expanded: true }), /− less/);111 assert.equal(railHtml({ scorer: 'overlap', ranked: [] }, {}), '');112 const evil = railRowHtml({ kind: 'view', ref: 'x', title: '<img src=x onerror=alert(1)>', score: 0.5 });113 assert.doesNotMatch(evil, /<img src=x/);114 assert.match(evil, /<img/);115});116 