From 2266f91e05896ca272d4c0da0be5c947cde2cc9f Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 13 May 2026 20:16:08 +0200 Subject: [PATCH] fix(cli): sanitize public ingest progress copy --- packages/cli/src/context-build-view.test.ts | 31 ++++++++++++++++++++ packages/cli/src/context-build-view.ts | 18 ++++++------ packages/cli/src/public-ingest.test.ts | 32 +++++++++++++++++++++ packages/cli/src/public-ingest.ts | 8 +----- 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/context-build-view.test.ts b/packages/cli/src/context-build-view.test.ts index ce5d36b2..65da90ba 100644 --- a/packages/cli/src/context-build-view.test.ts +++ b/packages/cli/src/context-build-view.test.ts @@ -625,6 +625,37 @@ describe('runContextBuild', () => { expect(io.stdout()).not.toContain('historic-sql'); }); + it('renders database ingest progress without scan wording', async () => { + const io = makeIo(); + const project = projectWithConnections({ warehouse: { driver: 'postgres' } }); + const executeTarget = vi.fn(async (target, _args, _targetIo, deps) => { + await deps.scanProgress?.update(0.05, 'Preparing scan'); + await deps.scanProgress?.update(0.15, 'Inspecting database schema'); + await deps.scanProgress?.update(0.7, 'Writing schema artifacts'); + return successResult(target.connectionId, target.driver, target.operation); + }); + + await expect( + runContextBuild( + project, + { + projectDir: '/tmp/project', + inputMode: 'disabled', + targetConnectionId: 'warehouse', + all: false, + }, + io.io, + { executeTarget, now: () => 1000, sourceProgressThrottleMs: 0 }, + ), + ).resolves.toMatchObject({ exitCode: 0 }); + + expect(io.stdout()).toContain('Preparing database ingest'); + expect(io.stdout()).toContain('Reading database schema'); + expect(io.stdout()).toContain('Writing schema context'); + expect(io.stdout()).not.toContain('Preparing scan'); + expect(io.stdout()).not.toMatch(/\bscan\b/i); + }); + it('passes schema-first notices from the plan into foreground output', async () => { const io = makeIo(); const project: KtxPublicIngestProject = { diff --git a/packages/cli/src/context-build-view.ts b/packages/cli/src/context-build-view.ts index 23365a28..94440091 100644 --- a/packages/cli/src/context-build-view.ts +++ b/packages/cli/src/context-build-view.ts @@ -1,6 +1,7 @@ import type { KtxProgressPort, KtxProgressUpdateOptions } from '@ktx/context/scan'; import type { KtxCliIo } from './index.js'; import type { KtxIngestProgressUpdate } from './ingest.js'; +import { publicDatabaseIngestMessage, publicQueryHistoryMessage } from './public-ingest-copy.js'; import type { KtxPublicIngestArgs, KtxPublicIngestDeps, @@ -578,17 +579,14 @@ export function initViewState(targets: KtxPublicIngestPlanTarget[]): ContextBuil } function publicProgressMessage(message: string, target: KtxPublicIngestPlanTarget): string { - if (!target.steps.includes('query-history')) { - return message; + let current = message; + if (target.operation === 'database-ingest') { + current = publicDatabaseIngestMessage(current); } - return message - .replace( - new RegExp(`Fetching source files for ${target.connectionId}/historic-sql`, 'i'), - `Fetching query history for ${target.connectionId}`, - ) - .replace(`${target.connectionId}/historic-sql`, `${target.connectionId} query history`) - .replace(/\bhistoric-sql\b/g, 'query history') - .replace(/\bhistoric SQL\b/gi, 'query history'); + if (target.steps.includes('query-history')) { + current = publicQueryHistoryMessage(current, target.connectionId); + } + return current; } function formatProgressDetail( diff --git a/packages/cli/src/public-ingest.test.ts b/packages/cli/src/public-ingest.test.ts index 78cfa453..beb3b405 100644 --- a/packages/cli/src/public-ingest.test.ts +++ b/packages/cli/src/public-ingest.test.ts @@ -523,6 +523,38 @@ describe('runKtxPublicIngest', () => { expect(io.stdout()).not.toContain('live-database'); }); + it('sanitizes captured database scan failure details in direct public output', async () => { + const io = makeIo(); + const project = deepReadyProject({ warehouse: { driver: 'postgres', context: { depth: 'deep' } } }); + const runScan = vi.fn(async (_args, scanIo) => { + scanIo.stdout.write('KTX scan enrichment failed after structural scan completed: embedding service timed out\n'); + return 1; + }); + + await expect( + runKtxPublicIngest( + { + command: 'run', + projectDir: '/tmp/project', + targetConnectionId: 'warehouse', + all: false, + json: false, + inputMode: 'disabled', + depth: 'deep', + }, + io.io, + { loadProject: vi.fn(async () => project), runScan }, + ), + ).resolves.toBe(1); + + expect(io.stdout()).toContain( + 'warehouse failed: Database enrichment failed after schema context completed: embedding service timed out.', + ); + expect(io.stdout()).toContain('Retry: ktx ingest warehouse --project-dir /tmp/project --deep'); + expect(io.stdout()).not.toContain('KTX scan enrichment failed'); + expect(io.stdout()).not.toContain('structural scan'); + }); + it('suppresses lower-level source report output during direct public source ingest', async () => { const io = makeIo(); const project = projectWithConnections({ diff --git a/packages/cli/src/public-ingest.ts b/packages/cli/src/public-ingest.ts index c81cdeb4..b6c006a6 100644 --- a/packages/cli/src/public-ingest.ts +++ b/packages/cli/src/public-ingest.ts @@ -9,6 +9,7 @@ import { isDatabaseDriver, normalizeConnectionDriver, } from './ingest-depth.js'; +import { publicIngestOutputLine } from './public-ingest-copy.js'; import type { KtxScanArgs, KtxScanDeps } from './scan.js'; import { profileMark } from './startup-profile.js'; @@ -587,13 +588,6 @@ function createCapturedPublicIngestIo(): CapturedPublicIngestIo { const INTERNAL_STATUS_LINE_RE = /^(Report|Run|Job|Status|Adapter|Connection|Sync|Diff|Work units|Saved memory|Provenance rows):\s*/; -function publicIngestOutputLine(line: string): string { - return line - .replace(/\blive-database\b/g, 'database schema') - .replace(/\bhistoric-sql\b/g, 'query history') - .replace(/\bhistoric SQL\b/gi, 'query history'); -} - function firstCapturedFailureLine(output: string): string | undefined { return output .split(/\r?\n/)