fix(changes): derive opencode backfill member from delivery
This commit is contained in:
parent
819a1f6e8f
commit
ba09010fcb
2 changed files with 170 additions and 6 deletions
|
|
@ -413,6 +413,10 @@ export class ChangeExtractorService {
|
|||
input.teamName,
|
||||
input.taskId
|
||||
);
|
||||
const backfillMemberName = this.resolveOpenCodeBackfillMemberName(
|
||||
input.effectiveOptions.owner,
|
||||
deliveryContextRecords
|
||||
);
|
||||
const deliveryContextFingerprint =
|
||||
this.hashOpenCodeDeliveryContextRecords(deliveryContextRecords);
|
||||
|
||||
|
|
@ -444,7 +448,7 @@ export class ChangeExtractorService {
|
|||
teamName: input.teamName,
|
||||
taskId: input.taskId,
|
||||
displayId: input.taskMeta?.displayId ?? null,
|
||||
memberName: input.effectiveOptions.owner ?? null,
|
||||
memberName: backfillMemberName ?? input.effectiveOptions.owner ?? null,
|
||||
projectDir,
|
||||
workspaceRoot,
|
||||
sourceGeneration,
|
||||
|
|
@ -466,7 +470,8 @@ export class ChangeExtractorService {
|
|||
workspaceRoot,
|
||||
cacheKey,
|
||||
deliveryContextRecords,
|
||||
sourceGeneration
|
||||
sourceGeneration,
|
||||
backfillMemberName
|
||||
).finally(() => {
|
||||
this.openCodeBackfillInFlight.delete(cacheKey);
|
||||
});
|
||||
|
|
@ -482,7 +487,8 @@ export class ChangeExtractorService {
|
|||
deliveryContextRecords: Awaited<
|
||||
ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>
|
||||
>,
|
||||
sourceGeneration: string | null
|
||||
sourceGeneration: string | null,
|
||||
backfillMemberName?: string
|
||||
): Promise<boolean> {
|
||||
const deliveryContext = await this.createOpenCodeDeliveryContextTempFile(
|
||||
input.teamName,
|
||||
|
|
@ -495,10 +501,10 @@ export class ChangeExtractorService {
|
|||
teamName: input.teamName,
|
||||
taskId: input.taskId,
|
||||
taskDisplayId: input.taskMeta?.displayId,
|
||||
memberName: input.effectiveOptions.owner,
|
||||
projectDir,
|
||||
workspaceRoot,
|
||||
attributionMode: OPEN_CODE_AUTO_BACKFILL_ATTRIBUTION_MODE,
|
||||
...(backfillMemberName ? { memberName: backfillMemberName } : {}),
|
||||
...(deliveryContext.filePath ? { deliveryContextPath: deliveryContext.filePath } : {}),
|
||||
});
|
||||
void appendOpenCodeTaskChangeDiag({
|
||||
|
|
@ -507,7 +513,7 @@ export class ChangeExtractorService {
|
|||
teamName: input.teamName,
|
||||
taskId: input.taskId,
|
||||
displayId: input.taskMeta?.displayId ?? null,
|
||||
memberName: input.effectiveOptions.owner ?? null,
|
||||
memberName: backfillMemberName ?? input.effectiveOptions.owner ?? null,
|
||||
projectDir,
|
||||
workspaceRoot,
|
||||
sourceGeneration,
|
||||
|
|
@ -562,7 +568,7 @@ export class ChangeExtractorService {
|
|||
teamName: input.teamName,
|
||||
taskId: input.taskId,
|
||||
displayId: input.taskMeta?.displayId ?? null,
|
||||
memberName: input.effectiveOptions.owner ?? null,
|
||||
memberName: backfillMemberName ?? input.effectiveOptions.owner ?? null,
|
||||
projectDir,
|
||||
workspaceRoot,
|
||||
deliveryRecordCount: deliveryContextRecords.length,
|
||||
|
|
@ -745,6 +751,18 @@ export class ChangeExtractorService {
|
|||
return records.slice(-200);
|
||||
}
|
||||
|
||||
private resolveOpenCodeBackfillMemberName(
|
||||
owner: string | undefined,
|
||||
records: Awaited<ReturnType<ChangeExtractorService['readOpenCodeDeliveryContextRecords']>>
|
||||
): string | undefined {
|
||||
const members = [...new Set(records.map((record) => record.memberName.trim()).filter(Boolean))];
|
||||
const normalizedOwner = owner?.trim();
|
||||
if (normalizedOwner && members.includes(normalizedOwner)) {
|
||||
return normalizedOwner;
|
||||
}
|
||||
return members.length === 1 ? members[0] : undefined;
|
||||
}
|
||||
|
||||
private async readOpenCodeRuntimeLaneIdsFromDisk(
|
||||
teamsBasePath: string,
|
||||
teamName: string
|
||||
|
|
|
|||
|
|
@ -1064,6 +1064,152 @@ describe('ChangeExtractorService', () => {
|
|||
expect(workerClient.computeTaskChanges).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the OpenCode delivery member when the current task owner changed later', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'change-extractor-service-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
await writeTaskFile(tmpDir, { displayId: 'abc12345', owner: 'alice' });
|
||||
const projectDir = path.join(tmpDir, 'project-dir');
|
||||
const projectPath = path.join(tmpDir, 'repo');
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
await fs.mkdir(projectPath, { recursive: true });
|
||||
await writeOpenCodeDeliveryLedger(tmpDir, { memberName: 'bob' });
|
||||
|
||||
const backfillOpenCodeTaskLedger = vi.fn(async (input: any) => ({
|
||||
schemaVersion: 1,
|
||||
providerId: 'opencode',
|
||||
teamName: input.teamName,
|
||||
taskId: input.taskId,
|
||||
projectDir: input.projectDir,
|
||||
workspaceRoot: input.workspaceRoot,
|
||||
dryRun: false,
|
||||
attributionMode: input.attributionMode,
|
||||
scannedSessions: 0,
|
||||
scannedToolparts: 0,
|
||||
candidateEvents: 0,
|
||||
importedEvents: 0,
|
||||
skippedEvents: 0,
|
||||
outcome: 'no-history',
|
||||
notices: [],
|
||||
diagnostics: [],
|
||||
}));
|
||||
const workerClient = {
|
||||
isAvailable: vi.fn(() => true),
|
||||
computeTaskChanges: vi.fn(async () =>
|
||||
makeTaskChangeResult(TASK_ID, { content: '', confidence: 'fallback' })
|
||||
),
|
||||
};
|
||||
|
||||
const service = new ChangeExtractorService(
|
||||
{
|
||||
getLogSourceWatchContext: vi.fn(async () => ({
|
||||
projectDir,
|
||||
projectPath,
|
||||
sessionIds: [],
|
||||
})),
|
||||
findLogFileRefsForTask: vi.fn(async () => []),
|
||||
findMemberLogPaths: vi.fn(async () => []),
|
||||
} as any,
|
||||
{
|
||||
parseBoundaries: vi.fn(async () => ({
|
||||
boundaries: [],
|
||||
scopes: [],
|
||||
isSingleTaskSession: true,
|
||||
detectedMechanism: 'none' as const,
|
||||
})),
|
||||
} as any,
|
||||
{ getConfig: vi.fn(async () => ({ projectPath })) } as any,
|
||||
undefined,
|
||||
workerClient as any,
|
||||
{ backfillOpenCodeTaskLedger } as any,
|
||||
{ getMeta: vi.fn(async () => ({ providerId: 'opencode' })) } as any
|
||||
);
|
||||
|
||||
await service.getTaskChanges(TEAM_NAME, TASK_ID, {
|
||||
owner: 'alice',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
expect(backfillOpenCodeTaskLedger).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
memberName: 'bob',
|
||||
attributionMode: 'strict-delivery',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('omits member filter when multiple OpenCode delivery members match the task', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'change-extractor-service-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
await writeTaskFile(tmpDir, { displayId: 'abc12345', owner: 'alice' });
|
||||
const projectDir = path.join(tmpDir, 'project-dir');
|
||||
const projectPath = path.join(tmpDir, 'repo');
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
await fs.mkdir(projectPath, { recursive: true });
|
||||
await writeOpenCodeDeliveryLedger(tmpDir, { memberName: 'bob', runtimeSessionId: 'session-1' });
|
||||
await writeOpenCodeDeliveryLedger(tmpDir, {
|
||||
memberName: 'carol',
|
||||
runtimeSessionId: 'session-2',
|
||||
});
|
||||
|
||||
const backfillOpenCodeTaskLedger = vi.fn(async (input: any) => ({
|
||||
schemaVersion: 1,
|
||||
providerId: 'opencode',
|
||||
teamName: input.teamName,
|
||||
taskId: input.taskId,
|
||||
projectDir: input.projectDir,
|
||||
workspaceRoot: input.workspaceRoot,
|
||||
dryRun: false,
|
||||
attributionMode: input.attributionMode,
|
||||
scannedSessions: 0,
|
||||
scannedToolparts: 0,
|
||||
candidateEvents: 0,
|
||||
importedEvents: 0,
|
||||
skippedEvents: 0,
|
||||
outcome: 'no-history',
|
||||
notices: [],
|
||||
diagnostics: [],
|
||||
}));
|
||||
const workerClient = {
|
||||
isAvailable: vi.fn(() => true),
|
||||
computeTaskChanges: vi.fn(async () =>
|
||||
makeTaskChangeResult(TASK_ID, { content: '', confidence: 'fallback' })
|
||||
),
|
||||
};
|
||||
|
||||
const service = new ChangeExtractorService(
|
||||
{
|
||||
getLogSourceWatchContext: vi.fn(async () => ({
|
||||
projectDir,
|
||||
projectPath,
|
||||
sessionIds: [],
|
||||
})),
|
||||
findLogFileRefsForTask: vi.fn(async () => []),
|
||||
findMemberLogPaths: vi.fn(async () => []),
|
||||
} as any,
|
||||
{
|
||||
parseBoundaries: vi.fn(async () => ({
|
||||
boundaries: [],
|
||||
scopes: [],
|
||||
isSingleTaskSession: true,
|
||||
detectedMechanism: 'none' as const,
|
||||
})),
|
||||
} as any,
|
||||
{ getConfig: vi.fn(async () => ({ projectPath })) } as any,
|
||||
undefined,
|
||||
workerClient as any,
|
||||
{ backfillOpenCodeTaskLedger } as any,
|
||||
{ getMeta: vi.fn(async () => ({ providerId: 'opencode' })) } as any
|
||||
);
|
||||
|
||||
await service.getTaskChanges(TEAM_NAME, TASK_ID, {
|
||||
owner: 'alice',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
expect(backfillOpenCodeTaskLedger).toHaveBeenCalledTimes(1);
|
||||
expect(backfillOpenCodeTaskLedger.mock.calls[0]?.[0]).not.toHaveProperty('memberName');
|
||||
});
|
||||
|
||||
it('does not run OpenCode backfill for explicit non-OpenCode teams even if stale runtime files exist', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'change-extractor-service-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
|
|
|||
Loading…
Reference in a new issue