From f8aedc858b491789560ce6d6f3b1db6c857dfd94 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 23:03:12 -0700 Subject: [PATCH] Preserve failed context build metadata --- packages/cli/src/context-build-view.test.ts | 30 +++++++++++++++ packages/cli/src/context-build-view.ts | 6 +-- packages/cli/src/setup-context.test.ts | 41 +++++++++++++++++++++ packages/cli/src/setup-context.ts | 20 +++++++++- 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/context-build-view.test.ts b/packages/cli/src/context-build-view.test.ts index 3e0c0049..e0132d33 100644 --- a/packages/cli/src/context-build-view.test.ts +++ b/packages/cli/src/context-build-view.test.ts @@ -587,6 +587,36 @@ describe('runContextBuild', () => { ], }); }); + + it('returns report IDs parsed from failed source-ingest target output', async () => { + const io = makeIo(); + const project = projectWithConnections({ + warehouse: { driver: 'postgres' }, + dbt_main: { driver: 'dbt' }, + }); + const executeTarget = vi.fn(async (target, _args, targetIo) => { + if (target.operation === 'scan') { + return successResult(target.connectionId, target.driver, target.operation); + } + + targetIo.stdout.write('Report: report-dbt-failed\n'); + targetIo.stdout.write('Work units: 3\n'); + return failedResult(target.connectionId, target.driver, target.operation); + }); + + const result = await runContextBuild( + project, + { projectDir: '/tmp/project', inputMode: 'disabled' }, + io.io, + { executeTarget, now: () => 1000 }, + ); + + expect(result).toMatchObject({ + exitCode: 1, + detached: false, + reportIds: ['report-dbt-failed'], + }); + }); }); describe('viewStateFromSourceProgress', () => { diff --git a/packages/cli/src/context-build-view.ts b/packages/cli/src/context-build-view.ts index 96a5c173..6b706ae7 100644 --- a/packages/cli/src/context-build-view.ts +++ b/packages/cli/src/context-build-view.ts @@ -648,10 +648,10 @@ export async function runContextBuild( targetState.status = failed ? 'failed' : 'done'; targetState.detailLine = null; const capturedOutput = capture.captured(); + const metadata = collectOutputMetadata(capturedOutput, targetState.target.operation); + for (const reportId of metadata.reportIds) reportIds.add(reportId); + for (const artifactPath of metadata.artifactPaths) artifactPaths.add(artifactPath); if (!failed) { - const metadata = collectOutputMetadata(capturedOutput, targetState.target.operation); - for (const reportId of metadata.reportIds) reportIds.add(reportId); - for (const artifactPath of metadata.artifactPaths) artifactPaths.add(artifactPath); targetState.summaryText = targetState.target.operation === 'scan' ? parseScanSummary(capturedOutput) diff --git a/packages/cli/src/setup-context.test.ts b/packages/cli/src/setup-context.test.ts index 0d803b7b..89de6a91 100644 --- a/packages/cli/src/setup-context.test.ts +++ b/packages/cli/src/setup-context.test.ts @@ -215,6 +215,47 @@ describe('setup context build state', () => { expect(io.stdout()).toContain('KTX context is ready for agents.'); }); + it('records only failed sources as retryable when the context build fails', async () => { + await writeReadyProject(tempDir); + const io = makeIo(); + const runContextBuildMock = vi.fn(async (_project, _args, _io, hooks) => { + hooks.onSourceProgress?.([ + { connectionId: 'warehouse', operation: 'scan', status: 'done', elapsedMs: 1000 }, + { connectionId: 'docs', operation: 'source-ingest', status: 'failed', elapsedMs: 2000 }, + ]); + return { + exitCode: 1, + detached: false, + reportIds: ['report-docs-failed'], + artifactPaths: ['raw-sources/docs/notion/sync-1/ingest-report.json'], + }; + }); + + await expect( + runKtxSetupContextStep( + { projectDir: tempDir, inputMode: 'disabled' }, + io.io, + { + runIdFactory: () => 'setup-context-local-failed', + now: () => new Date('2026-05-09T10:00:00.000Z'), + runContextBuild: runContextBuildMock, + }, + ), + ).resolves.toEqual({ status: 'failed', projectDir: tempDir }); + + await expect(readKtxSetupContextState(tempDir)).resolves.toMatchObject({ + runId: 'setup-context-local-failed', + status: 'failed', + reportIds: ['report-docs-failed'], + artifactPaths: ['raw-sources/docs/notion/sync-1/ingest-report.json'], + retryableFailedTargets: ['docs'], + sourceProgress: [ + { connectionId: 'warehouse', operation: 'scan', status: 'done', elapsedMs: 1000 }, + { connectionId: 'docs', operation: 'source-ingest', status: 'failed', elapsedMs: 2000 }, + ], + }); + }); + it('marks context complete without prompting when initial source ingest already made agent context', async () => { await writeReadyProject(tempDir); await mkdir(join(tempDir, 'semantic-layer', 'dbt-main'), { recursive: true }); diff --git a/packages/cli/src/setup-context.ts b/packages/cli/src/setup-context.ts index fc7a1aef..00928706 100644 --- a/packages/cli/src/setup-context.ts +++ b/packages/cli/src/setup-context.ts @@ -234,6 +234,24 @@ function normalizeSourceProgress(value: unknown): ContextBuildSourceProgressUpda return entries.length > 0 ? entries : undefined; } +function setupContextTargetIds(targets: KtxSetupContextTargets): string[] { + return [...new Set([...targets.primarySourceConnectionIds, ...targets.contextSourceConnectionIds])]; +} + +function retryableFailedTargetsFromProgress( + targets: KtxSetupContextTargets, + progress: ContextBuildSourceProgressUpdate[] | undefined, +): string[] { + const targetIds = setupContextTargetIds(targets); + if (!progress || progress.length === 0) { + return targetIds; + } + + const failedIds = new Set(progress.filter((source) => source.status === 'failed').map((source) => source.connectionId)); + const failedTargets = targetIds.filter((connectionId) => failedIds.has(connectionId)); + return failedTargets.length > 0 ? failedTargets : targetIds; +} + export async function readKtxSetupContextState(projectDir: string): Promise { const filePath = statePath(projectDir); if (!(await pathExists(filePath))) { @@ -614,7 +632,7 @@ async function runBuild( updatedAt, reportIds: completedReportIds, artifactPaths: completedArtifactPaths, - retryableFailedTargets: [...targets.primarySourceConnectionIds, ...targets.contextSourceConnectionIds], + retryableFailedTargets: retryableFailedTargetsFromProgress(targets, lastSourceProgress), failureReason: 'Context build failed.', ...(lastSourceProgress ? { sourceProgress: lastSourceProgress } : {}), });