build: streamline local and CI validation

This commit is contained in:
morluto 2026-07-22 15:58:29 +00:00
parent 9d84cad622
commit c08b9a7d6c
22 changed files with 316 additions and 231 deletions

View File

@ -6,6 +6,13 @@ on:
pull_request:
branches: [main]
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
jobs:
pr-title:
name: Conventional PR title
@ -23,77 +30,78 @@ jobs:
process.exit(1);
}'
typecheck:
changes:
name: Classify changes
runs-on: ubuntu-latest
outputs:
package: ${{ steps.package.outputs.required }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Select package and Windows lanes
id: package
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
echo "required=true" >> "${GITHUB_OUTPUT}"
elif git diff --quiet "${BASE_SHA}" "${HEAD_SHA}" -- \
. \
':(exclude)docs/**' \
':(exclude)AGENTS.md' \
':(exclude)CONTRIBUTING.md'; then
echo "required=false" >> "${GITHUB_OUTPUT}"
else
echo "required=true" >> "${GITHUB_OUTPUT}"
fi
static:
name: Static checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run check:ci
typecheck-compatibility:
name: Typecheck
needs: static
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run typecheck
- name: Require successful static checks
env:
STATIC_RESULT: ${{ needs.static.result }}
run: test "${STATIC_RESULT}" = "success"
lint:
lint-compatibility:
name: Lint (oxlint)
needs: static
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run lint
- name: Require successful static checks
env:
STATIC_RESULT: ${{ needs.static.result }}
run: test "${STATIC_RESULT}" = "success"
format:
format-compatibility:
name: Format Check
needs: static
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run format:check
dead-code:
name: Dead Code (knip)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run lint:dead
duplicates:
name: Duplicate Code (jscpd)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run jscpd
tech-debt:
name: Tech Debt Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run scan:todos
- name: Require successful static checks
env:
STATIC_RESULT: ${{ needs.static.result }}
run: test "${STATIC_RESULT}" = "success"
test:
name: Test (coverage + timing)
@ -105,7 +113,7 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run test:coverage -- --reporter=verbose --reporter=junit --outputFile.junit=test-results.xml
- run: npm run test:ci
- uses: actions/upload-artifact@v4
if: always()
with:
@ -115,6 +123,8 @@ jobs:
package-e2e:
name: Package E2E (${{ matrix.os }})
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
strategy:
fail-fast: false
matrix:
@ -131,6 +141,8 @@ jobs:
windows-curated:
name: Windows Ghidra P0 and package lane
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: windows-latest
timeout-minutes: 25
steps:
@ -140,7 +152,7 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run build
- run: npm run build:cached
- run: npm run typecheck
- name: Record named Windows host capabilities
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json

View File

@ -13,6 +13,13 @@ on:
- "package-lock.json"
workflow_dispatch:
concurrency:
group: real-browser-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
permissions:
contents: read

View File

@ -4,6 +4,9 @@ on:
repository_dispatch:
types: [real-ghidra-windows]
env:
HUSKY: "0"
permissions:
contents: read

View File

@ -3,6 +3,9 @@ name: Real Hopper Linux E2E
on:
workflow_dispatch:
env:
HUSKY: "0"
permissions:
contents: read

View File

@ -3,6 +3,9 @@ name: Real Hopper E2E
on:
workflow_dispatch:
env:
HUSKY: "0"
permissions:
contents: read

View File

@ -13,6 +13,13 @@ on:
- "package-lock.json"
workflow_dispatch:
concurrency:
group: real-replay-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
permissions:
contents: read

View File

@ -4,6 +4,9 @@ on:
push:
branches: [main]
env:
HUSKY: "0"
permissions:
contents: write
pull-requests: write

View File

@ -50,14 +50,20 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec
- `npm ci`: install exact lockfile dependencies.
- `npm run deps:check`: verify that installed direct dependency versions match the root lockfile; on failure, run `npm ci`, then retry.
- `npm run metadata:generate`: refresh checked-in package metadata after package, lockfile, or skill-version changes.
- `npm run metadata:check`: verify checked-in package metadata without rewriting it.
- `npm run build`: compile `src/` into `dist/`.
- `npm run build:cached`: compile through Turbo and reuse a valid local build cache entry.
- `npm test`: build, then run the Vitest suite once.
- `npm test`: restore or build `dist/`, then run the Vitest suite once without coverage or retries locally.
- `npm run test:fast`: run the parallel test project without the serial integration group.
- `npm run test:integration`: run the serial filesystem, process, and CLI integration group.
- `npm run test:watch`: watch tests affected by working-tree changes in the parallel project, without coverage.
- `npm run test:coverage`: run the complete suite with coverage; CI owns verbose and JUnit reporting.
- `npm run typecheck`: run strict TypeScript checks without emitting files.
- `npm run lint`: apply oxlint rules (complexity, max-lines, unused vars, and TypeScript-specific checks).
- `npm run lint:fix`: auto-fix oxlint violations where possible.
- `npm run format:check`: verify Prettier formatting.
- `npm run check`: run cached typecheck, lint, format:check, and knip tasks.
- `npm run check`: run cached typecheck, lint, format:check, knip, and package-metadata freshness tasks.
- `npm run check:ci`: run the static gate plus duplicate-code and debt-marker scans.
- `npm run check:test`: run the cached static tasks plus the uncached complete test suite.
- `npm run check:pr`: run the test gate plus uncached generated-document validation for PR preparation.
- `npm run knip`: detect unused files, dependencies, and exports.

View File

@ -12,10 +12,10 @@ npm test
```
`npm ci` installs the exact dependencies and prepares the Husky hooks without
building the project. Run `npm run build` when you need the standalone CLI or
MCP server. Turbo caches deterministic local builds and static checks across
Git worktrees; use `npm run build:cached` when you want that cache. After a
package, lockfile, or managed-skill version change, run
building the project. Run `npm run build:cached` when you need the standalone
CLI or MCP server; `npm run build` remains the uncached compiler leaf used by
Turbo. Turbo caches deterministic builds and static checks across Git
worktrees. After a package, lockfile, or managed-skill version change, run
`npm run metadata:generate` before building.
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.
@ -28,15 +28,26 @@ npm run verify:package
npm pack --dry-run
```
`npm run check` runs the cached typecheck, lint, formatting, and dead-code tasks;
`check:fast` remains as an alias. Use `npm run check:test` when the complete test
suite is relevant. Pre-commit formats and lints only staged files, and pre-push
runs `check`. `check:pr` additionally renders API documentation and checks all
committed generated metadata. Real provider and replay verifiers remain outside
Turbo's cache. CI uploads the rendered TypeDoc site as an `api-docs` artifact;
the generated HTML is not committed. The normal test and CI coverage runs
enforce the thresholds in `vitest.config.ts`; `npm run lint:dead` rejects unused
files, exports, and dependencies.
`npm run check` runs cached typecheck, lint, formatting, dead-code, and package
metadata freshness tasks; `check:fast` remains as an alias. Use `npm run
check:test` when the complete test suite is relevant. Pre-commit formats and
lints only staged files, and pre-push runs `check`. `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.
Local `npm test` runs without coverage, retries, or verbose output. Use `npm run
test:fast` for the parallel group, `npm run test:integration` for serial
filesystem/process/CLI cases, and `npm run test:watch` to watch tests affected by
working-tree changes. CI runs `npm run test:ci`, which adds coverage, retries,
verbose output, and JUnit results. Coverage thresholds remain in
`vitest.config.ts`.
CI installs dependencies once for all static checks, cancels superseded PR
runs, and skips package and Windows lanes when a PR changes only files outside
the package/runtime/test boundary. TypeDoc renders only in pull-request CI; it
does not run in local commit or pre-push hooks, or in post-merge `main` CI.
Tests that need a temporary directory must use
`createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`. The

View File

@ -44,6 +44,8 @@
"bridge/ghidra/ReaGhidraBridge.java",
"scripts/rea.mjs",
"scripts/hopper-demo-x11.py",
"scripts/prepack.mjs",
"scripts/prepare.mjs",
"skills",
"README.md",
"LICENSE"
@ -51,16 +53,18 @@
"scripts": {
"deps:check": "node scripts/check-dependency-install.mjs",
"metadata:generate": "node scripts/generate-package-metadata.mjs",
"prebuild": "npm run deps:check && node scripts/generate-package-metadata.mjs --check",
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
"prebuild": "npm run deps:check && npm run metadata:check",
"build": "tsc -p tsconfig.build.json",
"postbuild": "node scripts/record-build-cache.mjs",
"precheck": "npm run deps:check",
"check": "turbo run typecheck lint format:check knip",
"check": "turbo run typecheck lint format:check knip metadata:check",
"check:fast": "npm run check",
"precheck:test": "npm run deps:check",
"check:test": "turbo run typecheck lint format:check knip test:run",
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "turbo run typecheck lint format:check knip test:run docs:check:built",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run docs:check:built",
"precheck:ci": "npm run deps:check",
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
"build:cached": "turbo run build",
"format": "prettier --write .",
"format:check": "prettier --check .",
@ -71,42 +75,49 @@
"jscpd": "jscpd src/ tests/",
"scan:todos": "node scripts/scan-todos.mjs",
"start": "node scripts/rea.mjs mcp",
"pretest": "npm run build",
"pretest": "npm run build:cached",
"test": "vitest run",
"test:run": "vitest run",
"pretest:coverage": "npm run build",
"pretest:fast": "npm run build:cached",
"test:fast": "vitest run --project parallel",
"pretest:integration": "npm run build:cached",
"test:integration": "vitest run --project serial-integration",
"pretest:coverage": "npm run build:cached",
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
"test:ci": "npm run test:coverage -- --reporter=verbose --reporter=junit --outputFile.junit=test-results.xml",
"pretest:verbose": "npm run build:cached",
"test:verbose": "vitest run --reporter=verbose",
"test:watch": "vitest",
"pretest:watch": "npm run build:cached",
"test:watch": "vitest --project parallel --changed",
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build && node scripts/verify-real-hopper.mjs",
"verify:ghidra": "npm run build && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:windows": "npm run build && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build && node scripts/verify-real-browser.mjs",
"verify:javascript-runtime": "npm run build && node scripts/verify-local-javascript-runtime.mjs",
"verify:application-workflows": "npm run build && node scripts/verify-local-application-workflows.mjs",
"verify:managed": "npm run build && node scripts/verify-managed-conformance.mjs",
"verify:replay": "npm run build && node scripts/verify-real-javascript-replay.mjs",
"verify:agent": "npm run build && node scripts/verify-agent-experience.mjs",
"verify:hopper:linux": "npm run verify:fixtures && npm run build && node scripts/verify-real-hopper-linux.mjs",
"verify:setup:linux": "npm run build && node scripts/verify-linux-setup-e2e.mjs",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
"verify:managed": "npm run build:cached && node scripts/verify-managed-conformance.mjs",
"verify:replay": "npm run build:cached && node scripts/verify-real-javascript-replay.mjs",
"verify:agent": "npm run build:cached && node scripts/verify-agent-experience.mjs",
"verify:hopper:linux": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-linux.mjs",
"verify:setup:linux": "npm run build:cached && node scripts/verify-linux-setup-e2e.mjs",
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
"build:ghidra-windows-fixture": "node scripts/create-ghidra-windows-fixture.mjs",
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
"verify:package": "npm run build && node scripts/verify-package.mjs",
"verify:package:windows": "npm run build && node scripts/verify-windows-package.mjs",
"probe:windows-capabilities": "npm run build && node scripts/windows-capabilities.mjs",
"verify:package": "npm run build:cached && node scripts/verify-package.mjs",
"verify:package:windows": "npm run build:cached && node scripts/verify-windows-package.mjs",
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
"verify:published": "node scripts/verify-published-package.mjs",
"config:print": "node scripts/print-mcp-config.mjs",
"docs:api": "typedoc",
"docs:generate": "npm run metadata:generate && npm run build && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && npm run docs:api && node scripts/generate-error-schema.mjs",
"docs:check": "npm run build && npm run docs:check:built",
"docs:generate": "npm run metadata:generate && npm run build:cached && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && npm run docs:api && node scripts/generate-error-schema.mjs",
"docs:check": "turbo run docs:check:built",
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/check-doc-facts.mjs && npm run docs:api && node scripts/generate-error-schema.mjs --check",
"docs:error-schema": "npm run build && node scripts/generate-error-schema.mjs",
"prepack": "npm run build && node scripts/generate-skill-metadata.mjs --check",
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs",
"prepublishOnly": "npm run deps:check && node scripts/generate-skill-metadata.mjs --check",
"prepare": "husky"
"prepare": "node scripts/prepare.mjs"
},
"lint-staged": {
"*.{ts,mjs,json,md,yml,yaml}": "prettier --write",

View File

@ -1,103 +0,0 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
const CACHE_DIRECTORY = ["node_modules", ".cache"];
const BUILD_INFO_NAME = "rea-tsconfig-build.tsbuildinfo";
const MANIFEST_NAME = "rea-tsconfig-build-outputs.json";
const isMissing = (cause) =>
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
cause.code === "ENOENT";
const outputFiles = async (root) => {
const sourceRoot = join(root, "src");
const entries = await readdir(sourceRoot, { recursive: true });
return entries
.filter((entry) => entry.endsWith(".ts") && !entry.endsWith(".d.ts"))
.sort()
.map((entry) => {
const output = `${entry.slice(0, -3)}.js`;
return { output, path: join(root, "dist", output) };
});
};
const digest = async (path) =>
createHash("sha256")
.update(await readFile(path))
.digest("hex");
const cachePath = (root, name) => join(root, ...CACHE_DIRECTORY, name);
const invalidate = async (root) => {
await Promise.all([
rm(cachePath(root, BUILD_INFO_NAME), { force: true }),
rm(cachePath(root, MANIFEST_NAME), { force: true }),
]);
};
const readManifest = async (root) => {
try {
const parsed = JSON.parse(
await readFile(cachePath(root, MANIFEST_NAME), "utf8"),
);
if (
typeof parsed !== "object" ||
parsed === null ||
parsed.version !== 1 ||
typeof parsed.outputs !== "object" ||
parsed.outputs === null ||
Array.isArray(parsed.outputs)
)
return undefined;
return parsed.outputs;
} catch (cause) {
if (isMissing(cause) || cause instanceof SyntaxError) return undefined;
throw cause;
}
};
/** Invalidates incremental state when generated JavaScript was removed or changed. */
export const validateBuildCache = async (root) => {
const [files, manifest] = await Promise.all([
outputFiles(root),
readManifest(root),
]);
if (manifest === undefined || Object.keys(manifest).length !== files.length) {
await invalidate(root);
return;
}
const matches = await Promise.all(
files.map(async ({ output, path }) => {
if (typeof manifest[output] !== "string") return false;
try {
return (await digest(path)) === manifest[output];
} catch (cause) {
if (isMissing(cause)) return false;
throw cause;
}
}),
);
if (!matches.every(Boolean)) await invalidate(root);
};
/** Records generated JavaScript so a later incremental build can verify its cache. */
export const recordBuildCache = async (root) => {
const files = await outputFiles(root);
const entries = await Promise.all(
files.map(async ({ output, path }) => [output, await digest(path)]),
);
const source = `${JSON.stringify({ version: 1, outputs: Object.fromEntries(entries) })}\n`;
const directory = join(root, ...CACHE_DIRECTORY);
await mkdir(directory, { recursive: true });
const path = cachePath(root, MANIFEST_NAME);
let existing;
try {
existing = await readFile(path, "utf8");
} catch (cause) {
if (!isMissing(cause)) throw cause;
}
if (existing !== source) await writeFile(path, source, "utf8");
};

View File

@ -2,7 +2,6 @@ import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { validateBuildCache } from "./build-cache.mjs";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
const arguments_ = new Set(process.argv.slice(2));
@ -47,4 +46,3 @@ await ensureGeneratedFile({
check: arguments_.has("--check"),
generateCommand: "npm run metadata:generate",
});
await validateBuildCache(root);

55
scripts/prepack.mjs Normal file
View File

@ -0,0 +1,55 @@
import { spawnSync } from "node:child_process";
import { access } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const pathExists = async (path) => {
try {
await access(path);
return true;
} catch (cause) {
if (
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
cause.code === "ENOENT"
)
return false;
throw cause;
}
};
const run = (command, args) => {
const result = spawnSync(command, args, {
cwd: root,
encoding: "utf8",
maxBuffer: 4 * 1_024 * 1_024,
});
if (result.error !== undefined) throw result.error;
if (result.status !== 0) {
if (result.stdout.length > 0) process.stdout.write(result.stdout);
if (result.stderr.length > 0) process.stderr.write(result.stderr);
throw new Error(
`Prepack command failed with status ${String(result.status)}: ${command} ${args.join(" ")}`,
);
}
};
if (await pathExists(join(root, "src"))) {
const packageManagerPath = process.env.npm_execpath;
if (packageManagerPath === undefined)
throw new Error("Prepack could not resolve the active package manager");
run(process.execPath, [packageManagerPath, "run", "build:cached"]);
run(process.execPath, ["scripts/generate-skill-metadata.mjs", "--check"]);
} else {
for (const path of [
"dist/main.js",
"dist/mcpDoctor.js",
"dist/cli.js",
"dist/cliOutput.js",
"skills/reverse-engineer-anything/SKILL.md",
])
await access(join(root, path));
}

28
scripts/prepare.mjs Normal file
View File

@ -0,0 +1,28 @@
import { access } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const sourceHuskyIsAvailable = async () => {
try {
await access(join(root, "src"));
await access(join(root, "node_modules", "husky", "index.js"));
return true;
} catch (cause) {
if (
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
cause.code === "ENOENT"
)
return false;
throw cause;
}
};
if (process.env.HUSKY !== "0" && (await sourceHuskyIsAvailable())) {
const { default: installHusky } = await import("husky");
const message = installHusky();
if (message.length > 0) process.stdout.write(`${message}\n`);
}

View File

@ -1,7 +0,0 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { recordBuildCache } from "./build-cache.mjs";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
await recordBuildCache(root);

View File

@ -100,5 +100,17 @@ export async function verifyPackageInstall({
throw new Error(
`packaged CLI did not degrade without the optional PTY binary: ${JSON.stringify(noOptionalCapability)}`,
);
await verifyInstalledPackageCanRepack(prefix, environment);
return { cli, packageRunnerCli };
}
const verifyInstalledPackageCanRepack = async (prefix, environment) => {
const installedPackageRoot = join(prefix, "lib/node_modules/rea-agents");
const repackedTarball = (
await exec("npm", ["pack", "--silent"], {
cwd: installedPackageRoot,
env: environment,
})
).stdout.trim();
await rm(join(installedPackageRoot, repackedTarball), { force: true });
};

View File

@ -13,6 +13,7 @@ describe("package identity", () => {
bin: z.record(z.string(), z.string()),
scripts: z.object({
start: z.string(),
"metadata:check": z.string(),
prebuild: z.string(),
prepare: z.string(),
prepack: z.string(),
@ -42,13 +43,14 @@ describe("package identity", () => {
`npx -y ${packageJson.name}@${PRODUCT_IDENTITY.packageVersion} mcp`,
);
expect(packageJson.scripts.start).toBe("node scripts/rea.mjs mcp");
expect(packageJson.scripts["metadata:check"]).toBe(
"node scripts/generate-package-metadata.mjs --check",
);
expect(packageJson.scripts.prebuild).toBe(
"npm run deps:check && node scripts/generate-package-metadata.mjs --check",
);
expect(packageJson.scripts.prepare).toBe("husky");
expect(packageJson.scripts.prepack).toBe(
"npm run build && node scripts/generate-skill-metadata.mjs --check",
"npm run deps:check && npm run metadata:check",
);
expect(packageJson.scripts.prepare).toBe("node scripts/prepare.mjs");
expect(packageJson.scripts.prepack).toBe("node scripts/prepack.mjs");
expect(packageJson.scripts.prepublishOnly).toBe(
"npm run deps:check && node scripts/generate-skill-metadata.mjs --check",
);

View File

@ -18,6 +18,12 @@ describe("package installation workflows", () => {
expect(continuousIntegration).toContain("os: [ubuntu-latest, macos-14]");
expect(continuousIntegration).toContain("npm run verify:package");
expect(continuousIntegration).toContain("name: Static checks");
expect(continuousIntegration).toContain("npm run check:ci");
expect(continuousIntegration).toContain("needs: changes");
expect(continuousIntegration).toContain(
"cancel-in-progress: ${{ github.event_name == 'pull_request' }}",
);
for (const workflow of [
continuousIntegration,
realHopperLinux,

View File

@ -1,15 +1,29 @@
import { execFile } from "node:child_process";
import { readdir } from "node:fs/promises";
import { availableParallelism } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { SERIAL_INTEGRATION_TESTS } from "../vitest.config.js";
import vitestConfiguration, {
SERIAL_INTEGRATION_TESTS,
} from "../vitest.config.js";
const execute = promisify(execFile);
describe("Vitest project configuration", () => {
it("keeps coverage and noisy reporting out of the local test loop", () => {
const continuousIntegration = process.env.CI === "true";
expect(vitestConfiguration.test?.coverage?.enabled).toBe(false);
expect(vitestConfiguration.test?.reporters).toEqual(["default"]);
expect(vitestConfiguration.test?.retry).toBe(continuousIntegration ? 2 : 0);
expect(vitestConfiguration.test?.maxWorkers).toBe(
Math.min(2, availableParallelism()),
);
});
it("collects exact disjoint parallel and serial test sets", async () => {
const { stdout } = await execute(
process.execPath,

View File

@ -1,9 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": true,
"rootDir": "src",
"tsBuildInfoFile": "node_modules/.cache/rea-tsconfig-build.tsbuildinfo",
"types": ["node"]
},
"include": ["src/**/*.ts"],

View File

@ -4,10 +4,11 @@
"build": {
"inputs": [
".nvmrc",
"bridge/**",
"package.json",
"package-lock.json",
"scripts/**",
"scripts/check-dependency-install.mjs",
"scripts/generate-package-metadata.mjs",
"scripts/lib/generated-file.mjs",
"skills/reverse-engineer-anything/SKILL.md",
"src/**/*.ts",
"tsconfig.json",
@ -27,9 +28,21 @@
"vitest.config.ts"
]
},
"metadata:check": {
"inputs": [
"package.json",
"package-lock.json",
"scripts/generate-package-metadata.mjs",
"scripts/lib/generated-file.mjs",
"skills/reverse-engineer-anything/SKILL.md",
"src/generatedPackageMetadata.ts"
]
},
"lint": {},
"format:check": {},
"knip": {},
"jscpd": {},
"scan:todos": {},
"test:run": {
"dependsOn": ["build"],
"cache": false

View File

@ -5,6 +5,9 @@ import { join } from "node:path";
import { defineConfig } from "vitest/config";
export const TEST_FILES = ["tests/**/*.test.ts"];
const IS_CONTINUOUS_INTEGRATION = process.env.CI === "true";
// Subprocess-heavy parallel tests exceed their existing deadlines above two workers.
const MAX_TEST_WORKERS = 2;
const PROCESS_CAPTURE_TEST = "tests/processCapture.test.ts";
const CANONICAL_TEMPORARY_DIRECTORY = realpathSync(tmpdir());
export const SERIAL_INTEGRATION_TESTS = [
@ -17,7 +20,7 @@ export const SERIAL_INTEGRATION_TESTS = [
"tests/runtime.test.ts",
];
const isolatedProjects = {
maxWorkers: Math.min(2, availableParallelism()),
maxWorkers: Math.min(MAX_TEST_WORKERS, availableParallelism()),
projects: [
{
extends: true as const,
@ -44,10 +47,10 @@ export default defineConfig({
test: {
env: { TMPDIR: CANONICAL_TEMPORARY_DIRECTORY },
...isolatedProjects,
retry: 2,
reporters: ["default", "verbose"],
retry: IS_CONTINUOUS_INTEGRATION ? 2 : 0,
reporters: ["default"],
coverage: {
enabled: true,
enabled: false,
provider: "v8",
reportsDirectory: join(
tmpdir(),