chore(test): separate development feedback from full verification (#599)
This commit is contained in:
parent
9c25dc4ed2
commit
82957c98bf
|
|
@ -374,6 +374,7 @@ jobs:
|
|||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run docs:check
|
||||
- run: npm run docs:api:cached
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-docs
|
||||
|
|
|
|||
|
|
@ -30,9 +30,12 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
|
|||
|
||||
- `npm ci`: install the locked dependencies.
|
||||
- `npm run build:cached`: build the CLI and MCP server.
|
||||
- `npm run test:local`: run focused tests for changed code.
|
||||
- `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status.
|
||||
- `npm run test:focused -- PATH...`: run exact test files; build first for boundary, acceptance, or process-global tests.
|
||||
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
|
||||
- `npm run check:fast`: run cached typecheck and lint checks.
|
||||
- `npm run check:pr`: run the deterministic PR gate and generated-document checks.
|
||||
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
|
||||
- `npm run docs:check`: check committed generated documents; `npm run docs:api:cached` separately renders API HTML when needed.
|
||||
- For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification.
|
||||
- Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ REA development requires Node.js 24.18.x and npm 11.16.x. Real-Hopper verificati
|
|||
|
||||
```bash
|
||||
npm ci
|
||||
npm test
|
||||
npm run check:fast
|
||||
```
|
||||
|
||||
`npm ci` installs the exact dependencies and prepares the Husky hooks without
|
||||
|
|
@ -22,36 +22,43 @@ worktrees. After a package, lockfile, or managed-skill version change, run
|
|||
|
||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Prefer capability- and session-scoped tool advertisement over schema truncation.
|
||||
|
||||
Before submitting a pull request, run:
|
||||
## Development feedback and PR verification
|
||||
|
||||
For an ordinary edit, run the relevant regression and cached static checks:
|
||||
|
||||
```bash
|
||||
npm run check:pr
|
||||
npm run verify:package
|
||||
npm pack --dry-run
|
||||
npm run test:focused -- src/config.test.ts
|
||||
npm run check:fast
|
||||
```
|
||||
|
||||
`npm run check:fast` runs cached typecheck and lint for rapid local feedback.
|
||||
`npm run check` adds formatting, dead-code, and package-metadata freshness
|
||||
checks; use `npm run check:test` when the complete test suite is relevant.
|
||||
Pre-commit formats then lints staged source files, and pre-push runs
|
||||
`check:fast`. `check:pr` additionally renders API documentation and checks all
|
||||
committed generated metadata. Real
|
||||
provider and replay execution remains uncached, but their deterministic build
|
||||
prerequisite uses Turbo. CI uploads the rendered TypeDoc site as an `api-docs`
|
||||
artifact; the generated HTML is not committed.
|
||||
`test:local` selects dirty source tests without building; explicit source test
|
||||
paths run even on a clean tree. `check:changed` adds source tests affected since
|
||||
the branch merge base with `origin/main`. Use `npm run test:changed -- --base
|
||||
REVISION` to choose another base. Changed-test selection follows the import
|
||||
graph and is feedback, not complete correctness evidence. Inspect relevant
|
||||
boundary and provider behavior explicitly; see [docs/testing.md](docs/testing.md).
|
||||
|
||||
Local `npm test` runs every deterministic Vitest project without coverage,
|
||||
retries, or verbose output. Focused boundary, acceptance, changed-test, and
|
||||
watch commands are documented in [docs/testing.md](docs/testing.md), together
|
||||
with the path-based behavioral-depth rules. Changed-test selection follows the
|
||||
import graph and is feedback, not the release gate. CI splits the complete suite
|
||||
across four native Vitest shards, then merges coverage, JUnit, and JSON timing
|
||||
reports. Coverage thresholds remain in `vitest.config.ts`.
|
||||
Before handing off a PR, run the relevant tests, `npm run check`, and
|
||||
`npm run docs:check` when contracts or generated metadata change. Record which
|
||||
checks ran. CI owns complete deterministic tests and aggregate coverage. Use
|
||||
`npm run check:pr` for a deliberate full local gate on broad changes or when
|
||||
investigating CI failures; it is not required after each edit, rebase, or push.
|
||||
Packaging, setup, installation, or distribution changes also require
|
||||
`npm run verify:package` and `npm pack --dry-run`. Provider behavior changes
|
||||
require the matching real-provider `verify:*` lane.
|
||||
|
||||
CI installs dependencies once for all static checks, cancels superseded PR
|
||||
runs, and skips package, Windows, and full test lanes for documentation-only
|
||||
pull requests. TypeDoc renders only in pull-request CI; it does not run in local
|
||||
commit or pre-push hooks, or in post-merge `main` CI.
|
||||
`check:fast` runs cached typecheck and lint, reporting diagnostics on failure.
|
||||
`check` adds formatting, dead-code, and package-metadata freshness checks.
|
||||
Pre-commit formats and lints staged files; pre-push runs `check:fast`.
|
||||
`docs:check` checks committed generated metadata without rendering API HTML.
|
||||
`docs:generate` regenerates those files; render API HTML separately with
|
||||
`npm run docs:api:cached`. PR CI renders and uploads the `api-docs` artifact.
|
||||
Real-provider execution remains uncached; deterministic builds use Turbo.
|
||||
|
||||
Local `npm test` runs every deterministic Vitest project without coverage or
|
||||
retries. CI runs four coverage shards and merges JUnit and timing reports.
|
||||
CI cancels superseded PR runs and skips package, Windows, and full test lanes
|
||||
for documentation-only PRs. Coverage thresholds remain in `vitest.config.ts`.
|
||||
|
||||
Tests that need a temporary directory must use
|
||||
`createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`. The
|
||||
|
|
@ -92,7 +99,7 @@ Describe the behavior change and verification performed in the pull request. Nev
|
|||
|
||||
## Maintainer release checklist
|
||||
|
||||
Run the full checks, isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
|
||||
Run `npm run check:pr`, `npm run docs:api:cached`, the isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
|
||||
|
||||
```bash
|
||||
npm pack
|
||||
|
|
|
|||
|
|
@ -164,26 +164,55 @@ not make native host acceptance unavailable.
|
|||
|
||||
## Developer commands
|
||||
|
||||
`npm test` runs every deterministic project once. The narrower feedback loops
|
||||
are:
|
||||
Use source feedback while editing, explicit boundary checks for the changed
|
||||
behavior, and complete CI evidence before merging.
|
||||
|
||||
| Command | Scope |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run test:fast` | Domain, service, adapter, composition, boundary, MCP boundary, and conformance projects |
|
||||
| `npm run test:local` | Changed and related tests without the build step; lightweight local feedback loop |
|
||||
| `npm run test:boundary` | Boundary, MCP boundary, and process-global projects |
|
||||
| `npm run test:mcp` | MCP boundary project only |
|
||||
| `npm run test:acceptance` | Complete CLI and MCP acceptance workflows |
|
||||
| `npm run test:changed` | Changed tests in non-serial projects via Vitest's import graph |
|
||||
| `npm run test:watch` | Changed domain, service, adapter, composition, boundary, MCP boundary, conformance, and evaluation tests in watch mode |
|
||||
| `npm run test:watch:all` | Changed tests from every deterministic project in watch mode |
|
||||
| `npm run check:changed` | Cached static checks followed by changed tests |
|
||||
| `npm run check:pr` | Static, generated-document, and complete deterministic PR gate |
|
||||
| Command | Scope |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run test:local` | Dirty source tests via the import graph, without build; explicit source paths run regardless of Git status |
|
||||
| `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: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 |
|
||||
| `npm run test:watch:all` | Changed tests from every project; builds at startup, so rebuild after production edits before relying on compiled tests |
|
||||
| `npm run check:changed` | Cached typecheck/lint and branch-related source feedback |
|
||||
| `npm run check:pr` | Opt-in complete local deterministic gate and generated-file checks |
|
||||
| `npm run docs:check` | Committed generated-document freshness, without API HTML rendering |
|
||||
| `npm run docs:api:cached` | Explicit cached API HTML rendering |
|
||||
|
||||
Changed-test selection is a fast feedback aid, not release evidence. It can
|
||||
miss behavior connected through runtime registration, generated data, shell
|
||||
entrypoints, or other relationships that are absent from the import graph.
|
||||
Use `npm run check:pr` before handing off a contribution.
|
||||
For example:
|
||||
|
||||
```bash
|
||||
npm run test:local -- src/config.test.ts
|
||||
npm run test:focused -- tests/acceptance/applications/runtime.test.ts
|
||||
npm run test:changed -- --base origin/main
|
||||
npm run test:changed -- --dry-run
|
||||
```
|
||||
|
||||
`test:focused` accepts exact repository-relative test file paths. Source-only
|
||||
paths do not build; boundary, acceptance, process-global, or unfamiliar `tests/`
|
||||
paths build conservatively. The build and test run share the repository test
|
||||
lock. Explicit selections do not use `--changed` or permit zero-test success.
|
||||
The dry-run option reports the chosen merge base, scope and build prerequisite
|
||||
without executing tests or building. A missing Git base reports how to fetch
|
||||
it or select another revision.
|
||||
|
||||
Changed selection can miss runtime registration, generated data, shell
|
||||
entrypoints, bridges, or other relationships absent from the import graph.
|
||||
An empty changed selection means no tests were selected, not verified
|
||||
correctness. Select relevant boundary files and real-provider lanes explicitly.
|
||||
Source projects also contain large capacity regressions; `test:fast` promises
|
||||
no compiled-runtime prerequisite, not a fixed time budget.
|
||||
|
||||
Routine iterations and rebases need focused regressions and relevant checks.
|
||||
Before handing off a PR, run `npm run check` and generated-document checks when
|
||||
applicable; CI owns the full suite and coverage. Use the full local gate for
|
||||
broad changes or diagnosing CI, rather than after every edit. Package/install
|
||||
changes additionally need package verification; provider changes need actual
|
||||
provider evidence.
|
||||
|
||||
Local full-suite Vitest runs use up to two workers and schedule projects one
|
||||
at a time. Process, acceptance and process-global projects serialize their
|
||||
|
|
@ -197,8 +226,8 @@ process-global, and other boundary projects retain per-file isolation.
|
|||
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
|
||||
repository-local locks and fail fast when the same class of command is already
|
||||
running. The `npm test` build is inside that lock. `check:pr` runs its test task
|
||||
before starting documentation validation, so TypeDoc does not compete with the
|
||||
full suite for memory.
|
||||
before starting generated-document validation. TypeDoc rendering is a separate
|
||||
command and CI step.
|
||||
|
||||
Vitest and Node persistent compile caches are deliberately not enabled by
|
||||
default. To evaluate repeated local runs, opt in for both cold and warm
|
||||
|
|
@ -232,8 +261,9 @@ for another.
|
|||
The PR acceptance target is a median `npm run check:pr` wall time below three
|
||||
minutes across three warm-build runs on the benchmark host. Keep Vitest caches
|
||||
cold unless separately identified. A PR that touches packaging or real-system
|
||||
behavior also requires `npm run verify:package` and the applicable
|
||||
`verify:*` lanes.
|
||||
behavior requires the applicable `verify:*` lanes; packaging and installation
|
||||
changes also require `npm run verify:package`. The full-gate benchmark measures
|
||||
that explicit lane, not the routine iteration requirement.
|
||||
|
||||
## Apple native metadata and UI
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
{
|
||||
"$schema": "./node_modules/knip/schema.json",
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"entry": [
|
||||
"src/main.ts",
|
||||
"src/cli.ts",
|
||||
"src/contracts/errorSchemas.ts",
|
||||
"scripts/run-development-tests.mjs"
|
||||
],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"ignoreDependencies": ["electron"],
|
||||
"ignoreBinaries": ["ps"],
|
||||
|
|
|
|||
20
package.json
20
package.json
|
|
@ -66,9 +66,9 @@
|
|||
"precheck": "npm run deps:check",
|
||||
"check": "turbo run typecheck lint format:check knip metadata:check",
|
||||
"precheck:fast": "npm run deps:check",
|
||||
"check:fast": "turbo run typecheck lint",
|
||||
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
|
||||
"precheck:changed": "npm run deps:check",
|
||||
"check:changed": "turbo run typecheck lint && npm run test:changed",
|
||||
"check:changed": "npm run check:fast && npm run test:changed",
|
||||
"precheck:test": "npm run deps:check",
|
||||
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
|
||||
"precheck:pr": "npm run deps:check",
|
||||
|
|
@ -88,17 +88,15 @@
|
|||
"test": "node scripts/run-exclusive.mjs test npm run test:with-build",
|
||||
"test:with-build": "npm run build:cached && vitest run",
|
||||
"test:run": "node scripts/run-exclusive.mjs test vitest run",
|
||||
"test:local": "node scripts/run-exclusive.mjs test vitest run --changed --passWithNoTests --maxWorkers=1",
|
||||
"pretest:fast": "npm run build:cached",
|
||||
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance",
|
||||
"test:local": "node scripts/run-exclusive.mjs test 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 build:cached",
|
||||
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
|
||||
"pretest:mcp": "npm run build:cached",
|
||||
"test:mcp": "vitest run --project mcp-boundary",
|
||||
"pretest:acceptance": "npm run build:cached",
|
||||
"test:acceptance": "vitest run --project acceptance",
|
||||
"pretest:changed": "npm run build:cached",
|
||||
"test:changed": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
|
||||
"test:changed": "node scripts/run-exclusive.mjs test node scripts/run-development-tests.mjs changed",
|
||||
"pretest:integration": "npm run build:cached",
|
||||
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
|
||||
"pretest:coverage": "npm run build:cached",
|
||||
|
|
@ -110,8 +108,7 @@
|
|||
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
|
||||
"pretest:verbose": "npm run build:cached",
|
||||
"test:verbose": "vitest run --reporter=verbose",
|
||||
"pretest:watch": "npm run build:cached",
|
||||
"test:watch": "vitest --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
|
||||
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
|
||||
"pretest:watch:all": "npm run build:cached",
|
||||
"test:watch:all": "vitest --changed",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
@ -150,14 +147,15 @@
|
|||
"docs:api": "typedoc",
|
||||
"docs:api:cached": "turbo run docs:api",
|
||||
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
||||
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
|
||||
"prepack": "node scripts/prepack.mjs",
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
|
||||
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial"
|
||||
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
|
||||
"test:focused": "node scripts/run-exclusive.mjs test node scripts/run-development-tests.mjs focused"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
export interface DevelopmentTestRequest {
|
||||
readonly mode: "local" | "changed" | "focused";
|
||||
readonly paths: readonly string[];
|
||||
readonly base: string;
|
||||
readonly dryRun: boolean;
|
||||
}
|
||||
|
||||
/** Parse explicit test paths and an optional branch-comparison base. */
|
||||
export function parseDevelopmentTestRequest(
|
||||
mode: string,
|
||||
arguments_: readonly string[],
|
||||
): DevelopmentTestRequest;
|
||||
|
||||
/** Plan source feedback or a strict explicit run with its build prerequisite. */
|
||||
export function developmentTestPlan(
|
||||
request: DevelopmentTestRequest,
|
||||
baseCommit?: string,
|
||||
): {
|
||||
readonly needsBuild: boolean;
|
||||
readonly vitestArguments: readonly string[];
|
||||
};
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
const sourceProjects = [
|
||||
"domain",
|
||||
"services",
|
||||
"adapters",
|
||||
"composition",
|
||||
"conformance",
|
||||
"evaluation",
|
||||
];
|
||||
|
||||
const sourceTest = (path) =>
|
||||
path.startsWith("src/") ||
|
||||
["composition", "conformance", "evaluation"].some((project) =>
|
||||
path.startsWith(`tests/${project}/`),
|
||||
);
|
||||
|
||||
/** Parse explicit test paths and an optional branch-comparison base. */
|
||||
export const parseDevelopmentTestRequest = (mode, arguments_) => {
|
||||
if (!["local", "changed", "focused"].includes(mode))
|
||||
throw new Error("Expected local, changed, or focused test mode");
|
||||
const paths = [];
|
||||
let base = "origin/main";
|
||||
let dryRun = false;
|
||||
for (let index = 0; index < arguments_.length; index += 1) {
|
||||
const argument = arguments_[index];
|
||||
if (argument === "--dry-run") dryRun = true;
|
||||
else if (argument === "--base" || argument.startsWith("--base=")) {
|
||||
if (mode !== "changed") throw new Error("--base requires changed mode");
|
||||
base = argument === "--base" ? arguments_[++index] : argument.slice(7);
|
||||
if (!base || base.startsWith("-") || base.includes("\0"))
|
||||
throw new Error("--base requires a Git revision");
|
||||
} else {
|
||||
const path = argument.replaceAll("\\", "/").replace(/^\.\//u, "");
|
||||
if (
|
||||
!/^(?:src|tests)\/.+\.test\.ts$/u.test(path) ||
|
||||
path.split("/").some((part) => part === ".." || part === ".")
|
||||
)
|
||||
throw new Error(`Expected an exact repository test path: ${argument}`);
|
||||
if (mode !== "focused" && !sourceTest(path))
|
||||
throw new Error(
|
||||
`Use test:focused for compiled boundary tests: ${path}`,
|
||||
);
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
if (mode === "focused" && paths.length === 0)
|
||||
throw new Error("test:focused requires at least one exact test path");
|
||||
if (mode === "changed" && paths.length > 0)
|
||||
throw new Error("Use test:focused for explicit test paths");
|
||||
return { mode, paths: [...new Set(paths)], base, dryRun };
|
||||
};
|
||||
|
||||
/** Plan source feedback or a strict explicit run with its build prerequisite. */
|
||||
export const developmentTestPlan = (request, baseCommit) => {
|
||||
const explicit = request.paths.length > 0;
|
||||
if (
|
||||
request.mode === "changed" &&
|
||||
!/^[a-f0-9]{40,64}$/u.test(baseCommit ?? "")
|
||||
)
|
||||
throw new Error("Changed tests require a resolved Git merge base");
|
||||
const projects = request.mode === "focused" ? [] : sourceProjects;
|
||||
return {
|
||||
needsBuild: request.paths.some((path) => !sourceTest(path)),
|
||||
vitestArguments: [
|
||||
"run",
|
||||
"--maxWorkers=1",
|
||||
...projects.flatMap((project) => ["--project", project]),
|
||||
...(explicit
|
||||
? request.paths
|
||||
: [
|
||||
request.mode === "changed"
|
||||
? `--changed=${baseCommit}`
|
||||
: "--changed",
|
||||
"--passWithNoTests",
|
||||
]),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import {
|
||||
developmentTestPlan,
|
||||
parseDevelopmentTestRequest,
|
||||
} from "./lib/development-tests.mjs";
|
||||
|
||||
let child;
|
||||
let shutdownSignal;
|
||||
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||
for (const signal of Object.keys(signalExitCodes))
|
||||
process.on(signal, () => {
|
||||
shutdownSignal ??= signal;
|
||||
child?.kill(signal);
|
||||
});
|
||||
|
||||
try {
|
||||
const [mode, ...arguments_] = process.argv.slice(2);
|
||||
const request = parseDevelopmentTestRequest(mode, arguments_);
|
||||
for (const path of request.paths) {
|
||||
if (!(await stat(path)).isFile())
|
||||
throw new Error(`Test path is not a file: ${path}`);
|
||||
}
|
||||
let baseCommit;
|
||||
if (request.mode === "changed") {
|
||||
const base = await run("git", ["merge-base", "HEAD", request.base], true);
|
||||
if (base.code !== 0)
|
||||
throw new Error(
|
||||
`Cannot resolve test base ${request.base}. Fetch origin/main or pass --base REVISION.\n${base.output.trim()}`,
|
||||
);
|
||||
baseCommit = base.output.trim();
|
||||
}
|
||||
const plan = developmentTestPlan(request, baseCommit);
|
||||
if (request.dryRun) {
|
||||
console.log(JSON.stringify({ ...request, baseCommit, ...plan }, null, 2));
|
||||
} else {
|
||||
console.error(
|
||||
request.mode === "focused"
|
||||
? `Focused tests: ${request.paths.join(", ")}`
|
||||
: `Source-test feedback${baseCommit ? ` from merge base ${baseCommit}` : ""}; compiled boundaries and real providers need explicit verification.`,
|
||||
);
|
||||
if (plan.needsBuild) {
|
||||
if (!process.env.npm_execpath)
|
||||
throw new Error("Run compiled tests through npm run test:focused");
|
||||
const built = await run(process.execPath, [
|
||||
process.env.npm_execpath,
|
||||
"run",
|
||||
"build:cached",
|
||||
]);
|
||||
if (built.code !== 0) process.exitCode = built.code;
|
||||
}
|
||||
if (!process.exitCode && !shutdownSignal) {
|
||||
const tested = await run(process.execPath, [
|
||||
resolve("node_modules/vitest/vitest.mjs"),
|
||||
...plan.vitestArguments,
|
||||
]);
|
||||
process.exitCode = tested.code;
|
||||
}
|
||||
}
|
||||
} catch (cause) {
|
||||
console.error(cause instanceof Error ? cause.message : String(cause));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
if (shutdownSignal) process.exitCode = signalExitCodes[shutdownSignal];
|
||||
|
||||
function run(executable, arguments_, capture = false) {
|
||||
if (shutdownSignal)
|
||||
return Promise.resolve({
|
||||
code: signalExitCodes[shutdownSignal],
|
||||
output: "",
|
||||
});
|
||||
return new Promise((resolveResult, reject) => {
|
||||
let output = "";
|
||||
child = spawn(executable, arguments_, {
|
||||
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
});
|
||||
if (capture) {
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
child.stderr.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
}
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => resolveResult({ code: code ?? 1, output }));
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const runner = resolve("scripts/run-development-tests.mjs");
|
||||
|
||||
describe("development test runner", () => {
|
||||
it("rejects nonexistent explicit paths instead of returning a zero-test success", async () => {
|
||||
await expect(
|
||||
execute(process.execPath, [
|
||||
runner,
|
||||
"focused",
|
||||
"src/missing.test.ts",
|
||||
"--dry-run",
|
||||
]),
|
||||
).rejects.toMatchObject({ code: 1 });
|
||||
});
|
||||
|
||||
it("reports actionable remediation when the comparison base is unavailable", async () => {
|
||||
await expect(
|
||||
execute(process.execPath, [
|
||||
runner,
|
||||
"changed",
|
||||
"--base",
|
||||
"missing-test-base",
|
||||
"--dry-run",
|
||||
]),
|
||||
).rejects.toMatchObject({
|
||||
code: 1,
|
||||
stderr: expect.stringContaining(
|
||||
"Fetch origin/main or pass --base REVISION",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("runs a committed branch regression through real Git and Vitest without dist", async () => {
|
||||
const root = await createTestTempDirectory("rea-development-tests-");
|
||||
await mkdir(join(root, "src"));
|
||||
await symlink(
|
||||
resolve("node_modules"),
|
||||
join(root, "node_modules"),
|
||||
"junction",
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ type: "module" }),
|
||||
);
|
||||
await writeFile(join(root, ".gitignore"), "node_modules\n");
|
||||
await writeFile(
|
||||
join(root, "vitest.config.ts"),
|
||||
`export default { test: { projects: [{ test: { name: "domain", include: ["src/**/*.test.ts"] } }] } };`,
|
||||
);
|
||||
await writeFile(join(root, "src/value.ts"), "export const value = 1;\n");
|
||||
await writeFile(
|
||||
join(root, "src/value.test.ts"),
|
||||
`import { it, expect } from "vitest"; import { value } from "./value"; it("checks current source", () => expect(value).toBe(1));\n`,
|
||||
);
|
||||
const git = (arguments_: string[]) =>
|
||||
execute("git", arguments_, { cwd: root });
|
||||
await git(["init", "-b", "main"]);
|
||||
await git(["add", ".gitignore", "package.json", "vitest.config.ts", "src"]);
|
||||
const commit = [
|
||||
"-c",
|
||||
"user.name=REA fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.test",
|
||||
"commit",
|
||||
"-m",
|
||||
"fixture",
|
||||
];
|
||||
await git(commit);
|
||||
const { stdout: baseline } = await git(["rev-parse", "HEAD"]);
|
||||
await git(["update-ref", "refs/remotes/origin/main", baseline.trim()]);
|
||||
await git(["switch", "-c", "regression"]);
|
||||
await writeFile(join(root, "src/value.ts"), "export const value = 2;\n");
|
||||
await writeFile(
|
||||
join(root, "src/value.test.ts"),
|
||||
`import { it, expect } from "vitest"; import { value } from "./value"; it("checks current source", () => expect(value).toBe(2));\n`,
|
||||
);
|
||||
await git(["add", "src"]);
|
||||
await git(commit);
|
||||
expect((await git(["status", "--porcelain"])).stdout).toBe("");
|
||||
const { stdout } = await execute(process.execPath, [runner, "changed"], {
|
||||
cwd: root,
|
||||
});
|
||||
expect(stdout).toContain("1 passed");
|
||||
const explicit = await execute(
|
||||
process.execPath,
|
||||
[runner, "focused", "src/value.test.ts"],
|
||||
{ cwd: root },
|
||||
);
|
||||
expect(explicit.stdout).toContain("1 passed");
|
||||
}, 30_000);
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
developmentTestPlan,
|
||||
parseDevelopmentTestRequest,
|
||||
} from "../../scripts/lib/development-tests.mjs";
|
||||
|
||||
describe("development test selection", () => {
|
||||
it("keeps dirty source feedback independent of compiled artifacts", () => {
|
||||
const plan = developmentTestPlan(parseDevelopmentTestRequest("local", []));
|
||||
expect(plan.needsBuild).toBe(false);
|
||||
expect(plan.vitestArguments).toContain("--changed");
|
||||
expect(plan.vitestArguments).not.toContain("acceptance");
|
||||
expect(plan.vitestArguments).not.toContain("process-global");
|
||||
});
|
||||
|
||||
it("always runs explicitly selected source tests, including on a clean tree", () => {
|
||||
const path = "src/config.test.ts";
|
||||
const plan = developmentTestPlan(
|
||||
parseDevelopmentTestRequest("local", [path, path]),
|
||||
);
|
||||
expect(plan.needsBuild).toBe(false);
|
||||
expect(
|
||||
plan.vitestArguments.filter((argument) => argument === path),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
plan.vitestArguments.some((argument) => argument.startsWith("--changed")),
|
||||
).toBe(false);
|
||||
expect(plan.vitestArguments).not.toContain("--passWithNoTests");
|
||||
});
|
||||
|
||||
it("builds before explicitly selected compiled boundaries", () => {
|
||||
const plan = developmentTestPlan(
|
||||
parseDevelopmentTestRequest("focused", [
|
||||
"src/config.test.ts",
|
||||
"tests/acceptance/applications/runtime.test.ts",
|
||||
]),
|
||||
);
|
||||
expect(plan.needsBuild).toBe(true);
|
||||
expect(plan.vitestArguments).not.toContain("--passWithNoTests");
|
||||
expect(plan.vitestArguments).not.toContain("--changed");
|
||||
});
|
||||
|
||||
it("uses the resolved branch merge base for committed source changes", () => {
|
||||
const request = parseDevelopmentTestRequest("changed", ["--base", "main"]);
|
||||
const plan = developmentTestPlan(request, "a".repeat(40));
|
||||
expect(request.base).toBe("main");
|
||||
expect(plan.vitestArguments).toContain(`--changed=${"a".repeat(40)}`);
|
||||
expect(() => developmentTestPlan(request)).toThrow(
|
||||
"resolved Git merge base",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["focused", []],
|
||||
["focused", ["../src/config.test.ts"]],
|
||||
["focused", ["src/../config.test.ts"]],
|
||||
["focused", ["--passWithNoTests"]],
|
||||
["focused", ["--changed"]],
|
||||
["local", ["tests/acceptance/applications/runtime.test.ts"]],
|
||||
["changed", ["--base"]],
|
||||
["changed", ["--base=--help"]],
|
||||
["changed", ["src/config.test.ts"]],
|
||||
["local", ["--base", "main"]],
|
||||
["unknown", []],
|
||||
])("rejects invalid %s selections: %j", (mode, paths) => {
|
||||
expect(() => parseDevelopmentTestRequest(mode, paths)).toThrow();
|
||||
});
|
||||
});
|
||||
10
turbo.json
10
turbo.json
|
|
@ -12,6 +12,8 @@
|
|||
"scripts/lib/generated-file.mjs",
|
||||
"skills/reverse-engineer-anything/SKILL.md",
|
||||
"src/**/*.ts",
|
||||
"!src/**/*.test.ts",
|
||||
"!src/**/*.fixture.ts",
|
||||
"tsconfig.json",
|
||||
"tsconfig.build.json"
|
||||
],
|
||||
|
|
@ -90,8 +92,12 @@
|
|||
"package.json",
|
||||
"package-lock.json",
|
||||
"src/**/*.ts",
|
||||
"!src/**/*.test.ts",
|
||||
"!src/**/*.fixture.ts",
|
||||
"tsconfig.json",
|
||||
"typedoc.json"
|
||||
"typedoc.json",
|
||||
".nvmrc",
|
||||
"tsconfig.build.json"
|
||||
],
|
||||
"outputs": ["build/api-docs/**"],
|
||||
"outputLogs": "new-only"
|
||||
|
|
@ -101,7 +107,7 @@
|
|||
"cache": false
|
||||
},
|
||||
"docs:check:built": {
|
||||
"dependsOn": ["build", "docs:api"],
|
||||
"dependsOn": ["build"],
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
typedoc.json
10
typedoc.json
|
|
@ -15,8 +15,14 @@
|
|||
],
|
||||
"entryPointStrategy": "expand",
|
||||
"out": "build/api-docs",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"exclude": ["**/*.test.ts", "tests/**", "dist/**", "node_modules/**"],
|
||||
"tsconfig": "tsconfig.build.json",
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
"**/*.fixture.ts",
|
||||
"tests/**",
|
||||
"dist/**",
|
||||
"node_modules/**"
|
||||
],
|
||||
"excludeExternals": true,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": false,
|
||||
|
|
|
|||
Loading…
Reference in New Issue