fix: improve ingest runtime readiness (#124)

* fix: improve ingest runtime readiness

* fix(cli): mock runtime in slow setup tests

* test(cli): isolate setup runtime status
This commit is contained in:
Andrey Avtomonov 2026-05-17 10:27:29 +02:00 committed by GitHub
parent f49672ba5b
commit c89af7733a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1055 additions and 75 deletions

View file

@ -38,6 +38,51 @@ function makeIo() {
};
}
function runtimeReady(projectDir: string) {
return { status: 'ready' as const, projectDir, requirements: { features: ['core' as const], requirements: [] } };
}
async function writeReadyRuntime(rootDir: string, cliVersion = '0.2.0') {
const runtimeRoot = join(rootDir, '.runtime');
const versionDir = join(runtimeRoot, cliVersion);
const pythonPath = join(versionDir, '.venv', 'bin', 'python');
const daemonPath = join(versionDir, '.venv', 'bin', 'ktx-daemon');
await mkdir(join(versionDir, '.venv', 'bin'), { recursive: true });
await writeFile(pythonPath, '', 'utf-8');
await writeFile(daemonPath, '', 'utf-8');
await writeFile(
join(versionDir, 'manifest.json'),
`${JSON.stringify(
{
schemaVersion: 1,
cliVersion,
installedAt: '2026-05-09T10:02:00.000Z',
asset: {
schemaVersion: 1,
distributionName: 'kaelio-ktx',
normalizedName: 'kaelio_ktx',
version: '0.1.0',
wheel: {
file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
sha256: '0'.repeat(64),
bytes: 0,
},
},
features: ['core'],
python: {
executable: pythonPath,
daemonExecutable: daemonPath,
},
installLog: join(versionDir, 'install.log'),
},
null,
2,
)}\n`,
'utf-8',
);
return runtimeRoot;
}
describe('setup status', () => {
let tempDir: string;
@ -1054,7 +1099,7 @@ describe('setup status', () => {
);
});
it('auto-installs the managed runtime by default during setup', async () => {
it('prompts before installing the managed runtime by default during setup', async () => {
const io = makeIo();
const embeddings = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
const context = vi.fn(async () => ({ status: 'failed' as const, projectDir: tempDir }));
@ -1088,14 +1133,14 @@ describe('setup status', () => {
expect(embeddings).toHaveBeenCalledWith(
expect.objectContaining({
cliVersion: '0.2.0',
runtimeInstallPolicy: 'auto',
runtimeInstallPolicy: 'prompt',
}),
io.io,
);
expect(context).toHaveBeenCalledWith(
expect.objectContaining({
cliVersion: '0.2.0',
runtimeInstallPolicy: 'auto',
runtimeInstallPolicy: 'prompt',
}),
io.io,
);
@ -1508,6 +1553,10 @@ describe('setup status', () => {
calls.push('sources');
return { status: 'skipped', projectDir: tempDir };
},
runtime: async () => {
calls.push('runtime');
return runtimeReady(tempDir);
},
context: async () => {
calls.push('context');
return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
@ -1524,7 +1573,7 @@ describe('setup status', () => {
),
).resolves.toBe(0);
expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources', 'context', 'agents']);
expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources', 'runtime', 'context', 'agents']);
});
it('commits setup config changes written by later setup steps', async () => {
@ -1565,6 +1614,7 @@ describe('setup status', () => {
return { status: 'skipped', projectDir: tempDir };
},
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
runtime: async () => runtimeReady(tempDir),
context: async () => ({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' }),
agents: async () => ({
status: 'ready',
@ -1611,6 +1661,10 @@ describe('setup status', () => {
embeddings: async () => ({ status: 'skipped', projectDir: tempDir }),
databases: async () => ({ status: 'skipped', projectDir: tempDir }),
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
runtime: async () => {
calls.push('runtime');
return runtimeReady(tempDir);
},
context: async () => {
calls.push('context');
return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
@ -1627,7 +1681,7 @@ describe('setup status', () => {
),
).resolves.toBe(0);
expect(calls).toEqual(['context', 'agents']);
expect(calls).toEqual(['runtime', 'context', 'agents']);
});
it('does not install agents when non-interactive --agents finds context incomplete', async () => {
@ -1660,6 +1714,7 @@ describe('setup status', () => {
},
io.io,
{
runtime: async () => runtimeReady(tempDir),
context: async () => ({ status: 'skipped', projectDir: tempDir }),
agents,
},
@ -1695,7 +1750,7 @@ describe('setup status', () => {
'utf-8',
);
await writeKtxSetupState(tempDir, {
completed_steps: ['project', 'llm', 'embeddings', 'sources', 'context', 'agents'],
completed_steps: ['project', 'llm', 'embeddings', 'sources', 'runtime', 'context', 'agents'],
});
await writeFile(
join(tempDir, '.ktx/agents/install-manifest.json'),
@ -1726,55 +1781,69 @@ describe('setup status', () => {
commands: contextBuildCommands(tempDir, 'setup-context-local-ready'),
});
await expect(
runKtxSetup(
{
command: 'run',
projectDir: tempDir,
mode: 'existing',
agents: false,
inputMode: 'auto',
yes: false,
cliVersion: '0.2.0',
skipLlm: false,
skipEmbeddings: false,
skipDatabases: false,
skipSources: false,
skipAgents: false,
databaseSchemas: [],
},
io.io,
{
readyMenuDeps: { prompts: { select: vi.fn(async () => 'agents'), cancel: vi.fn() } },
model: async (args) => {
expect(args.skipLlm).toBe(true);
return { status: 'skipped', projectDir: tempDir };
const previousRuntimeRoot = process.env.KTX_RUNTIME_ROOT;
process.env.KTX_RUNTIME_ROOT = await writeReadyRuntime(tempDir);
try {
await expect(
runKtxSetup(
{
command: 'run',
projectDir: tempDir,
mode: 'existing',
agents: false,
inputMode: 'auto',
yes: false,
cliVersion: '0.2.0',
skipLlm: false,
skipEmbeddings: false,
skipDatabases: false,
skipSources: false,
skipAgents: false,
databaseSchemas: [],
},
embeddings: async (args) => {
expect(args.skipEmbeddings).toBe(true);
return { status: 'skipped', projectDir: tempDir };
io.io,
{
readyMenuDeps: { prompts: { select: vi.fn(async () => 'agents'), cancel: vi.fn() } },
model: async (args) => {
expect(args.skipLlm).toBe(true);
return { status: 'skipped', projectDir: tempDir };
},
embeddings: async (args) => {
expect(args.skipEmbeddings).toBe(true);
return { status: 'skipped', projectDir: tempDir };
},
databases: async (args) => {
expect(args.skipDatabases).toBe(true);
return { status: 'skipped', projectDir: tempDir };
},
sources: async (args) => {
expect(args.skipSources).toBe(true);
return { status: 'skipped', projectDir: tempDir };
},
runtime: async () => {
calls.push('runtime');
return runtimeReady(tempDir);
},
agents: async () => {
calls.push('agents');
return {
status: 'ready',
projectDir: tempDir,
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
};
},
},
databases: async (args) => {
expect(args.skipDatabases).toBe(true);
return { status: 'skipped', projectDir: tempDir };
},
sources: async (args) => {
expect(args.skipSources).toBe(true);
return { status: 'skipped', projectDir: tempDir };
},
agents: async () => {
calls.push('agents');
return {
status: 'ready',
projectDir: tempDir,
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
};
},
},
),
).resolves.toBe(0);
),
).resolves.toBe(0);
} finally {
if (previousRuntimeRoot === undefined) {
delete process.env.KTX_RUNTIME_ROOT;
} else {
process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
}
}
expect(calls).toEqual(['agents']);
expect(calls).toEqual(['runtime', 'agents']);
});
it('skips to agent setup when context is ready but agents are not configured', async () => {
@ -1854,6 +1923,10 @@ describe('setup status', () => {
expect(args.skipSources).toBe(true);
return { status: 'skipped', projectDir: tempDir };
},
runtime: async () => {
calls.push('runtime');
return runtimeReady(tempDir);
},
agents: async () => {
calls.push('agents');
return {
@ -1867,11 +1940,12 @@ describe('setup status', () => {
).resolves.toBe(0);
expect(readyMenuSelect).not.toHaveBeenCalled();
expect(calls).toEqual(['agents']);
expect(calls).toEqual(['runtime', 'agents']);
});
it('runs only project resolution, context gate, and agent setup in --agents mode', async () => {
it('runs only project resolution, runtime, context gate, and agent setup in --agents mode', async () => {
const io = makeIo();
const runtime = vi.fn(async () => runtimeReady(tempDir));
const context = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir, runId: 'setup-context-local-test' }));
const agents = vi.fn(async () => ({
status: 'ready' as const,
@ -1903,12 +1977,14 @@ describe('setup status', () => {
model: async () => {
throw new Error('model should not run');
},
runtime,
context,
agents,
},
),
).resolves.toBe(0);
expect(runtime).toHaveBeenCalledTimes(1);
expect(context).toHaveBeenCalledTimes(1);
expect(agents).toHaveBeenCalledTimes(1);
});