Skip to content

fix(cli): clean orphaned managed npm update artifacts - #7539

Merged
wenshao merged 6 commits into
QwenLM:mainfrom
patrick-andstar:codex/cleanup-managed-npm-artifacts
Jul 24, 2026
Merged

fix(cli): clean orphaned managed npm update artifacts#7539
wenshao merged 6 commits into
QwenLM:mainfrom
patrick-andstar:codex/cleanup-managed-npm-artifacts

Conversation

@patrick-andstar

Copy link
Copy Markdown
Contributor

What this PR does

Adds a conservative cleanup pass before a managed npm update begins. It removes only stale update staging directories and stale temporary active-pointer files associated with a definitively dead process.

Why it's needed

An update worker terminated by force, out-of-memory termination, or shutdown skips its normal cleanup and can leave scoped temporary artifacts on disk indefinitely. This implements Phase 1 of the linked issue without reclaiming immutable version payloads.

Reviewer Test Plan

How to verify

Create a managed-update launcher root that contains a staging directory and temporary active-pointer file for a PID that no longer exists, then begin another managed update. Confirm those two artifacts are removed. Confirm artifacts owned by a live PID, an immutable version payload, unrelated entries, and a staging-shaped symlink or junction remain.

Evidence (Before & After)

Before: the focused regression test failed because a stale managed staging directory remained. After: the focused regression test passes 13/13; Prettier and targeted ESLint also pass.

node_modules\.bin\vitest run --config .qwen/test-support/managed-npm-update.vitest.config.ts packages/cli/src/utils/managed-npm-update.test.ts --coverage.enabled=false

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ✅ tested
🐧 Linux ⚠️ not tested

Environment (optional)

Windows 11 focused Vitest harness; no user-visible or TUI behavior is changed.

Risk & Scope

  • Main risk or tradeoff: This change deletes filesystem entries, so it limits removal to the current launcher root, strict managed-artifact names, ordinary entries, and PIDs that explicitly return ESRCH; symlinks, uncertain liveness, filesystem errors, and unknown entries are retained.
  • Not validated / out of scope: The native package test configuration, package typecheck, and full preflight are unavailable in this sparse worktree because required setup files and declarations are absent; immutable version payload cleanup, lease/heartbeat design, and cross-launcher cleanup remain out of scope.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #7524

Duplicate check: searched open pull requests for 7524 in the title and body before submission; no matching open PR was found.

中文说明

此 PR 的内容

在托管 npm 更新开始前增加一个保守的清理步骤。它只删除与已明确不存在的进程关联的过期更新暂存目录和过期临时活动指针文件。

为什么需要它

更新工作进程被强制终止、因内存不足被终止或在关机时终止,会跳过正常清理,并可能无限期地在磁盘上留下受限范围内的临时工件。本 PR 实现关联 Issue 的第一阶段,不回收不可变的版本 payload。

审阅者测试计划

验证方式

创建一个托管更新 launcher 根目录,其中包含属于一个已不存在 PID 的暂存目录和临时活动指针文件,然后启动另一项托管更新。确认这两个工件被删除。确认属于存活 PID 的工件、不可变版本 payload、无关条目以及具有暂存目录形状的符号链接或 junction 均被保留。

证据(修改前与修改后)

修改前:聚焦回归测试失败,因为过期的托管暂存目录仍然存在。修改后:聚焦回归测试 13/13 通过;Prettier 和目标 ESLint 检查也通过。

node_modules\.bin\vitest run --config .qwen/test-support/managed-npm-update.vitest.config.ts packages/cli/src/utils/managed-npm-update.test.ts --coverage.enabled=false

测试平台

操作系统 状态
🍏 macOS ⚠️ 未测试
🪟 Windows ✅ 已测试
🐧 Linux ⚠️ 未测试

环境(可选)

Windows 11 聚焦 Vitest 测试工具;没有改变用户可见或 TUI 行为。

