fix(dashboard): repair CSS token/contrast regressions and ~19 stale tests (#1770)

Follow-up to #1765 (which fixed the first 30). Fixes ~19 more
deterministic dashboard test failures that were pre-existing on `main`
in the non-blocking full-suite lane. Mix of **real product fixes** and
stale-test reconciliation — no appeasement (no widened timeouts, skips,
or weakened assertions). Rebased onto latest `main`.

## Real product fixes (guards caught genuine regressions)
- **Info-toast contrast (WCAG AA):** shadcn-custom light theme had
white-on-`#0284c7` (4.10 < 4.5); enrolled it in the light-mode dark-text
correction.
- **27 undefined CSS token references** (typos/foreign tokens) renamed
to canonical tokens; defined the genuinely-intended `--border-strong`
and `--right-dock-min/max-width`.
- **Raw `rgba()` box-shadow** tokenized to `color-mix`; dev-server
mobile header split into its own responsive rule.

## Stale tests reconciled with intentional product changes
- `workflowColumns` graduated to always-on (`buildBoardWorkflowsPayload`
unit test).
- Workflow optional-steps source re-pointed to v2 `optional-group`
nodes.
- Command-center pricing docs (filled one real gap: `openai-codex:*`
keying).
- SetupWizardModal `detectWorkspace` + `workspaceMode`/`taskPrefix`
payload.
- board-mobile listener-count assertion → unmount no-throw behavior.
- CommandCenterControls / ThemeSelector "Default" → "Fusion Legacy"
relabel.
- ProjectOverview / WorkflowNodeEditor header divider intentionally
removed.

## Verification
- foundation-ui lane fully green (56 files / 487 tests); all
originally-failing files pass.
- All CSS hygiene/token-validity guards green post-tokenization (no new
violations).
- `patch` changeset added (toast-contrast + tokenization ship in
`@runfusion/fusion`).

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

<!-- stage-review-badge-begin -->

---

<a href="https://stagereview.app/Runfusion/Fusion/pull/1770">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
  </picture>
</a>

<!-- stage-review-badge-end -->

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved dashboard styling consistency across several screens,
including better contrast for toast messages and more reliable theme
colors, shadows, and spacing.
* Fixed mobile and responsive layout behavior in dashboard views so
headers, dropdowns, and terminal controls render more cleanly.
* Updated labels and wording in theme-related UI to match the current
experience.
* Resolved a few stale dashboard tests to align with recent product
changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-06-25 16:11:01 -07:00
committed by GitHub
28 changed files with 148 additions and 64 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix unreadable info-toast contrast and dashboard CSS token regressions.
category: fix
dev: Tokenized raw rgba/undefined CSS vars across ~15 dashboard component stylesheets, defined missing --border-strong / right-dock width tokens, enrolled the shadcn-custom light theme in the dark-text toast correction (WCAG AA). Also repairs ~19 stale dashboard tests that trailed intentional product changes (workflowColumns graduation, onboarding flow, theme relabels, header divider removal).

View File

@@ -865,7 +865,8 @@ Key server capabilities:
- Command Center analytics APIs (`GET /api/command-center/tokens`, `/tools`, `/activity`, `/productivity`, `/team`, `/github`, `/signals`, `/plugin-activations`, `/live`) are project-scoped dashboard routes. `/productivity` reads Lines changed from nullable `task_commit_associations.additions`/`deletions` merge-time or backfilled diff stats, derives estimated `hoursSaved` from that LOC via the exported `HUMAN_LINES_PER_HOUR` rate, and keeps the unavailable sentinel for both fields when no in-range association has stats. `POST /api/command-center/productivity/backfill-loc` is the explicit operator-triggered, dry-run-defaulting local-git backfill for historical NULL stats; it is not run during dashboard rendering or analytics reads. Its `taskDuration` payload aggregates done tasks whose `executionCompletedAt` falls in the selected range, using positive `tasks.cumulativeActiveMs` values for completed count, average, median, p90, and total active execution time; missing qualifying durations remain unavailable rather than zero. `/signals` aggregates real local `incidents` rows for total/open/resolved counts, MTTR, and source/severity/status breakdowns and returns honest empty/unavailable sentinels instead of synthetic signal volume. `/plugin-activations` aggregates persisted plugin/extension load events for the selected range and returns unavailable when no rows exist instead of treating missing history as zero activations.
<!-- FNXC:CommandCenter 2026-06-21-00:00: Maintainers need the pricing contract in architecture docs: MODEL_PRICING is hand-maintained, pricingAsOf changes with every rate edit, provider coverage includes OpenAI/Codex/Anthropic/Gemini, and Command Center never guesses or persists costs. -->
<!-- FNXC:CommandCenter 2026-06-22-00:00: FN-6876 made pricing operator-editable: user/global overrides and LiteLLM one-click refreshes must take precedence over the built-in table while remaining estimates, not persisted billing truth. -->
- Model pricing & cost estimation: Command Center token cost is derived at read time by `packages/core/src/model-pricing.ts` and is not persisted as billing truth. Maintainers still update the built-in `MODEL_PRICING` fallback table in that file; keys are lowercased `${provider}:${model}` with a bare `:model` fallback for callers that only know the model id. Each entry stores USD per 1M tokens for input, output, cache-read, and cache-write plus a `source` citation. Bump `pricingAsOf` in the same change as any built-in rate edit, because the dashboard surfaces it as the **prices as of** date and marks entries low-confidence after `PRICING_STALE_AFTER_MS` (approximately 180 days / two quarters) relative to that date. Global `modelPricingOverrides` from Settings take precedence over built-ins using the same exact-key then bare-model lookup order; `POST /api/command-center/pricing/fetch` is the only dashboard network path and fetches LiteLLM's model pricing JSON on explicit user action, parses it through the pure core parser, persists the resulting overrides with fetched metadata, and leaves the prior overrides intact on fetch/parse failure. Unknown models resolve to `unavailable` rather than a guessed price.
<!-- FNXC:CommandCenter 2026-06-25-13:30: Document the `openai-codex:*` keying gotcha explicitly. Codex runs persist the `openai-codex` provider, so their pricing entries must be keyed `openai-codex:<modelId>` (a whole dedicated section in model-pricing.ts) or cost reads `unavailable`; the maintenance contract doc must name this so the rule survives. -->
- Model pricing & cost estimation: Command Center token cost is derived at read time by `packages/core/src/model-pricing.ts` and is not persisted as billing truth. Maintainers still update the built-in `MODEL_PRICING` fallback table in that file; keys are lowercased `${provider}:${model}` with a bare `:model` fallback for callers that only know the model id. Codex runs store the `openai-codex` provider, so those rates must be keyed explicitly as `openai-codex:*` (for example `openai-codex:gpt-5-codex`) rather than relying on the OpenAI provider or bare-model fallback, otherwise Command Center shows their cost as `unavailable`. Each entry stores USD per 1M tokens for input, output, cache-read, and cache-write plus a `source` citation. Bump `pricingAsOf` in the same change as any built-in rate edit, because the dashboard surfaces it as the **prices as of** date and marks entries low-confidence after `PRICING_STALE_AFTER_MS` (approximately 180 days / two quarters) relative to that date. Global `modelPricingOverrides` from Settings take precedence over built-ins using the same exact-key then bare-model lookup order; `POST /api/command-center/pricing/fetch` is the only dashboard network path and fetches LiteLLM's model pricing JSON on explicit user action, parses it through the pure core parser, persists the resulting overrides with fetched metadata, and leaves the prior overrides intact on fetch/parse failure. Unknown models resolve to `unavailable` rather than a guessed price.
- Remote access APIs (`/api/remote/*`) for provider config, activation, tunnel lifecycle, status, token issuance, authenticated URL generation, and QR payload generation
- Operational runbook (prereqs/security/troubleshooting): [`docs/remote-access.md`](./remote-access.md)
- `/api/remote/tunnel/start`, `/api/remote/tunnel/stop`, and `/api/remote/tunnel/kill-external` cover tunnel lifecycle and external funnel cleanup.

View File

@@ -36,7 +36,12 @@ describe("chat tool-call mobile layout css", () => {
const nowrapRule = mobileCss.match(/\.chat-tool-calls-names,\s*\n\s*\.chat-tool-call-name,\s*\n\s*\.chat-tool-call-status-text,\s*\n\s*\.chat-tool-calls-group-status,\s*\n\s*\.chat-tool-calls-count\s*\{[^}]*\}/m)?.[0] ?? "";
expect(nowrapRule).toMatch(/white-space:\s*nowrap/);
const allMobileSummaryRules = [...mobileCss.matchAll(/\.chat-tool-calls-group-summary\s*\{[^}]*\}/g)].map((m) => m[0]);
// FNXC:ChatToolCalls 2026-06-25-13:15: Match any mobile rule whose selector list
// includes .chat-tool-calls-group-summary (grouped or standalone). The previously
// standalone quick-chat rule was removed when quick chat became the modal chat, so
// the invariant (no mobile group-summary rule may revert to flex-direction: column)
// must now be asserted against the grouped full-chat rule that actually exists.
const allMobileSummaryRules = [...mobileCss.matchAll(/\.chat-tool-calls-group-summary[^{}]*\{[^}]*\}/g)].map((m) => m[0]);
expect(allMobileSummaryRules.length).toBeGreaterThan(0);
expect(allMobileSummaryRules.every((rule) => !/flex-direction:\s*column/.test(rule))).toBe(true);
});

