From 905cbaf2398c905e96a1492b6145ece13e91c3cc Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:47:52 +0800 Subject: [PATCH] perf(ci): use tsrs and reduce PE acceptance startup (#5795) Use pinned tsrs on supported development hosts while retaining stable official TypeScript on Windows and unsupported platforms. Partition compile/typecheck caches by actual compiler and qualify fallback hosts for compiler inputs and lifecycle hooks. Reduce PE acceptance startup through the existing bounded compiled CLI. Retain upstream named MCP definitions, every fixture/assertion and the original 30s deadline. Verify that browser transport loss preserves the observed target. Validation: full qualification and real host lanes in https://github.com/morluto/rea/actions/runs/39590450889 ; 450 focused regressions passed with two documented conditional skips. All 1724 production emit files match SHA-256 and size across both compilers. Current-head Codex/Greptile review found no actionable issues. Measurements and limitations are in the PR. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage) --- .github/workflows/ci.yml | 6 + docs/testing.md | 30 ++- licenses/npm-dependencies.txt | 32 ++- package-lock.json | 118 ++++++++++ package.json | 43 ++-- scripts/ci/plan.mjs | 25 +++ scripts/ci/scopes.mjs | 12 + scripts/run-development-tests.mjs | 2 +- scripts/run-turbo.mjs | 11 + scripts/run-typescript.d.mts | 11 + scripts/run-typescript.mjs | 75 +++++++ scripts/verify-real-browser.mjs | 6 + .../binary/AnalysisProviderRegistry.test.ts | 9 +- .../applications/peResources.test.ts | 21 +- .../boundary/filesystem/ciChangeScope.test.ts | 92 +++++++- .../boundary/filesystem/ciTestReports.test.ts | 2 +- .../filesystem/dependencyInstall.test.ts | 4 +- .../filesystem/staticTaskCache.test.ts | 143 +++++++++++- .../filesystem/typescriptCompiler.test.ts | 206 ++++++++++++++++++ .../boundary/mcp/providerSelectionMcp.test.ts | 5 +- .../process-global/curatedWindowsLane.test.ts | 13 ++ turbo.json | 17 +- 22 files changed, 837 insertions(+), 46 deletions(-) create mode 100644 scripts/run-turbo.mjs create mode 100644 scripts/run-typescript.d.mts create mode 100644 scripts/run-typescript.mjs create mode 100644 tests/boundary/filesystem/typescriptCompiler.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a02b8463c1..fe8214e3d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -819,6 +819,11 @@ jobs: with: save: "true" - run: npm ci + - name: Typecheck with the official Windows compiler + env: + GOMAXPROCS: "2" + GOMEMLIMIT: "3GiB" + run: npm run typecheck -- --checkers 2 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: rea-build-cache @@ -849,6 +854,7 @@ jobs: npx vitest run --coverage.enabled=false src/process/capture/FilesystemSnapshot.test.ts tests/process-global/filesystemSnapshotFallbackCancellation.test.ts + tests/boundary/filesystem/typescriptCompiler.test.ts tests/boundary/filesystem/binaryTarget.test.ts tests/boundary/filesystem/safeOutputTree.test.ts tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts diff --git a/docs/testing.md b/docs/testing.md index 4152100bd3..43ba47cde5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -46,6 +46,24 @@ without executing tests or building. A missing Git base reports how to fetch it or select another revision. Source typechecking needs no compiled runtime or generated test metadata. +Typecheck, runtime compilation, and Frida binding verification share +`scripts/run-typescript.mjs`. The pinned tsrs package supplies Linux x64/arm64 +(GNU and musl) and macOS arm64 compilers. Windows and other unsupported hosts +retain the existing stable official TypeScript compiler. Its upstream version +can differ from tsrs, so both compiler tasks hash the selected package and version +through `scripts/run-turbo.mjs` before cache lookup. Outputs from different +compilers occupy different cache entries. Repository npm scripts and focused-test +preparation use that entrypoint; a raw Turbo compiler task without matching +identity fails instead of publishing an ambiguous successful cache entry. +A missing binary on a supported host also fails without silently falling back. + +The Rust compiler defaults to at most two checker threads. For `--noEmit` checks, +`--maxMemory 3G` also sets a soft memory target; emit builds do not use that target. +Explicit compiler arguments can override the defaults. These controls do not +impose a process-family memory limit. Termux's explicit build, Stryker's compiler +API dependency, and independent fixture compiler oracles keep their existing +TypeScript versions. + `npm run build:cached` compiles the CLI/MCP runtime and its packaged skill. Turbo's strict task environment preserves caller-selected `GOMAXPROCS`, `GOMEMLIMIT`, and `UV_THREADPOOL_SIZE` for resource-constrained builds and @@ -233,6 +251,11 @@ and retain representative compiled CLI parity checks. Every distinct source scenario still runs through the public workflow; session-lifecycle scenarios need their own sessions and cleanup assertions. +When a matrix already holds a named tool definition from live `tools/list`, +pass that definition through the SDK's `toolDefinition` call option. This keeps +the selected output-schema validation without eagerly compiling unrelated +catalog schemas. Re-read definitions if the server advertises a changed list. + Report unavailable authority as a named skip. Await owned cleanup before a verifier reports success. Native process inspectors are released at Vitest worker teardown; fork termination does not run Node exit hooks, and per-file @@ -648,8 +671,11 @@ missing entries are omitted as whole entries, and consumers rebuild cache misses including when every entry is unavailable. Malformed plans, non-file entries, and I/O errors other than disappearance still fail publication. After a trusted static run, superseded cross-run Turbo snapshots for that OS and architecture -are deleted down to the two newest generations. The Windows lane relies on the Linux static job for typechecking. They retain the normal build and prepack -commands, which restore matching outputs or rebuild on a cache miss. Native +are deleted down to the two newest generations. The Windows lane also runs the +official compiler's complete source typecheck with two checker threads, +independently of the accelerated Linux checker. Both lanes retain the normal +build and prepack commands, which restore matching outputs or rebuild on a cache +miss. Native artifacts, dependency installs, package archives, and runtime verification remain host-local. Keep platform-dependent outputs out of this shared cache. diff --git a/licenses/npm-dependencies.txt b/licenses/npm-dependencies.txt index 8225eedadf..03d4921e06 100644 --- a/licenses/npm-dependencies.txt +++ b/licenses/npm-dependencies.txt @@ -2,7 +2,7 @@ REA npm dependency inventory Prepared from the root package-lock.json; refresh this inventory when locked dependencies change. License identifiers are upstream lockfile declarations, not a legal assessment. Runtime includes transitive and optional platform packages; development includes build, test and website tooling. -Locked package entries: 752; runtime: 180. +Locked package entries: 758; runtime: 180. Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt. @algolia/abtesting@1.25.0 | development | MIT @@ -1390,6 +1390,31 @@ commander@13.1.0 | runtime | MIT Package: https://www.npmjs.com/package/@toon-format/toon/v/2.3.1 Archive: https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.1.tgz Integrity: sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw== +@tsrs/darwin-arm64@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0 + Lockfile path: node_modules/@tsrs/darwin-arm64 + Package: https://www.npmjs.com/package/@tsrs/darwin-arm64/v/0.18.0-ts7.1.0-dev.20260929 + Archive: https://registry.npmjs.org/@tsrs/darwin-arm64/-/darwin-arm64-0.18.0-ts7.1.0-dev.20260929.tgz + Integrity: sha512-sAirMpaGA9uRQm2W+t4zC7PjnijNuFa4PCSIfLKKo0JyAUCRfnMeev0UGBQ9F3g+9Nz187tvikUs5Irh5+eajA== +@tsrs/linux-arm64-gnu@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0 + Lockfile path: node_modules/@tsrs/linux-arm64-gnu + Package: https://www.npmjs.com/package/@tsrs/linux-arm64-gnu/v/0.18.0-ts7.1.0-dev.20260929 + Archive: https://registry.npmjs.org/@tsrs/linux-arm64-gnu/-/linux-arm64-gnu-0.18.0-ts7.1.0-dev.20260929.tgz + Integrity: sha512-zm9/z9yylig68JiZF4Aq0Cm26n09KEU8YoQRlG7tdDWTBNAIViYw8knxnWYsjMWVZ2XiU4mYq1xdrOKuOgABHg== +@tsrs/linux-arm64-musl@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0 + Lockfile path: node_modules/@tsrs/linux-arm64-musl + Package: https://www.npmjs.com/package/@tsrs/linux-arm64-musl/v/0.18.0-ts7.1.0-dev.20260929 + Archive: https://registry.npmjs.org/@tsrs/linux-arm64-musl/-/linux-arm64-musl-0.18.0-ts7.1.0-dev.20260929.tgz + Integrity: sha512-Lw7VVU/YQ9JcarODICEiwzMLdFqZprqh5RGTgbS5Uc6I95P635/g5xA1pOC8RVs4efBvAR30J54iDG3l5JEYiw== +@tsrs/linux-x64-gnu@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0 + Lockfile path: node_modules/@tsrs/linux-x64-gnu + Package: https://www.npmjs.com/package/@tsrs/linux-x64-gnu/v/0.18.0-ts7.1.0-dev.20260929 + Archive: https://registry.npmjs.org/@tsrs/linux-x64-gnu/-/linux-x64-gnu-0.18.0-ts7.1.0-dev.20260929.tgz + Integrity: sha512-EdBwBgNICGu1r1h23ixdcKwsMo6E9ll7ZsY3mNERNYqE1Pwdtcat99d5E7zB1J+oy/Xk6YqiizaB6nolIjpDTw== +@tsrs/linux-x64-musl@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0 + Lockfile path: node_modules/@tsrs/linux-x64-musl + Package: https://www.npmjs.com/package/@tsrs/linux-x64-musl/v/0.18.0-ts7.1.0-dev.20260929 + Archive: https://registry.npmjs.org/@tsrs/linux-x64-musl/-/linux-x64-musl-0.18.0-ts7.1.0-dev.20260929.tgz + Integrity: sha512-LHk+AOAtXuTlhJhOJ2+elriEIX17ycXljojeNxrrzckOipTG1LksoVWuCSas2nu00EnWAzrdKpt9oqPylUKZfQ== @turbo/darwin-64@2.11.7 | development, optional | MIT Lockfile path: node_modules/@turbo/darwin-64 Package: https://www.npmjs.com/package/@turbo/darwin-64/v/2.11.7 @@ -3400,6 +3425,11 @@ tslib@2.8.1 | development | 0BSD Package: https://www.npmjs.com/package/tslib/v/2.8.1 Archive: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz Integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +tsrs@0.18.0-ts7.1.0-dev.20260929 | development | Apache-2.0 + Lockfile path: node_modules/tsrs + Package: https://www.npmjs.com/package/tsrs/v/0.18.0-ts7.1.0-dev.20260929 + Archive: https://registry.npmjs.org/tsrs/-/tsrs-0.18.0-ts7.1.0-dev.20260929.tgz + Integrity: sha512-9T0b4Uam/yMFIK16GUx0fUrJqYZnJ/gvSmV9JB8NTyDePvwCI45WFWxyEV/UMV3KfdNRJG/PBsm3fSIB9WmWsA== tsx@4.23.15 | development | MIT Lockfile path: node_modules/tsx Package: https://www.npmjs.com/package/tsx/v/4.23.15 diff --git a/package-lock.json b/package-lock.json index a8d20bd23d..392b35cdbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "oxfmt": "0.72.0", "oxlint": "1.87.0", "rolldown": "1.2.12", + "tsrs": "0.18.0-ts7.1.0-dev.20260929", "tsx": "4.23.15", "turbo": "2.11.7", "typescript": "7.0.2", @@ -4407,6 +4408,103 @@ "integrity": "sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw==", "license": "MIT" }, + "node_modules/@tsrs/darwin-arm64": { + "version": "0.18.0-ts7.1.0-dev.20260929", + "resolved": "https://registry.npmjs.org/@tsrs/darwin-arm64/-/darwin-arm64-0.18.0-ts7.1.0-dev.20260929.tgz", + "integrity": "sha512-sAirMpaGA9uRQm2W+t4zC7PjnijNuFa4PCSIfLKKo0JyAUCRfnMeev0UGBQ9F3g+9Nz187tvikUs5Irh5+eajA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@tsrs/linux-arm64-gnu": { + "version": "0.18.0-ts7.1.0-dev.20260929", + "resolved": "https://registry.npmjs.org/@tsrs/linux-arm64-gnu/-/linux-arm64-gnu-0.18.0-ts7.1.0-dev.20260929.tgz", + "integrity": "sha512-zm9/z9yylig68JiZF4Aq0Cm26n09KEU8YoQRlG7tdDWTBNAIViYw8knxnWYsjMWVZ2XiU4mYq1xdrOKuOgABHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@tsrs/linux-arm64-musl": { + "version": "0.18.0-ts7.1.0-dev.20260929", + "resolved": "https://registry.npmjs.org/@tsrs/linux-arm64-musl/-/linux-arm64-musl-0.18.0-ts7.1.0-dev.20260929.tgz", + "integrity": "sha512-Lw7VVU/YQ9JcarODICEiwzMLdFqZprqh5RGTgbS5Uc6I95P635/g5xA1pOC8RVs4efBvAR30J54iDG3l5JEYiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@tsrs/linux-x64-gnu": { + "version": "0.18.0-ts7.1.0-dev.20260929", + "resolved": "https://registry.npmjs.org/@tsrs/linux-x64-gnu/-/linux-x64-gnu-0.18.0-ts7.1.0-dev.20260929.tgz", + "integrity": "sha512-EdBwBgNICGu1r1h23ixdcKwsMo6E9ll7ZsY3mNERNYqE1Pwdtcat99d5E7zB1J+oy/Xk6YqiizaB6nolIjpDTw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@tsrs/linux-x64-musl": { + "version": "0.18.0-ts7.1.0-dev.20260929", + "resolved": "https://registry.npmjs.org/@tsrs/linux-x64-musl/-/linux-x64-musl-0.18.0-ts7.1.0-dev.20260929.tgz", + "integrity": "sha512-LHk+AOAtXuTlhJhOJ2+elriEIX17ycXljojeNxrrzckOipTG1LksoVWuCSas2nu00EnWAzrdKpt9oqPylUKZfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@turbo/darwin-64": { "version": "2.11.7", "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.11.7.tgz", @@ -9832,6 +9930,26 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsrs": { + "version": "0.18.0-ts7.1.0-dev.20260929", + "resolved": "https://registry.npmjs.org/tsrs/-/tsrs-0.18.0-ts7.1.0-dev.20260929.tgz", + "integrity": "sha512-9T0b4Uam/yMFIK16GUx0fUrJqYZnJ/gvSmV9JB8NTyDePvwCI45WFWxyEV/UMV3KfdNRJG/PBsm3fSIB9WmWsA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsrs": "bin/tsrs" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@tsrs/darwin-arm64": "0.18.0-ts7.1.0-dev.20260929", + "@tsrs/linux-arm64-gnu": "0.18.0-ts7.1.0-dev.20260929", + "@tsrs/linux-arm64-musl": "0.18.0-ts7.1.0-dev.20260929", + "@tsrs/linux-x64-gnu": "0.18.0-ts7.1.0-dev.20260929", + "@tsrs/linux-x64-musl": "0.18.0-ts7.1.0-dev.20260929" + } + }, "node_modules/tsx": { "version": "4.23.15", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", diff --git a/package.json b/package.json index 8aab2719b9..4b34fdb6e8 100644 --- a/package.json +++ b/package.json @@ -104,31 +104,31 @@ "metadata:generate": "node scripts/run-exclusive.mjs artifacts node scripts/generate-package-metadata.mjs", "metadata:check": "node scripts/generate-package-metadata.mjs --check", "mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs", - "mcp-catalog:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog", - "mcp-catalog:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run mcp-catalog:check:built", + "mcp-catalog:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:mcp-catalog", + "mcp-catalog:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run mcp-catalog:check:built", "prebuild": "npm run deps:check", - "build": "node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills --force", + "build": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills --force", "precheck": "npm run deps:check", - "check": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards", + "check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards", "precheck:fast": "npm run deps:check", - "check:fast": "turbo run typecheck lint --output-logs=errors-only", + "check:fast": "node scripts/run-turbo.mjs run typecheck lint --output-logs=errors-only", "precheck:changed": "npm run deps:check", "check:changed": "npm run check:fast && npm run test:changed", "precheck:test": "npm run deps:check", - "check:test": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check test:run", + "check:test": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run", "precheck:pr": "npm run deps:check", - "check:pr": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos", + "check:pr": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos", "precheck:ci": "npm run deps:check", "precheck:ci:fast": "npm run deps:check", "precheck:ci:deep": "npm run deps:check", "check:ci": "npm run check:ci:fast && npm run check:ci:deep", "check:ci:fast": "node scripts/run-exclusive.mjs artifacts npm run check:ci:fast:tasks", - "check:ci:fast:tasks": "turbo run typecheck lint:built format:check", + "check:ci:fast:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check", "check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python", - "check:ci:deep:tasks": "turbo run knip metadata:check jscpd scan:todos", - "check:ci:tasks": "turbo run typecheck lint:built format:check knip metadata:check jscpd scan:todos", + "check:ci:deep:tasks": "node scripts/run-turbo.mjs run knip metadata:check jscpd scan:todos", + "check:ci:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check jscpd scan:todos", "ci:static-context": "node scripts/ci/static-task-context.mjs", - "build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills", + "build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills", "build:termux": "node scripts/run-exclusive.mjs artifacts npm run build:termux:run", "build:termux:run": "npm run compile:termux && npm run artifacts:skills", "format": "oxfmt .", @@ -151,7 +151,7 @@ "test:built": "node scripts/ci/test-group.mjs built", "pretest:boundary": "npm run test:prepare", "test:boundary": "vitest run --project boundary --project cli-boundary --project process-boundary --project mcp-boundary --project process-global", - "pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills artifacts:mcp-catalog", + "pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills artifacts:mcp-catalog", "test:mcp": "vitest run --project mcp-boundary", "pretest:acceptance": "npm run build:cached", "test:acceptance": "vitest run --project acceptance", @@ -174,7 +174,7 @@ "test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed", "pretest:watch:all": "npm run test:prepare", "test:watch:all": "vitest --changed", - "typecheck": "tsc --noEmit", + "typecheck": "node scripts/run-typescript.mjs --noEmit", "verify:test-temp-hygiene": "npm run test:prepare && node scripts/verify-test-temp-hygiene.mjs", "verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs", "verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs", @@ -213,7 +213,7 @@ "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:frida": "npm run build:cached && node scripts/verify-frida.mjs", - "verify:frida:bindings": "tsc --noEmit -p tsconfig.frida-verification.json", + "verify:frida:bindings": "node scripts/run-typescript.mjs --noEmit -p tsconfig.frida-verification.json", "verify:browser:dom": "node scripts/verify-browser-dom-destinations.mjs", "verify:browser:network": "npm run build:cached && node scripts/verify-browser-network.mjs", "verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs", @@ -256,17 +256,17 @@ "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", - "evidence:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:managed-evidence", - "evidence:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run evidence:check:built", + "evidence:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:managed-evidence", + "evidence:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run evidence:check:built", "predocs:dev": "npm run docs:generate", "docs:dev": "vitepress dev docs", "predocs:build": "npm run docs:generate", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "docs:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts npm run docs:prepare", - "docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run docs:check:built && npm run test:local -- tests/conformance/readmeConsistency.test.ts", + "docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run docs:check:built && npm run test:local -- tests/conformance/readmeConsistency.test.ts", "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/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check && npm run licenses:check", - "docs:error-schema": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:error-schema", + "docs:error-schema": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:error-schema", "prepack": "node scripts/prepack.mjs", "prepare": "node scripts/prepare.mjs", "verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs", @@ -305,16 +305,16 @@ "verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs", "verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs", "mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check", - "compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs", + "compile": "node scripts/clean-build-output.mjs && node scripts/run-typescript.mjs -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs", "compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs", "artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs", "artifacts:skills": "node scripts/generate-skill-metadata.mjs", "artifacts:product-catalog": "node scripts/generate-product-catalog.mjs", "artifacts:managed-evidence": "node scripts/generate-completion-ledger.mjs", "artifacts:error-schema": "node scripts/generate-error-schema.mjs", - "test:prepare": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog artifacts:product-catalog artifacts:managed-evidence", + "test:prepare": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:mcp-catalog artifacts:product-catalog artifacts:managed-evidence", "evidence:check:built": "node scripts/generate-completion-ledger.mjs --check", - "docs:prepare": "node scripts/generate-package-metadata.mjs && turbo run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema", + "docs:prepare": "node scripts/generate-package-metadata.mjs && node scripts/run-turbo.mjs run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema", "verify:web:qlog": "npm run build:cached && node scripts/verify-qlog-captures.mjs", "licenses:generate": "node scripts/generate-third-party-inventory.mjs", "licenses:check": "node scripts/generate-third-party-inventory.mjs --check" @@ -396,6 +396,7 @@ "oxfmt": "0.72.0", "oxlint": "1.87.0", "rolldown": "1.2.12", + "tsrs": "0.18.0-ts7.1.0-dev.20260929", "tsx": "4.23.15", "turbo": "2.11.7", "typescript": "7.0.2", diff --git a/scripts/ci/plan.mjs b/scripts/ci/plan.mjs index e7c3de8ddb..fb9260bde4 100644 --- a/scripts/ci/plan.mjs +++ b/scripts/ci/plan.mjs @@ -63,6 +63,31 @@ function classifyPackage(base, head) { const after = JSON.parse(git("show", `${head}:package.json`)); const { scripts: beforeScripts, ...beforeRest } = before; const { scripts: afterScripts, ...afterRest } = after; + const compilerDependencies = (metadata) => ({ + dependencies: { + typescript: metadata.dependencies?.typescript, + tsrs: metadata.dependencies?.tsrs, + }, + devDependencies: { + typescript: metadata.devDependencies?.typescript, + tsrs: metadata.devDependencies?.tsrs, + }, + }); + const compilerScripts = [ + "compile", + "typecheck", + "build", + "build:cached", + "test:prepare", + "docs:prepare", + "verify:frida:bindings", + ].flatMap((name) => [name, `pre${name}`, `post${name}`]); + selected.package ||= + JSON.stringify(compilerDependencies(before)) !== + JSON.stringify(compilerDependencies(after)) || + compilerScripts.some( + (name) => beforeScripts?.[name] !== afterScripts?.[name], + ); if (JSON.stringify(beforeRest) !== JSON.stringify(afterRest)) { full("Package metadata or dependencies changed"); return; diff --git a/scripts/ci/scopes.mjs b/scripts/ci/scopes.mjs index 32a15faaa3..54ebaa7d17 100644 --- a/scripts/ci/scopes.mjs +++ b/scripts/ci/scopes.mjs @@ -92,6 +92,17 @@ export const SCOPES = { ], // Four-host installed-package matrix and the Windows curated lane. package: [ + // Both host compilers must be exercised when their installation, options, + // or entrypoints change; Linux's accelerated checker cannot stand in for Go. + "scripts/run-typescript.*", + "scripts/run-turbo.mjs", + "turbo.json", + "tsconfig.json", + "tsconfig.build.json", + "tsconfig.frida-verification.json", + "package-lock.json", + ".nvmrc", + ".npmrc", ".github/actions/publish-mcp/**", ".github/actions/prepare-release-package/**", ".github/workflows/release-recovery.yml", @@ -135,6 +146,7 @@ export const SCOPES = { "src/application/DshCordis*.ts", "src/cli/interactiveSetup.ts", "src/cli/setupCommands.ts", + "tests/boundary/filesystem/typescriptCompiler.test.ts", "tests/boundary/filesystem/binaryTarget.test.ts", "tests/boundary/filesystem/safeOutputTree.test.ts", "tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts", diff --git a/scripts/run-development-tests.mjs b/scripts/run-development-tests.mjs index d53b59c847..8237f8ad58 100644 --- a/scripts/run-development-tests.mjs +++ b/scripts/run-development-tests.mjs @@ -59,7 +59,7 @@ try { "scripts/run-exclusive.mjs", "artifacts", process.execPath, - resolve("node_modules/turbo/bin/turbo"), + resolve("scripts/run-turbo.mjs"), "run", ...(plan.needsBuild ? ["artifacts:skills"] : []), ...plan.artifactTasks, diff --git a/scripts/run-turbo.mjs b/scripts/run-turbo.mjs new file mode 100644 index 0000000000..0c8404bcf1 --- /dev/null +++ b/scripts/run-turbo.mjs @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +import { createRequire } from "node:module"; +import { pathToFileURL } from "node:url"; + +import { resolveTypeScriptCompiler } from "./run-typescript.mjs"; + +// Compute the actual host compiler before Turbo hashes or restores its tasks. +process.env.REA_TYPESCRIPT_COMPILER = resolveTypeScriptCompiler().identity; +const require = createRequire(import.meta.url); +await import(pathToFileURL(require.resolve("turbo/bin/turbo")).href); diff --git a/scripts/run-typescript.d.mts b/scripts/run-typescript.d.mts new file mode 100644 index 0000000000..f5bafbab36 --- /dev/null +++ b/scripts/run-typescript.d.mts @@ -0,0 +1,11 @@ +export function selectTypeScriptCompiler( + platform: string, + arch: string, + optionalDependencies: Readonly>, +): "tsrs" | "typescript"; + +export function resolveTypeScriptCompiler(): { + readonly name: "tsrs" | "typescript"; + readonly identity: string; + readonly launcher: string; +}; diff --git a/scripts/run-typescript.mjs b/scripts/run-typescript.mjs new file mode 100644 index 0000000000..1fc0c2351a --- /dev/null +++ b/scripts/run-typescript.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { availableParallelism } from "node:os"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +/** Use the pinned package's platform inventory; let its launcher select libc. */ +export const selectTypeScriptCompiler = ( + platform, + arch, + optionalDependencies, +) => { + if (platform === "win32") return "typescript"; + const prefix = `@tsrs/${platform}-${arch}`; + return Object.keys(optionalDependencies).some( + (name) => name === prefix || name.startsWith(`${prefix}-`), + ) + ? "tsrs" + : "typescript"; +}; + +export const resolveTypeScriptCompiler = () => { + const require = createRequire(import.meta.url); + const rustPackagePath = require.resolve("tsrs/package.json"); + const rustPackage = JSON.parse(readFileSync(rustPackagePath, "utf8")); + const compiler = selectTypeScriptCompiler( + process.platform, + process.arch, + rustPackage.optionalDependencies, + ); + const packagePath = + compiler === "tsrs" + ? rustPackagePath + : require.resolve("typescript/package.json"); + const metadata = + compiler === "tsrs" + ? rustPackage + : JSON.parse(readFileSync(packagePath, "utf8")); + const command = compiler === "tsrs" ? "tsrs" : "tsc"; + return { + name: compiler, + identity: `${compiler}@${metadata.version}`, + launcher: resolve(dirname(packagePath), metadata.bin[command]), + }; +}; + +const main = async () => { + const compiler = resolveTypeScriptCompiler(); + if ( + process.env.TURBO_HASH !== undefined && + process.env.REA_TYPESCRIPT_COMPILER !== compiler.identity + ) + throw new Error( + "Compiler identity is missing or stale. Run Turbo through scripts/run-turbo.mjs.", + ); + if (compiler.name === "tsrs") { + const args = process.argv.slice(2); + // Rust consumes --maxMemory once; duplicating it becomes an unknown option. + // Preserve explicit resource choices rather than injecting another value. + const defaults = [ + ["--checkers", String(Math.min(2, availableParallelism()))], + ["--maxMemory", "3G"], + ].flatMap(([option, value]) => + args.includes(option) ? [] : [option, value], + ); + process.argv.splice(2, 0, ...defaults); + } + // Delegate execution, diagnostics, status and signals to the package's launcher. + // An unavailable binary on a supported host must fail rather than hide drift. + await import(pathToFileURL(compiler.launcher).href); +}; + +if (process.argv[1] === fileURLToPath(import.meta.url)) await main(); diff --git a/scripts/verify-real-browser.mjs b/scripts/verify-real-browser.mjs index 4ba8d1e964..ed5d8f8680 100644 --- a/scripts/verify-real-browser.mjs +++ b/scripts/verify-real-browser.mjs @@ -555,6 +555,12 @@ async function verifyPageScopedTransport(provider, proxy, origin) { throw new Error( "Page-scoped CDP disconnect must report transport loss without claiming a target crash", ); + const remaining = await provider.listTargets( + listBrowserTargetsInputSchema.parse({ cdp_endpoint: proxy.endpoint }), + ); + if (!remaining.ok) throw remaining.error; + if (!remaining.value.targets.some(({ target_id }) => target_id === target)) + throw new Error("Page-scoped CDP disconnect removed its live target"); } async function browserExecutable() { diff --git a/src/application/binary/AnalysisProviderRegistry.test.ts b/src/application/binary/AnalysisProviderRegistry.test.ts index bd258f3702..a09112ff2f 100644 --- a/src/application/binary/AnalysisProviderRegistry.test.ts +++ b/src/application/binary/AnalysisProviderRegistry.test.ts @@ -5,6 +5,7 @@ import { createAnalysisExecution, type AnalysisClient, type AnalysisProviderCandidate, + type AnalysisProviderInspectionOptions, type CapabilityDescriptor, type ProviderAvailability, type ProviderIdentity, @@ -825,7 +826,9 @@ it("joins cancelled readiness finalization and shutdown before admitting another const registry = new AnalysisProviderRegistry([ { ...base, - inspectAvailability: async ({ signal } = {}) => { + inspectAvailability: async ({ + signal, + }: AnalysisProviderInspectionOptions = {}) => { calls++; if (calls === 1) { observed = signal; @@ -982,7 +985,9 @@ it("registers selection before async readiness can race shutdown", async () => { const registry = new AnalysisProviderRegistry([ { ...base, - inspectAvailability: async ({ signal } = {}) => { + inspectAvailability: async ({ + signal, + }: AnalysisProviderInspectionOptions = {}) => { calls++; observed = signal; return base.inspectAvailability(); diff --git a/tests/acceptance/applications/peResources.test.ts b/tests/acceptance/applications/peResources.test.ts index 202a19e9d4..8b606bc011 100644 --- a/tests/acceptance/applications/peResources.test.ts +++ b/tests/acceptance/applications/peResources.test.ts @@ -9,7 +9,11 @@ import { peResourcesSchema } from "../../../src/domain/native/peResources.js"; import { peResourceFixture } from "../../../src/native/pe/PeResources.fixture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; -import { cliTest, type TestCli } from "../../support/cli/cliFixture.js"; +import { + cliTest, + runFileCli, + type TestCli, +} from "../../support/cli/cliFixture.js"; type PeResourceCall = ( arguments_: Record, @@ -22,7 +26,7 @@ const expectPeInputFailureAcrossAdapters = async (input: { readonly reason: "invalid_format" | "invalid_value"; }) => { const { cli, call, path, reason } = input; - const result = await cli.run({ + const result = await runFileCli(cli, { arguments: ["inspect-pe-resources", path, "--json"], }); expect(result.exitCode).toBe(1); @@ -105,7 +109,7 @@ const expectPeWordAlignedRecordsAcrossAdapters = async (input: { ); const path = join(root, `word-aligned-${plus}-${index}.exe`); await writeFile(path, fixture.bytes); - const result = await cli.run({ + const result = await runFileCli(cli, { arguments: ["inspect-pe-resources", path, "--json"], }); expect(result.exitCode, result.stderr).toBe(0); @@ -196,7 +200,7 @@ const expectPeMappingOwnershipAcrossAdapters = async ( }); continue; } - const result = await cli.run({ + const result = await runFileCli(cli, { arguments: ["inspect-pe-resources", path, "--json"], }); expect(result.exitCode, name).toBe(0); @@ -249,9 +253,14 @@ const expectPeInventoriesAcrossAdapters = async ( `resource${plus ? "64" : "32"}${empty ? "-empty" : ""}.exe`, ); await writeFile(path, fixture.bytes); - const result = await cli.run({ + const invocation = { arguments: ["inspect-pe-resources", path, "--json"], - }); + }; + // Keep an independent public-wrapper process for both PE32 and PE32+. + // The empty-payload variants reuse only compiled-command startup. + const result = await (empty + ? runFileCli(cli, invocation) + : cli.run(invocation)); expect(result.exitCode).toBe(0); const evidence = contract.outputSchema.parse(result.json); const report = peResourcesSchema.parse(evidence.normalized_result); diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index d68af5ab0c..44b8044502 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -660,7 +660,7 @@ it.each([ { code: true, runtime: true, - package: false, + package: true, evm: false, browser: false, }, @@ -1155,7 +1155,7 @@ it.each([ { code: true, runtime: true, - package: false, + package: true, browser: false, evm: false, sqlite: false, @@ -1166,7 +1166,7 @@ it.each([ { code: true, runtime: true, - package: false, + package: true, browser: false, evm: false, }, @@ -1496,3 +1496,89 @@ it("never runs a manual-only verifier from an automatic workflow", async () => { ), ).toEqual([]); }); + +it.each([ + "scripts/run-typescript.mjs", + "scripts/run-typescript.d.mts", + "scripts/run-turbo.mjs", + "turbo.json", + "tsconfig.json", + "tsconfig.build.json", + "tsconfig.frida-verification.json", + "package-lock.json", + ".nvmrc", + ".npmrc", +])("selects package hosts for compiler input %s", async (path) => { + const { directory, base } = await fixture(); + const head = await change(directory, [path]); + const plan = await classify(directory, base, head); + expect(plan).toMatchObject({ + tier: "fast", + code: true, + runtime: true, + package: true, + }); + expect(plan.matrix).toEqual({ + include: [ + { os: "ubuntu-latest", platform: "linux", architecture: "x64" }, + { os: "ubuntu-24.04-arm", platform: "linux", architecture: "arm64" }, + { os: "macos-15", platform: "darwin", architecture: "arm64" }, + ], + }); +}); + +it.each(["typescript", "tsrs"])( + "selects fallback host checks when compiler dependency %s changes", + async (name) => { + const { directory } = await fixture(); + const before = { name: "fixture", devDependencies: { [name]: "1.0.0" } }; + const base = await change( + directory, + ["package.json"], + JSON.stringify(before), + ); + const head = await change( + directory, + ["package.json"], + JSON.stringify({ ...before, devDependencies: { [name]: "2.0.0" } }), + ); + expect(await classify(directory, base, head)).toMatchObject({ + code: true, + runtime: true, + package: true, + }); + }, +); + +it.each([ + "compile", + "typecheck", + "build", + "build:cached", + "test:prepare", + "docs:prepare", + "verify:frida:bindings", + "prebuild:cached", + "postcompile", + "pretypecheck", +])( + "selects fallback host checks when compiler command %s changes", + async (name) => { + const { directory } = await fixture(); + const base = await change( + directory, + ["package.json"], + JSON.stringify({ scripts: { [name]: "node scripts/old.mjs" } }), + ); + const head = await change( + directory, + ["package.json"], + JSON.stringify({ scripts: { [name]: "node scripts/new.mjs" } }), + ); + expect(await classify(directory, base, head)).toMatchObject({ + code: true, + runtime: true, + package: true, + }); + }, +); diff --git a/tests/boundary/filesystem/ciTestReports.test.ts b/tests/boundary/filesystem/ciTestReports.test.ts index 582b8b77fb..815f736a50 100644 --- a/tests/boundary/filesystem/ciTestReports.test.ts +++ b/tests/boundary/filesystem/ciTestReports.test.ts @@ -364,7 +364,7 @@ it("runs the existing onboarding and Python producer guards from their selected "node scripts/run-exclusive.mjs artifacts npm run check:ci:fast:tasks", ); expect(scripts["check:ci:fast:tasks"]).toBe( - "turbo run typecheck lint:built format:check", + "node scripts/run-turbo.mjs run typecheck lint:built format:check", ); expect(scripts["test:python"]).toContain( "bridge/native/test_rea_lldb_tracer.py", diff --git a/tests/boundary/filesystem/dependencyInstall.test.ts b/tests/boundary/filesystem/dependencyInstall.test.ts index 1eef778637..34609de849 100644 --- a/tests/boundary/filesystem/dependencyInstall.test.ts +++ b/tests/boundary/filesystem/dependencyInstall.test.ts @@ -196,6 +196,8 @@ const createBuildCacheFixture = async (): Promise => { "tsconfig.json", "tsconfig.build.json", "scripts/check-dependency-install.mjs", + "scripts/run-turbo.mjs", + "scripts/run-typescript.mjs", "scripts/run-exclusive.mjs", "scripts/run-exclusive-command.mjs", "scripts/lib/windows-owned-command.mjs", @@ -277,7 +279,7 @@ const runTurbo = (cwd: string, environment: NodeJS.ProcessEnv = process.env) => run( process.execPath, [ - join(cwd, "node_modules", "turbo", "bin", "turbo"), + join(cwd, "scripts", "run-turbo.mjs"), "run", "artifacts:skills", "--output-logs=full", diff --git a/tests/boundary/filesystem/staticTaskCache.test.ts b/tests/boundary/filesystem/staticTaskCache.test.ts index fed6cf8e9c..823dc0a7cd 100644 --- a/tests/boundary/filesystem/staticTaskCache.test.ts +++ b/tests/boundary/filesystem/staticTaskCache.test.ts @@ -1,6 +1,7 @@ import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { expect, it } from "vitest"; @@ -8,9 +9,10 @@ import { z } from "zod"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { workflowGit } from "../../support/workflowGit.js"; +import { resolveTypeScriptCompiler } from "../../../scripts/run-typescript.mjs"; const repository = fileURLToPath(new URL("../../../", import.meta.url)); -const turbo = join(repository, "node_modules/turbo/bin/turbo"); +const turbo = join(repository, "scripts/run-turbo.mjs"); const reportScript = join(repository, "scripts/ci/static-check-report.mjs"); const rootConfigSchema = z.object({ globalDependencies: z.array(z.string()).optional(), @@ -349,6 +351,13 @@ it.each([ diagnostic: "TS2322", invalidatesCompile: false, }, + { + label: "compiler launcher", + path: "scripts/run-typescript.mjs", + content: 'throw new Error("compiler launcher changed");\n', + diagnostic: "compiler launcher changed", + invalidatesCompile: true, + }, { label: "source declaration", path: "src/types.d.mts", @@ -410,11 +419,15 @@ it.each([ ["compile", "typecheck"], { // Exercise the real compiler phase with the production task inputs. - compile: "tsc -p tsconfig.build.json", - typecheck: "tsc --noEmit", + compile: "node scripts/run-typescript.mjs -p tsconfig.build.json", + typecheck: "node scripts/run-typescript.mjs --noEmit", }, { "#compiler-scope": "./scripts/root-types.d.mts" }, ); + await writeFile( + join(root, "scripts/run-typescript.mjs"), + await readFile(join(repository, "scripts/run-typescript.mjs")), + ); for (const name of [ "tsconfig.json", "tsconfig.build.json", @@ -612,3 +625,125 @@ it("invalidates checker task hashes for Node, ignore, and VitePress config chang `${task} must ignore vitest.config.ts`, ).toBe(initial.get(task)); }); + +it.skipIf(resolveTypeScriptCompiler().name !== "tsrs")( + "does not replay an official compiler success for a Rust compiler error", + async () => { + const root = await createTestTempDirectory("rea-cross-compiler-cache-"); + const cache = await createTestTempDirectory("rea-cross-compiler-store-"); + const require = createRequire(import.meta.url); + await mkdir(join(root, "scripts")); + await mkdir(join(root, "src")); + await mkdir(join(root, "node_modules/tsrs"), { recursive: true }); + for (const name of ["typescript", "turbo"]) + await symlink( + dirname(require.resolve(`${name}/package.json`)), + join(root, "node_modules", name), + "junction", + ); + for (const name of ["run-typescript.mjs", "run-turbo.mjs"]) + await writeFile( + join(root, "scripts", name), + await readFile(join(repository, "scripts", name)), + ); + await writeTaskFixture(root, ["compile", "typecheck"], { + compile: "node scripts/run-typescript.mjs -p tsconfig.build.json", + typecheck: "node scripts/run-typescript.mjs --noEmit", + }); + const rustPath = require.resolve("tsrs/package.json"); + const rust = z + .object({ version: z.string(), bin: z.object({ tsrs: z.string() }) }) + .parse(JSON.parse(await readFile(rustPath, "utf8"))); + const writeInventory = (supported: boolean) => + writeFile( + join(root, "node_modules/tsrs/package.json"), + JSON.stringify({ + name: "tsrs", + version: rust.version, + bin: { tsrs: resolve(dirname(rustPath), rust.bin.tsrs) }, + optionalDependencies: supported + ? { [`@tsrs/${process.platform}-${process.arch}`]: rust.version } + : {}, + }), + ); + await writeInventory(false); + await writeFile( + join(root, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + strict: true, + rootDir: "src", + types: [], + lib: ["ES2023", "DOM"], + target: "ES2023", + module: "NodeNext", + outDir: "dist", + }, + include: ["src/**/*.ts"], + }), + ); + await writeFile( + join(root, "tsconfig.build.json"), + '{"extends":"./tsconfig.json"}\n', + ); + await writeFile( + join(root, ".gitignore"), + "node_modules/\ndist/\n.cache/\n.turbo/\n", + ); + // TS 7.0 accepts this contextual callback; TS 7.1 reports TS7031. + // The source stays identical while only the selected compiler changes. + await writeFile( + join(root, "src/repro.ts"), + [ + "interface Options { signal?: AbortSignal }", + "interface Provider { inspect(options?: Options): Promise }", + "declare const base: Provider;", + "const providers: Provider[] = [{ ...base, inspect: async ({ signal } = {}) => {", + " if (signal?.aborted) throw signal.reason;", + "} }];", + ].join("\n"), + ); + await workflowGit(root, ["init", "--initial-branch=main"]); + await workflowGit(root, ["add", "."]); + await workflowGit(root, ["commit", "-m", "test: seed compiler isolation"]); + const run = (task: string, dryRun = false) => + execute( + join(root, "scripts/run-turbo.mjs"), + [ + "run", + task, + ...(dryRun ? ["--dry=json"] : ["--output-logs=errors-only"]), + "--cache=local:rw", + "--cache-dir", + cache, + ], + root, + ); + const hash = (task: string) => { + const result = run(task, true); + expect(result.status, result.stdout + result.stderr).toBe(0); + const entry = dryRunSchema + .parse(JSON.parse(result.stdout)) + .tasks.find((value) => value.taskId === task); + if (entry === undefined) throw new Error(`Missing compiler task ${task}`); + return entry.hash; + }; + const hashes = new Map(); + for (const task of ["compile", "typecheck"]) { + const cold = run(task); + expect(cold.status, cold.stdout + cold.stderr).toBe(0); + hashes.set(task, hash(task)); + const warm = run(task); + expect(warm.status, warm.stdout + warm.stderr).toBe(0); + expect(warm.stdout).toMatch(/Cached:\s+1 cached/u); + } + await writeInventory(true); + for (const task of ["compile", "typecheck"]) { + const result = run(task); + expect(result.status, result.stdout + result.stderr).toBeGreaterThan(0); + expect(result.stdout + result.stderr).toContain("TS7031"); + expect(result.stdout).toMatch(/Cached:\s+0 cached/u); + expect(hash(task)).not.toBe(hashes.get(task)); + } + }, +); diff --git a/tests/boundary/filesystem/typescriptCompiler.test.ts b/tests/boundary/filesystem/typescriptCompiler.test.ts new file mode 100644 index 0000000000..d49f5b6089 --- /dev/null +++ b/tests/boundary/filesystem/typescriptCompiler.test.ts @@ -0,0 +1,206 @@ +import { spawnSync } from "node:child_process"; +import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { expect, it } from "vitest"; +import { z } from "zod"; + +import { + resolveTypeScriptCompiler, + selectTypeScriptCompiler, +} from "../../../scripts/run-typescript.mjs"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const repository = fileURLToPath(new URL("../../../", import.meta.url)); +const launcher = join(repository, "scripts/run-typescript.mjs"); +const require = createRequire(import.meta.url); +const rustPackage = z + .object({ + version: z.string(), + optionalDependencies: z.record(z.string(), z.string()), + }) + .parse( + JSON.parse(await readFile(require.resolve("tsrs/package.json"), "utf8")), + ); +const officialPackage = z + .object({ version: z.string() }) + .parse( + JSON.parse( + await readFile(require.resolve("typescript/package.json"), "utf8"), + ), + ); +const execute = (script: string, args: string[]) => + spawnSync(process.execPath, [script, ...args], { + encoding: "utf8", + timeout: 15_000, + env: { ...process.env, TURBO_HASH: undefined }, + }); + +it.each([ + ["linux", "x64", "tsrs"], + ["linux", "arm64", "tsrs"], + ["darwin", "arm64", "tsrs"], + ["win32", "x64", "typescript"], + ["win32", "arm64", "typescript"], + ["darwin", "x64", "typescript"], + ["android", "arm64", "typescript"], + ["linux", "riscv64", "typescript"], +])( + "selects %s/%s compiler %s from the pinned inventory", + (platform, arch, compiler) => { + expect( + selectTypeScriptCompiler( + platform, + arch, + rustPackage.optionalDependencies, + ), + ).toBe(compiler); + }, +); + +it("always retains the official compiler on Windows", () => { + expect( + selectTypeScriptCompiler("win32", "x64", { "@tsrs/win32-x64": "future" }), + ).toBe("typescript"); +}); + +it("runs the selected installed compiler", () => { + const result = execute(launcher, ["--version"]); + expect(result.error).toBeUndefined(); + expect(result.status, result.stderr).toBe(0); + const compiler = selectTypeScriptCompiler( + process.platform, + process.arch, + rustPackage.optionalDependencies, + ); + expect(result.stdout).toContain( + compiler === "tsrs" + ? `tsrs ${rustPackage.version}` + : `Version ${officialPackage.version}`, + ); +}); + +it("forwards build arguments and preserves real type errors and exit status", async () => { + const root = await createTestTempDirectory("rea-typescript-compiler-"); + const input = join(root, "input.ts"); + await writeFile( + join(root, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + strict: true, + target: "ES2023", + types: [], + outDir: "output", + }, + files: ["input.ts"], + }), + ); + await writeFile(input, "export const value: number = 1;\n"); + const valid = execute(launcher, ["-p", root, "--pretty", "false"]); + expect(valid.error).toBeUndefined(); + expect(valid.status, valid.stdout + valid.stderr).toBe(0); + expect(await readFile(join(root, "output/input.js"), "utf8")).toContain( + "value = 1", + ); + const resourceArgs = + resolveTypeScriptCompiler().name === "tsrs" + ? ["--checkers", "1", "--maxMemory", "1G"] + : ["--checkers", "1"]; + const checked = execute(launcher, ["-p", root, "--noEmit", ...resourceArgs]); + expect(checked.error).toBeUndefined(); + expect(checked.status, checked.stdout + checked.stderr).toBe(0); + await writeFile(input, 'export const value: number = "invalid";\n'); + const invalid = execute(launcher, [ + "-p", + root, + "--noEmit", + "--pretty", + "false", + ]); + const direct = execute(resolveTypeScriptCompiler().launcher, [ + "-p", + root, + "--noEmit", + "--pretty", + "false", + ]); + expect(direct.error).toBeUndefined(); + expect(direct.status, direct.stdout + direct.stderr).toBeGreaterThan(0); + expect(direct.stdout + direct.stderr).toContain("TS2322"); + expect(invalid.error).toBeUndefined(); + expect(invalid.status, invalid.stdout + invalid.stderr).toBe(direct.status); + expect(invalid.stdout + invalid.stderr).toContain("TS2322"); + expect(await readFile(join(root, "output/input.js"), "utf8")).toContain( + "value = 1", + ); +}); + +it.skipIf(process.platform === "win32")( + "does not retry failed supported compilers with the official compiler", + async () => { + const root = await createTestTempDirectory("rea-typescript-failure-"); + const script = join(root, "run-typescript.mjs"); + await copyFile(launcher, script); + for (const name of ["tsrs", "typescript"]) { + const directory = join(root, "node_modules", name); + await mkdir(directory, { recursive: true }); + await writeFile( + join(directory, "package.json"), + JSON.stringify({ + name, + type: "module", + bin: { [name === "tsrs" ? "tsrs" : "tsc"]: "cli.mjs" }, + optionalDependencies: { + [`@tsrs/${process.platform}-${process.arch}`]: "fixture", + }, + }), + ); + await writeFile( + join(directory, "cli.mjs"), + name === "tsrs" + ? 'console.error("supported compiler failed"); process.exitCode = 42;\n' + : 'console.log("unexpected fallback");\n', + ); + } + const result = execute(script, ["--noEmit"]); + expect(result.error).toBeUndefined(); + expect(result.status, result.stdout + result.stderr).toBe(42); + expect(result.stderr).toContain("supported compiler failed"); + expect(result.stdout).not.toContain("unexpected fallback"); + // The same selection must expose a missing installed launcher as a failure. + await writeFile( + join(root, "node_modules/tsrs/package.json"), + JSON.stringify({ + name: "tsrs", + bin: { tsrs: "missing.mjs" }, + optionalDependencies: { + [`@tsrs/${process.platform}-${process.arch}`]: "fixture", + }, + }), + ); + const missing = execute(script, ["--noEmit"]); + expect(missing.status, missing.stdout + missing.stderr).toBe(1); + expect(missing.stderr).toContain("ERR_MODULE_NOT_FOUND"); + expect(missing.stdout).not.toContain("unexpected fallback"); + }, +); + +it.each([undefined, "typescript@stale"])( + "rejects missing or stale compiler identity %s inside a Turbo task", + (identity) => { + const result = spawnSync(process.execPath, [launcher, "--version"], { + encoding: "utf8", + timeout: 15_000, + env: { + ...process.env, + TURBO_HASH: "compiler-task", + REA_TYPESCRIPT_COMPILER: identity, + }, + }); + expect(result.error).toBeUndefined(); + expect(result.status, result.stdout + result.stderr).toBe(1); + expect(result.stderr).toContain("Compiler identity is missing or stale"); + }, +); diff --git a/tests/boundary/mcp/providerSelectionMcp.test.ts b/tests/boundary/mcp/providerSelectionMcp.test.ts index df484959d2..66d49eec6b 100644 --- a/tests/boundary/mcp/providerSelectionMcp.test.ts +++ b/tests/boundary/mcp/providerSelectionMcp.test.ts @@ -12,6 +12,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { createAnalysisExecution, type AnalysisProviderCandidate, + type AnalysisProviderInspectionOptions, type CapabilityDescriptor, type ProviderIdentity, } from "../../../src/application/AnalysisProvider.js"; @@ -411,7 +412,9 @@ it.each(["ready", "cancelled", "physical"] as const)( new AnalysisProviderRegistry([ { ...base, - inspectAvailability: async ({ signal } = {}) => { + inspectAvailability: async ({ + signal, + }: AnalysisProviderInspectionOptions = {}) => { enter(); await gate; if (mode === "physical") diff --git a/tests/process-global/curatedWindowsLane.test.ts b/tests/process-global/curatedWindowsLane.test.ts index 637ca8d23a..48a1edda6b 100644 --- a/tests/process-global/curatedWindowsLane.test.ts +++ b/tests/process-global/curatedWindowsLane.test.ts @@ -136,3 +136,16 @@ it("keeps every explicit focused CI test path executable after file moves", asyn ); expect(missing.filter((path) => path !== undefined)).toEqual([]); }, 10_000); + +it("checks the complete source with the fallback compiler before Windows build reuse", async () => { + const workflow = workflowSchema.parse( + parse(await readFile(WORKFLOW, "utf8")), + ); + const steps = workflow.jobs["windows-curated"]?.steps ?? []; + const checked = steps.findIndex( + (step) => step.run === "npm run typecheck -- --checkers 2", + ); + const built = steps.findIndex((step) => step.run === "npm run build:cached"); + expect(checked).toBeGreaterThan(-1); + expect(built).toBeGreaterThan(checked); +}); diff --git a/turbo.json b/turbo.json index 37783a4afd..e3be6d9d9d 100644 --- a/turbo.json +++ b/turbo.json @@ -3,13 +3,20 @@ "cacheMaxAge": "7d", "cacheMaxSize": "500MB", "globalDependencies": [".nvmrc", ".npmrc"], - "globalPassThroughEnv": ["GOMAXPROCS", "GOMEMLIMIT", "UV_THREADPOOL_SIZE"], + "globalPassThroughEnv": [ + "GOMAXPROCS", + "GOMEMLIMIT", + "UV_THREADPOOL_SIZE", + "REA_TYPESCRIPT_COMPILER" + ], "tasks": { "compile": { "dependsOn": ["metadata:check"], "inputs": [ "package.json", "package-lock.json", + "scripts/run-turbo.mjs", + "scripts/run-typescript.mjs", "scripts/check-dependency-install.mjs", "scripts/clean-build-output.mjs", "scripts/bundle-runtime.mjs", @@ -22,12 +29,15 @@ "tsconfig.json", "tsconfig.build.json" ], - "outputs": ["dist/**"] + "outputs": ["dist/**"], + "env": ["REA_TYPESCRIPT_COMPILER"] }, "typecheck": { "inputs": [ "package.json", "package-lock.json", + "scripts/run-turbo.mjs", + "scripts/run-typescript.mjs", "src/**/*.{cts,mts,ts,tsx}", "src/**/package.json", "tests/**/*.{cts,mts,ts,tsx}", @@ -44,7 +54,8 @@ "!docs/.vitepress/cache/**", "!docs/.vitepress/dist/**" ], - "outputs": [] + "outputs": [], + "env": ["REA_TYPESCRIPT_COMPILER"] }, "metadata:check": { "inputs": [