风险与范围

  • 主要风险或权衡:此变更会删除文件系统条目,因此将删除严格限制为当前 launcher 根目录、严格的托管工件名称、普通条目以及明确返回 ESRCH 的 PID;符号链接、存活状态不确定的条目、文件系统错误和未知条目都会保留。
  • 未验证 / 范围外:由于这个稀疏 worktree 缺少所需的设置文件和声明,原生包测试配置、包类型检查和完整 preflight 均无法使用;不可变版本 payload 清理、lease/heartbeat 设计以及跨 launcher 清理仍不在范围内。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

修复 #7524

重复检查:提交前已在开放 Pull Request 的标题和正文中检索 7524;未发现匹配的开放 PR。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: this is a known, observed gap — not theoretical. The managed npm update system acknowledges it in code (the "ponytail" comment at the bottom of managed-npm-update.ts), and the linked issue #7524 was filed by a maintainer describing exactly this scenario: force-killed or OOM-terminated update workers leave orphaned staging dirs and temp files indefinitely. The issue was triaged and labeled welcome-pr with an explicit Phase 1 recommendation that matches this PR's scope.

Direction: aligned. This is Phase 1 of a maintainer-filed issue, implementing the exact low-risk cleanup the triage recommended — orphan staging dirs + stale active.json.<pid> temps for definitively dead PIDs. No CHANGELOG reference needed since the feature (#7322) is recent and the gap is self-documented.

Size: not applicable — packages/cli/src/utils/ is not a core path. 63 production lines + 90 test lines, well-scoped.

Approach: the scope feels right. Conservative by design — only removes artifacts matching strict managed-name patterns, only for PIDs that explicitly return ESRCH, skips symlinks and uncertain liveness. Every edit serves the stated goal; no unrelated changes. Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是一个已知的、已观测到的缺口,而非理论性问题。托管 npm 更新系统在代码中已明确承认此问题(managed-npm-update.ts 底部的 "ponytail" 注释),关联的 issue #7524 由维护者提交,准确描述了此场景:被强制终止或 OOM 终止的更新 worker 会无限期留下孤儿 staging 目录和临时文件。该 issue 已经过分诊并标记为 welcome-pr,明确推荐了与本 PR 范围一致的 Phase 1 方案。

方向:对齐。这是维护者提交的 issue 的 Phase 1 实现,与分诊推荐的低风险清理方案完全一致——清理已确定死亡 PID 的孤儿 staging 目录和残留 active.json.<pid> 临时文件。

规模:不适用——packages/cli/src/utils/ 不是核心路径。63 行生产代码 + 90 行测试代码,范围合理。

方案:范围合理。设计上保守——仅删除严格匹配托管工件命名模式的条目,仅针对明确返回 ESRCH 的 PID,跳过符号链接和存活状态不确定的条目。所有改动都服务于既定目标,无无关变更。进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 1cc73b8a0cd7b55645ab77e093376aee88cc7c3d · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I would add a synchronous cleanup function called at the start of prepareManagedNpmUpdate, scanning the versions dir for staging-pattern entries (.version-PID-XXXXXX) and the launcher root for active.json.PID temps, removing only those whose PID returns ESRCH from process.kill(pid, 0), skipping symlinks and uncertain liveness. ~50-60 lines.

Comparison: The PR matches this almost exactly. Three small helper functions (processDoesNotExist, readDirectoryEntries, cleanupOrphanedManagedNpmUpdateArtifacts) plus one call site — 63 production lines. No simpler path missed.

No critical blockers found. Specific observations:

  • The regex /^\.(.+)-([1-9]\d*)-[A-Za-z0-9]{6}$/ correctly handles greedy backtracking for versions with hyphens (prerelease tags), and the semver.valid(match[1]) !== match[1] guard prevents false matches on non-managed directories.
  • entry.isDirectory() + entry.isSymbolicLink() double-check correctly rejects symlinks on all platforms.
  • processDoesNotExist returns false (keep artifact) for any non-ESRCH error — EPERM, EACCES, etc. Conservative and correct.
  • All removals are wrapped in try/catch with continue — cleanup failures never propagate to the update path.
  • Tests cover the key scenarios: orphaned artifacts removed, live artifacts preserved, uncertain liveness preserved, symlinks preserved. 13/13 pass.
  • Typecheck and ESLint both clean.

Real-Scenario Testing

Non-user-visible change (internal filesystem cleanup during managed npm updates). Drove the cleanup path directly via a focused filesystem scenario script — creates orphaned artifacts for a dead PID (999999999), live artifacts for the current PID, a version dir, an unknown dir, and a staging-shaped symlink, then calls prepareManagedNpmUpdate and verifies the result.

Before (main branch — no cleanup)

=== BEFORE cleanup (prepareManagedNpmUpdate call) ===
Launcher root: /tmp/triage-7539-1REkiL/updates/7abae5fb0b01ecb2
Versions dir:  /tmp/triage-7539-1REkiL/updates/7abae5fb0b01ecb2/versions

Artifacts present:
  [versions] .2.0.0-1852597-2zUTZl  (dir=true, symlink=false)
  [versions] .2.1.0-999999999-q1Bkx2  (dir=true, symlink=false)
  [versions] .2.2.0-1852597-bxppjE  (dir=true, symlink=false)
  [versions] .2.3.0-999999999-abcdef  (dir=true, symlink=true)
  [versions] 1.0.0  (dir=true, symlink=false)
  [versions] unrelated  (dir=true, symlink=false)
  [launcher] active.json.1852597
  [launcher] active.json.999999999

=== AFTER cleanup ===
Artifacts present:
  [versions] .2.0.0-1852597-2zUTZl  (dir=true, symlink=false)
  [versions] .2.1.0-999999999-q1Bkx2  (dir=true, symlink=false)   ← stale, NOT removed
  [versions] .2.2.0-1852597-bxppjE  (dir=true, symlink=false)
  [versions] .2.3.0-999999999-abcdef  (dir=true, symlink=true)
  [versions] .3.0.0-1852597-naD7PO  (dir=true, symlink=false)
  [versions] 1.0.0  (dir=true, symlink=false)
  [versions] unrelated  (dir=true, symlink=false)
  [launcher] active.json.1852597
  [launcher] active.json.999999999                                  ← stale, NOT removed

=== VERIFICATION ===
  ❌ Stale staging dir removed
  ❌ Stale active file removed
  ✅ Live staging dir preserved
  ✅ Live active file preserved
  ✅ Version dir preserved
  ✅ Unknown dir preserved
  ✅ Symlink preserved
  ✅ New staging dir created

SOME CHECKS FAILED ❌

After (PR #7539 — with cleanup)

=== BEFORE cleanup (prepareManagedNpmUpdate call) ===
Launcher root: /tmp/triage-7539-pp76OR/updates/625c5faea02e225a
Versions dir:  /tmp/triage-7539-pp76OR/updates/625c5faea02e225a/versions

Artifacts present:
  [versions] .2.0.0-1852754-GNY11w  (dir=true, symlink=false)
  [versions] .2.1.0-999999999-D9aG8l  (dir=true, symlink=false)
  [versions] .2.2.0-1852754-PFzq6X  (dir=true, symlink=false)
  [versions] .2.3.0-999999999-abcdef  (dir=true, symlink=true)
  [versions] 1.0.0  (dir=true, symlink=false)
  [versions] unrelated  (dir=true, symlink=false)
  [launcher] active.json.1852754
  [launcher] active.json.999999999

=== AFTER cleanup ===
Artifacts present:
  [versions] .2.0.0-1852754-GNY11w  (dir=true, symlink=false)
  [versions] .2.2.0-1852754-PFzq6X  (dir=true, symlink=false)
  [versions] .2.3.0-999999999-abcdef  (dir=true, symlink=true)
  [versions] .3.0.0-1852754-C9qGHS  (dir=true, symlink=false)
  [versions] 1.0.0  (dir=true, symlink=false)
  [versions] unrelated  (dir=true, symlink=false)
  [launcher] active.json.1852754

=== VERIFICATION ===
  ✅ Stale staging dir removed
  ✅ Stale active file removed
  ✅ Live staging dir preserved
  ✅ Live active file preserved
  ✅ Version dir preserved
  ✅ Unknown dir preserved
  ✅ Symlink preserved
  ✅ New staging dir created

ALL CHECKS PASSED ✅

Unit tests

 ✓ src/utils/managed-npm-update.test.ts (13 tests) 744ms

 Test Files  1 passed (1)
      Tests  13 passed (13)
中文说明

代码审查

独立方案:prepareManagedNpmUpdate 开头添加同步清理函数,扫描 versions 目录中匹配 staging 模式(.version-PID-XXXXXX)的条目和 launcher 根目录中的 active.json.PID 临时文件,仅删除 process.kill(pid, 0) 返回 ESRCH 的条目,跳过符号链接和存活状态不确定的条目。约 50-60 行。

对比: PR 与独立方案几乎完全一致。三个小辅助函数加一个调用点——63 行生产代码。没有遗漏更简路径。

未发现关键阻塞问题。具体观察:

  • 正则 /^\.(.+)-([1-9]\d*)-[A-Za-z0-9]{6}$/ 正确处理了含连字符版本(预发布标签)的贪婪回溯,semver.valid 守卫防止误匹配非托管目录。
  • entry.isDirectory() + entry.isSymbolicLink() 双重检查在所有平台上正确拒绝符号链接。
  • processDoesNotExist 对任何非 ESRCH 错误返回 false(保留工件)——保守且正确。
  • 所有删除操作都包裹在 try/catch 中——清理失败不会传播到更新路径。
  • 测试覆盖关键场景:孤儿工件被删除、存活工件被保留、不确定存活状态被保留、符号链接被保留。13/13 通过。
  • 类型检查和 ESLint 均通过。

真实场景测试

非用户可见变更(托管 npm 更新期间的内部文件系统清理)。通过聚焦文件系统场景脚本直接驱动清理路径——为死亡 PID 创建孤儿工件,为当前 PID 创建存活工件,以及版本目录、未知目录和 staging 形状的符号链接,然后调用 prepareManagedNpmUpdate 验证结果。

修改前(main 分支):过期工件 persist,2 项检查失败。
修改后(PR #7539):过期工件被清理,所有 8 项检查通过。

Qwen Code · qwen3.8-max-preview

Reviewed at 1cc73b8a0cd7b55645ab77e093376aee88cc7c3d · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage; implements exactly what the maintainer recommended in #7524, conservative by design, and the before/after proves it works.

This is a textbook Phase 1 implementation. The issue triage recommended "orphan staging + temp cleanup on startup, ~30 lines, zero risk to running sessions" — the PR delivers exactly that in 63 production lines with comprehensive tests. The cleanup is deliberately conservative: only ESRCH triggers removal, symlinks and uncertain liveness are preserved, failures never propagate. My independent proposal was essentially the same approach, and I didn't find a simpler path.

The before/after scenario confirms the fix: stale staging dirs and temp active files for dead PIDs are removed, while live artifacts, version dirs, unknown entries, and symlinks are all preserved. 13/13 unit tests pass, typecheck and lint are clean.

If I had to maintain this in six months, I'd thank the author — the code is clear, the regex patterns are self-documenting through the test cases, and the conservative error handling means this cleanup can never break the update path.

中文说明

置信度:5/5——每个阶段都干净;精确实现了维护者在 #7524 中推荐的方案,设计上保守,before/after 证明有效。

这是一个教科书式的 Phase 1 实现。Issue 分诊推荐"启动时清理孤儿 staging + 临时文件,约 30 行,对运行中会话零风险"——PR 以 63 行生产代码和全面测试精确交付。清理刻意保守:仅 ESRCH 触发删除,符号链接和不确定存活状态被保留,失败不会传播。我的独立方案本质上是相同的方法,没有找到更简路径。

before/after 场景确认了修复:死亡 PID 的过期 staging 目录和临时活动文件被删除,而存活工件、版本目录、未知条目和符号链接全部保留。13/13 单元测试通过,类型检查和 lint 干净。

Qwen Code · qwen3.8-max-preview

Reviewed at 1cc73b8a0cd7b55645ab77e093376aee88cc7c3d · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@gwinthis

Copy link
Copy Markdown
Collaborator

Review & Local Verification Report

代码审查

设计评价:安全的孤儿进程清理。 本 PR 在 managed npm update 的 staging 阶段清理崩溃进程遗留的临时文件。

核心逻辑:

  1. processDoesNotExist(pid):用 process.kill(pid, 0) 探测进程存活
    • ESRCH → 进程不存在 → 可安全清理
    • EPERM → 进程存在但无权限 → 保留(不确定时不清理)
    • 无异常 → 进程存活 → 保留
  2. 目录名正则 /^\.(.+)-([1-9]\d*)-[A-Za-z0-9]{6}$/:匹配 staging 目录格式(.版本-PID-随机后缀
  3. semver.valid() 验证版本号,避免误删非 staging 目录
  4. 跳过符号链接(entry.isSymbolicLink()),避免跟随链接删除

安全设计亮点:

  • 只删除确认已死进程的产物(ESRCH),不确定时保留
  • 同时清理 active.json.<pid> 临时文件
  • rmSync 失败时 continue,不中断整体清理

测试: 2 个测试——清理孤儿 + 不确定时保留。mock process.kill 模拟 ESRCH/EPERM。

结论

LGTM。 保守的清理策略(不确定时保留),正确的进程存活检测,完善的边界处理。

@wenshao

wenshao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

🧪 Maintainer Local Verification Report (macOS)

Tested at commit 1cc73b8a0cd7 on macOS (darwin arm64, Node v22.23.1, vitest 3.2.4). Built and run locally from an isolated worktree. Updated: added real-flow (no-mock) E2E, an A/B load-bearing proof, and screenshots; also diagnosed the failing CI check (unrelated — see §5).

verification terminal

cleanup decision matrix

1. Shipped test suite — 13/13 ✅

 ✓ src/utils/managed-npm-update.test.ts (13 tests) 391ms
      Tests  13 passed (13)

Includes the two new tests: removes only orphaned managed update artifacts before staging and keeps artifacts when process liveness is uncertain.

2. Real-flow E2E — no mocks, real macOS process.kill — 3/3 ✅

The shipped test mocks process.kill, so it never exercises the real liveness syscall — precisely the thing that differs across platforms (the PR was Windows-only tested). I added a harness that drives the real prepareManagedNpmUpdate() with zero mocks and real PIDs:

 ✓ removes dead-PID artifacts; keeps live-PID, immutable, symlink, unknown
 ✓ keeps artifacts when liveness is UNCERTAIN (real EPERM from pid 1)
 ✓ leaves a non-semver-prefixed staging-shaped dir alone even for a dead PID
      Tests  3 passed (3)

Real process.kill(pid, 0) outcomes observed on macOS (matching the PR's assumptions exactly):

Target PID Syscall Liveness Verdict
self / live worker no throw alive keep
999999999 (nonexistent) ESRCH dead remove
just-exited child ESRCH dead remove
1 = launchd (root-owned) EPERM uncertain keep

3. A/B load-bearing proof ✅

Overlaid the base managed-npm-update.ts (git show <merge-base>:…, cleanup absent) while keeping all tests, then restored head:

BASE source  →  Tests  2 failed | 14 passed (16)   ← the two removal tests fail
HEAD source  →  Tests  16 passed (16)              ← all green with the fix

The two failures are exactly the discriminating removal assertions (removes only orphaned… and my real-flow removes dead-PID…); every "keep" test passes on base too (base retains everything). This proves the cleanup is load-bearing, not tautological.

4. Typecheck & Lint ✅

  • prettier --check (both changed files): All matched files use Prettier code style!
  • eslint (both changed files): clean (exit 0)
  • tsc --noEmit: 0 errors in the changed file. (52 residual errors were TS6305 — sibling dist not built in the isolated worktree; symlinking packages/*/dist clears 51, the last being an unrelated useGeminiStream.ts type-skew. None touch this PR.)

5. ⚠️ The red Test (ubuntu) check is unrelated to this PR

The failing job fails on:

FAIL  packages/core/src/tools/agent/agent.test.ts:3157
  AgentTool > Fork dispatch (subagent_type: "fork") > runs a non-interactive fork through the background registry
  AssertionError: expected "spy" to be called with arguments: [ Any<String>, …(2) ]
 Test Files  1 failed | 521 passed | 1 skipped (523)

This is in packages/core, a fork-dispatch test. This PR changes only packages/cli/src/utils/managed-npm-update.{ts,test.ts} — zero files under packages/core — so that file is byte-identical to main. Evidence it is a pre-existing main-side failure, not caused by #7539:

  • Reproduces deterministically (2/2) on the PR head locally.
  • The same test is also red on an unrelated PR (feat(cli): add version upgrade notices #7542).
  • It lives in the area touched by the recent main commit 8511de61d fix(core): make fork subagents discoverable (#7460).

Recommendation: a rebase onto current main and/or CI re-run should clear this; it does not reflect a defect in this PR.

6. Code review notes

  • Conservative fail-safe design. Deletes only entries matching strict managed-artifact names (.<semver>-<pid>-XXXXXX staging dirs, active.json.<pid> temps) and only when process.kill(pid,0) returns ESRCH. Symlinks/junctions (isSymbolicLink() guard, target left intact), uncertain liveness (EPERM), non-semver names, unknown entries, the canonical active.json, and immutable version payloads are all retained. Any rmSync error is swallowed with continue.
  • No user-visible / TUI behavior change — the pass runs synchronously inside prepareManagedNpmUpdate before staging.
  • Implements Phase 1 of Track disk cleanup for managed npm update artifacts #7524, consistent with the existing ponytail note in the source (immutable payloads left for a later lease-based GC).

Verdict

The change itself is merge-ready — minimal, conservative, and now verified on macOS with real (un-mocked) process-liveness semantics, an A/B load-bearing proof, and clean lint/typecheck/format. The only outstanding item is the unrelated red core agent.test.ts check (§5), which should be resolved via rebase/re-run independently of this PR.

🇨🇳 中文报告

🧪 维护者本地验证报告(macOS)

测试提交 1cc73b8a0cd7,环境 macOS(darwin arm64,Node v22.23.1,vitest 3.2.4),在隔离 worktree 中本地构建并运行。本次更新:新增无 mock 的真实流程 E2E、A/B 载荷证明、截图,并诊断了失败的 CI 检查(与本 PR 无关,见第 5 节)。

1. 既有测试套件 — 13/13 ✅ 包含本 PR 新增的 2 个测试(孤立工件清理 + 存活状态不确定时保留)。

2. 真实流程 E2E — 无 mock,真实 macOS process.kill — 3/3 ✅
既有测试会 mock process.kill,因此从未真正调用跨平台差异所在的存活探测系统调用(本 PR 仅在 Windows 上测试过)。我新增了一个零 mock、用真实 PID 驱动真实 prepareManagedNpmUpdate() 的用例:

  • self / 存活 worker → 不抛异常 → alive → 保留
  • 999999999(不存在)/ 刚退出的子进程 → ESRCH → dead → 删除
  • 1 = launchd(root 所有)→ EPERM → 不确定 → 保留

真实观测到的行为与 PR 的假设完全一致。

3. A/B 载荷证明 ✅ 覆盖 base 版源码(git show <merge-base>:…,无清理逻辑)并保留全部测试:base 下 2 failed | 14 passed(恰好是两个"删除"断言失败,"保留"类断言在 base 下全部通过),恢复 head 后 16 passed (16)。证明清理逻辑是真正起作用的,而非恒真。

4. 类型检查 & Lint ✅ prettier、eslint 均干净;tsc --noEmit 在改动文件中 0 错误(其余 52 个是隔离 worktree 未构建 sibling distTS6305,与本 PR 无关)。

5. ⚠️ 失败的 Test (ubuntu) 检查与本 PR 无关
失败位于 packages/core/src/tools/agent/agent.test.ts:3157Fork dispatch > runs a non-interactive fork through the background registry)。本 PR 改动 packages/cli/src/utils/managed-npm-update.{ts,test.ts},未触碰 packages/core 任何文件——该文件与 main 逐字节相同。证据:本地在 PR head 上稳定复现(2/2);无关 PR #7542 也在同一测试上失败;该测试位于近期 main 提交 8511de61d fix(core): make fork subagents discoverable (#7460) 涉及的区域。建议:rebase 到最新 main 并/或重跑 CI 即可清除,这并非本 PR 的缺陷。

6. 代码审查要点 保守的失败安全设计:仅当名称严格匹配托管工件格式 process.kill 返回 ESRCH(确认已死)时才删除;符号链接、EPERM、非 semver 名称、未知条目、active.json、不可变版本 payload 全部保留;rmSync 出错时 continue。无用户可见 / TUI 行为变更。实现 #7524 第一阶段,与源码中已有的 ponytail 注释一致。

结论 ✅ 变更本身可合并——最小、保守,且已在 macOS 上用真实(未 mock)的进程存活语义、A/B 载荷证明及干净的 lint/typecheck/format 验证。唯一未决项是与本 PR 无关的 red 核心 agent.test.ts 检查(第 5 节),应通过 rebase/重跑独立解决。

wenshao
wenshao previously approved these changes Jul 23, 2026

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline.

— qwen3.8-max-preview via Qwen Code /review

Comment thread packages/cli/src/utils/managed-npm-update.test.ts

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@patrick-andstar

Copy link
Copy Markdown
Contributor Author

The PR is approved and updated with main, but five workflows are awaiting maintainer approval. Could you approve the pending workflows and merge once checks pass?

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

🍏 macOS Local Test Report — PR #7539

Maintainer-verified on macOS (darwin-arm64, Node v22.22.2) to complement the author's Windows testing.

1. Unit Tests — 13/13 ✅

All 13 tests in managed-npm-update.test.ts pass, including the 2 new tests added by this PR:

  • removes only orphaned managed update artifacts before staging
  • keeps artifacts when process liveness is uncertain

Note: The default vitest config hits a pre-existing @xterm/headless CJS/ESM interop error (also reproducible on main). Tests were run with @xterm/headless added to server.deps.inline to work around this unrelated issue.

Unit test results

2. Real E2E Test — 9/9 checks ✅

A real end-to-end test (no mocks) was run against the built dist/ output, exercising the actual prepareManagedNpmUpdate code path:

# Check Result
1 Stale staging dir (dead PID) removed
2 Stale temp active-pointer file (dead PID) removed
3 Live staging dir (current PID) retained
4 Live temp active-pointer file (current PID) retained
5 Immutable version payload (1.0.0/) retained
6 Unknown/unrelated directory retained
7 Non-semver staging-shaped dir retained
8 Staging-shaped symlink retained
9 New staging dir created for next update

The test used a real dead PID (spawned a child process, captured its PID, waited for exit, confirmed ESRCH) — no process.kill mocking.

E2E test results

3. TypeCheck & Lint ✅

  • tsc --noEmit -p packages/cli/tsconfig.json — clean
  • eslint packages/cli/src/utils/managed-npm-update.ts packages/cli/src/utils/managed-npm-update.test.ts — clean

4. Code Review Notes

The implementation is conservative and well-scoped:

  • processDoesNotExist: only returns true on explicit ESRCH; EPERM or other errors → retained (safe default)
  • Staging dir regex ^\.(.+)-([1-9]\d*)-[A-Za-z0-9]{6}$: matches mkdtemp output pattern; requires valid semver + numeric PID + 6-char suffix
  • Active pointer regex ^active\.json\.([1-9]\d*)$: matches the ${activeFile}.${process.pid} temp file pattern
  • Symlink guard: entry.isSymbolicLink() check prevents deleting junction/symlink artifacts
  • semver.valid guard: prevents deleting non-semver staging-shaped dirs
  • Cleanup runs synchronously before mkdtempSync, scoped to the current launcher root only

Test Environment

Item Value
OS macOS (darwin-arm64)
Node v22.22.2
Vitest 3.2.4
Branch codex/cleanup-managed-npm-artifacts

Evidence

Test evidence (screenshots, raw output, E2E script) is archived at:
wenshao/qwen-code@pr-7539-test-evidence


🇨🇳 中文测试报告

🍏 macOS 本地测试报告 — PR #7539

由维护者在 macOS (darwin-arm64, Node v22.22.2) 上验证,补充作者的 Windows 测试。

1. 单元测试 — 13/13 全部通过 ✅

managed-npm-update.test.ts 中全部 13 个测试通过,包括本 PR 新增的 2 个:

  • removes only orphaned managed update artifacts before staging
  • keeps artifacts when process liveness is uncertain

注意: 默认 vitest 配置存在 @xterm/headless CJS/ESM 互操作错误(在 main 分支上也可复现,属于已有问题)。测试通过将 @xterm/headless 添加到 server.deps.inline 来绕过此无关问题。

2. 真实 E2E 测试 — 9/9 检查项全部通过 ✅

针对构建后dist/ 输出运行了真实端到端测试(无 mock),实际调用 prepareManagedNpmUpdate 代码路径:

# 检查项 结果
1 过期暂存目录(死 PID)被删除
2 过期临时 active 指针文件(死 PID)被删除
3 存活暂存目录(当前 PID)被保留
4 存活临时 active 指针文件(当前 PID)被保留
5 不可变版本 payload(1.0.0/)被保留
6 未知/无关目录被保留
7 非 semver 暂存形状目录被保留
8 暂存形状符号链接被保留
9 为下次更新创建了新暂存目录

测试使用了真实死 PID(启动子进程、捕获 PID、等待退出、确认 ESRCH)——未使用 process.kill mock。

3. 类型检查 & Lint ✅

  • tsc --noEmit — 无错误
  • eslint — 无警告

4. 代码审查要点

实现保守且范围明确:

  • processDoesNotExist:仅在明确 ESRCH 时返回 trueEPERM 或其他错误 → 保留(安全默认)
  • 暂存目录正则 ^\.(.+)-([1-9]\d*)-[A-Za-z0-9]{6}$:匹配 mkdtemp 输出模式;要求有效 semver + 数字 PID + 6 字符后缀
  • Active 指针正则 ^active\.json\.([1-9]\d*)$:匹配 ${activeFile}.${process.pid} 临时文件模式
  • 符号链接防护entry.isSymbolicLink() 检查防止删除 junction/symlink 工件
  • semver.valid 防护:防止删除非 semver 暂存形状目录
  • 清理在 mkdtempSync 之前同步运行,仅限当前 launcher 根目录

测试环境

项目
操作系统 macOS (darwin-arm64)
Node v22.22.2
Vitest 3.2.4
分支 codex/cleanup-managed-npm-artifacts

证据存档

测试证据(截图、原始输出、E2E 脚本)存档于:
wenshao/qwen-code@pr-7539-test-evidence

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI still running. Reviewed.

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Jul 24, 2026
Merged via the queue into QwenLM:main with commit 9e0b9ac Jul 24, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Track disk cleanup for managed npm update artifacts

4 participants