mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
138 lines
5.4 KiB
TypeScript
138 lines
5.4 KiB
TypeScript
|
|
import type { Command } from '@commander-js/extra-typings';
|
||
|
|
import { describe, expect, it } from 'vitest';
|
||
|
|
import { buildKtxProgram } from '../../src/cli-program.js';
|
||
|
|
import type { KtxCliIo, KtxCliPackageInfo } from '../../src/cli-runtime.js';
|
||
|
|
import { type CompletionProviders, computeCompletions } from '../../src/completion/complete-engine.js';
|
||
|
|
|
||
|
|
function stubIo(): KtxCliIo {
|
||
|
|
return { stdout: { isTTY: false, columns: 80, write: () => {} }, stderr: { write: () => {} } };
|
||
|
|
}
|
||
|
|
|
||
|
|
function stubPackageInfo(): KtxCliPackageInfo {
|
||
|
|
return { name: '@kaelio/ktx', version: '0.0.0-test' };
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildProgram(): Command {
|
||
|
|
return buildKtxProgram({ io: stubIo(), deps: {}, packageInfo: stubPackageInfo(), runInit: async () => 0 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const SOURCES = ['orders', 'customers'];
|
||
|
|
const WIKI_KEYS = ['revenue', 'churn'];
|
||
|
|
const CONNECTIONS = ['warehouse'];
|
||
|
|
|
||
|
|
function fakeProviders(overrides: Partial<CompletionProviders> = {}): CompletionProviders {
|
||
|
|
return {
|
||
|
|
async positionalCandidates(commandPath) {
|
||
|
|
const key = commandPath.join(' ');
|
||
|
|
if (key === 'sl read' || key === 'sl validate') {
|
||
|
|
return SOURCES;
|
||
|
|
}
|
||
|
|
if (key === 'wiki read') {
|
||
|
|
return WIKI_KEYS;
|
||
|
|
}
|
||
|
|
return [];
|
||
|
|
},
|
||
|
|
async optionValueCandidates(_commandPath, optionFlag) {
|
||
|
|
return optionFlag === '--connection-id' ? CONNECTIONS : [];
|
||
|
|
},
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function complete(words: string[], providers: CompletionProviders = fakeProviders()): Promise<string[]> {
|
||
|
|
return computeCompletions(buildProgram(), words, providers);
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('computeCompletions', () => {
|
||
|
|
it('lists top-level commands and hides internal ones', async () => {
|
||
|
|
const result = await complete(['']);
|
||
|
|
expect(result).toContain('sl');
|
||
|
|
expect(result).toContain('wiki');
|
||
|
|
expect(result).toContain('completion');
|
||
|
|
expect(result).not.toContain('__complete');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('filters top-level commands by prefix', async () => {
|
||
|
|
expect(await complete(['co'])).toEqual(['completion', 'connection']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('hides Commander-hidden subcommands such as `mcp serve-internal`', async () => {
|
||
|
|
const result = await complete(['mcp', '']);
|
||
|
|
expect(result).not.toContain('serve-internal');
|
||
|
|
expect(result).toEqual(['logs', 'start', 'status', 'stdio', 'stop']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('offers only sl subcommands at the bare sl positional', async () => {
|
||
|
|
expect(await complete(['sl', ''])).toEqual(['query', 'read', 'validate']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('offers source names for sl read and sl validate', async () => {
|
||
|
|
expect(await complete(['sl', 'read', ''])).toEqual(['customers', 'orders']);
|
||
|
|
expect(await complete(['sl', 'validate', ''])).toEqual(['customers', 'orders']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('offers only the wiki read subcommand at the bare wiki positional', async () => {
|
||
|
|
expect(await complete(['wiki', ''])).toEqual(['read']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('offers wiki page keys for wiki read', async () => {
|
||
|
|
expect(await complete(['wiki', 'read', ''])).toEqual(['churn', 'revenue']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not complete entity names for bare search positionals', async () => {
|
||
|
|
expect(await complete(['sl', 'o'])).toEqual([]);
|
||
|
|
expect(await complete(['wiki', 'r'])).toEqual(['read']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('completes flags (own + inherited globals) when the partial starts with a dash', async () => {
|
||
|
|
const result = await complete(['sl', '-']);
|
||
|
|
expect(result).toContain('--connection-id');
|
||
|
|
expect(result).toContain('--output');
|
||
|
|
expect(result).toContain('--json');
|
||
|
|
expect(result).toContain('--debug');
|
||
|
|
expect(result).toContain('--project-dir');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('completes option choices for the `--opt value` form', async () => {
|
||
|
|
expect(await complete(['sl', '--output', ''])).toEqual(['json', 'plain', 'pretty']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('completes option choices for the `--opt=value` form', async () => {
|
||
|
|
expect(await complete(['sl', '--output=pr'])).toEqual(['--output=pretty']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('completes option values from a provider for options without static choices', async () => {
|
||
|
|
expect(await complete(['sl', '--connection-id', ''])).toEqual(['warehouse']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('falls through to positional completion after a boolean flag', async () => {
|
||
|
|
const result = await complete(['sl', '--json', '']);
|
||
|
|
expect(result).toEqual(['query', 'read', 'validate']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not treat a value-taking option value as a subcommand', async () => {
|
||
|
|
// A connection id that happens to match a subcommand name (`query`, `read`)
|
||
|
|
// is the `--connection-id` value, not a subcommand: the next positional must
|
||
|
|
// still offer the `sl` subcommands rather than resolving into `sl query`/`sl read`.
|
||
|
|
expect(await complete(['sl', '--connection-id', 'query', ''])).toEqual(['query', 'read', 'validate']);
|
||
|
|
expect(await complete(['sl', '--connection-id', 'read', ''])).toEqual(['query', 'read', 'validate']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('still returns subcommands/flags when dynamic providers yield nothing (no project)', async () => {
|
||
|
|
const empty = fakeProviders({
|
||
|
|
positionalCandidates: async () => [],
|
||
|
|
optionValueCandidates: async () => [],
|
||
|
|
});
|
||
|
|
expect(await complete(['sl', ''], empty)).toEqual(['query', 'read', 'validate']);
|
||
|
|
expect(await complete(['-'], empty)).toContain('--debug');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('completes the completion command shell positional from its static choices', async () => {
|
||
|
|
expect(await complete(['completion', ''])).toEqual(['bash', 'zsh']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('filters positional argument choices by prefix', async () => {
|
||
|
|
expect(await complete(['completion', 'z'])).toEqual(['zsh']);
|
||
|
|
});
|
||
|
|
});
|