/** * CLIContext Tests * * Tests persistent process implementation with CLI server mode */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { CLIContext } from './cli-context'; import { type IntegrationTestContext, setupIntegrationTest } from './integration'; describe('CLIContext - Persistent Process', () => { let ctx: IntegrationTestContext; let cli: CLIContext; beforeEach(async () => { ctx = await setupIntegrationTest(); cli = await CLIContext.create('src/cli/index.ts', { CELILO_DB_PATH: ctx.dbPath, CELILO_DATA_DIR: ctx.dataDir, }); }); afterEach(async () => { await cli.dispose(); await ctx.cleanup(); }); describe('basic command execution', () => { test('runs successful command', async () => { const result = await cli.run('module list'); expect(result.exitCode).toBe(0); }); test('captures stdout', async () => { const result = await cli.run('module list'); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('No modules installed'); }); test('captures stderr on error', async () => { // Use --help with invalid syntax to trigger error const result = await cli.run('module'); expect(result.exitCode).not.toBe(0); }); test('returns CLIResult with fluent API', async () => { const result = await cli.run('module list'); expect(() => { result.expectSuccess().expectStdout(/modules/i); }).not.toThrow(); }); }); describe('runExpectingFailure', () => { test('accepts failing command', async () => { const result = await cli.runExpectingFailure('module'); expect(result.exitCode).not.toBe(0); expect(() => result.expectFailure()).not.toThrow(); }); }); describe('middleware hooks', () => { test('beforeEach runs before command', async () => { const calls: string[] = []; cli.beforeEach(async () => { calls.push('before'); }); await cli.run('module list'); calls.push('after'); expect(calls).toEqual(['before', 'after']); }); test('afterEach runs after command', async () => { const calls: string[] = []; cli.afterEach(async () => { calls.push('after'); }); calls.push('before'); await cli.run('module list'); expect(calls).toEqual(['before', 'after']); }); test('multiple hooks run in order', async () => { const calls: string[] = []; cli.beforeEach(async () => { calls.push('before1'); }); cli.beforeEach(async () => { calls.push('before2'); }); cli.afterEach(async () => { calls.push('after1'); }); cli.afterEach(async () => { calls.push('after2'); }); await cli.run('module list'); expect(calls).toEqual(['before1', 'before2', 'after1', 'after2']); }); }); describe('filesystem helpers', () => { let tempDir: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), 'cli-context-test-')); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); test('mkdir creates directory', async () => { const testPath = join(tempDir, 'test-dir'); await cli.mkdir(testPath); const exists = await cli.exists(testPath); expect(exists).toBe(true); }); test('writeFile creates file', async () => { const testFile = join(tempDir, 'test.txt'); await cli.writeFile(testFile, 'hello world'); const exists = await cli.exists(testFile); expect(exists).toBe(true); }); test('unlink removes file', async () => { const testFile = join(tempDir, 'test.txt'); await cli.writeFile(testFile, 'content'); expect(await cli.exists(testFile)).toBe(true); await cli.unlink(testFile); expect(await cli.exists(testFile)).toBe(false); }); test('rmdir removes directory', async () => { const testPath = join(tempDir, 'test-dir'); await cli.mkdir(testPath); expect(await cli.exists(testPath)).toBe(true); await cli.rmdir(testPath); expect(await cli.exists(testPath)).toBe(false); }); test('exists returns false for non-existent path', async () => { const exists = await cli.exists(join(tempDir, 'nonexistent')); expect(exists).toBe(false); }); }); describe('verbose mode', () => { test('setVerbose returns this for chaining', () => { expect(cli.setVerbose(true)).toBe(cli); }); }); describe('trace export', () => { test('exportTrace returns JSON', async () => { await cli.run('module list'); const trace = cli.exportTrace('json'); expect(() => JSON.parse(trace)).not.toThrow(); const parsed = JSON.parse(trace); expect(Array.isArray(parsed)).toBe(true); expect(parsed.length).toBeGreaterThan(0); }); test('exportTrace returns markdown', async () => { await cli.run('module list'); const trace = cli.exportTrace('markdown'); expect(trace).toContain('# CLI Execution Trace'); expect(trace).toContain('command'); }); test('exportTrace returns HTML', async () => { await cli.run('module list'); const trace = cli.exportTrace('html'); expect(trace).toContain(''); expect(trace).toContain(''); }); }); describe('clone', () => { test('creates new context with same config', async () => { const cloned = await cli.clone(); expect(cloned).not.toBe(cli); expect(cloned).toBeInstanceOf(CLIContext); await cloned.dispose(); }); test('cloned context runs commands independently', async () => { const cloned = await cli.clone(); await cli.run('module list'); const result = await cloned.run('module list'); expect(result.exitCode).toBe(0); await cloned.dispose(); }); test('cloned context copies hooks', async () => { const calls: string[] = []; cli.beforeEach(async () => { calls.push('before'); }); const cloned = await cli.clone(); await cloned.run('module list'); expect(calls).toContain('before'); await cloned.dispose(); }); }); describe('custom methods (module system)', () => { test('registers custom method', () => { CLIContext.register('testMethod', () => 'test result'); const instance = cli as unknown as { testMethod: () => string }; expect(instance.testMethod()).toBe('test result'); }); test('registers multiple methods at once', () => { CLIContext.register({ method1: () => 'result1', method2: () => 'result2', }); const instance = cli as unknown as { method1: () => string; method2: () => string; }; expect(instance.method1()).toBe('result1'); expect(instance.method2()).toBe('result2'); }); }); describe('interactive features', () => { test('interactive prompts are now available', () => { // These will be tested in integration tests expect(cli.on).toBeDefined(); expect(cli.expectOutput).toBeDefined(); expect(cli.sendKeys).toBeDefined(); }); }); });