From e48ecf664abfc1a9f2b856cfff865a0fcacccb9a Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 28 Apr 2026 11:28:20 +0300 Subject: [PATCH] fix(changes): keep metadata-only ledger events manual --- .../services/team/ReviewApplierService.ts | 9 +++ .../services/team/TaskChangeLedgerReader.ts | 4 +- .../team/ReviewApplierService.test.ts | 66 +++++++++++++++++++ .../team/TaskChangeLedgerReader.test.ts | 31 ++++++++- 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/main/services/team/ReviewApplierService.ts b/src/main/services/team/ReviewApplierService.ts index 6e8face6..e81214d2 100644 --- a/src/main/services/team/ReviewApplierService.ts +++ b/src/main/services/team/ReviewApplierService.ts @@ -471,6 +471,15 @@ export class ReviewApplierService { ); const relation = this.resolveLedgerRelation(ledgerSnippets); + if (hasUnavailableState) { + return { + handled: true, + status: 'error', + code: 'manual-review-required', + error: 'Ledger content metadata is unavailable; manual review is required.', + }; + } + if (!fullReject) { if (relation?.kind === 'rename' || relation?.kind === 'copy') { return { diff --git a/src/main/services/team/TaskChangeLedgerReader.ts b/src/main/services/team/TaskChangeLedgerReader.ts index 28927f26..f36c7595 100644 --- a/src/main/services/team/TaskChangeLedgerReader.ts +++ b/src/main/services/team/TaskChangeLedgerReader.ts @@ -105,7 +105,8 @@ interface LedgerEvent { | 'powershell_snapshot' | 'post_tool_hook_snapshot' | 'opencode_toolpart_write' - | 'opencode_toolpart_edit'; + | 'opencode_toolpart_edit' + | 'opencode_toolpart_apply_patch'; operation: 'create' | 'modify' | 'delete'; confidence: LedgerConfidence; workspaceRoot: string; @@ -1135,6 +1136,7 @@ export class TaskChangeLedgerReader { case 'notebook_edit': return 'NotebookEdit'; case 'opencode_toolpart_edit': + case 'opencode_toolpart_apply_patch': return 'Edit'; case 'bash_simulated_sed': case 'shell_snapshot': diff --git a/test/main/services/team/ReviewApplierService.test.ts b/test/main/services/team/ReviewApplierService.test.ts index ed50464e..5b784788 100644 --- a/test/main/services/team/ReviewApplierService.test.ts +++ b/test/main/services/team/ReviewApplierService.test.ts @@ -189,6 +189,72 @@ describe('ReviewApplierService', () => { 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; diff --git a/test/main/services/team/TaskChangeLedgerReader.test.ts b/test/main/services/team/TaskChangeLedgerReader.test.ts index 52b4487b..a60807d0 100644 --- a/test/main/services/team/TaskChangeLedgerReader.test.ts +++ b/test/main/services/team/TaskChangeLedgerReader.test.ts @@ -241,6 +241,32 @@ describe('TaskChangeLedgerReader', () => { linesAdded: 1, linesRemoved: 1, }, + { + schemaVersion: 1, + eventId: 'event-apply-patch', + taskId: TASK_ID, + taskRef: TASK_ID, + taskRefKind: 'canonical', + phase: 'work', + executionSeq: 3, + sessionId: 'opencode-session-1', + memberName: 'bob', + toolUseId: 'part-apply-patch', + source: 'opencode_toolpart_apply_patch', + operation: 'modify', + confidence: 'medium', + workspaceRoot: '/repo', + filePath: '/repo/src/new.ts', + relativePath: 'src/new.ts', + timestamp: '2026-03-01T10:02:00.000Z', + toolStatus: 'succeeded', + before: null, + after: null, + beforeState: { exists: true, unavailableReason: 'opencode-apply-patch-before-content-unavailable' }, + afterState: { exists: true, unavailableReason: 'opencode-apply-patch-final-content-unavailable' }, + linesAdded: 0, + linesRemoved: 0, + }, ], }); @@ -254,8 +280,9 @@ describe('TaskChangeLedgerReader', () => { }); const snippets = result?.files[0]?.snippets ?? []; - expect(snippets.map((snippet) => snippet.toolName)).toEqual(['Write', 'Edit']); - expect(snippets.map((snippet) => snippet.type)).toEqual(['write-new', 'edit']); + expect(snippets.map((snippet) => snippet.toolName)).toEqual(['Write', 'Edit', 'Edit']); + expect(snippets.map((snippet) => snippet.type)).toEqual(['write-new', 'edit', 'edit']); + expect(snippets[2]?.ledger?.source).toBe('ledger-snapshot'); }); it('groups rename relations in summary-only bundles without losing absolute paths', async () => {