View File

@@ -106,8 +106,13 @@ describe("Status color CSS custom properties", () => {
});
it("uses --surface-hover token references with tokenized fallback (no raw rgba)", () => {
// FNXC:DashboardThemeTokens 2026-06-25-13:15: The invariant is that every
// --surface-hover fallback stays tokenized (color-mix), never a raw rgba().
// The original example string (QuickChatFAB's `var(--surface) 55%, transparent`)
// was removed when quick chat was replaced by the modal chat refactor, so we
// assert against a still-present tokenized fallback form instead.
expect(css).toContain("var(--surface-hover)");
expect(css).toContain("var(--surface-hover, color-mix(in srgb, var(--surface) 55%, transparent))");
expect(css).toContain("var(--surface-hover, color-mix(in srgb, var(--text) 6%, transparent))");
expect(css).not.toMatch(/var\(--surface-hover,\s*rgba\(/);
});

View File

@@ -152,7 +152,8 @@ Keep each block full-width and float the role/timestamp badge as a sticky overla
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--surface) 88%, transparent);
box-shadow: 0 1px 4px color-mix(in srgb, var(--shadow-color, #000) 12%, transparent);
/* FNXC:DashboardThemeTokens 2026-06-25-13:15: Shadow base uses the repo #000000 color-mix convention; --shadow-color was never a defined token (token-validity guard). */
box-shadow: 0 1px 4px color-mix(in srgb, #000000 12%, transparent);
pointer-events: none;
white-space: nowrap;
}

View File

@@ -714,7 +714,11 @@ exactly when the surrounding chrome is gone.
max-width: none;
}
.devserver-preview-header,
/* FNXC:DevServerLayout 2026-06-25-13:15: Keep the preview header's mobile wrap rule standalone (not grouped) so the narrow-screen responsiveness guard can assert flex-wrap on the header specifically. */
.devserver-preview-header {
flex-wrap: wrap;
}
.devserver-preview-modal-launcher__copy {
flex-wrap: wrap;
}
@@ -937,7 +941,11 @@ vertically (.dev-server-view overflow-y:auto), so each panel just needs to be fu
max-width: none;
}
.devserver-preview-header,
/* FNXC:DevServerLayout 2026-06-25-13:15: Keep the preview header's mobile wrap rule standalone (not grouped) so the narrow-screen responsiveness guard can assert flex-wrap on the header specifically. */
.devserver-preview-header {
flex-wrap: wrap;
}
.devserver-preview-modal-launcher__copy {
flex-wrap: wrap;
}

