test(FN-000): resolve dashboard test noise

This commit is contained in:
Aron Prins
2026-05-05 22:25:29 +02:00
parent 46072542cf
commit a9f2fb65ef
15 changed files with 113 additions and 298 deletions

View File

@@ -61,6 +61,32 @@ jobs:
- name: Typecheck - name: Typecheck
run: pnpm typecheck run: pnpm typecheck
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "24"
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Build
run: pnpm build
test-shards: test-shards:
name: Test shard ${{ matrix.shard }}/3 name: Test shard ${{ matrix.shard }}/3
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -88,8 +114,5 @@ jobs:
- name: Install Bun - name: Install Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Build
run: pnpm build
- name: Test (deterministic shard) - name: Test (deterministic shard)
run: pnpm test:ci:shard --shard ${{ matrix.shard }} --total 3 run: pnpm test:ci:shard --shard ${{ matrix.shard }} --total 3

View File

@@ -10,7 +10,7 @@ why it broke, how it was fixed, and what command verified the fix.
| ID | Area | Symptom | Root cause | Planned fix | Status | | ID | Area | Symptom | Root cause | Planned fix | Status |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| TSH-007 | Dashboard test runtime/noise | `pnpm test:full` passes but dashboard tests take about 10 minutes locally and emit repeated SQLite experimental warnings plus git default-branch hints. | The suite exercises many SQLite-backed and git-backed paths; Node emits SQLite warnings globally and fixture git repos use default branch initialization. | Consider a follow-up cleanup to quiet expected warnings and split expensive dashboard lanes without reducing coverage. | Open | | _None_ | | | | | |
## Fixed ## Fixed
@@ -23,3 +23,8 @@ why it broke, how it was fixed, and what command verified the fix.
| TSH-005 | Plugin changed-test targeting | Plugin edits still forced the entire suite after package-aware targeting was added. | `shouldForceFullSuite()` treated every `plugins/**` path as broad repo surface area. | Plugin workspace paths are now resolved like package paths, so plugin edits run the relevant plugin tests instead of automatically falling back to all tests. | `node --test scripts/__tests__/test-changed.test.mjs` | | TSH-005 | Plugin changed-test targeting | Plugin edits still forced the entire suite after package-aware targeting was added. | `shouldForceFullSuite()` treated every `plugins/**` path as broad repo surface area. | Plugin workspace paths are now resolved like package paths, so plugin edits run the relevant plugin tests instead of automatically falling back to all tests. | `node --test scripts/__tests__/test-changed.test.mjs` |
| TSH-006 | Skipped-test inventory | The skipped-test inventory documented the wrong gate ownership for the new and legacy extension suites. | The maintained extension integration lane and legacy exhaustive gate were renamed during implementation, but the inventory text still reflected the earlier split. | `docs/skipped-test-inventory.md` now lists `FUSION_TEST_EXTENSION_INTEGRATION` for the maintained built-extension test and `FUSION_TEST_LEGACY_EXTENSION_INTEGRATION` for the legacy suite. | `git diff --check`, `node --test scripts/__tests__/test-governance.test.mjs`, and `pnpm test:full` | | TSH-006 | Skipped-test inventory | The skipped-test inventory documented the wrong gate ownership for the new and legacy extension suites. | The maintained extension integration lane and legacy exhaustive gate were renamed during implementation, but the inventory text still reflected the earlier split. | `docs/skipped-test-inventory.md` now lists `FUSION_TEST_EXTENSION_INTEGRATION` for the maintained built-extension test and `FUSION_TEST_LEGACY_EXTENSION_INTEGRATION` for the legacy suite. | `git diff --check`, `node --test scripts/__tests__/test-governance.test.mjs`, and `pnpm test:full` |
| TSH-008 | Test isolation | `custom-providers.test.ts` wrote to the real `~/.fusion/settings.json` path during the first local full-suite run. | The test used `os.homedir()` directly instead of a fixture home directory. | `readCustomProviders()` now accepts an injectable home directory, and the test uses a temp HOME fixture that is removed after each case. | `pnpm --filter @fusion/engine test -- src/__tests__/custom-providers.test.ts` (ran the full engine lane: 108 files, 3339 tests) | | TSH-008 | Test isolation | `custom-providers.test.ts` wrote to the real `~/.fusion/settings.json` path during the first local full-suite run. | The test used `os.homedir()` directly instead of a fixture home directory. | `readCustomProviders()` now accepts an injectable home directory, and the test uses a temp HOME fixture that is removed after each case. | `pnpm --filter @fusion/engine test -- src/__tests__/custom-providers.test.ts` (ran the full engine lane: 108 files, 3339 tests) |
| TSH-007 | Dashboard runtime/noise | `pnpm test:full` passed but dashboard tests took about 10 minutes locally and emitted repeated SQLite experimental warnings plus git default-branch hints. | Dashboard tests mix real SQLite-backed route coverage, git-backed route fixtures, and jsdom UI suites in one package lane. Node emits `node:sqlite` experimental warnings globally, some git fixtures used plain `git init`, and dashboard tests used deprecated `environmentMatchGlobs`. | Dashboard tests now use explicit Vitest projects for `dashboard-app` and `dashboard-api`, package scripts expose `test:app`/`test:api`/`test:browser-smoke` lanes, the shared Vitest setup filters known SQLite experimental warnings, fixture repos initialize with `main`, and low-value agent CSS class-presence assertions were cut in favor of focused layout/token/no-inline-style contracts. | `pnpm --filter @fusion/dashboard test:app` (287 files, 7254 tests), `pnpm --filter @fusion/dashboard test:api` (132 files, 3493 tests), `pnpm --filter @fusion/core exec vitest run src/__tests__/central-db.test.ts --silent=passed-only --reporter=dot`, and `pnpm test:full` |
| TSH-009 | PR action minutes | PR test shards built the whole workspace before running tests, duplicating the separate build gate. | `pr-checks.yml` had a `pnpm build` step inside every test-shard job. | Removed the pre-test build from PR test shards; package tests that require artifacts build their own fixtures, while `build` remains an explicit PR workflow job. | `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/ci-workflow.test.ts --silent=passed-only --reporter=dot` and `pnpm build` |
| TSH-010 | Dashboard lane scripts | `pnpm --filter @fusion/dashboard test:api` failed with "No test files found" after converting dashboard to Vitest projects. | Positional glob filters were evaluated against all projects instead of selecting the intended dashboard project. | Dashboard lane scripts now use `--project dashboard-app` and `--project dashboard-api`. | `pnpm --filter @fusion/dashboard test:api` and `pnpm --filter @fusion/dashboard exec vitest run --project dashboard-app app/__tests__/agent-css-classes.test.ts --silent=passed-only --reporter=dot` |
| TSH-011 | PR build coverage | While reducing PR shard minutes, the workflow temporarily had no standalone `pnpm build` job. | The old workflow only had lint, typecheck, and test shards, so removing the shard-local build also removed PR build coverage. | Added an explicit `build` job to `pr-checks.yml` and updated the workflow contract test to require it. | `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/ci-workflow.test.ts --silent=passed-only --reporter=dot` and `pnpm build` |
| TSH-012 | Warning filtering | The first SQLite warning filter also installed a `process.on("warning")` listener that could duplicate unrelated warnings. | Node still prints non-filtered warnings by default, so manually writing them again made future warning output noisier. | The shared setup now only wraps `process.emitWarning` for the known SQLite experimental warning, and the dashboard noisy-output marker list no longer suppresses generic trace-warning guidance. | `pnpm --filter @fusion/core typecheck`, `pnpm --filter @fusion/core exec vitest run src/__tests__/central-db.test.ts --silent=passed-only --reporter=dot`, and `pnpm test:full` |

