import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { surveyMain } from '../src/index.mjs'; import { BASE, FIXTURES, MARKER, repo } from './helpers.mjs'; const ROOT = fileURLToPath(new URL('../../..', import.meta.url)); async function run(args) { let stdout = '', stderr = ''; const code = await surveyMain(args, { stdout: { write: s => { stdout += s; } }, stderr: { write: s => { stderr += s; } } }); return { code, stdout, stderr }; } // Strings from the fixture that are text, values, file names or link targets: none may be printed. const NEVER = [MARKER, 'Grey eyes', 'The Ferryman', 'Mara Vell', 'Tomas', 'Harbor Town', 'What If', 'Inbox Note', 'Chapter 01', 'Sketch', 'Board', 'Not A Link', 'the harbor', 'Docks', 'character']; test('the survey prints folder names, counts, key sets and link-form counts, and never text', async t => { const r = repo(t, { ...BASE, 'Book One/Places/Odd.md': ['---', `"${MARKER} key": 1`, '---', ''].join('\n') }); const out = await run(['survey', r.repo]); assert.equal(out.code, 0, out.stderr); assert.equal(out.stdout, [ `commit ${r.commit}`, 'entries 10', 'folders (direct children: md canvas base other)', ' (root) 1 0 0 0', ' "Book One" 0 1 0 0', ' "Book One/Chapters" 1 0 0 0', ' "Book One/Characters" 2 0 0 0', ' "Book One/Places" 2 0 0 0', ' "Book One/Playground" 1 1 0 0', ' "Second World/Chapters" 1 0 0 0', 'markdown 8 files, 8 read, 3 without front matter, 0 with front matter not fully parsed', 'front matter key sets', ' 1 [(other key)]', ' 1 [appears_in, name, relationships, type]', ' 1 [appears_in, name, type]', ' 1 [name]', ' 1 [type]', 'body links plain 3, label 1, heading 1, embed 0, self 0, markdown 1; attachment 0', 'field appears_in wikilink 2, name 0', 'field relationships wikilink 1, name 1', ''].join('\n')); for (const s of NEVER) assert.ok(!out.stdout.includes(s), s); }); test('folder names print DEL, C1 and bidi controls, U+061C among them, as escapes', async t => { // One name per escaped range, with each range's first and last code point. const names = ['A\u007fB', 'C\u0080D\u009fE', 'F\u200eG\u200fH', 'I\u202aJ\u202eK', 'L\u2066M\u2069N', 'O\u061cP']; const r = repo(t, Object.fromEntries(names.map(n => [`${n}/a.md`, `${MARKER}\n`]))); const out = await run(['survey', r.repo]); assert.equal(out.code, 0, out.stderr); assert.deepEqual(out.stdout.split('\n').slice(3, 9), [ ' "A\\u007fB" 1 0 0 0', ' "C\\u0080D\\u009fE" 1 0 0 0', ' "F\\u200eG\\u200fH" 1 0 0 0', ' "I\\u202aJ\\u202eK" 1 0 0 0', ' "L\\u2066M\\u2069N" 1 0 0 0', ' "O\\u061cP" 1 0 0 0']); assert.doesNotMatch(out.stdout, /[\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/); }); test('an embed of an attachment counts as an embed and as an attachment; a plain link to one does not', async t => { const r = repo(t, { 'Book One/Chapters/A.md': ['![[cover.png]] ![[missing-note]] [[map.PDF]] [[Chapter 1.5]]', ''].join('\n') }); const out = await run(['survey', r.repo]); assert.match(out.stdout, /^body links plain 2, label 0, heading 0, embed 2, self 0, markdown 0; attachment 1$/m); }); test('with --mapping the survey adds scope, resolver step and problem counts', async t => { const r = repo(t); const out = await run(['survey', r.repo, '--mapping', join(FIXTURES, 'mapping.json')]); assert.equal(out.code, 0, out.stderr); const tail = out.stdout.trimEnd().split('\n').slice(-3); assert.deepEqual(tail, [ 'scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6', 'resolved root 2, vault-prefix 1, relative 2, shortest 3, unresolved 0', 'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0, R8 0']); for (const s of NEVER) assert.ok(!out.stdout.includes(s), s); }); test('C2-01d: with --mapping the survey counts supporting files', async t => { const r = repo(t); const entry = JSON.parse(readFileSync(join(FIXTURES, 'mapping.json'), 'utf8')); entry.universes.harbor.supporting = ['Book One/Places/']; const file = join(r.root, 'supporting.json'); writeFileSync(file, JSON.stringify(entry)); const out = await run(['survey', r.repo, '--mapping', file]); assert.equal(out.code, 0, out.stderr); assert.match(out.stdout, /^scopes canon 4, playground 1, supporting 1, unmapped 3; indexed 6$/m); }); test('survey exit codes: usage 4, bad mapping 3, unreadable repo 2', async t => { const r = repo(t); for (const args of [[], ['scan', r.repo], ['survey'], ['survey', r.repo, '--mapping'], ['survey', r.repo, '--other'], ['survey', r.repo, r.repo]]) assert.equal((await run(args)).code, 4, args.join(' ')); const bad = join(r.root, 'bad.json'); writeFileSync(bad, JSON.stringify({ kind: 'library' })); const refused = await run(['survey', r.repo, '--mapping', bad]); assert.equal(refused.code, 3); assert.match(refused.stderr, /library-mapping-invalid/); writeFileSync(bad, 'not json'); assert.equal((await run(['survey', r.repo, '--mapping', bad])).code, 3); assert.equal((await run(['survey', join(r.root, 'missing')])).code, 2); assert.equal((await run(['survey', r.root])).code, 2, 'a directory that is not a repo'); }); test('scripts/mosaic library survey reaches the survey', t => { const r = repo(t); const out = spawnSync(join(ROOT, 'scripts/mosaic'), ['library', 'survey', r.repo], { encoding: 'utf8', timeout: 30000 }); assert.equal(out.status, 0, out.stderr); assert.match(out.stdout, new RegExp(`^commit ${r.commit}\n`)); assert.ok(!out.stdout.includes(MARKER)); });