Root cause: two proven corruption mechanisms — (M1) non-idempotent apply stamped the same block N times when a quantized model re-emitted the same edit_file call or a turn was retried; (M2) Levenshtein tier 4 was fail-open with no uniqueness guard, silently splicing into the wrong location. Fixes applied at every layer of the pipeline: Matcher (fuzzy-match.ts): raise SIMILARITY_THRESHOLD 0.66 → 0.85; add AMBIGUITY_EPSILON uniqueness guard — two windows within 0.05 of the top score → ambiguous, not a guess; add block-anchor gate (≥3-line needles require first+last line exact match before a window is scored). Edit planner (pending_changes.ts): extract planEdit() as a pure function; idempotency guards detect already-applied states (anchored insert re-stamp, old-gone-but-new-present); findPendingDuplicate() collapses identical pending rows at queue time so M1 never reaches applyOne. Atomic writes (pending_changes.ts): temp-file + rename on the same filesystem so a crash can't leave a half-written source file; realpath() first so symlinks survive the rename. Per-file mutex (pending_changes.ts): withFileLock() serializes concurrent read-modify-write on the same path via a chained-Promise Map. EOL preservation (pending_changes.ts): normalize CRLF → LF for matching, restore native line ending on write so Windows-style files stay clean. Context isolation (inference_context.ts): replace module-level singleton with AsyncLocalStorage so concurrent inference runs (arena parallel dispatch, dispatcher poll racing a user message) each get their own scoped context with no clobbering. Tests: plan-edit.test.ts (pure planEdit unit tests), extended fuzzy-match and pending_changes_integration suites, ALS isolation test that proves overlapping runs get correct session IDs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { planEdit } from '../pending_changes.js';
|
|
|
|
// planEdit is the pure core of applyOne's edit splice. These tests pin the
|
|
// idempotency guards that stop the "block stamped 2-3x" corruption: applying the
|
|
// same queued edit more than once must be a no-op, never a duplicate.
|
|
|
|
describe('planEdit — normal application', () => {
|
|
it('applies a unique exact edit', () => {
|
|
const content = 'a\nfoo\nb\n';
|
|
const plan = planEdit(content, 'foo', 'bar');
|
|
expect(plan).toEqual({ kind: 'apply', updated: 'a\nbar\nb\n' });
|
|
});
|
|
|
|
it('reports ambiguous when old_string occurs more than once', () => {
|
|
const content = 'foo\nx\nfoo\n';
|
|
const plan = planEdit(content, 'foo', 'bar');
|
|
expect(plan).toEqual({ kind: 'ambiguous', count: 2 });
|
|
});
|
|
|
|
it('reports not_found when old_string is absent and new is not present', () => {
|
|
const content = 'alpha\nbeta\n';
|
|
const plan = planEdit(content, 'gamma that is clearly nowhere', 'delta');
|
|
expect(plan).toEqual({ kind: 'not_found' });
|
|
});
|
|
});
|
|
|
|
describe('planEdit — idempotency (the corruption guard)', () => {
|
|
it('treats a re-applied anchored insert as already-applied (no duplicate)', () => {
|
|
// The exact mechanism that tripled `const recordFormats` in settings.html:
|
|
// an anchored insert (old=anchor, new=anchor+block) where the anchor still
|
|
// matches uniquely after the first apply.
|
|
const oldStr = '<script>';
|
|
const newStr = '<script>\nconst recordFormats = ["gif","mp4"];';
|
|
const before = '<script>\nfunction render() {}\n</script>\n';
|
|
|
|
const first = planEdit(before, oldStr, newStr);
|
|
expect(first.kind).toBe('apply');
|
|
const after = first.kind === 'apply' ? first.updated : '';
|
|
expect((after.match(/const recordFormats/g) || []).length).toBe(1);
|
|
|
|
// Re-applying the identical edit to the already-edited content is a no-op.
|
|
const second = planEdit(after, oldStr, newStr);
|
|
expect(second).toEqual({ kind: 'noop', reason: 'already-applied' });
|
|
});
|
|
|
|
it('treats an edit whose old_string is gone but new_string is present as already-applied', () => {
|
|
const content = 'const total = sum + tax;\n';
|
|
const plan = planEdit(content, 'const subtotal = sum;', 'const total = sum + tax;');
|
|
expect(plan).toEqual({ kind: 'noop', reason: 'already-applied' });
|
|
});
|
|
|
|
it('treats a no-change splice as a noop', () => {
|
|
const content = 'a\nfoo\nb\n';
|
|
const plan = planEdit(content, 'foo', 'foo');
|
|
expect(plan).toEqual({ kind: 'noop', reason: 'identical' });
|
|
});
|
|
|
|
it('does not duplicate across three repeated applications', () => {
|
|
const oldStr = 'function f() {';
|
|
const newStr = 'function f() {\n const x = 1;';
|
|
let content = 'function f() {\n return x;\n}\n';
|
|
for (let i = 0; i < 3; i++) {
|
|
const plan = planEdit(content, oldStr, newStr);
|
|
if (plan.kind === 'apply') content = plan.updated;
|
|
}
|
|
expect((content.match(/const x = 1;/g) || []).length).toBe(1);
|
|
});
|
|
});
|