import test from 'node:test'; import assert from 'node:assert/strict'; import { inspect } from 'node:util'; import { LibraryError, supportingPath } from '../src/index.mjs'; import { mapping } from './helpers.mjs'; // C2-01d (M12): the path checks an agent write passes. The tree is a listTree array built by hand, // with a symlink, a gitlink, case twins and an NFC name, so no git is needed. const m = mapping(e => { e.universes.harbor.playground = ['Book One/Playground/', 'Book One/Sketches/']; e.universes.harbor.supporting = ['Book One/Playground/', 'Book One/Notes/', 'Harbor Notes/']; e.universes.second.supporting = ['Second Notes/']; e.universes.idea = { kind: 'draft', name: 'New World', playground: ['Drafts/New World/'], supporting: ['Drafts/New World Notes/'] }; return e; }); const blob = (path, mode = '100644') => ({ mode, type: mode === '160000' ? 'commit' : 'blob', oid: '0'.repeat(40), size: 1, path }); const tree = [ blob('Book One/Characters/Mara Vell.md'), blob('Book One/Notes/Existing.md'), blob('Book One/Notes/Linked', '120000'), blob('Book One/Notes/Vendor', '160000'), blob('Book One/Notes/Pointer.md', '120000'), blob('Book One/Notes/Case/Twin.md'), blob('Book One/Notes/Café.md'), blob('Harbor Notes/Places/Island.md'), ]; const refuses = (universe, path, pattern) => assert.throws(() => supportingPath(m, universe, path, tree), e => e instanceof LibraryError && e.code === 'library-write-story-path' && pattern.test(e.message), inspect(path)); test('C2-01d: a path passes at book level, universe level, in a draft and in a folder in both lists', () => { for (const [universe, path] of [['harbor', 'Book One/Notes/Characters/New.md'], ['harbor', 'Book One/Notes/Existing.md'], ['harbor', 'Harbor Notes/Places/Island.md'], ['harbor', 'Harbor Notes/Loose.md'], ['idea', 'Drafts/New World Notes/Ana.md'], ['harbor', 'Book One/Playground/What If.md'], ['second', 'Second Notes/A.md'], ['harbor', 'Book One/Notes/Café.md']]) assert.equal(supportingPath(m, universe, path, tree), path); assert.equal(supportingPath(m, 'harbor', 'Book One/Notes/x.md', []), 'Book One/Notes/x.md'); }); test('C2-01d: a path of exactly 1024 bytes passes, and 1025 bytes made of multi-byte characters refuses', () => { const prefix = 'Harbor Notes/'; const exact = `${prefix}${'a'.repeat(1024 - prefix.length - 3)}.md`; assert.equal(Buffer.byteLength(exact), 1024); assert.equal(supportingPath(m, 'harbor', exact, tree), exact); // é is two bytes: 1025 bytes but far fewer characters. const name = 'é'.repeat((1025 - prefix.length - 3 - 1) / 2); const long = `${prefix}${name}a.md`; assert.equal(Buffer.byteLength(long), 1025); assert.ok(long.length < 1024); refuses('harbor', long, /is longer than 1024 bytes/); }); test('C2-01d: a path outside the universe\'s supporting folders refuses', () => { const outside = /is not under a supporting folder of universe/; for (const path of ['Inbox Note.md', 'Book One/Notes/Board.canvas', 'Book One/Notes/Table.base', 'Book One/Notes/cover.png', 'Book One/Characters/Mara Vell.md', 'Book One/Sketches/Idea.md', 'Drafts/New World/Ana.md', 'Second Notes/A.md', 'Book One/Notes.md']) refuses('harbor', path, outside); refuses('idea', 'Book One/Notes/A.md', /universe "idea"/); refuses('nowhere', 'Book One/Notes/A.md', /universe "nowhere"/); }); test('C2-01d: malformed paths refuse by class, in order', () => { refuses('harbor', '/Book One/Notes/A.md', /"\/Book One\/Notes\/A.md" is absolute/); for (const path of ['Book One/Notes/../Notes/A.md', 'Book One/Notes/./A.md', 'Book One//Notes/A.md', 'Book One/Notes/A/', 'Book One/Notes/..']) refuses('harbor', path, /has an empty, \. or \.\. segment/); refuses('harbor', 'Book One/Notes/A\\B.md', /holds a backslash/); for (const c of ['\u0000', '\u0001', '\n', '\u001f', '\u007f', '\u0085', '\u009f']) assert.throws(() => supportingPath(m, 'harbor', `Book One/Notes/A${c}.md`, tree), e => e.code === 'library-write-story-path' && e.message === 'library-write-story-path: the path holds a control character'); for (const path of [7, null, undefined, 1n, ['Book One/Notes/A.md']]) refuses('harbor', path, /the path must be a string/); // A backslash in a path that is also absolute names the backslash: the order is fixed. refuses('harbor', '/a\\b.md', /holds a backslash/); assert.throws(() => supportingPath(m, 'harbor', 'Book One/Notes/A.md', { 'Book One/Notes/A.md': '100644' }), TypeError); assert.throws(() => supportingPath(m, 'harbor', 'Book One/Notes/A.md'), TypeError); }); test('C2-01d: a symlink or gitlink on the way, or at the path itself, refuses', () => { refuses('harbor', 'Book One/Notes/Linked/A.md', /goes through the symlink or gitlink "Book One\/Notes\/Linked"/); refuses('harbor', 'Book One/Notes/Vendor/Deep/A.md', /goes through the symlink or gitlink "Book One\/Notes\/Vendor"/); refuses('harbor', 'Book One/Notes/Pointer.md', /goes through the symlink or gitlink "Book One\/Notes\/Pointer.md"/); }); test('C2-01d: a name that differs from a tree name only in case or Unicode form refuses, at the file or any folder', () => { refuses('harbor', 'Book One/Notes/existing.md', /differs only in case or Unicode form from "Book One\/Notes\/Existing.md"/); refuses('harbor', 'Book One/Notes/case/New.md', /differs only in case or Unicode form from "Book One\/Notes\/Case"/); refuses('harbor', 'Book One/Notes/CASE/Twin.md', /from "Book One\/Notes\/Case"/); refuses('harbor', 'Harbor notes/A.md', /is not under a supporting folder/); // NFD e + combining acute against the NFC name in the tree: one file on APFS. refuses('harbor', 'Book One/Notes/Café.md', /differs only in case or Unicode form from "Book One\/Notes\/Café.md"/); // A tree that already holds case twins refuses a write to either (R7). const twins = [...tree, blob('Book One/Notes/existing.md')]; for (const path of ['Book One/Notes/Existing.md', 'Book One/Notes/existing.md']) assert.throws(() => supportingPath(m, 'harbor', path, twins), e => e.code === 'library-write-story-path' && /differs only in case/.test(e.message)); });