mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
Vitest's expect.poll and vi.waitFor stop after one second by default, a host-speed guess that the project's deadline policy does not see. Under hosted-CI load, kunaProjectConcurrency failed that way twice on unrelated PRs. Another 256 waits relied on the default, and 97 passed literals picked to fit some host. Waits now take as long as their test may run, less the reporting reserve that children already keep. Each Vitest project sets expect.poll's default, and the new waitFor in tests/support/testWait.ts replaces vi.waitFor. Fixture readiness waits (awaitAcknowledgement and the provider process waits) default to the same deadline, and their host-speed literal arguments are removed. One remainingTestDeadline owns the arithmetic for both children and waits. Ten waits whose short timeout was the real check now assert it directly (Kuna version probe, Cutter discovery, doctor and Inspector cancellation, apktool fixture teardown, JADX disconnect): settlement before the production deadline that would otherwise also satisfy the wait, or a deadline pinned beyond every test deadline. The rea/project-wait-deadline lint rule rejects vi.waitFor and explicit expect.poll timeouts. The testing guide and the CI tests audit skill now document the policy. Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
233 lines
8.0 KiB
TypeScript
233 lines
8.0 KiB
TypeScript
import { realpathSync } from "node:fs";
|
|
import { availableParallelism, tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { defineConfig } from "vitest/config";
|
|
import { parseCLI } from "vitest/node";
|
|
import {
|
|
BUILT_TEST_PROJECTS,
|
|
SOURCE_TEST_PROJECTS,
|
|
} from "./scripts/lib/test-projects.mjs";
|
|
|
|
import { CiTestSequencer } from "./tests/ciTestSequencer.js";
|
|
import {
|
|
COVERAGE_DEADLINE_SCALE,
|
|
HOSTED_RUNNER_DEADLINE_SCALE,
|
|
LONG_RUNNING_DEADLINE_MS,
|
|
remainingTestDeadline,
|
|
TEST_DEADLINE_MS as BASE_TEST_DEADLINE_MS,
|
|
} from "./tests/support/testDeadlines.js";
|
|
|
|
// Playwright's registry does not derive a cache directory for Termux's
|
|
// `android` platform. Match the production CLI fallback before workers import
|
|
// browser modules.
|
|
if (
|
|
process.platform === "android" &&
|
|
process.env.PLAYWRIGHT_BROWSERS_PATH === undefined
|
|
) {
|
|
const cacheRoot =
|
|
process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? ".", ".cache");
|
|
process.env.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright");
|
|
}
|
|
|
|
const CANONICAL_TEMPORARY_DIRECTORY = realpathSync(tmpdir());
|
|
// Use Vitest's option parser so explicit false and spaced shard values retain
|
|
// their meaning. Unknown worker/embedding arguments are not Vitest options.
|
|
const { options: testOptions } = parseCLI(
|
|
["vitest", ...process.argv.slice(2)],
|
|
{ allowUnknownOptions: true },
|
|
);
|
|
const COVERAGE_ENABLED = testOptions.coverage?.enabled === true;
|
|
const COVERAGE_SHARD = testOptions.shard !== undefined;
|
|
// Bound concurrency by the runner rather than command locks. A hosted CI job
|
|
// owns its whole four-core runner, so it may use every core; other hosts stay
|
|
// at two workers because developers and agents share them with other checks.
|
|
const DEDICATED_CI_RUNNER = process.env.GITHUB_ACTIONS === "true";
|
|
const MAX_TEST_WORKERS = Math.min(
|
|
DEDICATED_CI_RUNNER ? 4 : 2,
|
|
availableParallelism(),
|
|
);
|
|
// A test deadline only turns a hang into a named failure; it is not a
|
|
// performance assertion. Projects own it so tests do not hard-code host speed.
|
|
// Built projects launch compiled CLI, MCP, and provider processes while other
|
|
// lanes share the runner, and coverage instrumentation slows every lane. Hosted
|
|
// CI runs a worker per core, so CPU-bound cases there share every core with
|
|
// other workers and their subprocesses.
|
|
const DEADLINE_SCALE =
|
|
(COVERAGE_ENABLED ? COVERAGE_DEADLINE_SCALE : 1) *
|
|
(DEDICATED_CI_RUNNER ? HOSTED_RUNNER_DEADLINE_SCALE : 1);
|
|
const TEST_DEADLINE_MS = {
|
|
source: BASE_TEST_DEADLINE_MS.source * DEADLINE_SCALE,
|
|
built: BASE_TEST_DEADLINE_MS.built * DEADLINE_SCALE,
|
|
} as const;
|
|
|
|
// Vitest's one-second `expect.poll` default is a host-speed guess. A poll the
|
|
// project owns waits as long as its test may run, less a reporting reserve, so
|
|
// a condition that never holds still fails with the poll's own diagnostics.
|
|
const pollPolicy = (deadlineMs: number) => {
|
|
const timeout = remainingTestDeadline(deadlineMs, 0);
|
|
return timeout === undefined ? {} : { timeout };
|
|
};
|
|
|
|
const TEST_PROJECTS = [...SOURCE_TEST_PROJECTS, ...BUILT_TEST_PROJECTS];
|
|
const PROJECT_POLICIES = {
|
|
domain: {
|
|
pool: "threads" as const,
|
|
maxWorkers: MAX_TEST_WORKERS,
|
|
// Domain and contract modules are pure and own no process-global state, so
|
|
// per-file module isolation adds startup cost without protecting a seam.
|
|
},
|
|
services: {
|
|
pool: "threads" as const,
|
|
maxWorkers: MAX_TEST_WORKERS,
|
|
// Service tests use recording ports and own no process-global state. Share
|
|
// their module graph so file startup does not dominate the lane.
|
|
},
|
|
adapters: { pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS },
|
|
composition: {
|
|
pool: "threads" as const,
|
|
maxWorkers: MAX_TEST_WORKERS,
|
|
// Composition tests use provider-neutral sessions and test-scoped cleanup;
|
|
// sharing their immutable module graph avoids per-file startup work.
|
|
},
|
|
boundary: { pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS },
|
|
"cli-boundary": {
|
|
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.
|
|
// The shared source/contract graph also needs transient schema/help space.
|
|
// Ordinary CI exhausted 384 MiB after earlier files warmed that graph.
|
|
// Use the measured 512 MiB budget with the same bounded concurrency.
|
|
execArgv: ["--max-old-space-size=512"],
|
|
},
|
|
"process-boundary": {
|
|
pool: "forks" as const,
|
|
maxWorkers: MAX_TEST_WORKERS,
|
|
// Process-tree observations share host sampling resources; keep this small
|
|
// lane serial while the independent projects run together.
|
|
fileParallelism: false,
|
|
sequence: { groupOrder: 1 },
|
|
},
|
|
"mcp-boundary": {
|
|
pool: "threads" as const,
|
|
maxWorkers: MAX_TEST_WORKERS,
|
|
// Each file creates and closes an explicit in-memory MCP session. Reuse the
|
|
// immutable server graph so module startup does not outweigh transport work.
|
|
},
|
|
acceptance: { pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS },
|
|
"process-global": { pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS },
|
|
conformance: {
|
|
pool: "threads" as const,
|
|
maxWorkers: MAX_TEST_WORKERS,
|
|
// Contract cases use recording providers and own no process-global state.
|
|
},
|
|
evaluation: {
|
|
pool: "threads" as const,
|
|
maxWorkers: MAX_TEST_WORKERS,
|
|
// Evaluator tests inspect deterministic inputs without mutating shared state.
|
|
},
|
|
};
|
|
|
|
const ZERO_COVERAGE_THRESHOLDS = {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
"src/domain/**": {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
},
|
|
"src/contracts/**": {
|
|
statements: 0,
|
|
branches: 0,
|
|
functions: 0,
|
|
lines: 0,
|
|
},
|
|
};
|
|
|
|
const projects = TEST_PROJECTS.map((project) => {
|
|
const policy =
|
|
PROJECT_POLICIES[project.name as keyof typeof PROJECT_POLICIES];
|
|
if (!policy)
|
|
throw new Error(`Missing test runner policy for ${project.name}`);
|
|
return {
|
|
extends: true as const,
|
|
test: {
|
|
name: project.name,
|
|
include: [...project.include],
|
|
...(project.exclude ? { exclude: [...project.exclude] } : {}),
|
|
isolate: project.isolate !== false,
|
|
testTimeout: TEST_DEADLINE_MS[project.group],
|
|
hookTimeout: TEST_DEADLINE_MS[project.group],
|
|
expect: { poll: pollPolicy(TEST_DEADLINE_MS[project.group]) },
|
|
...policy,
|
|
},
|
|
};
|
|
});
|
|
|
|
export default defineConfig({
|
|
test: {
|
|
env: { TMPDIR: CANONICAL_TEMPORARY_DIRECTORY },
|
|
maxWorkers: MAX_TEST_WORKERS,
|
|
projects,
|
|
...(process.env.REA_CI_TEST_HISTORY
|
|
? { sequence: { sequencer: CiTestSequencer } }
|
|
: {}),
|
|
retry: 0,
|
|
reporters: ["default"],
|
|
runner: "./tests/processOwnershipRunner.ts",
|
|
testTimeout: TEST_DEADLINE_MS.source,
|
|
hookTimeout: TEST_DEADLINE_MS.source,
|
|
expect: { poll: pollPolicy(TEST_DEADLINE_MS.source) },
|
|
// Deadline probes compare what Vitest applies with the configured policy.
|
|
provide: { testDeadlineScale: DEADLINE_SCALE },
|
|
tags: [
|
|
{
|
|
name: "long-running",
|
|
description:
|
|
"A bounded workload that needs more than its project's deadline",
|
|
timeout: LONG_RUNNING_DEADLINE_MS * DEADLINE_SCALE,
|
|
},
|
|
],
|
|
coverage: {
|
|
enabled: false,
|
|
provider: "v8",
|
|
reportsDirectory: join(
|
|
tmpdir(),
|
|
`rea-vitest-coverage-${String(process.pid)}`,
|
|
),
|
|
include: ["src/**"],
|
|
thresholds: COVERAGE_SHARD
|
|
? ZERO_COVERAGE_THRESHOLDS
|
|
: {
|
|
statements: 65,
|
|
branches: 60,
|
|
functions: 60,
|
|
lines: 68,
|
|
"src/domain/**": {
|
|
statements: 80,
|
|
branches: 75,
|
|
functions: 75,
|
|
lines: 80,
|
|
},
|
|
"src/contracts/**": {
|
|
statements: 85,
|
|
branches: 80,
|
|
functions: 80,
|
|
lines: 85,
|
|
},
|
|
},
|
|
reporter: ["text", "text-summary"],
|
|
},
|
|
},
|
|
});
|
|
|
|
declare module "vitest" {
|
|
export interface ProvidedContext {
|
|
readonly testDeadlineScale: number;
|
|
}
|
|
}
|