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:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @file Run.defaultCwd.test.tsx
|
||||
* @description The Run page must default its working directory to the user's
|
||||
* HOME directory (a neutral spawn location) rather than the dashboard's own
|
||||
* cwd, which would make ad-hoc runs inherit this repo's project context
|
||||
* (.claude/agents, skills, rules, CLAUDE.md, .mcp.json) — issue #202. Falls
|
||||
* back to the dashboard cwd only when no home suggestion is available.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, act, screen } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import i18n from "i18next";
|
||||
|
||||
const HOME = { kind: "home", path: "/Users/tester", label: "Home" };
|
||||
const DASHBOARD = {
|
||||
kind: "dashboard",
|
||||
path: "/Users/tester/projects/dashboard",
|
||||
label: "Dashboard server",
|
||||
};
|
||||
const RECENT = { kind: "recent", path: "/Users/tester/projects/other", label: "other" };
|
||||
|
||||
// Mutable per-test cwd suggestion payload, read by the api mock below.
|
||||
let cwdItems: Array<{ kind: string; path: string; label: string }> = [];
|
||||
|
||||
vi.mock("../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
const r = (value: unknown) => vi.fn().mockResolvedValue(value);
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
run: {
|
||||
list: r({ runs: [], items: [] }),
|
||||
history: r({ items: [] }),
|
||||
binary: r({ found: true, path: "/usr/bin/claude" }),
|
||||
cwds: vi.fn().mockImplementation(() => Promise.resolve({ items: cwdItems })),
|
||||
files: r({ items: [] }),
|
||||
start: r({ id: "run-1", status: "running" }),
|
||||
get: r({ id: "run-1", status: "running", messages: [], envelopes: [] }),
|
||||
send: r({ messageId: "m-1" }),
|
||||
kill: r({ ok: true }),
|
||||
},
|
||||
ccConfig: {
|
||||
commands: r({ items: [] }),
|
||||
plugins: r({ plugins: [] }),
|
||||
file: r({ content: "" }),
|
||||
},
|
||||
sessions: {
|
||||
list: r({ sessions: [], total: 0, limit: 50, offset: 0 }),
|
||||
transcript: r({ messages: [] }),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../lib/eventBus", () => ({
|
||||
eventBus: {
|
||||
subscribe: () => () => {},
|
||||
publish: () => {},
|
||||
onConnection: () => () => {},
|
||||
connected: true,
|
||||
setConnected: () => {},
|
||||
},
|
||||
}));
|
||||
|
||||
import { Workspace as Run } from "../Workspace";
|
||||
|
||||
class ObserverStub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
globalThis.ResizeObserver =
|
||||
globalThis.ResizeObserver || (ObserverStub as unknown as typeof ResizeObserver);
|
||||
for (const fn of ["scrollIntoView", "scrollBy", "scrollTo"] as const) {
|
||||
if (!(Element.prototype as unknown as Record<string, unknown>)[fn]) {
|
||||
(Element.prototype as unknown as Record<string, unknown>)[fn] = function () {};
|
||||
}
|
||||
}
|
||||
|
||||
async function settle() {
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
}
|
||||
|
||||
async function renderRun() {
|
||||
const utils = render(
|
||||
<MemoryRouter initialEntries={["/run"]}>
|
||||
<Run />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
return utils;
|
||||
}
|
||||
|
||||
function cwdInput(): HTMLInputElement {
|
||||
const placeholder = i18n.t("run:fields.cwdPlaceholder");
|
||||
return screen.getByPlaceholderText(placeholder) as HTMLInputElement;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
i18n.changeLanguage("en");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Run page — default working directory (issue #202)", () => {
|
||||
it("pre-fills cwd with the HOME suggestion, not the dashboard cwd", async () => {
|
||||
cwdItems = [DASHBOARD, HOME, RECENT];
|
||||
await renderRun();
|
||||
expect(cwdInput().value).toBe(HOME.path);
|
||||
});
|
||||
|
||||
it("falls back to the dashboard cwd when no home suggestion exists", async () => {
|
||||
cwdItems = [DASHBOARD, RECENT];
|
||||
await renderRun();
|
||||
expect(cwdInput().value).toBe(DASHBOARD.path);
|
||||
});
|
||||
|
||||
it("leaves cwd empty when there are no suggestions at all", async () => {
|
||||
cwdItems = [];
|
||||
await renderRun();
|
||||
expect(cwdInput().value).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user