From f178bf1c00139e1b7fe2d14fcfe1f558fe08fbe2 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:29:12 +0800 Subject: [PATCH] perf(test): reuse CLI boundary worker module graphs Share CLI boundary module evaluation in bounded forks while preserving fresh command instances and child processes. Keep module-mocked cleanup tests and source/compiled product catalog verification isolated, and update focused artifact selection. Flameox full cohort with V8 coverage: 102.8s to 96.6s; 290 passed and two existing skips. Statement, function and branch coverage maps and hit sets match across all 1,153 source files. After syncing main, 75 focused tests, typecheck, lint, docs checks and formatting pass; all 834 test files are discovered exactly once. --- docs/testing.md | 19 +++++++---- package.json | 4 +-- scripts/lib/development-tests.mjs | 4 ++- .../productCatalog.test.ts | 0 .../browserScenarioCleanup.test.ts | 34 +++++++++---------- vitest.config.ts | 13 +++++++ 6 files changed, 48 insertions(+), 26 deletions(-) rename tests/boundary/{cli => filesystem}/productCatalog.test.ts (100%) rename tests/{boundary/cli => process-global}/browserScenarioCleanup.test.ts (86%) diff --git a/docs/testing.md b/docs/testing.md index 0b90c687f..548b1867d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -927,7 +927,7 @@ behavior, and complete CI evidence before merging. | `npm run test:focused -- PATH...` | Exact existing test files; compiled boundaries build first, and unmatched paths fail | | `npm run test:changed` | Source tests affected since the merge base with `origin/main`, including committed and dirty changes | | `npm run test:fast` | All domain, service, adapter, composition, conformance, and evaluation tests without build | -| `npm run test:boundary` | Boundary, MCP boundary, process boundary, and process-global projects | +| `npm run test:boundary` | Boundary, CLI boundary, MCP boundary, process boundary, and process-global projects | | `npm run test:mcp` | MCP boundary project | | `npm run test:acceptance` | Complete compiled CLI and MCP acceptance workflows | | `npm run test:watch` | Dirty source tests in watch mode, without build | @@ -989,11 +989,18 @@ Vitest projects use up to two workers, bounded by host parallelism. `process-boundary` runs later with serial files because process-tree sampling shares host resources. Acceptance and process-global cases use isolated forks without serial scheduling. Pure domain/service, composition, conformance, -evaluation, and MCP boundary projects share module graphs; their tests avoid -process-global state and keep resource cleanup test-scoped. Each MCP session -still owns its resources. Adapter, non-MCP boundary, acceptance, and -process-global projects retain per-file isolation. See `vitest.config.ts` for -current settings. +evaluation, CLI boundary, and MCP boundary projects share module graphs. Their +tests avoid process-global state and keep resource cleanup test-scoped. Each +MCP session still owns its resources, and CLI cases retain fresh command +instances or subprocesses. CLI module mocks belong in `tests/process-global/` +so they keep per-file isolation. Product catalog verification stays with the +isolated filesystem boundary tests because it loads both source and compiled +module graphs. Adapter, other boundary, acceptance, and process-global projects +retain per-file isolation. See `vitest.config.ts` for current settings. + +The reused CLI boundary workers cap V8 old space at 384 MiB, based on their +profiled allocation and cleanup behavior. This encourages collection between +files; spawned REA processes retain their own heap settings. Vitest worker limits do not bound subprocesses launched within a test. Bound those batches separately so repeated CLI validation cannot oversubscribe the diff --git a/package.json b/package.json index 553970a12..3d8d6fdfa 100644 --- a/package.json +++ b/package.json @@ -124,14 +124,14 @@ "test:local": "node scripts/run-development-tests.mjs local", "test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation", "pretest:boundary": "npm run test:prepare", - "test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global", + "test:boundary": "vitest run --project boundary --project cli-boundary --project process-boundary --project mcp-boundary --project process-global", "pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills artifacts:mcp-catalog", "test:mcp": "vitest run --project mcp-boundary", "pretest:acceptance": "npm run build:cached", "test:acceptance": "vitest run --project acceptance", "test:changed": "node scripts/run-development-tests.mjs changed", "pretest:integration": "npm run test:prepare", - "test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance", + "test:integration": "vitest run --project boundary --project cli-boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance", "pretest:coverage": "npm run test:prepare", "test:coverage": "vitest run --coverage.enabled --coverage.provider=v8", "test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml", diff --git a/scripts/lib/development-tests.mjs b/scripts/lib/development-tests.mjs index ccf15f152..8ddebe115 100644 --- a/scripts/lib/development-tests.mjs +++ b/scripts/lib/development-tests.mjs @@ -56,7 +56,9 @@ const artifactConsumers = { "tests/boundary/mcp/contractPresentation.test.ts", "tests/boundary/mcp/toolSchemaValidity.test.ts", ], - "artifacts:product-catalog": ["tests/boundary/cli/productCatalog.test.ts"], + "artifacts:product-catalog": [ + "tests/boundary/filesystem/productCatalog.test.ts", + ], "artifacts:managed-evidence": [ "tests/boundary/filesystem/setupSkill.test.ts", ], diff --git a/tests/boundary/cli/productCatalog.test.ts b/tests/boundary/filesystem/productCatalog.test.ts similarity index 100% rename from tests/boundary/cli/productCatalog.test.ts rename to tests/boundary/filesystem/productCatalog.test.ts diff --git a/tests/boundary/cli/browserScenarioCleanup.test.ts b/tests/process-global/browserScenarioCleanup.test.ts similarity index 86% rename from tests/boundary/cli/browserScenarioCleanup.test.ts rename to tests/process-global/browserScenarioCleanup.test.ts index 45df1b49b..b2acfd02e 100644 --- a/tests/boundary/cli/browserScenarioCleanup.test.ts +++ b/tests/process-global/browserScenarioCleanup.test.ts @@ -1,33 +1,33 @@ import { Cli } from "incur"; import { expect, it, vi } from "vitest"; -import type { BrowserScenarioCapturePort } from "../../../src/application/BrowserScenarioCapturePort.js"; -import { createBrowserScenarioEvidence } from "../../../src/application/BrowserScenarioEvidence.js"; -import type { BrowserScenarioSessionPort } from "../../../src/browser/BrowserScenarioSessionPort.js"; -import { PlaywrightBrowserScenarioProvider } from "../../../src/browser/PlaywrightBrowserScenarioProvider.js"; -import { PlaywrightScenarioBrowserCleanupOwner } from "../../../src/browser/PlaywrightScenarioBrowser.js"; -import { registerBrowserScenarioCommands } from "../../../src/cliBrowserScenarioCommands.js"; -import { sanitizeBrowserUrl } from "../../../src/domain/browserObservation.js"; +import type { BrowserScenarioCapturePort } from "../../src/application/BrowserScenarioCapturePort.js"; +import { createBrowserScenarioEvidence } from "../../src/application/BrowserScenarioEvidence.js"; +import type { BrowserScenarioSessionPort } from "../../src/browser/BrowserScenarioSessionPort.js"; +import { PlaywrightBrowserScenarioProvider } from "../../src/browser/PlaywrightBrowserScenarioProvider.js"; +import { PlaywrightScenarioBrowserCleanupOwner } from "../../src/browser/PlaywrightScenarioBrowser.js"; +import { registerBrowserScenarioCommands } from "../../src/cliBrowserScenarioCommands.js"; +import { sanitizeBrowserUrl } from "../../src/domain/browserObservation.js"; import { browserScenarioSchema, type BrowserScenario, -} from "../../../src/domain/browserScenario.js"; -import type { BrowserScenarioAction } from "../../../src/domain/browserScenarioValues.js"; -import { browserStepArtifactsSchema } from "../../../src/domain/browserScenarioCaptureValues.js"; -import { silentLogger } from "../../../src/logger.js"; +} from "../../src/domain/browserScenario.js"; +import type { BrowserScenarioAction } from "../../src/domain/browserScenarioValues.js"; +import { browserStepArtifactsSchema } from "../../src/domain/browserScenarioCaptureValues.js"; +import { silentLogger } from "../../src/logger.js"; import { AnalysisCancelledError, AnalysisTimeoutError, -} from "../../../src/domain/analysisErrorCore.js"; -import { BrowserObservationError } from "../../../src/domain/browserObservationError.js"; -import { parseEvidence } from "../../../src/domain/evidence.js"; -import { jsonObjectSchema } from "../../../src/domain/jsonValue.js"; -import { ok } from "../../../src/domain/result.js"; +} from "../../src/domain/analysisErrorCore.js"; +import { BrowserObservationError } from "../../src/domain/browserObservationError.js"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { jsonObjectSchema } from "../../src/domain/jsonValue.js"; +import { ok } from "../../src/domain/result.js"; const state = vi.hoisted(() => ({ provider: undefined as BrowserScenarioCapturePort | undefined, })); -vi.mock("../../../src/composition/browserScenario.js", () => ({ +vi.mock("../../src/composition/browserScenario.js", () => ({ createBrowserScenarioProvider: () => state.provider, })); diff --git a/vitest.config.ts b/vitest.config.ts index 3e45d5800..7a9485c4e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -69,12 +69,25 @@ const TEST_PROJECTS = [ name: "boundary", include: ["tests/boundary/**/*.test.ts"], exclude: [ + "tests/boundary/cli/**/*.test.ts", "tests/boundary/mcp/**/*.test.ts", "tests/boundary/process/**/*.test.ts", ], pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS, }, + { + name: "cli-boundary", + include: ["tests/boundary/cli/**/*.test.ts"], + pool: "forks" as const, + maxWorkers: MAX_TEST_WORKERS, + // CLI tests create fresh command instances or subprocesses and own their + // cleanup. Reuse the immutable graph; module mocks belong in process-global. + isolate: false, + // Long-lived workers should collect temporary command/schema allocations + // before their two heaps compete with the real CLI child processes. + execArgv: ["--max-old-space-size=384"], + }, { // Process-tree observations share host sampling resources; keep this small // lane serial while the independent projects run together.