Watch setup context until terminal status

This commit is contained in:
Luca Martial 2026-05-10 16:25:02 -07:00
parent 440a07d0d2
commit 82848e5de9
2 changed files with 90 additions and 3 deletions

View file

@ -372,6 +372,48 @@ describe('setup context build state', () => {
});
});
it('watches setup context command status until the run reaches a terminal state', async () => {
await mkdir(join(tempDir, '.ktx', 'setup'), { recursive: true });
await writeKtxSetupContextState(tempDir, {
runId: 'setup-context-local-watch',
status: 'running',
startedAt: '2026-05-09T10:00:00.000Z',
updatedAt: '2026-05-09T10:00:00.000Z',
primarySourceConnectionIds: ['warehouse'],
contextSourceConnectionIds: ['docs'],
reportIds: [],
artifactPaths: [],
retryableFailedTargets: [],
commands: contextBuildCommands(tempDir, 'setup-context-local-watch'),
});
const io = makeIo();
const completeRun = async () => {
await writeKtxSetupContextState(tempDir, {
runId: 'setup-context-local-watch',
status: 'completed',
startedAt: '2026-05-09T10:00:00.000Z',
updatedAt: '2026-05-09T10:02:00.000Z',
completedAt: '2026-05-09T10:02:00.000Z',
primarySourceConnectionIds: ['warehouse'],
contextSourceConnectionIds: ['docs'],
reportIds: [],
artifactPaths: [],
retryableFailedTargets: [],
commands: contextBuildCommands(tempDir, 'setup-context-local-watch'),
});
};
await expect(
runKtxSetupContextCommand(
{ command: 'watch', projectDir: tempDir, runId: 'setup-context-local-watch', inputMode: 'disabled' },
io.io,
{ sleep: completeRun, watchIntervalMs: 1 },
),
).resolves.toBe(0);
expect(io.stdout()).toContain('KTX context built: running');
expect(io.stdout()).toContain('KTX context built: yes');
});
it('runs direct build commands without asking for setup confirmation first', async () => {
await writeReadyProject(tempDir);
const io = makeIo();

View file

@ -99,6 +99,8 @@ export interface KtxSetupContextDeps {
now?: () => Date;
runContextBuild?: typeof runContextBuild;
verifyContextReady?: (projectDir: string) => Promise<KtxSetupContextReadiness>;
sleep?: (ms: number) => Promise<void>;
watchIntervalMs?: number;
}
interface KtxSetupContextTargets {
@ -109,6 +111,7 @@ interface KtxSetupContextTargets {
const SETUP_CONTEXT_STATE_PATH = ['.ktx', 'setup', 'context-build.json'] as const;
const LIVE_DATABASE_ADAPTER = 'live-database';
const SCAN_REPORT_FILE = 'scan-report.json';
const DEFAULT_WATCH_INTERVAL_MS = 2_000;
function createPromptAdapter(): KtxSetupContextPromptAdapter {
return {
@ -698,6 +701,18 @@ function stateMatchesRunId(state: KtxSetupContextState, runId: string | undefine
return !runId || state.runId === runId;
}
function isActiveStatus(status: KtxSetupContextBuildStatus): boolean {
return status === 'running' || status === 'detached';
}
function watchExitCode(status: KtxSetupContextBuildStatus): number {
return status === 'failed' || status === 'interrupted' || status === 'stale' ? 1 : 0;
}
function defaultSleep(ms: number): Promise<void> {
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
}
function statusPayload(state: KtxSetupContextState): KtxSetupContextStatusSummary {
return setupContextStatusFromState(state, { completedStep: state.status === 'completed' });
}
@ -714,6 +729,38 @@ function writeContextStatus(state: KtxSetupContextState, io: KtxCliIo): void {
}
}
async function watchContextStatus(
args: Extract<KtxSetupContextCommandArgs, { command: 'watch' }>,
initialState: KtxSetupContextState,
io: KtxCliIo,
deps: KtxSetupContextDeps,
): Promise<number> {
const sleep = deps.sleep ?? defaultSleep;
const intervalMs = deps.watchIntervalMs ?? DEFAULT_WATCH_INTERVAL_MS;
let state = initialState;
let lastRenderedStatus = '';
io.stdout.write('KTX context build\n');
while (true) {
const renderedStatus = `${state.status}:${state.updatedAt ?? ''}:${state.completedAt ?? ''}:${state.failureReason ?? ''}`;
if (renderedStatus !== lastRenderedStatus) {
writeContextStatus(state, io);
lastRenderedStatus = renderedStatus;
}
if (!isActiveStatus(state.status)) {
return watchExitCode(state.status);
}
await sleep(intervalMs);
state = await readKtxSetupContextState(args.projectDir);
if (!stateMatchesRunId(state, args.runId)) {
io.stderr.write(`KTX setup context run "${args.runId}" was not found.\n`);
return 1;
}
}
}
export async function runKtxSetupContextCommand(
args: KtxSetupContextCommandArgs,
io: KtxCliIo,
@ -744,9 +791,7 @@ export async function runKtxSetupContextCommand(
}
if (args.command === 'watch') {
io.stdout.write('KTX context build\n');
writeContextStatus(state, io);
return 0;
return await watchContextStatus(args, state, io, deps);
}
const updatedAt = new Date().toISOString();