/** * @file Tests for server/lib/lane-features.js: slug canonicalization, * activate/archive semantics, and read access to a lane's feature history. * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const path = require("node:path"); const fs = require("node:fs"); const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-features-")); process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db"); process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes"); const { describe, it, after } = require("node:test"); const assert = require("node:assert/strict"); const lanesLib = require("../lib/lanes"); const features = require("../lib/lane-features"); after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true })); let laneSeq = 0; function makeLane() { laneSeq += 1; const cwd = path.join(SUITE_ROOT, `lane-cwd-${laneSeq}`); fs.mkdirSync(cwd, { recursive: true }); return lanesLib.createLane({ title: `lane ${laneSeq}`, cwd, kind: "managed" }); } describe("canonicalizeSlug", () => { it("drops a leading feat/ prefix", () => { assert.equal(features.canonicalizeSlug("feat/my-thing"), "my-thing"); }); it("turns slashes and spaces into a single flat dash-separated segment", () => { assert.equal(features.canonicalizeSlug("feat/some thing/ nested"), "some-thing-nested"); }); it("keeps dots, underscores, and case as-is (unlike worktree.js:slugify)", () => { assert.equal(features.canonicalizeSlug("My_Feature.v2"), "My_Feature.v2"); }); it("collapses repeated separators and trims leading/trailing dashes", () => { assert.equal(features.canonicalizeSlug("feat//too many///slashes/"), "too-many-slashes"); }); it("refuses an empty result", () => { assert.throws( () => features.canonicalizeSlug("feat/"), (err) => err.code === "EBADSLUG" ); assert.throws( () => features.canonicalizeSlug(" "), (err) => err.code === "EBADSLUG" ); }); }); describe("activateFeature / archiveActiveFeature", () => { it("activating a brand-new slug creates a live (unarchived) feature row and points the lane at it", () => { const lane = makeLane(); const { lane: updated, feature } = features.activateFeature(lane.id, "feat/one"); assert.equal(feature.slug, "one"); assert.equal(feature.archived_at, null); assert.equal(updated.active_feature_id, feature.id); }); it("activating a second slug archives the first with its final stage intact", () => { const lane = makeLane(); features.activateFeature(lane.id, "one"); lanesLib.setStage(lane.id, { stage: "review", evidence: "looks good" }); const { feature: second } = features.activateFeature(lane.id, "two"); assert.equal(second.slug, "two"); assert.equal(second.archived_at, null); const first = features.getFeature(lane.id, "one"); assert.notEqual(first.archived_at, null); assert.equal(first.stage, "review"); assert.deepEqual(first.stages.review.evidence, "looks good"); }); it("re-activating an archived slug restores its saved stage onto the live lane row", () => { const lane = makeLane(); features.activateFeature(lane.id, "one"); lanesLib.setStage(lane.id, { stage: "implement" }); features.activateFeature(lane.id, "two"); // archives "one" at stage=implement const { lane: reactivated } = features.activateFeature(lane.id, "one"); assert.equal(reactivated.stage, "implement"); assert.equal(features.getFeature(lane.id, "one").archived_at, null); assert.notEqual(features.getFeature(lane.id, "two").archived_at, null); }); it("re-activating the CURRENTLY active slug is a no-op, not a self-archive", () => { const lane = makeLane(); features.activateFeature(lane.id, "one"); lanesLib.setStage(lane.id, { stage: "review" }); const { lane: updated } = features.activateFeature(lane.id, "one"); assert.equal(updated.stage, "review"); assert.equal(features.getFeature(lane.id, "one").archived_at, null); }); it("archiveActiveFeature returns null and touches nothing when no feature is active", () => { const lane = makeLane(); assert.equal(features.archiveActiveFeature(lane.id), null); }); it("echoes back the canonicalized slug, not the caller's raw input", () => { const lane = makeLane(); const { feature } = features.activateFeature(lane.id, "feat/Weird Input/"); assert.equal(feature.slug, "Weird-Input"); }); it("a feature's own title survives being archived (never clobbered by the lane's title)", () => { const lane = makeLane(); features.activateFeature(lane.id, "one", { title: "Custom title" }); features.activateFeature(lane.id, "two"); // archives "one" assert.equal(features.getFeature(lane.id, "one").title, "Custom title"); }); it("links survive a full activate -> archive -> reactivate round trip", () => { const lane = makeLane(); features.activateFeature(lane.id, "one"); lanesLib.updateLane(lane.id, { links: { pr: "https://example.com/pr/1" } }); features.activateFeature(lane.id, "two"); // archives "one" with its links const archived = features.getFeature(lane.id, "one"); assert.deepEqual(archived.links, { pr: "https://example.com/pr/1" }); const { lane: reactivated } = features.activateFeature(lane.id, "one"); assert.deepEqual(reactivated.links, { pr: "https://example.com/pr/1" }); }); }); describe("listFeatures / getFeature", () => { it("lists every feature for a lane, most recently touched first", () => { const lane = makeLane(); features.activateFeature(lane.id, "one"); features.activateFeature(lane.id, "two"); const list = features.listFeatures(lane.id); assert.deepEqual( list.map((f) => f.slug), ["two", "one"] ); }); it("getFeature returns null for an unknown slug", () => { const lane = makeLane(); assert.equal(features.getFeature(lane.id, "never-activated"), null); }); });