chore(test): separate development feedback from full verification (#599)

This commit is contained in:
N0zoM1z0 2026-10-06 00:59:26 +08:00 committed by GitHub
parent 9c25dc4ed2
commit 82957c98bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 479 additions and 66 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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"],

View File

@ -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}": [

View File

@ -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[];
};

View File

@ -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",
]),
],
};
};

View File

@ -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 }));
});
}

View File

@ -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);
});

View File

@ -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();
});
});

View File

@ -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
}
}

View File

@ -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,