mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(cli): isolate standalone version probe startup environment
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
1 parent
c57e2cf8b4
commit
ce67edaba5
5 files changed
+335
-111
No files matched your search
@@ -9,7 +9,7 @@ Status: implemented in MYWO-8 S1 (this change). Audience: S2 (Bun PTY), S3
|
||||
|
||||
`process.execPath` conflates two distinct identities:
|
||||
|
||||
- the **app launcher** — how a subprocess reaches *this build* of the CLI;
|
||||
- the **app launcher** — how a subprocess reaches _this build_ of the CLI;
|
||||
- the **JS interpreter** — the binary that runs an arbitrary JavaScript file
|
||||
or `-e` snippet as a child.
|
||||
|
||||
@@ -30,35 +30,35 @@ Classified by which identity the site actually needs.
|
||||
|
||||
### App launcher identity (how to re-invoke this build)
|
||||
|
||||
| Site | Mechanism | Notes |
|
||||
| --- | --- | --- |
|
||||
| `scripts/cli-entry.js` | stamps `QWEN_CODE_CLI` (shim path when `QWEN_CODE_LAUNCHER_PATH` set, else the entry file); captures + deletes `QWEN_CODE_LAUNCHER_PATH` | the launcher source of truth |
|
||||
| `packages/core/src/services/shellContextEnv.ts` | propagates `QWEN_CODE_CLI` into shell subprocess env; blanks a set-but-unusable script entry | consumers use `"${QWEN_CODE_CLI:-qwen}"` |
|
||||
| `packages/cli/src/cli.ts` (`getShellContextEnvVars` wiring) | same env propagation for every shell the CLI spawns | |
|
||||
| `packages/cli/src/serve/routes/daemon-update.ts` | reads `QWEN_CODE_CLI` as the bootstrap for update relaunch | |
|
||||
| `packages/cli/src/commands/review/**` | `"${QWEN_CODE_CLI:-qwen}" review …` self-invocation in agent prompts | launcher identity, pinned by tests |
|
||||
| `scripts/cli-entry.js` `relaunchAfterUpdate()` | `findLauncher()` prefers `QWEN_CODE_LAUNCHER_PATH`, then PATH scan scored by install prefix | update relaunch |
|
||||
| `packages/cli/src/utils/relaunch.ts` (`relaunchAppInChildProcess`) | supervised relaunch: `spawn(process.execPath, [execArgv, argv[1], args], ipc)`; `process.execve` variant when available | same-interpreter fallback of the launcher |
|
||||
| `packages/cli/src/utils/processUtils.ts` (`relaunchApp`) | `execve(process.execPath, [execPath, execArgv, argv…])` | same-interpreter fallback |
|
||||
| `packages/core/src/agents/arena/ArenaManager.ts` | `command: process.execPath, args: [resolve(argv[1]), …]` per arena agent | app re-invocation in a worktree; must stay launcher-shaped |
|
||||
| `packages/cli/src/serve/managed-runtime-session-worker.ts` (`currentCliWorkerLaunch`) | `command: process.execPath, args: [execArgv, QWEN_CLI_ENTRY \|\| argv[1], 'managed-runtime-worker']` | app re-invocation |
|
||||
| `packages/cli/src/commands/review/run.ts` (agent runner spawn) | `process.execPath` + entry | app re-invocation |
|
||||
| Site | Mechanism | Notes |
|
||||
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| `scripts/cli-entry.js` | stamps `QWEN_CODE_CLI` (shim path when `QWEN_CODE_LAUNCHER_PATH` set, else the entry file); captures + deletes `QWEN_CODE_LAUNCHER_PATH` | the launcher source of truth |
|
||||
| `packages/core/src/services/shellContextEnv.ts` | propagates `QWEN_CODE_CLI` into shell subprocess env; blanks a set-but-unusable script entry | consumers use `"${QWEN_CODE_CLI:-qwen}"` |
|
||||
| `packages/cli/src/cli.ts` (`getShellContextEnvVars` wiring) | same env propagation for every shell the CLI spawns | |
|
||||
| `packages/cli/src/serve/routes/daemon-update.ts` | reads `QWEN_CODE_CLI` as the bootstrap for update relaunch | |
|
||||
| `packages/cli/src/commands/review/**` | `"${QWEN_CODE_CLI:-qwen}" review …` self-invocation in agent prompts | launcher identity, pinned by tests |
|
||||
| `scripts/cli-entry.js` `relaunchAfterUpdate()` | `findLauncher()` prefers `QWEN_CODE_LAUNCHER_PATH`, then PATH scan scored by install prefix | update relaunch |
|
||||
| `packages/cli/src/utils/relaunch.ts` (`relaunchAppInChildProcess`) | supervised relaunch: `spawn(process.execPath, [execArgv, argv[1], args], ipc)`; `process.execve` variant when available | same-interpreter fallback of the launcher |
|
||||
| `packages/cli/src/utils/processUtils.ts` (`relaunchApp`) | `execve(process.execPath, [execPath, execArgv, argv…])` | same-interpreter fallback |
|
||||
| `packages/core/src/agents/arena/ArenaManager.ts` | `command: process.execPath, args: [resolve(argv[1]), …]` per arena agent | app re-invocation in a worktree; must stay launcher-shaped |
|
||||
| `packages/cli/src/serve/managed-runtime-session-worker.ts` (`currentCliWorkerLaunch`) | `command: process.execPath, args: [execArgv, QWEN_CLI_ENTRY \|\| argv[1], 'managed-runtime-worker']` | app re-invocation |
|
||||
| `packages/cli/src/commands/review/run.ts` (agent runner spawn) | `process.execPath` + entry | app re-invocation |
|
||||
|
||||
### JS interpreter identity (how to run an arbitrary JS child)
|
||||
|
||||
| Site | Use | Wired to `getJsInterpreterCommand` in this change |
|
||||
| --- | --- | --- |
|
||||
| `packages/core/src/code-mode/host-client.ts` (`hostCommand`) | code-mode host child (`host.js` / bundled `codeModeHost.js`; dev: `--import tsx host.ts`) | yes |
|
||||
| `packages/core/src/sandbox/file-worker-client.ts` | sandbox file-worker child (`sandboxAsset('file-worker')`) | yes |
|
||||
| `packages/core/src/hooks/hookRunner.ts` (surviving-hook supervisor) | `spawn(--input-type=commonjs --eval SUPERVISOR …)`, detached | yes |
|
||||
| `packages/core/src/hooks/hook-command-cgroup.ts` (`launch`) | cgroup-v2 launcher child (`--input-type=commonjs --eval LAUNCHER …`) running the hook under its delegated unit | not yet (S3) — Linux only (cgroup v2 platform guard) |
|
||||
| `packages/core/src/sandbox/bwrap-execution.ts` (`executeBwrap`) | stages the interpreter (`realpathSync(process.execPath)`) into the bubblewrap allowlist; the relay spawns the JS relay under it | not yet (S3) — Linux only (bubblewrap) |
|
||||
| `packages/core/src/sandbox/landlock-execution.ts` (`executeLandlock`) | stages the interpreter into the landlock allowlist; the relay spawns the JS relay under it | not yet (S3) — Linux only (landlock LSM) |
|
||||
| `packages/core/src/utils/getPty.ts` (`loadPty`) | Bun detection for the PTY gate (PTY disabled under Bun today — **S2 replaces this branch with a real backend**) | detection via `isBunRuntime()` |
|
||||
| `packages/cli/src/config/mem0-settings.ts` (`bundledMem0Server`) | bundled mem0 MCP child; identity check `server.command !== process.execPath` | not yet; must migrate with its identity check together (S3) |
|
||||
| `packages/core/src/agents/arena/ArenaManager.ts` | (also) worker-side in-process runtime | launcher-shaped, see above |
|
||||
| `packages/cli/src/serve/pem-certificate-blocks.ts` | helper child spawn | not yet (S3 sweep) |
|
||||
| `packages/cli/src/commands/review/test-efficacy.ts` | spawns vitest via `process.execPath` (dev-tooling path) | intentionally not; host-tool identity |
|
||||
| Site | Use | Wired to `getJsInterpreterCommand` in this change |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| `packages/core/src/code-mode/host-client.ts` (`hostCommand`) | code-mode host child (`host.js` / bundled `codeModeHost.js`; dev: `--import tsx host.ts`) | yes |
|
||||
| `packages/core/src/sandbox/file-worker-client.ts` | sandbox file-worker child (`sandboxAsset('file-worker')`) | yes |
|
||||
| `packages/core/src/hooks/hookRunner.ts` (surviving-hook supervisor) | `spawn(--input-type=commonjs --eval SUPERVISOR …)`, detached | yes |
|
||||
| `packages/core/src/hooks/hook-command-cgroup.ts` (`launch`) | cgroup-v2 launcher child (`--input-type=commonjs --eval LAUNCHER …`) running the hook under its delegated unit | not yet (S3) — Linux only (cgroup v2 platform guard) |
|
||||
| `packages/core/src/sandbox/bwrap-execution.ts` (`executeBwrap`) | stages the interpreter (`realpathSync(process.execPath)`) into the bubblewrap allowlist; the relay spawns the JS relay under it | not yet (S3) — Linux only (bubblewrap) |
|
||||
| `packages/core/src/sandbox/landlock-execution.ts` (`executeLandlock`) | stages the interpreter into the landlock allowlist; the relay spawns the JS relay under it | not yet (S3) — Linux only (landlock LSM) |
|
||||
| `packages/core/src/utils/getPty.ts` (`loadPty`) | Bun detection for the PTY gate (PTY disabled under Bun today — **S2 replaces this branch with a real backend**) | detection via `isBunRuntime()` |
|
||||
| `packages/cli/src/config/mem0-settings.ts` (`bundledMem0Server`) | bundled mem0 MCP child; identity check `server.command !== process.execPath` | not yet; must migrate with its identity check together (S3) |
|
||||
| `packages/core/src/agents/arena/ArenaManager.ts` | (also) worker-side in-process runtime | launcher-shaped, see above |
|
||||
| `packages/cli/src/serve/pem-certificate-blocks.ts` | helper child spawn | not yet (S3 sweep) |
|
||||
| `packages/cli/src/commands/review/test-efficacy.ts` | spawns vitest via `process.execPath` (dev-tooling path) | intentionally not; host-tool identity |
|
||||
|
||||
Rows marked `not yet` are owned by the **S3 migration sweep** (V8/GC/TLS/env
|
||||
compatibility stage), which uses this table as its checklist. The
|
||||
@@ -69,13 +69,13 @@ platform.
|
||||
|
||||
### Update / install identity (must stay host-Node)
|
||||
|
||||
| Site | Notes |
|
||||
| --- | --- |
|
||||
| `packages/cli/src/utils/managed-npm-update.ts` | runs npm's `cli.js` via `process.execPath`; npm **must** run under a Node identity, never the private bundled Bun — keep host Node resolution here |
|
||||
| `packages/cli/src/ui/handleAutoUpdate.ts` | picks the update command; Node-identity path |
|
||||
| `packages/cli/src/serve/daemon-update-restart.ts` | already disabled under Bun (`process.versions['bun']`) and execve-feature-detected |
|
||||
| `packages/cli/src/utils/installationInfo.ts` | reports `process.execPath` as the node path (informational) |
|
||||
| `scripts/build-standalone-release.js` / `create-standalone-package.js` | pin Bun 1.3.14, five targets, `manifest.json` (`runtime`, version); layout `bun/bin/bun`, `node/bin/node` |
|
||||
| Site | Notes |
|
||||
| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `packages/cli/src/utils/managed-npm-update.ts` | runs npm's `cli.js` via `process.execPath`; npm **must** run under a Node identity, never the private bundled Bun — keep host Node resolution here |
|
||||
| `packages/cli/src/ui/handleAutoUpdate.ts` | picks the update command; Node-identity path |
|
||||
| `packages/cli/src/serve/daemon-update-restart.ts` | already disabled under Bun (`process.versions['bun']`) and execve-feature-detected |
|
||||
| `packages/cli/src/utils/installationInfo.ts` | reports `process.execPath` as the node path (informational) |
|
||||
| `scripts/build-standalone-release.js` / `create-standalone-package.js` | pin Bun 1.3.14, five targets, `manifest.json` (`runtime`, version); layout `bun/bin/bun`, `node/bin/node` |
|
||||
|
||||
### SDK consumers
|
||||
|
||||
@@ -93,7 +93,7 @@ source installs do not have.
|
||||
`getShellContextEnvVars` propagates it. Self-invocation uses
|
||||
`"${QWEN_CODE_CLI:-qwen}"` in shells, or the stamped value for direct
|
||||
spawns.
|
||||
- Re-invoking the app *inside* the same process tree (relaunch, managed
|
||||
- Re-invoking the app _inside_ the same process tree (relaunch, managed
|
||||
workers, arena agents) keeps the same-interpreter fallback
|
||||
`process.execPath + process.argv[1]`, because the entry is a JS file in
|
||||
every shipped layout. A compiled flavor must publish its launcher through
|
||||
@@ -135,18 +135,18 @@ through `QWEN_CODE_CLI` / the entry fallback.
|
||||
## Bun compatibility facts (probed this change)
|
||||
|
||||
Probed with Bun 1.4.0 local vs Node 24.19.0 (same fixture, see
|
||||
`packages/core/src/utils/runtime-boundary.node-bun.test.ts`). The *shipped*
|
||||
`packages/core/src/utils/runtime-boundary.node-bun.test.ts`). The _shipped_
|
||||
Bun 1.3.14 pin is verified by the packaging acceptance, not here.
|
||||
|
||||
| Behavior | Node 24.19.0 | Bun 1.4.0 |
|
||||
| --- | --- | --- |
|
||||
| `--expose-gc script.js` → `typeof gc === 'function'` | yes | yes (`--expose-gc` accepted) |
|
||||
| `--input-type=commonjs --eval '…'` | yes | yes |
|
||||
| Node IPC channel (`spawn stdio:[…,'ipc']`, `process.send` / `on('message')`) | yes | yes, round-trip OK |
|
||||
| SIGTERM handler → graceful `exit(0)` | yes | yes |
|
||||
| exit-code propagation via `process.exit(n)` | yes | yes |
|
||||
| `process.execve` | yes | absent — code must feature-detect (`relaunch.ts`, `processUtils.ts`, `daemon-update-restart.ts` already do) |
|
||||
| PTY (`@lydell/node-pty`) | yes | **disabled in `getPty`** — S2 blocker |
|
||||
| Behavior | Node 24.19.0 | Bun 1.4.0 |
|
||||
| ---------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `--expose-gc script.js` → `typeof gc === 'function'` | yes | yes (`--expose-gc` accepted) |
|
||||
| `--input-type=commonjs --eval '…'` | yes | yes |
|
||||
| Node IPC channel (`spawn stdio:[…,'ipc']`, `process.send` / `on('message')`) | yes | yes, round-trip OK |
|
||||
| SIGTERM handler → graceful `exit(0)` | yes | yes |
|
||||
| exit-code propagation via `process.exit(n)` | yes | yes |
|
||||
| `process.execve` | yes | absent — code must feature-detect (`relaunch.ts`, `processUtils.ts`, `daemon-update-restart.ts` already do) |
|
||||
| PTY (`@lydell/node-pty`) | yes | **disabled in `getPty`** — S2 blocker |
|
||||
|
||||
## Validation plan
|
||||
|
||||
@@ -180,7 +180,8 @@ checksum verification, identity/version rejection, activation and rollback;
|
||||
|
||||
Both smoke processes use a single private empty directory, with removal attempted in `finally`,
|
||||
to avoid executing project or archive `bunfig.toml` preloads. Smoke environment
|
||||
copies remove NODE_OPTIONS/BUN_OPTIONS while retaining other variables. Installed
|
||||
copies remove NODE_OPTIONS/BUN_OPTIONS, CLI_VERSION, and QWEN_CODE_RELAUNCH_ARGS
|
||||
without changing the parent environment or ordinary business variables. Installed
|
||||
manifest null, scalars, arrays and invalid runtime declarations are rejected
|
||||
before download or lock allocation; an object without runtime retains the legacy
|
||||
Node fallback. Allocation assertions require a successful positive control; on
|
||||
@@ -200,11 +201,21 @@ using the existing semver rules, including `v` normalization and prereleases.
|
||||
Legal older/newer versions must be rejected before replacement in both direct
|
||||
and prepared updates. Version-only preload controls match `--version` for both
|
||||
CLI entrypoints and verify that the identity probe does not execute the preload.
|
||||
The regression also uses the actual entry wrapper with polluted ambient versions:
|
||||
correct packages still update and roll back, while older/newer packages remain
|
||||
rejected in both modes under both hosts and payload interpreters. Node runs this
|
||||
wrapper as a regression fixture; its production bundle has a compiled version.
|
||||
Test-only callback observation reports probe stage, elapsed time, signal and
|
||||
killed status where the host supports builtin binding observation. This does not
|
||||
change the 10-second budget or explain the recorded first-run timeouts; their
|
||||
root cause remains unresolved.
|
||||
Cleanup failures retain the original smoke outcome and may leave a temporary
|
||||
directory; swallowing a removal error is not evidence that it was removed.
|
||||
|
||||
Remote CI execution is still pending. The frozen PR #14187 head `d14b2ad1`
|
||||
has no `ci.yml` run: its stacked base is outside that workflow's branch filter.
|
||||
Remote CI execution is still pending. Neither the frozen PR #14210 head
|
||||
`c57e2cf8` nor the preceding PR #14187 head `d14b2ad1` has a `ci.yml` run.
|
||||
Both PRs currently target stacked branches outside the workflow's
|
||||
`pull_request` target-branch filter.
|
||||
The ECS lane reuses Node 22 and its access to nodejs.org has not been measured;
|
||||
neither that installation nor an unverified pool image satisfies the fixed
|
||||
Node 24.19.0/Bun 1.3.14 contract. Local script results do not establish a remote
|
||||
@@ -213,12 +224,16 @@ lane result. Branch filters, the PR base, and manual dispatch remain unchanged.
|
||||
For a pool with a trusted copy of the Node distribution, CI mode accepts
|
||||
`QWEN_TEST_NODE_BASE_URL`: an HTTPS directory ending in `/` containing the
|
||||
fixed archive and `SHASUMS256.txt`. The default remains the official directory;
|
||||
the checksum and hard version assertions remain mandatory. Alternatively,
|
||||
the checksum and hard version assertions remain mandatory. This override
|
||||
only supplies Node. Bun still needs access to the npm registry unless a
|
||||
verified Bun 1.3.14 executable is supplied as `PINNED_BUN`. An isolated pool
|
||||
using the Node distribution override must also supply `PINNED_BUN`. Alternatively,
|
||||
provide real `PINNED_NODE`/`PINNED_BUN` executables; the driver resolves both to
|
||||
absolute paths before changing cwd. No mirror or preinstalled fixed tools have
|
||||
been established for ECS. The minimal pending workflow change is to pass the
|
||||
verified pool resource into the existing script-test step, for example
|
||||
`QWEN_TEST_NODE_BASE_URL: ${{ vars.QWEN_TEST_NODE_BASE_URL }}`, or both executable
|
||||
`QWEN_TEST_NODE_BASE_URL: ${{ vars.QWEN_TEST_NODE_BASE_URL }}` together with
|
||||
a verified `PINNED_BUN` path when registry access is unavailable, or both executable
|
||||
paths. It must then produce an actual Actions job log with `test:scripts` rc 0
|
||||
and both worker `actualExitCode: 0` records. Until those resources and results
|
||||
exist, this is a reviewable provisioning option, not a validated CI lane.
|
||||
@@ -19,47 +19,47 @@
|
||||
|
||||
### App launcher 身份(如何再次唤起本构建)
|
||||
|
||||
| 位置 | 机制 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| `scripts/cli-entry.js` | 写入 `QWEN_CODE_CLI`(设置了 `QWEN_CODE_LAUNCHER_PATH` 时为 shim 路径,否则为入口文件);捕获并删除 `QWEN_CODE_LAUNCHER_PATH` | launcher 的事实来源 |
|
||||
| `packages/core/src/services/shellContextEnv.ts` | 将 `QWEN_CODE_CLI` 传播进 shell 子进程环境;已设置但不可用的脚本入口置空 | 消费方使用 `"${QWEN_CODE_CLI:-qwen}"` |
|
||||
| `packages/cli/src/cli.ts`(`getShellContextEnvVars` 接线) | 对 CLI 启动的所有 shell 做同样的环境传播 | |
|
||||
| `packages/cli/src/serve/routes/daemon-update.ts` | 读取 `QWEN_CODE_CLI` 作为更新重启的 bootstrap | |
|
||||
| `packages/cli/src/commands/review/**` | agent 提示词中的 `"${QWEN_CODE_CLI:-qwen}" review …` 自调用 | launcher 身份,已被测试固定 |
|
||||
| `scripts/cli-entry.js` `relaunchAfterUpdate()` | `findLauncher()` 优先 `QWEN_CODE_LAUNCHER_PATH`,其次按安装前缀打分的 PATH 扫描 | 更新后重启 |
|
||||
| `packages/cli/src/utils/relaunch.ts`(`relaunchAppInChildProcess`) | 监督式重启:`spawn(process.execPath, [execArgv, argv[1], args], ipc)`;可用时走 `process.execve` | launcher 的同解释器回退 |
|
||||
| `packages/cli/src/utils/processUtils.ts`(`relaunchApp`) | `execve(process.execPath, [execPath, execArgv, argv…])` | 同解释器回退 |
|
||||
| `packages/core/src/agents/arena/ArenaManager.ts` | 每个 arena agent `command: process.execPath, args: [resolve(argv[1]), …]` | worktree 内的应用再唤起;必须保持 launcher 形态 |
|
||||
| `packages/cli/src/serve/managed-runtime-session-worker.ts`(`currentCliWorkerLaunch`) | `command: process.execPath, args: [execArgv, QWEN_CLI_ENTRY \|\| argv[1], 'managed-runtime-worker']` | 应用再唤起 |
|
||||
| `packages/cli/src/commands/review/run.ts`(agent runner spawn) | `process.execPath` + 入口 | 应用再唤起 |
|
||||
| 位置 | 机制 | 备注 |
|
||||
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| `scripts/cli-entry.js` | 写入 `QWEN_CODE_CLI`(设置了 `QWEN_CODE_LAUNCHER_PATH` 时为 shim 路径,否则为入口文件);捕获并删除 `QWEN_CODE_LAUNCHER_PATH` | launcher 的事实来源 |
|
||||
| `packages/core/src/services/shellContextEnv.ts` | 将 `QWEN_CODE_CLI` 传播进 shell 子进程环境;已设置但不可用的脚本入口置空 | 消费方使用 `"${QWEN_CODE_CLI:-qwen}"` |
|
||||
| `packages/cli/src/cli.ts`(`getShellContextEnvVars` 接线) | 对 CLI 启动的所有 shell 做同样的环境传播 | |
|
||||
| `packages/cli/src/serve/routes/daemon-update.ts` | 读取 `QWEN_CODE_CLI` 作为更新重启的 bootstrap | |
|
||||
| `packages/cli/src/commands/review/**` | agent 提示词中的 `"${QWEN_CODE_CLI:-qwen}" review …` 自调用 | launcher 身份,已被测试固定 |
|
||||
| `scripts/cli-entry.js` `relaunchAfterUpdate()` | `findLauncher()` 优先 `QWEN_CODE_LAUNCHER_PATH`,其次按安装前缀打分的 PATH 扫描 | 更新后重启 |
|
||||
| `packages/cli/src/utils/relaunch.ts`(`relaunchAppInChildProcess`) | 监督式重启:`spawn(process.execPath, [execArgv, argv[1], args], ipc)`;可用时走 `process.execve` | launcher 的同解释器回退 |
|
||||
| `packages/cli/src/utils/processUtils.ts`(`relaunchApp`) | `execve(process.execPath, [execPath, execArgv, argv…])` | 同解释器回退 |
|
||||
| `packages/core/src/agents/arena/ArenaManager.ts` | 每个 arena agent `command: process.execPath, args: [resolve(argv[1]), …]` | worktree 内的应用再唤起;必须保持 launcher 形态 |
|
||||
| `packages/cli/src/serve/managed-runtime-session-worker.ts`(`currentCliWorkerLaunch`) | `command: process.execPath, args: [execArgv, QWEN_CLI_ENTRY \|\| argv[1], 'managed-runtime-worker']` | 应用再唤起 |
|
||||
| `packages/cli/src/commands/review/run.ts`(agent runner spawn) | `process.execPath` + 入口 | 应用再唤起 |
|
||||
|
||||
### JS interpreter 身份(如何运行任意 JS 子进程)
|
||||
|
||||
| 位置 | 用途 | 本变更是否接入 `getJsInterpreterCommand` |
|
||||
| --- | --- | --- |
|
||||
| `packages/core/src/code-mode/host-client.ts`(`hostCommand`) | code-mode host 子进程(`host.js` / 内置 `codeModeHost.js`;dev 为 `--import tsx host.ts`) | 是 |
|
||||
| `packages/core/src/sandbox/file-worker-client.ts` | sandbox file-worker 子进程(`sandboxAsset('file-worker')`) | 是 |
|
||||
| `packages/core/src/hooks/hookRunner.ts`(幸存 hook 监督进程) | `spawn(--input-type=commonjs --eval SUPERVISOR …)`,detached | 是 |
|
||||
| `packages/core/src/hooks/hook-command-cgroup.ts`(`launch`) | cgroup v2 launcher 子进程(`--input-type=commonjs --eval LAUNCHER …`),在 hook 的受托单元内运行 | 暂未(S3)—— 仅 Linux(cgroup v2 平台守卫) |
|
||||
| `packages/core/src/sandbox/bwrap-execution.ts`(`executeBwrap`) | 将解释器(`realpathSync(process.execPath)`)纳入 bubblewrap allowlist;relay 在其下启动 JS relay | 暂未(S3)—— 仅 Linux(bubblewrap) |
|
||||
| `packages/core/src/sandbox/landlock-execution.ts`(`executeLandlock`) | 将解释器纳入 landlock allowlist;relay 在其下启动 JS relay | 暂未(S3)—— 仅 Linux(landlock LSM) |
|
||||
| `packages/core/src/utils/getPty.ts`(`loadPty`) | PTY 门控的 Bun 检测(当前 Bun 下 PTY 禁用 —— **S2 用真实后端替换此分支**) | 检测走 `isBunRuntime()` |
|
||||
| `packages/cli/src/config/mem0-settings.ts`(`bundledMem0Server`) | 内置 mem0 MCP 子进程;身份判断 `server.command !== process.execPath` | 暂未;须与其身份判断一起迁移(S3) |
|
||||
| `packages/core/src/agents/arena/ArenaManager.ts` | (另)worker 侧 in-process 运行时 | launcher 形态,见上表 |
|
||||
| `packages/cli/src/serve/pem-certificate-blocks.ts` | 辅助子进程 spawn | 暂未(S3 清扫) |
|
||||
| `packages/cli/src/commands/review/test-efficacy.ts` | 经 `process.execPath` 启动 vitest(开发工具路径) | 有意不接入;保持宿主工具身份 |
|
||||
| 位置 | 用途 | 本变更是否接入 `getJsInterpreterCommand` |
|
||||
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------- |
|
||||
| `packages/core/src/code-mode/host-client.ts`(`hostCommand`) | code-mode host 子进程(`host.js` / 内置 `codeModeHost.js`;dev 为 `--import tsx host.ts`) | 是 |
|
||||
| `packages/core/src/sandbox/file-worker-client.ts` | sandbox file-worker 子进程(`sandboxAsset('file-worker')`) | 是 |
|
||||
| `packages/core/src/hooks/hookRunner.ts`(幸存 hook 监督进程) | `spawn(--input-type=commonjs --eval SUPERVISOR …)`,detached | 是 |
|
||||
| `packages/core/src/hooks/hook-command-cgroup.ts`(`launch`) | cgroup v2 launcher 子进程(`--input-type=commonjs --eval LAUNCHER …`),在 hook 的受托单元内运行 | 暂未(S3)—— 仅 Linux(cgroup v2 平台守卫) |
|
||||
| `packages/core/src/sandbox/bwrap-execution.ts`(`executeBwrap`) | 将解释器(`realpathSync(process.execPath)`)纳入 bubblewrap allowlist;relay 在其下启动 JS relay | 暂未(S3)—— 仅 Linux(bubblewrap) |
|
||||
| `packages/core/src/sandbox/landlock-execution.ts`(`executeLandlock`) | 将解释器纳入 landlock allowlist;relay 在其下启动 JS relay | 暂未(S3)—— 仅 Linux(landlock LSM) |
|
||||
| `packages/core/src/utils/getPty.ts`(`loadPty`) | PTY 门控的 Bun 检测(当前 Bun 下 PTY 禁用 —— **S2 用真实后端替换此分支**) | 检测走 `isBunRuntime()` |
|
||||
| `packages/cli/src/config/mem0-settings.ts`(`bundledMem0Server`) | 内置 mem0 MCP 子进程;身份判断 `server.command !== process.execPath` | 暂未;须与其身份判断一起迁移(S3) |
|
||||
| `packages/core/src/agents/arena/ArenaManager.ts` | (另)worker 侧 in-process 运行时 | launcher 形态,见上表 |
|
||||
| `packages/cli/src/serve/pem-certificate-blocks.ts` | 辅助子进程 spawn | 暂未(S3 清扫) |
|
||||
| `packages/cli/src/commands/review/test-efficacy.ts` | 经 `process.execPath` 启动 vitest(开发工具路径) | 有意不接入;保持宿主工具身份 |
|
||||
|
||||
标注"暂未"的行由 **S3 迁移清扫**(V8/GC/TLS/env 兼容阶段)负责,该表即其检查清单。cgroup/bwrap/landlock 三行是**仅 Linux** 机制(macOS/Windows 上有平台守卫或不生效),其迁移须对照平台矩阵中的 Linux target 验证;mem0 与 pem 两行适用于所有平台。
|
||||
|
||||
### 更新 / 安装身份(必须保持宿主 Node)
|
||||
|
||||
| 位置 | 备注 |
|
||||
| --- | --- |
|
||||
| `packages/cli/src/utils/managed-npm-update.ts` | 经 `process.execPath` 运行 npm 的 `cli.js`;npm **必须**在 Node 身份下运行,绝不能落入私有内置 Bun —— 这里要保持宿主 Node 解析 |
|
||||
| `packages/cli/src/ui/handleAutoUpdate.ts` | 选择更新命令;Node 身份路径 |
|
||||
| `packages/cli/src/serve/daemon-update-restart.ts` | 已在 Bun 下禁用(`process.versions['bun']`)且对 execve 做特性检测 |
|
||||
| `packages/cli/src/utils/installationInfo.ts` | 以 `process.execPath` 报告 node 路径(信息性) |
|
||||
| `scripts/build-standalone-release.js` / `create-standalone-package.js` | 固定 Bun 1.3.14、五个 target、`manifest.json`(`runtime`、版本);布局 `bun/bin/bun`、`node/bin/node` |
|
||||
| 位置 | 备注 |
|
||||
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `packages/cli/src/utils/managed-npm-update.ts` | 经 `process.execPath` 运行 npm 的 `cli.js`;npm **必须**在 Node 身份下运行,绝不能落入私有内置 Bun —— 这里要保持宿主 Node 解析 |
|
||||
| `packages/cli/src/ui/handleAutoUpdate.ts` | 选择更新命令;Node 身份路径 |
|
||||
| `packages/cli/src/serve/daemon-update-restart.ts` | 已在 Bun 下禁用(`process.versions['bun']`)且对 execve 做特性检测 |
|
||||
| `packages/cli/src/utils/installationInfo.ts` | 以 `process.execPath` 报告 node 路径(信息性) |
|
||||
| `scripts/build-standalone-release.js` / `create-standalone-package.js` | 固定 Bun 1.3.14、五个 target、`manifest.json`(`runtime`、版本);布局 `bun/bin/bun`、`node/bin/node` |
|
||||
|
||||
### SDK 消费方
|
||||
|
||||
@@ -87,18 +87,18 @@
|
||||
## Bun 兼容事实(本变更实测)
|
||||
|
||||
以本机 Bun 1.4.0 对照 Node 24.19.0(同一 fixture,见
|
||||
`packages/core/src/utils/runtime-boundary.node-bun.test.ts`)。*交付* 的
|
||||
`packages/core/src/utils/runtime-boundary.node-bun.test.ts`)。_交付_ 的
|
||||
Bun 1.3.14 固定版本由打包验收负责,不在此处。
|
||||
|
||||
| 行为 | Node 24.19.0 | Bun 1.4.0 |
|
||||
| --- | --- | --- |
|
||||
| `--expose-gc script.js` → `typeof gc === 'function'` | 是 | 是(接受 `--expose-gc`) |
|
||||
| `--input-type=commonjs --eval '…'` | 是 | 是 |
|
||||
| Node IPC 通道(`spawn stdio:[…,'ipc']`、`process.send` / `on('message')`) | 是 | 是,往返正常 |
|
||||
| SIGTERM 处理函数 → 优雅 `exit(0)` | 是 | 是 |
|
||||
| `process.exit(n)` 退出码传播 | 是 | 是 |
|
||||
| `process.execve` | 是 | 缺失 —— 代码必须特性检测(`relaunch.ts`、`processUtils.ts`、`daemon-update-restart.ts` 已做) |
|
||||
| PTY(`@lydell/node-pty`) | 是 | **在 `getPty` 中禁用** —— S2 阻塞项 |
|
||||
| 行为 | Node 24.19.0 | Bun 1.4.0 |
|
||||
| -------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------- |
|
||||
| `--expose-gc script.js` → `typeof gc === 'function'` | 是 | 是(接受 `--expose-gc`) |
|
||||
| `--input-type=commonjs --eval '…'` | 是 | 是 |
|
||||
| Node IPC 通道(`spawn stdio:[…,'ipc']`、`process.send` / `on('message')`) | 是 | 是,往返正常 |
|
||||
| SIGTERM 处理函数 → 优雅 `exit(0)` | 是 | 是 |
|
||||
| `process.exit(n)` 退出码传播 | 是 | 是 |
|
||||
| `process.execve` | 是 | 缺失 —— 代码必须特性检测(`relaunch.ts`、`processUtils.ts`、`daemon-update-restart.ts` 已做) |
|
||||
| PTY(`@lydell/node-pty`) | 是 | **在 `getPty` 中禁用** —— S2 阻塞项 |
|
||||
|
||||
## 验证计划
|
||||
|
||||
@@ -121,8 +121,8 @@ checksum、身份/版本拒绝、激活和回滚;`.sig` 404 仅属 checksum-on
|
||||
不代表生产签名验证。
|
||||
|
||||
两次 smoke 共用一个私有空目录,并在 `finally` 尝试删除,避免执行项目或归档中的
|
||||
`bunfig.toml` preload。smoke 环境副本移除 NODE_OPTIONS/BUN_OPTIONS,保留
|
||||
其他变量。安装 manifest 为 null、标量、数组或非法 runtime 时,在下载或锁
|
||||
`bunfig.toml` preload。smoke 环境副本移除 NODE_OPTIONS/BUN_OPTIONS、
|
||||
CLI_VERSION 和 QWEN_CODE_RELAUNCH_ARGS,不改变父环境或普通业务变量。安装 manifest 为 null、标量、数组或非法 runtime 时,在下载或锁
|
||||
分配前拒绝;合法对象缺 runtime 仍保留 legacy Node 回退。分配断言必须有
|
||||
成功更新的正对照;Bun 1.3.14 的 builtin 绑定观察不可用,仅检查目录清理,
|
||||
不宣称零分配。此入口避开 Vitest 的 Bun loader/coverage 限制,不改变其历史
|
||||
@@ -136,22 +136,31 @@ Node 归档(核对发布 SHA256)和固定 @oven Bun 包(核对发布 integ
|
||||
Smoke 同时按既有 semver 规则比较实际 CLI 与请求版本,保留 `v` 规范化和预发布
|
||||
兼容。direct 与 prepared 更新的合法旧版/新版错配均须在替换前拒绝。仅版本
|
||||
preload 正对照通过 `--version` 匹配两个 CLI 入口,并验证身份探针不会执行
|
||||
preload。清理失败保留原 smoke 结果,可能留下临时目录;吞掉删除异常不能证明
|
||||
preload。回归还使用真实入口 wrapper 注入 ambient 版本:正确包仍能更新和回滚,
|
||||
旧/新错配仍拒绝,覆盖双宿主、双 payload 解释器及两种更新模式。Node 在此
|
||||
使用 wrapper 专项夹具,其生产 bundle 的版本是编译期常量。测试侧 callback
|
||||
观察在宿主支持 builtin 绑定观察时记录探针阶段、耗时、signal 和 killed,
|
||||
不改变 10 秒预算,也不能解释已记录的首轮 timeout;其根因仍未确定。
|
||||
清理失败保留原 smoke 结果,可能留下临时目录;吞掉删除异常不能证明
|
||||
目录已经删除。
|
||||
|
||||
真实远端 CI 执行仍待验证。冻结 PR #14187 head `d14b2ad1` 没有 `ci.yml` run:
|
||||
堆叠 base 不在该 workflow 的分支过滤范围内。ECS lane 复用 Node 22,尚未实测
|
||||
真实远端 CI 执行仍待验证。本次冻结 PR #14210 head `c57e2cf8` 与前序
|
||||
PR #14187 head `d14b2ad1` 均没有 `ci.yml` run:两 PR 当前的堆叠目标分支
|
||||
均不匹配该 workflow 的 `pull_request` 目标分支过滤。ECS lane 复用 Node 22,尚未实测
|
||||
其对 nodejs.org 的可达性;该安装或未经核实的池镜像均不能满足固定
|
||||
Node 24.19.0/Bun 1.3.14 合同。本地脚本结果不能代表远端 lane 通过。
|
||||
分支过滤、PR base 与手动 dispatch 均维持原状。
|
||||
|
||||
若池具有可信 Node 分发副本,CI 模式接受 `QWEN_TEST_NODE_BASE_URL`:以 `/`
|
||||
结尾的 HTTPS 目录,包含固定归档和 `SHASUMS256.txt`。默认仍为官方目录,
|
||||
checksum 和固定版本硬断言仍必须通过。也可传入真实的 `PINNED_NODE`/
|
||||
checksum 和固定版本硬断言仍必须通过。此覆盖仅供应 Node;除非传入已核证的
|
||||
Bun 1.3.14 `PINNED_BUN`,Bun 仍需访问 npm registry。隔离池采用 Node 分发
|
||||
副本方案时必须同时供应 `PINNED_BUN`。也可传入真实的 `PINNED_NODE`/
|
||||
`PINNED_BUN` 可执行文件;driver 在改变 cwd 前将两者解析为绝对路径。
|
||||
ECS 的镜像或固定预装工具尚未落实。待验证的最小 workflow 修改是在既有
|
||||
脚本测试步骤传入已核证的池资源,例如
|
||||
`QWEN_TEST_NODE_BASE_URL: ${{ vars.QWEN_TEST_NODE_BASE_URL }}`,或两条解释器路径。
|
||||
`QWEN_TEST_NODE_BASE_URL: ${{ vars.QWEN_TEST_NODE_BASE_URL }}`,registry 不可达时
|
||||
同时传入已核证的 `PINNED_BUN` 路径;或传入两条解释器路径。
|
||||
随后必须获得真实 Actions job 日志,证明 `test:scripts` rc 0 和两个 worker 的
|
||||
`actualExitCode: 0`。资源与结果未落实前,这只是可评审的配置方案,不是已验证
|
||||
的 CI lane。
|
||||
@@ -726,6 +726,10 @@ describe('standalone-update', () => {
|
||||
vi.stubEnv('NODE_OPTIONS', `--require ${preload}`);
|
||||
vi.stubEnv('BUN_OPTIONS', `--preload ${preload}`);
|
||||
vi.stubEnv('QWEN_UPDATE_TEST_ENV', 'preserved');
|
||||
vi.stubEnv('CLI_VERSION', '9.9.9');
|
||||
vi.stubEnv('QWEN_CODE_RELAUNCH_ARGS', '["--help"]');
|
||||
vi.stubEnv('cli_version', '9.9.9');
|
||||
vi.stubEnv('qwen_code_relaunch_args', '["--help"]');
|
||||
for (const name of ['TMPDIR', 'TEMP', 'TMP']) vi.stubEnv(name, tempDir);
|
||||
const installedManifest = JSON.stringify({ target, runtime });
|
||||
fs.writeFileSync(
|
||||
@@ -738,7 +742,7 @@ describe('standalone-update', () => {
|
||||
target,
|
||||
interpreterPath: process.execPath,
|
||||
cliSource:
|
||||
'if (process.env.QWEN_UPDATE_TEST_ENV !== "preserved") process.exit(9); console.log("1.2.3");',
|
||||
'if (process.env.QWEN_UPDATE_TEST_ENV !== "preserved" || Object.keys(process.env).some(key => /^(CLI_VERSION|QWEN_CODE_RELAUNCH_ARGS)$/i.test(key))) process.exit(9); console.log("1.2.3");',
|
||||
});
|
||||
expect(await performStandaloneUpdate(standaloneDir, '1.2.3')).toBe(
|
||||
'done',
|
||||
@@ -750,6 +754,10 @@ describe('standalone-update', () => {
|
||||
).toBe('1.2.3');
|
||||
expect(process.env['NODE_OPTIONS']).toContain(preload);
|
||||
expect(process.env['BUN_OPTIONS']).toContain(preload);
|
||||
expect(process.env['CLI_VERSION']).toBe('9.9.9');
|
||||
expect(process.env['QWEN_CODE_RELAUNCH_ARGS']).toBe('["--help"]');
|
||||
expect(process.env['cli_version']).toBe('9.9.9');
|
||||
expect(process.env['qwen_code_relaunch_args']).toBe('["--help"]');
|
||||
expect(
|
||||
fs
|
||||
.readdirSync(tempDir)
|
||||
@@ -891,13 +899,18 @@ describe('standalone-update', () => {
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === 'win32').each([
|
||||
['direct', '0.0.9'],
|
||||
['direct', '9.9.9'],
|
||||
['prepared', '0.0.9'],
|
||||
['prepared', '9.9.9'],
|
||||
['direct', '0.0.9', false],
|
||||
['direct', '9.9.9', false],
|
||||
['prepared', '0.0.9', false],
|
||||
['prepared', '9.9.9', false],
|
||||
['direct', '0.0.9', true],
|
||||
['direct', '9.9.9', true],
|
||||
['prepared', '0.0.9', true],
|
||||
['prepared', '9.9.9', true],
|
||||
])(
|
||||
'rejects a legal CLI version mismatch before replacing live or backup (%s, %s)',
|
||||
async (mode, actualVersion) => {
|
||||
'rejects a legal CLI version mismatch before replacing live or backup (%s, %s, ambient: %s)',
|
||||
async (mode, actualVersion, ambient) => {
|
||||
if (ambient) vi.stubEnv('CLI_VERSION', '1.2.3');
|
||||
for (const name of ['TMPDIR', 'TEMP', 'TMP']) vi.stubEnv(name, tempDir);
|
||||
const runtime = process.versions['bun'] ? 'bun' : 'node';
|
||||
const target = `${os.platform()}-${os.arch()}`;
|
||||
@@ -918,7 +931,9 @@ describe('standalone-update', () => {
|
||||
runtime,
|
||||
target,
|
||||
interpreterPath: process.execPath,
|
||||
cliSource: `console.log(${JSON.stringify(actualVersion)});`,
|
||||
cliSource: ambient
|
||||
? `console.log(process.env.CLI_VERSION || ${JSON.stringify(actualVersion)});`
|
||||
: `console.log(${JSON.stringify(actualVersion)});`,
|
||||
});
|
||||
const envBefore = { ...process.env };
|
||||
const pending =
|
||||
|
||||
@@ -436,9 +436,14 @@ function spawnAndCapture(
|
||||
cwd: string,
|
||||
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||
const env = { ...process.env };
|
||||
// Probe the shipped interpreter and CLI without inherited preload hooks.
|
||||
// Probe the shipped interpreter and CLI without inherited startup overrides.
|
||||
for (const key of Object.keys(env)) {
|
||||
if (/^(NODE_OPTIONS|BUN_OPTIONS)$/i.test(key)) delete env[key];
|
||||
if (
|
||||
/^(NODE_OPTIONS|BUN_OPTIONS|CLI_VERSION|QWEN_CODE_RELAUNCH_ARGS)$/i.test(
|
||||
key,
|
||||
)
|
||||
)
|
||||
delete env[key];
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
|
||||
@@ -9,7 +9,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import https from 'node:https';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import childProcess, { spawnSync } from 'node:child_process';
|
||||
import { syncBuiltinESMExports } from 'node:module';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
@@ -45,6 +45,30 @@ async function worker(root, node, bun, expectedHost) {
|
||||
binarySha256: sha(process.execPath),
|
||||
}),
|
||||
);
|
||||
const started = performance.now();
|
||||
let phase = 'setup';
|
||||
const originalExecFile = childProcess.execFile;
|
||||
let probeCount = 0;
|
||||
childProcess.execFile = (command, args, options, callback) => {
|
||||
const probeStarted = performance.now();
|
||||
const probePhase = args[0] === '-e' ? 'identity' : 'version';
|
||||
return originalExecFile(command, args, options, (err, out, stderr) => {
|
||||
probeCount++;
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
host,
|
||||
phase,
|
||||
probePhase,
|
||||
elapsedMs: Math.round(performance.now() - probeStarted),
|
||||
code: err?.code ?? 0,
|
||||
signal: err?.signal ?? null,
|
||||
killed: err?.killed ?? false,
|
||||
}),
|
||||
);
|
||||
callback(err, out, stderr);
|
||||
});
|
||||
};
|
||||
syncBuiltinESMExports();
|
||||
const originalEnv = { ...process.env };
|
||||
const originalCwd = process.cwd();
|
||||
const tmp = path.join(root, 'tmp');
|
||||
@@ -216,6 +240,7 @@ async function worker(root, node, bun, expectedHost) {
|
||||
'legacy',
|
||||
]) {
|
||||
if (scenario === 'legacy' && runtime !== 'node') continue;
|
||||
phase = `${runtime}/${scenario}`;
|
||||
const dir = path.join(root, `${runtime}-${scenario}`);
|
||||
const live = installFixture(dir, {
|
||||
version: '1.0.0',
|
||||
@@ -354,6 +379,7 @@ async function worker(root, node, bun, expectedHost) {
|
||||
delete process.env.BUN_OPTIONS;
|
||||
for (const mode of ['direct', 'prepared']) {
|
||||
for (const actualVersion of ['0.0.9', '9.9.9']) {
|
||||
phase = `${runtime}/${mode}/mismatch/${actualVersion}`;
|
||||
pack(
|
||||
`if(process.env.QWEN_UPDATE_TEST_ENV!=='preserved')process.exit(19);console.log(${JSON.stringify(actualVersion)});`,
|
||||
);
|
||||
@@ -400,6 +426,7 @@ async function worker(root, node, bun, expectedHost) {
|
||||
['v1.2.3', '1.2.3', 'v1.2.3'],
|
||||
['1.2.3-rc.1', '1.2.3-rc.1', 'v1.2.3-rc.1'],
|
||||
]) {
|
||||
phase = `${runtime}/${mode}/normalized/${requested}`;
|
||||
pack(`console.log(${JSON.stringify(actual)});`, manifest);
|
||||
const dir = path.join(root, `${runtime}-${mode}-${requested}`);
|
||||
const live = installFixture(dir, {
|
||||
@@ -449,6 +476,137 @@ async function worker(root, node, bun, expectedHost) {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Use the production wrapper verbatim, including its ambient version
|
||||
// fast path. Node also runs this wrapper-specific fixture; its normal
|
||||
// bundled CLI is covered by the original fixed-output cases above.
|
||||
const wrapper = fs.readFileSync(
|
||||
path.join(repo, 'scripts/cli-entry.js'),
|
||||
'utf8',
|
||||
);
|
||||
for (const mode of ['direct', 'prepared']) {
|
||||
for (const actualVersion of ['1.2.3', '0.0.9', '9.9.9']) {
|
||||
phase = `${runtime}/${mode}/ambient/${actualVersion}`;
|
||||
const legStarted = performance.now();
|
||||
const ambientVersion = actualVersion === '1.2.3' ? '9.9.9' : '1.2.3';
|
||||
fs.writeFileSync(
|
||||
path.join(payload, 'lib/package.json'),
|
||||
JSON.stringify({
|
||||
name: '@qwen-code/qwen-code',
|
||||
type: 'module',
|
||||
version: actualVersion,
|
||||
}),
|
||||
);
|
||||
pack(wrapper);
|
||||
const controlEnv = { ...process.env };
|
||||
for (const key of Object.keys(controlEnv))
|
||||
if (
|
||||
/^(NODE_OPTIONS|BUN_OPTIONS|CLI_VERSION|QWEN_CODE_RELAUNCH_ARGS)$/i.test(
|
||||
key,
|
||||
)
|
||||
)
|
||||
delete controlEnv[key];
|
||||
assert.equal(
|
||||
run(binaries[runtime], [cli, '--version'], { env: controlEnv }),
|
||||
actualVersion,
|
||||
);
|
||||
assert.equal(
|
||||
run(binaries[runtime], [cli, '--version'], {
|
||||
env: { ...controlEnv, CLI_VERSION: ambientVersion },
|
||||
}),
|
||||
ambientVersion,
|
||||
'wrapper pollution positive control',
|
||||
);
|
||||
const dir = path.join(
|
||||
root,
|
||||
`${runtime}-${mode}-ambient-${actualVersion}`,
|
||||
);
|
||||
const live = installFixture(dir, {
|
||||
version: '1.0.0',
|
||||
runtime,
|
||||
target,
|
||||
});
|
||||
const before = snapshot(dir);
|
||||
const parentEnv = { ...process.env };
|
||||
process.env.CLI_VERSION = ambientVersion;
|
||||
process.env.QWEN_CODE_RELAUNCH_ARGS = '["--version"]';
|
||||
const envBefore = { ...process.env };
|
||||
let pending;
|
||||
try {
|
||||
pending =
|
||||
mode === 'prepared'
|
||||
? await prepareStandaloneUpdate(live, '1.2.3')
|
||||
: undefined;
|
||||
const update = pending
|
||||
? pending.activate()
|
||||
: performStandaloneUpdate(live, '1.2.3');
|
||||
if (actualVersion === '1.2.3') {
|
||||
assert.equal(await update, 'done');
|
||||
assert.equal(
|
||||
sha(path.join(live, 'lib', path.basename(cli))),
|
||||
sha(cli),
|
||||
);
|
||||
assert.equal(
|
||||
JSON.parse(fs.readFileSync(path.join(live, 'manifest.json')))
|
||||
.version,
|
||||
'1.2.3',
|
||||
);
|
||||
assert.deepEqual(
|
||||
snapshot(live + '.old').filter(
|
||||
([name]) => name !== '.qwen-rollback-info.json',
|
||||
),
|
||||
before.find(([name]) => name === 'installed')[1],
|
||||
);
|
||||
assert.deepEqual(rollbackStandaloneUpdate(live), { ok: true });
|
||||
assert.deepEqual(
|
||||
snapshot(live).filter(
|
||||
([name]) => name !== '.qwen-rollback-info.json',
|
||||
),
|
||||
before.find(([name]) => name === 'installed')[1],
|
||||
);
|
||||
assert.equal(fs.existsSync(live + '.old'), false);
|
||||
} else {
|
||||
await assert.rejects(update, {
|
||||
message:
|
||||
'Smoke test failed: CLI version does not match the expected version',
|
||||
});
|
||||
assert.deepEqual(snapshot(dir), before);
|
||||
}
|
||||
assert.deepEqual({ ...process.env }, envBefore);
|
||||
assert.equal(fs.existsSync(archiveMarker), false);
|
||||
} finally {
|
||||
pending?.cleanup();
|
||||
for (const key of ['CLI_VERSION', 'QWEN_CODE_RELAUNCH_ARGS']) {
|
||||
if (key in parentEnv) process.env[key] = parentEnv[key];
|
||||
else delete process.env[key];
|
||||
}
|
||||
}
|
||||
assert.deepEqual({ ...process.env }, parentEnv);
|
||||
assert.deepEqual(fs.readdirSync(tmp), []);
|
||||
assert.deepEqual(
|
||||
fs.readdirSync(dir).sort(),
|
||||
actualVersion === '1.2.3'
|
||||
? ['installed']
|
||||
: ['installed', 'installed.old'],
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
host,
|
||||
runtime,
|
||||
mode,
|
||||
actualVersion,
|
||||
ambientVersion,
|
||||
wrapperSha256: sha(path.join(repo, 'scripts/cli-entry.js')),
|
||||
scenario: 'ambient-wrapper',
|
||||
result:
|
||||
actualVersion === '1.2.3'
|
||||
? 'updated-and-rolled-back'
|
||||
: 'rejected',
|
||||
elapsedMs: Math.round(performance.now() - legStarted),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
fs.rmSync(path.join(payload, 'lib/package.json'));
|
||||
// Real interpreter failures: malformed CLI version and nonzero exit.
|
||||
for (const [failure, source, message] of [
|
||||
[
|
||||
@@ -462,6 +620,7 @@ async function worker(root, node, bun, expectedHost) {
|
||||
'Smoke test failed: new binary exited with code 23',
|
||||
],
|
||||
]) {
|
||||
phase = `${runtime}/${failure}`;
|
||||
pack(source);
|
||||
const dir = path.join(root, `${runtime}-${failure}`);
|
||||
const live = installFixture(dir, { runtime, target });
|
||||
@@ -474,6 +633,7 @@ async function worker(root, node, bun, expectedHost) {
|
||||
console.log(JSON.stringify({ runtime, failure, rejected: true }));
|
||||
}
|
||||
// A real interpreter of the OTHER runtime must not satisfy identity.
|
||||
phase = `${runtime}/identity-mismatch`;
|
||||
fs.copyFileSync(
|
||||
binaries[runtime === 'bun' ? 'node' : 'bun'],
|
||||
path.join(payload, runtime, 'bin', runtime),
|
||||
@@ -506,6 +666,7 @@ async function worker(root, node, bun, expectedHost) {
|
||||
{ target, runtime: null },
|
||||
{ target, runtime: 'deno' },
|
||||
].entries()) {
|
||||
phase = `manifest/${mode}/${index}`;
|
||||
const dir = path.join(root, `manifest-${mode}-${index}`);
|
||||
const live = installFixture(dir, manifest);
|
||||
const before = snapshot(dir),
|
||||
@@ -537,9 +698,25 @@ async function worker(root, node, bun, expectedHost) {
|
||||
complete: true,
|
||||
unsignedFixture: 'checksum-only; signature 404',
|
||||
tlsVerification: true,
|
||||
elapsedMs: Math.round(performance.now() - started),
|
||||
probeCount,
|
||||
rawProbeObservable: probeCount > 0,
|
||||
tmpResidue: fs.readdirSync(tmp),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
host,
|
||||
failurePhase: phase,
|
||||
elapsedMs: Math.round(performance.now() - started),
|
||||
probeCount,
|
||||
rawProbeObservable: probeCount > 0,
|
||||
}),
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
childProcess.execFile = originalExecFile;
|
||||
process.chdir(originalCwd);
|
||||
fs.mkdtempSync = originalMkdtemp;
|
||||
syncBuiltinESMExports();
|
||||
@@ -689,6 +866,7 @@ if (process.argv[2] === '--ci') {
|
||||
]) {
|
||||
const dir = path.join(root, host);
|
||||
fs.mkdirSync(dir);
|
||||
const workerStarted = performance.now();
|
||||
const result = spawnSync(
|
||||
bin,
|
||||
[script, '--worker', dir, node, bun, host],
|
||||
@@ -707,6 +885,8 @@ if (process.argv[2] === '--ci') {
|
||||
host,
|
||||
actualExitCode: result.status,
|
||||
signal: result.signal,
|
||||
errorCode: result.error?.code ?? null,
|
||||
elapsedMs: Math.round(performance.now() - workerStarted),
|
||||
}),
|
||||
);
|
||||
assert.equal(result.error, undefined);
|
||||
|
||||
Reference in new issue
Block a user