import { describe, expect, it, vi } from 'vitest'; import { runKtxCli } from '../src/index.js'; function makeIo() { let stdout = ''; let stderr = ''; return { io: { stdout: { write: (chunk: string) => { stdout += chunk; }, }, stderr: { write: (chunk: string) => { stderr += chunk; }, }, }, stdout: () => stdout, stderr: () => stderr, }; } describe('admin Commander tree', () => { it('prints visible admin help with supported low-level command groups', async () => { const testIo = makeIo(); await expect(runKtxCli(['admin', '--help'], testIo.io)).resolves.toBe(0); expect(testIo.stdout()).toContain('Usage: ktx admin [options] [command]'); for (const command of ['init', 'runtime', 'reindex']) { expect(testIo.stdout()).toContain(command); } for (const removed of [ 'doctor', 'scan', 'ingest', 'mapping', 'knowledge', 'model', 'replay', 'report', 'status', 'artifacts', 'config', 'tools', 'daemon', ]) { expect(testIo.stdout()).not.toContain(`${removed} `); } expect(testIo.stderr()).toBe(''); }); it('lists admin in root command rows', async () => { const testIo = makeIo(); await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0); expect(testIo.stdout()).not.toContain('Advanced:'); expect(testIo.stdout()).toContain('admin'); expect(testIo.stdout()).toMatch(/Low-level project initialization,\s+runtime,\s+and index management/); expect(testIo.stderr()).toBe(''); }); it('does not keep a dev alias', async () => { const testIo = makeIo(); await expect(runKtxCli(['dev', '--help'], testIo.io)).resolves.toBe(1); expect(testIo.stderr()).toContain("unknown command 'dev'"); }); it('keeps project scaffolding under admin init', async () => { const { mkdtemp, readFile, rm } = await import('node:fs/promises'); const { tmpdir } = await import('node:os'); const { join } = await import('node:path'); const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-init-')); const projectDir = join(tempDir, 'warehouse'); const testIo = makeIo(); try { await expect(runKtxCli(['admin', 'init', projectDir], testIo.io)).resolves.toBe(0); expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`); await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.not.toContain('project:'); expect(testIo.stderr()).toBe(''); } finally { await rm(tempDir, { recursive: true, force: true }); } }); it('uses global project-dir for admin init when the positional directory is omitted', async () => { const { mkdtemp, rm } = await import('node:fs/promises'); const { tmpdir } = await import('node:os'); const { join } = await import('node:path'); const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-init-global-')); const projectDir = join(tempDir, 'global-init'); const testIo = makeIo(); try { await expect( runKtxCli(['--project-dir', projectDir, 'admin', 'init'], testIo.io), ).resolves.toBe(0); expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`); expect(testIo.stderr()).toBe(''); } finally { await rm(tempDir, { recursive: true, force: true }); } }); it('prints config schema without requiring a KTX project directory', async () => { const { mkdtemp, rm } = await import('node:fs/promises'); const { tmpdir } = await import('node:os'); const { join } = await import('node:path'); const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-schema-')); const missingProjectDir = join(tempDir, 'missing-project'); const originalProjectDir = process.env.KTX_PROJECT_DIR; const testIo = makeIo(); try { process.env.KTX_PROJECT_DIR = missingProjectDir; await expect(runKtxCli(['admin', 'schema'], testIo.io)).resolves.toBe(0); expect(JSON.parse(testIo.stdout())).toMatchObject({ title: 'ktx.yaml', type: 'object', }); expect(testIo.stderr()).toBe(''); } finally { if (originalProjectDir === undefined) { delete process.env.KTX_PROJECT_DIR; } else { process.env.KTX_PROJECT_DIR = originalProjectDir; } await rm(tempDir, { recursive: true, force: true }); } }); it('rejects removed admin command groups', async () => { for (const argv of [ ['admin', 'doctor', 'setup'], ['admin', 'runtime', 'doctor'], ['admin', 'runtime', 'prune', '--dry-run'], ['admin', 'scan', 'warehouse'], ['admin', 'ingest', 'run'], ['admin', 'mapping', 'list'], ['admin', 'completion', 'zsh'], ['admin', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', ''], ['admin', 'knowledge', 'list'], ['admin', 'model', 'list'], ['admin', 'artifacts'], ]) { const testIo = makeIo(); await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1); expect(testIo.stderr()).toMatch(/unknown command|error:/); } }); it.each([ { argv: ['admin', 'runtime', '--help'], expected: ['Usage: ktx admin runtime', 'install', 'start', 'stop', 'status'], }, ])('prints generated nested help for $argv', async ({ argv, expected }) => { const io = makeIo(); const doctor = vi.fn(async () => 0); await expect(runKtxCli(argv, io.io, { doctor })).resolves.toBe(0); for (const text of expected) { expect(io.stdout()).toContain(text); } if (argv.join(' ') === 'admin runtime --help') { expect(io.stdout()).not.toContain('prune'); expect(io.stdout()).not.toContain('doctor'); } expect(io.stderr()).toBe(''); expect(doctor).not.toHaveBeenCalled(); }); it('rejects old adapter-backed ingest flags through public option parsing and keeps run out of ingest help', async () => { const helpIo = makeIo(); const runIo = makeIo(); const publicIngest = vi.fn(async () => 0); await expect(runKtxCli(['ingest', '--help'], helpIo.io, { publicIngest })).resolves.toBe(0); await expect( runKtxCli( ['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase', '--project-dir', '/tmp/project'], runIo.io, { publicIngest }, ), ).resolves.toBe(1); expect(helpIo.stdout()).not.toMatch(/^ run\s/m); expect(runIo.stderr()).toMatch(/unknown option '--connection-id'|error:/); expect(publicIngest).not.toHaveBeenCalled(); }); it.each([ { argv: ['scan'] }, { argv: ['scan', '--help'] }, { argv: ['scan', 'warehouse'] }, { argv: ['scan', 'warehouse', '--project-dir', '/tmp/project', '--dry-run'] }, { argv: ['scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'] }, ])('rejects removed top-level scan command $argv', async ({ argv }) => { const io = makeIo(); const publicIngest = vi.fn(async () => 0); await expect(runKtxCli(argv, io.io, { publicIngest })).resolves.toBe(1); expect(publicIngest).not.toHaveBeenCalled(); expect(io.stderr()).toMatch(/unknown command|error:/); }); it('rejects old adapter-backed top-level ingest flags without low-level ingest registration', async () => { const io = makeIo(); const publicIngest = vi.fn(async () => 0); await expect( runKtxCli( [ 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase', '--project-dir', '/tmp/project', '--json', ], io.io, { publicIngest }, ), ).resolves.toBe(1); expect(publicIngest).not.toHaveBeenCalled(); expect(io.stderr()).toMatch(/unknown option '--connection-id'|error:/); }); });