agent-ecosystem/test/main/services/team/ReviewApplierService.test.ts

1134 lines
38 KiB
TypeScript

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<typeof import('fs/promises')>();
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<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
const mkdir = fsPromises.mkdir as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
const mkdir = fsPromises.mkdir as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
const unlink = fsPromises.unlink as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
const writeFile = fsPromises.writeFile as unknown as ReturnType<typeof vi.fn>;
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<number, string> {
const patch = structuredPatch('file', 'file', original, modified);
const out: Record<number, string> = {};
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;
}