fix(changes): pass verified opencode delivery context

This commit is contained in:
777genius 2026-04-28 22:52:05 +03:00
parent ee590d0a62
commit 53012ed623
4 changed files with 43 additions and 17 deletions

View file

@ -9,7 +9,7 @@ import {
} from '@shared/utils/taskChangeState';
import { createHash } from 'crypto';
import { existsSync } from 'fs';
import { mkdtemp, readdir, readFile, rm, stat, writeFile } from 'fs/promises';
import { chmod, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
@ -77,6 +77,7 @@ interface OpenCodeBackfillAttempt {
interface OpenCodeDeliveryContextTempFile {
filePath: string | null;
hash: string | null;
cleanup: () => Promise<void>;
}
@ -513,7 +514,12 @@ export class ChangeExtractorService {
workspaceRoot,
attributionMode: OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE,
...(backfillMemberName ? { memberName: backfillMemberName } : {}),
...(deliveryContext.filePath ? { deliveryContextPath: deliveryContext.filePath } : {}),
...(deliveryContext.filePath
? {
deliveryContextPath: deliveryContext.filePath,
deliveryContextHash: deliveryContext.hash ?? undefined,
}
: {}),
});
void appendOpenCodeTaskChangeDiag({
event: 'backfill_result',
@ -661,27 +667,26 @@ export class ChangeExtractorService {
records: Awaited<ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>>
): Promise<OpenCodeDeliveryContextTempFile> {
if (records.length === 0) {
return { filePath: null, cleanup: async () => undefined };
return { filePath: null, hash: null, cleanup: async () => undefined };
}
const dir = await mkdtemp(path.join(os.tmpdir(), 'claude-team-opencode-ledger-context-'));
await chmod(dir, 0o700).catch(() => undefined);
const filePath = path.join(dir, 'delivery-context.json');
await writeFile(
filePath,
`${JSON.stringify(
{
schemaVersion: 1,
teamName,
taskId,
records,
},
null,
2
)}\n`,
{ encoding: 'utf8', mode: 0o600 }
);
const rawContext = `${JSON.stringify(
{
schemaVersion: 1,
teamName,
taskId,
records,
},
null,
2
)}\n`;
await writeFile(filePath, rawContext, { encoding: 'utf8', mode: 0o600 });
return {
filePath,
hash: createHash('sha256').update(rawContext).digest('hex'),
cleanup: async () => {
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
},

View file

@ -239,6 +239,7 @@ export interface OpenCodeBackfillTaskLedgerCommandBody {
projectDir?: string;
workspaceRoot?: string;
deliveryContextPath?: string;
deliveryContextHash?: string;
attributionMode?: OpenCodeBackfillTaskLedgerAttributionMode;
dryRun?: boolean;
}

View file

@ -1,5 +1,6 @@
import * as os from 'os';
import * as path from 'path';
import { createHash } from 'crypto';
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'fs/promises';
@ -999,7 +1000,12 @@ describe('ChangeExtractorService', () => {
await fs.mkdir(projectPath, { recursive: true });
await writeOpenCodeDeliveryLedger(tmpDir);
let deliveryContextHashVerified = false;
const backfillOpenCodeTaskLedger = vi.fn(async (input: any) => {
deliveryContextHashVerified =
createHash('sha256')
.update(await fs.readFile(input.deliveryContextPath, 'utf8'))
.digest('hex') === input.deliveryContextHash;
await writeOpenCodeLedgerBundle(input.projectDir, projectPath);
return {
schemaVersion: 1,
@ -1069,6 +1075,12 @@ describe('ChangeExtractorService', () => {
attributionMode: 'strict-delivery',
})
);
const backfillInput = backfillOpenCodeTaskLedger.mock.calls[0]?.[0];
expect(backfillInput.deliveryContextPath).toEqual(
expect.stringContaining('delivery-context.json')
);
expect(backfillInput.deliveryContextHash).toMatch(/^[a-f0-9]{64}$/);
expect(deliveryContextHashVerified).toBe(true);
expect(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]).not.toHaveProperty('evidenceMode');
expect(workerClient.computeTaskChanges).not.toHaveBeenCalled();
});
@ -1513,6 +1525,7 @@ describe('ChangeExtractorService', () => {
projectDir,
workspaceRoot: projectPath,
deliveryContextPath: expect.stringContaining('delivery-context.json'),
deliveryContextHash: expect.stringMatching(/^[a-f0-9]{64}$/),
attributionMode: 'strict-delivery',
})
);
@ -1621,6 +1634,7 @@ describe('ChangeExtractorService', () => {
expect(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]?.deliveryContextPath).toEqual(
expect.stringContaining('delivery-context.json')
);
expect(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]?.deliveryContextHash).toMatch(/^[a-f0-9]{64}$/);
});
it('does not cache negative OpenCode backfill while delivery context already exists', async () => {
@ -1732,8 +1746,10 @@ describe('ChangeExtractorService', () => {
expect(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]?.deliveryContextPath).toEqual(
expect.stringContaining('delivery-context.json')
);
expect(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]?.deliveryContextHash).toMatch(/^[a-f0-9]{64}$/);
expect(backfillOpenCodeTaskLedger.mock.calls[1]?.[0]?.deliveryContextPath).toEqual(
expect.stringContaining('delivery-context.json')
);
expect(backfillOpenCodeTaskLedger.mock.calls[1]?.[0]?.deliveryContextHash).toMatch(/^[a-f0-9]{64}$/);
});
});

View file

@ -170,6 +170,8 @@ describe('OpenCodeReadinessBridge', () => {
taskDisplayId: 'abc12345',
projectDir: '/claude/project',
workspaceRoot: '/repo',
deliveryContextPath: '/tmp/claude-team-opencode-ledger-context-test/delivery-context.json',
deliveryContextHash: 'a'.repeat(64),
})
).resolves.toMatchObject({
outcome: 'imported',
@ -184,6 +186,8 @@ describe('OpenCodeReadinessBridge', () => {
taskDisplayId: 'abc12345',
projectDir: '/claude/project',
workspaceRoot: '/repo',
deliveryContextPath: '/tmp/claude-team-opencode-ledger-context-test/delivery-context.json',
deliveryContextHash: 'a'.repeat(64),
},
{
cwd: '/repo',