From 4d79cd74021b901d5a0b468ad4f6ee6988a1f431 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 18:36:00 +0000 Subject: [PATCH] build: drop tsc entirely and fix sandbox-hostile tests Follow-ups from the bundler/CI speed work, re-validated against latest main. The typescript package is gone from the toolchain: - pnpm typecheck stays on tsgo; the typecheck:tsc escape hatch is removed along with the typescript devDependency. - args.test.ts extracted cli.ts dispatch literals through the TypeScript compiler API - the only remaining consumer. It now walks the same AST via oxc-parser (matching the OXC lint/format/build stack); both implementations extract an identical 14-literal set from cli.ts, verified side by side before the swap. The substitution-free template case ts.isStringLiteralLike covered is preserved. - dts bundling is unaffected: the tsdown build uses the tsgo backend and builds green with no typescript package installed. Test fixes for containerized agent environments: - The missing-binary doctor-guidance web provider test pins Node 24 via the file's existing withNodeRuntimeVersion helper, so it asserts the setup hint instead of inheriting the host Node and failing on Node 22 (the supported engines floor). - The clean-xcuitest cleanup-failure smoke test skips as root: chmod 0o500 cannot force a removal failure when the process bypasses directory permissions. AGENTS.md toolchain notes updated to match. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS --- AGENTS.md | 6 +- package.json | 3 +- pnpm-lock.yaml | 263 ++++++++++++++++-- .../web/agent-browser-provider.test.ts | 59 ++-- src/utils/__tests__/args.test.ts | 94 +++++-- .../smoke-clean-xcuitest-derived.test.ts | 44 +-- 6 files changed, 364 insertions(+), 105 deletions(-) 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);