文件
mynode/packages/lib/services/ai/tools/utils/noteOutline.test.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

115 行
3.7 KiB
TypeScript

import { noteOutline, noteSections, renderOutline, resolveSection } from './noteOutline';
describe('noteOutline', () => {
test('should build positional paths reflecting nesting', () => {
const body = [
'Preamble text',
'',
'# One',
'',
'## One A',
'',
'## One B',
'',
'### One B i',
'',
'# Two',
'',
].join('\n');
expect(noteOutline(body).map(e => [e.path, e.heading])).toEqual([
['0', ''],
['1', 'One'],
['1.1', 'One A'],
['1.2', 'One B'],
['1.2.1', 'One B i'],
['2', 'Two'],
]);
});
test.each([
['### deep\n\n# top\n\n## mid\n', ['1', '2', '2.1']],
['# a\n\n### x\n\n## y\n\n# b\n', ['1', '1.1', '1.2', '2']],
['## a\n\n## b\n\n# c\n\n## d\n', ['1', '2', '3', '3.1']],
])('should keep paths unique when heading levels are irregular (%#)', (body, expected) => {
expect(noteOutline(body).map(e => e.path)).toEqual(expected);
});
test('should omit the preamble entry when there is no content before the first heading', () => {
const outline = noteOutline('# One\n\ntext\n');
expect(outline.map(e => e.path)).toEqual(['1']);
});
test('should return a single section covering the whole body when there are no headings', () => {
const body = 'Just some text\n\nand more.\n';
expect(noteOutline(body)).toEqual([{ path: '0', heading: '', chars: body.length }]);
});
test('should include subsections in a section size and extent', () => {
const body = '# One\n\nabc\n\n## One A\n\ndef\n\n# Two\n\nxyz\n';
const sections = noteSections(body);
const one = sections.find(s => s.path === '1');
expect(body.slice(one.start, one.end)).toBe('# One\n\nabc\n\n## One A\n\ndef\n\n');
expect(noteOutline(body).find(e => e.path === '1').chars).toBe(one.end - one.start);
});
test('should ignore heading-like lines inside code blocks', () => {
const body = '# Real\n\n```\n# Fake\n```\n';
expect(noteOutline(body).map(e => e.heading)).toEqual(['Real']);
});
test('should detect setext headings', () => {
const body = 'Title\n=====\n\ntext\n\nSub\n---\n\nmore\n';
expect(noteOutline(body).map(e => [e.path, e.heading])).toEqual([['1', 'Title'], ['1.1', 'Sub']]);
});
test.each([
['1.2', 'Target'],
['Target', 'Target'],
[' target ', 'Target'],
])('should resolve section address %s', (address, expectedHeading) => {
const body = '# One\n\n## Other\n\n## Target\n\ncontent\n';
const resolution = resolveSection(body, address);
expect(resolution.error).toBeNull();
expect(resolution.section.heading).toBe(expectedHeading);
});
test.each([
['# a\tb\n', 'a b'],
['a\nb\n===\n', 'a b'],
['# spaced out \n', 'spaced out'],
])('should collapse whitespace in headings (%#)', (body, expected) => {
expect(noteOutline(body)[0].heading).toBe(expected);
// The collapsed form must still resolve, since that is what the model sees.
expect(resolveSection(body, expected).section.path).toBe('1');
});
test('should render the outline as tab-separated lines', () => {
const body = '# One\n\nabc\n\n## One A\n\ndef\n';
expect(renderOutline(body)).toBe([
'path\tchars\theading',
'1\t26\tOne',
'1.1\t14\tOne A',
].join('\n'));
});
test('should error and list candidates when several headings match', () => {
const body = '# Desktop\n\n## Build\n\n# Mobile\n\n## Build\n';
const resolution = resolveSection(body, 'Build');
expect(resolution.section).toBeNull();
expect(resolution.error).toContain('1.1 (under "Desktop")');
expect(resolution.error).toContain('2.1 (under "Mobile")');
});
test.each([
['9.9'],
['No such heading'],
])('should error on unresolvable address %s', (address) => {
const resolution = resolveSection('# One\n\ntext\n', address);
expect(resolution.section).toBeNull();
expect(resolution.error).toBeTruthy();
});
});