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

View file

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

View file

@ -1,5 +1,6 @@
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { createHash } from 'crypto';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
@ -999,7 +1000,12 @@ describe('ChangeExtractorService', () => {
await fs.mkdir(projectPath, { recursive: true }); await fs.mkdir(projectPath, { recursive: true });
await writeOpenCodeDeliveryLedger(tmpDir); await writeOpenCodeDeliveryLedger(tmpDir);
let deliveryContextHashVerified = false;
const backfillOpenCodeTaskLedger = vi.fn(async (input: any) => { 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); await writeOpenCodeLedgerBundle(input.projectDir, projectPath);
return { return {
schemaVersion: 1, schemaVersion: 1,
@ -1069,6 +1075,12 @@ describe('ChangeExtractorService', () => {
attributionMode: 'strict-delivery', 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(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]).not.toHaveProperty('evidenceMode');
expect(workerClient.computeTaskChanges).not.toHaveBeenCalled(); expect(workerClient.computeTaskChanges).not.toHaveBeenCalled();
}); });
@ -1513,6 +1525,7 @@ describe('ChangeExtractorService', () => {
projectDir, projectDir,
workspaceRoot: projectPath, workspaceRoot: projectPath,
deliveryContextPath: expect.stringContaining('delivery-context.json'), deliveryContextPath: expect.stringContaining('delivery-context.json'),
deliveryContextHash: expect.stringMatching(/^[a-f0-9]{64}$/),
attributionMode: 'strict-delivery', attributionMode: 'strict-delivery',
}) })
); );
@ -1621,6 +1634,7 @@ describe('ChangeExtractorService', () => {
expect(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]?.deliveryContextPath).toEqual( expect(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]?.deliveryContextPath).toEqual(
expect.stringContaining('delivery-context.json') 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 () => { 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(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]?.deliveryContextPath).toEqual(
expect.stringContaining('delivery-context.json') 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(backfillOpenCodeTaskLedger.mock.calls[1]?.[0]?.deliveryContextPath).toEqual(
expect.stringContaining('delivery-context.json') 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', taskDisplayId: 'abc12345',
projectDir: '/claude/project', projectDir: '/claude/project',
workspaceRoot: '/repo', workspaceRoot: '/repo',
deliveryContextPath: '/tmp/claude-team-opencode-ledger-context-test/delivery-context.json',
deliveryContextHash: 'a'.repeat(64),
}) })
).resolves.toMatchObject({ ).resolves.toMatchObject({
outcome: 'imported', outcome: 'imported',
@ -184,6 +186,8 @@ describe('OpenCodeReadinessBridge', () => {
taskDisplayId: 'abc12345', taskDisplayId: 'abc12345',
projectDir: '/claude/project', projectDir: '/claude/project',
workspaceRoot: '/repo', workspaceRoot: '/repo',
deliveryContextPath: '/tmp/claude-team-opencode-ledger-context-test/delivery-context.json',
deliveryContextHash: 'a'.repeat(64),
}, },
{ {
cwd: '/repo', cwd: '/repo',