Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146/** * Tests for `shard web` dispatch — admin subcommand routing vs. daemon-start. * * `shard web` with no admin subcommand starts the localhost daemon + browser client and * prints the URL. Admin subcommands (approve, clients, revoke, token) route to the web * admin client unchanged. These tests use subprocesss (the dispatch lives in the * `await main()` entrypoint, which cannot be imported without side effects) with an * isolated HOME + SHARD_SKIP_WEB_BUILD so no real state is touched and no vite build runs. */import { describe, expect, it } from 'bun:test';import { mkdtempSync, rmSync } from 'node:fs';import { join } from 'node:path';import { tmpdir } from 'node:os';
/** Spawn the CLI with an isolated home and capture stdout/stderr text incrementally. */async function runCli( argv: string[], opts: { killAfterMs?: number; timeoutMs?: number } = {},): Promise<{ code: number | null; stdout: string; stderr: string }> { const home = mkdtempSync(join(tmpdir(), 'shard-web-')); const env: Record<string, string | undefined> = { ...process.env, HOME: home, XDG_CONFIG_HOME: join(home, '.config'), SHARD_SKIP_WEB_BUILD: '1', }; const proc = Bun.spawn(['bun', 'src/cli/index.ts', ...argv], { cwd: process.cwd(), env: env as Record<string, string>, stdout: 'pipe', stderr: 'pipe', });
if (opts.killAfterMs !== undefined) { // Long-running daemon path: read output incrementally until a needle or timeout, // then kill. We can't await proc.exited (it never exits); read streams with a deadline. const dec = new TextDecoder(); const outReader = proc.stdout.getReader(); const errReader = proc.stderr.getReader(); let stdout = ''; let stderr = ''; const deadline = Date.now() + opts.killAfterMs; while (Date.now() < deadline) { const [outChunk, errChunk] = await Promise.all([ Promise.race([ outReader.read().then((r) => r.value ?? null).catch(() => null), Bun.sleep(80).then(() => null), ]), Promise.race([ errReader.read().then((r) => r.value ?? null).catch(() => null), Bun.sleep(80).then(() => null), ]), ]); if (outChunk) stdout += dec.decode(outChunk); if (errChunk) stderr += dec.decode(errChunk); if (stdout.includes('http://127.0.0.1:') || stderr.includes('listening on')) break; } outReader.cancel().catch(() => {}); errReader.cancel().catch(() => {}); proc.kill(); try { await proc.exited; } catch { /* killed */ } rmSync(home, { recursive: true, force: true }); return { code: null, stdout, stderr }; }
const [code, stdout, stderr] = await Promise.all([ proc.exited, new Response(proc.stdout).text(), new Response(proc.stderr).text(), ]); rmSync(home, { recursive: true, force: true }); return { code, stdout, stderr };}
describe('shard web — admin subcommand routing (preserved)', () => { it('`shard web clients` (no daemon) → exit 2, "daemon is not running"', async () => { const { code, stderr } = await runCli(['web', 'clients']); expect(code).toBe(2); expect(stderr).toContain('daemon is not running'); // It did NOT start a daemon (no URL printed). expect(stderr).not.toContain('listening on'); }, 10_000);
it('`shard web approve 123456` (no daemon) → exit 2', async () => { const { code, stderr } = await runCli(['web', 'approve', '123456']); expect(code).toBe(2); expect(stderr).toContain('daemon is not running'); }, 10_000);
it('`shard web token rotate` (no daemon) → exit 2', async () => { const { code, stderr } = await runCli(['web', 'token', 'rotate']); expect(code).toBe(2); expect(stderr).toContain('daemon is not running'); }, 10_000);});
describe('shard web — daemon-start (no admin subcommand)', () => { it('bare `shard` defaults to the web experience', async () => { const { stdout, stderr } = await runCli([], { killAfterMs: 6_000 }); expect(stdout).toContain('Shard web ready: http://127.0.0.1:'); expect(stderr).toContain('listening on'); }, 15_000);
it('`shard web` starts the daemon and prints a URL', async () => { const { stdout, stderr } = await runCli(['web'], { killAfterMs: 6_000 }); expect(stdout).toContain('Shard web ready: http://127.0.0.1:'); expect(stderr).toContain('listening on'); }, 15_000);
it('`shard web --web-port <port>` starts on the pinned port', async () => { // Find a free port to avoid collisions in shared CI/dev environments. const { createServer } = await import('node:net'); const freePort: number = await new Promise((resolve, reject) => { const s = createServer(); s.unref(); s.on('error', reject); s.listen(0, '127.0.0.1', () => { const addr = s.address(); s.close(() => resolve(typeof addr === 'object' && addr ? addr.port : 0)); }); }); const { stdout } = await runCli(['web', '--web-port', String(freePort)], { killAfterMs: 6_000 }); expect(stdout).toContain(`http://127.0.0.1:${freePort}`); }, 15_000);
it('`shard web --help` prints web usage and exits 0', async () => { const { code, stderr } = await runCli(['web', '--help']); expect(code).toBe(0); expect(stderr).toContain('start the daemon + browser client'); expect(stderr).toContain('approve <code>'); }, 10_000);});
describe('shard daemon — refactored path still works', () => { it('`shard daemon` starts and prints the socket', async () => { const { stderr } = await runCli(['daemon'], { killAfterMs: 6_000 }); expect(stderr).toContain('shardd: listening on'); }, 15_000);
it('`shard daemon --help` prints daemon usage and exits 0', async () => { const { code, stderr } = await runCli(['daemon', '--help']); expect(code).toBe(0); expect(stderr).toContain('Usage: shard daemon'); }, 10_000);});