Files
fusion/packages/dashboard/app/components/command-center/__tests__/SystemControlsArea.test.tsx
gsxdsm 743df98aa4 capacity, part 1: merge pinned at 1, worktrees-off mode, and one dead knob deleted (#2502)
First slice of the capacity simplification. Operator: *"just have two
capacity — overall per project agent count and max worktrees. Remove all
other capacities and counts."* Plus two later additions: **merge is
always 1, fixed**, and **worktrees off ⇒ limit by total agents only**.

Three independently revertable commits. No limiter is added anywhere;
one is deleted, one is made structurally absent, and one is pinned.

---

## 1. Merge concurrency ratcheted at 1 (test-only)

I was asked to add a limiter if merge concurrency could be raised. **It
cannot** — there is no setting, workflow property, pool or trait config
anywhere that raises it, so this adds no code and pins what already
holds.

Serialization lives in the **pump**: `drainMergeQueue`’s `mergeRunning`
re-entrancy latch, `activeMergeTaskId` as a single-slot identity, the
`mergeBodyInFlight` next-generation latch, and one `ProjectEngine` per
projectId.

**Not** in the merge-queue lease, which is a per-task ROW (`primaryKey
[projectId, taskId]`) — two tasks can hold leases simultaneously by
construction, and it has exactly one caller (the worktree-reuse
handoff). Ordinary merges never take it. A lease-level test would have
been describing an invariant that layer has never held.

The second half guards the other direction: a merge-concurrency
*setting* would not fail the pump ratchet — it would sit unread until
someone wired it up.

**Revert-proof:** deleting the latch → `expected 1 times, but got 2
times`; deleting the `finally` → latch-stuck; injecting
`maxConcurrentMerges: 2` → fails naming the key; injecting a
`maxParallelLanes` merge-trait field → fails naming the field. Sources
restored byte-identical after each injection.

## 2. `worktreesEnabled` — off means the worktree limit cannot bind

No worktrees-off mode existed (no
`worktreesEnabled`/`useWorktrees`/`worktreeMode` anywhere — only
worktree *configuration*).

**Why not `maxWorktrees: 0`, which needs no new key:** it deadlocks. `??
4` keeps `0` (not nullish), the gate is `used >= limit`, so `0 >= 0`
holds **on an empty board** and nothing ever dispatches — while the
operator-visible reason reads `gate=maxWorktrees; used=0/0`, a limiter
that looks like it is working while the board is dead. It also needs the
Command Center `{min:1}` clamp relaxed. So `0` costs the gate rewrite
*and* the clamp change *and* encodes a mode as a magic value.

**Off is absence, not a big number.** `resolveWorktreeCapacityLimit`
returns `number | null`; `ConcurrencyGateDiagnostic.maxWorktreesGate` is
now optional, so consulting a worktree limit in OFF mode does not
type-check. A gate holding `Infinity` can start binding again the moment
someone "fixes" a comparison; an absent gate cannot.

That paid for itself immediately: making it nullable surfaced a
**second, independent** worktree gate (`activeWorktrees >= maxWorktrees`
early-return) that a skip-by-convention approach would have missed
silently.

**Scope, deliberately:** this is a statement about *counting*, not
isolation. It does not make concurrent agents safe to share one checkout
and builds nothing toward that — the non-worktree paths that exist today
are fallbacks to the operator’s own tree, one of which caused FN-8600.

**Revert-proof:** a resolver ignoring the flag turns both OFF scheduler
tests red while every ON test stays green — they reuse the *same*
fixture (5 in-progress, limit 4) that pre-existing tests prove blocks,
so the pair moves in opposite directions. Removing `disabled:` reddens
the UI test.

## 3. `maxTriageConcurrent` deleted — it controlled nothing

**Measured: zero enforcement reads.** The only `.maxTriageConcurrent`
reference in the repo was a route echoing it back in `/config`. FN-8453
removed the pool it gated and left the knob shipping in
`DEFAULT_SETTINGS`, the settings type, the section registry, the API
response and six i18n catalogs, doing nothing, for releases.

Historical FNXC comments are **updated, not deleted** — they explain a
real past incident; they now say "planning admission slot" so they stop
implying a live setting. Tombstoned so it cannot return.

`/config` loses a field; safe in-repo since `fetchConfig`’s own return
type never declared it.

---

## Two corrections worth recording

- I earlier reported `maxWorktrees` had **no** Settings UI. Wrong —
`WorktreesSection.tsx:47`; my grep was truncated by `head`. It changed
the placement (toggle beside it, rather than a duplicate key in
Scheduling).
- I planned to assert the queued-reason string is rewritten in OFF mode.
Measured that it is **unreachable**: when `maxConcurrent` binds, the
sweep bails before the per-task reason and logs nothing. The test
asserts absence instead.

Two near-misses caught before commit: a pre-existing FN-7505 guard
caught my *new* key missing a description mapping; and editing i18n via
`json.load/dump` silently dropped unrelated duplicate keys
(`autoUpdateAndRestart` in `fr`) — Python keeps only the last of a
duplicated key. Redone textually, every catalog re-validated.

## Verification

`pnpm lint` clean · core/engine/dashboard/i18n typecheck clean · `pnpm
test:gate` green (309 + 10 + 71) · capacity/worktree suites 11/11 ·
engine merge-invariant + scheduler 45/45 · dashboard settings 114/114.
Rebased onto current main and re-verified.

Nothing was booted at any point.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a project setting to enable or disable running tasks in
worktrees.
* Disabling worktrees removes worktree capacity limits from task
scheduling.
* The “Max Worktrees” setting is disabled when worktree execution is
turned off.

* **Changes**
* Removed the unused triage concurrency setting from configuration and
dashboard responses.
* Updated scheduling diagnostics and queue messages to reflect disabled
worktree capacity limits.
  * Added localized labels and help text for the new setting.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:52:31 -07:00

442 lines
19 KiB
TypeScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import "@testing-library/jest-dom";
import { CommandCenter } from "../CommandCenter";
import { readAppFile } from "../../../test/cssFixture";
/*
FNXC:CommandCenterSystemTests 2026-07-23-22:35:
jsdom elements do not implement scrollIntoView, but SystemControlsArea's
job-section effect calls jobSectionRef.current?.scrollIntoView(...) inside a
requestAnimationFrame while a rebuild job is running. If that frame fires
before unmount cleanup, the missing method throws as an unhandled error and
fails the whole run even though every assertion passed. Stub it module-wide
(same convention as ChatView tests); the per-test spy below still swaps in
its own vi.fn() and restores this stub afterwards.
*/
Element.prototype.scrollIntoView = vi.fn();
const apiMock = vi.fn();
const mockFetchSystemInfo = vi.fn();
const mockFetchCurrentSystemRebuild = vi.fn();
const mockFetchSystemStats = vi.fn();
const mockFetchSystemLogs = vi.fn();
const mockFetchNodeSystemStats = vi.fn();
const mockFetchGlobalSettings = vi.fn();
const mockFetchNodes = vi.fn();
const subscribeSseMock = vi.fn(() => () => undefined);
vi.mock("../../../api/legacy", () => ({
fetchCodebaseMetrics: vi.fn().mockResolvedValue({ tokenEstimate: 0, sourceFileCount: 0, sourceByteCount: 0, diskBytes: 0, diskFileCount: 0, method: "local", truncated: false }),
api: (path: string, opts?: RequestInit) => apiMock(path, opts),
withProjectId: (path: string, projectId?: string) =>
projectId ? `${path}${path.includes("?") ? "&" : "?"}projectId=${encodeURIComponent(projectId)}` : path,
fetchOrgTree: vi.fn().mockResolvedValue([]),
fetchExecutorStats: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false, maxConcurrent: 2 }),
fetchSettings: vi.fn().mockResolvedValue({ maxConcurrent: 2, maxWorktrees: 5 }),
fetchConfig: vi.fn().mockResolvedValue({ maxConcurrent: 2, rootDir: "/" }),
updateSettings: vi.fn().mockResolvedValue({}),
createBackup: vi.fn().mockResolvedValue({ ok: true }),
fetchDashboardHealth: vi.fn().mockResolvedValue({ ok: true }),
fetchCurrentSystemRebuild: (...args: unknown[]) => mockFetchCurrentSystemRebuild(...args),
fetchSystemInfo: (...args: unknown[]) => mockFetchSystemInfo(...args),
fetchSystemLogs: (...args: unknown[]) => mockFetchSystemLogs(...args),
reloadAllSystemPlugins: vi.fn().mockResolvedValue({ ok: true }),
requestSystemRestart: vi.fn().mockResolvedValue({ ok: true }),
restartAllSystemAgents: vi.fn().mockResolvedValue({ ok: true }),
restartSystemEngines: vi.fn().mockResolvedValue({ ok: true }),
startSystemRebuild: vi.fn().mockResolvedValue({ id: "job-1", status: "running", kind: "rebuild", scope: "app", lines: [] }),
startFnBinaryLinkLocal: vi.fn().mockResolvedValue({
id: "job-fn-local",
status: "running",
kind: "fn-binary",
scope: "link-local",
lines: [],
}),
startFnBinaryUseGlobal: vi.fn().mockResolvedValue({
id: "job-fn-global",
status: "running",
kind: "fn-binary",
scope: "use-global",
lines: [],
}),
refreshUpdateCheck: vi.fn().mockResolvedValue({
currentVersion: "0.60.0",
latestVersion: "0.60.0",
updateAvailable: false,
}),
}));
vi.mock("../../../api", () => ({
fetchSystemStats: (...args: unknown[]) => mockFetchSystemStats(...args),
fetchNodeSystemStats: (...args: unknown[]) => mockFetchNodeSystemStats(...args),
fetchGlobalSettings: (...args: unknown[]) => mockFetchGlobalSettings(...args),
fetchNodes: (...args: unknown[]) => mockFetchNodes(...args),
killVitestProcesses: vi.fn().mockResolvedValue({ killed: 0, pids: [] }),
updateGlobalSettings: vi.fn().mockResolvedValue({}),
}));
vi.mock("../../../sse-bus", () => ({
subscribeSse: (...args: unknown[]) => subscribeSseMock(...args),
}));
vi.mock("../../../hooks/useAppSettings", () => ({
useAppSettings: () => ({
globalPaused: false,
enginePaused: false,
toggleGlobalPause: vi.fn(),
toggleEnginePause: vi.fn(),
refresh: vi.fn().mockResolvedValue(undefined),
}),
}));
function emptyOverviewResponse(path: string) {
if (path.includes("/command-center/tokens")) {
return { totals: { totalTokens: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, nTasks: 0 }, cost: { usd: null, unavailable: true, stale: false }, groups: [] };
}
if (path.includes("/command-center/tools")) return { totals: { calls: 0, errors: 0 }, groups: [] };
if (path.includes("/command-center/activity")) return { totals: { agentRuns: 0, tasksDone: 0 }, daily: [] };
if (path.includes("/command-center/signals")) return { totals: { open: 0, closed: 0 }, groups: [] };
if (path.includes("/command-center/live")) return { activeTasks: 0, activeAgents: 0, openSignals: 0, tokensPerMinute: 0, tasksByColumn: [] };
return {};
}
function systemInfoFixture(overrides: Record<string, unknown> = {}) {
return {
pid: 12345,
nodeVersion: "v22.0.0",
platform: "darwin",
arch: "arm64",
sourceCheckout: true,
supervised: true,
restartSupported: true,
rebuildSupported: true,
fnBinaryLinkLocalSupported: true,
fnBinaryUseGlobalSupported: true,
engineAvailable: true,
engineRestartSupported: true,
agentRestartSupported: true,
pluginReloadSupported: true,
logsSupported: true,
activeRebuild: null,
...overrides,
};
}
function systemStatsFixture() {
return {
systemStats: {
rss: 1024,
heapUsed: 512,
heapTotal: 1024,
heapLimit: 2048,
external: 0,
arrayBuffers: 0,
cpuPercent: 10,
loadAvg: [0.1, 0.2, 0.3] as [number, number, number],
cpuCount: 8,
systemTotalMem: 4096,
systemFreeMem: 2048,
pid: 12345,
nodeVersion: "v22.0.0",
platform: "darwin/arm64",
},
taskStats: {
total: 0,
byColumn: {},
active: 0,
agents: { idle: 0, active: 0, running: 0, error: 0 },
},
vitestProcessCount: 0,
vitestLastAutoKillAt: null,
};
}
describe("SystemControlsArea layout integration", () => {
const originalClipboard = navigator.clipboard;
const originalExecCommand = document.execCommand;
function mockClipboard(value: unknown) {
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value,
});
}
function mockExecCommand(result: boolean) {
const execCommand = vi.fn().mockReturnValue(result);
Object.defineProperty(document, "execCommand", {
configurable: true,
value: execCommand,
});
return execCommand;
}
async function renderSystemTab(addToast = vi.fn()) {
render(<CommandCenter projectId="proj-1" addToast={addToast} />);
fireEvent.click(screen.getByTestId("command-center-tab-system"));
const diagnosticsCard = await screen.findByTestId("cc-syscontrol-diagnostics");
return { addToast, diagnosticsCard };
}
beforeEach(() => {
vi.clearAllMocks();
subscribeSseMock.mockImplementation(() => () => undefined);
apiMock.mockImplementation((path: string) => Promise.resolve(emptyOverviewResponse(path)));
mockFetchSystemInfo.mockResolvedValue(systemInfoFixture());
mockFetchSystemLogs.mockResolvedValue({
entries: [{ timestamp: "2026-07-12T00:00:00.000Z", level: "info", message: "ready" }],
});
mockFetchCurrentSystemRebuild.mockResolvedValue({ job: null });
mockFetchSystemStats.mockResolvedValue(systemStatsFixture());
mockFetchNodeSystemStats.mockResolvedValue(systemStatsFixture());
mockFetchGlobalSettings.mockResolvedValue({ vitestAutoKillEnabled: true, vitestKillThresholdPct: 90 });
mockFetchNodes.mockResolvedValue([]);
});
afterEach(() => {
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: originalClipboard,
});
Object.defineProperty(document, "execCommand", {
configurable: true,
value: originalExecCommand,
});
document.body.innerHTML = "";
});
it("wraps System controls, Server logs, and Live system health in the shared gap owner", async () => {
render(<CommandCenter projectId="proj-1" />);
fireEvent.click(screen.getByTestId("command-center-tab-system"));
const systemTab = await screen.findByTestId("cc-system-tab");
const controls = await screen.findByTestId("cc-system-controls");
const logs = await screen.findByTestId("cc-system-logs");
const stats = await screen.findByTestId("cc-area-system");
expect(systemTab).toHaveClass("cc-system-tab");
expect(systemTab).toContainElement(controls);
expect(systemTab).toContainElement(logs);
expect(systemTab).toContainElement(stats);
expect(controls.parentElement).toBe(systemTab);
expect(logs.parentElement).toBe(systemTab);
expect(stats.parentElement).toBe(systemTab);
expect(screen.getByTestId("cc-system-logs-toggle")).toBeInTheDocument();
await waitFor(() => expect(mockFetchSystemInfo).toHaveBeenCalled());
});
it("keeps the System controls refresh button in the scoped inline header", async () => {
render(<CommandCenter projectId="proj-1" />);
fireEvent.click(screen.getByTestId("command-center-tab-system"));
const controls = await screen.findByTestId("cc-system-controls");
const header = controls.querySelector<HTMLElement>(".cc-system-controls-header");
const title = screen.getByRole("heading", { name: "System controls" });
const refresh = screen.getByTestId("cc-system-refresh");
expect(header).toBeInTheDocument();
expect(header).toHaveClass("cc-area-section-header", "cc-system-controls-header");
expect(title.parentElement).toBe(header);
expect(refresh.parentElement).toBe(header);
});
it("copies diagnostics through the execCommand fallback when Clipboard API is unavailable", async () => {
mockClipboard(undefined);
const execCommand = mockExecCommand(true);
const { addToast, diagnosticsCard } = await renderSystemTab();
fireEvent.click(within(diagnosticsCard).getByRole("button", { name: "Copy" }));
await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
expect(addToast).toHaveBeenCalledWith("Diagnostics copied to clipboard", "success");
expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("writeText"), "error");
});
it("copies diagnostics through navigator.clipboard.writeText in secure contexts", async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
mockClipboard({ writeText });
const { addToast, diagnosticsCard } = await renderSystemTab();
fireEvent.click(within(diagnosticsCard).getByRole("button", { name: "Copy" }));
await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
expect(writeText.mock.calls[0]?.[0]).toContain('"recentLogs"');
expect(addToast).toHaveBeenCalledWith("Diagnostics copied to clipboard", "success");
expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("writeText"), "error");
});
it("shows a failure toast when diagnostics cannot be copied by either clipboard path", async () => {
mockFetchSystemInfo.mockResolvedValue({ ...systemInfoFixture(), logsSupported: false });
mockClipboard(undefined);
const execCommand = mockExecCommand(false);
const { addToast, diagnosticsCard } = await renderSystemTab();
fireEvent.click(within(diagnosticsCard).getByRole("button", { name: "Copy" }));
await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
expect(addToast).toHaveBeenCalledWith("Could not copy diagnostics to clipboard", "error");
expect(addToast).not.toHaveBeenCalledWith("Diagnostics copied to clipboard", "success");
expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("writeText"), "error");
});
it("opens the guided report modal instead of a legacy GitHub window", async () => {
const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
await renderSystemTab();
const reportCard = await screen.findByTestId("cc-syscontrol-report-bug");
fireEvent.click(within(reportCard).getByRole("button", { name: "Report" }));
fireEvent.click(await screen.findByRole("menuitem", { name: "Report bug" }));
expect(await screen.findByRole("dialog", { name: "bug report" })).toBeTruthy();
expect(openSpy).not.toHaveBeenCalled();
openSpy.mockRestore();
});
it("keeps the System controls header row override active on mobile", () => {
const css = readAppFile("components/command-center/areas/SystemControlsArea.css");
expect(css).toMatch(/\.cc-area-section-header\.cc-system-controls-header\s*{[^}]*flex-direction:\s*row;[^}]*justify-content:\s*space-between;/s);
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*{[\s\S]*\.cc-area-section-header\.cc-system-controls-header\s*{[^}]*flex-direction:\s*row;[^}]*justify-content:\s*space-between;/);
});
it("keeps the System tab gap and mobile scroll-owner CSS contracts tokenized", () => {
const css = readAppFile("components/command-center/CommandCenter.css");
expect(css).toMatch(/\.cc-system-tab\s*{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*gap:\s*var\(--space-lg\);/s);
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*{[\s\S]*\.cc-tabpanel\s*{[^}]*padding-bottom:\s*calc\(var\(--space-lg\) \+ env\(safe-area-inset-bottom, 0\) \+ var\(--standalone-bottom-gap\)\);/);
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*{[\s\S]*\.cc-system-tab\s*{[^}]*gap:\s*var\(--space-lg\);/);
expect(css).not.toMatch(/\.cc-system-tab\s*{[^}]*overflow-y:\s*auto;/s);
});
/*
FNXC:SystemPanelFnBinary 2026-07-15-09:54:
Source/dev hosts expose build-and-link-local; packaged hosts hide it. Use-global
and check-for-updates stay available, and starting a build job surfaces the
shared log viewer.
*/
it("shows fn binary and update controls for a source checkout and scrolls job output into view", async () => {
const scrollIntoView = vi.fn();
const original = Element.prototype.scrollIntoView;
Element.prototype.scrollIntoView = scrollIntoView;
render(<CommandCenter projectId="proj-1" />);
fireEvent.click(screen.getByTestId("command-center-tab-system"));
const linkLocal = await screen.findByTestId("cc-syscontrol-fn-link-local");
const useGlobal = screen.getByTestId("cc-syscontrol-fn-use-global");
const checkUpdates = screen.getByTestId("cc-syscontrol-check-updates");
expect(linkLocal).toBeInTheDocument();
expect(useGlobal).toBeInTheDocument();
expect(checkUpdates).toBeInTheDocument();
fireEvent.click(within(linkLocal).getByRole("button", { name: "Build & link" }));
await waitFor(() => {
expect(screen.getByTestId("cc-system-rebuild-output")).toBeInTheDocument();
});
await waitFor(() => {
expect(scrollIntoView).toHaveBeenCalled();
});
Element.prototype.scrollIntoView = original;
});
function installScrollGeometry(element: HTMLElement, scrollHeight = 500, clientHeight = 100) {
let scrollTop = 0;
Object.defineProperties(element, {
scrollHeight: { configurable: true, get: () => scrollHeight },
clientHeight: { configurable: true, get: () => clientHeight },
scrollTop: {
configurable: true,
get: () => scrollTop,
set: (value: number) => {
scrollTop = value;
},
},
});
return {
get scrollTop() { return scrollTop; },
set scrollTop(value: number) { scrollTop = value; },
setScrollHeight(value: number) { scrollHeight = value; },
};
}
function getStreamEvents(path: string) {
const call = [...subscribeSseMock.mock.calls].reverse().find(([url]) => url === path);
expect(call).toBeDefined();
return (call?.[1] as { events: Record<string, (event: MessageEvent) => void> }).events;
}
it("keeps manually scrolled rebuild output in place while SSE lines grow", async () => {
render(<CommandCenter projectId="proj-1" />);
fireEvent.click(screen.getByTestId("command-center-tab-system"));
const rebuild = await screen.findByTestId("cc-syscontrol-rebuild-app");
fireEvent.click(within(rebuild).getByRole("button", { name: "Rebuild" }));
const output = (await screen.findByTestId("cc-system-rebuild-output")).querySelector("pre")!;
const geometry = installScrollGeometry(output);
const events = getStreamEvents("/api/system/jobs/job-1/stream");
await act(async () => events.line(new MessageEvent("message", { data: JSON.stringify({ i: 1, stream: "stdout", text: "first" }) })));
geometry.scrollTop = 100;
fireEvent.scroll(output);
geometry.setScrollHeight(600);
await act(async () => events.line(new MessageEvent("message", { data: JSON.stringify({ i: 2, stream: "stdout", text: "second" }) })));
expect(geometry.scrollTop).toBe(100);
geometry.scrollTop = 500;
fireEvent.scroll(output);
geometry.setScrollHeight(700);
await act(async () => events.line(new MessageEvent("message", { data: JSON.stringify({ i: 3, stream: "stdout", text: "third" }) })));
expect(geometry.scrollTop).toBe(700);
});
it("keeps manually scrolled live server logs in place while SSE lines grow", async () => {
render(<CommandCenter projectId="proj-1" />);
fireEvent.click(screen.getByTestId("command-center-tab-system"));
await screen.findByTestId("cc-system-controls");
fireEvent.click(screen.getByTestId("cc-system-logs-toggle"));
const output = await screen.findByText("No log entries yet.");
const container = output.parentElement!;
const geometry = installScrollGeometry(container);
const events = getStreamEvents("/api/system/logs/stream");
await act(async () => events.log(new MessageEvent("message", { data: JSON.stringify({ timestamp: "2026-07-18T00:00:00.000Z", level: "info", message: "first" }) })));
geometry.scrollTop = 100;
fireEvent.scroll(container);
geometry.setScrollHeight(600);
await act(async () => events.log(new MessageEvent("message", { data: JSON.stringify({ timestamp: "2026-07-18T00:00:01.000Z", level: "info", message: "second" }) })));
expect(geometry.scrollTop).toBe(100);
geometry.scrollTop = 500;
fireEvent.scroll(container);
geometry.setScrollHeight(700);
await act(async () => events.log(new MessageEvent("message", { data: JSON.stringify({ timestamp: "2026-07-18T00:00:02.000Z", level: "info", message: "third" }) })));
expect(geometry.scrollTop).toBe(700);
});
it("hides build-and-link-local when the host is not a source checkout", async () => {
mockFetchSystemInfo.mockResolvedValue(
systemInfoFixture({
rebuildSupported: false,
fnBinaryLinkLocalSupported: false,
sourceWorkspaceRoot: undefined,
}),
);
render(<CommandCenter projectId="proj-1" />);
fireEvent.click(screen.getByTestId("command-center-tab-system"));
await screen.findByTestId("cc-system-controls");
expect(screen.queryByTestId("cc-syscontrol-fn-link-local")).not.toBeInTheDocument();
expect(screen.getByTestId("cc-syscontrol-fn-use-global")).toBeInTheDocument();
expect(screen.getByTestId("cc-syscontrol-check-updates")).toBeInTheDocument();
});
});