test(sync): implement test-strategy Phase 1-2 (pure unit/golden/property), +102 tests
Work through test-strategy-report.md, high-ROI no-refactor subset (no regen). - R-Infra: vitest resolve.alias docmost-client -> packages/docmost-client/src (fixes the dist-vs-src coverage artifact: canonicalize 0% -> real) - R-Cfg-1: export parseArgs + tests - canonicalize: align family / comment.resolved kept / link non-default + fixpoint & docsCanonicallyEqual reflexive/symmetric properties (0 -> 100%) - markdown-converter golden matrix: columns/embed/audio/pdf, drawio data-align rule, inline-mark matrix, textAlign, escaping idempotence, table sanitization (61 -> 79%) - schema parse-closures via generateJSON (TextStyle/comment/mention/Highlight/Column) - node-ops (immutability, table edge cases, makeFreshId property), transforms (setCalloutRange/insertMarkerAfter/commentsToFootnotes + renumber property) - stabilize normalize-on-write fixpoint (0 -> 100%); diff coarse-fallback; client-utils; firstDivergence; corpus fixtures details/columns/mention - 593 -> 695 green; build clean; corpus STABLE Deferred (Phase 3-4, refactor-gated): pull/collab/client-REST/git-merge integration.
This commit is contained in:
@@ -284,5 +284,46 @@ describe('applyTextEdits', () => {
|
||||
{ find: 'gamma', replacements: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('a LATE edit failure leaves the INPUT document unmodified (no partial mutation)', () => {
|
||||
// The first edit is valid but a later one is absent and throws. Because the
|
||||
// function mutates only a deep copy and throws before returning, the
|
||||
// caller's input must be byte-identical afterwards (no partial apply leaks).
|
||||
const doc = singleParagraph('alpha beta');
|
||||
const snapshot = structuredClone(doc);
|
||||
expect(() =>
|
||||
applyTextEdits(doc, [
|
||||
{ find: 'alpha', replace: 'A' }, // would succeed
|
||||
{ find: 'MISSING', replace: 'X' }, // throws "text not found"
|
||||
]),
|
||||
).toThrow(/text not found/);
|
||||
// Input doc is untouched: the successful first edit was applied only to the
|
||||
// internal copy, which was discarded when the second edit threw.
|
||||
expect(doc).toEqual(snapshot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find === replace and self-containing replaceAll', () => {
|
||||
it('a find === replace edit is a no-op on the text but still reports a replacement', () => {
|
||||
const doc = singleParagraph('hello world');
|
||||
const { doc: result, results } = applyTextEdits(doc, [
|
||||
{ find: 'world', replace: 'world' },
|
||||
]);
|
||||
expect(result.content[0].content[0].text).toBe('hello world');
|
||||
// The replacement still counts (it spliced the same value back in).
|
||||
expect(results).toEqual([{ find: 'world', replacements: 1 }]);
|
||||
});
|
||||
|
||||
it('replaceAll where the replacement CONTAINS the find ("a" -> "aa") does not re-scan', () => {
|
||||
// split().join() replaces all original occurrences in one pass; it must NOT
|
||||
// re-match the inserted "aa" (which would loop). "a a a" -> "aa aa aa".
|
||||
const doc = singleParagraph('a a a');
|
||||
const { doc: result, results } = applyTextEdits(doc, [
|
||||
{ find: 'a', replace: 'aa', replaceAll: true },
|
||||
]);
|
||||
expect(result.content[0].content[0].text).toBe('aa aa aa');
|
||||
// Exactly the 3 original occurrences are counted, not 6.
|
||||
expect(results).toEqual([{ find: 'a', replacements: 3 }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user