View File

@@ -71,7 +71,7 @@ GitHub Actions now runs deterministic test sharding via `pnpm test:ci:shard --sh
- `pnpm test:full` remains the canonical full local suite. - `pnpm test:full` remains the canonical full local suite.
- `pnpm verify:workspace` remains the canonical local lint -> test -> build gate. - `pnpm verify:workspace` remains the canonical local lint -> test -> build gate.
`test:ci:shard` is a CI-focused entrypoint (`scripts/ci-test-shard.mjs`) that partitions a fixed package list by shard index modulo total shard count so coverage is deterministic and reproducible. `test:ci:shard` is a CI-focused entrypoint (`scripts/ci-test-shard.mjs`) that partitions workspace packages with `test` scripts by shard index modulo total shard count so coverage is deterministic and reproducible.
`pnpm test` now uses a changed-only entrypoint (`scripts/test-changed.mjs`) for faster local iteration. It resolves the comparison base from `.changeset/config.json` (`baseBranch`) and runs only affected workspaces from `pnpm-workspace.yaml` (both `packages/*` and `plugins/**`) using safe package-first filtering (`pnpm --filter <pkg> test`). It automatically falls back to the full suite when the run is forced (CI / `--full`), the git comparison base or diff cannot be resolved, no changes are detected, shared/root test infrastructure changes, or changed workspace paths cannot be resolved to a workspace package (fail-safe coverage behavior). `pnpm test` now uses a changed-only entrypoint (`scripts/test-changed.mjs`) for faster local iteration. It resolves the comparison base from `.changeset/config.json` (`baseBranch`) and runs only affected workspaces from `pnpm-workspace.yaml` (both `packages/*` and `plugins/**`) using safe package-first filtering (`pnpm --filter <pkg> test`). It automatically falls back to the full suite when the run is forced (CI / `--full`), the git comparison base or diff cannot be resolved, no changes are detected, shared/root test infrastructure changes, or changed workspace paths cannot be resolved to a workspace package (fail-safe coverage behavior).
@@ -159,6 +159,19 @@ pnpm --filter @runfusion/fusion test:extension-integration
`test:extension-integration` enables `FUSION_TEST_EXTENSION_INTEGRATION=1` and runs the full fn pi extension integration suite. It remains an explicit opt-in lane so default workspace verification stays fast, while still providing a discoverable command for full extension-tool integration coverage. `test:extension-integration` enables `FUSION_TEST_EXTENSION_INTEGRATION=1` and runs the full fn pi extension integration suite. It remains an explicit opt-in lane so default workspace verification stays fast, while still providing a discoverable command for full extension-tool integration coverage.
## Dashboard Test Lanes
Dashboard tests are split into explicit local lanes. The default dashboard package gate runs the app/API tests; the built-client contract remains a separate lane because it performs its own production build:
```bash
pnpm --filter @fusion/dashboard test:app # React/jsdom app tests
pnpm --filter @fusion/dashboard test:api # Node API/server tests
pnpm --filter @fusion/dashboard test:browser-smoke # local browser layout smoke
pnpm --filter @fusion/dashboard test:build # built client output contract
```
Use these lanes for local iteration before PRs. The full workspace test gate runs the default dashboard app/API suite, `pnpm build` remains an explicit PR gate, and PR test shards avoid a redundant pre-test workspace build to save GitHub Actions minutes.
## Release Process ## Release Process
Fusion uses Changesets + version PR workflow. Fusion uses Changesets + version PR workflow.

View File

@@ -171,11 +171,22 @@ describe("PR checks workflow (.github/workflows/pr-checks.yml)", () => {
it("uses the same deterministic test sharding command as manual CI", () => { it("uses the same deterministic test sharding command as manual CI", () => {
expect(workflow.jobs?.lint).toBeDefined(); expect(workflow.jobs?.lint).toBeDefined();
expect(workflow.jobs?.typecheck).toBeDefined(); expect(workflow.jobs?.typecheck).toBeDefined();
expect(workflow.jobs?.build).toBeDefined();
expect(workflow.jobs?.["test-shards"]).toBeDefined(); expect(workflow.jobs?.["test-shards"]).toBeDefined();
expect(workflow.jobs?.["test-shards"]?.strategy?.matrix?.shard).toEqual([1, 2, 3]); expect(workflow.jobs?.["test-shards"]?.strategy?.matrix?.shard).toEqual([1, 2, 3]);
expect(content).toContain("pnpm test:ci:shard --shard ${{ matrix.shard }} --total 3"); expect(content).toContain("pnpm test:ci:shard --shard ${{ matrix.shard }} --total 3");
expect(content).not.toContain("run: pnpm test\n"); expect(content).not.toContain("run: pnpm test\n");
}); });
it("keeps build coverage as an explicit PR gate", () => {
const buildSteps = workflow.jobs?.build?.steps ?? [];
expect(buildSteps.some((step: any) => step.name === "Build" && step.run === "pnpm build")).toBe(true);
});
it("does not spend PR action minutes on a pre-test workspace build", () => {
const testSteps = workflow.jobs?.["test-shards"]?.steps ?? [];
expect(testSteps.some((step: any) => step.name === "Build" || step.run === "pnpm build")).toBe(false);
});
}); });
describe("Version & Release workflow (.github/workflows/version.yml)", () => { describe("Version & Release workflow (.github/workflows/version.yml)", () => {

View File

@@ -38,6 +38,24 @@ const fsPromises = requireFromHere("node:fs/promises") as FsPromisesModule;
const childProcess = requireFromHere("node:child_process") as ChildProcessModule; const childProcess = requireFromHere("node:child_process") as ChildProcessModule;
const { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } = fs; const { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } = fs;
function installWarningFilter(): void {
const warningState = globalThis as typeof globalThis & { __fusionTestWarningFilterInstalled?: boolean };
if (warningState.__fusionTestWarningFilterInstalled) return;
warningState.__fusionTestWarningFilterInstalled = true;
const originalEmitWarning = process.emitWarning.bind(process);
process.emitWarning = ((warning: string | Error, ...args: unknown[]) => {
const warningText = warning instanceof Error ? warning.message : warning;
const warningType = typeof args[0] === "string" ? args[0] : undefined;
if (warningType === "ExperimentalWarning" && warningText.includes("SQLite is an experimental feature")) {
return;
}
return originalEmitWarning(warning as string, ...(args as Parameters<typeof process.emitWarning> extends [any, ...infer Rest] ? Rest : never));
}) as typeof process.emitWarning;
}
installWarningFilter();
const TEST_HOME_PREFIX = "fn-test-home-"; const TEST_HOME_PREFIX = "fn-test-home-";
const DEFAULT_TEST_SUBPROCESS_TIMEOUT_MS = Math.max( const DEFAULT_TEST_SUBPROCESS_TIMEOUT_MS = Math.max(
1_000, 1_000,

View File

@@ -25,12 +25,6 @@ function hasClass(cls: string): boolean {
return new RegExp(`${escaped}(?=[\\s,{:.#>+~])`).test(stylesContent); return new RegExp(`${escaped}(?=[\\s,{:.#>+~])`).test(stylesContent);
} }
function extractSection(startMarker: string, endMarker: string): string {
const start = stylesContent.indexOf(startMarker);
const end = stylesContent.indexOf(endMarker, start + startMarker.length);
return start >= 0 && end >= 0 ? stylesContent.slice(start, end) : "";
}
describe("Agent CSS classes", () => { describe("Agent CSS classes", () => {
// Verify agent state CSS variables are defined in the global stylesheet // Verify agent state CSS variables are defined in the global stylesheet
it("should define --state-* CSS variables", () => { it("should define --state-* CSS variables", () => {
@@ -48,109 +42,6 @@ describe("Agent CSS classes", () => {
expect(stylesContent).toContain("--state-error-border:"); expect(stylesContent).toContain("--state-error-border:");
}); });
// Verify BEM button modifier classes exist
it("should define BEM button modifier classes", () => {
expect(hasClass(".btn--sm")).toBe(true);
expect(hasClass(".btn--primary")).toBe(true);
expect(hasClass(".btn--danger")).toBe(true);
expect(hasClass(".btn--warning")).toBe(true);
expect(hasClass(".btn--compact")).toBe(true);
});
// Verify badge base class
it("should define .badge base class", () => {
expect(hasClass(".badge")).toBe(true);
});
// Verify AgentMetricsBar classes
it("should define AgentMetricsBar CSS classes", () => {
expect(hasClass(".agent-metrics-bar")).toBe(true);
expect(hasClass(".agent-metric-card")).toBe(true);
expect(hasClass(".agent-metric-card--active")).toBe(true);
expect(hasClass(".agent-metric-card--tasks")).toBe(true);
expect(hasClass(".agent-metric-card--success")).toBe(true);
expect(hasClass(".agent-metric-card--runs")).toBe(true);
expect(hasClass(".agent-metric-info")).toBe(true);
expect(hasClass(".agent-metric-value")).toBe(true);
expect(hasClass(".agent-metric-label")).toBe(true);
});
// Verify AgentsView classes
it("should define AgentsView CSS classes", () => {
expect(hasClass(".agents-view")).toBe(true);
expect(hasClass(".agents-view-header")).toBe(true);
expect(hasClass(".agents-view-title")).toBe(true);
expect(hasClass(".agents-view-controls")).toBe(true);
expect(hasClass(".agents-view-primary-actions")).toBe(true);
expect(hasClass(".agents-view-content")).toBe(true);
expect(hasClass(".agents-overview-bar")).toBe(true);
expect(hasClass(".agents-overview-bar__toggle")).toBe(true);
expect(hasClass(".agents-overview-bar__content")).toBe(true);
expect(hasClass(".agent-controls-trigger")).toBe(true);
expect(hasClass(".agent-controls-trigger--active")).toBe(true);
expect(hasClass(".agent-controls-panel")).toBe(true);
expect(hasClass(".agent-controls")).toBe(true);
expect(hasClass(".agent-controls-filters")).toBe(true);
expect(hasClass(".agent-state-filter")).toBe(true);
expect(hasClass(".agent-state-filter-select")).toBe(true);
expect(hasClass(".agent-system-filter")).toBe(true);
expect(hasClass(".agent-controls-actions")).toBe(true);
expect(hasClass(".agent-global-controls")).toBe(true);
expect(hasClass(".agent-org-chart-shell")).toBe(true);
expect(hasClass(".agent-org-chart-controls")).toBe(true);
expect(hasClass(".agent-org-chart-viewport")).toBe(true);
expect(hasClass(".agent-org-chart-canvas")).toBe(true);
expect(hasClass(".agent-org-chart-canvas--zoom-75")).toBe(true);
expect(hasClass(".agent-org-chart-canvas--zoom-100")).toBe(true);
expect(hasClass(".agent-org-chart-canvas--zoom-125")).toBe(true);
expect(hasClass(".agent-org-chart-canvas--zoom-150")).toBe(true);
expect(hasClass(".agent-org-chart--vertical")).toBe(true);
expect(hasClass(".agent-board")).toBe(true);
expect(hasClass(".agent-board-card")).toBe(true);
expect(hasClass(".agent-board-card--idle")).toBe(true);
expect(hasClass(".agent-board-card--active")).toBe(true);
expect(hasClass(".agent-board-card--running")).toBe(true);
expect(hasClass(".agent-board-card--paused")).toBe(true);
expect(hasClass(".agent-board-card--error")).toBe(true);
expect(hasClass(".agent-board-header")).toBe(true);
expect(hasClass(".agent-board-icon")).toBe(true);
expect(hasClass(".agent-board-badge")).toBe(true);
expect(hasClass(".agent-badge--idle")).toBe(true);
expect(hasClass(".agent-badge--active")).toBe(true);
expect(hasClass(".agent-badge--running")).toBe(true);
expect(hasClass(".agent-badge--paused")).toBe(true);
expect(hasClass(".agent-badge--error")).toBe(true);
expect(hasClass(".agent-board-health")).toBe(true);
expect(hasClass(".agent-board-name")).toBe(true);
expect(hasClass(".agent-board-id")).toBe(true);
expect(hasClass(".agent-board-clickable")).toBe(true);
expect(hasClass(".agent-board-actions")).toBe(true);
expect(hasClass(".agent-list")).toBe(true);
expect(hasClass(".agent-card")).toBe(true);
expect(hasClass(".agent-card--idle")).toBe(true);
expect(hasClass(".agent-card--active")).toBe(true);
expect(hasClass(".agent-card--running")).toBe(true);
expect(hasClass(".agent-card--paused")).toBe(true);
expect(hasClass(".agent-card--error")).toBe(true);
expect(hasClass(".agent-card-header")).toBe(true);
expect(hasClass(".agent-card-body")).toBe(true);
expect(hasClass(".agent-card-actions")).toBe(true);
expect(hasClass(".agent-info")).toBe(true);
expect(hasClass(".agent-info--clickable")).toBe(true);
expect(hasClass(".agent-icon")).toBe(true);
expect(hasClass(".agent-icon--clickable")).toBe(true);
expect(hasClass(".agent-meta")).toBe(true);
expect(hasClass(".agent-name")).toBe(true);
expect(hasClass(".agent-id")).toBe(true);
expect(hasClass(".agent-badges")).toBe(true);
expect(hasClass(".agent-card-chevron")).toBe(true);
expect(hasClass(".agent-task")).toBe(true);
expect(hasClass(".agent-heartbeat")).toBe(true);
expect(hasClass(".agent-role-select")).toBe(true);
expect(hasClass(".agent-empty")).toBe(true);
expect(hasClass(".spin")).toBe(true);
});
it("keeps split layout as the scroll-constrained pane container", () => { it("keeps split layout as the scroll-constrained pane container", () => {
const splitLayout = extractRuleBlock(".agents-split-layout"); const splitLayout = extractRuleBlock(".agents-split-layout");
expect(splitLayout).toContain("flex: 1"); expect(splitLayout).toContain("flex: 1");
@@ -218,7 +109,6 @@ describe("Agent CSS classes", () => {
expect(orgChartSection).not.toMatch(/1\.5rem|0\.75rem|0\.72rem|0\.78rem|0\.65rem|120ms\s+ease|10px/); expect(orgChartSection).not.toMatch(/1\.5rem|0\.75rem|0\.72rem|0\.78rem|0\.65rem|120ms\s+ease|10px/);
}); });
// Verify AgentDetailView classes
it("encodes compact mobile agent detail header layout contracts", () => { it("encodes compact mobile agent detail header layout contracts", () => {
const css = fs.readFileSync(path.join(__dirname, "../components/AgentDetailView.css"), "utf-8"); const css = fs.readFileSync(path.join(__dirname, "../components/AgentDetailView.css"), "utf-8");
const mobileStart = css.indexOf("@media (max-width: 768px)"); const mobileStart = css.indexOf("@media (max-width: 768px)");
@@ -232,117 +122,6 @@ describe("Agent CSS classes", () => {
expect(mobileCss).toContain(".agent-detail-mobile-icon-control .agent-detail-control-label {"); expect(mobileCss).toContain(".agent-detail-mobile-icon-control .agent-detail-control-label {");
}); });
it("should define AgentDetailView CSS classes", () => {
expect(hasClass(".agent-detail-overlay")).toBe(true);
expect(hasClass(".agent-detail-modal")).toBe(true);
expect(hasClass(".agent-detail-loading")).toBe(true);
expect(hasClass(".agent-detail-header")).toBe(true);
expect(hasClass(".agent-detail-title")).toBe(true);
expect(hasClass(".agent-detail-icon")).toBe(true);
expect(hasClass(".agent-detail-info")).toBe(true);
expect(hasClass(".agent-detail-badges")).toBe(true);
expect(hasClass(".agent-detail-actions")).toBe(true);
// Redesigned compact header structure
expect(hasClass(".agent-detail-identity")).toBe(true);
expect(hasClass(".agent-detail-inline-back")).toBe(true);
expect(hasClass(".agent-detail-controls")).toBe(true);
expect(hasClass(".agent-detail-utility-actions")).toBe(true);
expect(hasClass(".agent-detail-tabs")).toBe(true);
expect(hasClass(".agent-detail-tab")).toBe(true);
expect(hasClass(".agent-detail-content")).toBe(true);
expect(hasClass(".agent-detail-footer")).toBe(true);
expect(hasClass(".agent-detail-id")).toBe(true);
expect(hasClass(".dashboard-tab")).toBe(true);
expect(hasClass(".dashboard-section")).toBe(true);
expect(hasClass(".info-grid")).toBe(true);
expect(hasClass(".info-item")).toBe(true);
expect(hasClass(".info-label")).toBe(true);
expect(hasClass(".info-value")).toBe(true);
expect(hasClass(".inline-badge")).toBe(true);
expect(hasClass(".stats-grid")).toBe(true);
expect(hasClass(".stat-card")).toBe(true);
expect(hasClass(".stat-value")).toBe(true);
expect(hasClass(".stat-label")).toBe(true);
expect(hasClass(".current-task")).toBe(true);
expect(hasClass(".task-badge")).toBe(true);
expect(hasClass(".metadata-json")).toBe(true);
expect(hasClass(".logs-tab")).toBe(true);
expect(hasClass(".logs-header")).toBe(true);
expect(hasClass(".logs-count")).toBe(true);
expect(hasClass(".streaming-indicator")).toBe(true);
expect(hasClass(".streaming-dot")).toBe(true);
expect(hasClass(".logs-empty")).toBe(true);
expect(hasClass(".runs-tab")).toBe(true);
expect(hasClass(".runs-empty")).toBe(true);
expect(hasClass(".run-card")).toBe(true);
expect(hasClass(".run-card--active")).toBe(true);
expect(hasClass(".run-header")).toBe(true);
expect(hasClass(".run-live-indicator")).toBe(true);
expect(hasClass(".live-dot")).toBe(true);
expect(hasClass(".run-id")).toBe(true);
expect(hasClass(".run-status")).toBe(true);
expect(hasClass(".run-details")).toBe(true);
expect(hasClass(".config-tab")).toBe(true);
expect(hasClass(".config-section")).toBe(true);
expect(hasClass(".config-description")).toBe(true);
expect(hasClass(".config-fields")).toBe(true);
expect(hasClass(".config-field")).toBe(true);
expect(hasClass(".config-hint")).toBe(true);
expect(hasClass(".config-error")).toBe(true);
expect(hasClass(".config-actions")).toBe(true);
expect(hasClass(".config-saved-indicator")).toBe(true);
expect(hasClass(".input--error")).toBe(true);
});
it("should define AgentReflectionsTab and ratings CSS classes", () => {
expect(hasClass(".reflections-tab")).toBe(true);
expect(hasClass(".reflections-header")).toBe(true);
expect(hasClass(".reflections-stats-grid")).toBe(true);
expect(hasClass(".reflections-stat-card")).toBe(true);
expect(hasClass(".reflections-no-data")).toBe(true);
expect(hasClass(".reflections-loading-indicator")).toBe(true);
expect(hasClass(".reflections-ratings-section")).toBe(true);
expect(hasClass(".reflections-list")).toBe(true);
expect(hasClass(".reflection-cards")).toBe(true);
expect(hasClass(".reflection-card")).toBe(true);
expect(hasClass(".reflection-card--expanded")).toBe(true);
expect(hasClass(".reflection-card-header")).toBe(true);
expect(hasClass(".reflection-trigger-badge")).toBe(true);
expect(hasClass(".reflection-summary")).toBe(true);
expect(hasClass(".reflection-details")).toBe(true);
expect(hasClass(".reflection-empty")).toBe(true);
expect(hasClass(".rating-summary-card")).toBe(true);
expect(hasClass(".rating-score-display")).toBe(true);
expect(hasClass(".rating-average")).toBe(true);
expect(hasClass(".rating-stats")).toBe(true);
expect(hasClass(".rating-count")).toBe(true);
expect(hasClass(".rating-trend-badge")).toBe(true);
expect(hasClass(".trend-improving")).toBe(true);
expect(hasClass(".trend-declining")).toBe(true);
expect(hasClass(".trend-stable")).toBe(true);
expect(hasClass(".trend-insufficient")).toBe(true);
expect(hasClass(".category-breakdown")).toBe(true);
expect(hasClass(".category-item")).toBe(true);
expect(hasClass(".category-name")).toBe(true);
expect(hasClass(".category-score")).toBe(true);
expect(hasClass(".add-rating-form")).toBe(true);
expect(hasClass(".add-rating-category-select")).toBe(true);
expect(hasClass(".add-rating-comment-input")).toBe(true);
expect(hasClass(".star-selector")).toBe(true);
expect(hasClass(".star-btn")).toBe(true);
expect(hasClass(".rating-stars")).toBe(true);
expect(hasClass(".star-filled")).toBe(true);
expect(hasClass(".star-empty")).toBe(true);
expect(hasClass(".rating-history")).toBe(true);
expect(hasClass(".rating-history-item")).toBe(true);
expect(hasClass(".rating-item-header")).toBe(true);
expect(hasClass(".rating-category-badge")).toBe(true);
expect(hasClass(".rating-time")).toBe(true);
expect(hasClass(".rating-delete-btn")).toBe(true);
expect(hasClass(".rating-comment")).toBe(true);
});
it("should apply accessible focus/hover styles for merged evaluation cards and actions", () => { it("should apply accessible focus/hover styles for merged evaluation cards and actions", () => {
expect(stylesContent).toContain(".star-btn:focus-visible"); expect(stylesContent).toContain(".star-btn:focus-visible");
expect(extractRuleBlock(".star-btn:focus-visible")).toContain("box-shadow: var(--focus-ring-strong)"); expect(extractRuleBlock(".star-btn:focus-visible")).toContain("box-shadow: var(--focus-ring-strong)");
@@ -358,49 +137,6 @@ describe("Agent CSS classes", () => {
expect(stylesContent).toContain("flex-wrap: wrap"); expect(stylesContent).toContain("flex-wrap: wrap");
}); });
// Verify ActiveAgentsPanel classes
it("should define ActiveAgentsPanel CSS classes", () => {
expect(hasClass(".active-agents-panel")).toBe(true);
expect(hasClass(".active-agents-panel-header")).toBe(true);
expect(hasClass(".active-agents-grid")).toBe(true);
expect(hasClass(".live-agent-card")).toBe(true);
expect(hasClass(".live-agent-card-header")).toBe(true);
expect(hasClass(".live-agent-card-name")).toBe(true);
expect(hasClass(".status-dot")).toBe(true);
expect(hasClass(".live-agent-task")).toBe(true);
expect(hasClass(".live-agent-card-transcript")).toBe(true);
expect(hasClass(".live-agent-card-empty")).toBe(true);
expect(hasClass(".live-agent-card-line")).toBe(true);
expect(hasClass(".live-agent-card-footer")).toBe(true);
expect(hasClass(".live-agent-streaming-dot")).toBe(true);
});
// Verify NewAgentDialog classes
it("should define NewAgentDialog CSS classes", () => {
expect(hasClass(".agent-dialog-overlay")).toBe(true);
expect(hasClass(".agent-dialog")).toBe(true);
expect(hasClass(".agent-dialog-header")).toBe(true);
expect(hasClass(".agent-dialog-header-title")).toBe(true);
expect(hasClass(".agent-dialog-body")).toBe(true);
expect(hasClass(".agent-dialog-footer")).toBe(true);
expect(hasClass(".agent-dialog-steps")).toBe(true);
expect(hasClass(".agent-dialog-step")).toBe(true);
expect(hasClass(".agent-dialog-field")).toBe(true);
expect(hasClass(".agent-role-grid")).toBe(true);
expect(hasClass(".agent-role-option")).toBe(true);
expect(hasClass(".agent-role-option-icon")).toBe(true);
expect(hasClass(".agent-role-option-label")).toBe(true);
expect(hasClass(".agent-dialog-summary")).toBe(true);
expect(hasClass(".agent-dialog-summary-row")).toBe(true);
expect(hasClass(".agent-dialog-summary-row-label")).toBe(true);
expect(hasClass(".agent-dialog-summary-row-value")).toBe(true);
expect(hasClass(".agent-dialog-required")).toBe(true);
expect(hasClass(".agent-dialog-optional")).toBe(true);
expect(hasClass(".agent-dialog-error")).toBe(true);
expect(hasClass(".agent-dialog-info")).toBe(true);
expect(hasClass(".agent-dialog-loading")).toBe(true);
});
it("should give role option buttons a tokenized focus-visible state", () => { it("should give role option buttons a tokenized focus-visible state", () => {
expect(stylesContent).toContain(".agent-role-option:focus-visible"); expect(stylesContent).toContain(".agent-role-option:focus-visible");
const roleFocusBlock = extractRuleBlock(".agent-role-option:focus-visible"); const roleFocusBlock = extractRuleBlock(".agent-role-option:focus-visible");
@@ -408,10 +144,7 @@ describe("Agent CSS classes", () => {
expect(roleFocusBlock).toContain("box-shadow: var(--focus-ring-strong)"); expect(roleFocusBlock).toContain("box-shadow: var(--focus-ring-strong)");
}); });
it("should define shared AgentEmptyState component primitives", () => { it("should keep the create-agent empty-state action copy", () => {
expect(hasClass(".agent-empty-state__icon")).toBe(true);
expect(hasClass(".agent-empty-state__title")).toBe(true);
expect(hasClass(".agent-empty-state__description")).toBe(true);
expect(agentEmptyStateContent).toContain("Create Agent"); expect(agentEmptyStateContent).toContain("Create Agent");
}); });

View File

@@ -6,6 +6,7 @@ const COMPONENTS_DIR = join(APP_DIR, "components");
let cached: string | null = null; let cached: string | null = null;
let stylesCached: string | null = null; let stylesCached: string | null = null;
let baseOnlyCached: string | null = null;
export function loadStylesCss(): string { export function loadStylesCss(): string {
if (stylesCached !== null) return stylesCached; if (stylesCached !== null) return stylesCached;
@@ -40,6 +41,7 @@ export function loadAllAppCss(): string {
* against @media overrides that happen to come earlier in the source order. * against @media overrides that happen to come earlier in the source order.
*/ */
export function loadAllAppCssBaseOnly(): string { export function loadAllAppCssBaseOnly(): string {
if (baseOnlyCached !== null) return baseOnlyCached;
const src = loadAllAppCss(); const src = loadAllAppCss();
// Walk and excise any top-level @<rule> { ... } block (e.g. @media, @supports) // Walk and excise any top-level @<rule> { ... } block (e.g. @media, @supports)
let out = ""; let out = "";
@@ -60,5 +62,6 @@ export function loadAllAppCssBaseOnly(): string {
} }
out += src[i++]; out += src[i++];
} }
return out; baseOnlyCached = out;
return baseOnlyCached;
} }

