文件
mynode/packages/lib/services/ai/tools/utils/applyNoteEdits.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

135 行
5.5 KiB
TypeScript

import { applyAnchorEdits } from './applyNoteEdits';
describe('applyNoteEdits', () => {
it('appends with paragraph break (covered in detail below)', () => {
const { newBody } = applyAnchorEdits('hello', [
{ op: 'appendToNote', text: 'world' },
], 0);
expect(newBody).toBe('hello\n\nworld');
});
it('inserts before/after anchor', () => {
const before = applyAnchorEdits('one two three', [
{ op: 'insertBefore', anchor: 'two', text: 'X ' },
], 0);
expect(before.newBody).toBe('one X two three');
const after = applyAnchorEdits('one two three', [
{ op: 'insertAfter', anchor: 'two', text: ' X' },
], 0);
expect(after.newBody).toBe('one two X three');
});
it('replaces range and reports missing anchor', () => {
const replaced = applyAnchorEdits('alpha beta gamma', [
{ op: 'replaceRange', anchor: 'beta', text: 'BETA' },
], 0);
expect(replaced.newBody).toBe('alpha BETA gamma');
const missing = applyAnchorEdits('alpha beta gamma', [
{ op: 'replaceRange', anchor: 'delta', text: 'X' },
], 0);
expect(missing.newBody).toBe('alpha beta gamma');
expect(missing.appliedEdits[0].status).toBe('anchor-not-found');
});
it('picks anchor closest to cursor on duplicates', () => {
// "foo" appears at index 0 and index 8. Cursor near second occurrence.
const body = 'foo bar foo baz';
const { newBody } = applyAnchorEdits(body, [
{ op: 'insertAfter', anchor: 'foo', text: '!' },
], 10);
expect(newBody).toBe('foo bar foo! baz');
});
it('appends with blank line for Markdown paragraph break', () => {
// Empty body — no separator needed.
expect(applyAnchorEdits('', [{ op: 'appendToNote', text: 'first' }], 0).newBody).toBe('first');
// No trailing newline — needs full \n\n.
expect(applyAnchorEdits('hello', [{ op: 'appendToNote', text: 'world' }], 0).newBody).toBe('hello\n\nworld');
// One trailing newline — add one more to make a blank line.
expect(applyAnchorEdits('hello\n', [{ op: 'appendToNote', text: 'world' }], 0).newBody).toBe('hello\n\nworld');
// Already has blank line — no extra separator.
expect(applyAnchorEdits('hello\n\n', [{ op: 'appendToNote', text: 'world' }], 0).newBody).toBe('hello\n\nworld');
});
it('rejects invalid edits without mutating the body', () => {
// Empty anchor, missing text, wrong shape, unknown op.
const body = 'hello world';
const cases = [
{ op: 'insertBefore', anchor: '', text: 'X' },
{ op: 'insertAfter', anchor: 'world' }, // missing text
{ op: 'appendToNote' }, // missing text
{ op: 'mysteryOp', text: 'X' },
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = applyAnchorEdits(body, cases as any, 0);
expect(result.newBody).toBe(body);
expect(result.appliedEdits.every(e => e.status === 'invalid')).toBe(true);
});
it('replaces the inner content of a fenced block', () => {
const body = 'Intro paragraph.\n\n```jsoncanvas\n{"nodes":[],"edges":[]}\n```\n\nOutro.';
const { newBody, appliedEdits } = applyAnchorEdits(body, [
{ op: 'replaceFencedBlock', tag: 'jsoncanvas', text: '{"nodes":[{"id":"a"}],"edges":[]}' },
], 0);
expect(appliedEdits[0].status).toBe('applied');
expect(newBody).toContain('```jsoncanvas\n{"nodes":[{"id":"a"}],"edges":[]}\n```');
// Surrounding prose is preserved.
expect(newBody).toContain('Intro paragraph.');
expect(newBody).toContain('Outro.');
});
it('picks the fenced block closest to the cursor', () => {
const first = '```jsoncanvas\n{"v":1}\n```';
const second = '```jsoncanvas\n{"v":2}\n```';
const body = `${first}\n\nSome prose between.\n\n${second}`;
// Cursor near the second block — should edit that one, leaving the first untouched.
const cursorNearSecond = body.indexOf(second) + 5;
const { newBody } = applyAnchorEdits(body, [
{ op: 'replaceFencedBlock', tag: 'jsoncanvas', text: '{"v":99}' },
], cursorNearSecond);
expect(newBody).toContain('{"v":1}'); // first untouched
expect(newBody).toContain('{"v":99}'); // second replaced
expect(newBody).not.toContain('{"v":2}');
// Cursor at 0 — first block wins (fallback behaviour preserved).
const { newBody: firstWins } = applyAnchorEdits(body, [
{ op: 'replaceFencedBlock', tag: 'jsoncanvas', text: '{"v":0}' },
], 0);
expect(firstWins).toContain('{"v":0}');
expect(firstWins).toContain('{"v":2}'); // second untouched
expect(firstWins).not.toContain('{"v":1}');
});
it('reports anchor-not-found when fenced block missing', () => {
const body = 'No whiteboard in this note.';
const { newBody, appliedEdits } = applyAnchorEdits(body, [
{ op: 'replaceFencedBlock', tag: 'jsoncanvas', text: '{}' },
], 0);
expect(newBody).toBe(body);
expect(appliedEdits[0].status).toBe('anchor-not-found');
});
it('rejects replaceFencedBlock with unsupported tag', () => {
// Plain code fences (```js, ```python, ...) are not structured-document
// formats — the model should use anchor-based ops instead.
const body = '```js\nconsole.log("x");\n```';
const { newBody, appliedEdits } = applyAnchorEdits(body, [
{ op: 'replaceFencedBlock', tag: 'js', text: 'console.log("y");' },
], 0);
expect(newBody).toBe(body);
expect(appliedEdits[0].status).toBe('invalid');
});
it('refuses replaceRange anchor covering most of the body', () => {
const body = 'short text';
// Anchor is >50% of body — would be destructive; refuse.
const { newBody, appliedEdits } = applyAnchorEdits(body, [
{ op: 'replaceRange', anchor: 'short text', text: '' },
], 0);
expect(newBody).toBe(body);
expect(appliedEdits[0].status).toBe('invalid');
});
});