import { afterEach, describe, expect, test, spyOn } from "bun:test"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { YAML } from "bun"; import { initializeProject } from "../../command/init"; import { managedHeader } from "../../lib/scaffold"; import { readTrust, resolveTrustIdentity, updateTrust } from "../../lib/trust"; import { KuberApiError } from "../../lib/api"; import type { ScaffoldQuestion } from "../../lib/scaffold-prompts"; const roots: string[] = []; const originalConfig = process.env.XDG_CONFIG_HOME; afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); if (originalConfig === undefined) delete process.env.XDG_CONFIG_HOME; else process.env.XDG_CONFIG_HOME = originalConfig; }); async function root() { const path = await mkdtemp(join(tmpdir(), "kuber-init-test-")); roots.push(path); process.env.XDG_CONFIG_HOME = join(path, "config"); return path; } describe("kuber init", () => { test("interactive YAML fields finish before login and automatic trust", async () => { const cwd = await root(); const events: string[] = []; const session = { token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }; const log = spyOn(console, "log").mockImplementation(() => {}); try { await initializeProject( cwd, {}, { prompt: async (question) => { events.push(question.name); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe( false, ); if (question.name === "source") return "image"; if (question.name === "image") return "nginx:stable"; if (question.name === "path") return "."; expect(question.type).toBe("snippet"); expect(question.template).toStartWith( "name: ${project}\nservices:\n ${name}:", ); expect(question.template).not.toContain("managedBy"); const project = question.fields?.find( ({ name }) => name === "project", ); expect(project?.validate?.("Bad Project")).not.toBe(true); expect(project?.validate?.("demo")).toBe(true); return { values: { project: "demo", name: "web", replicas: "1" }, result: "malicious preview", }; }, session: async () => { events.push("session"); return undefined; }, login: async () => { events.push("login"); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe( false, ); return session; }, request: async (_path, init) => { events.push(init?.method ?? "GET"); return (init?.method ? undefined : { fingerprints: [] }) as never; }, persistTrust: async () => { events.push("trust"); }, }, ); expect(events).toEqual([ "source", "image", "path", "compose", "session", "login", "GET", "POST", "trust", ]); const content = await readFile(join(cwd, "compose.yml"), "utf8"); expect(content.split("\n")[0]).toBe(managedHeader); expect(YAML.parse(content)).toEqual({ managedBy: "kuber", name: "demo", services: { web: { image: "nginx:stable", deploy: { replicas: 1 } } }, }); expect(content).not.toContain("malicious"); } finally { log.mockRestore(); } }); test("flagged project and configured project bypass editable project field; existing login is reused", async () => { for (const configured of [false, true]) { const cwd = await root(); if (configured) await writeFile( join(cwd, ".kuberrc.ts"), 'export default { project: "fixed" };\n', ); let logins = 0; const questions: ScaffoldQuestion[] = []; const log = spyOn(console, "log").mockImplementation(() => {}); try { await initializeProject( cwd, { source: "auto", cnb: "", path: ".", ...(configured ? {} : { project: "fixed" }), }, { prompt: async (question) => { questions.push(question); expect(question.template).toStartWith('name: "fixed"'); expect( question.fields?.some(({ name }) => name === "project"), ).toBe(false); return { values: { project: "ignored", name: "web", replicas: "1" }, }; }, session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), login: async () => { logins++; throw new Error("unneeded login"); }, request: async (_path, init) => (init?.method ? undefined : { fingerprints: [] }) as never, persistTrust: async () => {}, }, ); expect(questions.map(({ type }) => type)).toEqual(["snippet"]); expect(logins).toBe(0); expect( YAML.parse(await readFile(join(cwd, "compose.yml"), "utf8")), ).toMatchObject({ name: "fixed", services: { web: { build: "auto" } }, }); } finally { log.mockRestore(); } } }); test("cancel or invalid snippet performs no login, trust, or filesystem writes", async () => { for (const failure of ["cancel", "invalid"]) { const cwd = await root(); let downstream = 0; await expect( initializeProject( cwd, { source: "template", template: "bun-service", path: "." }, { prompt: async () => { if (failure === "cancel") throw new Error("cancelled"); return { values: { project: "demo", name: "web", cpu: "-1", replicas: "1", }, }; }, session: async () => { downstream++; return undefined; }, login: async () => { downstream++; throw new Error("unexpected login"); }, request: async () => { downstream++; return undefined as never; }, }, ), ).rejects.toThrow( failure === "cancel" ? "cancelled" : "CPU must be a positive", ); expect(downstream).toBe(0); for (const name of ["compose.yml", "Dockerfile", ".dockerignore"]) expect(await Bun.file(join(cwd, name)).exists()).toBe(false); } }); test("uses an existing configured project for Compose and trust", async () => { const cwd = await root(); await writeFile( join(cwd, ".kuberrc.ts"), 'export default { project: "configured" };\n', ); const paths: string[] = []; const log = spyOn(console, "log").mockImplementation(() => {}); try { await initializeProject( cwd, { nonInteractive: true }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (path, init) => { paths.push(path); return ( init?.method === "POST" ? undefined : { fingerprints: [] } ) as never; }, }, ); expect( YAML.parse(await readFile(join(cwd, "compose.yml"), "utf8")), ).toMatchObject({ name: "configured" }); expect(paths).toEqual([ "/workspaces/configured/trust", "/workspaces/configured/trust", ]); } finally { log.mockRestore(); } }); test("empty directory creates documented image-only example and trusts selected project", async () => { const cwd = await root(); const calls: string[] = []; const log = spyOn(console, "log").mockImplementation(() => {}); try { await initializeProject( cwd, { nonInteractive: true, project: "demo" }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (path, init) => { calls.push(path); return ( init?.method === "POST" ? undefined : { fingerprints: [] } ) as never; }, }, ); const content = await readFile(join(cwd, "compose.yml"), "utf8"); expect(content.split("\n")[0]).toBe(managedHeader); const compose = YAML.parse(content); expect(compose).toMatchObject({ managedBy: "kuber", name: "demo", services: { [join(cwd).split("/").at(-1)!.toLowerCase()]: { image: "nginx:stable", }, }, }); expect(calls).toEqual([ "/workspaces/demo/trust", "/workspaces/demo/trust", ]); expect(await readTrust()).toHaveLength(1); expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(false); } finally { log.mockRestore(); } }); test("multiple manifests require explicit selection in noninteractive mode", async () => { const cwd = await root(); for (const name of ["one", "two"]) { await mkdir(join(cwd, name)); await writeFile(join(cwd, name, "package.json"), "{}"); } await expect( initializeProject( cwd, { nonInteractive: true }, { session: async () => undefined }, ), ).rejects.toThrow("specify --path"); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(false); }); test("revoked session retries once without duplicating trust; failed grant never writes local trust", async () => { const cwd = await root(); let attempts = 0; let logins = 0; const log = spyOn(console, "log").mockImplementation(() => {}); try { await initializeProject( cwd, { nonInteractive: true, project: "demo" }, { session: async () => ({ token: "old", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), login: async () => { logins++; return { token: "new", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }; }, request: async (_path, init) => { if (++attempts === 2) throw new KuberApiError("revoked", 401); return ( init?.method === "POST" ? undefined : { fingerprints: [] } ) as never; }, }, ); expect([attempts, logins]).toEqual([4, 1]); expect(await readTrust()).toHaveLength(1); } finally { log.mockRestore(); } const other = await root(); await expect( initializeProject( other, { nonInteractive: true, project: "bad" }, { session: async () => undefined, login: async () => ({ token: "new", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async () => { throw new KuberApiError("failed", 503); }, }, ), ).rejects.toThrow("failed"); expect(await readTrust()).toEqual([]); expect(await Bun.file(join(other, "compose.yml")).exists()).toBe(false); }); test("successful grant with failed local persistence keeps registration and generated files", async () => { const cwd = await root(); const identity = await resolveTrustIdentity("demo", cwd); const trusted = new Set(); const calls: string[] = []; const log = spyOn(console, "log").mockImplementation(() => {}); try { const failure = await initializeProject( cwd, { nonInteractive: true, project: "demo", source: "template", template: "static-nginx", }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (path, init) => { calls.push(init?.method ?? "GET"); if (init?.method === "POST") trusted.add((init.json as { fingerprint: string }).fingerprint); expect(path).toBe("/workspaces/demo/trust"); return ( init?.method ? undefined : { fingerprints: [...trusted] } ) as never; }, persistTrust: async () => { throw new Error("disk full: secret-token"); }, }, ).catch((error: unknown) => error); expect(failure).toBeInstanceOf(Error); expect((failure as Error).message).toMatch( /Server registration exists.*kuber trust/, ); expect((failure as Error).message).not.toContain("secret-token"); expect((failure as Error).cause).toBeUndefined(); expect(calls).toEqual(["GET", "POST"]); expect(trusted.has(identity.fingerprint)).toBe(true); expect(await readTrust()).toEqual([]); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true); expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(true); expect(await Bun.file(join(cwd, ".dockerignore")).exists()).toBe(true); expect(log).not.toHaveBeenCalled(); } finally { log.mockRestore(); } }); test("pre-existing server grant and local trust survive local persistence failure", async () => { const cwd = await root(); const identity = await resolveTrustIdentity("demo", cwd); const existing = await resolveTrustIdentity("other", cwd); await updateTrust(() => [existing]); const calls: string[] = []; await expect( initializeProject( cwd, { nonInteractive: true, project: "demo" }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (_path, init) => { calls.push(init?.method ?? "GET"); return { fingerprints: [identity.fingerprint] } as never; }, persistTrust: async () => { throw new Error("disk full"); }, }, ), ).rejects.toThrow(/Server registration exists.*kuber trust/); expect(calls).toEqual(["GET"]); expect(await readTrust()).toEqual([existing]); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true); }); test("concurrent grant between GET and POST is never revoked on local failure", async () => { const cwd = await root(); const identity = await resolveTrustIdentity("demo", cwd); const calls: string[] = []; const trusted = new Set(); await expect( initializeProject( cwd, { nonInteractive: true, project: "demo" }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (_path, init) => { calls.push(init?.method ?? "GET"); if (!init?.method) { trusted.add(identity.fingerprint); // Another client grants after our GET snapshot. return { fingerprints: [] } as never; } if (init.method === "POST") { expect(trusted.has(identity.fingerprint)).toBe(true); return undefined as never; // Idempotent POST did not create this grant. } throw new Error(`Unexpected ${init.method}`); }, persistTrust: async () => { throw new Error("disk full"); }, }, ), ).rejects.toThrow(/Server registration exists.*kuber trust/); expect(calls).toEqual(["GET", "POST"]); expect(trusted.has(identity.fingerprint)).toBe(true); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true); }); test("partial local write still reports failure without revoking remote trust", async () => { const cwd = await root(); const identity = await resolveTrustIdentity("demo", cwd); const calls: string[] = []; const log = spyOn(console, "log").mockImplementation(() => {}); try { await expect( initializeProject( cwd, { nonInteractive: true, project: "demo" }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (_path, init) => { calls.push(init?.method ?? "GET"); return (init?.method ? undefined : { fingerprints: [] }) as never; }, persistTrust: async (update) => { await updateTrust(update); // Simulates rename succeeding before chmod fails. throw new Error("chmod failed"); }, }, ), ).rejects.toThrow(/Server registration exists.*kuber trust/); expect(calls).toEqual(["GET", "POST"]); expect(await readTrust()).toEqual([identity]); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true); expect(log).not.toHaveBeenCalled(); } finally { log.mockRestore(); } }); test("failed POST rolls back generated files without altering existing trust", async () => { const cwd = await root(); const existing = await resolveTrustIdentity("other", cwd); await updateTrust(() => [existing]); const calls: string[] = []; await expect( initializeProject( cwd, { nonInteractive: true, project: "demo", source: "template", template: "static-nginx", }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (_path, init) => { calls.push(init?.method ?? "GET"); if (init?.method === "POST") throw new KuberApiError("grant failed", 400); return { fingerprints: [] } as never; }, }, ), ).rejects.toThrow("grant failed"); expect(calls).toEqual(["GET", "POST"]); expect(await readTrust()).toEqual([existing]); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(false); expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(false); expect(await Bun.file(join(cwd, ".dockerignore")).exists()).toBe(false); }); test("committed POST with lost response keeps generated files after reconciliation", async () => { const cwd = await root(); const identity = await resolveTrustIdentity("demo", cwd); const trusted = new Set(); const calls: string[] = []; await expect( initializeProject( cwd, { nonInteractive: true, project: "demo", source: "template", template: "static-nginx", }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (_path, init) => { calls.push(init?.method ?? "GET"); if (init?.method === "POST") { trusted.add(identity.fingerprint); throw new Error("socket closed after commit"); } return { fingerprints: [...trusted] } as never; }, }, ), ).rejects.toThrow(/registration succeeded.*files were kept/); expect(calls).toEqual(["GET", "POST", "GET"]); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true); expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(true); expect(await Bun.file(join(cwd, ".dockerignore")).exists()).toBe(true); }); test("unavailable reconciliation keeps generated files and explains repair", async () => { const cwd = await root(); const calls: string[] = []; await expect( initializeProject( cwd, { nonInteractive: true, project: "demo", }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (_path, init) => { calls.push(init?.method ?? "GET"); if (init?.method === "POST") throw new Error("response lost"); if (calls.length === 3) throw new Error("trust service unavailable"); return { fingerprints: [] } as never; }, }, ), ).rejects.toThrow(/Could not confirm.*kuber trust status.*kuber trust/); expect(calls).toEqual(["GET", "POST", "GET"]); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true); }); test("concurrent grant found during reconciliation keeps generated files", async () => { const cwd = await root(); const identity = await resolveTrustIdentity("demo", cwd); const calls: string[] = []; await expect( initializeProject( cwd, { nonInteractive: true, project: "demo", }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async (_path, init) => { calls.push(init?.method ?? "GET"); if (init?.method === "POST") throw new Error("response lost"); return { fingerprints: calls.length === 3 ? [identity.fingerprint] : [], } as never; }, }, ), ).rejects.toThrow(/registration succeeded.*files were kept/); expect(calls).toEqual(["GET", "POST", "GET"]); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true); }); test("existing Compose file is not overwritten and no grant is attempted", async () => { const cwd = await root(); const composePath = join(cwd, "compose.yml"); const existing = "name: existing\nservices: {}\n"; await writeFile(composePath, existing); let requested = false; await expect( initializeProject( cwd, { nonInteractive: true, project: "demo" }, { request: async () => { requested = true; return undefined as never; }, }, ), ).rejects.toThrow("A Compose file already exists"); expect(await readFile(composePath, "utf8")).toBe(existing); expect(requested).toBe(false); }); test("existing Dockerfile is not overwritten by template init", async () => { const cwd = await root(); const dockerfile = join(cwd, "Dockerfile"); await writeFile(dockerfile, "FROM private-image\n"); let requested = false; await expect( initializeProject( cwd, { nonInteractive: true, project: "demo", source: "template", template: "static-nginx", }, { session: async () => ({ token: "test", expiresAt: "2099-01-01", user: { username: "a", roles: [] }, }), request: async () => { requested = true; return undefined as never; }, }, ), ).rejects.toThrow(); expect(await readFile(dockerfile, "utf8")).toBe("FROM private-image\n"); expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(false); expect(requested).toBe(false); }); });