fix(changes): keep metadata-only ledger events manual
This commit is contained in:
parent
8f7712ed74
commit
e48ecf664a
4 changed files with 107 additions and 3 deletions
|
|
@ -471,6 +471,15 @@ export class ReviewApplierService {
|
||||||
);
|
);
|
||||||
const relation = this.resolveLedgerRelation(ledgerSnippets);
|
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 (!fullReject) {
|
||||||
if (relation?.kind === 'rename' || relation?.kind === 'copy') {
|
if (relation?.kind === 'rename' || relation?.kind === 'copy') {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,8 @@ interface LedgerEvent {
|
||||||
| 'powershell_snapshot'
|
| 'powershell_snapshot'
|
||||||
| 'post_tool_hook_snapshot'
|
| 'post_tool_hook_snapshot'
|
||||||
| 'opencode_toolpart_write'
|
| 'opencode_toolpart_write'
|
||||||
| 'opencode_toolpart_edit';
|
| 'opencode_toolpart_edit'
|
||||||
|
| 'opencode_toolpart_apply_patch';
|
||||||
operation: 'create' | 'modify' | 'delete';
|
operation: 'create' | 'modify' | 'delete';
|
||||||
confidence: LedgerConfidence;
|
confidence: LedgerConfidence;
|
||||||
workspaceRoot: string;
|
workspaceRoot: string;
|
||||||
|
|
@ -1135,6 +1136,7 @@ export class TaskChangeLedgerReader {
|
||||||
case 'notebook_edit':
|
case 'notebook_edit':
|
||||||
return 'NotebookEdit';
|
return 'NotebookEdit';
|
||||||
case 'opencode_toolpart_edit':
|
case 'opencode_toolpart_edit':
|
||||||
|
case 'opencode_toolpart_apply_patch':
|
||||||
return 'Edit';
|
return 'Edit';
|
||||||
case 'bash_simulated_sed':
|
case 'bash_simulated_sed':
|
||||||
case 'shell_snapshot':
|
case 'shell_snapshot':
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,72 @@ describe('ReviewApplierService', () => {
|
||||||
expect(unlink).toHaveBeenCalledWith(filePath);
|
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 () => {
|
it('ledger create reject blocks when current hash changed', async () => {
|
||||||
const fsPromises = await import('fs/promises');
|
const fsPromises = await import('fs/promises');
|
||||||
const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
|
const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,32 @@ describe('TaskChangeLedgerReader', () => {
|
||||||
linesAdded: 1,
|
linesAdded: 1,
|
||||||
linesRemoved: 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 ?? [];
|
const snippets = result?.files[0]?.snippets ?? [];
|
||||||
expect(snippets.map((snippet) => snippet.toolName)).toEqual(['Write', 'Edit']);
|
expect(snippets.map((snippet) => snippet.toolName)).toEqual(['Write', 'Edit', 'Edit']);
|
||||||
expect(snippets.map((snippet) => snippet.type)).toEqual(['write-new', '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 () => {
|
it('groups rename relations in summary-only bundles without losing absolute paths', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue