Skip to content

test(self-update): let the Python drivers run under their own owner - #7698

Closed
xiaoan17 wants to merge 1 commit into
loopx-project:mainfrom
xiaoan17:codex/fix-windows-python-dist-update-tests
Closed

xiaoan17 wants to merge 1 commit into
loopx-project:mainfrom
xiaoan17:codex/fix-windows-python-dist-update-tests

Conversation

@xiaoan17

@xiaoan17 xiaoan17 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Goal and source

The windows-powershell lane fails on main with four cases in
tests/test_self_update_runtime_activation.py:

FAILED test_update_preserves_selected_route_through_install_and_readback[python_pip]
FAILED test_update_preserves_selected_route_through_install_and_readback[python_pipx]
FAILED test_python_distribution_activation_ignores_archive_bin_override[python_pip]
FAILED test_python_distribution_activation_ignores_archive_bin_override[python_pipx]
4 failed, 415 passed, 20 skipped

windows-powershell is a merge-gate dependency
(scripts/ci/review_gate.py, JOB_OUTPUT["windows-powershell"] = "python_tests"), so this blocks every pull request, including the ones that
carry no Windows-specific change.

The defect

Both cases build their plan from the shared doctor_payload() fixture. That
fixture carries package.release_root, which identifies the installation as a
release snapshot. On Windows a release snapshot's apply is owned by
install-windows.ps1, so build_update_plan sets
apply_supported = os.name != "nt" to false and refuses before any driver
runs:

error = '`loopx update apply` cannot mutate an installation owned by loopx_release_snapshot'
execution = None

The cases then force execution_driver to python_pip / python_pipx and
assert result["ok"], which cannot hold for an owner that refuses the apply.
The cases are written for the Python drivers, but their fixture describes a
different owner, so on Windows they assert against a refusal.

Read back from the Windows runner:

DIAG platform='win32' loader='SourceFileLoader' ok=False changes_applied=False
DIAG execution=None
DIAG error='`loopx update apply` cannot mutate an installation owned by loopx_release_snapshot'

The Windows console-launcher refusal (_windows_console_update_refusal) is
not involved here: the runner's __main__ is a SourceFileLoader, not a
zipimport.zipimporter, and the refusal message does not appear in the log.
The upstream test_windows_console_apply_refuses_before_installation already
covers that separate path.

What this PR delivers

Give each driver a doctor report whose owner can actually run it.

  • python_distribution_doctor(driver) reports a package-manager installation
    with the matching installer (pip or pipx), so the update runs through
    the driver under test.
  • update_owner_doctor(driver) routes archive_snapshot to the existing
    release-snapshot fixture and the Python drivers to the package-manager one.
  • test_update_preserves_selected_route_through_install_and_readback keeps
    the source build for the archive driver, because that driver installs a
    source build; the Python drivers upgrade the environment the package
    manager already owns.

One file, 33 insertions and 4 deletions. No production code, schema, wire,
default or persisted format changes: the update path and its ownership rules
are untouched, and the cases now exercise the driver they name.

Validation

pytest tests/test_self_update_runtime_activation.py                        50 passed, 3 skipped
pytest tests/test_self_update_runtime_activation.py tests/test_self_update_download.py
                                                                         62 passed, 3 skipped
ruff check tests/test_self_update_runtime_activation.py                    clean

The Windows lane is the qualification for this change; the cases cannot pass
on macOS before the fix and cannot fail on it after.

Scope check

The file this PR changes is byte-identical between main and the other open
pull request that carries the same base-red (git diff <other-head> origin/main -- tests/test_self_update_runtime_activation.py is empty), and
main still contains the defect this PR removes. So this fixes the base
branch, not a branch-local state, and the other pull request's
windows-powershell red goes away when this merges.

@xiaoan17
xiaoan17 force-pushed the codex/fix-windows-python-dist-update-tests branch from c38d7a9 to 94d51fb Compare October 29, 2026 19:43
The Windows lane failed these two cases because the doctor fixture
identifies a release snapshot, whose apply belongs to install-windows.ps1
on Windows: the plan refused with 'cannot mutate an installation owned by
loopx_release_snapshot' before any driver ran.

Give each driver a doctor report whose owner can run it. The archive
driver keeps the release snapshot and its source build; the Python
drivers report a package-manager installation with the matching
installer, so the update runs through the driver under test.

Signed-off-by: xiaoan17 <anbc1224@gmail.com>
@xiaoan17
xiaoan17 force-pushed the codex/fix-windows-python-dist-update-tests branch from 9afccb7 to 6473a13 Compare October 29, 2026 21:14
@xiaoan17 xiaoan17 changed the title test: probe the Windows self-update environment (temporary) test(self-update): let the Python drivers run under their own owner Oct 29, 2026

@loopx-agent loopx-agent 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.

Reviewer: model_agent; gpt-6.1-sol; OpenAI; runtime_reported; reasoning_effort=max.

动机 / Motivation

未发现阻断 finding。维护更新测试的人需要验证 pip、pipx 和 archive 各自的安装路径。旧用例把 release snapshot 报告强行改成 Python driver,在 Windows owner 判断处提前拒绝;新头直接提供匹配的 package-manager 报告,让原断言验证真正的目标路径。修复测试输入的安装归属,保留原生产保护、路由与激活断言。本次不改变更新机制、安装权限、生产默认或 Windows console 的自保护。

这是对完整新头 6473a135c712d2e2acea8648faea4c23482234b4 的独立评审。先前临时探针头 9afccb7a6134bc0cd0093ca772013f604faf85e0 的本地判断没有发布,也没有沿用到本次结论;当前提交已移除临时输出并完成同一项测试修复。

改动思路 / Approach

python_distribution_doctor 在既有 doctor fixture 上明确 package-manager 安装及对应的 pip/pipx installer;update_owner_doctor 为 archive 保留原 snapshot fixture。两个测试不再在完成计划之后强改 execution_driver,而是让现有 _install_lifecycle 从输入事实推导正确 owner 和 driver。archive 仍使用 source build,Python distribution 使用已有包管理环境。

这复用真实安装 owner,避免了让测试宣称“Python 安装”却保留 snapshot 的拒绝条件。两个 helper 仅服务原测试,默认 doctor_payload 没有改变,生产代码也没有差异。相比在测试中关闭 Windows 保护或增加全局例外,这个输入修复的范围更小。

具体改动 / Changes

关键内容讲解

完整 base-to-head 为一个测试文件 +33/-4。新 helper 生成相应安装归属;test_update_preserves_selected_route_through_install_and_readback 保留 registry/runtime 读回检查;test_python_distribution_activation_ignores_archive_bin_override 保留 package-manager 更新后激活检查。没有新增跳过、放宽旧断言或留下诊断 dump。

采用规范 docs/guides/installing-loopx.md,精确版本 a36e01c32b0a290a96466c766629a0b01850f823,并按 docs/development/good-smokes.md 检查保留价值。原文没有条款编号,以下标签仅用于本次映射:owner-preservation 已实现,pip/pipx 和 snapshot 输入与原 owner 规则一致;console-safety 已保留,既有 test_windows_console_apply_refuses_before_installation 仍断言拒绝时不启动安装 subprocess,未将它与 snapshot owner 拒绝混为一谈。

独立执行两个原测试 × 两个 Python driver × 两种 owner 分支环境,共 base/head 16 次。普通 POSIX owner 环境两头均通过;只模拟 Windows owner 分派时,基线四次均因 loopx_release_snapshot 拒绝,新头四次均通过,实际计划中的 owner 和 driver 与参数一致。这里使用真实 plan/execution owner、模块局部 OS-name 适配及原测试的安装/重启 mock;没有冒充原生 Windows IO。

对主干的风险 / Risks

PASS:两个相关文件 62 tests;Ruff、精确 diff 检查、quick premerge 的四项直接检查与 Python compile;另已写入精确 diff 的通过质量回执。quick 层针对这一个 fixture 文件,catalog 执行项为零,没有把零项说成完整运行时回归。Goal 严格质量门已在同一精确 head 上读回为 valid,quick gate 为 passed。

3 个原生 Windows 参数实例在 macOS 跳过,仍为 UNVERIFIED;没有查询或等待远端 CI,也没有独立确认作者引用的 Windows run 已恢复。PR 描述最后关于“macOS before/after”的句子建议改清楚:本次实测普通 POSIX 分支前后都通过,只有 Windows owner 分派对照区分了修复。这是验证表述修正,不影响当前测试输入修复。

同作者最近六项 PR 的有界扫描显示不同维护主题;没有足够证据建议贡献限制。当前经验提示仅用于检查实际维护结果,恢复 RFC 的场景与历史 verdict 不适用于这里,未宣称 memory 采用收益。

我的整体评价 / Verdict

APPROVE。 本次 goal_achieved 指这项 fixture 归属修复已经完成,不代表实际 Windows 安装或远端流水线资格。long_horizon 的测试维护路径得到改进:错误 owner 造成的阻断可被同一 oracle 区分,正常分支和原保护保留;user_experience 为 preserved,生产用户流程没有增加步骤或权限要求。

有界整理已应用在作者新头:复用原 fixture/owner,删除临时输出及 driver 强改,不需要新框架或人为后续任务。已检查测试消费者和现有更新入口;没有渲染页面、布局或产品控件变化,视觉评审不适用,视觉未验证范围为 none。最强未验证项是原生 Windows IO;正式合并仍走既有维护者权限与独立门禁,本评审没有执行安装或合并。

English verdict: APPROVE — 6473a13. Correct installation-owner fixtures replace the temporary probe. Sixteen source-context comparisons retain ordinary behavior and turn four Windows-owner refusals into passes; 62 local tests pass, with 3 native Windows cases unverified. Production update and console-safety behavior are unchanged.

@xiaoan17

Copy link
Copy Markdown
Contributor Author

同一 lane 上剩下的两个失败(不在本 PR 范围内)

修掉上面 4 个 self-update 用例后,windows-powershell 上还剩两个间歇失败。它们与本 PR 无关(diff 不碰这两个文件),但同样卡着 merge-gate,所以把已经查到的证据留在��里。

1. test_effect_runtime_integration.py::test_locator_publication_failure_surfaces_safe_typed_startup_diagnostic[live_lock-False-mutation_lock_timeout]

Windows 上得到 runtime_startup_timeout,期望 mutation_lock_timeout。

这是一个启动预算的竞态,不是分类逻辑错了:

  • TypeScript 侧在发布启动 locator 时要取 mutation lock,被一个活着的属主占住时最多等
    MUTATION_LOCK_TIMEOUT_MS = 5_000,然后用
    EffectRuntimeLockTimeoutError → mutation_lock_timeout 写启动信封并退出
    (loopx/control_plane/effect_runtime_io.ts、effect_runtime_errors.ts、
    effect_runtime_server.ts 的 failStartup)。
  • Python 侧的 ready 预算是 STARTUP_READY_TIMEOUT_SECONDS = 15.0
    (loopx/control_plane/runtime/node_probe.py)。这个预算从 node 进程启动之后
    才开始计时(仓库里 test_runtime_ready_budget_starts_after_start_lock_acquisition
    就钉住了这个语义)。
  • 只有当 node 子进程在 15 秒内退出时,Python 才会读 stderr 并报告那条精确
    诊断;否则到点就 terminate() 子进程,只报通用的 runtime_startup_timeout。

所以只要 node 冷启动 + 5s 锁等待 > 15s,精确诊断就被通用超时覆盖。

本地实测(macOS):同一个场景 5.16s 就拿到 mutation_lock_timeout,即 node 冷启动
只占约 0.16s。Windows CI 上 node 需要 --experimental-strip-types 加载运行时
模块,在被占满的 runner 上冷启动超过 10s 就会越过 15s 预算——这与观察到的
runtime_startup_timeout 一致。

这是一个需要决定的问题,不是显然的缺陷:15s 的 ready 预算保护的是"卡死的启动不能
无限期阻塞调用方",而 5s 的 TS 锁预算保护的是"活的属主正在做 mutation 时不能被
抢走"。放宽其中任何一个都要有依据,不适合在本 PR 里顺手改。

2. tests/test_delegation_task_revalidation.py::test_original_task_revalidation_repairs_output_without_repeating_host[sqlite]

Windows 上 runner.resume("analysis-1") 之后读到的是:

{'status': 'turn_returned', 'worker_active': False, 'recovery_required': True}

期望 status == "accepted"。测试自己已经注明 "A Windows MCP host may close its
entire Job on disconnect",并用独立 caller 去调和那个已提交的 Turn——也就是说这条
路径本来就带着 Windows 专属的 Job 语义。这是间歇失败(同一批 Windows run 里有的
出现有的不出现),需要 Windows 上真实复现才能定性。

这两个都不在本 PR 的 diff 范围内,因此这里只记录证据,不在这里修。

