import { afterAll, describe, expect, test } from "bun:test"; import { spawn } from "node:child_process"; import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { safeParseTale } from "@taleseal/core"; /** * End-to-end for `taleseal init` — the tale.cli.test.ts idiom (subprocess against a * throwaway HOME and a fake node:http server), plus a PATH-prepended stub `claude` * executable that records its argv, so the plugin shell-outs are asserted without * any real tool installed. */ const cliPath = fileURLToPath(new URL("./cli.ts", import.meta.url)); const root = mkdtempSync(join(tmpdir(), "taleseal-init-cli-")); afterAll(() => rmSync(root, { recursive: true, force: true })); let caseId = 0; function freshHome(): string { caseId += 1; const home = join(root, `home-${caseId}`); mkdirSync(home, { recursive: true }); return home; } /** A stub tool executable on its own PATH dir; every invocation appends its argv to a log. */ function stubTool(home: string, name: string, exitCode = 0): { pathDir: string; argvLog: string } { const pathDir = join(home, "stub-bin"); mkdirSync(pathDir, { recursive: true }); const argvLog = join(home, `${name}-argv.log`); const script = `#!/bin/sh\necho "$@" >> ${JSON.stringify(argvLog)}\nexit ${exitCode}\n`; const bin = join(pathDir, name); writeFileSync(bin, script); chmodSync(bin, 0o755); return { pathDir, argvLog }; } const argvLines = (log: string): string[] => { try { return readFileSync(log, "utf8").trim().split("\n"); } catch { return []; } }; interface CliResult { status: number | null; stdout: string; stderr: string; } function runCli(args: string[], home: string, extra: Record = {}): Promise { const env: NodeJS.ProcessEnv = { ...process.env, HOME: home }; delete env.TALESEAL_API_KEY; delete env.TALESEAL_URL; delete env.XDG_CONFIG_HOME; Object.assign(env, extra); return new Promise((resolve, reject) => { const child = spawn(process.execPath, [cliPath, ...args], { env, stdio: ["ignore", "pipe", "pipe"], timeout: 30_000, }); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk: string) => { stdout += chunk; }); child.stderr.on("data", (chunk: string) => { stderr += chunk; }); child.on("error", reject); child.on("close", (status) => resolve({ status, stdout, stderr })); }); } // ---------- the fake API ---------- const TALE_ID = "wlcm1234abcd"; const CLAIM_URL = "https://taleseal.example/claim/tc_9hXk2mQ7RfLpZw4TnB8eVs6yUdCa01Gt"; interface Canned { status: number; body: unknown; } interface Seen { method: string; url: string; body: string; auth: string | undefined; client: string | undefined; } const anonLive: Canned = { status: 200, body: { id: TALE_ID, url: `https://taleseal.example/t/${TALE_ID}`, status: "published", claimUrl: CLAIM_URL, claimExpiresAt: "2026-07-22T09:14:03.000Z", }, }; async function startServer(anon: Canned = anonLive): Promise<{ url: string; seen: Seen[]; close: () => void }> { const seen: Seen[] = []; const server = createServer((req, res) => { let raw = ""; req.setEncoding("utf8"); req.on("data", (chunk: string) => { raw += chunk; }); req.on("end", () => { const method = req.method ?? ""; const url = req.url ?? ""; seen.push({ method, url, body: raw, auth: req.headers.authorization, client: req.headers["x-taleseal-client"] as string | undefined, }); const send = (canned: Canned): void => { res.statusCode = canned.status; res.setHeader("content-type", "application/json"); res.end(JSON.stringify(canned.body)); }; if (method === "POST" && url.endsWith("/v1/tales/anonymous")) return send(anon); if (method === "POST" && url.endsWith("/v1/tales")) { return send({ status: 200, body: { id: TALE_ID, url: `https://taleseal.example/t/${TALE_ID}`, status: "draft" }, }); } if (method === "POST" && url.endsWith("/publish")) { return send({ status: 200, body: { id: TALE_ID, url: `https://taleseal.example/t/${TALE_ID}`, status: "published", revision: 1 }, }); } res.statusCode = 500; res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ error: `unrouted ${method} ${url}` })); }); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; return { url, seen, close: () => server.close() }; } // ---------- tests ---------- describe("taleseal init (bare)", () => { test("publishes anonymously — no auth header, real client header, valid tale — and prints both links", async () => { const server = await startServer(); try { const res = await runCli(["init"], freshHome(), { TALESEAL_URL: server.url }); expect(res.status).toBe(0); const post = server.seen.find((s) => s.url.endsWith("/v1/tales/anonymous")); expect(post).toBeDefined(); expect(post?.auth).toBeUndefined(); expect(post?.client).toMatch(/^taleseal-cli\//); expect(safeParseTale(JSON.parse(post?.body ?? "{}")).success).toBe(true); expect(res.stdout).toContain(`https://taleseal.example/t/${TALE_ID}`); expect(res.stdout).toContain(CLAIM_URL); expect(res.stdout).toContain("shown once"); } finally { server.close(); } }); test("404 (anonymous publishing off) exits 0 with the login fallback", async () => { const server = await startServer({ status: 404, body: { error: "not found" } }); try { const res = await runCli(["init"], freshHome(), { TALESEAL_URL: server.url }); expect(res.status).toBe(0); expect(res.stderr).toContain("not switched on"); expect(res.stderr).toContain("npx -y taleseal@latest login"); } finally { server.close(); } }); test("503 (at capacity) exits 0 with the try-again message", async () => { const server = await startServer({ status: 503, body: { error: "at capacity" } }); try { const res = await runCli(["init"], freshHome(), { TALESEAL_URL: server.url }); expect(res.status).toBe(0); expect(res.stderr).toContain("at capacity"); } finally { server.close(); } }); test("a stored key publishes to the account instead — draft then publish, no anonymous call", async () => { const home = freshHome(); const configDir = join(home, ".config", "taleseal"); mkdirSync(configDir, { recursive: true }); writeFileSync(join(configDir, "config.json"), JSON.stringify({ apiKey: "tk_test" })); const server = await startServer(); try { const res = await runCli(["init"], home, { TALESEAL_URL: server.url }); expect(res.status).toBe(0); expect(server.seen.some((s) => s.url.endsWith("/v1/tales/anonymous"))).toBe(false); expect(server.seen.some((s) => s.method === "POST" && s.url.endsWith("/v1/tales"))).toBe(true); expect(server.seen.some((s) => s.url.endsWith("/publish"))).toBe(true); expect(res.stdout).toContain("already yours"); } finally { server.close(); } }); test("an unknown tool exits 1 and names the valid ones", async () => { const res = await runCli(["init", "nonsense"], freshHome()); expect(res.status).toBe(1); expect(res.stderr).toContain("claude, codex, cursor"); }); test("--json prints one machine-readable object", async () => { const server = await startServer(); try { const res = await runCli(["init", "--json"], freshHome(), { TALESEAL_URL: server.url }); expect(res.status).toBe(0); const out = JSON.parse(res.stdout) as Record; expect(out.tool).toBeNull(); expect(out.setup).toBe("skipped"); expect(out.publish).toBe("live"); expect(out.url).toBe(`https://taleseal.example/t/${TALE_ID}`); expect(out.claimUrl).toBe(CLAIM_URL); } finally { server.close(); } }); }); describe("taleseal init ", () => { test("init claude runs both plugin commands via the stub AND still publishes", async () => { const home = freshHome(); const stub = stubTool(home, "claude"); const server = await startServer(); try { const res = await runCli(["init", "claude"], home, { TALESEAL_URL: server.url, PATH: `${stub.pathDir}:${process.env.PATH ?? ""}`, }); expect(res.status).toBe(0); expect(argvLines(stub.argvLog)).toEqual([ "--version", "plugin marketplace add Taleseal/taleseal", "plugin install taleseal@taleseal", ]); expect(server.seen.some((s) => s.url.endsWith("/v1/tales/anonymous"))).toBe(true); expect(res.stdout).toContain(`https://taleseal.example/t/${TALE_ID}`); } finally { server.close(); } }); test("an absent tool prints the manual commands and the publish still lands, exit 0", async () => { const home = freshHome(); const server = await startServer(); try { // an empty PATH dir: `claude` resolves nowhere const emptyBin = join(home, "empty-bin"); mkdirSync(emptyBin, { recursive: true }); const res = await runCli(["init", "claude"], home, { TALESEAL_URL: server.url, PATH: emptyBin }); expect(res.status).toBe(0); expect(res.stderr).toContain("claude plugin marketplace add Taleseal/taleseal"); expect(server.seen.some((s) => s.url.endsWith("/v1/tales/anonymous"))).toBe(true); } finally { server.close(); } }); test("a failing install warns with manual steps but never blocks the publish, exit 0", async () => { const home = freshHome(); const stub = stubTool(home, "codex", 1); const server = await startServer(); try { const res = await runCli(["init", "codex"], home, { TALESEAL_URL: server.url, PATH: `${stub.pathDir}:${process.env.PATH ?? ""}`, }); expect(res.status).toBe(0); expect(res.stderr).toContain("codex plugin add taleseal@taleseal"); expect(server.seen.some((s) => s.url.endsWith("/v1/tales/anonymous"))).toBe(true); } finally { server.close(); } }); test("init cursor spawns nothing, prints the manual steps, publishes", async () => { const home = freshHome(); const server = await startServer(); try { const res = await runCli(["init", "cursor"], home, { TALESEAL_URL: server.url }); expect(res.status).toBe(0); expect(res.stderr).toContain("plugins/cursor"); expect(server.seen.some((s) => s.url.endsWith("/v1/tales/anonymous"))).toBe(true); } finally { server.close(); } }); });