feat(ingest): use foreground view for interactive public ingest

This commit is contained in:
Andrey Avtomonov 2026-05-13 18:51:10 +02:00
parent 0764acaadd
commit 9c7b4f9b84
4 changed files with 149 additions and 5 deletions

View file

@ -428,6 +428,43 @@ describe('runContextBuild', () => {
expect(callOrder).toEqual(['warehouse', 'dbt_main']);
});
it('runs only the requested connection when foreground build receives a target', async () => {
const io = makeIo();
const project = projectWithConnections({
warehouse: { driver: 'postgres' },
docs: { driver: 'notion' },
});
const executeTarget = vi.fn(async (target) =>
successResult(target.connectionId, target.driver, target.operation),
);
await expect(
runContextBuild(
project,
{
projectDir: '/tmp/project',
inputMode: 'disabled',
targetConnectionId: 'warehouse',
all: false,
depth: 'fast',
queryHistory: 'default',
},
io.io,
{ executeTarget, now: () => 1000 },
),
).resolves.toMatchObject({ exitCode: 0 });
expect(executeTarget).toHaveBeenCalledTimes(1);
expect(executeTarget.mock.calls[0]?.[0]).toMatchObject({
connectionId: 'warehouse',
operation: 'database-ingest',
databaseDepth: 'fast',
});
expect(io.stdout()).toContain('Databases:');
expect(io.stdout()).toContain('warehouse');
expect(io.stdout()).not.toContain('docs');
});
it('returns exit code 1 when any target fails', async () => {
const io = makeIo();
const project = projectWithConnections({

View file

@ -39,6 +39,11 @@ export interface ContextBuildViewState {
export interface ContextBuildArgs {
projectDir: string;
inputMode: 'auto' | 'disabled';
targetConnectionId?: string;
all?: boolean;
depth?: Extract<KtxPublicIngestArgs, { command: 'run' }>['depth'];
queryHistory?: Extract<KtxPublicIngestArgs, { command: 'run' }>['queryHistory'];
queryHistoryWindowDays?: number;
scanMode?: 'structural' | 'enriched';
detectRelationships?: boolean;
}
@ -565,7 +570,15 @@ export async function runContextBuild(
io: KtxCliIo,
deps: ContextBuildDeps = {},
): Promise<ContextBuildResult> {
const plan = buildPublicIngestPlan(project, { projectDir: args.projectDir, all: true });
const plan = buildPublicIngestPlan(project, {
projectDir: args.projectDir,
...(args.targetConnectionId ? { targetConnectionId: args.targetConnectionId } : {}),
all: args.all ?? true,
...(args.depth ? { depth: args.depth } : {}),
...(args.queryHistory ? { queryHistory: args.queryHistory } : {}),
...(args.queryHistoryWindowDays !== undefined ? { queryHistoryWindowDays: args.queryHistoryWindowDays } : {}),
...(args.scanMode ? { scanMode: args.scanMode } : {}),
});
const state = initViewState(plan.targets);
const isTTY = io.stdout.isTTY === true;
const nowFn = deps.now ?? (() => Date.now());
@ -614,11 +627,15 @@ export async function runContextBuild(
const runArgs: Extract<KtxPublicIngestArgs, { command: 'run' }> = {
command: 'run',
projectDir: args.projectDir,
all: true,
...(args.targetConnectionId ? { targetConnectionId: args.targetConnectionId } : {}),
all: args.all ?? true,
json: false,
inputMode: args.inputMode,
scanMode: args.scanMode,
detectRelationships: args.detectRelationships,
...(args.depth ? { depth: args.depth } : {}),
...(args.queryHistory ? { queryHistory: args.queryHistory } : {}),
...(args.queryHistoryWindowDays !== undefined ? { queryHistoryWindowDays: args.queryHistoryWindowDays } : {}),
...(args.scanMode ? { scanMode: args.scanMode } : {}),
...(args.detectRelationships !== undefined ? { detectRelationships: args.detectRelationships } : {}),
};
let hasFailure = false;

View file

@ -2,11 +2,19 @@ import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '@ktx/contex
import { describe, expect, it, vi } from 'vitest';
import { buildPublicIngestPlan, type KtxPublicIngestProject, runKtxPublicIngest } from './public-ingest.js';
function makeIo(options: { isTTY?: boolean } = {}) {
function makeIo(options: { isTTY?: boolean; interactive?: boolean } = {}) {
let stdout = '';
let stderr = '';
return {
io: {
...(options.interactive
? {
stdin: {
isTTY: true,
setRawMode: vi.fn(),
},
}
: {}),
stdout: {
isTTY: options.isTTY,
write: (chunk: string) => {
@ -399,6 +407,43 @@ describe('runKtxPublicIngest', () => {
expect(io.stdout()).not.toContain('live-database');
});
it('delegates interactive TTY public ingest to the foreground context-build view', async () => {
const io = makeIo({ isTTY: true, interactive: true });
const project = projectWithConnections({ warehouse: { driver: 'postgres' } });
const runContextBuild = vi.fn(async () => ({ exitCode: 0 }));
const runScan = vi.fn(async () => 0);
await expect(
runKtxPublicIngest(
{
command: 'run',
projectDir: '/tmp/project',
targetConnectionId: 'warehouse',
all: false,
json: false,
inputMode: 'auto',
depth: 'fast',
queryHistory: 'default',
},
io.io,
{ loadProject: vi.fn(async () => project), runContextBuild, runScan },
),
).resolves.toBe(0);
expect(runContextBuild).toHaveBeenCalledWith(
project,
expect.objectContaining({
projectDir: '/tmp/project',
targetConnectionId: 'warehouse',
all: false,
depth: 'fast',
queryHistory: 'default',
}),
io.io,
);
expect(runScan).not.toHaveBeenCalled();
});
it('runs all independent targets and reports partial failures', async () => {
const io = makeIo();
const project = projectWithConnections({

View file

@ -86,10 +86,27 @@ export interface KtxPublicIngestDeps {
loadProject?: (options: Parameters<typeof loadKtxProject>[0]) => Promise<KtxPublicIngestProject>;
runScan?: (args: KtxScanArgs, io: KtxCliIo, deps?: KtxScanDeps) => Promise<number>;
runIngest?: (args: KtxIngestArgs, io: KtxCliIo, deps?: KtxIngestDeps) => Promise<number>;
runContextBuild?: (
project: KtxPublicIngestProject,
args: KtxPublicContextBuildArgs,
io: KtxCliIo,
) => Promise<{ exitCode: number }>;
scanProgress?: KtxProgressPort;
ingestProgress?: (update: KtxIngestProgressUpdate) => void;
}
interface KtxPublicContextBuildArgs {
projectDir: string;
inputMode: 'auto' | 'disabled';
targetConnectionId?: string;
all?: boolean;
depth?: KtxPublicIngestDepth;
queryHistory?: KtxPublicIngestQueryHistoryFlag;
queryHistoryWindowDays?: number;
scanMode?: Extract<KtxScanArgs, { command: 'run' }>['mode'];
detectRelationships?: boolean;
}
const sourceAdapterByDriver = new Map<string, string>([
['metabase', 'metabase'],
['local_metabase', 'metabase'],
@ -455,6 +472,13 @@ function sourceIngestOutputMode(args: Extract<KtxPublicIngestArgs, { command: 'r
return args.inputMode === 'auto' && io.stdout.isTTY === true && hasInteractiveInput(io) ? 'viz' : 'plain';
}
function shouldUseForegroundContextBuildView(
args: Extract<KtxPublicIngestArgs, { command: 'run' }>,
io: KtxCliIo,
): boolean {
return args.inputMode === 'auto' && args.json !== true && io.stdout.isTTY === true && hasInteractiveInput(io);
}
interface CapturedPublicIngestIo extends KtxCliIo {
capturedOutput(): string;
}
@ -597,6 +621,27 @@ export async function runKtxPublicIngest(
const loadProject = deps.loadProject ?? loadKtxProject;
const project = await loadProject({ projectDir: args.projectDir });
if (shouldUseForegroundContextBuildView(args, io)) {
const { runContextBuild } = await import('./context-build-view.js');
const contextBuild = deps.runContextBuild ?? runContextBuild;
const result = await contextBuild(
project,
{
projectDir: args.projectDir,
...(args.targetConnectionId ? { targetConnectionId: args.targetConnectionId } : {}),
all: args.all,
inputMode: args.inputMode,
...(args.depth ? { depth: args.depth } : {}),
...(args.queryHistory ? { queryHistory: args.queryHistory } : {}),
...(args.queryHistoryWindowDays !== undefined ? { queryHistoryWindowDays: args.queryHistoryWindowDays } : {}),
...(args.scanMode ? { scanMode: args.scanMode } : {}),
...(args.detectRelationships !== undefined ? { detectRelationships: args.detectRelationships } : {}),
},
io,
);
return result.exitCode;
}
const plan = buildPublicIngestPlan(project, args);
const results: KtxPublicIngestTargetResult[] = [];