import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createHash } from 'crypto'; import { structuredPatch } from 'diff'; import { computeDiffContextHash } from '@shared/utils/diffContextHash'; import type { SnippetDiff } from '@shared/types'; vi.mock('fs/promises', async (importOriginal) => { const actual = await importOriginal(); const readFile = vi.fn(); const writeFile = vi.fn(); const unlink = vi.fn(); const mkdir = vi.fn(); return { ...actual, mkdir, readFile, writeFile, unlink, // ESM interop: some code paths expect a default export default: { ...actual, mkdir, readFile, writeFile, unlink }, }; }); describe('ReviewApplierService', () => { beforeEach(() => { vi.resetAllMocks(); }); it('previewReject avoids write-update snippet-level replacement', async () => { const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const original = 'hello\nworld\n'; const modified = 'HELLO\nworld\n'; // Sanity: ensure there is at least one hunk for this change const patch = structuredPatch('file', 'file', original, modified); expect(patch.hunks.length).toBeGreaterThan(0); const snippets: SnippetDiff[] = [ { toolUseId: 't1', filePath: '/tmp/file.txt', toolName: 'Write', type: 'write-update', oldString: '', newString: modified, // full file write replaceAll: false, timestamp: new Date().toISOString(), isError: false, }, ]; const svc = new ReviewApplierService(); // Preview should restore original content (and must not collapse to empty due to write-update). const preview = await svc.previewReject('/tmp/file.txt', original, modified, [0], snippets); expect(preview.hasConflicts).toBe(false); expect(preview.preview).toBe(original); }); it('deletes a newly created file when fully rejected', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; readFile.mockResolvedValue('content\n'); unlink.mockResolvedValue(undefined); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const filePath = '/tmp/new-file.txt'; const snippets: SnippetDiff[] = [ { toolUseId: 't1', filePath, toolName: 'Write', type: 'write-new', oldString: '', newString: 'content\n', replaceAll: false, timestamp: new Date().toISOString(), isError: false, }, ]; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' }, }, ], }, new Map([ [ filePath, { filePath, relativePath: 'new-file.txt', snippets, linesAdded: 1, linesRemoved: 0, isNewFile: true, originalFullContent: '', modifiedFullContent: 'content\n', contentSource: 'snippet-reconstruction', }, ], ]) ); expect(res.applied).toBe(1); expect(unlink).toHaveBeenCalledWith(filePath); expect(writeFile).not.toHaveBeenCalled(); }); it('ledger create reject deletes only when current hash matches', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; const content = 'created\n'; readFile.mockResolvedValue(content); unlink.mockResolvedValue(undefined); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const filePath = '/tmp/ledger-created.txt'; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' }, }, ], }, new Map([ [ filePath, { filePath, relativePath: 'ledger-created.txt', snippets: [ { toolUseId: 'ledger-1', filePath, toolName: 'Bash', type: 'shell-snapshot', oldString: '', newString: content, replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-1', source: 'ledger-snapshot', confidence: 'high', originalFullContent: null, modifiedFullContent: content, beforeHash: null, afterHash: sha(content), operation: 'create', beforeState: { exists: false }, afterState: { exists: true, sha256: sha(content), sizeBytes: content.length }, }, }, ], linesAdded: 1, linesRemoved: 0, isNewFile: true, originalFullContent: '', modifiedFullContent: content, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res).toMatchObject({ applied: 1, conflicts: 0 }); expect(unlink).toHaveBeenCalledWith(filePath); }); it('ledger create reject blocks metadata-only create even when final hash is known', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const filePath = '/tmp/metadata-only-created.txt'; const content = 'created\n'; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [{ filePath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' } }], }, new Map([ [ filePath, { filePath, relativePath: 'metadata-only-created.txt', snippets: [ { toolUseId: 'ledger-1', filePath, toolName: 'Edit', type: 'edit', oldString: '', newString: '', replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-1', source: 'ledger-snapshot', confidence: 'medium', originalFullContent: null, modifiedFullContent: null, beforeHash: null, afterHash: sha(content), operation: 'create', beforeState: { exists: false, unavailableReason: 'gitless-before-content-unavailable', }, afterState: { exists: true, sha256: sha(content), sizeBytes: content.length }, }, }, ], linesAdded: 0, linesRemoved: 0, isNewFile: true, originalFullContent: null, modifiedFullContent: null, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res.applied).toBe(0); expect(res.errors[0]?.code).toBe('manual-review-required'); expect(readFile).not.toHaveBeenCalled(); expect(unlink).not.toHaveBeenCalled(); }); it('ledger create reject blocks when current hash changed', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; readFile.mockResolvedValue('user changed\n'); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const filePath = '/tmp/ledger-conflict.txt'; const ledgerContent = 'created\n'; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [{ filePath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' } }], }, new Map([ [ filePath, { filePath, relativePath: 'ledger-conflict.txt', snippets: [ { toolUseId: 'ledger-1', filePath, toolName: 'Bash', type: 'shell-snapshot', oldString: '', newString: ledgerContent, replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-1', source: 'ledger-snapshot', confidence: 'high', originalFullContent: null, modifiedFullContent: ledgerContent, beforeHash: null, afterHash: sha(ledgerContent), operation: 'create', beforeState: { exists: false }, afterState: { exists: true, sha256: sha(ledgerContent) }, }, }, ], linesAdded: 1, linesRemoved: 0, isNewFile: true, originalFullContent: '', modifiedFullContent: ledgerContent, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res.applied).toBe(0); expect(res.conflicts).toBe(1); expect(res.errors[0]?.code).toBe('conflict'); expect(unlink).not.toHaveBeenCalled(); }); it('ledger delete reject restores only when file is missing', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; readFile.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })); writeFile.mockResolvedValue(undefined); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const filePath = '/tmp/deleted.txt'; const original = 'restore me\n'; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [{ filePath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' } }], }, new Map([ [ filePath, { filePath, relativePath: 'deleted.txt', snippets: [ { toolUseId: 'ledger-1', filePath, toolName: 'Bash', type: 'shell-snapshot', oldString: original, newString: '', replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-1', source: 'ledger-snapshot', confidence: 'high', originalFullContent: original, modifiedFullContent: null, beforeHash: sha(original), afterHash: null, operation: 'delete', beforeState: { exists: true, sha256: sha(original) }, afterState: { exists: false }, }, }, ], linesAdded: 0, linesRemoved: 1, isNewFile: false, originalFullContent: original, modifiedFullContent: '', contentSource: 'ledger-snapshot', }, ], ]) ); expect(res.applied).toBe(1); expect(writeFile).toHaveBeenCalledWith(filePath, original, 'utf8'); }); it('ledger binary or large unavailable content requires manual review', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; readFile.mockResolvedValue('binary placeholder'); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const filePath = '/tmp/blob.bin'; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [{ filePath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' } }], }, new Map([ [ filePath, { filePath, relativePath: 'blob.bin', snippets: [ { toolUseId: 'ledger-1', filePath, toolName: 'Bash', type: 'shell-snapshot', oldString: '', newString: '', replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-1', source: 'ledger-snapshot', confidence: 'high', originalFullContent: null, modifiedFullContent: null, beforeHash: null, afterHash: null, operation: 'modify', beforeState: { exists: true, unavailableReason: 'binary file' }, afterState: { exists: true, unavailableReason: 'binary file' }, }, }, ], linesAdded: 0, linesRemoved: 0, isNewFile: false, originalFullContent: null, modifiedFullContent: null, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res.applied).toBe(0); expect(res.errors[0]?.code).toBe('manual-review-required'); expect(writeFile).not.toHaveBeenCalled(); }); it('ledger rename reject restores old path and deletes new path with hash guards', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; const mkdir = fsPromises.mkdir as unknown as ReturnType; const oldPath = '/repo/src/old.ts'; const newPath = '/repo/src/new.ts'; const oldContent = 'old\n'; const newContent = 'new\n'; readFile.mockImplementation(async (filePath: string) => { if (filePath === newPath) return newContent; if (filePath === oldPath) throw Object.assign(new Error('missing'), { code: 'ENOENT' }); throw new Error(`unexpected read ${filePath}`); }); mkdir.mockResolvedValue(undefined); writeFile.mockResolvedValue(undefined); unlink.mockResolvedValue(undefined); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const relation = { kind: 'rename' as const, oldPath: 'src/old.ts', newPath: 'src/new.ts' }; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath: newPath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' } }, ], }, new Map([ [ newPath, { filePath: newPath, relativePath: 'src/new.ts', snippets: [ { toolUseId: 'ledger-1', filePath: oldPath, toolName: 'Bash', type: 'shell-snapshot', oldString: oldContent, newString: '', replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-old', source: 'ledger-snapshot', confidence: 'high', originalFullContent: oldContent, modifiedFullContent: null, beforeHash: sha(oldContent), afterHash: null, operation: 'delete', beforeState: { exists: true, sha256: sha(oldContent) }, afterState: { exists: false }, relation, }, }, { toolUseId: 'ledger-1', filePath: newPath, toolName: 'Bash', type: 'shell-snapshot', oldString: '', newString: newContent, replaceAll: false, timestamp: '2026-03-01T10:00:01.000Z', isError: false, ledger: { eventId: 'event-new', source: 'ledger-snapshot', confidence: 'high', originalFullContent: null, modifiedFullContent: newContent, beforeHash: null, afterHash: sha(newContent), operation: 'create', beforeState: { exists: false }, afterState: { exists: true, sha256: sha(newContent) }, relation, }, }, ], linesAdded: 1, linesRemoved: 1, isNewFile: false, originalFullContent: oldContent, modifiedFullContent: newContent, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res).toMatchObject({ applied: 1, conflicts: 0 }); expect(mkdir).toHaveBeenCalledWith('/repo/src', { recursive: true }); expect(writeFile).toHaveBeenCalledWith(oldPath, oldContent, 'utf8'); expect(unlink).toHaveBeenCalledWith(newPath); }); it('ledger rename reject blocks when new path hash changed', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; const oldPath = '/repo/src/old.ts'; const newPath = '/repo/src/new.ts'; const oldContent = 'old\n'; const newContent = 'new\n'; readFile.mockResolvedValue('user changed\n'); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const relation = { kind: 'rename' as const, oldPath: 'src/old.ts', newPath: 'src/new.ts' }; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath: newPath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' } }, ], }, new Map([ [ newPath, { filePath: newPath, relativePath: 'src/new.ts', snippets: [ { toolUseId: 'ledger-1', filePath: oldPath, toolName: 'Bash', type: 'shell-snapshot', oldString: oldContent, newString: '', replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-old', source: 'ledger-snapshot', confidence: 'high', originalFullContent: oldContent, modifiedFullContent: null, beforeHash: sha(oldContent), afterHash: null, operation: 'delete', beforeState: { exists: true, sha256: sha(oldContent) }, afterState: { exists: false }, relation, }, }, { toolUseId: 'ledger-1', filePath: newPath, toolName: 'Bash', type: 'shell-snapshot', oldString: '', newString: newContent, replaceAll: false, timestamp: '2026-03-01T10:00:01.000Z', isError: false, ledger: { eventId: 'event-new', source: 'ledger-snapshot', confidence: 'high', originalFullContent: null, modifiedFullContent: newContent, beforeHash: null, afterHash: sha(newContent), operation: 'create', beforeState: { exists: false }, afterState: { exists: true, sha256: sha(newContent) }, relation, }, }, ], linesAdded: 1, linesRemoved: 1, isNewFile: false, originalFullContent: oldContent, modifiedFullContent: newContent, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res.applied).toBe(0); expect(res.conflicts).toBe(1); expect(res.errors[0]?.code).toBe('conflict'); expect(writeFile).not.toHaveBeenCalled(); expect(unlink).not.toHaveBeenCalled(); }); it('ledger rename reject resolves Windows relation paths case-insensitively', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; const mkdir = fsPromises.mkdir as unknown as ReturnType; const newPath = 'C:\\Repo\\SRC\\New.ts'; const expectedOldPath = 'C:/Repo/src/OLD.ts'; const oldContent = 'old\n'; const newContent = 'new\n'; readFile.mockImplementation(async (filePath: string) => { if (filePath === newPath) return newContent; if (filePath === expectedOldPath) throw Object.assign(new Error('missing'), { code: 'ENOENT' }); throw new Error(`unexpected read ${filePath}`); }); mkdir.mockResolvedValue(undefined); writeFile.mockResolvedValue(undefined); unlink.mockResolvedValue(undefined); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const relation = { kind: 'rename' as const, oldPath: 'src/OLD.ts', newPath: 'src/NEW.ts' }; const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath: newPath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' } }, ], }, new Map([ [ newPath, { filePath: newPath, relativePath: 'SRC\\New.ts', snippets: [ { toolUseId: 'ledger-1', filePath: newPath, toolName: 'Bash', type: 'shell-snapshot', oldString: '', newString: newContent, replaceAll: false, timestamp: '2026-03-01T10:00:01.000Z', isError: false, ledger: { eventId: 'event-new', source: 'ledger-snapshot', confidence: 'high', originalFullContent: null, modifiedFullContent: newContent, beforeHash: null, afterHash: sha(newContent), operation: 'create', beforeState: { exists: false }, afterState: { exists: true, sha256: sha(newContent) }, relation, }, }, ], linesAdded: 1, linesRemoved: 1, isNewFile: false, originalFullContent: oldContent, modifiedFullContent: newContent, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res).toMatchObject({ applied: 1, conflicts: 0 }); expect(mkdir).toHaveBeenCalledWith('C:/Repo/src', { recursive: true }); expect(writeFile).toHaveBeenCalledWith(expectedOldPath, oldContent, 'utf8'); expect(unlink).toHaveBeenCalledWith(newPath); }); it('ledger rename reject does not infer related paths from unsafe suffix matches', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; const newPath = 'C:\\Repo\\src\\renew.ts'; const newContent = 'new\n'; const relation = { kind: 'rename' as const, oldPath: 'old.ts', newPath: 'new.ts' }; const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath: newPath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' } }, ], }, new Map([ [ newPath, { filePath: newPath, relativePath: 'src\\renew.ts', snippets: [ { toolUseId: 'ledger-1', filePath: newPath, toolName: 'Bash', type: 'shell-snapshot', oldString: '', newString: newContent, replaceAll: false, timestamp: '2026-03-01T10:00:01.000Z', isError: false, ledger: { eventId: 'event-new', source: 'ledger-snapshot', confidence: 'high', originalFullContent: null, modifiedFullContent: newContent, beforeHash: null, afterHash: sha(newContent), operation: 'create', beforeState: { exists: false }, afterState: { exists: true, sha256: sha(newContent) }, relation, }, }, ], linesAdded: 1, linesRemoved: 1, isNewFile: false, originalFullContent: 'old\n', modifiedFullContent: newContent, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res.errors[0]?.code).toBe('manual-review-required'); expect(readFile).not.toHaveBeenCalled(); expect(writeFile).not.toHaveBeenCalled(); expect(unlink).not.toHaveBeenCalled(); }); it('treats delete-then-create on an existing ledger file as modify, not new-file delete', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; const unlink = fsPromises.unlink as unknown as ReturnType; const filePath = '/tmp/replaced.ts'; const original = 'export const value = 1;\n'; const modified = 'export const value = 2;\n'; readFile.mockResolvedValue(modified); writeFile.mockResolvedValue(undefined); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected', 1: 'rejected' }, }, ], }, new Map([ [ filePath, { filePath, relativePath: 'replaced.ts', snippets: [ { toolUseId: 'ledger-delete', filePath, toolName: 'Bash', type: 'shell-snapshot', oldString: original, newString: '', replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-delete', source: 'ledger-snapshot', confidence: 'high', originalFullContent: original, modifiedFullContent: null, beforeHash: sha(original), afterHash: null, operation: 'delete', beforeState: { exists: true, sha256: sha(original) }, afterState: { exists: false }, }, }, { toolUseId: 'ledger-create', filePath, toolName: 'Bash', type: 'shell-snapshot', oldString: '', newString: modified, replaceAll: false, timestamp: '2026-03-01T10:00:01.000Z', isError: false, ledger: { eventId: 'event-create', source: 'ledger-snapshot', confidence: 'high', originalFullContent: null, modifiedFullContent: modified, beforeHash: null, afterHash: sha(modified), operation: 'create', beforeState: { exists: false }, afterState: { exists: true, sha256: sha(modified) }, }, }, ], linesAdded: 1, linesRemoved: 1, isNewFile: false, originalFullContent: original, modifiedFullContent: modified, contentSource: 'ledger-snapshot', }, ], ]) ); expect(res).toMatchObject({ applied: 1, conflicts: 0 }); expect(writeFile).toHaveBeenCalledWith(filePath, original, 'utf8'); expect(unlink).not.toHaveBeenCalled(); }); it('ledger full modify reject accepts legacy afterHash when afterState hash is absent', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; const filePath = '/tmp/legacy-ledger.ts'; const original = 'export const value = 1;\n'; const modified = 'export const value = 2;\n'; readFile.mockResolvedValue(modified); writeFile.mockResolvedValue(undefined); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath, fileDecision: 'rejected', hunkDecisions: { 0: 'rejected' }, }, ], }, new Map([ [ filePath, { filePath, relativePath: 'legacy-ledger.ts', snippets: [ { toolUseId: 'ledger-1', filePath, toolName: 'Edit', type: 'edit', oldString: original, newString: modified, replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-1', source: 'ledger-exact', confidence: 'exact', originalFullContent: original, modifiedFullContent: modified, beforeHash: sha(original), afterHash: sha(modified), operation: 'modify', beforeState: { exists: true, sha256: sha(original) }, afterState: { exists: true }, }, }, ], linesAdded: 1, linesRemoved: 1, isNewFile: false, originalFullContent: original, modifiedFullContent: modified, contentSource: 'ledger-exact', }, ], ]) ); expect(res).toMatchObject({ applied: 1, conflicts: 0 }); expect(writeFile).toHaveBeenCalledWith(filePath, original, 'utf8'); }); it('ledger exact partial reject stays in the strict ledger lane and applies inverse hunk patch', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; const filePath = '/tmp/exact-ledger.ts'; const original = 'const value = 1;\nconst keep = true;\n'; const modified = 'const value = 2;\nconst keep = true;\n'; readFile.mockResolvedValue(modified); writeFile.mockResolvedValue(undefined); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath, fileDecision: 'pending', hunkDecisions: { 0: 'rejected', 1: 'pending' }, hunkContextHashes: buildHunkContextHashes(original, modified), }, ], }, new Map([ [ filePath, { filePath, relativePath: 'exact-ledger.ts', snippets: [ { toolUseId: 'ledger-1', filePath, toolName: 'Edit', type: 'edit', oldString: 'const value = 1;\n', newString: 'const value = 2;\n', replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-1', source: 'ledger-exact', confidence: 'exact', originalFullContent: original, modifiedFullContent: modified, beforeHash: sha(original), afterHash: sha(modified), operation: 'modify', beforeState: { exists: true, sha256: sha(original) }, afterState: { exists: true, sha256: sha(modified) }, }, }, ], linesAdded: 1, linesRemoved: 1, isNewFile: false, originalFullContent: original, modifiedFullContent: modified, contentSource: 'ledger-exact', }, ], ]) ); expect(res).toMatchObject({ applied: 1, conflicts: 0 }); expect(writeFile).toHaveBeenCalledWith(filePath, original, 'utf8'); }); it('ledger partial reject refuses stale hunk context instead of falling back to index', async () => { const fsPromises = await import('fs/promises'); const readFile = fsPromises.readFile as unknown as ReturnType; const writeFile = fsPromises.writeFile as unknown as ReturnType; const filePath = '/tmp/stale-ledger.ts'; const original = 'const value = 1;\nconst keep = true;\n'; const modified = 'const value = 2;\nconst keep = true;\n'; readFile.mockResolvedValue(modified); const { ReviewApplierService } = await import('@main/services/team/ReviewApplierService'); const svc = new ReviewApplierService(); const res = await svc.applyReviewDecisions( { teamName: 'team', decisions: [ { filePath, fileDecision: 'pending', hunkDecisions: { 0: 'rejected', 1: 'pending' }, hunkContextHashes: { 0: 'stale-context-hash' }, }, ], }, new Map([ [ filePath, { filePath, relativePath: 'stale-ledger.ts', snippets: [ { toolUseId: 'ledger-1', filePath, toolName: 'Edit', type: 'edit', oldString: 'const value = 1;\n', newString: 'const value = 2;\n', replaceAll: false, timestamp: '2026-03-01T10:00:00.000Z', isError: false, ledger: { eventId: 'event-1', source: 'ledger-exact', confidence: 'exact', originalFullContent: original, modifiedFullContent: modified, beforeHash: sha(original), afterHash: sha(modified), operation: 'modify', beforeState: { exists: true, sha256: sha(original) }, afterState: { exists: true, sha256: sha(modified) }, }, }, ], linesAdded: 1, linesRemoved: 1, isNewFile: false, originalFullContent: original, modifiedFullContent: modified, contentSource: 'ledger-exact', }, ], ]) ); expect(res.applied).toBe(0); expect(res.conflicts).toBe(1); expect(res.errors[0]?.code).toBe('conflict'); expect(writeFile).not.toHaveBeenCalled(); }); }); function sha(content: string): string { return createHash('sha256').update(content).digest('hex'); } function buildHunkContextHashes(original: string, modified: string): Record { const patch = structuredPatch('file', 'file', original, modified); const out: Record = {}; for (let i = 0; i < patch.hunks.length; i++) { const hunk = patch.hunks[i]!; const oldSideContent = hunk.lines .filter((line) => !line.startsWith('+')) .map((line) => line.slice(1)) .join('\n'); const newSideContent = hunk.lines .filter((line) => !line.startsWith('-')) .map((line) => line.slice(1)) .join('\n'); out[i] = computeDiffContextHash(oldSideContent, newSideContent); } return out; }