From b175d1ac7308dc63239f26d41e87a46ba2d22532 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 13 May 2026 19:46:55 +0200 Subject: [PATCH] fix: add public ingest retry guidance --- packages/cli/src/public-ingest.test.ts | 32 +++++++++++++++- packages/cli/src/public-ingest.ts | 52 ++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/public-ingest.test.ts b/packages/cli/src/public-ingest.test.ts index 3dd1174c..13c0fc00 100644 --- a/packages/cli/src/public-ingest.test.ts +++ b/packages/cli/src/public-ingest.test.ts @@ -533,7 +533,37 @@ describe('runKtxPublicIngest', () => { ); expect(io.stdout()).toContain('Ingest finished with partial failures'); expect(io.stdout()).toContain('warehouse failed at database-schema.'); - expect(io.stdout()).toContain('Debug: ktx ingest warehouse --debug'); + expect(io.stdout()).toContain('Retry: ktx ingest warehouse --project-dir /tmp/project --fast'); + expect(io.stdout()).not.toContain('Debug: ktx ingest warehouse --debug'); + }); + + it('prints query-history retry guidance for query-history facet failures', async () => { + const io = makeIo(); + const project = deepReadyProject({ + warehouse: { driver: 'postgres', context: { depth: 'deep' } }, + }); + const runScan = vi.fn(async () => 0); + const runIngest = vi.fn(async () => 1); + + await expect( + runKtxPublicIngest( + { + command: 'run', + projectDir: '/tmp/project', + targetConnectionId: 'warehouse', + all: false, + json: false, + inputMode: 'disabled', + queryHistory: 'enabled', + }, + io.io, + { loadProject: vi.fn(async () => project), runScan, runIngest }, + ), + ).resolves.toBe(1); + + expect(io.stdout()).toContain('warehouse failed at query-history.'); + expect(io.stdout()).toContain('Retry: ktx ingest warehouse --project-dir /tmp/project --deep --query-history'); + expect(io.stdout()).not.toContain('historic-sql'); }); it('fails deep-readiness targets before work starts while continuing independent --all targets', async () => { diff --git a/packages/cli/src/public-ingest.ts b/packages/cli/src/public-ingest.ts index f2119edf..3669dcd6 100644 --- a/packages/cli/src/public-ingest.ts +++ b/packages/cli/src/public-ingest.ts @@ -412,8 +412,43 @@ function defaultSteps(target: KtxPublicIngestPlanTarget): KtxPublicIngestTargetR ]; } +function retryCommandForTarget( + target: KtxPublicIngestPlanTarget, + args: Extract, +): string { + const projectPart = ` --project-dir ${args.projectDir}`; + const depthPart = target.databaseDepth ? ` --${target.databaseDepth}` : ''; + const queryHistoryPart = target.queryHistory?.enabled === true ? ' --query-history' : ''; + const windowPart = + target.queryHistory?.enabled === true && target.queryHistory.windowDays !== undefined + ? ` --query-history-window-days ${target.queryHistory.windowDays}` + : ''; + return `ktx ingest ${target.connectionId}${projectPart}${depthPart}${queryHistoryPart}${windowPart}`; +} + +function trimTrailingPeriod(value: string): string { + return value.endsWith('.') ? value.slice(0, -1) : value; +} + +function failureDetailWithRetry(input: { + target: KtxPublicIngestPlanTarget; + args: Extract; + failedOperation: KtxPublicIngestStepName; + failureDetail?: string; +}): string { + const detail = input.failureDetail?.trim(); + const base = + detail && detail.startsWith(`${input.target.connectionId} `) + ? detail + : detail + ? `${input.target.connectionId} failed: ${detail}` + : `${input.target.connectionId} failed at ${input.failedOperation}.`; + return `${trimTrailingPeriod(base)}. Retry: ${retryCommandForTarget(input.target, input.args)}`; +} + function markTargetResult( target: KtxPublicIngestPlanTarget, + args: Extract, status: 'done' | 'failed', failedOperation?: KtxPublicIngestStepName, failureDetail?: string, @@ -434,7 +469,12 @@ function markTargetResult( return { ...step, status: 'failed', - detail: failureDetail ?? `${target.connectionId} failed at ${selectedFailedOperation}.`, + detail: failureDetailWithRetry({ + target, + args, + failedOperation: selectedFailedOperation, + failureDetail, + }), }; } return { ...step, status: 'not-run' }; @@ -478,9 +518,6 @@ function renderPlainResults(results: KtxPublicIngestTargetResult[], io: KtxCliIo continue; } io.stdout.write(` ${failedStep.detail ?? `${result.connectionId} failed.`}\n`); - if (failedStep.debugCommand) { - io.stdout.write(` Debug: ${failedStep.debugCommand}\n`); - } } } @@ -572,6 +609,7 @@ export async function executePublicIngestTarget( if (scanExitCode !== 0) { return markTargetResult( target, + args, 'failed', 'database-schema', capturedScanIo ? firstCapturedFailureLine(capturedScanIo.capturedOutput()) : undefined, @@ -596,11 +634,11 @@ export async function executePublicIngestTarget( }; const qhExitCode = await runIngest(ingestArgs, io); if (qhExitCode !== 0) { - return markTargetResult(target, 'failed', 'query-history'); + return markTargetResult(target, args, 'failed', 'query-history'); } } - return markTargetResult(target, 'done'); + return markTargetResult(target, args, 'done'); } const { runKtxIngest } = await import('./ingest.js'); @@ -618,7 +656,7 @@ export async function executePublicIngestTarget( const exitCode = deps.ingestProgress ? await runIngest(ingestArgs, io, { progress: deps.ingestProgress }) : await runIngest(ingestArgs, io); - return markTargetResult(target, exitCode === 0 ? 'done' : 'failed'); + return markTargetResult(target, args, exitCode === 0 ? 'done' : 'failed'); } export async function runKtxPublicIngest(