View File

@@ -37,6 +37,8 @@
"dev": "pnpm build && pnpm typecheck && pnpm dev:serve", "dev": "pnpm build && pnpm typecheck && pnpm dev:serve",
"dev:serve": "vite dev", "dev:serve": "vite dev",
"test": "vitest run --silent=passed-only --reporter=dot --exclude '**/build-output.test.ts'", "test": "vitest run --silent=passed-only --reporter=dot --exclude '**/build-output.test.ts'",
"test:app": "vitest run --project dashboard-app --silent=passed-only --reporter=dot --exclude '**/build-output.test.ts'",
"test:api": "vitest run --project dashboard-api --silent=passed-only --reporter=dot",
"test:browser-smoke": "node scripts/browser-layout-smoke.mjs", "test:browser-smoke": "node scripts/browser-layout-smoke.mjs",
"test:build": "vitest run --silent=passed-only --reporter=dot app/__tests__/build-output.test.ts", "test:build": "vitest run --silent=passed-only --reporter=dot app/__tests__/build-output.test.ts",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json" "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json"

View File

@@ -42,7 +42,7 @@ describe("Session files endpoint", () => {
mkdirSync(testWorktree, { recursive: true }); mkdirSync(testWorktree, { recursive: true });
// Initialize git repo // Initialize git repo
execSync("git init", { cwd: testWorktree }); execSync("git init --initial-branch=main", { cwd: testWorktree });
execSync("git config user.email test@test.com", { cwd: testWorktree }); execSync("git config user.email test@test.com", { cwd: testWorktree });
execSync("git config user.name Test", { cwd: testWorktree }); execSync("git config user.name Test", { cwd: testWorktree });

View File

@@ -716,7 +716,7 @@ describe("POST /api/projects route handler", () => {
const cloneDestination = join(tempRoot, "cloned-project"); const cloneDestination = join(tempRoot, "cloned-project");
try { try {
execFileSync("git", ["init", "--bare", bareRepo]); execFileSync("git", ["init", "--bare", "--initial-branch=main", bareRepo]);
const res = await request( const res = await request(
app, app,

View File

@@ -307,8 +307,8 @@ function getSharedGitTestRepo(): GitTestRepo {
const repoDir = join(root, "repo"); const repoDir = join(root, "repo");
mkdirSync(repoDir, { recursive: true }); mkdirSync(repoDir, { recursive: true });
execFileSync("git", ["init", "--bare", remoteDir], { stdio: "pipe" }); execFileSync("git", ["init", "--bare", "--initial-branch=main", remoteDir], { stdio: "pipe" });
execFileSync("git", ["init", repoDir], { stdio: "pipe" }); execFileSync("git", ["init", "--initial-branch=main", repoDir], { stdio: "pipe" });
execFileSync("git", ["-C", repoDir, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" }); execFileSync("git", ["-C", repoDir, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" });
execFileSync("git", ["-C", repoDir, "config", "user.name", "KB Tests"], { stdio: "pipe" }); execFileSync("git", ["-C", repoDir, "config", "user.name", "KB Tests"], { stdio: "pipe" });
writeFileSync(join(repoDir, "README.md"), "# Test Repo\n"); writeFileSync(join(repoDir, "README.md"), "# Test Repo\n");
@@ -1485,4 +1485,3 @@ describe("Workspace File Routes", () => {
}); });
}); });
}); });