@xiaoan17

Copy link
Copy Markdown
Contributor Author

补充:delegation 那个失败的机制(读代码得出,未在 Windows 上复现)

上一条评论只给了现象。这里把机制读清楚了,结论是:执行在"host turn 返回"和"记录终态"之间被中断,而不是校验或分类出错。

读回的记录是 status: 'turn_returned'、worker_active: False、recovery_required: True,而且没有 error 字段。这三点组合起来是唯一的:

  • loopx/collaboration_mcp.py 的 read() 里,recovery_required 的条件是
    「worker 不活跃 + 状态非终态 + 没有 stop receipt + 记录已超过 15 秒」。
    DELEGATION_TERMINAL_STATUSES = {accepted, rejected, stopped}。
  • _execute() 在 host turn 返回后先调 record_turn_result(),把状态推进到
    turn_returned(delegation_validation.py),然后才进入结算与接受那一段。
  • 那之后的每一条出口都至少会留下一个可观察结果:要么 _observe(..., "accepted"),
    要么 _observe(..., "rejected") 并写入 row["error"],要么异常分支写入
    row["error"] = str(exc)[:180]。

也就是说,只要那段代码跑完了,记录要么是终态、要么带 error。观察到的记录两者
都不是——所以那段代码根本没跑到结尾。

这与测试自己写的注释一致:"A Windows MCP host may close its entire Job on
disconnect. Reconcile the original committed Turn through the existing
independent caller.":Windows 上 MCP host 断开时整个 Job 被拆除,连带杀掉还在跑的
delegation worker,于是 worker 在 host turn 刚返回、还没记录结算结果时就没了,
记录停在 turn_returned,读回自然报 recovery_required。

还没确定的部分:具体是哪一个进程被哪个 Job 杀掉。这需要在 Windows 上复现才能
定性。

可能相关的上游工作:#7602
fix(host): contain Windows Host descendants in an owned Job 仍在开放中,改的正是
Windows Host 的 Job 归属(loopx/control_plane/turn_driver/windows_host_job.py、
host_process_transport.py)。它落地后这条路径的 Windows 行为可能就会变,建议在它
之后重新观察这个用例,而不是在这里单独修——这里的失败是 Job 拆除的结果,不是
delegation 接受路径本身的缺陷。

两个用例都不在本 PR 的 diff 范围内,因此这里只记录证据,不在这里修。

@xiaoan17

Copy link
Copy Markdown
Contributor Author

本 PR 被上游 #7675 取代,关闭以避免同一文件上的重复与冲突。

#7675 修的是同一批用例、同一处根因,做法也一致:让每个 driver 使用自己能执行的属主 doctor report。核对结果:

Windows 上的验证证据已交接给 #7675(见那里的评论):一份等价的修正在 Windows runner
上跑出 419 passed, 20 skipped、零失败,其中包含这 4 个用例。#7675 自己的
windows-powershell run 红在另外两个用例上(test_chat_manager_inspection.py 的
TimeoutError: chat turn wait timed out),与这个修复无关,所以那份实测对它是有用的
补充。

本 PR 里另外两条分析也保留在 #7698 的评论里,供后续参考:

  1. tests/control_plane/test_effect_runtime_integration.py::test_locator_publication_failure_surfaces_safe_typed_startup_diagnostic[live_lock-False-mutation_lock_timeout]
    在 Windows 上得到 runtime_startup_timeout 而非 mutation_lock_timeout——这是启动预算
    的竞态(TS 侧 mutation lock 等 5s + node 冷启动 vs Python 侧 15s ready 预算),不是分类
    逻辑错误,需要 owner 决定是否动预算。
  2. tests/test_delegation_task_revalidation.py::test_original_task_revalidation_repairs_output_without_repeating_host[sqlite]
    在 Windows 上读回 turn_returned + recovery_required 且无 error——执行在"host turn
    返回"与"记录终态"之间被中断,与测试自己注明的 "A Windows MCP host may close its entire
    Job on disconnect" 一致。开放中的
    #7602 改的正是 Windows Host 的 Job
    归属,建议在它之后重新观察这个用例,而不是单独修。

@xiaoan17

Copy link
Copy Markdown
Contributor Author

被上游 #7675 取代,关闭。Windows 上的实测证据已交接给 #7675。

@xiaoan17 xiaoan17 closed this Oct 30, 2026
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.

2 participants