diff --git a/AGENTS.md b/AGENTS.md index 68dbaadc7..bd0699413 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ Single-context repo. Read `CONTEXT.md` for domain language and testing/architect - Decide docs/skills impact up front. ## Principles (expensive lessons — each cost an incident) -- Guarantees erode at path boundaries. Any new dispatch path or fast path classifies its cells in `src/contracts/interaction-guarantees.ts` first; tsc forces completeness, you supply honesty. ADR 0011. +- Guarantees erode at path boundaries. Any new dispatch path or fast path classifies its cells in `src/contracts/interaction-guarantees.ts` first; the typechecker forces completeness, you supply honesty. ADR 0011. - A registry claim is not a semantic check: never mark a cell `runner` without reading whether the Swift code implements the guarantee's *definition*, not just a similar-sounding behavior. - Delegation-on-error is not success-path parity. A fast path that falls back on failure can still succeed on a candidate the shared rules would refuse. - Do not measure before confirming the code path can fire. An A/B whose B-arm cannot execute returns two green runs masquerading as evidence. @@ -79,7 +79,7 @@ Keep `src/daemon.ts` a thin router and `src/daemon/request-router.ts` orchestrat - Lint/format stack is OXC: - config: `.oxlintrc.json`, `.oxfmtrc.json` - TypeScript is strict enough to surface dead code early: `strict`, `isolatedModules`, `noUnusedLocals`, and `noUnusedParameters` are enabled. -- The repo emits with `tsdown` (Rolldown), not `tsc`; `pnpm typecheck` runs `tsgo` (native preview), with `typecheck:tsc` as the fallback. If declaration generation fails, inspect `tsconfig.lib.json` first. +- The repo emits with `tsdown` (Rolldown) and typechecks with `tsgo` (native preview); the `typescript` package is not a dependency, so there is no `tsc` to fall back to. If declaration generation fails, inspect `tsconfig.lib.json` first. - Dev-loop staleness has three layers; after editing runtime or runner code: `pnpm build` (dist), restart the daemon (it does not self-reload), and remember `shutdown` deliberately HANDS OFF a healthy simulator runner — the adopted runner keeps serving the old Swift binary until you kill its process or the source fingerprint changes. Verifying "my change did nothing" against an adopted runner is a classic false negative. - `tsconfig.lib.json` needs an explicit `rootDir: "./src"` for declaration layout. - Use the aggregate scripts in `package.json` when possible; they encode the expected validation bundles better than ad hoc command lists. @@ -255,7 +255,7 @@ This repo encodes invariants as self-declaring gates. The correct response to a - Duplicating logs backend logic in handlers instead of `src/daemon/app-log.ts`. - Growing `src/daemon/handlers/session.ts` or `src/platforms/apple/core/apps.ts` further without extracting Apple-family/macOS-specific helpers first. - Reintroducing an npm lockfile or assuming ESLint/Prettier still exist in this repo. -- Changing `tsconfig.lib.json`/build tooling without running `pnpm check:tooling`; declaration generation is stricter than `tsc --noEmit`. +- Changing `tsconfig.lib.json`/build tooling without running `pnpm check:tooling`; declaration generation is stricter than a plain typecheck. ## Docs & Skills - Versioned CLI help is the agent-facing source of truth. Put workflow guidance/help topics in `src/utils/cli-help.ts`, flags in `src/utils/cli-flags.ts`, CLI command overrides in `src/utils/cli-command-overrides.ts`, and assertions for important copy in `src/utils/__tests__/args.test.ts`. diff --git a/package.json b/package.json index 13f1dc925..cb41b3b59 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,6 @@ "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit", "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm", "typecheck": "tsgo -p tsconfig.json", - "typecheck:tsc": "tsc -p tsconfig.json", "test-app:install": "pnpm install --dir examples/test-app --ignore-workspace", "test-app:start": "pnpm --dir examples/test-app start", "test-app:ios": "pnpm --dir examples/test-app ios", @@ -223,11 +222,11 @@ "@typescript/native-preview": "7.0.0-dev.20260613.1", "@vitest/coverage-v8": "4.1.8", "fallow": "^2.95.0", + "oxc-parser": "^0.138.0", "oxfmt": "^0.42.0", "oxlint": "^1.69.0", "skillgym": "^0.9.1", "tsdown": "^0.22.3", - "typescript": "^6.0.3", "vite": "^8.0.16", "vitest": "^4.1.8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c03a7675..57ac97bdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: fallow: specifier: ^2.95.0 version: 2.95.0 + oxc-parser: + specifier: ^0.138.0 + version: 0.138.0 oxfmt: specifier: ^0.42.0 version: 0.42.0 @@ -35,10 +38,7 @@ importers: version: 0.9.1 tsdown: specifier: ^0.22.3 - version: 0.22.3(@typescript/native-preview@7.0.0-dev.20260613.1)(typescript@6.0.3) - typescript: - specifier: ^6.0.3 - version: 6.0.3 + version: 0.22.3(@typescript/native-preview@7.0.0-dev.20260613.1) vite: specifier: ^8.0.16 version: 8.0.16(@types/node@22.19.21)(yaml@2.9.0) @@ -203,6 +203,133 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@oxc-parser/binding-android-arm-eabi@0.138.0': + resolution: {integrity: sha512-hSYAD+F9W2Qh8SETMqBsQRx6YHvB4z+i/i36shlC7tfdZQauMs4vf3G/EQwKOkNlN7rkTiKINvsNmQb9q2MWcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.138.0': + resolution: {integrity: sha512-Ns5LLTp8cVyP8DsYqD482h0HE84xiGYRgtm7g4LtTinq209NAiMF768e/8r2NHaa0UMirS5mrT1m1VwiVmBi4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.138.0': + resolution: {integrity: sha512-Yka0m4YhKUHBIZufafSLAeO+DUrfHPtNXBlZSj7DxshquIl41x/a+i/MbRnbOy8heuLiYU1STa6h0FAAzT7Pbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.138.0': + resolution: {integrity: sha512-MWLUZZzmNRUqTWueZF27ncreaZ1wZ0gboWL2QMPxRQA2xgOmBPlGg2H9pAKJSPBlwEHcWa9TdWRiehAS+yls8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.138.0': + resolution: {integrity: sha512-Vae5tzsrzZ/lCDVCZUMi/vzSiiHEgcOEfsyIfWOHmjZ2ji+gT+n96T757yX5/f7/7JIJuiannAHJKV5ARaF6ng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.138.0': + resolution: {integrity: sha512-qkU8wv5mYexrCw0X4DHFgxGbRScwGLIIKUkHXU7xXEiLoMnQzELak2gujxfa9GFrlEgPjbyLUDFHWm67Zs38ng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.138.0': + resolution: {integrity: sha512-3HgULIvoDV7h2ZfVYzxQwOSOJnAjMwYmyUBzndNuLRGgBNI549ED0P6AGmN9y2TnSvrwJ+Q8zqdxqssMnGXitA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.138.0': + resolution: {integrity: sha512-pIonbH2p0KLCwz4CNPCi0xGqci4numpMQDCLJwLfsrEky7NUuByKDFhCjzE0E7vR3aj/lBjyMoTskHBo/qSg8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.138.0': + resolution: {integrity: sha512-cT5L1Xz/5m6Ga1hD3922gLc+fePOauJZJdApPTI/2Vu0EmYo62uHG9V5Dq65hhgU9TW10oDi2840y9cGdd7BIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.138.0': + resolution: {integrity: sha512-hKy/vvejKk3LNE/FsRbekWejLa046//TnLWtSo7ur29NIsNbSIvnOVYIirSVC7fsd6NO8UFzwDdcoZfCyBvSBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.138.0': + resolution: {integrity: sha512-bh6tjNGq0v0b9GAMu0pTv/YpTqepCFy0TIOtQHm8+41fZwLXTaB6xiEWVUSarNCXqc5kyzYcH6EOfwW1sJxJOw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.138.0': + resolution: {integrity: sha512-HhOkddcClSTtTxY10f/mACblKcQdxWy4lYYwX12G23j+S5eiJ5y1kpo1r7kKng+2bdnCBO+lCDWOVVc9kVl9+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.138.0': + resolution: {integrity: sha512-5mi+wtbeJiEa4waGG88EcEGgJBBNJdDeIcayPPcrLNMXbCrgdtbb80q0Nrat7A8NglLUVzhuTAAp7K6PjmUO8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.138.0': + resolution: {integrity: sha512-ckbq3AMI7lI8AhQtE8KdqYRmzmzwKfCU12QN/PBKXO72PfWdvvZQN0hFShDX/XRNsPqjddLmvXaQMT3zfYtNlw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.138.0': + resolution: {integrity: sha512-JrCOzHO9BYEs5Xz5JHYBxSc/hYKxfXUj5QQb64sERSbkQot6+KEgMTOR2C9hLrhaqOui65OYcFyTTS+YxXDtnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.138.0': + resolution: {integrity: sha512-eASMMfOOIfLHkWJRPSu8llByvVRM+c1M/lh18KjsjELM3y10+7B5iBbbrht9LdtsJXQ+mRuP/lJ7UWe3Ok3ehw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.138.0': + resolution: {integrity: sha512-BnTCO87Iwc57NufXS7vcrkrmpN+daeCeYr1+/xgPT6HjwNs0lBmJYeFrcOs4WkNN8yscdd6Rc4FxWh3+59hAFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.138.0': + resolution: {integrity: sha512-+Zi47boD2wKNL0hOA47Vkwk6njMZ8sOsr4Geu/56EUtlooDh9crNOU41U6bXGS0UjC4Y72HtRA1iuB6qx1ARUw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.138.0': + resolution: {integrity: sha512-SYcV674Wi2WuoBefUFgf0PBMNlZe5IF0YZ0TnP7DK+EusMVpEWq6iz+7r64svjAb7vjthzlas0FUCSlz8YkqYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.138.0': + resolution: {integrity: sha512-QZplnCxS4vPe4StAVBtvD2bW3pELlidf0Ek6iQ/HHiCjbEtrs5pFZZfLAoPhKLJyDzyxoGAdic9bSIYrJYTZcg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} @@ -799,9 +926,6 @@ packages: '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1595,6 +1719,10 @@ packages: oniguruma-to-es@4.3.5: resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} + oxc-parser@0.138.0: + resolution: {integrity: sha512-c25lvfpZ2+WY1yk6NkP0X0RTQg0ZxgSVaZHDa7lt6fEe1jwZjPWkRWvTyZ1xyaM7roVJMdtRCfbhUj/d4ims3Q==} + engines: {node: ^20.19.0 || >=22.12.0} + oxfmt@0.42.0: resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1944,11 +2072,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - unconfig-core@7.5.0: resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} @@ -2272,7 +2395,14 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 optional: true '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': @@ -2282,6 +2412,70 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@oxc-parser/binding-android-arm-eabi@0.138.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.138.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.138.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.138.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.138.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.138.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.138.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.138.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.138.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.138.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.138.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.138.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.138.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.138.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.138.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.138.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.138.0': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.138.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.138.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.138.0': + optional: true + '@oxc-project/types@0.133.0': {} '@oxc-project/types@0.138.0': {} @@ -2480,7 +2674,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-wasm32-wasi@1.1.4': @@ -2697,11 +2891,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -3744,6 +3933,31 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + oxc-parser@0.138.0: + dependencies: + '@oxc-project/types': 0.138.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.138.0 + '@oxc-parser/binding-android-arm64': 0.138.0 + '@oxc-parser/binding-darwin-arm64': 0.138.0 + '@oxc-parser/binding-darwin-x64': 0.138.0 + '@oxc-parser/binding-freebsd-x64': 0.138.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.138.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.138.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.138.0 + '@oxc-parser/binding-linux-arm64-musl': 0.138.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.138.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.138.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.138.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.138.0 + '@oxc-parser/binding-linux-x64-gnu': 0.138.0 + '@oxc-parser/binding-linux-x64-musl': 0.138.0 + '@oxc-parser/binding-openharmony-arm64': 0.138.0 + '@oxc-parser/binding-wasm32-wasi': 0.138.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.138.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.138.0 + '@oxc-parser/binding-win32-x64-msvc': 0.138.0 + oxfmt@0.42.0: dependencies: tinypool: 2.1.0 @@ -3986,7 +4200,7 @@ snapshots: resolve-pkg-maps@1.0.0: {} - rolldown-plugin-dts@0.26.0(@typescript/native-preview@7.0.0-dev.20260613.1)(rolldown@1.1.4)(typescript@6.0.3): + rolldown-plugin-dts@0.26.0(@typescript/native-preview@7.0.0-dev.20260613.1)(rolldown@1.1.4): dependencies: '@babel/generator': 8.0.0 '@babel/helper-validator-identifier': 8.0.2 @@ -3999,7 +4213,6 @@ snapshots: rolldown: 1.1.4 optionalDependencies: '@typescript/native-preview': 7.0.0-dev.20260613.1 - typescript: 6.0.3 transitivePeerDependencies: - oxc-resolver @@ -4149,7 +4362,7 @@ snapshots: trough@2.2.0: {} - tsdown@0.22.3(@typescript/native-preview@7.0.0-dev.20260613.1)(typescript@6.0.3): + tsdown@0.22.3(@typescript/native-preview@7.0.0-dev.20260613.1): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -4160,14 +4373,12 @@ snapshots: obug: 2.1.3 picomatch: 4.0.4 rolldown: 1.1.4 - rolldown-plugin-dts: 0.26.0(@typescript/native-preview@7.0.0-dev.20260613.1)(rolldown@1.1.4)(typescript@6.0.3) + rolldown-plugin-dts: 0.26.0(@typescript/native-preview@7.0.0-dev.20260613.1)(rolldown@1.1.4) semver: 7.8.5 tinyexec: 1.2.4 tinyglobby: 0.2.17 tree-kill: 1.2.2 unconfig-core: 7.5.0 - optionalDependencies: - typescript: 6.0.3 transitivePeerDependencies: - '@ts-macro/tsc' - '@typescript/native-preview' @@ -4176,8 +4387,6 @@ snapshots: tslib@2.8.1: {} - typescript@6.0.3: {} - unconfig-core@7.5.0: dependencies: '@quansync/fs': 1.0.0 diff --git a/src/platforms/web/agent-browser-provider.test.ts b/src/platforms/web/agent-browser-provider.test.ts index 0f442c8e1..762a143f3 100644 --- a/src/platforms/web/agent-browser-provider.test.ts +++ b/src/platforms/web/agent-browser-provider.test.ts @@ -336,35 +336,40 @@ test('agent-browser provider surfaces stale ref failures during requested snapsh }); test('agent-browser provider adds doctor guidance for missing binary and invalid JSON', async () => { - const missingStateDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'agent-device-web-provider-missing-'), - ); - try { - const provider = createAgentBrowserWebProvider({ stateDir: missingStateDir }); - await assert.rejects( - async () => await provider.open('https://example.test'), - (error: unknown) => - error instanceof AppError && - error.code === 'TOOL_MISSING' && - error.details?.hint === 'Run `agent-device web setup` to install the managed web backend.', + // Pin a web-supported Node version so the missing-binary path yields the + // setup hint instead of the Node upgrade hint on Node <24 hosts. + await withNodeRuntimeVersion('24.0.0', async () => { + const missingStateDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'agent-device-web-provider-missing-'), ); - } finally { - fs.rmSync(missingStateDir, { recursive: true, force: true }); - } + try { + const provider = createAgentBrowserWebProvider({ stateDir: missingStateDir }); + await assert.rejects( + async () => await provider.open('https://example.test'), + (error: unknown) => + error instanceof AppError && + error.code === 'TOOL_MISSING' && + error.details?.hint === + 'Run `agent-device web setup` to install the managed web backend.', + ); + } finally { + fs.rmSync(missingStateDir, { recursive: true, force: true }); + } - await withManagedAgentBrowserProvider({}, async (installedProvider) => { - await assert.rejects( - () => - withCommandExecutorOverride( - async () => ({ stdout: 'not-json', stderr: '', exitCode: 0 }), - async () => await installedProvider.open('https://example.test'), - ), - (error: unknown) => - error instanceof AppError && - error.code === 'COMMAND_FAILED' && - error.message === 'agent-browser returned invalid JSON' && - typeof error.details?.hint === 'string', - ); + await withManagedAgentBrowserProvider({}, async (installedProvider) => { + await assert.rejects( + () => + withCommandExecutorOverride( + async () => ({ stdout: 'not-json', stderr: '', exitCode: 0 }), + async () => await installedProvider.open('https://example.test'), + ), + (error: unknown) => + error instanceof AppError && + error.code === 'COMMAND_FAILED' && + error.message === 'agent-browser returned invalid JSON' && + typeof error.details?.hint === 'string', + ); + }); }); }); diff --git a/src/utils/__tests__/args.test.ts b/src/utils/__tests__/args.test.ts index bd4c1db53..cbf1e48c4 100644 --- a/src/utils/__tests__/args.test.ts +++ b/src/utils/__tests__/args.test.ts @@ -2,7 +2,8 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; -import * as ts from 'typescript'; +import { parseSync } from 'oxc-parser'; +import type { BinaryExpression, Expression, PrivateIdentifier } from 'oxc-parser'; import { parseArgs, usage, usageForCommand } from '../../cli/parser/args.ts'; import { AppError } from '../../kernel/errors.ts'; import { listCapabilityCommands } from '../../core/capabilities.ts'; @@ -2416,49 +2417,86 @@ test('removed trigger aliases are no longer documented as commands', async () => function collectCliDispatchCommandLiterals(): Set { const cliPath = fileURLToPath(new URL('../../cli.ts', import.meta.url)); const sourceText = fs.readFileSync(cliPath, 'utf8'); - const sourceFile = ts.createSourceFile( - cliPath, - sourceText, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TS, - ); + const parsed = parseSync(cliPath, sourceText); const commands = new Set(); - function visit(node: ts.Node): void { - if (ts.isBinaryExpression(node) && isEqualityOperator(node.operatorToken.kind)) { - const command = readCommandComparisonLiteral(node.left, node.right); - if (command) commands.add(command); - } - ts.forEachChild(node, visit); - } + visitAstNodes(parsed.program, (node) => { + const command = readBinaryComparisonCommandLiteral(node); + if (command) commands.add(command); + }); - visit(sourceFile); return commands; } -function isEqualityOperator(kind: ts.SyntaxKind): boolean { +type AstNode = { type: string }; + +function isAstNode(value: unknown): value is AstNode { return ( - kind === ts.SyntaxKind.EqualsEqualsEqualsToken || - kind === ts.SyntaxKind.ExclamationEqualsEqualsToken + typeof value === 'object' && + value !== null && + typeof (value as { type?: unknown }).type === 'string' ); } -function readCommandComparisonLiteral(left: ts.Expression, right: ts.Expression): string | null { - if (isCommandExpression(left) && ts.isStringLiteralLike(right)) return right.text; - if (isCommandExpression(right) && ts.isStringLiteralLike(left)) return left.text; - return null; +function visitAstNodes(root: AstNode, visit: (node: AstNode) => void): void { + const stack: AstNode[] = [root]; + while (stack.length > 0) { + const node = stack.pop(); + if (!node) continue; + visit(node); + for (const value of Object.values(node)) { + for (const child of Array.isArray(value) ? value : [value]) { + if (isAstNode(child)) stack.push(child); + } + } + } } -function isCommandExpression(expression: ts.Expression): boolean { +function readBinaryComparisonCommandLiteral(node: AstNode): string | null { + if (node.type !== 'BinaryExpression') return null; + const binary = node as unknown as BinaryExpression; + if (binary.operator !== '===' && binary.operator !== '!==') return null; + return ( + readCommandComparisonLiteral(binary.left, binary.right) ?? + readCommandComparisonLiteral(binary.right, binary.left) + ); +} + +function readCommandComparisonLiteral( + commandSide: Expression | PrivateIdentifier, + literalSide: Expression | PrivateIdentifier, +): string | null { + if (!isCommandExpression(commandSide)) return null; + return readStringLiteralText(unwrapParenthesizedExpression(literalSide)); +} + +function isCommandExpression(expression: Expression | PrivateIdentifier): boolean { const unwrapped = unwrapParenthesizedExpression(expression); - if (ts.isIdentifier(unwrapped)) return unwrapped.text === 'command'; - return ts.isPropertyAccessExpression(unwrapped) && unwrapped.name.text === 'command'; + if (unwrapped.type === 'Identifier') return unwrapped.name === 'command'; + return ( + unwrapped.type === 'MemberExpression' && + !unwrapped.computed && + unwrapped.property.type === 'Identifier' && + unwrapped.property.name === 'command' + ); +} + +// Mirrors ts.isStringLiteralLike: string literals plus substitution-free templates. +function readStringLiteralText(expression: Expression | PrivateIdentifier): string | null { + if (expression.type === 'Literal' && typeof expression.value === 'string') { + return expression.value; + } + if (expression.type === 'TemplateLiteral' && expression.expressions.length === 0) { + return expression.quasis[0]?.value.cooked ?? null; + } + return null; } -function unwrapParenthesizedExpression(expression: ts.Expression): ts.Expression { +function unwrapParenthesizedExpression( + expression: Expression | PrivateIdentifier, +): Expression | PrivateIdentifier { let current = expression; - while (ts.isParenthesizedExpression(current)) { + while (current.type === 'ParenthesizedExpression') { current = current.expression; } return current; diff --git a/test/integration/smoke-clean-xcuitest-derived.test.ts b/test/integration/smoke-clean-xcuitest-derived.test.ts index 04cbea4f1..d989d601d 100644 --- a/test/integration/smoke-clean-xcuitest-derived.test.ts +++ b/test/integration/smoke-clean-xcuitest-derived.test.ts @@ -84,25 +84,33 @@ test('clean-xcuitest macos removes the entire platform directory when present', } }); -test('clean-xcuitest reports cleanup failures directly', () => { - const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-clean-xcuitest-failure-')); - const derivedRoot = path.join(homeDir, '.agent-device', 'apple-runner', 'derived'); - try { - fs.mkdirSync(path.join(derivedRoot, 'Build'), { recursive: true }); - fs.chmodSync(derivedRoot, 0o500); +// Root bypasses directory permissions, so chmod cannot force the cleanup +// failure this test provokes (common in containerized agent environments). +const isRoot = process.getuid?.() === 0; - const result = runCleanXcuitest(homeDir, 'ios', { allowFailure: true }); - assert.equal(result.exitCode, 1); - assert.equal(result.stdout, ''); - assert.match( - result.stderr.trim(), - new RegExp(`^Failed to clean iOS XCTest derived data under ${escapeRegExp(derivedRoot)}: `), - ); - } finally { - fs.chmodSync(derivedRoot, 0o700); - fs.rmSync(homeDir, { recursive: true, force: true }); - } -}); +test( + 'clean-xcuitest reports cleanup failures directly', + { skip: isRoot && 'running as root: chmod cannot force a cleanup failure' }, + () => { + const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-clean-xcuitest-failure-')); + const derivedRoot = path.join(homeDir, '.agent-device', 'apple-runner', 'derived'); + try { + fs.mkdirSync(path.join(derivedRoot, 'Build'), { recursive: true }); + fs.chmodSync(derivedRoot, 0o500); + + const result = runCleanXcuitest(homeDir, 'ios', { allowFailure: true }); + assert.equal(result.exitCode, 1); + assert.equal(result.stdout, ''); + assert.match( + result.stderr.trim(), + new RegExp(`^Failed to clean iOS XCTest derived data under ${escapeRegExp(derivedRoot)}: `), + ); + } finally { + fs.chmodSync(derivedRoot, 0o700); + fs.rmSync(homeDir, { recursive: true, force: true }); + } + }, +); function runCleanXcuitest(homeDir: string, ...args: Array) { const lastArg = args.at(-1);