feat: route sl query managed runtime policy

This commit is contained in:
Andrey Avtomonov 2026-05-11 10:22:09 +02:00
parent 399353986f
commit b1ca79ac9c
3 changed files with 82 additions and 0 deletions

View file

@ -64,6 +64,8 @@ export const slQueryCommandSchema = z.object({
}),
format: z.enum(['json', 'sql']),
execute: z.boolean(),
cliVersion: z.string().min(1),
runtimeInstallPolicy: z.enum(['prompt', 'auto', 'never']),
maxRows: z.number().int().positive().optional(),
});

View file

@ -6,6 +6,7 @@ import {
resolveCommandProjectDir,
} from '../cli-program.js';
import { slQueryCommandSchema } from '../command-schemas.js';
import type { KtxManagedPythonInstallPolicy } from '../managed-python-command.js';
import type { KtxSlArgs } from '../sl.js';
import { profileMark } from '../startup-profile.js';
@ -32,6 +33,16 @@ function collectOrderBy(
return [...previous, parseOrderBy(value)];
}
function runtimeInstallPolicy(options: { yes?: boolean; input?: boolean }): KtxManagedPythonInstallPolicy {
if (options.yes === true && options.input === false) {
throw new Error('Choose only one runtime install mode: --yes or --no-input');
}
if (options.yes === true) {
return 'auto';
}
return options.input === false ? 'never' : 'prompt';
}
async function runSlArgs(context: KtxCliCommandContext, args: KtxSlArgs): Promise<void> {
const runner = context.deps.sl ?? (await import('../sl.js')).runKtxSl;
context.setExitCode(await runner(args, context.io));
@ -121,6 +132,8 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
.option('--include-empty', 'Include empty rows', false)
.addOption(new Option('--format <format>', 'json or sql').choices(['json', 'sql']).default('json'))
.option('--execute', 'Execute the compiled query', false)
.option('--yes', 'Install the managed Python runtime without prompting when required', false)
.option('--no-input', 'Disable interactive managed runtime installation')
.option('--max-rows <n>', 'Maximum rows to return when executing', parsePositiveIntegerOption)
.action(async (options, command) => {
if (options.measure.length === 0) {
@ -141,6 +154,8 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
},
format: options.format,
execute: options.execute === true,
cliVersion: context.packageInfo.version,
runtimeInstallPolicy: runtimeInstallPolicy(options),
...(options.maxRows !== undefined ? { maxRows: options.maxRows } : {}),
});
await runSlArgs(context, args);

View file

@ -178,6 +178,71 @@ describe('runKtxCli', () => {
);
});
it('routes sl query managed runtime install policies', async () => {
const sl = vi.fn(async () => 0);
const promptIo = makeIo();
await expect(
runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count'], promptIo.io, { sl }),
).resolves.toBe(0);
expect(sl).toHaveBeenLastCalledWith(
expect.objectContaining({
command: 'query',
projectDir: tempDir,
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'prompt',
query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }),
}),
promptIo.io,
);
const autoIo = makeIo();
await expect(
runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--yes'], autoIo.io, {
sl,
}),
).resolves.toBe(0);
expect(sl).toHaveBeenLastCalledWith(
expect.objectContaining({
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'auto',
}),
autoIo.io,
);
const noInputIo = makeIo();
await expect(
runKtxCli(
['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--no-input'],
noInputIo.io,
{ sl },
),
).resolves.toBe(0);
expect(sl).toHaveBeenLastCalledWith(
expect.objectContaining({
cliVersion: '0.0.0-private',
runtimeInstallPolicy: 'never',
}),
noInputIo.io,
);
});
it('rejects conflicting sl query runtime install flags', async () => {
const io = makeIo();
const sl = vi.fn(async () => 0);
await expect(
runKtxCli(
['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--yes', '--no-input'],
io.io,
{ sl },
),
).resolves.toBe(1);
expect(sl).not.toHaveBeenCalled();
expect(io.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
});
it('exposes demo under setup help instead of root help', async () => {
const testIo = makeIo();