diff --git a/.github/actions/ci-build-typecheck/action.yaml b/.github/actions/ci-build-typecheck/action.yaml index 8ead369add0..7a3dd0b539a 100644 --- a/.github/actions/ci-build-typecheck/action.yaml +++ b/.github/actions/ci-build-typecheck/action.yaml @@ -37,7 +37,7 @@ runs: - name: Typecheck plugin shell: bash - run: cd nemoclaw && npx tsc --noEmit --incremental + run: npm --prefix nemoclaw run typecheck - name: Typecheck JavaScript config shell: bash diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb5e0c8f941..9dee317b86c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -229,7 +229,7 @@ repos: hooks: - id: tsc-plugin name: TypeScript (plugin) - entry: bash -c 'cd nemoclaw && npx tsc --noEmit --incremental' + entry: npm --prefix nemoclaw run typecheck language: system pass_filenames: false files: ^nemoclaw/ diff --git a/AGENTS.md b/AGENTS.md index 494c79f859b..6bd49b20c97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ Package-specific guides: | Run repo-wide pre-commit and coverage checks | `npm run check` | | Reproduce `pre-commit`, `commit-msg`, and `pre-push` checks for the current diff | `npm run check:diff` | | Type-check CLI | `npm run typecheck:cli` | +| Type-check plugin and plugin tests | `npm --prefix nemoclaw run typecheck` | | Auto-format | `npm run format` | | Build docs | `npm run docs` | | Serve docs locally | `npm run docs:live` | @@ -143,7 +144,8 @@ For shell scripts use `#` comments. For Markdown use HTML comments. - Plugin code in `nemoclaw/src/` is linted and formatted by the root Biome config - CLI type-checking via `tsconfig.cli.json` -- Plugin type-checking via `nemoclaw/tsconfig.json` +- Plugin production and test type-checking via `npm --prefix nemoclaw run typecheck`, using + `nemoclaw/tsconfig.json` and `nemoclaw/tsconfig.test.json` ### Shell Scripts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c92fb023211..8331f396ede 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,7 +149,7 @@ npm --prefix nemoclaw install --include=dev --ignore-scripts npm run build:cli npm --prefix nemoclaw run build npm run typecheck:cli -./nemoclaw/node_modules/.bin/tsc --noEmit -p nemoclaw/tsconfig.json +npm --prefix nemoclaw run typecheck ./node_modules/.bin/prek install ``` @@ -161,6 +161,7 @@ The TypeScript plugin lives in `nemoclaw/` and compiles with `tsc`: cd nemoclaw npm run build # one-time compile npm run dev # watch mode +npm run typecheck # type-check production and test sources without emitting ``` The CLI (`bin/`, `scripts/`) is type-checked separately: @@ -196,6 +197,7 @@ These are the primary npm scripts for day-to-day development: | `npm run check:diff` | Reproduce `pre-commit`, `commit-msg`, and `pre-push` checks for the diff from `origin/main` | | `npm run format` | Auto-format Biome-supported source files | | `npm run typecheck:cli` | Type-check the root TypeScript project using `tsconfig.cli.json` | +| `npm --prefix nemoclaw run typecheck` | Type-check plugin production and test sources without emitting files | | `npm test` | Build package artifacts and run every non-live Vitest project for broad changes | | `npm run test:spec` | Run every non-live test with hierarchical behavior-oriented output | | `npm run test:fast` | Clean `dist/` and run source CLI, plugin, and E2E-support tests | diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index dc5c8847410..5395c28d86f 100644 --- a/ci/test-file-size-budget.json +++ b/ci/test-file-size-budget.json @@ -2,7 +2,7 @@ "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\nSPDX-License-Identifier: Apache-2.0", "defaultMaxLines": 1500, "legacyMaxLines": { - "nemoclaw/src/commands/migration-state.test.ts": 1566, + "nemoclaw/src/commands/migration-state.test.ts": 1565, "src/lib/inference/nim.test.ts": 2068, "src/lib/onboard/preflight.test.ts": 1904, "test/generate-openclaw-config.test.ts": 1941, diff --git a/nemoclaw/package-lock.json b/nemoclaw/package-lock.json index 5cd0a2f1adb..35958517fda 100644 --- a/nemoclaw/package-lock.json +++ b/nemoclaw/package-lock.json @@ -18,7 +18,7 @@ "@biomejs/biome": "^2.4.14", "@types/node": "^22.0.0", "typescript": "6.0.3", - "vitest": "^4.1.0" + "vitest": "^4.1.9" }, "engines": { "node": ">=22.16.0" @@ -188,21 +188,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -211,9 +211,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -241,14 +241,14 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -260,9 +260,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.124.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", - "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", "funding": { @@ -270,9 +270,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -287,9 +287,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -321,9 +321,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -338,9 +338,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", - "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -355,13 +355,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -372,13 +375,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -389,13 +395,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -406,13 +415,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -423,13 +435,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -440,13 +455,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -457,9 +475,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -474,9 +492,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", - "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ "wasm32" ], @@ -484,18 +502,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.3" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -510,9 +528,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -527,9 +545,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", - "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -559,9 +577,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -588,9 +606,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -605,31 +623,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -638,7 +656,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -650,26 +668,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.9", "pathe": "^2.0.3" }, "funding": { @@ -677,14 +695,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -693,9 +711,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true, "license": "MIT", "funding": { @@ -703,15 +721,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1100,6 +1118,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1121,6 +1142,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1142,6 +1166,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1163,6 +1190,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1250,9 +1280,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -1343,9 +1373,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -1356,9 +1386,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz", + "integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==", "dev": true, "funding": [ { @@ -1376,7 +1406,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1400,14 +1430,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", - "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.124.0", - "@rolldown/pluginutils": "1.0.0-rc.15" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -1416,21 +1446,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-x64": "1.0.0-rc.15", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/shebang-command": { @@ -1543,14 +1573,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -1611,17 +1641,17 @@ } }, "node_modules/vite": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", - "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz", + "integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.15", - "tinyglobby": "^0.2.15" + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -1637,7 +1667,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -1689,19 +1719,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -1712,8 +1742,8 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -1729,13 +1759,15 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -1756,6 +1788,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, diff --git a/nemoclaw/package.json b/nemoclaw/package.json index 171a8cdfd21..f9f32250a0e 100644 --- a/nemoclaw/package.json +++ b/nemoclaw/package.json @@ -26,7 +26,8 @@ "lint:fix": "biome lint --write src", "format": "biome format --write src", "format:check": "biome format src", - "check": "npm run lint && npm run format:check && tsc --noEmit", + "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json", + "check": "npm run lint && npm run format:check && npm run typecheck", "clean": "rm -rf dist/" }, "dependencies": { @@ -39,7 +40,7 @@ "@biomejs/biome": "^2.4.14", "@types/node": "^22.0.0", "typescript": "6.0.3", - "vitest": "^4.1.0" + "vitest": "^4.1.9" }, "engines": { "node": ">=22.16.0" diff --git a/nemoclaw/src/commands/migration-state.test.ts b/nemoclaw/src/commands/migration-state.test.ts index 95d2d6ac18f..5a2a1e7dcfe 100644 --- a/nemoclaw/src/commands/migration-state.test.ts +++ b/nemoclaw/src/commands/migration-state.test.ts @@ -29,7 +29,7 @@ function addSymlink(p: string): void { } vi.mock("node:fs", async (importOriginal) => { - const original = await importOriginal(); + const original = await importOriginal(); return { ...original, existsSync: (p: string) => store.has(p), @@ -94,7 +94,6 @@ vi.mock("node:fs", async (importOriginal) => { unlinkSync: vi.fn((p: string) => { store.delete(p); }), - chmodSync: vi.fn(), }; }); diff --git a/nemoclaw/src/onboard/config.test.ts b/nemoclaw/src/onboard/config.test.ts index c539b3b455a..a275ff619a2 100644 --- a/nemoclaw/src/onboard/config.test.ts +++ b/nemoclaw/src/onboard/config.test.ts @@ -19,7 +19,7 @@ import { const store = new Map(); vi.mock("node:fs", async (importOriginal) => { - const original = await importOriginal(); + const original = await importOriginal(); return { ...original, existsSync: (p: string) => store.has(p), @@ -88,10 +88,10 @@ describe("onboard/config", () => { it("handles non-URL endpoint strings gracefully", () => { const config = makeConfig({ - endpointType: "local", + endpointType: "custom", endpointUrl: "not-a-url", }); - expect(describeOnboardEndpoint(config)).toBe("local (not-a-url)"); + expect(describeOnboardEndpoint(config)).toBe("custom (not-a-url)"); }); }); diff --git a/nemoclaw/src/register.test.ts b/nemoclaw/src/register.test.ts index 646be6c3566..153b031a95d 100644 --- a/nemoclaw/src/register.test.ts +++ b/nemoclaw/src/register.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest"; import type { OpenClawPluginApi } from "./index.js"; vi.mock("node:fs", async (importOriginal) => { @@ -32,7 +32,7 @@ const mockedReadFileSync = vi.mocked(readFileSync); const mockedLoadOnboardConfig = vi.mocked(loadOnboardConfig); const originalReadFileSync = (await vi.importActual("node:fs")) .readFileSync; -let stderrWrite: ReturnType; +let stderrWrite: MockInstance; function mockStderrWrite(): void { stderrWrite = vi diff --git a/nemoclaw/tsconfig.test.json b/nemoclaw/tsconfig.test.json new file mode 100644 index 00000000000..ec6a574c550 --- /dev/null +++ b/nemoclaw/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src/**/*.test.ts", "vitest.config.ts", "vitest.project.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/nemoclaw/vitest.config.ts b/nemoclaw/vitest.config.ts index 06b0fee1acd..c0eb92ef9bd 100644 --- a/nemoclaw/vitest.config.ts +++ b/nemoclaw/vitest.config.ts @@ -3,26 +3,16 @@ import path from "node:path"; -import { defineConfig } from "vitest/config"; +import { defineConfig, defineProject } from "vitest/config"; -const canonicalOpenShellPolicyBoundary = path.resolve( - import.meta.dirname, - "src/shared/openshell-policy-boundary.cts", -); +import pluginVitestProjectOptions from "./vitest.project.js"; + +const pluginVitestProject = defineProject(pluginVitestProjectOptions); export default defineConfig({ - oxc: { - include: /\.(?:[cm]?ts|[jt]sx)$/, - }, + ...pluginVitestProject, test: { - alias: [ - { - find: /^.*openshell-policy-boundary\.cjs$/, - replacement: canonicalOpenShellPolicyBoundary, - }, - ], - environment: "node", + ...pluginVitestProject.test, globalSetup: path.resolve(import.meta.dirname, "../test/helpers/vitest-temp-root.ts"), - include: ["src/**/*.test.ts"], }, }); diff --git a/nemoclaw/vitest.project.ts b/nemoclaw/vitest.project.ts new file mode 100644 index 00000000000..6aba2560bb0 --- /dev/null +++ b/nemoclaw/vitest.project.ts @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; + +const repositoryRoot = path.resolve(import.meta.dirname, ".."); +const canonicalOpenShellPolicyBoundary = path.resolve( + import.meta.dirname, + "src/shared/openshell-policy-boundary.cts", +); + +type PluginVitestProjectOptions = { + root: string; + oxc: { include: RegExp }; + test: { + name: "plugin"; + alias: Array<{ find: RegExp; replacement: string }>; + env: Record; + environment: "node"; + setupFiles: string[]; + include: string[]; + }; +}; + +const pluginVitestProjectOptions = { + root: repositoryRoot, + oxc: { + include: /\.(?:[cm]?ts|[jt]sx)$/, + }, + test: { + name: "plugin", + alias: [ + { + find: /^.*openshell-policy-boundary\.cjs$/, + replacement: canonicalOpenShellPolicyBoundary, + }, + ], + env: { + NEMOCLAW_DISABLE_GATEWAY_DRIFT_PREFLIGHT: "1", + }, + environment: "node", + setupFiles: ["test/helpers/normalize-fixture-umask.ts"], + include: ["nemoclaw/src/**/*.test.ts"], + }, +} satisfies PluginVitestProjectOptions; + +export default pluginVitestProjectOptions; diff --git a/scripts/checks/no-test-dist-imports.ts b/scripts/checks/no-test-dist-imports.ts index c004856d135..5ad594a6466 100644 --- a/scripts/checks/no-test-dist-imports.ts +++ b/scripts/checks/no-test-dist-imports.ts @@ -1,15 +1,32 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import ts from "typescript"; -export type Violation = { file: string; line: number; detail: string }; +export type Violation = { + chain?: string[]; + detail: string; + file: string; + line: number; +}; + +type StaticModuleReference = { + file: string; + line: number; + specifier: string; +}; + +type SourceAnalysis = { + moduleLoadViolations: Violation[]; + references: StaticModuleReference[]; + violations: Violation[]; +}; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); -const SKIP_DIRS = new Set([".git", "coverage", "dist", "node_modules"]); +const SKIP_DIRS = new Set([".git", "node_modules"]); // These tests intentionally construct fake dist/lib trees; they do not load // repository build output. The self-audit below prevents this list growing or // retaining an exemption after the fixture no longer needs one. @@ -37,18 +54,27 @@ export function isScannedTestPath(relativePath: string): boolean { return relativePath.startsWith("test/") && /\.[cm]?[jt]sx?$/.test(relativePath); } +export function isFastProjectTestPath(relativePath: string): boolean { + const normalized = relativePath.replaceAll("\\", "/"); + if (normalized.startsWith("src/") && normalized.split("/").includes(".claude")) return false; + return /^(?:src|nemoclaw\/src|test\/e2e\/support)\/.+\.test\.ts$/.test(normalized); +} + function isScannedTestFile(absolutePath: string): boolean { return isScannedTestPath(repoPath(absolutePath)); } -function* walk(directory: string): Generator { +function* walk( + directory: string, + acceptsFile: (absolutePath: string) => boolean = isScannedTestFile, +): Generator { if (!existsSync(directory)) return; for (const entry of readdirSync(directory)) { if (SKIP_DIRS.has(entry)) continue; const absolutePath = path.join(directory, entry); const stats = statSync(absolutePath); - if (stats.isDirectory()) yield* walk(absolutePath); - else if (stats.isFile() && isScannedTestFile(absolutePath)) yield absolutePath; + if (stats.isDirectory()) yield* walk(absolutePath, acceptsFile); + else if (stats.isFile() && acceptsFile(absolutePath)) yield absolutePath; } } @@ -275,7 +301,7 @@ function templateSource(node: ts.TemplateLiteral, scope: LexicalScope): string { ); } -export function findCompiledInternalViolations(file: string, source: string): Violation[] { +function analyzeSource(file: string, source: string): SourceAnalysis { const sourceFile = ts.createSourceFile( file, source, @@ -284,8 +310,16 @@ export function findCompiledInternalViolations(file: string, source: string): Vi file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, ); const violations: Violation[] = []; - - function scan(scannedFile: ts.SourceFile, lineOffset = 0, scanTemplates = true): void { + const moduleLoadViolations: Violation[] = []; + const references: StaticModuleReference[] = []; + + function scan( + scannedFile: ts.SourceFile, + lineOffset = 0, + scanTemplates = true, + recordReferences = true, + recordModuleLoads = true, + ): void { function createScope(parent: LexicalScope | null, kind: ScopeKind): LexicalScope { return { bindings: new Map(), kind, parent }; } @@ -585,14 +619,28 @@ export function findCompiledInternalViolations(file: string, source: string): Vi }); } - function add(node: ts.Node, detail: string): void { + function createViolation(node: ts.Node, detail: string): Violation { const position = scannedFile.getLineAndCharacterOfPosition(node.getStart(scannedFile)); - violations.push({ file, line: position.line + lineOffset + 1, detail }); + return { file, line: position.line + lineOffset + 1, detail }; + } + + function add(node: ts.Node, detail: string): void { + violations.push(createViolation(node, detail)); + } + + function addModuleLoad(node: ts.Node, detail: string): void { + if (recordModuleLoads) moduleLoadViolations.push(createViolation(node, detail)); } function checkSpecifier(node: ts.Node, specifier: string): void { + if (recordReferences) { + const position = scannedFile.getLineAndCharacterOfPosition(node.getStart(scannedFile)); + references.push({ file, line: position.line + lineOffset + 1, specifier }); + } if (isCompiledInternalSpecifier(specifier)) { - add(node, `imports compiled CLI internals from ${JSON.stringify(specifier)}`); + const detail = `imports compiled CLI internals from ${JSON.stringify(specifier)}`; + add(node, detail); + addModuleLoad(node, detail); } } @@ -626,10 +674,26 @@ export function findCompiledInternalViolations(file: string, source: string): Vi node.expression.name.text === "resolve"; const firstArgument = node.arguments[0]; const specifier = staticString(firstArgument, scope); - if ((isRequire || isDynamicImport || isRequireResolve) && firstArgument && specifier) { + const isModuleLoad = isRequire || isDynamicImport || isRequireResolve; + if (isModuleLoad && firstArgument && specifier) { checkSpecifier(firstArgument, specifier); } + const unwrappedArgument = firstArgument && unwrapExpression(firstArgument); + if ( + isModuleLoad && + !specifier && + unwrappedArgument && + ts.isCallExpression(unwrappedArgument) + ) { + const moduleTarget = compiledPathBuilderTarget(unwrappedArgument, scope); + if (moduleTarget === "dist/nemoclaw.js") { + addModuleLoad(firstArgument, "loads compiled CLI internals from dist/nemoclaw.js"); + } else if (moduleTarget) { + addModuleLoad(firstArgument, `loads compiled CLI internals from ${moduleTarget}`); + } + } + const pathTarget = compiledPathBuilderTarget(node, scope); if (pathTarget === "dist/nemoclaw.js") { add(node, "constructs a path to dist/nemoclaw.js"); @@ -651,7 +715,10 @@ export function findCompiledInternalViolations(file: string, source: string): Vi const templateLine = scannedFile.getLineAndCharacterOfPosition( node.template.getStart(scannedFile), ).line; - scan(embeddedSource, lineOffset + templateLine, false); + // Generated script references resolve in the spawned program's runtime + // context, not relative to this source module. Still record forbidden + // compiled loads from the embedded program itself. + scan(embeddedSource, lineOffset + templateLine, false, false, true); } ts.forEachChild(node, (child) => { let childScope = scope; @@ -679,14 +746,200 @@ export function findCompiledInternalViolations(file: string, source: string): Vi } scan(sourceFile); - return violations.filter( - (violation, index, all) => - all.findIndex( - (candidate) => candidate.file === violation.file && candidate.line === violation.line, - ) === index, + return { + moduleLoadViolations: moduleLoadViolations.filter( + (violation, index, all) => + all.findIndex( + (candidate) => candidate.file === violation.file && candidate.line === violation.line, + ) === index, + ), + references: references.filter( + (reference, index, all) => + all.findIndex( + (candidate) => + candidate.file === reference.file && + candidate.line === reference.line && + candidate.specifier === reference.specifier, + ) === index, + ), + violations: violations.filter( + (violation, index, all) => + all.findIndex( + (candidate) => candidate.file === violation.file && candidate.line === violation.line, + ) === index, + ), + }; +} + +export function findCompiledInternalViolations(file: string, source: string): Violation[] { + return analyzeSource(file, source).violations; +} + +type ModuleResolutionContext = { + cache: ts.ModuleResolutionCache; + options: ts.CompilerOptions; +}; + +type GraphVisit = { + absolutePath: string; + chain: string[]; +}; + +let moduleResolutionContext: ModuleResolutionContext | undefined; + +function loadModuleResolutionContext(): ModuleResolutionContext { + if (moduleResolutionContext) return moduleResolutionContext; + + const configPath = path.join(REPO_ROOT, "tsconfig.cli.json"); + const configFile = ts.readConfigFile(configPath, ts.sys.readFile); + if (configFile.error) { + throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n")); + } + const parsed = ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + REPO_ROOT, + {}, + configPath, + ); + if (parsed.errors.length > 0) { + throw new Error( + parsed.errors + .map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n")) + .join("\n"), + ); + } + + const canonicalFileName = ts.sys.useCaseSensitiveFileNames + ? (fileName: string) => fileName + : (fileName: string) => fileName.toLowerCase(); + moduleResolutionContext = { + cache: ts.createModuleResolutionCache(REPO_ROOT, canonicalFileName, parsed.options), + options: parsed.options, + }; + return moduleResolutionContext; +} + +function isRepoSourceModule(absolutePath: string): boolean { + const relativePath = repoPath(absolutePath); + const segments = relativePath.split("/"); + return ( + relativePath !== ".." && + !relativePath.startsWith("../") && + !path.isAbsolute(relativePath) && + !segments.includes("dist") && + !segments.includes("node_modules") && + /\.[cm]?[jt]sx?$/.test(relativePath) ); } +type ResolvedRepoModule = { + absolutePath: string; + compiledTarget?: string; +}; + +function resolvedCompiledInternalTarget(absolutePath: string): string | undefined { + const candidates = [absolutePath]; + try { + candidates.push(realpathSync(absolutePath)); + } catch { + // TypeScript resolved the path, but leave an unavailable target to the + // ordinary source-module filter below. + } + return candidates + .map(repoPath) + .find( + (relativePath) => + /^dist\/(?:lib|commands)(?:\/|$)/.test(relativePath) || + /^dist\/nemoclaw(?:\.js)?$/.test(relativePath), + ); +} + +function resolveRepoModule(importer: string, specifier: string): ResolvedRepoModule | undefined { + const context = loadModuleResolutionContext(); + const resolved = ts.resolveModuleName(specifier, importer, context.options, ts.sys, context.cache) + .resolvedModule?.resolvedFileName; + if (!resolved) return undefined; + const absolutePath = path.resolve(resolved); + const compiledTarget = resolvedCompiledInternalTarget(absolutePath); + if (compiledTarget) return { absolutePath, compiledTarget }; + return isRepoSourceModule(absolutePath) ? { absolutePath } : undefined; +} + +function collectFastProjectEntries(): string[] { + const acceptsFile = (absolutePath: string) => isFastProjectTestPath(repoPath(absolutePath)); + return [ + ...walk(path.join(REPO_ROOT, "src"), acceptsFile), + ...walk(path.join(REPO_ROOT, "nemoclaw", "src"), acceptsFile), + ...walk(path.join(REPO_ROOT, "test", "e2e", "support"), acceptsFile), + ].sort(); +} + +export function findFastProjectTransitiveViolations( + entryFiles: readonly string[] = collectFastProjectEntries(), +): Violation[] { + const roots = [...new Set(entryFiles.map((entry) => path.resolve(entry)))].sort(); + const queue: GraphVisit[] = roots.map((absolutePath) => ({ + absolutePath, + chain: [repoPath(absolutePath)], + })); + const visited = new Set(roots); + const violations: Violation[] = []; + + for (let cursor = 0; cursor < queue.length; cursor += 1) { + const current = queue[cursor]; + if (!current) continue; + const analysis = analyzeSource( + repoPath(current.absolutePath), + readFileSync(current.absolutePath, "utf8"), + ); + violations.push( + ...analysis.moduleLoadViolations.map((violation) => ({ + ...violation, + chain: current.chain, + })), + ); + + const references = [...analysis.references].sort( + (left, right) => left.line - right.line || left.specifier.localeCompare(right.specifier), + ); + for (const reference of references) { + if (isCompiledInternalSpecifier(reference.specifier)) continue; + const resolved = resolveRepoModule(current.absolutePath, reference.specifier); + if (!resolved) continue; + if (resolved.compiledTarget) { + violations.push({ + chain: current.chain, + detail: `imports compiled CLI internals from ${JSON.stringify(reference.specifier)} (resolves to ${resolved.compiledTarget})`, + file: reference.file, + line: reference.line, + }); + continue; + } + if (visited.has(resolved.absolutePath)) continue; + visited.add(resolved.absolutePath); + queue.push({ + absolutePath: resolved.absolutePath, + chain: [...current.chain, repoPath(resolved.absolutePath)], + }); + } + } + + return violations + .filter( + (violation, index, all) => + all.findIndex( + (candidate) => candidate.file === violation.file && candidate.line === violation.line, + ) === index, + ) + .sort( + (left, right) => + left.file.localeCompare(right.file) || + left.line - right.line || + (left.chain ?? []).join("\0").localeCompare((right.chain ?? []).join("\0")), + ); +} + function findViolations(absolutePath: string): Violation[] { return findCompiledInternalViolations(repoPath(absolutePath), readFileSync(absolutePath, "utf8")); } @@ -703,10 +956,16 @@ function main(): void { process.exit(1); } - const violations = [ + const directViolations = [ ...walk(path.join(REPO_ROOT, "src")), ...walk(path.join(REPO_ROOT, "test")), ].flatMap(findViolations); + const violations = [...findFastProjectTransitiveViolations(), ...directViolations].filter( + (violation, index, all) => + all.findIndex( + (candidate) => candidate.file === violation.file && candidate.line === violation.line, + ) === index, + ); if (violations.length > 0) { console.error( @@ -714,6 +973,7 @@ function main(): void { ); for (const violation of violations) { console.error(` ${violation.file}:${violation.line} ${violation.detail}`); + if (violation.chain) console.error(` reachable via ${violation.chain.join(" -> ")}`); } console.error( "Import src/ instead, or move a genuine compiled-package contract under test/package-contract/.", diff --git a/src/lib/actions/completion.test.ts b/src/lib/actions/completion.test.ts index a202698f3cf..9de4715f00d 100644 --- a/src/lib/actions/completion.test.ts +++ b/src/lib/actions/completion.test.ts @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Config as OclifConfig } from "@oclif/core"; import { spawnSync } from "node:child_process"; import { describe, expect, it } from "vitest"; @@ -91,26 +90,6 @@ describe("buildCompletionModel", () => { expect(JSON.stringify(model)).not.toContain("internal:secret"); expect(JSON.stringify(model)).not.toContain("credentials:add"); }); - - it("tracks the repository's discovered oclif and public-route registries", async () => { - const config = await OclifConfig.load(process.cwd()); - const model = buildCompletionModel(config.commands); - - expect(findCase(model, "global", "")?.candidates).toEqual( - expect.arrayContaining(["help", "resources", "uninstall", "use", "version"]), - ); - expect(findCase(model, "global", "")?.flags).toEqual( - expect.arrayContaining(["--help", "--version", "-h", "-v"]), - ); - expect(findCase(model, "global", "inference")?.candidates).toEqual( - expect.arrayContaining(["get", "set"]), - ); - expect(findCase(model, "sandbox", "sessions")?.candidates).toEqual( - expect.arrayContaining(["delete", "export", "list", "reset"]), - ); - expect(findCase(model, "sandbox", "gateway")?.candidates).toContain("restart"); - expect(findCase(model, "sandbox", "gateway-token")?.flags).toContain("--quiet"); - }); }); describe("generateCompletionScript", () => { diff --git a/src/lib/actions/sandbox/doctor-flow.test.ts b/src/lib/actions/sandbox/doctor-flow.test.ts index 04ca1c6daf4..d1a105a5f2c 100644 --- a/src/lib/actions/sandbox/doctor-flow.test.ts +++ b/src/lib/actions/sandbox/doctor-flow.test.ts @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import fs from "node:fs"; import { createRequire } from "node:module"; +import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest"; import { testTimeoutOptions } from "../../../../test/helpers/timeouts"; @@ -180,6 +182,14 @@ function createDoctorHarness(): { ]); logSpy.mockClear(); + const runSandboxDoctor = requireDist(doctorModulePath).runSandboxDoctor; + const existsSync = fs.existsSync.bind(fs); + const cliBuildPath = [process.cwd(), "dist", "nemoclaw.js"].join(path.sep); + vi.spyOn(fs, "existsSync").mockImplementation((candidate) => + typeof candidate === "string" && path.resolve(candidate) === cliBuildPath + ? true + : existsSync(candidate), + ); return { buildToolScopeChecksSpy, @@ -197,7 +207,7 @@ function createDoctorHarness(): { recoverNamedGatewayRuntimeSpy, repairMutableConfigPermsSpy, resolveOpenShellSpy, - runSandboxDoctor: requireDist(doctorModulePath).runSandboxDoctor, + runSandboxDoctor, }; } diff --git a/src/lib/cli/oclif-command-metadata.test.ts b/src/lib/cli/oclif-command-metadata.test.ts deleted file mode 100644 index a97ea23400d..00000000000 --- a/src/lib/cli/oclif-command-metadata.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { Config as OclifConfig } from "@oclif/core"; -import { describe, expect, it } from "vitest"; - -type OclifCommandClass = { - flags?: Record; -}; - -function extendsNemoClawCommand(commandClass: unknown): boolean { - if (typeof commandClass !== "function") return false; - let current = Object.getPrototypeOf(commandClass) as { name?: string } | null; - while (current) { - if (current.name === "NemoClawCommand") return true; - current = Object.getPrototypeOf(current) as { name?: string } | null; - } - return false; -} - -function commandOwnsHelpFlag(commandClass: unknown): boolean { - return ( - typeof commandClass === "function" && - Object.hasOwn(commandClass as OclifCommandClass, "flags") && - Object.hasOwn((commandClass as OclifCommandClass).flags ?? {}, "help") - ); -} - -describe("oclif command metadata", () => { - it("keeps discovered commands on the shared NemoClaw oclif base", async () => { - const config = await OclifConfig.load(process.cwd()); - const nonConforming: string[] = []; - - for (const command of config.commands) { - const commandClass = await command.load(); - if (!extendsNemoClawCommand(commandClass)) nonConforming.push(command.id); - } - - expect(nonConforming).toEqual([]); - }); - - it("keeps the help flag centralized on the shared base command", async () => { - const config = await OclifConfig.load(process.cwd()); - const duplicatedHelpFlags: string[] = []; - - for (const command of config.commands) { - const commandClass = await command.load(); - if (commandOwnsHelpFlag(commandClass)) duplicatedHelpFlags.push(command.id); - } - - expect(duplicatedHelpFlags).toEqual([]); - }); - - it("keeps public discovered commands documented in oclif statics", async () => { - const config = await OclifConfig.load(process.cwd()); - const publicCommands = config.commands.filter((command) => command.hidden !== true); - const missing: string[] = []; - - for (const command of publicCommands) { - if (!command.summary) missing.push(`${command.id}: summary`); - if (!command.description) missing.push(`${command.id}: description`); - if (!Array.isArray(command.usage) || command.usage.length === 0) { - missing.push(`${command.id}: usage`); - } - if (!Array.isArray(command.examples) || command.examples.length === 0) { - missing.push(`${command.id}: examples`); - } - } - - expect(missing).toEqual([]); - }); -}); diff --git a/src/lib/cli/oclif-metadata.test.ts b/src/lib/cli/oclif-metadata.test.ts new file mode 100644 index 00000000000..2124df9716f --- /dev/null +++ b/src/lib/cli/oclif-metadata.test.ts @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { CLI_DISPLAY_NAME } from "./branding"; +import { + getRegisteredOclifCommandMetadata, + getRegisteredOclifCommandSummary, + getRegisteredOclifCommandsMetadata, +} from "./oclif-metadata"; + +describe("source oclif metadata lookup", () => { + it("discovers source command IDs and static metadata without loading command modules", () => { + const loadedBefore = new Set(Object.keys(require.cache)); + const metadata = getRegisteredOclifCommandsMetadata(); + const sourceCommandsRoot = `${path.join(process.cwd(), "src", "commands")}${path.sep}`; + const newlyLoadedCommandModules = Object.keys(require.cache).filter( + (file) => file.startsWith(sourceCommandsRoot) && !loadedBefore.has(file), + ); + + expect(Object.keys(metadata)).toEqual( + expect.arrayContaining([ + "onboard", + "sandbox:status", + "sandbox:channels:start", + "internal:uninstall:plan", + ]), + ); + expect(metadata["sandbox:sessions"]).toMatchObject({ + id: "sandbox:sessions", + strict: false, + summary: "List conversation sessions in a sandbox", + }); + expect(metadata["root:help"]).toMatchObject({ + hidden: true, + id: "root:help", + strict: false, + summary: "Show help", + }); + expect(newlyLoadedCommandModules).toEqual([]); + }); + + it("looks up source summaries and rejects unknown command IDs", () => { + expect(getRegisteredOclifCommandSummary("sandbox:logs")).toBe("Stream sandbox logs"); + expect(getRegisteredOclifCommandSummary("update")).toBe( + `Run the maintained ${CLI_DISPLAY_NAME} installer update flow`, + ); + expect(getRegisteredOclifCommandSummary("internal:uninstall:run-plan")).toBe( + `${CLI_DISPLAY_NAME} Uninstaller`, + ); + expect(getRegisteredOclifCommandMetadata("missing:nope")).toBeNull(); + }); +}); diff --git a/src/lib/cli/oclif-metadata.ts b/src/lib/cli/oclif-metadata.ts index 4914c1eeae0..8b2060a6627 100644 --- a/src/lib/cli/oclif-metadata.ts +++ b/src/lib/cli/oclif-metadata.ts @@ -4,6 +4,7 @@ import fs from "node:fs"; import path from "node:path"; +import { CLI_DISPLAY_NAME } from "./branding"; import type { PublicCommandDisplayEntry } from "./command-display"; const GENERATED_METADATA_FILE = "oclif-command-metadata.generated.json"; @@ -35,13 +36,98 @@ function generatedMetadataPath(): string { let cachedMetadata: Record | null = null; +function commandIdFromSourceFile(relativeFile: string): string { + const parsed = path.parse(relativeFile); + const topics = parsed.dir.split(path.sep).filter(Boolean); + const command = parsed.name === "index" ? null : parsed.name; + return [...topics, command].filter(Boolean).join(":"); +} + +function* walkSourceCommandFiles(dir: string, prefix = ""): Generator { + for (const entry of fs + .readdirSync(dir, { withFileTypes: true }) + .sort((a, b) => a.name.localeCompare(b.name))) { + const absolute = path.join(dir, entry.name); + const relative = path.join(prefix, entry.name); + if (entry.isDirectory()) { + yield* walkSourceCommandFiles(absolute, relative); + } else if ( + entry.isFile() && + /\.(?:[cm]?ts|tsx)$/.test(entry.name) && + !/\.(?:d|test|spec)\.(?:[cm]?ts|tsx)$/.test(entry.name) + ) { + yield relative; + } + } +} + +// Source tests need routing metadata before command modules can load. Read only +// recognized literal/branding statics so discovery cannot execute a command or +// re-enter this registry. +function staticJsonString(source: string, property: "summary"): string | undefined { + const match = source.match( + new RegExp(`\\bstatic\\s+(?:readonly\\s+)?${property}\\s*=\\s*("(?:\\\\.|[^"\\\\])*")\\s*;`), + ); + if (!match?.[1]) return undefined; + return JSON.parse(match[1]) as string; +} + +function staticSummary(source: string): string | undefined { + const literalSummary = staticJsonString(source, "summary"); + if (literalSummary !== undefined) return literalSummary; + + const brandedTemplate = source.match( + /\bstatic\s+(?:readonly\s+)?summary\s*=\s*`([^`$\\]*)\$\{CLI_DISPLAY_NAME\}([^`$\\]*)`\s*;/, + ); + if (!brandedTemplate) return undefined; + return `${brandedTemplate[1]}${CLI_DISPLAY_NAME}${brandedTemplate[2]}`; +} + +function staticBoolean(source: string, property: "hidden" | "strict"): boolean | undefined { + const match = source.match( + new RegExp(`\\bstatic\\s+(?:readonly\\s+)?${property}\\s*=\\s*(true|false)\\s*;`), + ); + return match?.[1] === undefined ? undefined : match[1] === "true"; +} + +function sourceCommandsRoot(): string | null { + const root = packageRoot(); + const sourceCliDir = path.join(root, "src", "lib", "cli"); + // A compiled module must always use the generated package manifest below. + return path.resolve(__dirname) === sourceCliDir ? path.join(root, "src", "commands") : null; +} + +function loadSourceOclifMetadata(commandRoot: string): Record { + const metadata: Record = {}; + for (const relativeFile of walkSourceCommandFiles(commandRoot)) { + const source = fs.readFileSync(path.join(commandRoot, relativeFile), "utf-8"); + const commandId = commandIdFromSourceFile(relativeFile); + const commandMetadata: OclifCommandMetadata = { id: commandId }; + const summary = staticSummary(source); + const hidden = staticBoolean(source, "hidden"); + const strict = staticBoolean(source, "strict"); + if (summary !== undefined) commandMetadata.summary = summary; + if (hidden !== undefined) commandMetadata.hidden = hidden; + if (strict !== undefined) commandMetadata.strict = strict; + if (metadata[commandId]) throw new Error(`Duplicate source oclif command ID: ${commandId}`); + metadata[commandId] = commandMetadata; + } + return metadata; +} + function isGeneratingMetadataManifest(): boolean { return process.env.OCLIF_METADATA_MANIFEST_GENERATION === "1"; } -function loadGeneratedOclifMetadata(): Record { +function loadOclifMetadata(): Record { if (cachedMetadata) return cachedMetadata; + const commandRoot = sourceCommandsRoot(); + if (commandRoot) { + cachedMetadata = loadSourceOclifMetadata(commandRoot); + return cachedMetadata; + } + const metadataPath = generatedMetadataPath(); if (!fs.existsSync(metadataPath) && isGeneratingMetadataManifest()) return {}; if (!fs.existsSync(metadataPath)) { @@ -58,7 +144,7 @@ function loadGeneratedOclifMetadata(): Record { } export function getRegisteredOclifCommandsMetadata(): Record { - return loadGeneratedOclifMetadata(); + return loadOclifMetadata(); } export function getRegisteredOclifCommandMetadata(commandId: string): OclifCommandMetadata | null { diff --git a/test/e2e/live/openshell-gateway-auth-source-contract-helpers.ts b/test/e2e/live/openshell-gateway-auth-source-contract-helpers.ts index 87ee728058a..f9cb0df8db4 100644 --- a/test/e2e/live/openshell-gateway-auth-source-contract-helpers.ts +++ b/test/e2e/live/openshell-gateway-auth-source-contract-helpers.ts @@ -9,11 +9,9 @@ import net from "node:net"; import os from "node:os"; import path from "node:path"; -import { buildDockerDriverGatewayLaunch } from "../../../dist/lib/onboard/docker-driver-gateway-launch"; -import { - ensureDockerDriverGatewayLocalTlsBundle, - getDockerDriverGatewayLocalTlsBundle, -} from "../../../dist/lib/onboard/docker-driver-gateway-local-tls"; +import type { buildDockerDriverGatewayLaunch as buildDockerDriverGatewayLaunchSource } from "../../../src/lib/onboard/docker-driver-gateway-launch"; +import type { ensureDockerDriverGatewayLocalTlsBundle as ensureDockerDriverGatewayLocalTlsBundleSource } from "../../../src/lib/onboard/docker-driver-gateway-local-tls"; +import { getDockerDriverGatewayLocalTlsBundle } from "../../../src/lib/onboard/docker-driver-gateway-local-tls"; import type { ArtifactSink } from "../fixtures/artifacts.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import type { CleanupRegistry } from "../fixtures/cleanup.ts"; @@ -49,6 +47,11 @@ type ScenarioFixtures = { skip: SkipFn; }; +export type GatewayAuthSourceContractDependencies = { + buildDockerDriverGatewayLaunch: typeof buildDockerDriverGatewayLaunchSource; + ensureDockerDriverGatewayLocalTlsBundle: typeof ensureDockerDriverGatewayLocalTlsBundleSource; +}; + type GrpcResult = { body: string; error?: string; @@ -634,12 +637,10 @@ function createDockerBindableTempDir(prefix: string): string { return fs.mkdtempSync(path.join(root, prefix)); } -async function runOpenShellGatewayAuthSourceContractScenarioUnchecked({ - artifacts, - cleanup, - host, - skip, -}: ScenarioFixtures): Promise { +async function runOpenShellGatewayAuthSourceContractScenarioUnchecked( + { artifacts, cleanup, host, skip }: ScenarioFixtures, + dependencies: GatewayAuthSourceContractDependencies, +): Promise { const gatewayBin = requireGatewayBin(skip); const dockerBin = requireDockerBin(skip); @@ -663,7 +664,7 @@ async function runOpenShellGatewayAuthSourceContractScenarioUnchecked({ const networkCreate = run(dockerBin, ["network", "create", networkName]); expect(networkCreate.status, commandOutput(networkCreate)).toBe(0); - const certBundle = ensureDockerDriverGatewayLocalTlsBundle({ + const certBundle = dependencies.ensureDockerDriverGatewayLocalTlsBundle({ env: { ...process.env, XDG_CONFIG_HOME: path.join(stateDir, "xdg-config"), @@ -684,7 +685,7 @@ async function runOpenShellGatewayAuthSourceContractScenarioUnchecked({ OPENSHELL_SSH_GATEWAY_HOST: "127.0.0.1", OPENSHELL_SSH_GATEWAY_PORT: String(port), }; - const launch = buildDockerDriverGatewayLaunch({ + const launch = dependencies.buildDockerDriverGatewayLaunch({ env: { ...process.env, NEMOCLAW_OPENSHELL_GATEWAY_CONTAINER_PATCH: "0", @@ -808,13 +809,14 @@ async function runOpenShellGatewayAuthSourceContractScenarioUnchecked({ await artifacts.writeText("openshell-gateway.log", gatewayLog); } -export async function runOpenShellGatewayAuthSourceContractScenario({ - artifacts, - cleanup, - host, - skip, -}: ScenarioFixtures): Promise { +export async function runOpenShellGatewayAuthSourceContractScenario( + { artifacts, cleanup, host, skip }: ScenarioFixtures, + dependencies: GatewayAuthSourceContractDependencies, +): Promise { await withOpenShellGatewayAuthArtifactSafety(artifacts.rootDir, () => - runOpenShellGatewayAuthSourceContractScenarioUnchecked({ artifacts, cleanup, host, skip }), + runOpenShellGatewayAuthSourceContractScenarioUnchecked( + { artifacts, cleanup, host, skip }, + dependencies, + ), ); } diff --git a/test/e2e/live/openshell-gateway-auth-source-contract.test.ts b/test/e2e/live/openshell-gateway-auth-source-contract.test.ts index 806b5dba71e..83bf11d6a35 100644 --- a/test/e2e/live/openshell-gateway-auth-source-contract.test.ts +++ b/test/e2e/live/openshell-gateway-auth-source-contract.test.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { buildDockerDriverGatewayLaunch } from "../../../dist/lib/onboard/docker-driver-gateway-launch"; +import { ensureDockerDriverGatewayLocalTlsBundle } from "../../../dist/lib/onboard/docker-driver-gateway-local-tls"; import { test } from "../fixtures/e2e-test.ts"; import { runOpenShellGatewayAuthSourceContractScenario } from "./openshell-gateway-auth-source-contract-helpers.ts"; @@ -10,5 +12,9 @@ const OPENSHELL_GATEWAY_AUTH_CONTRACT_VERSION = "0.0.72"; test( `OpenShell ${OPENSHELL_GATEWAY_AUTH_CONTRACT_VERSION} Docker-driver gateway auth uses NemoClaw mTLS plus sandbox JWT`, { timeout: LIVE_TIMEOUT_MS }, - runOpenShellGatewayAuthSourceContractScenario, + (fixtures) => + runOpenShellGatewayAuthSourceContractScenario(fixtures, { + buildDockerDriverGatewayLaunch, + ensureDockerDriverGatewayLocalTlsBundle, + }), ); diff --git a/test/e2e/mock-parity.json b/test/e2e/mock-parity.json index 347ede250fd..5b0b06cd764 100644 --- a/test/e2e/mock-parity.json +++ b/test/e2e/mock-parity.json @@ -22,6 +22,12 @@ "live": "test/e2e/live/network-policy.test.ts", "fast": ["test/validate-blueprint.test.ts"] }, + { + "live": "test/e2e/live/openshell-gateway-auth-source-contract.test.ts", + "fast": [ + "test/e2e/support/openshell-gateway-auth-source-contract-helpers.test.ts" + ] + }, { "live": "test/e2e/live/openclaw-tui-chat-correlation.test.ts", "fast": [ diff --git a/test/package-contract/cli/completion.test.ts b/test/package-contract/cli/completion.test.ts new file mode 100644 index 00000000000..7c507d48c34 --- /dev/null +++ b/test/package-contract/cli/completion.test.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Config as OclifConfig } from "@oclif/core"; +import { describe, expect, it } from "vitest"; + +import { buildCompletionModel } from "../../../dist/lib/actions/completion"; + +function findCase( + model: ReturnType, + scope: "global" | "sandbox", + key: string, +) { + return model[scope].find((entry) => entry.key === `${scope}:${key}`); +} + +describe("compiled completion metadata", () => { + it("tracks the repository's discovered oclif and public-route registries", async () => { + const config = await OclifConfig.load(process.cwd()); + const model = buildCompletionModel(config.commands); + + expect(findCase(model, "global", "")?.candidates).toEqual( + expect.arrayContaining(["help", "resources", "uninstall", "use", "version"]), + ); + expect(findCase(model, "global", "")?.flags).toEqual( + expect.arrayContaining(["--help", "--version", "-h", "-v"]), + ); + expect(findCase(model, "global", "inference")?.candidates).toEqual( + expect.arrayContaining(["get", "set"]), + ); + expect(findCase(model, "sandbox", "sessions")?.candidates).toEqual( + expect.arrayContaining(["delete", "export", "list", "reset"]), + ); + expect(findCase(model, "sandbox", "gateway")?.candidates).toContain("restart"); + expect(findCase(model, "sandbox", "gateway-token")?.flags).toContain("--quiet"); + }); +}); diff --git a/test/package-contract/cli/oclif-metadata.test.ts b/test/package-contract/cli/oclif-metadata.test.ts index 6d8b12f500b..6995df7c53c 100644 --- a/test/package-contract/cli/oclif-metadata.test.ts +++ b/test/package-contract/cli/oclif-metadata.test.ts @@ -1,6 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + import { Config as OclifConfig } from "@oclif/core"; import { describe, expect, it } from "vitest"; @@ -32,4 +37,34 @@ describe("oclif metadata lookup", () => { it("returns null for unknown command IDs", () => { expect(getRegisteredOclifCommandMetadata("missing:nope")).toBeNull(); }); + + it("fails closed when compiled metadata has no generated manifest", () => { + const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-oclif-metadata-")); + const fixtureModule = path.join(fixtureRoot, "dist", "lib", "cli", "oclif-metadata.js"); + const sourceModule = path.join(process.cwd(), "dist", "lib", "cli", "oclif-metadata.js"); + const fixtureBranding = path.join(fixtureRoot, "dist", "lib", "cli", "branding.js"); + const sourceBranding = path.join(process.cwd(), "dist", "lib", "cli", "branding.js"); + const fixtureAliases = path.join(fixtureRoot, "dist", "lib", "agent", "aliases.js"); + const sourceAliases = path.join(process.cwd(), "dist", "lib", "agent", "aliases.js"); + const env = { ...process.env }; + delete env.OCLIF_METADATA_MANIFEST_GENERATION; + + try { + fs.mkdirSync(path.dirname(fixtureModule), { recursive: true }); + fs.mkdirSync(path.dirname(fixtureAliases), { recursive: true }); + fs.copyFileSync(sourceModule, fixtureModule); + fs.copyFileSync(sourceBranding, fixtureBranding); + fs.copyFileSync(sourceAliases, fixtureAliases); + const result = spawnSync( + process.execPath, + ["-e", `require(${JSON.stringify(fixtureModule)}).getRegisteredOclifCommandsMetadata()`], + { encoding: "utf-8", env }, + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("Missing generated oclif metadata manifest"); + } finally { + fs.rmSync(fixtureRoot, { force: true, recursive: true }); + } + }); }); diff --git a/test/package-contract/cli/oclif-pattern-discovery-helpers.ts b/test/package-contract/cli/oclif-pattern-discovery-helpers.ts new file mode 100644 index 00000000000..a3bcfb7aeb2 --- /dev/null +++ b/test/package-contract/cli/oclif-pattern-discovery-helpers.ts @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { Command } from "@oclif/core"; + +type OclifCommandClass = { + flags?: Record; +}; + +export function extendsNemoClawCommand( + commandClass: unknown, + nemoClawCommandBase: unknown, +): boolean { + if (typeof commandClass !== "function") return false; + let current = Object.getPrototypeOf(commandClass) as object | null; + while (current) { + if (current === nemoClawCommandBase) return true; + current = Object.getPrototypeOf(current) as object | null; + } + return false; +} + +export function commandOwnsHelpFlag(commandClass: unknown): boolean { + return ( + typeof commandClass === "function" && + Object.hasOwn((commandClass as OclifCommandClass).flags ?? {}, "help") + ); +} + +export async function findCommandsOutsideNemoClawBase( + commands: readonly Command.Loadable[], + nemoClawCommandBase: unknown, +): Promise { + const nonConforming: string[] = []; + for (const command of commands) { + const commandClass = await command.load(); + if (!extendsNemoClawCommand(commandClass, nemoClawCommandBase)) nonConforming.push(command.id); + } + return nonConforming; +} + +export async function findCommandsOwningHelpFlag( + commands: readonly Command.Loadable[], +): Promise { + const duplicatedHelpFlags: string[] = []; + for (const command of commands) { + const commandClass = await command.load(); + if (commandOwnsHelpFlag(commandClass)) duplicatedHelpFlags.push(command.id); + } + return duplicatedHelpFlags; +} + +export function findMissingPublicCommandStatics(commands: readonly Command.Loadable[]): string[] { + const missing: string[] = []; + for (const command of commands.filter((candidate) => candidate.hidden !== true)) { + if (!command.summary) missing.push(`${command.id}: summary`); + if (!command.description) missing.push(`${command.id}: description`); + const hasUsage = + (typeof command.usage === "string" && command.usage.length > 0) || + (Array.isArray(command.usage) && command.usage.length > 0); + if (!hasUsage) { + missing.push(`${command.id}: usage`); + } + if (!Array.isArray(command.examples) || command.examples.length === 0) { + missing.push(`${command.id}: examples`); + } + } + return missing; +} diff --git a/test/package-contract/cli/oclif-pattern-discovery.test.ts b/test/package-contract/cli/oclif-pattern-discovery.test.ts index aa8a4c8315e..2d134a34b30 100644 --- a/test/package-contract/cli/oclif-pattern-discovery.test.ts +++ b/test/package-contract/cli/oclif-pattern-discovery.test.ts @@ -2,11 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 import fs from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; -import { Config as OclifConfig } from "@oclif/core"; +import { type Command, Config as OclifConfig } from "@oclif/core"; import { describe, expect, it } from "vitest"; +import { + commandOwnsHelpFlag, + extendsNemoClawCommand, + findCommandsOutsideNemoClawBase, + findCommandsOwningHelpFlag, + findMissingPublicCommandStatics, +} from "./oclif-pattern-discovery-helpers"; + +const requireFromNode = createRequire(import.meta.url); +const { NemoClawCommand: SharedNemoClawCommand } = requireFromNode( + path.join(process.cwd(), "dist", "lib", "cli", "nemoclaw-oclif-command.js"), +) as { NemoClawCommand: unknown }; + describe("oclif pattern command discovery", () => { it("discovers representative command ids from oclif's pattern config", async () => { const config = await OclifConfig.load(process.cwd()); @@ -27,4 +41,50 @@ describe("oclif pattern command discovery", () => { false, ); }); + + it("keeps discovered commands on the shared NemoClaw oclif base", async () => { + const config = await OclifConfig.load(process.cwd()); + expect(await findCommandsOutsideNemoClawBase(config.commands, SharedNemoClawCommand)).toEqual( + [], + ); + }); + + it("compares the shared base by identity instead of constructor name", () => { + class NemoClawCommand {} + class LookalikeCommand extends NemoClawCommand {} + + expect(extendsNemoClawCommand(LookalikeCommand, SharedNemoClawCommand)).toBe(false); + }); + + it("keeps the help flag centralized on the shared base command", async () => { + const config = await OclifConfig.load(process.cwd()); + expect(await findCommandsOwningHelpFlag(config.commands)).toEqual([]); + }); + + it("detects help flags inherited from another command class", () => { + class ParentCommand { + static flags = { help: {} }; + } + class ChildCommand extends ParentCommand {} + + expect(commandOwnsHelpFlag(ChildCommand)).toBe(true); + }); + + it("keeps public discovered commands documented in oclif statics", async () => { + const config = await OclifConfig.load(process.cwd()); + expect(findMissingPublicCommandStatics(config.commands)).toEqual([]); + }); + + it("accepts Oclif's string form for command usage", () => { + const command = { + description: "Describe the command", + examples: ["<%= config.bin %> example"], + hidden: false, + id: "example", + summary: "Summarize the command", + usage: "example", + } as Command.Loadable; + + expect(findMissingPublicCommandStatics([command])).toEqual([]); + }); }); diff --git a/test/plugin-vitest-project.test.ts b/test/plugin-vitest-project.test.ts new file mode 100644 index 00000000000..fbaf849caf3 --- /dev/null +++ b/test/plugin-vitest-project.test.ts @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { execFileSync } from "node:child_process"; +import { createRequire } from "node:module"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import standalonePluginVitestConfig from "../nemoclaw/vitest.config"; +import pluginVitestProjectOptions from "../nemoclaw/vitest.project"; +import rootVitestConfig from "../vitest.config"; + +type PolicyAlias = { + find: RegExp; + replacement: string; +}; + +const repositoryRoot = path.resolve(import.meta.dirname, ".."); +const fixtureUmaskSetup = "test/helpers/normalize-fixture-umask.ts"; +const rootRequire = createRequire(path.join(repositoryRoot, "package.json")); +const pluginRequire = createRequire(path.join(repositoryRoot, "nemoclaw", "package.json")); +const pluginTypeScript = pluginRequire.resolve("typescript/bin/tsc"); + +function installedVersion(requireFromPackage: NodeJS.Require, packageName: string): string { + return (requireFromPackage(`${packageName}/package.json`) as { version: string }).version; +} + +function listedTypeScriptFiles(configPath: string): string[] { + return execFileSync( + process.execPath, + [pluginTypeScript, "--noEmit", "-p", configPath, "--listFilesOnly"], + { cwd: repositoryRoot, encoding: "utf8" }, + ) + .trim() + .split("\n") + .map((file) => path.normalize(file)); +} + +describe("plugin Vitest project contract", () => { + it("defines one canonical plugin project for root and standalone runs", () => { + const sourceTransform = pluginVitestProjectOptions.oxc; + const policyAliases = pluginVitestProjectOptions.test.alias as PolicyAlias[]; + const rootProjects = (rootVitestConfig.test?.projects ?? []) as unknown as Array<{ + test?: { name?: string }; + }>; + const rootPluginProjects = rootProjects.filter((project) => project.test?.name === "plugin"); + + expect(pluginVitestProjectOptions.root).toBe(repositoryRoot); + expect(sourceTransform.include).toEqual(/\.(?:[cm]?ts|[jt]sx)$/); + expect(pluginVitestProjectOptions.test.name).toBe("plugin"); + expect(pluginVitestProjectOptions.test.environment).toBe("node"); + expect(pluginVitestProjectOptions.test.env).toEqual({ + NEMOCLAW_DISABLE_GATEWAY_DRIFT_PREFLIGHT: "1", + }); + expect(pluginVitestProjectOptions.test.setupFiles).toEqual([fixtureUmaskSetup]); + expect(pluginVitestProjectOptions.test.include).toEqual(["nemoclaw/src/**/*.test.ts"]); + expect(policyAliases).toEqual([ + { + find: /^.*openshell-policy-boundary\.cjs$/, + replacement: path.join(repositoryRoot, "nemoclaw/src/shared/openshell-policy-boundary.cts"), + }, + ]); + expect(pluginVitestProjectOptions.test).not.toHaveProperty("globalSetup"); + expect(rootPluginProjects).toEqual([pluginVitestProjectOptions]); + expect(standalonePluginVitestConfig).toEqual({ + ...pluginVitestProjectOptions, + test: { + ...pluginVitestProjectOptions.test, + globalSetup: path.join(repositoryRoot, "test/helpers/vitest-temp-root.ts"), + }, + }); + }); + + it("keeps standalone plugin dependencies on the root Vitest toolchain", () => { + for (const packageName of ["vitest", "vite"] as const) { + expect(installedVersion(pluginRequire, packageName), packageName).toBe( + installedVersion(rootRequire, packageName), + ); + } + }); + + it("typechecks plugin production and test sources without emitting tests", () => { + const productionFiles = listedTypeScriptFiles("nemoclaw/tsconfig.json"); + const testFiles = listedTypeScriptFiles("nemoclaw/tsconfig.test.json"); + const typecheckOutput = execFileSync("npm", ["--prefix", "nemoclaw", "run", "typecheck"], { + cwd: repositoryRoot, + encoding: "utf8", + }); + + expect(productionFiles.some((file) => file.endsWith(".test.ts"))).toBe(false); + expect(testFiles).toContain(path.join(repositoryRoot, "nemoclaw", "src", "register.test.ts")); + expect(testFiles).toContain(path.join(repositoryRoot, "nemoclaw", "vitest.config.ts")); + expect(testFiles).toContain(path.join(repositoryRoot, "nemoclaw", "vitest.project.ts")); + expect(typecheckOutput).toContain("tsc --noEmit -p tsconfig.test.json"); + }); +}); diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts index 2cdbeaf0a67..27d2826282f 100644 --- a/test/pr-workflow-contract.test.ts +++ b/test/pr-workflow-contract.test.ts @@ -618,6 +618,7 @@ describe("pull request and main workflow contracts", () => { const files = new RegExp(cliTypecheck?.files ?? "(?!)", "u"); const jsFiles = new RegExp(jsTypecheck?.files ?? "(?!)", "u"); + expect(pluginTypecheck?.entry).toBe("npm --prefix nemoclaw run typecheck"); expect(cliTypecheck?.entry).toBe("npm run typecheck:cli -- --incremental"); expect(cliTypecheck?.always_run).toBeUndefined(); for (const include of cliTypeScriptConfig.include) { @@ -966,7 +967,7 @@ describe("pull request and main workflow contracts", () => { expect(buildRuns).toContain("npm run build:cli"); expect(buildRuns).toContain("npx vitest run --project package-contract"); expect(buildRuns).toContain("npm run typecheck:cli"); - expect(buildRuns).toContain("cd nemoclaw && npx tsc --noEmit --incremental"); + expect(buildRuns).toContain("npm --prefix nemoclaw run typecheck"); expect(buildRuns).toContain("npx tsc -p jsconfig.json"); expect(buildRuns).toContain("bash scripts/check-version-tag-sync.sh"); diff --git a/test/test-boundary-guards.test.ts b/test/test-boundary-guards.test.ts index f124addb872..130a4a7cccb 100644 --- a/test/test-boundary-guards.test.ts +++ b/test/test-boundary-guards.test.ts @@ -9,6 +9,8 @@ import { describe, expect, it } from "vitest"; import { findCompiledInternalViolations, + findFastProjectTransitiveViolations, + isFastProjectTestPath, isScannedTestPath, } from "../scripts/checks/no-test-dist-imports"; import { findProjectOverlaps, parseProjectListing } from "../scripts/checks/vitest-project-overlap"; @@ -16,6 +18,27 @@ import { findProjectOverlaps, parseProjectListing } from "../scripts/checks/vite const REPO_ROOT = path.join(import.meta.dirname, ".."); const SOURCE_RUNTIME = path.join(REPO_ROOT, "test", "helpers", "onboard-script-mocks.cjs"); +function withImportGraphFixture( + files: Readonly>, + inspect: (root: string) => T, +): T { + const root = fs.mkdtempSync(path.join(REPO_ROOT, "test", ".compiled-import-graph-")); + try { + for (const [file, source] of Object.entries(files)) { + const absolutePath = path.join(root, file); + fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); + fs.writeFileSync(absolutePath, source); + } + return inspect(root); + } finally { + fs.rmSync(root, { force: true, recursive: true }); + } +} + +function fixtureRepoPath(root: string, file: string): string { + return path.relative(REPO_ROOT, path.join(root, file)).split(path.sep).join("/"); +} + describe("compiled-test import boundary", () => { it("detects every supported compiled-internal reference shape", () => { const specifier = (target: string) => ["..", "dist", target].join("/"); @@ -421,6 +444,206 @@ describe("compiled-test import boundary", () => { }); }); +describe("fast-project transitive import boundary", () => { + it("uses exactly the test:fast Vitest project paths as graph roots (#6692)", () => { + expect( + [ + "src/lib/example.test.ts", + "src/lib/coverage/example.test.ts", + "src/lib/dist/example.test.ts", + "src/lib/.claude/example.test.ts", + "src/.claude/example.test.ts", + "nemoclaw/src/blueprint/example.test.ts", + "nemoclaw/src/.claude/example.test.ts", + "test/e2e/support/example.test.ts", + "test/example.test.ts", + "test/e2e/live/example.test.ts", + "test/package-contract/example.test.ts", + ].map(isFastProjectTestPath), + ).toEqual([true, true, true, false, false, true, true, true, false, false, false]); + }); + + it("reports a shortest chain through static import, export, dynamic import, and require edges (#6692)", () => { + withImportGraphFixture( + { + "dynamic.ts": 'void import("./required.js");\n', + "entry.test.ts": 'import "./exporter.js";\n', + "exporter.ts": 'export * from "./dynamic.js";\n', + "helper.ts": 'import "../../dist/lib/onboard.js";\n', + "required.ts": 'const loaded = require("./helper.js");\nexport { loaded };\n', + }, + (root) => { + expect(findFastProjectTransitiveViolations([path.join(root, "entry.test.ts")])).toEqual([ + { + chain: [ + fixtureRepoPath(root, "entry.test.ts"), + fixtureRepoPath(root, "exporter.ts"), + fixtureRepoPath(root, "dynamic.ts"), + fixtureRepoPath(root, "required.ts"), + fixtureRepoPath(root, "helper.ts"), + ], + detail: 'imports compiled CLI internals from "../../dist/lib/onboard.js"', + file: fixtureRepoPath(root, "helper.ts"), + line: 1, + }, + ]); + }, + ); + }); + + it("terminates cyclic createRequire graphs with one deterministic violation (#6692)", () => { + withImportGraphFixture( + { + "a.ts": 'import "./b.js";\n', + "b.ts": [ + 'import { createRequire } from "node:module";', + "const load = createRequire(import.meta.url);", + 'load("./c.js");', + ].join("\n"), + "c.ts": ['import "./a.js";', 'import "../../dist/commands/cycle.js";'].join("\n"), + "entry.test.ts": 'import "./a.js";\n', + }, + (root) => { + expect(findFastProjectTransitiveViolations([path.join(root, "entry.test.ts")])).toEqual([ + { + chain: [ + fixtureRepoPath(root, "entry.test.ts"), + fixtureRepoPath(root, "a.ts"), + fixtureRepoPath(root, "b.ts"), + fixtureRepoPath(root, "c.ts"), + ], + detail: 'imports compiled CLI internals from "../../dist/commands/cycle.js"', + file: fixtureRepoPath(root, "c.ts"), + line: 2, + }, + ]); + }, + ); + }); + + it("reports the shortest route when a longer import chain is discovered first (#6692)", () => { + withImportGraphFixture( + { + "entry.test.ts": ['import "./long-a.js";', 'import "./short.js";'].join("\n"), + "long-a.ts": 'import "./long-b.js";\n', + "long-b.ts": 'import "./target.js";\n', + "short.ts": 'import "./target.js";\n', + "target.ts": 'import "../../dist/lib/compiled.js";\n', + }, + (root) => { + expect(findFastProjectTransitiveViolations([path.join(root, "entry.test.ts")])).toEqual([ + { + chain: [ + fixtureRepoPath(root, "entry.test.ts"), + fixtureRepoPath(root, "short.ts"), + fixtureRepoPath(root, "target.ts"), + ], + detail: 'imports compiled CLI internals from "../../dist/lib/compiled.js"', + file: fixtureRepoPath(root, "target.ts"), + line: 1, + }, + ]); + }, + ); + }); + + it("rejects an alias whose resolved target is a compiled CLI module (#6692)", () => { + withImportGraphFixture( + { + "entry.test.ts": 'import "./source-alias.js";\n', + }, + (root) => { + const distRoot = path.join(REPO_ROOT, "dist"); + const compiledRoot = path.join(distRoot, "lib"); + const parentsToRemove = [compiledRoot, distRoot].filter((dir) => !fs.existsSync(dir)); + fs.mkdirSync(compiledRoot, { recursive: true }); + const compiledFixtureRoot = fs.mkdtempSync( + path.join(compiledRoot, ".compiled-import-alias-"), + ); + const compiledTarget = path.join(compiledFixtureRoot, "compiled.ts"); + + try { + fs.writeFileSync(compiledTarget, "export const compiled = true;\n"); + fs.symlinkSync(compiledTarget, path.join(root, "source-alias.ts")); + const compiledRepoPath = path + .relative(REPO_ROOT, compiledTarget) + .split(path.sep) + .join("/"); + expect(findFastProjectTransitiveViolations([path.join(root, "entry.test.ts")])).toEqual([ + { + chain: [fixtureRepoPath(root, "entry.test.ts")], + detail: `imports compiled CLI internals from "./source-alias.js" (resolves to ${compiledRepoPath})`, + file: fixtureRepoPath(root, "entry.test.ts"), + line: 1, + }, + ]); + } finally { + fs.rmSync(compiledFixtureRoot, { force: true, recursive: true }); + for (const parent of parentsToRemove) { + try { + fs.rmdirSync(parent); + } catch { + // Another test or build may now own the generated parent. + } + } + } + }, + ); + }); + + it("reports compiled loads inside generated scripts reached through fast helpers (#6692)", () => { + withImportGraphFixture( + { + "entry.test.ts": 'import "./helper.js";\n', + "helper.ts": 'export const script = String.raw`require("../../dist/lib/embedded.js");`;\n', + }, + (root) => { + expect(findFastProjectTransitiveViolations([path.join(root, "entry.test.ts")])).toEqual([ + { + chain: [fixtureRepoPath(root, "entry.test.ts"), fixtureRepoPath(root, "helper.ts")], + detail: 'imports compiled CLI internals from "../../dist/lib/embedded.js"', + file: fixtureRepoPath(root, "helper.ts"), + line: 1, + }, + ]); + }, + ); + }); + + it("ignores external packages whose own layout contains dist/lib (#6692)", () => { + withImportGraphFixture( + { + "entry.test.ts": 'import type { External } from "boundary-external";\n', + "node_modules/boundary-external/dist/lib/index.d.ts": "export type External = string;\n", + "node_modules/boundary-external/package.json": JSON.stringify({ + name: "boundary-external", + types: "dist/lib/index.d.ts", + version: "1.0.0", + }), + }, + (root) => { + expect(findFastProjectTransitiveViolations([path.join(root, "entry.test.ts")])).toEqual([]); + }, + ); + }); + + it("ignores unreachable compiled imports and ordinary runtime dist paths (#6692)", () => { + withImportGraphFixture( + { + "entry.test.ts": 'import "./safe.js";\n', + "orphan.ts": 'import "../../dist/lib/orphan.js";\n', + "safe.ts": [ + 'import path from "node:path";', + 'export const runtimePath = path.join(root, "dist", "lib", "runtime.js");', + ].join("\n"), + }, + (root) => { + expect(findFastProjectTransitiveViolations([path.join(root, "entry.test.ts")])).toEqual([]); + }, + ); + }); +}); + describe("Vitest project membership boundary", () => { it("accepts disjoint listings and reports duplicate membership", () => { const disjoint = parseProjectListing("[cli] src/a.test.ts\n[integration] test/b.test.ts\n"); diff --git a/vitest.config.ts b/vitest.config.ts index 38c8977d0cf..c769105aa59 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,8 +4,9 @@ import { availableParallelism } from "node:os"; import path from "node:path"; -import { defineConfig } from "vitest/config"; +import { defineConfig, defineProject } from "vitest/config"; +import pluginVitestProjectOptions from "./nemoclaw/vitest.project"; import { shouldRunBranchValidationE2E, shouldRunLiveE2E, @@ -46,6 +47,7 @@ const controlledNonLiveEnv = { // intentionally excluded below and keep their own stricter umask handling. See // test/helpers/normalize-fixture-umask.ts (#6448). const fixtureUmaskSetup = "test/helpers/normalize-fixture-umask.ts"; +const pluginVitestProject = defineProject(pluginVitestProjectOptions); const integrationProjectScheduling = resolveIntegrationProjectScheduling({ isCi, npmLifecycleEvent: process.env.npm_lifecycle_event, @@ -147,16 +149,7 @@ export default defineConfig({ include: ["test/package-contract/**/*.test.ts"], }, }, - { - ...typedSourceTransform, - test: { - name: "plugin", - alias: canonicalOpenShellPolicyAlias, - env: controlledNonLiveEnv, - setupFiles: [fixtureUmaskSetup], - include: ["nemoclaw/src/**/*.test.ts"], - }, - }, + pluginVitestProject, { ...typedSourceTransform, test: {