/** * @file With NO pipeline template installed (the shipped state since the * built-in templates were removed), lanes must still be created, rendered and * reported on: getPipeline() answers an empty pipeline, `default` stays a * valid id, and a stage declaration is refused with a plain explanation. * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const fs = require("node:fs"); const path = require("node:path"); const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "pipelines-dormant-")); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db"); process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "data"); process.env.CLAUDE_HOME = path.join(ROOT, ".claude"); process.env.AGENTHUB_SHELL_PATH = "0"; const savedDir = process.env.DASHBOARD_PIPELINES_DIR; delete process.env.DASHBOARD_PIPELINES_DIR; const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const http = require("node:http"); const pipelines = require("../lib/pipelines"); pipelines.reload(); const lanes = require("../lib/lanes"); const { createApp } = require("../index"); let app; let base; const request = (method, p, body) => fetch(`${base}${p}`, { method, headers: { "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }).then(async (r) => ({ status: r.status, body: await r.json().catch(() => null) })); before(async () => { app = http.createServer(createApp()); await new Promise((r) => app.listen(0, "127.0.0.1", r)); base = `http://127.0.0.1:${app.address().port}`; }); after(async () => { await new Promise((r) => app.close(r)); if (savedDir !== undefined) process.env.DASHBOARD_PIPELINES_DIR = savedDir; pipelines.reload(); fs.rmSync(ROOT, { recursive: true, force: true }); }); describe("no pipeline template installed", () => { it("ships no template and answers an empty default pipeline", () => { assert.deepEqual(pipelines.listPipelines(), []); const p = pipelines.getPipeline("default"); assert.equal(p.id, "default"); assert.deepEqual(p.nodes, []); assert.equal(pipelines.getPipeline("anything").nodes.length, 0); assert.deepEqual(pipelines.nodeStates(p, { stage: "plan", stages: {} }), []); assert.equal(pipelines.progressPct(p, { stage: "plan" }), 0); }); it("still creates a lane on the default pipeline and renders it", () => { const lane = lanes.createLane({ cwd: path.join(ROOT, "a"), title: "a" }); assert.equal(lane.pipeline, "default"); const payload = lanes.lanePayload(lane); assert.deepEqual(payload.pipeline_nodes, []); assert.equal(payload.progress, 0); }); it("refuses a stage declaration with a plain reason", async () => { const lane = lanes.createLane({ cwd: path.join(ROOT, "b"), title: "b" }); const r = await request("POST", `/api/lanes/${lane.id}/stage`, { stage: "plan" }); assert.equal(r.status, 400); assert.equal(r.body.error.code, "ENOPIPELINE"); assert.match(r.body.error.message, /no pipeline template is installed/); }); it("lists no pipelines over the API and still serves the lane list", async () => { assert.deepEqual((await request("GET", "/api/lanes/pipelines")).body, { pipelines: [] }); assert.equal((await request("GET", "/api/lanes")).status, 200); }); });