mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
fix(cli): clear prefetch watch before build
This commit is contained in:
parent
55114e03fd
commit
89e9fdb416
4 changed files with 152 additions and 12 deletions
|
|
@ -309,6 +309,41 @@ describe('renderContextBuildView', () => {
|
|||
});
|
||||
|
||||
describe('createRepainter', () => {
|
||||
it('does not leave a stale header when terminal rows wrap differently than reported', () => {
|
||||
const io = makeIo({ isTTY: true });
|
||||
const repainter = createRepainter(io.io);
|
||||
const state = initViewState([
|
||||
{
|
||||
connectionId: 'postgres-warehouse',
|
||||
driver: 'postgres',
|
||||
operation: 'scan',
|
||||
debugCommand: '',
|
||||
steps: ['scan'],
|
||||
},
|
||||
{
|
||||
connectionId: 'dbt-main',
|
||||
driver: 'dbt',
|
||||
operation: 'source-ingest',
|
||||
adapter: 'dbt',
|
||||
debugCommand: '',
|
||||
steps: ['source-ingest', 'memory-update'],
|
||||
},
|
||||
]);
|
||||
state.primarySources[0].status = 'done';
|
||||
state.primarySources[0].summaryText = '7 tables';
|
||||
state.primarySources[0].elapsedMs = 18000;
|
||||
state.contextSources[0].status = 'running';
|
||||
state.contextSources[0].elapsedMs = 1000;
|
||||
state.totalElapsedMs = 1000;
|
||||
|
||||
repainter.paint(renderContextBuildView(state, { styled: false, showHint: true }));
|
||||
state.contextSources[0].elapsedMs = 5000;
|
||||
state.totalElapsedMs = 5000;
|
||||
repainter.paint(renderContextBuildView(state, { styled: false, showHint: true }));
|
||||
|
||||
expect(io.stdout()).toContain('\x1b[14A\r');
|
||||
});
|
||||
|
||||
it('moves up visual rows, not just newline count, when content wraps', () => {
|
||||
const io = makeIo({ isTTY: true, columns: 5 });
|
||||
const repainter = createRepainter(io.io);
|
||||
|
|
@ -342,6 +377,17 @@ describe('createRepainter', () => {
|
|||
const cursorMoves = [...io.stdout().matchAll(/\[(\d+)A/g)].map((m) => Number(m[1]));
|
||||
expect(cursorMoves).toEqual([2]);
|
||||
});
|
||||
|
||||
it('clears the current frame', () => {
|
||||
const io = makeIo({ isTTY: true, columns: 80 });
|
||||
const repainter = createRepainter(io.io);
|
||||
|
||||
repainter.paint('hello\nworld\n');
|
||||
repainter.clear();
|
||||
io.io.stdout.write('after\n');
|
||||
|
||||
expect(io.stdout()).toContain('\x1b[2A\r\x1b[2K\x1b[Jafter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runContextBuild', () => {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ export function renderContextBuildView(
|
|||
|
||||
const ESC_K_RE = new RegExp(`${ESC.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[K`, 'g');
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
const DEFAULT_REPAINT_COLUMNS = 40;
|
||||
|
||||
export function extractProgressMessage(chunk: string): string | null {
|
||||
const cleaned = chunk.replace(/^\r/, '').replace(ESC_K_RE, '').replace(/\n$/, '').trim();
|
||||
|
|
@ -354,9 +355,11 @@ export function createRepainter(io: KtxCliIo) {
|
|||
|
||||
const terminalColumns = () => {
|
||||
for (const columns of [io.stdout.columns, process.stdout.columns]) {
|
||||
if (typeof columns === 'number' && Number.isFinite(columns) && columns > 0) return columns;
|
||||
if (typeof columns === 'number' && Number.isFinite(columns) && columns > 0) {
|
||||
return columns;
|
||||
}
|
||||
}
|
||||
return 80;
|
||||
return DEFAULT_REPAINT_COLUMNS;
|
||||
};
|
||||
|
||||
const visualRows = (line: string, columns: number) => {
|
||||
|
|
@ -390,6 +393,15 @@ export function createRepainter(io: KtxCliIo) {
|
|||
hasPainted = true;
|
||||
lastCursorUpRows = cursorUpRowsAfterWrite(content);
|
||||
},
|
||||
clear() {
|
||||
if (!hasPainted) return;
|
||||
if (lastCursorUpRows > 0) {
|
||||
io.stdout.write(`${ESC}[${lastCursorUpRows}A`);
|
||||
}
|
||||
io.stdout.write(`\r${ESC}[2K${ESC}[J`);
|
||||
hasPainted = false;
|
||||
lastCursorUpRows = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ import {
|
|||
writeKtxSetupContextState,
|
||||
} from './setup-context.js';
|
||||
|
||||
function makeIo() {
|
||||
function makeIo(options: { isTTY?: boolean } = {}) {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
return {
|
||||
io: {
|
||||
stdout: {
|
||||
isTTY: options.isTTY,
|
||||
write: (chunk: string) => {
|
||||
stdout += chunk;
|
||||
},
|
||||
|
|
@ -522,12 +523,21 @@ describe('setup context build state', () => {
|
|||
],
|
||||
});
|
||||
};
|
||||
const runContextBuildMock = vi.fn(async () => ({
|
||||
exitCode: 0,
|
||||
detached: false,
|
||||
reportIds: ['docs-report'],
|
||||
artifactPaths: ['raw-sources/docs/notion/sync-1/ingest-report.json'],
|
||||
}));
|
||||
const runContextBuildMock = vi.fn(async () => {
|
||||
await expect(readKtxSetupContextState(tempDir)).resolves.toMatchObject({
|
||||
status: 'running',
|
||||
sourceProgress: [
|
||||
{ connectionId: 'warehouse', operation: 'scan', status: 'done', elapsedMs: 120000 },
|
||||
{ connectionId: 'docs', operation: 'source-ingest', status: 'queued' },
|
||||
],
|
||||
});
|
||||
return {
|
||||
exitCode: 0,
|
||||
detached: false,
|
||||
reportIds: ['docs-report'],
|
||||
artifactPaths: ['raw-sources/docs/notion/sync-1/ingest-report.json'],
|
||||
};
|
||||
});
|
||||
const verifyContextReady = vi.fn(async () => ({
|
||||
ready: true,
|
||||
agentContextReady: true,
|
||||
|
|
@ -568,6 +578,74 @@ describe('setup context build state', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('clears the auto-watch progress view before continuing the full context build', async () => {
|
||||
await writeReadyProject(tempDir);
|
||||
await writeKtxSetupContextState(tempDir, {
|
||||
runId: 'setup-context-local-prefetch-clear',
|
||||
status: 'detached',
|
||||
startedAt: '2026-05-09T10:00:00.000Z',
|
||||
updatedAt: '2026-05-09T10:00:00.000Z',
|
||||
primarySourceConnectionIds: ['warehouse'],
|
||||
contextSourceConnectionIds: [],
|
||||
reportIds: [],
|
||||
artifactPaths: [],
|
||||
retryableFailedTargets: [],
|
||||
commands: contextBuildCommands(tempDir, 'setup-context-local-prefetch-clear'),
|
||||
sourceProgress: [
|
||||
{ connectionId: 'warehouse', operation: 'scan' as const, status: 'running' as const, startedAtMs: Date.now() },
|
||||
],
|
||||
});
|
||||
const io = makeIo({ isTTY: true });
|
||||
const completePrefetch = async () => {
|
||||
await writeKtxSetupContextState(tempDir, {
|
||||
runId: 'setup-context-local-prefetch-clear',
|
||||
status: 'paused',
|
||||
startedAt: '2026-05-09T10:00:00.000Z',
|
||||
updatedAt: '2026-05-09T10:02:00.000Z',
|
||||
primarySourceConnectionIds: ['warehouse'],
|
||||
contextSourceConnectionIds: [],
|
||||
reportIds: ['warehouse-report'],
|
||||
artifactPaths: ['raw-sources/warehouse/live-database/sync-1/scan-report.json'],
|
||||
retryableFailedTargets: [],
|
||||
commands: contextBuildCommands(tempDir, 'setup-context-local-prefetch-clear'),
|
||||
sourceProgress: [
|
||||
{ connectionId: 'warehouse', operation: 'scan' as const, status: 'done' as const, elapsedMs: 120000 },
|
||||
],
|
||||
});
|
||||
};
|
||||
const runContextBuildMock = vi.fn(async () => {
|
||||
io.io.stdout.write('foreground-build-started\n');
|
||||
return {
|
||||
exitCode: 0,
|
||||
detached: false,
|
||||
reportIds: ['docs-report'],
|
||||
artifactPaths: ['raw-sources/docs/notion/sync-1/ingest-report.json'],
|
||||
};
|
||||
});
|
||||
|
||||
await expect(
|
||||
runKtxSetupContextStep(
|
||||
{ projectDir: tempDir, inputMode: 'auto', autoWatch: true },
|
||||
io.io,
|
||||
{
|
||||
sleep: completePrefetch,
|
||||
watchIntervalMs: 1,
|
||||
runIdFactory: () => 'setup-context-local-final-clear',
|
||||
now: () => new Date('2026-05-09T10:03:00.000Z'),
|
||||
runContextBuild: runContextBuildMock,
|
||||
verifyContextReady: vi.fn(async () => ({
|
||||
ready: true,
|
||||
agentContextReady: true,
|
||||
semanticSearchReady: true,
|
||||
details: ['ready'],
|
||||
})),
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-final-clear' });
|
||||
|
||||
expect(io.stdout()).toMatch(/\x1b\[\d+A\r\x1b\[2K\x1b\[Jforeground-build-started/);
|
||||
});
|
||||
|
||||
it('shows newly configured context sources while watching an active primary scan prefetch', async () => {
|
||||
await writeReadyProject(tempDir);
|
||||
await writeKtxSetupContextState(tempDir, {
|
||||
|
|
|
|||
|
|
@ -622,7 +622,8 @@ async function runBuild(
|
|||
const now = deps.now ?? (() => new Date());
|
||||
const runId = deps.runIdFactory?.() ?? runIdFactory();
|
||||
const startedAt = now().toISOString();
|
||||
const completedSourceProgress = existingState?.sourceProgress?.filter((source) => source.status === 'done') ?? [];
|
||||
const existingSourceProgress = sourceProgressWithTargets(existingState?.sourceProgress, targets) ?? [];
|
||||
const completedSourceProgress = existingSourceProgress.filter((source) => source.status === 'done');
|
||||
const runningState: KtxSetupContextState = {
|
||||
runId,
|
||||
status: 'running',
|
||||
|
|
@ -634,12 +635,12 @@ async function runBuild(
|
|||
artifactPaths: [],
|
||||
retryableFailedTargets: [],
|
||||
commands: contextBuildCommands(args.projectDir, runId),
|
||||
...(completedSourceProgress.length > 0 ? { sourceProgress: completedSourceProgress } : {}),
|
||||
...(existingSourceProgress.length > 0 ? { sourceProgress: existingSourceProgress } : {}),
|
||||
};
|
||||
await writeKtxSetupContextState(args.projectDir, runningState);
|
||||
|
||||
let lastSourceProgress: ContextBuildSourceProgressUpdate[] | undefined =
|
||||
completedSourceProgress.length > 0 ? completedSourceProgress : undefined;
|
||||
existingSourceProgress.length > 0 ? existingSourceProgress : undefined;
|
||||
const contextBuild = deps.runContextBuild ?? runContextBuild;
|
||||
const buildResult = await contextBuild(
|
||||
project,
|
||||
|
|
@ -1025,6 +1026,9 @@ async function watchContextStatusWithProgressView(
|
|||
}
|
||||
|
||||
if (!isActiveStatus(state.status)) {
|
||||
if (state.status === 'paused') {
|
||||
repainter?.clear();
|
||||
}
|
||||
return { exitCode: watchExitCode(state.status), state };
|
||||
}
|
||||
if (detached) break;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue