// Query tests for design 3 (C2-05a): universe isolation, scope labels, unmapped files, hybrids, paging // under the reply cap, refusals and manuscript paging. Fixtures are made up and built under the scratch area. import { test } from 'node:test'; import { writeFileSync } from 'node:fs'; import { join } from 'node:path'; import assert from 'node:assert/strict'; import { REPLY_CAP } from '../../harness/src/tools.mjs'; import { find, get, search, manuscript, universeInfo, QUERY_REPLY_CAP, LOOKUP_LIMIT, MANUSCRIPT_LINES } from '../src/query.mjs'; import { mapping, MARKER, git } from './helpers.mjs'; import { scratchRepo, note, EXCLUSION, EXCLUDED_PATHS, SUPPORT_FILES, withSupporting } from './query-fixture.mjs'; // A hybrid over harbor and second. Its one constraint is EXCLUSION (query-fixture.mjs) and it declares no // books or playground. const WITH_BLEND = e => ({ ...e, universes: { ...e.universes, blend: { kind: 'hybrid', name: 'Blend', sources: ['harbor', 'second'], books: [], playground: [], constraints: [EXCLUSION], notes: 'made-up hybrid', }, }, }); // PRD 1.14 levels (C2-01c): harbor gets a universe-level playground prefix outside every book, and a draft // universe has only playground prefixes. Files there have book null. const WITH_LEVELS = e => ({ ...WITH_BLEND(e), universes: { ...WITH_BLEND(e).universes, harbor: { ...e.universes.harbor, playground: ['Book One/Playground/', 'Notes/Harbor/'] }, idea: { kind: 'draft', name: 'New World', playground: ['Drafts/New World/'] }, }, }); const LEVEL_FILES = { 'Notes/Harbor/Scraps.md': note('summary', 'Scraps', `${MARKER} loose`), 'Notes/Harbor/Chapters/Idea Chapter.md': `# Idea\n${MARKER} unbooked chapter\n`, 'Drafts/New World/Characters/Rin.md': note('character', 'Rin', `${MARKER} draft`), 'Drafts/New World/Summary.md': note('summary', 'Summary', `${MARKER} draft`), }; const FILES = { 'Book One/Characters/Mara Vell.md': note('character', 'Mara Vell', `Grey eyes. ${MARKER}`, ['appears_in: "[[Book One/Chapters/Chapter 01]]"', 'relationships:', ' - "[[Tomas]]"']), 'Book One/Characters/Tomas.md': note('character', 'Tomas', MARKER), 'Book One/Chapters/Chapter 01.md': `# One\n${MARKER} [[Mara Vell]] met [[Book One/Places/Harbor Town|the harbor]].\n`, 'Book One/Chapters/Chapter 02.md': 'Quiet. [[Ferry Logs]] [[Ilse]]\n', 'Book One/Places/Harbor Town.md': note('place', 'Harbor Town', `Salt. ${MARKER}`), 'Book One/Research/Ferry Logs.md': note('research', 'Ferry Logs', `Timetable. ${MARKER}`), 'Book One/Playground/What If.md': note('summary', 'What If', `${MARKER} [[Mara Vell]]`), 'Second World/Characters/Ilse.md': note('character', 'Ilse', `${MARKER} Ilse.`), 'Second World/Chapters/01.md': '# Elsewhere\n[[Ilse]] waits.\n', 'Inbox Note.md': `${MARKER}\n`, 'Book One/Board.canvas': '{"nodes":[]}\n', }; const src = (ref, map = mapping()) => ({ repo: ref.repo, commit: ref.commit, mapping: map }); const paths = reply => reply.results.map(r => r.path); test('the query reply cap equals the harness reply cap', () => { assert.equal(QUERY_REPLY_CAP, REPLY_CAP); }); test('find is bound to its universe and refuses a call without one', async t => { const s = src(scratchRepo(t, FILES)); const names = paths(await find(s, { universe: 'harbor', query: '' })); assert.ok(names.length > 0); assert.ok(names.every(p => p.startsWith('Book One/'))); assert.ok(!names.includes('Second World/Characters/Ilse.md')); await assert.rejects(find(s, { query: '' }), { code: 'query-argument-invalid' }); await assert.rejects(find(s, { universe: 'nowhere', query: '' }), { code: 'query-universe-unknown' }); }); test('every result carries its scope, and playground appears only when asked for', async t => { const s = src(scratchRepo(t, FILES)); const canon = (await find(s, { universe: 'harbor', query: '' })).results; assert.ok(canon.every(r => r.scope === 'canon')); assert.ok(!canon.some(r => r.path.includes('/Playground/'))); const play = (await find(s, { universe: 'harbor', query: '', scope: 'playground' })).results; assert.deepEqual(play.map(r => r.path), ['Book One/Playground/What If.md']); assert.ok(play.every(r => r.scope === 'playground')); }); test('unmapped files never appear in any reply', async t => { const s = src(scratchRepo(t, FILES)); assert.deepEqual((await find(s, { universe: 'harbor', query: 'Inbox' })).results, []); assert.deepEqual((await find(s, { universe: 'harbor', query: 'Board' })).results, []); const hits = (await search(s, { universe: 'harbor', text: MARKER })).results; assert.ok(hits.length > 0); assert.ok(hits.every(r => r.path !== 'Inbox Note.md' && r.path !== 'Book One/Board.canvas')); }); test('a hybrid sees its sources canon, minus exclusions, and no playground', async t => { const s = src(scratchRepo(t, FILES), mapping(WITH_BLEND)); const reply = await find(s, { universe: 'blend', query: '' }); assert.deepEqual(paths(reply).sort(), [ 'Book One/Chapters/Chapter 01.md', 'Book One/Chapters/Chapter 02.md', 'Book One/Characters/Mara Vell.md', 'Book One/Characters/Tomas.md', 'Book One/Places/Harbor Town.md', 'Second World/Chapters/01.md', 'Second World/Characters/Ilse.md', ]); assert.ok(reply.results.every(r => r.scope === 'canon' && ['harbor', 'second'].includes(r.universe))); assert.deepEqual((await find(s, { universe: 'blend', query: '', scope: 'playground' })).results, []); assert.deepEqual((await find(s, { universe: 'blend', query: 'Ferry' })).results, []); }); test('a hybrid gets a source file it can see, and not an excluded or foreign one', async t => { const ref = scratchRepo(t, FILES); const hybrid = src(ref, mapping(WITH_BLEND)); const ilse = await get(hybrid, { universe: 'blend', path: 'Second World/Characters/Ilse.md' }); assert.equal(ilse.scope, 'canon'); assert.ok(ilse.links_in.some(l => l.path === 'Second World/Chapters/01.md')); assert.equal(await get(hybrid, { universe: 'blend', path: 'Book One/Research/Ferry Logs.md' }), null); assert.equal(await get(src(ref), { universe: 'harbor', path: 'Second World/Characters/Ilse.md' }), null); }); test('get lists visible links both ways and drops a target another universe cannot see', async t => { const ref = scratchRepo(t, FILES); const harbor = src(ref); const ch1 = await get(harbor, { universe: 'harbor', path: 'Book One/Chapters/Chapter 01.md' }); assert.ok(ch1.links_out.some(l => l.path === 'Book One/Characters/Mara Vell.md' && l.line === 2)); assert.ok(ch1.links_out.some(l => l.path === 'Book One/Places/Harbor Town.md')); const ch2 = await get(harbor, { universe: 'harbor', path: 'Book One/Chapters/Chapter 02.md' }); assert.ok(ch2.links_out.some(l => l.path === 'Book One/Research/Ferry Logs.md')); assert.ok(!ch2.links_out.some(l => l.path === 'Second World/Characters/Ilse.md')); const mara = await get(harbor, { universe: 'harbor', path: 'Book One/Characters/Mara Vell.md' }); assert.ok(mara.links_in.some(l => l.path === 'Book One/Chapters/Chapter 01.md')); assert.ok(mara.links_in.some(l => l.path === 'Book One/Playground/What If.md')); assert.ok(mara.links_out.some(l => l.path === 'Book One/Characters/Tomas.md' && l.field === 'relationships')); assert.ok(mara.links_out.some(l => l.path === 'Book One/Chapters/Chapter 01.md' && l.field === 'appears_in')); const blendCh2 = await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path: 'Book One/Chapters/Chapter 02.md' }); assert.ok(blendCh2.links_out.some(l => l.path === 'Second World/Characters/Ilse.md')); }); test('a long list pages by position, never cuts an item, and returns 50 per page', async t => { const files = { ...FILES }; for (let i = 1; i <= 120; i++) files[`Book One/Places/Note ${i}.md`] = note('place', `Note ${i}`); const s = src(scratchRepo(t, files)); const seen = []; let offset = 0; for (let pages = 0; pages < 10; pages++) { const reply = await find(s, { universe: 'harbor', query: 'Note', offset }); assert.ok(JSON.stringify(reply).length <= QUERY_REPLY_CAP); assert.ok(reply.results.length <= LOOKUP_LIMIT); if (pages === 0) assert.equal(reply.results.length, LOOKUP_LIMIT); seen.push(...paths(reply)); if (reply.next === null) break; offset = reply.next; } assert.equal(seen.length, 120); assert.equal(new Set(seen).size, 120); }); test('a page stays under the reply cap when items are large', async t => { const big = 'x'.repeat(2000); const files = { ...FILES }; for (let i = 1; i <= 40; i++) files[`Book One/Places/Big ${i}.md`] = note('place', `Big ${i}`, '', [`appears_in: "${big}"`]); const s = src(scratchRepo(t, files)); const first = await find(s, { universe: 'harbor', query: 'Big' }); assert.ok(JSON.stringify(first).length <= QUERY_REPLY_CAP); assert.ok(first.results.length > 0 && first.results.length < 40); assert.notEqual(first.next, null); const seen = []; let offset = 0; for (let pages = 0; pages < 10; pages++) { const reply = await find(s, { universe: 'harbor', query: 'Big', offset }); assert.ok(JSON.stringify(reply).length <= QUERY_REPLY_CAP); seen.push(...paths(reply)); if (reply.next === null) break; offset = reply.next; } assert.equal(new Set(seen).size, 40); }); test('a search line too long to fit alone comes back marked, with no excerpt, and the other hits still return', async t => { const huge = 'y'.repeat(QUERY_REPLY_CAP + 1000); const s = src(scratchRepo(t, { ...FILES, 'Book One/Places/Wall.md': note('place', 'Wall', `${huge} ${MARKER}`) })); const reply = await search(s, { universe: 'harbor', text: 'yyyyyyyy' }); assert.deepEqual(reply.results, [{ path: 'Book One/Places/Wall.md', line: 5, excerpt: null, excerpt_too_long: true, scope: 'canon', universe: 'harbor' }]); const mixed = await search(s, { universe: 'harbor', text: MARKER.toLowerCase() }); assert.ok(mixed.results.some(r => r.excerpt_too_long === true)); assert.ok(mixed.results.some(r => typeof r.excerpt === 'string' && !('excerpt_too_long' in r))); assert.ok(JSON.stringify(mixed).length <= QUERY_REPLY_CAP); }); test('a find item that cannot fit alone is refused, not cut', async t => { const huge = 'y'.repeat(QUERY_REPLY_CAP + 1000); const s = src(scratchRepo(t, { ...FILES, 'Book One/Places/Wall.md': note('place', 'Wall', '', [`appears_in: "${huge}"`]) })); await assert.rejects(find(s, { universe: 'harbor', query: 'Wall' }), { code: 'query-item-too-large' }); }); test('get pages its lines, and the next line picks up where the page stopped', async t => { const body = `${Array.from({ length: 500 }, (_, i) => `Line ${i + 1} ${'z'.repeat(120)}`).join('\n')}\n`; const s = src(scratchRepo(t, { ...FILES, 'Book One/Chapters/Long.md': body })); const first = await get(s, { universe: 'harbor', path: 'Book One/Chapters/Long.md' }); assert.equal(first.lines[0][0], 1); assert.ok(first.lines.length < 501); assert.notEqual(first.next, null); let from = 1, count = 0; for (let pages = 0; pages < 10; pages++) { const page = await get(s, { universe: 'harbor', path: 'Book One/Chapters/Long.md', from }); count += page.lines.length; if (page.next === null) break; from = page.next; } assert.equal(count, 501); }); test('get refuses a file whose front matter alone is over the cap, and a line alone is over the cap', async t => { const huge = 'q'.repeat(QUERY_REPLY_CAP + 1000); const s = src(scratchRepo(t, { ...FILES, 'Book One/Places/Heavy.md': note('place', 'Heavy', '', [`appears_in: "${huge}"`]), 'Book One/Chapters/Wide.md': `${huge}\n`, })); await assert.rejects(get(s, { universe: 'harbor', path: 'Book One/Places/Heavy.md' }), { code: 'query-item-too-large' }); await assert.rejects(get(s, { universe: 'harbor', path: 'Book One/Chapters/Wide.md' }), { code: 'query-item-too-large' }); }); test('get pages the links of a hub note, every page carries the front matter, and the pages add up', async t => { const files = { ...FILES }; for (let i = 1; i <= 900; i++) files[`Book One/Chapters/Mention ${i}.md`] = `[[Tomas]] ${'w'.repeat(60)}\n`; const s = src(scratchRepo(t, files)); const seen = []; const PAST_END = 1000; let linksFrom = 0, pages = 0, lines = 0, from = 1; for (; pages < 40; pages++) { const page = await get(s, { universe: 'harbor', path: 'Book One/Characters/Tomas.md', from, links_from: linksFrom }); assert.ok(JSON.stringify(page).length <= QUERY_REPLY_CAP); assert.equal(page.frontmatter.name, 'Tomas'); seen.push(...page.links_in.map(l => l.path)); if (from !== PAST_END) lines += page.lines.length; // Once the lines are read, ask past the last one: the page then carries links alone. from = page.next ?? PAST_END; if (page.links_next === null && page.next === null) break; if (page.links_next !== null) linksFrom = page.links_next; } assert.ok(pages > 1, 'the hub needed more than one page'); assert.ok(pages < 40); assert.equal(seen.filter(p => p.startsWith('Book One/Chapters/Mention ')).length, 900); assert.equal(new Set(seen).size, seen.length, 'no link comes back twice'); assert.equal(lines, 6, 'the five lines of the note and the empty one after its last newline'); const tail = await get(s, { universe: 'harbor', path: 'Book One/Characters/Tomas.md', from: PAST_END }); assert.deepEqual(tail.lines, []); assert.equal(tail.next, null); }); test('get rejects a bad links_from', async t => { const s = src(scratchRepo(t, FILES)); await assert.rejects(get(s, { universe: 'harbor', path: 'Book One/Characters/Tomas.md', links_from: -1 }), { code: 'query-argument-invalid' }); const past = await get(s, { universe: 'harbor', path: 'Book One/Characters/Tomas.md', links_from: 99 }); assert.deepEqual([past.links_out, past.links_in, past.links_next], [[], [], null]); }); test('search returns scoped line hits, case-insensitively, and refuses empty text', async t => { const s = src(scratchRepo(t, FILES)); const reply = await search(s, { universe: 'harbor', text: 'marker' }); assert.ok(reply.results.length > 0); assert.ok(reply.results.every(r => r.scope === 'canon' && r.path.startsWith('Book One/') && Number.isInteger(r.line))); assert.ok(reply.results.some(r => r.path === 'Book One/Chapters/Chapter 01.md' && r.line === 2)); await assert.rejects(search(s, { universe: 'harbor', text: '' }), { code: 'query-argument-invalid' }); }); test('a manuscript call returns one chapter, at most 400 lines, and names its next line', async t => { const long = `${Array.from({ length: 900 }, (_, i) => `Scene ${i + 1}`).join('\n')}\n`; const s = src(scratchRepo(t, { ...FILES, 'Book One/Chapters/Long Chapter.md': long })); const first = await manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Long Chapter' }); assert.equal(first.lines.length, MANUSCRIPT_LINES); assert.equal(first.next, MANUSCRIPT_LINES + 1); const second = await manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Long Chapter', from: first.next }); assert.equal(second.lines[0][0], MANUSCRIPT_LINES + 1); }); test('a manuscript call without a chapter returns the first chapter in path order and names the next', async t => { const s = src(scratchRepo(t, FILES)); const first = await manuscript(s, { universe: 'harbor', book: 'Book One' }); assert.equal(first.path, 'Book One/Chapters/Chapter 01.md'); assert.equal(first.next_chapter, 'Chapter 02'); const last = await manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Chapter 02' }); assert.equal(last.path, 'Book One/Chapters/Chapter 02.md'); assert.equal(last.next_chapter, null); assert.equal(await manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Nope' }), null); }); test('a chapter name that matches two visible files is refused', async t => { const s = src(scratchRepo(t, { ...FILES, 'Book One/Chapters/Part/Chapter 01.md': 'Again.\n' })); await assert.rejects(manuscript(s, { universe: 'harbor', book: 'Book One', chapter: 'Chapter 01' }), { code: 'query-chapter-ambiguous' }); }); test('a hybrid reads a source chapter through its declared sources only', async t => { const ref = scratchRepo(t, FILES); const hybrid = await manuscript(src(ref, mapping(WITH_BLEND)), { universe: 'blend', book: 'Second World', chapter: '01' }); assert.equal(hybrid.path, 'Second World/Chapters/01.md'); assert.equal(hybrid.scope, 'canon'); assert.equal(await manuscript(src(ref), { universe: 'harbor', book: 'Second World', chapter: '01' }), null); }); test('universe info reports a hybrid sources, constraints and notes, and refuses an unknown universe', async t => { const s = src(scratchRepo(t, FILES), mapping(WITH_BLEND)); const blend = await universeInfo(s, { universe: 'blend' }); assert.equal(blend.kind, 'hybrid'); assert.deepEqual(blend.sources, ['harbor', 'second']); assert.deepEqual(blend.constraints, [EXCLUSION]); assert.equal(blend.notes, 'made-up hybrid'); const harbor = await universeInfo(s, { universe: 'harbor' }); assert.equal(harbor.kind, 'universe'); assert.equal(harbor.sources, undefined); await assert.rejects(universeInfo(s, { universe: 'ghost' }), { code: 'query-universe-unknown' }); }); test('an exclusion hides what it names from a hybrid and from nothing else', async t => { const ref = scratchRepo(t, FILES); const hybrid = src(ref, mapping(WITH_BLEND)); const seen = paths(await find(hybrid, { universe: 'blend', query: '' })); for (const path of EXCLUDED_PATHS) assert.ok(!seen.includes(path), `${path} stays hidden`); const own = paths(await find(src(ref), { universe: 'harbor', query: '' })); for (const path of EXCLUDED_PATHS) assert.ok(own.includes(path), `${path} stays visible to its own universe`); }); test('PRD 1.14: a lookup that names a book leaves book-null files out', async t => { const s = src(scratchRepo(t, { ...FILES, ...LEVEL_FILES }), mapping(WITH_LEVELS)); const all = paths(await find(s, { universe: 'harbor', query: '', scope: 'playground' })); assert.deepEqual(all.sort(), ['Book One/Playground/What If.md', 'Notes/Harbor/Chapters/Idea Chapter.md', 'Notes/Harbor/Scraps.md']); const booked = await find(s, { universe: 'harbor', query: '', scope: 'playground', book: 'Book One' }); assert.deepEqual(paths(booked), ['Book One/Playground/What If.md']); assert.ok(booked.results.every(r => r.book === 'Book One')); const hits = (await search(s, { universe: 'harbor', text: 'unbooked', scope: 'playground' })).results; assert.deepEqual(hits.map(r => r.path), ['Notes/Harbor/Chapters/Idea Chapter.md']); assert.deepEqual((await search(s, { universe: 'harbor', text: 'unbooked', scope: 'playground', book: 'Book One' })).results, []); }); test('PRD 1.14: library_manuscript never reads a book-null file', async t => { const s = src(scratchRepo(t, { ...FILES, ...LEVEL_FILES }), mapping(WITH_LEVELS)); // The unbooked file sits under a manuscript folder, so it has type manuscript and book null. const listed = await find(s, { universe: 'harbor', query: 'Idea', scope: 'playground' }); assert.deepEqual(listed.results.map(r => [r.path, r.type, r.book]), [['Notes/Harbor/Chapters/Idea Chapter.md', 'manuscript', null]]); assert.equal(await manuscript(s, { universe: 'harbor', book: 'Book One', scope: 'playground', chapter: 'Idea Chapter' }), null); assert.equal(await manuscript(s, { universe: 'harbor', book: 'Book One', scope: 'playground' }), null); assert.equal(await manuscript(s, { universe: 'harbor', book: 'Notes', scope: 'playground', chapter: 'Idea Chapter' }), null); }); test('PRD 1.14: a draft universe has no canon, so its lookups return files only with scope playground', async t => { const s = src(scratchRepo(t, { ...FILES, ...LEVEL_FILES }), mapping(WITH_LEVELS)); assert.deepEqual((await find(s, { universe: 'idea', query: '' })).results, []); assert.deepEqual((await search(s, { universe: 'idea', text: MARKER })).results, []); const found = await find(s, { universe: 'idea', query: '', scope: 'playground' }); assert.deepEqual(paths(found).sort(), ['Drafts/New World/Characters/Rin.md', 'Drafts/New World/Summary.md']); assert.ok(found.results.every(r => r.scope === 'playground' && r.universe === 'idea' && r.book === null)); const hits = (await search(s, { universe: 'idea', text: 'draft', scope: 'playground' })).results; assert.equal(hits.length, 2); const rin = await get(s, { universe: 'idea', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' }); assert.equal(rin.scope, 'playground'); assert.equal(await get(s, { universe: 'harbor', path: 'Drafts/New World/Characters/Rin.md', scope: 'playground' }), null); }); test('PRD 1.14: library_universe on a draft returns its kind, name and prefixes', async t => { const s = src(scratchRepo(t, { ...FILES, ...LEVEL_FILES }), mapping(WITH_LEVELS)); const draft = await universeInfo(s, { universe: 'idea' }); assert.equal(draft.kind, 'draft'); assert.equal(draft.name, 'New World'); assert.deepEqual(draft.playground, ['Drafts/New World/']); assert.deepEqual(draft.books, []); assert.equal(draft.sources, undefined); }); test('a hybrid gets a source playground file as null, while the source gets it with scope playground', async t => { const ref = scratchRepo(t, FILES); const path = 'Book One/Playground/What If.md'; assert.equal(await get(src(ref, mapping(WITH_BLEND)), { universe: 'blend', path, scope: 'playground' }), null); const own = await get(src(ref), { universe: 'harbor', path, scope: 'playground' }); assert.equal(own.scope, 'playground'); }); // mos-1330 item3: the new get contract defaults to canon even for an explicit playground path. test('C2-05c get without scope returns null for a playground path', async t => { const s = src(scratchRepo(t, FILES)); assert.equal(await get(s, { universe: 'harbor', path: 'Book One/Playground/What If.md' }), null); }); // Row284: scope is explicit for get as well as lists; blob and lines share a served snapshot. test('C2-05c supporting find/search/get are opt-in at book, universe and draft level', async t => { const s = src(scratchRepo(t, SUPPORT_FILES), mapping(withSupporting)); const expected = ['Book One/Support/Notes/Helper.md', 'Notes/HarborSupport/Loose.md']; assert.deepEqual(paths(await find(s, { universe: 'harbor', query: '', scope: 'supporting' })), expected); assert.deepEqual((await search(s, { universe: 'harbor', text: 'Scope fixture', scope: 'supporting' })).results.map(x => x.path), expected); assert.deepEqual(paths(await find(s, { universe: 'harbor', query: '' })), ['Book One/Characters/Canon.md']); assert.deepEqual((await search(s, { universe: 'harbor', text: 'Scope fixture' })).results.map(x => x.path), ['Book One/Characters/Canon.md']); for (const path of expected) { assert.equal(await get(s, { universe: 'harbor', path }), null); assert.equal(await get(s, { universe: 'harbor', path, scope: 'playground' }), null); assert.equal((await get(s, { universe: 'harbor', path, scope: 'supporting' })).scope, 'supporting'); assert.equal(await get(s, { universe: 'second', path, scope: 'supporting' }), null); } const draftPath = 'Drafts/Idea/Support/Seed.md'; assert.deepEqual(paths(await find(s, { universe: 'idea', query: '', scope: 'supporting' })), [draftPath]); assert.equal((await get(s, { universe: 'idea', path: draftPath, scope: 'supporting' })).scope, 'supporting'); assert.equal(await get(s, { universe: 'idea', path: draftPath }), null); assert.equal((await find(s, { universe: 'harbor', query: '', book: 'Book One', scope: 'supporting' })).results.length, 1); }); test('C2-05c an absent supporting prefix returns nothing without refusal', async t => { const s = src(scratchRepo(t, SUPPORT_FILES)); assert.deepEqual((await find(s, { universe: 'harbor', query: '', scope: 'supporting' })).results, []); assert.deepEqual((await search(s, { universe: 'harbor', text: 'Scope fixture', scope: 'supporting' })).results, []); assert.equal(await get(s, { universe: 'harbor', path: 'Book One/Characters/Canon.md', scope: 'supporting' }), null); assert.equal(await get(s, { universe: 'harbor', path: 'Book One/Support/Notes/Helper.md', scope: 'supporting' }), null); }); test('C2-05c each get blob equals its served tree object, even after source HEAD advances', async t => { const ref = scratchRepo(t, SUPPORT_FILES), s = src(ref, mapping(withSupporting)); const canon = 'Book One/Characters/Canon.md'; const priorBlob = git(ref.repo, 'rev-parse', `${ref.commit}:${canon}`); writeFileSync(join(ref.repo, canon), note('character', 'Canon changed', 'New HEAD must not be read.')); git(ref.repo, 'add', '-A'); git(ref.repo, 'commit', '-qm', 'advance head fixture'); assert.notEqual(git(ref.repo, 'rev-parse', `HEAD:${canon}`), priorBlob); for (const [scope, path] of [['canon', canon], ['playground', 'Book One/Playground/Plan.md'], ['supporting', 'Book One/Support/Notes/Helper.md']]) { const result = await get(s, { universe: 'harbor', path, scope }); assert.equal(result.commit, ref.commit); assert.equal(result.blob, git(ref.repo, 'rev-parse', `${ref.commit}:${path}`)); assert.ok(!JSON.stringify(result.lines).includes('New HEAD must not be read')); } }); test('C2-05c full outgoing and incoming links stay in scope order across pages without losing cross-scope links', async t => { const files = {}, groups = { supporting: [], playground: [], canon: [] }; for (const [scope, folder] of [['canon', 'Book One/Characters'], ['playground', 'Book One/Playground'], ['supporting', 'Book One/Support']]) { for (let n = 0; n < 100; n++) { const path = `${folder}/${scope}-${String(n).padStart(3, '0')}-${'x'.repeat(80)}.md`; groups[scope].push(path); files[path] = `[[Book One/Characters/Hub]]\n`; } } const all = [...groups.canon, ...groups.playground, ...groups.supporting]; files['Book One/Characters/Hub.md'] = all.map(path => `[[${path.slice(0, -3)}]]`).join('\n'); // Other-universe links are never returned, regardless of rank. files['Second World/Other.md'] = '[[Book One/Characters/Hub]]\n'; const s = src(scratchRepo(t, files), mapping(withSupporting)); const outs = [], ins = []; let offset = 0, pages = 0; for (; pages < 20; pages++) { const page = await get(s, { universe: 'harbor', path: 'Book One/Characters/Hub.md', from: 999_999, links_from: offset }); assert.ok(JSON.stringify(page).length <= QUERY_REPLY_CAP); outs.push(...page.links_out.map(x => x.path)); ins.push(...page.links_in.map(x => x.path)); if (page.links_next === null) break; offset = page.links_next; } const expected = [...groups.supporting, ...groups.playground, ...groups.canon]; assert.ok(pages > 0 && pages < 20, 'links occupy multiple bounded pages'); assert.deepEqual(outs, expected); assert.deepEqual(ins, expected); assert.equal(new Set(outs).size, 300); assert.equal(new Set(ins).size, 300); }); test('C2-05c manuscript still refuses supporting rather than silently widening its scope', async t => { const s = src(scratchRepo(t, SUPPORT_FILES), mapping(withSupporting)); await assert.rejects(manuscript(s, { universe: 'harbor', book: 'Book One', scope: 'supporting' }), { code: 'query-argument-invalid' }); }); test('C2-05c scope expansion keeps hybrid sources canon-only and refuses all/array scopes', async t => { const s = src(scratchRepo(t, SUPPORT_FILES), mapping(entry => withSupporting(WITH_BLEND(entry)))); assert.deepEqual((await find(s, { universe: 'blend', query: '', scope: 'supporting' })).results, []); assert.equal(await get(s, { universe: 'blend', path: 'Book One/Support/Notes/Helper.md', scope: 'supporting' }), null); assert.equal((await get(s, { universe: 'blend', path: 'Book One/Characters/Canon.md' })).scope, 'canon'); for (const scope of ['all', ['canon', 'supporting']]) await assert.rejects(get(s, { universe: 'harbor', path: 'Book One/Characters/Canon.md', scope }), { code: 'query-argument-invalid' }); });