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 { 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 { 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']); }); });