View File

@@ -307,8 +307,8 @@ function getSharedGitTestRepo(): GitTestRepo {
const repoDir = join(root, "repo"); const repoDir = join(root, "repo");
mkdirSync(repoDir, { recursive: true }); mkdirSync(repoDir, { recursive: true });
execFileSync("git", ["init", "--bare", remoteDir], { stdio: "pipe" }); execFileSync("git", ["init", "--bare", "--initial-branch=main", remoteDir], { stdio: "pipe" });
execFileSync("git", ["init", repoDir], { stdio: "pipe" }); execFileSync("git", ["init", "--initial-branch=main", repoDir], { stdio: "pipe" });
execFileSync("git", ["-C", repoDir, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" }); execFileSync("git", ["-C", repoDir, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" });
execFileSync("git", ["-C", repoDir, "config", "user.name", "KB Tests"], { stdio: "pipe" }); execFileSync("git", ["-C", repoDir, "config", "user.name", "KB Tests"], { stdio: "pipe" });
writeFileSync(join(repoDir, "README.md"), "# Test Repo\n"); writeFileSync(join(repoDir, "README.md"), "# Test Repo\n");
@@ -2029,4 +2029,3 @@ describe("GET /tasks/:id/file-diffs", () => {
// --- Git Management route tests --- // --- Git Management route tests ---
// These are integration tests that run against the actual git repository // These are integration tests that run against the actual git repository

View File

@@ -694,8 +694,8 @@ function getSharedGitTestRepo(): GitTestRepo {
const repoDir = join(root, "repo"); const repoDir = join(root, "repo");
mkdirSync(repoDir, { recursive: true }); mkdirSync(repoDir, { recursive: true });
execFileSync("git", ["init", "--bare", remoteDir], { stdio: "pipe" }); execFileSync("git", ["init", "--bare", "--initial-branch=main", remoteDir], { stdio: "pipe" });
execFileSync("git", ["init", repoDir], { stdio: "pipe" }); execFileSync("git", ["init", "--initial-branch=main", repoDir], { stdio: "pipe" });
execFileSync("git", ["-C", repoDir, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" }); execFileSync("git", ["-C", repoDir, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" });
execFileSync("git", ["-C", repoDir, "config", "user.name", "KB Tests"], { stdio: "pipe" }); execFileSync("git", ["-C", repoDir, "config", "user.name", "KB Tests"], { stdio: "pipe" });
writeFileSync(join(repoDir, "README.md"), "# Test Repo\n"); writeFileSync(join(repoDir, "README.md"), "# Test Repo\n");
@@ -720,4 +720,3 @@ afterAll(() => {
afterEach(() => { afterEach(() => {
resetDiagnosticsSink(); resetDiagnosticsSink();
}); });

View File

@@ -36,20 +36,7 @@ export default defineConfig({
}, },
}, },
test: { test: {
// `app/**` is React UI — needs jsdom + CSS. `src/**` is the Express
// backend, mostly Node-only logic; running it in node env trims jsdom
// env+CSS-include cost. The handful of src tests that genuinely need DOM
// opt-in via `// @vitest-environment jsdom`.
environment: "node",
environmentMatchGlobs: [
["app/**", "jsdom"],
],
// Process CSS imports only for jsdom-based tests that assert on
// getComputedStyle. Node-env tests under src/** don't need CSS rules and
// skipping the transform there cuts a large slice of total wall time.
css: { include: [/app\//] },
globals: true, globals: true,
include: ["app/**/*.test.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
setupFiles: [ setupFiles: [
resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts"), resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts"),
"./vitest.setup.ts", "./vitest.setup.ts",
@@ -66,6 +53,28 @@ export default defineConfig({
// 5s default under workspace-concurrent runs. // 5s default under workspace-concurrent runs.
testTimeout: 15_000, testTimeout: 15_000,
hookTimeout: 15_000, hookTimeout: 15_000,
projects: [
{
extends: true,
test: {
name: "dashboard-app",
environment: "jsdom",
include: ["app/**/*.test.{ts,tsx}"],
// Process CSS imports only for jsdom tests that assert on
// getComputedStyle. Node API tests do not need CSS transforms.
css: { include: [/app\//] },
},
},
{
extends: true,
test: {
name: "dashboard-api",
environment: "node",
include: ["src/**/*.test.{ts,tsx}"],
css: { include: [] },
},
},
],
coverage: { coverage: {
enabled: false, enabled: false,
reporter: ["text", "html", "json"], reporter: ["text", "html", "json"],

View File

@@ -12,6 +12,7 @@ const clearDaemonAuthEnv = () => {
clearDaemonAuthEnv(); clearDaemonAuthEnv();
const noisyOutputMarkers = [ const noisyOutputMarkers = [
"ExperimentalWarning: SQLite is an experimental feature",
"Subagent result watcher failed", "Subagent result watcher failed",
"pi-async-subagent-results", "pi-async-subagent-results",
"[pi] createFnAgent called", "[pi] createFnAgent called",