import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test'; import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { initCommand, listTemplates } from '../src/commands/init.js'; import { tryLoadConfig, loadConfig } from '../src/config.js'; import { doctorCommand } from '../src/commands/doctor.js'; import { createPrekeyServer, MemoryPrekeyStore } from '@shade/server'; import { SubtleCryptoProvider } from '@shade/crypto-web'; const crypto = new SubtleCryptoProvider(); describe('CLI: init command', () => { let tmpDir: string; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'shade-cli-')); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); test('listTemplates returns the bundled templates', () => { const templates = listTemplates(); expect(templates).toContain('bun-server'); expect(templates).toContain('chat-demo'); }); test('init scaffolds a bun-server project with substitutions', async () => { await initCommand({ name: 'my-app', template: 'bun-server', cwd: tmpDir }); const target = join(tmpDir, 'my-app'); expect(existsSync(target)).toBe(true); expect(existsSync(join(target, 'package.json'))).toBe(true); expect(existsSync(join(target, 'src/index.ts'))).toBe(true); expect(existsSync(join(target, '.shaderc.json'))).toBe(true); const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf-8')); expect(pkg.name).toBe('my-app'); const shaderc = JSON.parse(readFileSync(join(target, '.shaderc.json'), 'utf-8')); expect(shaderc.address).toBe('my-app'); const index = readFileSync(join(target, 'src/index.ts'), 'utf-8'); expect(index).not.toContain('__PROJECT_NAME__'); expect(index).toContain('my-app'); }); test('init with custom prekey-server URL', async () => { await initCommand({ name: 'app2', template: 'bun-server', cwd: tmpDir, prekeyServer: 'https://custom.example.com', }); const target = join(tmpDir, 'app2'); const shaderc = JSON.parse(readFileSync(join(target, '.shaderc.json'), 'utf-8')); expect(shaderc.prekeyServer).toBe('https://custom.example.com'); }); test('init refuses to overwrite existing directory', async () => { await initCommand({ name: 'foo', cwd: tmpDir }); expect(initCommand({ name: 'foo', cwd: tmpDir })).rejects.toThrow(/already exists/); }); test('init with unknown template throws with helpful error', async () => { expect(initCommand({ name: 'x', template: 'nonexistent', cwd: tmpDir })).rejects.toThrow( /not found/, ); }); test('chat-demo template scaffolds correctly', async () => { await initCommand({ name: 'chat', template: 'chat-demo', cwd: tmpDir }); const target = join(tmpDir, 'chat'); expect(existsSync(join(target, 'src/alice.ts'))).toBe(true); expect(existsSync(join(target, 'src/bob.ts'))).toBe(true); const alice = readFileSync(join(target, 'src/alice.ts'), 'utf-8'); expect(alice).not.toContain('__PROJECT_NAME__'); }); }); describe('CLI: config loading', () => { let tmpDir: string; let originalEnv: typeof process.env; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'shade-config-')); originalEnv = { ...process.env }; delete process.env.SHADE_PREKEY_SERVER; delete process.env.SHADE_DB_PATH; delete process.env.SHADE_OBSERVER_TOKEN; }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); process.env = originalEnv; }); test('loads .shaderc.json from cwd', () => { writeFileSync( join(tmpDir, '.shaderc.json'), JSON.stringify({ prekeyServer: 'https://example.com', storage: 'sqlite:./test.db' }), ); const config = loadConfig(tmpDir); expect(config.prekeyServer).toBe('https://example.com'); expect(config.storage).toBe('sqlite:./test.db'); }); test('falls back to SHADE_PREKEY_SERVER env var', () => { process.env.SHADE_PREKEY_SERVER = 'https://env.example.com'; const config = loadConfig(tmpDir); expect(config.prekeyServer).toBe('https://env.example.com'); }); test('file config takes precedence over env var', () => { process.env.SHADE_PREKEY_SERVER = 'https://env.example.com'; writeFileSync( join(tmpDir, '.shaderc.json'), JSON.stringify({ prekeyServer: 'https://file.example.com' }), ); const config = loadConfig(tmpDir); expect(config.prekeyServer).toBe('https://file.example.com'); }); test('throws with clear error when missing prekeyServer', () => { expect(() => loadConfig(tmpDir)).toThrow(/Missing prekeyServer/); }); test('tryLoadConfig returns error without throwing', () => { const result = tryLoadConfig(tmpDir); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain('Missing prekeyServer'); }); }); describe('CLI: doctor command', () => { let tmpDir: string; let originalEnv: typeof process.env; let originalCwd: string; let serverStop: (() => void) | null = null; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'shade-doctor-')); originalEnv = { ...process.env }; originalCwd = process.cwd(); process.chdir(tmpDir); }); afterEach(() => { process.chdir(originalCwd); if (serverStop) { serverStop(); serverStop = null; } rmSync(tmpDir, { recursive: true, force: true }); process.env = originalEnv; }); test('doctor reports missing config', async () => { const logs: string[] = []; const originalLog = console.log; console.log = (...args) => logs.push(args.join(' ')); try { await doctorCommand(); } finally { console.log = originalLog; } const out = logs.join('\n'); expect(out).toContain('Missing prekeyServer'); }); test('doctor reports reachable prekey server', async () => { // Spin up a real prekey server const port = 19300 + Math.floor(Math.random() * 200); const app = createPrekeyServer({ crypto, store: new MemoryPrekeyStore(), disableRateLimit: true, }); const server = Bun.serve({ port, fetch: app.fetch }); serverStop = () => server.stop(); writeFileSync( join(tmpDir, '.shaderc.json'), JSON.stringify({ prekeyServer: `http://localhost:${port}`, storage: `sqlite:${tmpDir}/client.db`, }), ); const logs: string[] = []; const originalLog = console.log; console.log = (...args) => logs.push(args.join(' ')); try { await doctorCommand(); } finally { console.log = originalLog; } const out = logs.join('\n'); expect(out).toContain('Prekey server is reachable'); }); });