feat: Claude Code Monitor — lanes, pipelines and a merged workspace

Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit d2fc4a4701
783 changed files with 221217 additions and 0 deletions
@@ -0,0 +1,172 @@
/**
* @file AddLaneModal.test.tsx
* @description Pins the "+ Add lane" flow after it was rebuilt around a source
* repo instead of an existing folder: picking or typing a repo path triggers a
* branch lookup, the base-branch picker only appears once that lookup resolves,
* confirm submits through the provisioning endpoint (not the adopt/ensure one),
* an unresolvable path degrades to a quiet hint instead of blocking the form,
* and a server error surfaces instead of closing the modal.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AddLaneModal } from "../AddLaneModal";
import { api } from "../../../lib/api";
import type { CwdSuggestion } from "../../../lib/api";
import type { Lane } from "../../../lib/types";
vi.mock("../../../lib/api", () => ({
api: { lanes: { branches: vi.fn(), worktree: vi.fn() } },
}));
function laneFixture(over: Partial<Lane> = {}): Lane {
return {
id: 9,
title: "",
cwd: "/lanes/repo__feature",
branch: "feat/feature",
kind: "managed",
pipeline: "default",
session_id: null,
run_id: null,
stage: "idle",
stage_since: null,
status: "provisioning",
gate_decision: null,
ci_status: null,
needs_action: null,
links: {},
stages: {},
notes: null,
pipeline_name: "Default",
pipeline_nodes: [],
progress: 0,
stage_seconds: null,
last_event_seconds: null,
liveness: "idle",
detected_stage: null,
detected_signal: null,
...over,
};
}
const SUGGESTIONS: CwdSuggestion[] = [
{ kind: "home", path: "/Users/tester", label: "Home" },
{ kind: "recent", path: "/Users/tester/projects/repo", label: "repo" },
];
function renderModal(over: Partial<React.ComponentProps<typeof AddLaneModal>> = {}) {
return render(
<AddLaneModal
open
onClose={vi.fn()}
onAdded={vi.fn()}
cwdSuggestions={SUGGESTIONS}
{...over}
/>
);
}
/** ConfirmModal focuses its Cancel button on a 0ms timer after mount, which
* races userEvent.type() and can eat the first keystroke. Let that timer fire,
* then click the field to reclaim focus before typing. */
async function focusField(user: ReturnType<typeof userEvent.setup>, el: HTMLElement) {
await new Promise((r) => setTimeout(r, 0));
await user.click(el);
}
beforeEach(() => {
vi.mocked(api.lanes.branches).mockReset();
vi.mocked(api.lanes.worktree).mockReset();
});
describe("AddLaneModal", () => {
it("disables confirm until a repo, a title, and a resolved branch list are all present", () => {
renderModal();
expect(screen.getByRole("button", { name: "Add lane" })).toBeDisabled();
});
it("looks up branches once the repo path settles, and shows them as a picker", async () => {
vi.mocked(api.lanes.branches).mockResolvedValue({
branches: ["main", "feat/other"],
current: "main",
});
renderModal();
const user = userEvent.setup();
const repoField = screen.getByLabelText("Source repository");
await focusField(user, repoField);
await user.type(repoField, "/Users/tester/projects/repo");
await waitFor(() => expect(api.lanes.branches).toHaveBeenCalledWith("/Users/tester/projects/repo"));
const base = await screen.findByLabelText("Branch to fork from");
expect(base).toHaveValue("main"); // the repo's current branch is preselected
expect(screen.getByRole("option", { name: "feat/other" })).toBeInTheDocument();
});
it("stays disabled and shows a quiet hint when the path is not a resolvable repo", async () => {
vi.mocked(api.lanes.branches).mockRejectedValue(new Error("EBADSOURCEREPO"));
renderModal();
const user = userEvent.setup();
const repoField = screen.getByLabelText("Source repository");
await focusField(user, repoField);
await user.type(repoField, "/not/a/repo");
await waitFor(() => expect(api.lanes.branches).toHaveBeenCalled());
expect(await screen.findByText(/Not a git repository/)).toBeInTheDocument();
expect(screen.queryByLabelText("Branch to fork from")).toBeNull();
expect(screen.getByRole("button", { name: "Add lane" })).toBeDisabled();
});
it("submits through the worktree provisioning endpoint, not ensure", async () => {
vi.mocked(api.lanes.branches).mockResolvedValue({ branches: ["main"], current: "main" });
vi.mocked(api.lanes.worktree).mockResolvedValue({ lane: laneFixture({ id: 9 }) });
const onAdded = vi.fn();
const onClose = vi.fn();
renderModal({ onClose, onAdded });
const user = userEvent.setup();
const repoField = screen.getByLabelText("Source repository");
await focusField(user, repoField);
await user.type(repoField, "/Users/tester/projects/repo");
await screen.findByLabelText("Branch to fork from");
await user.type(screen.getByLabelText("Title"), "New feature");
await user.click(screen.getByRole("button", { name: "Add lane" }));
await waitFor(() => {
expect(api.lanes.worktree).toHaveBeenCalledWith({
sourceRepo: "/Users/tester/projects/repo",
title: "New feature",
base: "main",
});
});
expect(onAdded).toHaveBeenCalledWith(expect.objectContaining({ id: 9, status: "provisioning" }));
expect(onClose).toHaveBeenCalled();
});
it("shows a server error and leaves the modal open instead of closing silently", async () => {
vi.mocked(api.lanes.branches).mockResolvedValue({ branches: ["main"], current: "main" });
vi.mocked(api.lanes.worktree).mockRejectedValue(new Error("EWORKTREEDIRCOLLISION"));
const onClose = vi.fn();
renderModal({ onClose });
const user = userEvent.setup();
const repoField = screen.getByLabelText("Source repository");
await focusField(user, repoField);
await user.type(repoField, "/Users/tester/projects/repo");
await screen.findByLabelText("Branch to fork from");
await user.type(screen.getByLabelText("Title"), "New feature");
await user.click(screen.getByRole("button", { name: "Add lane" }));
expect(await screen.findByText("EWORKTREEDIRCOLLISION")).toBeInTheDocument();
expect(onClose).not.toHaveBeenCalled();
});
it("renders nothing when closed", () => {
renderModal({ open: false });
expect(screen.queryByRole("dialog")).toBeNull();
});
});
@@ -0,0 +1,457 @@
/**
* @file Tests for DestructiveLaneModal: the preflight-gated confirmation for
* reset/remove/purge. Covers that the displayed counts are exactly the
* preflight facts, that `reset` is refused for adopted/missing/unreadable
* lanes while `remove` stays available for all of them (the server permits it,
* so the UI must not be stricter), that the Force checkbox appears exactly when
* the server would demand force, and that confirming echoes back exactly the
* `expect` block the modal displayed.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
const { preflightMock } = vi.hoisted(() => ({ preflightMock: vi.fn() }));
vi.mock("../../../lib/api", () => ({
api: { lanes: { preflight: preflightMock } },
}));
import { DestructiveLaneModal } from "../DestructiveLaneModal";
import type { Lane, LanePurgePreflight, LaneWorktreePreflight } from "../../../lib/types";
function makeLane(overrides: Partial<Lane> = {}): Lane {
return {
id: 1,
title: "demo",
cwd: "/work/demo",
branch: "lane/demo",
kind: "managed",
pipeline: "default",
session_id: null,
run_id: null,
stage: "plan",
stage_since: null,
status: "idle",
gate_decision: null,
ci_status: null,
needs_action: null,
links: {},
stages: {},
notes: null,
pipeline_name: "Default",
pipeline_nodes: [],
progress: 0,
stage_seconds: null,
last_event_seconds: null,
liveness: "idle",
detected_stage: null,
detected_signal: null,
...overrides,
};
}
function worktreePreflight(overrides: Partial<LaneWorktreePreflight> = {}): LaneWorktreePreflight {
return {
action: "reset",
lane: 1,
kind: "managed",
branch: "lane/demo",
head: "abc1234",
dirty: 2,
untracked: 3,
unpushed: 0,
blocked: [],
warnings: [],
...overrides,
};
}
beforeEach(() => {
preflightMock.mockReset();
});
describe("DestructiveLaneModal", () => {
it("renders exactly the counts the preflight returned", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({ head: "deadbee", dirty: 4, untracked: 5, unpushed: 0 })
);
render(
<DestructiveLaneModal
lane={makeLane()}
action="reset"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
expect(await screen.findByText("deadbee")).toBeInTheDocument();
expect(screen.getByText("4")).toBeInTheDocument();
expect(screen.getByText("5")).toBeInTheDocument();
});
it("disables RESET for an adopted lane (a worktree action can never touch it)", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({ action: "reset", kind: "adopted", blocked: ["adopted"] })
);
render(
<DestructiveLaneModal
lane={makeLane({ kind: "adopted" })}
action="reset"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
await waitFor(() => expect(confirmButton).toBeDisabled());
});
it("ENABLES remove for an adopted lane and sends the payload — the server forgets the row and leaves the directory alone", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({
action: "remove",
kind: "adopted",
head: "adopt01",
dirty: 0,
untracked: 0,
unpushed: 0,
blocked: ["adopted"],
})
);
const onConfirm = vi.fn();
render(
<DestructiveLaneModal
lane={makeLane({ kind: "adopted" })}
action="remove"
open
onClose={vi.fn()}
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
// "adopted" is shown as context, not as a refusal.
expect(screen.getByText(/Only the dashboard's record of it is dropped/)).toBeInTheDocument();
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalledWith({
expect: { head: "adopt01", dirty: 0, untracked: 0, unpushed: 0 },
});
});
it("forgets an adopted lane with unpushed commits without offering Force — the server does not require it", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({
action: "remove",
kind: "adopted",
head: "adopt02",
dirty: 0,
untracked: 0,
unpushed: 7,
blocked: ["adopted", "unpushed-commits"],
})
);
const onConfirm = vi.fn();
render(
<DestructiveLaneModal
lane={makeLane({ kind: "adopted" })}
action="remove"
open
onClose={vi.fn()}
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalledWith({
expect: { head: "adopt02", dirty: 0, untracked: 0, unpushed: 7 },
});
});
it("disables RESET when the worktree directory is missing", async () => {
preflightMock.mockResolvedValue(worktreePreflight({ action: "reset", blocked: ["missing"] }));
render(
<DestructiveLaneModal
lane={makeLane()}
action="reset"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
await waitFor(() => expect(confirmButton).toBeDisabled());
});
it("ENABLES remove when the worktree directory is missing — the server takes the prune path", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({
action: "remove",
head: null,
dirty: 0,
untracked: 0,
unpushed: 0,
blocked: ["missing"],
})
);
const onConfirm = vi.fn();
render(
<DestructiveLaneModal
lane={makeLane()}
action="remove"
open
onClose={vi.fn()}
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
expect(screen.getByText(/The lane directory is already gone/)).toBeInTheDocument();
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalledWith({
expect: { head: null, dirty: 0, untracked: 0, unpushed: 0 },
});
});
it("disables RESET when the worktree directory is unreadable", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({ action: "reset", blocked: ["unreadable"] })
);
render(
<DestructiveLaneModal
lane={makeLane()}
action="reset"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
await waitFor(() => expect(confirmButton).toBeDisabled());
});
it("ENABLES remove when the worktree directory is unreadable — the server attempts removal and falls back to deregistering it", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({
action: "remove",
head: null,
dirty: 0,
untracked: 0,
unpushed: 0,
blocked: ["unreadable"],
})
);
const onConfirm = vi.fn();
render(
<DestructiveLaneModal
lane={makeLane()}
action="remove"
open
onClose={vi.fn()}
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
expect(screen.getByText(/cannot be read as a Git worktree/)).toBeInTheDocument();
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalledWith({
expect: { head: null, dirty: 0, untracked: 0, unpushed: 0 },
});
});
it("shows the Force checkbox only when the sole blocker is unpushed commits, and ticking it enables confirm", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({ action: "remove", unpushed: 3, blocked: ["unpushed-commits"] })
);
render(
<DestructiveLaneModal
lane={makeLane()}
action="remove"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
await waitFor(() => expect(confirmButton).toBeDisabled());
fireEvent.click(screen.getByRole("checkbox"));
expect(confirmButton).not.toBeDisabled();
});
it("a local-only managed lane (no-remote warning, unpushed-commits blocker) is resettable with Force", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({
action: "reset",
unpushed: 3,
blocked: ["unpushed-commits"],
warnings: ["no-remote"],
})
);
render(
<DestructiveLaneModal
lane={makeLane()}
action="reset"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
// The warning is shown as context, not as an obstacle.
expect(await screen.findByText(/No Git remote is configured/)).toBeInTheDocument();
const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
await waitFor(() => expect(confirmButton).toBeDisabled());
fireEvent.click(screen.getByRole("checkbox"));
expect(confirmButton).not.toBeDisabled();
});
it("does not offer the Force checkbox when a hard blocker makes confirming impossible anyway", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({ action: "reset", unpushed: 3, blocked: ["unpushed-commits", "missing"] })
);
render(
<DestructiveLaneModal
lane={makeLane()}
action="reset"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
await waitFor(() => expect(confirmButton).toBeDisabled());
expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
});
it("confirming a worktree action passes back exactly the expect block that was displayed", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({ head: "cafefeed", dirty: 1, untracked: 2, unpushed: 0, blocked: [] })
);
const onConfirm = vi.fn();
render(
<DestructiveLaneModal
lane={makeLane()}
action="reset"
open
onClose={vi.fn()}
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalledWith({
expect: { head: "cafefeed", dirty: 1, untracked: 2, unpushed: 0 },
});
});
it("confirming an unpushed-commits removal with Force ticked sends force:true plus the same expect block", async () => {
preflightMock.mockResolvedValue(
worktreePreflight({
action: "remove",
head: "abc0000",
dirty: 0,
untracked: 0,
unpushed: 2,
blocked: ["unpushed-commits"],
})
);
const onConfirm = vi.fn();
render(
<DestructiveLaneModal
lane={makeLane()}
action="remove"
open
onClose={vi.fn()}
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
fireEvent.click(screen.getByRole("checkbox"));
await waitFor(() => expect(confirmButton).not.toBeDisabled());
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalledWith({
expect: { head: "abc0000", dirty: 0, untracked: 0, unpushed: 2 },
force: true,
});
});
it("confirming a purge passes back the purge-specific expect block", async () => {
const purgePreflight: LanePurgePreflight = {
action: "purge",
lane: 1,
sessions: 4,
events: 120,
tokenRows: 30,
bytesEstimate: 4096,
activeSessionSkipped: false,
};
preflightMock.mockResolvedValue(purgePreflight);
const onConfirm = vi.fn();
render(
<DestructiveLaneModal
lane={makeLane()}
action="purge"
open
onClose={vi.fn()}
onConfirm={onConfirm}
/>
);
const confirmButton = await screen.findByRole("button", { name: "Purge history" });
await waitFor(() => expect(confirmButton).not.toBeDisabled());
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalledWith({
expect: { sessions: 4, events: 120, tokenRows: 30 },
});
});
it("shows the purge size estimate and says when a live session was spared", async () => {
const purgePreflight: LanePurgePreflight = {
action: "purge",
lane: 1,
sessions: 2,
events: 8,
tokenRows: 2,
bytesEstimate: 5120,
activeSessionSkipped: true,
};
preflightMock.mockResolvedValue(purgePreflight);
render(
<DestructiveLaneModal
lane={makeLane()}
action="purge"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
expect(await screen.findByText("5.0 KB")).toBeInTheDocument();
expect(screen.getByText(/still active and will be kept/)).toBeInTheDocument();
});
it("does not claim a session was spared when none was", async () => {
const purgePreflight: LanePurgePreflight = {
action: "purge",
lane: 1,
sessions: 1,
events: 1,
tokenRows: 0,
bytesEstimate: 512,
activeSessionSkipped: false,
};
preflightMock.mockResolvedValue(purgePreflight);
render(
<DestructiveLaneModal
lane={makeLane()}
action="purge"
open
onClose={vi.fn()}
onConfirm={vi.fn()}
/>
);
expect(await screen.findByText("512 B")).toBeInTheDocument();
expect(screen.queryByText(/still active and will be kept/)).not.toBeInTheDocument();
});
});
@@ -0,0 +1,262 @@
/**
* @file Regression test for the lane card's status badge. Every lane status
* the server can set must have a real translated word behind
* `t("status." + lane.status)`; before this test existed, no locale defined
* any `status.*` key, and i18next's default missing-key behavior (return the
* key itself) hid that from the `||` fallback, so the badge showed literal
* text like `status.active`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import LaneCard from "../LaneCard";
import type { Lane } from "../../../lib/types";
import { api } from "../../../lib/api";
vi.mock("../../../lib/api", () => ({
api: {
lanes: { git: vi.fn(), preflight: vi.fn().mockResolvedValue({ blocked: [], warnings: [] }) },
},
}));
beforeEach(() => {
vi.mocked(api.lanes.git).mockReset();
vi.mocked(api.lanes.git).mockResolvedValue({ available: false });
});
function makeLane(overrides: Partial<Lane> = {}): Lane {
return {
id: 1,
title: "demo",
cwd: "/work/demo",
branch: "lane/demo",
kind: "adopted",
pipeline: "default",
session_id: null,
run_id: null,
stage: "plan",
stage_since: null,
status: "idle",
gate_decision: null,
ci_status: null,
needs_action: null,
links: {},
stages: {},
notes: null,
pipeline_name: "Default",
pipeline_nodes: [],
progress: 0,
stage_seconds: null,
last_event_seconds: null,
liveness: "idle",
detected_stage: null,
detected_signal: null,
...overrides,
};
}
describe("LaneCard status badge", () => {
for (const status of ["idle", "running", "provisioning", "failed"] as const) {
it(`renders a real word for status "${status}", not the raw key`, () => {
render(<LaneCard lane={makeLane({ status })} onAction={vi.fn()} />);
expect(screen.queryByText(`status.${status}`)).not.toBeInTheDocument();
expect(screen.queryByText(status.toUpperCase())).not.toBeInTheDocument();
});
}
});
const pipelineNodes: Lane["pipeline_nodes"] = [
{ id: "intake", label: "intake", icon: "📥", gate: false, state: "done" },
{ id: "plan", label: "plan", icon: "🧭", gate: false, state: "done" },
{ id: "implement", label: "implement", icon: "🛠", gate: false, state: "current" },
{ id: "tests", label: "tests", icon: "🧪", gate: false, state: "pending" },
];
describe("LaneCard auto: chip", () => {
it("shows the auto chip when the detected stage is ahead of the declared stage", () => {
render(
<LaneCard
lane={makeLane({ stage: "plan", pipeline_nodes: pipelineNodes, detected_stage: "tests" })}
onAction={vi.fn()}
/>
);
expect(screen.getByText("auto: tests")).toBeInTheDocument();
});
it("hides the auto chip when the detected stage matches the declared stage", () => {
render(
<LaneCard
lane={makeLane({ stage: "plan", pipeline_nodes: pipelineNodes, detected_stage: "plan" })}
onAction={vi.fn()}
/>
);
expect(screen.queryByText("auto: plan")).not.toBeInTheDocument();
});
it("hides the auto chip when the detected stage trails the declared stage", () => {
render(
<LaneCard
lane={makeLane({
stage: "implement",
pipeline_nodes: pipelineNodes,
detected_stage: "intake",
})}
onAction={vi.fn()}
/>
);
expect(screen.queryByText("auto: intake")).not.toBeInTheDocument();
});
it("hides the auto chip when nothing is detected", () => {
render(
<LaneCard
lane={makeLane({ stage: "plan", pipeline_nodes: pipelineNodes, detected_stage: null })}
onAction={vi.fn()}
/>
);
expect(screen.queryByText(/^auto:/)).not.toBeInTheDocument();
});
});
describe("LaneCard rebuilt layout", () => {
const full = (over: Partial<Lane> = {}) =>
makeLane({
id: 7,
title: "Rename Metric to Rule",
kind: "managed",
status: "running",
liveness: "active",
stage: "plan",
stage_seconds: 152,
progress: 48,
ci_status: "green",
pipeline_nodes: pipelineNodes,
...over,
});
it("labels the card with the lane id", () => {
render(<LaneCard lane={full()} onAction={vi.fn()} />);
expect(screen.getByTestId("lane-card-7")).toBeInTheDocument();
});
it("shows the declared stage, the progress percentage and the time on stage", () => {
render(<LaneCard lane={full()} onAction={vi.fn()} />);
expect(screen.getByTestId("lane-stage").textContent).toBe("plan");
expect(screen.getByText("48%")).toBeInTheDocument();
expect(screen.getByText("2m 32s")).toBeInTheDocument();
});
it("gives the progress bar a width matching the lane's progress", () => {
render(<LaneCard lane={full()} onAction={vi.fn()} />);
expect(screen.getByTestId("lane-progress-fill").getAttribute("style")).toContain("48%");
});
it("surfaces a needs-you message", () => {
render(<LaneCard lane={full({ needs_action: "waiting on approval" })} onAction={vi.fn()} />);
expect(screen.getByText(/waiting on approval/)).toBeInTheDocument();
});
it("fires a plain action with its own name", async () => {
const onAction = vi.fn();
render(<LaneCard lane={full()} onAction={onAction} />);
await userEvent.setup().click(screen.getByTestId("lane-action-stop"));
expect(onAction).toHaveBeenCalledWith("stop");
});
it("keeps the destructive verbs out of the card until the menu is opened", async () => {
render(<LaneCard lane={full()} onAction={vi.fn()} />);
// A wall of red buttons makes none of them read as the dangerous one, so
// reset/remove/purge live behind the ⋯ menu.
expect(screen.queryByTestId("lane-action-reset")).toBeNull();
expect(screen.queryByTestId("lane-action-remove")).toBeNull();
await userEvent.setup().click(screen.getByTestId("lane-more"));
expect(screen.getByTestId("lane-action-reset")).toBeInTheDocument();
expect(screen.getByTestId("lane-action-remove")).toBeInTheDocument();
});
it("routes reset through the confirmation modal rather than firing it", async () => {
const onAction = vi.fn();
render(<LaneCard lane={full()} onAction={onAction} />);
const user = userEvent.setup();
await user.click(screen.getByTestId("lane-more"));
await user.click(screen.getByTestId("lane-action-reset"));
expect(onAction).not.toHaveBeenCalled();
});
it("offers reset only for a managed lane", async () => {
const user = userEvent.setup();
const { unmount } = render(<LaneCard lane={full()} onAction={vi.fn()} />);
await user.click(screen.getByTestId("lane-more"));
expect(screen.getByTestId("lane-action-reset")).toBeInTheDocument();
unmount();
render(<LaneCard lane={full({ kind: "adopted" })} onAction={vi.fn()} />);
await user.click(screen.getByTestId("lane-more"));
expect(screen.queryByTestId("lane-action-reset")).toBeNull();
});
});
describe("LaneCard git block", () => {
const facts = {
available: true as const,
branch: "feat/rename-metric",
head: "9b3e74a",
subject: "free-text rule mode in the form",
dirty: 2,
untracked: 1,
};
it("renders the live branch, head, subject and uncommitted counts", async () => {
vi.mocked(api.lanes.git).mockResolvedValueOnce(facts);
render(<LaneCard lane={makeLane({ branch: "stale/recorded" })} onAction={vi.fn()} />);
const block = await screen.findByTestId("lane-git");
expect(block.textContent).toContain("feat/rename-metric");
expect(block.textContent).toContain("9b3e74a");
expect(block.textContent).toContain("free-text rule mode in the form");
expect(block.textContent).toContain("2");
// The live branch replaces the lane's recorded one rather than doubling it.
expect(screen.queryByText(/stale\/recorded/)).toBeNull();
});
it("omits the uncommitted line when the tree is clean", async () => {
vi.mocked(api.lanes.git).mockResolvedValueOnce({ ...facts, dirty: 0, untracked: 0 });
render(<LaneCard lane={makeLane()} onAction={vi.fn()} />);
const block = await screen.findByTestId("lane-git");
expect(block.textContent).not.toContain("modified");
});
it("renders the card with no git block and no error when git is unavailable", async () => {
vi.mocked(api.lanes.git).mockResolvedValueOnce({ available: false });
render(<LaneCard lane={makeLane({ title: "plain dir lane" })} onAction={vi.fn()} />);
expect(await screen.findByText("plain dir lane")).toBeInTheDocument();
expect(screen.queryByTestId("lane-git")).toBeNull();
});
it("swallows a rejected request instead of surfacing an error", async () => {
vi.mocked(api.lanes.git).mockRejectedValueOnce(new Error("network down"));
render(<LaneCard lane={makeLane({ title: "offline lane" })} onAction={vi.fn()} />);
expect(await screen.findByText("offline lane")).toBeInTheDocument();
expect(screen.queryByTestId("lane-git")).toBeNull();
expect(screen.queryByText(/network down/)).toBeNull();
});
it("stops polling once the card unmounts", async () => {
vi.useFakeTimers();
try {
vi.mocked(api.lanes.git).mockResolvedValue({ available: false });
const { unmount } = render(<LaneCard lane={makeLane()} onAction={vi.fn()} />);
expect(api.lanes.git).toHaveBeenCalledTimes(1);
unmount();
vi.advanceTimersByTime(120_000);
expect(api.lanes.git).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
}
});
});
@@ -0,0 +1,106 @@
/**
* @file Rendering tests for the lane pipeline map: every node renders with a
* state-specific class so "done", "current" and "passed without evidence" stay
* visually distinguishable, and the amber state is never conflated with done.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import PipelineMap from "../PipelineMap";
import type { LaneNode } from "../../../lib/types";
const nodes: LaneNode[] = [
{ id: "plan", label: "plan", icon: "🧭", gate: false, state: "done" },
{ id: "implement", label: "implement", icon: "🛠", gate: false, state: "passed-no-evidence" },
{ id: "review", label: "review", icon: "👀", gate: true, state: "current" },
{ id: "gate", label: "gate", icon: "🚦", gate: true, state: "failed" },
{ id: "done", label: "done", icon: "✅", gate: false, state: "pending" },
];
describe("PipelineMap", () => {
it("renders one element per node, labelled by state", () => {
render(<PipelineMap nodes={nodes} />);
expect(screen.getAllByTestId(/^pipeline-node-/)).toHaveLength(5);
expect(screen.getByTestId("pipeline-node-plan")).toHaveAttribute("data-state", "done");
expect(screen.getByTestId("pipeline-node-implement")).toHaveAttribute(
"data-state",
"passed-no-evidence"
);
expect(screen.getByTestId("pipeline-node-review")).toHaveAttribute("data-state", "current");
expect(screen.getByTestId("pipeline-node-gate")).toHaveAttribute("data-state", "failed");
expect(screen.getByTestId("pipeline-node-done")).toHaveAttribute("data-state", "pending");
});
it("gives amber nodes a different class from done nodes", () => {
render(<PipelineMap nodes={nodes} />);
const done = screen.getByTestId("pipeline-node-plan").className;
const amber = screen.getByTestId("pipeline-node-implement").className;
// The done node must contain emerald colour token and the amber node must contain amber token.
expect(done).toContain("emerald");
expect(amber).toContain("amber");
expect(done).not.toEqual(amber);
});
it("renders nothing but an empty hint when there are no nodes", () => {
render(<PipelineMap nodes={[]} />);
expect(screen.queryAllByTestId(/^pipeline-node-/)).toHaveLength(0);
});
describe("detected (inferred) nodes", () => {
const detectedNodes: LaneNode[] = [
{ id: "intake", label: "intake", icon: "📥", gate: false, state: "pending", detected: true },
{ id: "tests", label: "tests", icon: "🧪", gate: false, state: "pending", detected: true },
{ id: "plan", label: "plan", icon: "🧭", gate: false, state: "done" },
{
id: "implement",
label: "implement",
icon: "🛠",
gate: false,
state: "passed-no-evidence",
},
];
it("marks a detected node with data-detected and a dashed-border class token", () => {
render(<PipelineMap nodes={detectedNodes} detectedSignal="npm run test:server" />);
const node = screen.getByTestId("pipeline-node-tests");
expect(node).toHaveAttribute("data-detected", "true");
expect(node.className).toContain("border-dashed");
});
it("gives a detected node a class different from both done and plain passed-no-evidence", () => {
render(<PipelineMap nodes={detectedNodes} detectedSignal="npm run test:server" />);
const detected = screen.getByTestId("pipeline-node-tests").className;
const done = screen.getByTestId("pipeline-node-plan").className;
const amber = screen.getByTestId("pipeline-node-implement").className;
expect(detected).not.toEqual(done);
expect(detected).not.toEqual(amber);
});
it("names the signal in the detected node's tooltip", () => {
render(<PipelineMap nodes={detectedNodes} detectedSignal="npm run test:server" />);
const node = screen.getByTestId("pipeline-node-tests");
expect(node).toHaveAttribute("title", "tests ← npm run test:server");
});
it("PREMISE GUARD: detected wins over state=done — dashed amber, never emerald", () => {
// The server never emits this pair, and this is the guard that says the
// component would not paint an inference green even if it did. Asserting
// against a fixture whose detected nodes are already `pending` would only
// re-assert the fixture.
const impossible: LaneNode[] = [
{ id: "tests", label: "tests", icon: "🧪", gate: false, state: "done", detected: true },
];
render(<PipelineMap nodes={impossible} detectedSignal="npm run test:server" />);
const node = screen.getByTestId("pipeline-node-tests");
expect(node.className).toContain("border-dashed");
expect(node.className).toContain("amber");
expect(node.className).not.toContain("emerald");
});
it("non-detected nodes carry no data-detected attribute", () => {
render(<PipelineMap nodes={detectedNodes} detectedSignal="npm run test:server" />);
expect(screen.getByTestId("pipeline-node-plan")).not.toHaveAttribute("data-detected");
});
});
});