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.