fix(changes): pass verified opencode delivery context
This commit is contained in:
parent
ee590d0a62
commit
53012ed623
4 changed files with 43 additions and 17 deletions
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue