import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { OneNotePackagedSectionDto } from '../model/dto.js'; import type { OneNoteTableOfContents, OneNoteTableOfContentsEntry, } from '../parser/parse-toc.js'; import { DESKTOP_REVISION_STORE_FORMAT, ONE_SECTION_FILE_TYPE, } from '../parser/detect-format.js'; import type { OnePkgError } from './errors.js'; import { buildOneNotePackageTree, resolveTocChildPath, } from './toc-hierarchy.js'; import type { ExtractedCabFile } from './types.js'; const parseTocMock = vi.hoisted(() => vi.fn()); vi.mock('../parser/parse-toc.js', async (importOriginal) => ({ ...(await importOriginal()), parseOneNoteTableOfContents: parseTocMock, })); beforeEach(() => parseTocMock.mockReset()); describe('ONEPKG TOC child paths', () => { it('joins a single NFC-normalized child name within its section group', () => { expect(resolveTocChildPath('Projects', 'Cafe\u0301.one')).toBe( 'Projects/Café.one' ); }); it.each(['../Secret.one', '..', 'nested/Secret.one', 'C:\\Secret.one'])( 'rejects unsafe FolderChildFilename %s', (filename) => { expect(() => resolveTocChildPath('Projects', filename)).toThrow( /Unsafe FolderChildFilename/u ); } ); }); describe('ONEPKG TOC hierarchy budgets and indexes', () => { it('resolves identity fallbacks in authored order with near-linear section lookups', async () => { const count = 400; const pathReads = { count: 0 }; const entries: OneNoteTableOfContentsEntry[] = []; const extracted: ExtractedCabFile[] = [tocFile()]; const sections: OneNotePackagedSectionDto[] = []; for (let index = 0; index < count; index += 1) { const identity = indexedGuid(index + 1); const path = `Section-${index.toString().padStart(4, '0')}.one`; extracted.push(sectionFile(path, identity, index + 1)); sections.push(sectionWithObservedPath(path, pathReads)); entries.push({ filename: `Alias-${index.toString().padStart(4, '0')}.one`, fileIdentity: identity, orderingId: index, }); } parseTocMock.mockReturnValue(toc(entries)); let yields = 0; const result = await buildOneNotePackageTree(extracted, sections, { limits: { maxTocOperations: count * 8 + 100 }, cancellation: { yield: () => { yields += 1; }, }, }); expect(result.tree.nodes).toHaveLength(count); expect( result.tree.nodes.map((node) => node.kind === 'section' ? node.sectionId : `group:${node.id}` ) ).toEqual(sections.map(({ id }) => id)); expect(pathReads.count).toBeLessThanOrEqual(count * 5); expect(yields).toBeGreaterThan(0); }); it('keeps ambiguous identities unresolved and fallback sections sorted', async () => { const identity = indexedGuid(42); parseTocMock.mockReturnValue( toc([{ filename: 'Alias.one', fileIdentity: identity, orderingId: 1 }]) ); const sections = [section('Z.one'), section('A.one')]; const result = await buildOneNotePackageTree( [ tocFile(), sectionFile('Z.one', identity, 1), sectionFile('A.one', identity, 2), ], sections ); expect( result.tree.nodes.map((node) => node.kind === 'section' ? node.sectionId : `group:${node.id}` ) ).toEqual(['A.one', 'Z.one']); expect(result.diagnostics.map(({ code }) => code)).toEqual( expect.arrayContaining([ 'onetoc2-entry-unresolved', 'onetoc2-unlisted-sections', ]) ); expect(result.diagnostics).not.toEqual( expect.arrayContaining([ expect.objectContaining({ code: 'onetoc2-section-filename-mismatch', }), ]) ); }); it('caps adversarial unresolved-entry diagnostics with an explicit marker', async () => { parseTocMock.mockReturnValue( toc( Array.from({ length: 50 }, (_, index) => ({ filename: `Missing-${index}.one`, fileIdentity: indexedGuid(index + 1), orderingId: index, })) ) ); const result = await buildOneNotePackageTree([tocFile()], [], { limits: { maxTocDiagnostics: 5, maxTocOperations: 1_000 }, }); expect(result.diagnostics).toHaveLength(5); expect(result.diagnostics.slice(0, 4).map(({ code }) => code)).toEqual([ 'onetoc2-entry-unresolved', 'onetoc2-entry-unresolved', 'onetoc2-entry-unresolved', 'onetoc2-entry-unresolved', ]); expect(result.diagnostics[4]?.code).toBe('onetoc2-diagnostic-limit'); }); it('fails closed at the hierarchy operation limit', async () => { parseTocMock.mockReturnValue( toc( Array.from({ length: 20 }, (_, index) => ({ filename: `Missing-${index}.one`, fileIdentity: indexedGuid(index + 1), orderingId: index, })) ) ); await expect( buildOneNotePackageTree([tocFile()], [], { limits: { maxTocOperations: 10 }, }) ).rejects.toEqual( expect.objectContaining>({ diagnostic: expect.objectContaining({ code: 'onetoc2-operation-limit', }), }) ); }); it('yields and observes cancellation during a large hierarchy build', async () => { parseTocMock.mockReturnValue( toc( Array.from({ length: 1_000 }, (_, index) => ({ filename: `Missing-${index}.one`, fileIdentity: indexedGuid(index + 1), orderingId: index, })) ) ); const controller = new AbortController(); let yields = 0; await expect( buildOneNotePackageTree([tocFile()], [], { cancellation: { signal: controller.signal, yield: () => { yields += 1; controller.abort(); }, }, }) ).rejects.toEqual( expect.objectContaining>({ diagnostic: expect.objectContaining({ code: 'onepkg-cancelled' }), }) ); expect(yields).toBe(1); }); }); function toc(entries: OneNoteTableOfContentsEntry[]): OneNoteTableOfContents { return { sourceName: 'Open Notebook.onetoc2', fileIdentity: indexedGuid(0), entries, diagnostics: [], }; } function tocFile(): ExtractedCabFile { return extractedFile('Open Notebook.onetoc2', Uint8Array.of(1), 0); } function sectionFile( path: string, identity: string, index: number ): ExtractedCabFile { const bytes = new Uint8Array(64); writeGuid(bytes, 0, ONE_SECTION_FILE_TYPE); writeGuid(bytes, 16, identity); writeGuid(bytes, 48, DESKTOP_REVISION_STORE_FORMAT); return extractedFile(path, bytes, index); } function extractedFile( path: string, bytes: Uint8Array, index: number ): ExtractedCabFile { return { index, path, normalizedPath: path, folderIndex: 0, uncompressedOffset: 0, uncompressedSize: bytes.byteLength, attributes: 0, compression: { kind: 'none', raw: 0, label: 'None' }, bytes, }; } function section(path: string): OneNotePackagedSectionDto { return { id: path, path, displayName: path.slice(0, -4), parseStatus: 'not-requested', diagnostics: [], }; } function sectionWithObservedPath( path: string, reads: { count: number } ): OneNotePackagedSectionDto { const result = section(path); Object.defineProperty(result, 'path', { enumerable: true, get: () => { reads.count += 1; return path; }, }); return result; } function indexedGuid(index: number): string { const plain = index.toString(16).toUpperCase().padStart(32, '0'); return `{${plain.slice(0, 8)}-${plain.slice(8, 12)}-${plain.slice(12, 16)}-${plain.slice(16, 20)}-${plain.slice(20)}}`; } function writeGuid(target: Uint8Array, offset: number, guid: string): void { const plain = guid.replace(/[{}-]/gu, ''); const canonical = Array.from({ length: 16 }, (_, index) => Number.parseInt(plain.slice(index * 2, index * 2 + 2), 16) ); const order = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]; for (let index = 0; index < order.length; index += 1) { target[offset + order[index]!] = canonical[index]!; } }