View File

@@ -80,7 +80,7 @@ Hidden until a file is selected; when selected it overlays the tree as the singl
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--font-sm);
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--text);
}
@@ -111,12 +111,12 @@ Hidden until a file is selected; when selected it overlays the tree as the singl
.dock-files-viewer__status {
padding: var(--space-md);
font-size: var(--font-sm);
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.dock-files-viewer__status--error {
color: var(--danger, var(--text));
color: var(--color-error, var(--text));
}
/* FNXC:RightDockFiles 2026-06-22-12:00: empty-state placeholder shown in the wide right pane until a file is selected. */

View File

@@ -49,7 +49,7 @@ Header is the drag handle. `touch-action: none` (matching the resize handles) ha
min-height: 44px;
padding: var(--space-sm) var(--space-md);
border-bottom: thin solid var(--border);
background: var(--surface-elevated, var(--surface));
background: var(--surface-2, var(--surface));
cursor: grab;
user-select: none;
touch-action: none;

View File

@@ -777,13 +777,13 @@ The markdown variant must NOT pre-wrap/clamp — MailboxMessageContent emits rea
}
.preview-state-badge--open {
color: var(--success, var(--accent));
border-color: color-mix(in srgb, var(--success, var(--accent)) 40%, transparent);
color: var(--color-success, var(--accent));
border-color: color-mix(in srgb, var(--color-success, var(--accent)) 40%, transparent);
}
.preview-state-badge--closed {
color: var(--danger, var(--text-muted));
border-color: color-mix(in srgb, var(--danger, var(--text-muted)) 40%, transparent);
color: var(--color-error, var(--text-muted));
border-color: color-mix(in srgb, var(--color-error, var(--text-muted)) 40%, transparent);
}
.preview-state-badge--merged {
@@ -853,7 +853,7 @@ Checks + Comments sections live below the PR body in the scrollable preview pane
}
.preview-detail-error {
color: var(--danger, var(--text-muted));
color: var(--color-error, var(--text-muted));
font-size: 12px;
}
@@ -894,18 +894,18 @@ Checks + Comments sections live below the PR body in the scrollable preview pane
}
.github-import-pr-check-pill--success {
color: var(--success, var(--accent));
border-color: color-mix(in srgb, var(--success, var(--accent)) 40%, transparent);
color: var(--color-success, var(--accent));
border-color: color-mix(in srgb, var(--color-success, var(--accent)) 40%, transparent);
}
.github-import-pr-check-pill--failure {
color: var(--danger, var(--text-muted));
border-color: color-mix(in srgb, var(--danger, var(--text-muted)) 40%, transparent);
color: var(--color-error, var(--text-muted));
border-color: color-mix(in srgb, var(--color-error, var(--text-muted)) 40%, transparent);
}
.github-import-pr-check-pill--pending {
color: var(--warning, var(--accent));
border-color: color-mix(in srgb, var(--warning, var(--accent)) 40%, transparent);
color: var(--color-warning, var(--accent));
border-color: color-mix(in srgb, var(--color-warning, var(--accent)) 40%, transparent);
}
.github-import-pr-check-pill--neutral {
@@ -1004,8 +1004,8 @@ Across the thread: a top filter (All/Human/Bot) and prev/next chevrons live in t
}
.github-import-comment__type-badge--bot {
color: var(--warning, var(--accent));
border-color: color-mix(in srgb, var(--warning, var(--accent)) 40%, transparent);
color: var(--color-warning, var(--accent));
border-color: color-mix(in srgb, var(--color-warning, var(--accent)) 40%, transparent);
}
.github-import-comment__time {
@@ -1092,7 +1092,7 @@ Across the thread: a top filter (All/Human/Bot) and prev/next chevrons live in t
.github-import-comments-filter__chip.active {
background: var(--accent);
color: var(--accent-contrast, #fff);
color: var(--accent-text, #fff);
}
/* Back button - hidden on desktop by default */

View File

@@ -14,6 +14,9 @@ The right dock OVERLAYS the page content (floats over the right edge) instead of
z-index: 20;
display: flex;
flex-direction: column;
/* FNXC:RightDock 2026-06-25-13:15: Define the dock width override hooks on the host so the var() references resolve (token-validity guard); themes/JS can still override them. */
--right-dock-min-width: calc(var(--space-2xl) * 8);
--right-dock-max-width: calc(var(--space-2xl) * 40);
min-width: min(100%, var(--right-dock-min-width, calc(var(--space-2xl) * 8)));
/*
FNXC:RightDock 2026-06-23-00:50:

View File

@@ -1340,7 +1340,7 @@ instead of resolving to a flat --card with no accent emphasis.
*/
.automation-list-row.active {
border-color: var(--accent);
background: var(--accent-subtle, color-mix(in srgb, var(--accent) 10%, transparent));
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.automation-list-row-name {
@@ -2341,7 +2341,7 @@ The previous bespoke rules here hid the tab labels (icon-only) and used a crampe
.gm-repo-selector {
flex: 1;
min-width: 0;
background: var(--bg-input);
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
border-radius: 4px;

View File

@@ -492,7 +492,7 @@
position: fixed;
left: 50%;
bottom: var(--space-xl);
z-index: var(--z-modal, 1000);
z-index: 1000;
width: min(520px, calc(100vw - (var(--space-lg) * 2)));
transform: translateX(-50%);
align-items: flex-start;

View File

@@ -24,7 +24,7 @@
.shadcn-color-picker-description {
margin: var(--space-xs) 0 0;
color: var(--text-muted);
font-size: var(--font-size-sm);
font-size: var(--font-size-xs);
}
.shadcn-color-picker-grid {
@@ -50,7 +50,7 @@
flex-direction: column;
gap: var(--space-xs);
color: var(--text);
font-size: var(--font-size-sm);
font-size: var(--font-size-xs);
font-weight: 500;
}

View File

@@ -2299,7 +2299,7 @@ Read-only list/placeholder only — not the deferred rich per-repo-status compon
.workspace-worktrees-placeholder {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary, inherit);
color: var(--text-muted, inherit);
margin-bottom: var(--space-xs);
}
.workspace-worktrees-list {
@@ -2321,5 +2321,5 @@ Read-only list/placeholder only — not the deferred rich per-repo-status compon
font-weight: 600;
}
.workspace-worktrees-branch {
color: var(--color-text-secondary, inherit);
color: var(--text-muted, inherit);
}

View File

@@ -48,7 +48,7 @@ Quick Chat and Terminal are peer footer launchers. Keep the Terminal footer vari
.terminal-launcher--footer .terminal-launcher__chevron {
width: auto;
padding-inline: var(--space-xxs);
padding-inline: var(--space-xs);
}
.terminal-launcher--footer .terminal-launcher__label {
@@ -79,7 +79,7 @@ Quick Chat and Terminal are peer footer launchers. Keep the Terminal footer vari
}
.terminal-launcher__main {
min-height: var(--control-height-sm);
min-height: 28px;
gap: var(--space-xs);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
@@ -93,19 +93,19 @@ Quick Chat and Terminal are peer footer launchers. Keep the Terminal footer vari
.terminal-launcher__label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
font-weight: 500;
}
.terminal-launcher__chevron {
min-height: var(--control-height-sm);
width: var(--control-height-sm);
min-height: 28px;
width: 28px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
color: var(--text-muted);
}
.terminal-launcher__divider {
width: var(--border-width, 1px);
width: 1px;
height: var(--space-md);
background: var(--border);
}
@@ -130,7 +130,7 @@ Quick Chat and Terminal are peer footer launchers. Keep the Terminal footer vari
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: var(--z-popover);
z-index: 1000;
padding: var(--space-sm);
}
@@ -155,11 +155,11 @@ Quick Chat and Terminal are peer footer launchers. Keep the Terminal footer vari
.quick-scripts-dropdown__empty p {
margin: 0;
color: var(--text);
font-size: var(--font-size-sm);
font-size: var(--font-size-xs);
}
.quick-scripts-dropdown__empty-action {
min-height: var(--control-height-sm);
min-height: 28px;
}
.quick-scripts-dropdown__list {
@@ -199,14 +199,14 @@ Quick Chat and Terminal are peer footer launchers. Keep the Terminal footer vari
.quick-scripts-dropdown__item-info {
display: flex;
flex-direction: column;
gap: var(--space-xxs);
gap: var(--space-xs);
min-width: 0;
}
.quick-scripts-dropdown__item-name {
color: var(--text);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
font-size: var(--font-size-xs);
font-weight: 500;
}
.quick-scripts-dropdown__item-command {
@@ -225,7 +225,7 @@ Quick Chat and Terminal are peer footer launchers. Keep the Terminal footer vari
.quick-scripts-dropdown__manage {
color: var(--text-muted);
font-size: var(--font-size-sm);
font-size: var(--font-size-xs);
}
.quick-scripts-dropdown__manage span {

View File

@@ -128,7 +128,7 @@ Larger grab target for the docked terminal top resize handle: it straddles the p
width: calc(var(--space-xl) * 2);
height: calc(var(--space-xs) / 2);
transform: translateX(-50%);
border-radius: var(--radius-full);
border-radius: var(--radius-pill);
background: var(--border);
}

View File

@@ -572,7 +572,7 @@ Redesign Todos to fit the rest of the dashboard theme: full-height tokenized wor
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--card);
box-shadow: var(--shadow-xs, 0 1px 2px color-mix(in srgb, var(--bg) 70%, transparent));
box-shadow: var(--shadow-sm, 0 1px 2px color-mix(in srgb, var(--bg) 70%, transparent));
transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}

View File

@@ -37,7 +37,8 @@
background: var(--surface, #fff);
border: 1px solid var(--border);
border-radius: var(--radius-sm, 6px);
box-shadow: var(--shadow-md, 0 6px 20px rgba(0, 0, 0, 0.18));
/* FNXC:DashboardThemeTokens 2026-06-25-13:15: Shadow fallback must stay tokenized via color-mix, never a raw alpha color call, so the global/component no-raw-rgb CSS hygiene guards pass. */
box-shadow: var(--shadow-md, 0 6px 20px color-mix(in srgb, #000000 18%, transparent));
}
.wf-optional-steps-dropdown-option {

View File

@@ -142,7 +142,10 @@ describe("ProjectOverview", () => {
expect(projectOverviewCss).toContain("width: 100%;");
expect(projectOverviewCss).toContain("flex: 0 0 auto;");
expect(projectOverviewCss).toContain("background: var(--surface);");
expect(projectOverviewCss).toContain("border-bottom-color: var(--border);");
// FNXC:Dashboard 2026-06-25-12:30: The Dashboard header intentionally dropped its
// bottom divider so the shared ViewHeader chrome matches Missions and Chat (see
// ProjectOverview.css FNXC note). Assert the divider is absent rather than present.
expect(projectOverviewCss).not.toContain("border-bottom-color: var(--border);");
expect(projectOverviewCss).toContain("max-width: 1400px;");
});

View File

@@ -38,9 +38,15 @@ vi.mock("../../hooks/useNodes", () => ({
}));
// Mock api module
// FNXC:Onboarding 2026-06-25-13:20:
// SetupWizardModal now probes `detectWorkspace` on existing-directory path entry
// (FNXC:Workspace 2026-06-24-21:00). The mock must export it or the component's
// path-change handler throws an unhandled rejection. Default to a non-workspace
// single-repo result so the legacy submit-payload assertions stay deterministic.
vi.mock("../../api", () => ({
registerProject: vi.fn(),
createAgent: vi.fn(),
detectWorkspace: vi.fn().mockResolvedValue({ repos: [], isWorkspace: false }),
browseDirectory: vi.fn().mockResolvedValue({
currentPath: "/home/user",
parentPath: "/home",
@@ -234,12 +240,18 @@ describe("SetupWizardModal", () => {
fireEvent.click(screen.getByText("Register Project"));
await waitFor(() => {
// FNXC:Onboarding 2026-06-25-13:20:
// Existing-directory payload now carries `workspaceMode` (FNXC:Workspace) and
// auto-derived `taskPrefix` (FNXC:TaskPrefix). A non-workspace dir stays
// workspaceMode:false; prefix derives from name "project" -> "PROJ".
expect(mockRegisterProject).toHaveBeenCalledWith({
name: "project",
path: "/existing/project",
isolationMode: "in-process",
nodeId: undefined,
cloneUrl: undefined,
workspaceMode: false,
taskPrefix: "PROJ",
});
});
});
@@ -291,12 +303,17 @@ describe("SetupWizardModal", () => {
fireEvent.click(screen.getByText("Register Project"));
await waitFor(() => {
// FNXC:Onboarding 2026-06-25-13:20:
// Clone payload also carries workspaceMode:false (clone always creates a single
// fresh repo) and taskPrefix derived from name "fusion" -> "FUSI".
expect(mockRegisterProject).toHaveBeenCalledWith({
name: "fusion",
path: "/tmp/fusion",
isolationMode: "in-process",
nodeId: undefined,
cloneUrl: "https://github.com/runfusion/fusion.git",
workspaceMode: false,
taskPrefix: "FUSI",
});
});
expect(await screen.findByText("Create your first agent")).toBeDefined();

View File

@@ -89,7 +89,11 @@ describe("ThemeSelector", () => {
expect(screen.getByLabelText(`${theme.label} theme`)).toBeDefined();
}
expect(THEME_OPTIONS.map((theme) => theme.value)).toEqual([...COLOR_THEMES]);
expect(screen.getByLabelText("Default theme").getAttribute("aria-pressed")).toBe("true");
// FNXC:Theme 2026-06-25-13:15: The "default" color-theme value is labeled "Fusion Legacy"
// (brand rename); its accessible option label is therefore "Fusion Legacy theme". Resolve
// the active option by the value's current label so this stays robust to relabels.
const defaultLabel = THEME_OPTIONS.find((theme) => theme.value === "default")?.label ?? "Fusion Legacy";
expect(screen.getByLabelText(`${defaultLabel} theme`).getAttribute("aria-pressed")).toBe("true");
});
it("renders every shared swatch class from themeOptions", () => {

View File

@@ -200,7 +200,7 @@ describe("Board mobile initial render stabilization (FN-4574)", () => {
},
});
render(<Board {...boardProps} />);
const { unmount } = render(<Board {...boardProps} />);
/*
* FNXC:MobileBoard 2026-06-25-11:24:
@@ -216,6 +216,9 @@ describe("Board mobile initial render stabilization (FN-4574)", () => {
});
}).not.toThrow();
// Exercises the Android seam: cleanup must not throw without removeEventListener.
expect(() => unmount()).not.toThrow();
viewportSpy.mockRestore();
});

View File

@@ -273,7 +273,11 @@ describe("CommandCenterControls", () => {
);
await flushPromises();
fireEvent.click(screen.getByRole("button", { name: /default/i }));
// FNXC:Theme 2026-06-25-13:40:
// The historical "default" colorTheme id is now surfaced as "Fusion Legacy"
// (Ocean is the new default label, see themeOptions). The trigger button name
// therefore reads "Fusion Legacy" for colorTheme="default".
fireEvent.click(screen.getByRole("button", { name: /fusion legacy/i }));
fireEvent.click(screen.getAllByRole("option").find((element) => element.textContent?.trim() === "Forest")!);
expect(onColorThemeChange).toHaveBeenCalledWith("forest");

View File

@@ -1,24 +1,24 @@
.model-pricing-section {
display: flex;
flex-direction: column;
gap: var(--spacing-md, var(--space-md));
gap: var(--space-md);
}
.model-pricing-section__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing-md, var(--space-md));
gap: var(--space-md);
}
.model-pricing-section__meta {
margin: var(--spacing-xs, var(--space-xs)) 0 0;
margin: var(--space-xs) 0 0;
}
.model-pricing-table {
display: flex;
flex-direction: column;
gap: var(--spacing-xs, var(--space-xs));
gap: var(--space-xs);
overflow-x: auto;
}
@@ -32,7 +32,7 @@
minmax(7rem, 1fr)
minmax(10rem, 1.2fr)
minmax(5rem, auto);
gap: var(--spacing-xs, var(--space-xs));
gap: var(--space-xs);
align-items: center;
}
@@ -43,7 +43,7 @@
}
.model-pricing-row--add {
padding-top: var(--spacing-xs, var(--space-xs));
padding-top: var(--space-xs);
border-top: thin solid var(--border);
}
@@ -67,7 +67,7 @@
.model-pricing-row {
grid-template-columns: minmax(14rem, 1fr);
padding: var(--spacing-sm, var(--space-sm));
padding: var(--space-sm);
border: thin solid var(--border);
border-radius: var(--radius-md);
}

View File

@@ -284,6 +284,8 @@ svg.spinning {
--surface-1: color-mix(in srgb, var(--surface) 97%, var(--text) 3%);
--surface-2: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--border-subtle: color-mix(in srgb, var(--border) 60%, transparent);
/* FNXC:DashboardThemeTokens 2026-06-25-13:15: --border-strong is a higher-contrast border (mixes --text into --border) for surfaces that need a visible outline, e.g. the workflow minimap node stroke. Defined so the var(--border-strong, var(--border)) reference is backed by a real token (token-validity guard). */
--border-strong: color-mix(in srgb, var(--border) 55%, var(--text) 45%);
--bg-secondary: color-mix(in srgb, var(--surface) 70%, var(--card));
--bg-tertiary: color-mix(in srgb, var(--surface) 40%, var(--card));
--border: #30363d;
@@ -528,6 +530,8 @@ svg.spinning {
--surface-1: color-mix(in srgb, var(--surface) 98%, var(--text) 2%);
--surface-2: color-mix(in srgb, var(--surface) 95%, var(--text) 5%);
--border-subtle: color-mix(in srgb, var(--border) 60%, transparent);
/* FNXC:DashboardThemeTokens 2026-06-25-13:15: --border-strong is a higher-contrast border (mixes --text into --border) for surfaces that need a visible outline, e.g. the workflow minimap node stroke. Defined so the var(--border-strong, var(--border)) reference is backed by a real token (token-validity guard). */
--border-strong: color-mix(in srgb, var(--border) 55%, var(--text) 45%);
--bg: #ffffff;
--surface: #f6f8fa;
--card: #ffffff;
@@ -2801,6 +2805,8 @@ Toast text must contrast its status background across every dashboard theme and
color: var(--bg);
}
/* FNXC:ToastTheming 2026-06-25-13:15: shadcn-custom light info toast must use dark --text, not white --bg. Its info background (#0284c7 sky-600) only reaches 4.10 contrast against white but ~4.85 against --text, so enroll it in the light-mode dark-text correction list to satisfy WCAG AA. */
[data-color-theme="shadcn-custom"][data-theme="light"] .toast-info,
[data-color-theme="shadcn"][data-theme="light"] .toast-info,
[data-color-theme="shadcn-green"][data-theme="light"] .toast-success,
[data-color-theme="shadcn-green"][data-theme="light"] .toast-info,

View File

@@ -16,7 +16,10 @@ describe("Command Center pricing documentation contract", () => {
expect(dashboardGuide).toContain("estimated cost");
expect(dashboardGuide).toContain("derived at read time");
expect(dashboardGuide).toContain("it is not persisted");
// FNXC:CommandCenter 2026-06-25-13:30: assertion matches the doc's
// sentence-initial casing ("It is not persisted"); the lowercase form was a
// substring drift, the documented not-persisted semantic is correct as written.
expect(dashboardGuide).toContain("It is not persisted");
expect(dashboardGuide).toContain("prices as of");
expect(dashboardGuide).toContain("low-confidence");
expect(dashboardGuide).toContain("cost unavailable");

View File

@@ -239,11 +239,17 @@ describe("workflow routes (U4)", () => {
it("GET /workflows/:id/optional-steps resolves declared optional steps", async () => {
const builtin = await get("/api/workflows/builtin%3Acoding/optional-steps");
expect(builtin.status).toBe(200);
// FNXC:WorkflowOptionalGroup 2026-06-25-13:25: optional-step metadata is now
// sourced from v2 `optional-group` NODES (see workflow-optional-steps.ts),
// which carry no icon. The retired legacy `ir.optionalSteps` declaration
// supplied `icon: "globe"`; the group node instead yields `description: ""`
// and `phase: "pre-merge"`. Assert the current group-sourced shape.
expect(builtin.body).toEqual([
expect.objectContaining({
templateId: "browser-verification",
name: "Browser Verification",
icon: "globe",
description: "",
phase: "pre-merge",
defaultOn: false,
}),
]);

View File

@@ -49,12 +49,19 @@ function makeStore(opts: {
}
describe("buildBoardWorkflowsPayload", () => {
it("returns flagEnabled:false and empty maps when the flag is OFF", async () => {
// FNXC:WorkflowColumns 2026-06-25-13:20: workflowColumns graduated from the
// experimental flag (isWorkflowColumnsEnabled always returns true; a stale
// persisted/explicit `false` must resolve as enabled). The retired flag-OFF
// empty single-lane shape no longer exists. Assert the graduation invariant:
// even with persisted workflowColumns:false the payload is fully enabled and
// maps the visible card to the default workflow lane. Mirrors the route test
// (board-workflows-route.test.ts, FNXC:WorkflowColumns 2026-06-25-11:40).
it("treats persisted workflowColumns:false as enabled (graduated)", async () => {
const store = makeStore({ flagOn: false, selections: {} });
const payload = await buildBoardWorkflowsPayload(store as never, ["FN-1"]);
expect(payload.flagEnabled).toBe(false);
expect(payload.workflows).toEqual([]);
expect(payload.taskWorkflowIds).toEqual({});
expect(payload.flagEnabled).toBe(true);
expect(payload.workflows.length).toBeGreaterThan(0);
expect(payload.taskWorkflowIds["FN-1"]).toBe(DEFAULT_WORKFLOW_LANE_ID);
});
it("resolves null selections to the default workflow lane", async () => {