fix: allow agent setup without context

This commit is contained in:
Andrey Avtomonov 2026-05-19 11:58:06 +02:00
parent eb41d084af
commit fa01c5fb90
9 changed files with 54 additions and 78 deletions

View file

@ -7,10 +7,10 @@ import {
} from './runtime-requirements.js';
describe('runtime requirement detection', () => {
it('requires core for agent/MCP setup', () => {
it('does not require runtime for agent/MCP setup alone', () => {
const config = buildDefaultKtxProjectConfig();
expect(resolveProjectRuntimeRequirements(config, { agents: true }).features).toEqual(['core']);
expect(resolveProjectRuntimeRequirements(config).features).toEqual([]);
});
it('requires core for Looker source ingest unless an external daemon is configured', () => {

View file

@ -8,7 +8,6 @@ import type { KtxRuntimeFeature } from './managed-python-runtime.js';
import type { KtxPublicIngestPlan } from './public-ingest.js';
type KtxRuntimeRequirementReason =
| 'agent-mcp'
| 'query-history'
| 'looker-source'
| 'database-introspection'
@ -26,7 +25,6 @@ export interface KtxRuntimeRequirements {
}
export interface KtxProjectRuntimeRequirementOptions {
agents?: boolean;
databaseIntrospectionFallback?: boolean;
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
}
@ -92,14 +90,6 @@ export function resolveProjectRuntimeRequirements(
const env = options.env ?? process.env;
const requirements: KtxRuntimeRequirement[] = [];
if (options.agents === true) {
requirements.push({
feature: 'core',
reason: 'agent-mcp',
detail: 'Agent MCP setup uses semantic-layer query tools and SQL validation.',
});
}
if (options.databaseIntrospectionFallback === true && !hasDaemonOverride(env)) {
requirements.push({
feature: 'core',

View file

@ -4,7 +4,6 @@ import { join } from 'node:path';
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
import { buildDefaultKtxProjectConfig, readKtxSetupState, type KtxProjectConfig } from '@ktx/context/project';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ManagedPythonCommandRuntime } from './managed-python-command.js';
import { runKtxSetupRuntimeStep } from './setup-runtime.js';
function makeIo() {
@ -43,9 +42,9 @@ describe('runKtxSetupRuntimeStep', () => {
await rm(tempDir, { recursive: true, force: true });
});
it('ensures core runtime for agent setup and records the runtime step', async () => {
it('skips runtime setup when the project has no direct runtime requirements', async () => {
const io = makeIo();
const ensureRuntime = vi.fn(async (): Promise<ManagedPythonCommandRuntime> => ({} as ManagedPythonCommandRuntime));
const ensureRuntime = vi.fn();
await expect(
runKtxSetupRuntimeStep(
@ -54,7 +53,6 @@ describe('runKtxSetupRuntimeStep', () => {
inputMode: 'auto',
cliVersion: '0.2.0',
runtimeInstallPolicy: 'prompt',
agents: true,
},
io.io,
{
@ -63,17 +61,11 @@ describe('runKtxSetupRuntimeStep', () => {
env: {},
},
),
).resolves.toMatchObject({ status: 'ready' });
).resolves.toMatchObject({ status: 'skipped' });
expect(ensureRuntime).toHaveBeenCalledWith(
expect.objectContaining({
cliVersion: '0.2.0',
installPolicy: 'prompt',
feature: 'core',
}),
);
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('runtime');
expect(io.stdout()).toContain('Runtime ready: yes (core)');
expect(ensureRuntime).not.toHaveBeenCalled();
expect((await readKtxSetupState(tempDir)).completed_steps).not.toContain('runtime');
expect(io.stdout()).toContain('Runtime setup skipped.');
});
it('fails fast when required runtime features cannot be installed in no-input mode', async () => {
@ -89,7 +81,7 @@ describe('runKtxSetupRuntimeStep', () => {
inputMode: 'disabled',
cliVersion: '0.2.0',
runtimeInstallPolicy: 'never',
agents: true,
databaseIntrospectionFallback: true,
},
io.io,
{
@ -131,7 +123,6 @@ describe('runKtxSetupRuntimeStep', () => {
inputMode: 'auto',
cliVersion: '0.2.0',
runtimeInstallPolicy: 'auto',
agents: false,
},
io.io,
{

View file

@ -24,7 +24,6 @@ export interface KtxSetupRuntimeArgs {
inputMode: 'auto' | 'disabled';
cliVersion: string;
runtimeInstallPolicy: KtxManagedPythonInstallPolicy;
agents: boolean;
databaseIntrospectionFallback?: boolean;
}
@ -62,7 +61,6 @@ export async function runKtxSetupRuntimeStep(
const loadProjectForRuntime = deps.loadProject ?? loadKtxProject;
const project = await loadProjectForRuntime({ projectDir: args.projectDir });
const requirements = resolveProjectRuntimeRequirements(project.config, {
agents: args.agents,
databaseIntrospectionFallback: args.databaseIntrospectionFallback,
env: deps.env ?? process.env,
});

View file

@ -1733,7 +1733,7 @@ describe('setup status', () => {
expect(committedConfig.stdout).toContain('warehouse:');
});
it('runs agent setup after context succeeds in --agents mode', async () => {
it('runs agent setup without runtime or context in --agents mode', async () => {
const calls: string[] = [];
const io = makeIo();
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
@ -1765,11 +1765,11 @@ describe('setup status', () => {
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
runtime: async () => {
calls.push('runtime');
return runtimeReady(tempDir);
throw new Error('runtime should not run');
},
context: async () => {
calls.push('context');
return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
throw new Error('context should not run');
},
agents: async () => {
calls.push('agents');
@ -1783,11 +1783,13 @@ describe('setup status', () => {
),
).resolves.toBe(0);
expect(calls).toEqual(['runtime', 'context', 'agents']);
expect(calls).toEqual(['agents']);
});
it('does not install agents when non-interactive --agents finds context incomplete', async () => {
it('installs agents when non-interactive --agents finds context incomplete', async () => {
const io = makeIo();
const runtime = vi.fn(async () => runtimeReady(tempDir));
const context = vi.fn(async () => ({ status: 'skipped' as const, projectDir: tempDir }));
const agents = vi.fn(async () => ({
status: 'ready' as const,
projectDir: tempDir,
@ -1816,15 +1818,17 @@ describe('setup status', () => {
},
io.io,
{
runtime: async () => runtimeReady(tempDir),
context: async () => ({ status: 'skipped', projectDir: tempDir }),
runtime,
context,
agents,
},
),
).resolves.toBe(1);
).resolves.toBe(0);
expect(agents).not.toHaveBeenCalled();
expect(io.stderr()).toContain('KTX context is not ready for agents.');
expect(runtime).not.toHaveBeenCalled();
expect(context).not.toHaveBeenCalled();
expect(agents).toHaveBeenCalledTimes(1);
expect(io.stderr()).not.toContain('KTX context is not ready for agents.');
});
it('routes a ready project menu selection to agent setup', async () => {
@ -1945,7 +1949,7 @@ describe('setup status', () => {
}
}
expect(calls).toEqual(['runtime', 'agents']);
expect(calls).toEqual(['agents']);
});
it('skips to agent setup when context is ready but agents are not configured', async () => {
@ -2042,10 +2046,10 @@ describe('setup status', () => {
).resolves.toBe(0);
expect(readyMenuSelect).not.toHaveBeenCalled();
expect(calls).toEqual(['runtime', 'agents']);
expect(calls).toEqual(['agents']);
});
it('runs only project resolution, runtime, context gate, and agent setup in --agents mode', async () => {
it('runs only project resolution 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' }));
@ -2086,8 +2090,8 @@ describe('setup status', () => {
),
).resolves.toBe(0);
expect(runtime).toHaveBeenCalledTimes(1);
expect(context).toHaveBeenCalledTimes(1);
expect(runtime).not.toHaveBeenCalled();
expect(context).not.toHaveBeenCalled();
expect(agents).toHaveBeenCalledTimes(1);
});

View file

@ -339,7 +339,6 @@ export async function readKtxSetupStatus(
}
const agents = [...agentMap.values()];
const runtimeRequirements = resolveProjectRuntimeRequirements(project.config, {
agents: agents.length > 0,
env: options.env ?? process.env,
});
let runtimeReady = runtimeRequirements.features.length === 0 || completedSteps.includes('runtime');
@ -493,12 +492,6 @@ function shouldPrintConciseReadySummary(status: KtxSetupStatus): boolean {
return setupStatusReady(status) && setupContextReady(status) && status.agents.some((agent) => agent.ready);
}
function writeContextNotReadyForAgents(projectDir: string, io: KtxCliIo): void {
io.stderr.write('KTX context is not ready for agents.\n\n');
io.stderr.write(`Build context first:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
io.stderr.write(`Then install agent integration:\n ktx setup --agents --project-dir ${resolve(projectDir)}\n`);
}
function setupRuntimeInstallPolicy(args: Extract<KtxSetupArgs, { command: 'run' }>): 'prompt' | 'auto' | 'never' {
if (args.yes) {
return 'auto';
@ -587,18 +580,19 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
}
const runOnly = readyAction;
const agentOnlySetup = agentsRequested || runOnly === 'agents';
const shouldRunModels = !runOnly || runOnly === 'models';
const shouldRunEmbeddings = !runOnly || runOnly === 'embeddings';
const shouldRunDatabases = !runOnly || runOnly === 'databases';
const shouldRunSources = !runOnly || runOnly === 'sources';
const shouldRunRuntime =
agentsRequested || !runOnly || runOnly === 'runtime' || runOnly === 'context' || runOnly === 'agents';
const shouldRunContext = agentsRequested || !runOnly || runOnly === 'context';
!agentOnlySetup && (!runOnly || runOnly === 'runtime' || runOnly === 'context');
const shouldRunContext = !agentOnlySetup && (!runOnly || runOnly === 'context');
const shouldRunAgents = agentsRequested || !runOnly || runOnly === 'agents';
const showPromptInstructions = projectResult.confirmedCreation !== true;
const setupSteps: KtxSetupFlowStep[] = agentsRequested
? ['runtime', 'context']
const setupSteps: KtxSetupFlowStep[] = agentOnlySetup
? []
: ['models', 'embeddings', 'databases', 'sources', 'runtime', 'context'];
if (shouldRunAgents && args.skipAgents !== true) {
setupSteps.push('agents');
@ -737,7 +731,6 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
inputMode: args.inputMode,
cliVersion: args.cliVersion,
runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
agents: shouldRunAgents && args.skipAgents !== true,
},
io,
);
@ -799,10 +792,6 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
}
if (step === 'context' && stepResult.status !== 'ready') {
if (shouldRunAgents && args.skipAgents !== true) {
if (agentsRequested) {
writeContextNotReadyForAgents(projectResult.projectDir, io);
return args.inputMode === 'disabled' ? 1 : 0;
}
return 0;
}
}