|
| 1 | +import fs from "fs" |
| 2 | +import path from "path" |
| 3 | +import { expect } from "bun:test" |
| 4 | +import { add, link, sync, remove } from "@spader/dotllm/cli/commands/index" |
| 5 | +import { Config } from "@spader/dotllm/core" |
| 6 | +import type { Command } from "@spader/dotllm/cli/yargs" |
| 7 | + |
| 8 | +const commands: Record<string, Command> = { add, link, sync, remove } |
| 9 | + |
| 10 | +type ExpectedEntry = { |
| 11 | + name?: string |
| 12 | + uri?: string |
| 13 | + kind?: "file" | "url" |
| 14 | + description?: string |
| 15 | +} |
| 16 | + |
| 17 | +type ExpectedStore = Record<string, { symlink: string } | true> |
| 18 | + |
| 19 | +type Spec = { |
| 20 | + command: string |
| 21 | + argv: Record<string, unknown> |
| 22 | + expect: { |
| 23 | + exit?: number |
| 24 | + global?: Record<string, ExpectedEntry> |
| 25 | + store?: ExpectedStore |
| 26 | + local?: Record<string, ExpectedEntry> |
| 27 | + localAbsent?: string[] |
| 28 | + ref?: string[] |
| 29 | + refAbsent?: string[] |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +class ExitError extends Error { |
| 34 | + code: number |
| 35 | + constructor(code: number) { |
| 36 | + super(`process.exit(${code})`) |
| 37 | + this.code = code |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +export async function run(spec: Spec): Promise<void> { |
| 42 | + const cmd = commands[spec.command] |
| 43 | + if (!cmd?.handler) throw new Error(`unknown command: ${spec.command}`) |
| 44 | + |
| 45 | + const original = process.exit |
| 46 | + let exitCode: number | undefined |
| 47 | + |
| 48 | + process.exit = (code?: number) => { |
| 49 | + exitCode = code ?? 0 |
| 50 | + throw new ExitError(exitCode) |
| 51 | + } |
| 52 | + |
| 53 | + try { |
| 54 | + await cmd.handler(spec.argv) |
| 55 | + } catch (e) { |
| 56 | + if (!(e instanceof ExitError)) throw e |
| 57 | + } finally { |
| 58 | + process.exit = original |
| 59 | + } |
| 60 | + |
| 61 | + if (spec.expect.exit !== undefined) { |
| 62 | + expect(exitCode).toBe(spec.expect.exit) |
| 63 | + } |
| 64 | + |
| 65 | + if (spec.expect.global) { |
| 66 | + const global = Config.Global.read() |
| 67 | + for (const [name, expected] of Object.entries(spec.expect.global)) { |
| 68 | + const entry = Config.Global.find(global, name) |
| 69 | + expect(entry).toBeDefined() |
| 70 | + if (!entry) continue |
| 71 | + if (expected.name !== undefined) expect(entry.name).toBe(expected.name) |
| 72 | + if (expected.uri !== undefined) expect(entry.uri).toBe(expected.uri) |
| 73 | + if (expected.kind !== undefined) expect(entry.kind).toBe(expected.kind) |
| 74 | + if (expected.description !== undefined) expect(entry.description).toBe(expected.description) |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + if (spec.expect.store) { |
| 79 | + for (const [name, expected] of Object.entries(spec.expect.store)) { |
| 80 | + const target = path.join(Config.storeDir(), name) |
| 81 | + expect(fs.existsSync(target)).toBe(true) |
| 82 | + if (expected === true) continue |
| 83 | + expect(fs.lstatSync(target).isSymbolicLink()).toBe(true) |
| 84 | + expect(fs.readlinkSync(target)).toBe(expected.symlink) |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + if (spec.expect.local) { |
| 89 | + const local = Config.Local.read() |
| 90 | + for (const [name, expected] of Object.entries(spec.expect.local)) { |
| 91 | + expect(Config.Local.has(local, name)).toBe(true) |
| 92 | + const entry = local.refs[name] |
| 93 | + if (!entry) continue |
| 94 | + if (expected.name !== undefined) expect(entry.name).toBe(expected.name) |
| 95 | + if (expected.uri !== undefined) expect(entry.uri).toBe(expected.uri) |
| 96 | + if (expected.kind !== undefined) expect(entry.kind).toBe(expected.kind) |
| 97 | + if (expected.description !== undefined) expect(entry.description).toBe(expected.description) |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + if (spec.expect.localAbsent) { |
| 102 | + const local = Config.Local.read() |
| 103 | + for (const name of spec.expect.localAbsent) { |
| 104 | + expect(Config.Local.has(local, name)).toBe(false) |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + if (spec.expect.ref) { |
| 109 | + for (const name of spec.expect.ref) { |
| 110 | + const target = path.join(Config.refDir(), name) |
| 111 | + expect(fs.existsSync(target)).toBe(true) |
| 112 | + expect(fs.lstatSync(target).isSymbolicLink()).toBe(true) |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + if (spec.expect.refAbsent) { |
| 117 | + for (const name of spec.expect.refAbsent) { |
| 118 | + const target = path.join(Config.refDir(), name) |
| 119 | + expect(fs.existsSync(target)).toBe(false) |
| 120 | + } |
| 121 | + } |
| 122 | +} |
0 commit comments