From 3521b1d906aaff92b656f238ac5551cd31cf148e Mon Sep 17 00:00:00 2001 From: slyxyllt Date: Thu, 3 Sep 2026 04:17:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(verify-package):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20PR=20#35=20=E5=BC=95=E5=85=A5=E7=9A=84=20verify=5Fpackage=5F?= =?UTF-8?q?contract=20=E4=B8=A4=E4=B8=AA=E9=A2=84=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit probe_desktop() 的 TOCTOU 竞态:Popen 后第一次 poll() 落在进程真正退出前 的窗口内返回 None,立即 exit 0 的桩被误判为 'alive after launch'(pass), 负向用例期望 exit 1 却得 0(desktop immediate exit: expected exit 1, got 0)。 改为轮询等满整个 launch_timeout 窗口:窗口内任一时刻退出(含 exit 0)判 fail,活满窗口才判 pass,消除竞态(该 flake 概率性出现,曾本地 5/5 复现)。 verify_package_test.sh darwin app bundle 用例的 seed 复制嵌套:mkdir -p 已 创建 seed 目标目录后 cp -R src dst 会把源复制成 dst/seed(seed/seed/...), 与 assets/seed 比对报 seed bundle unexpected。改为 cp -R src/. dst/ 填充。 两处均为 PR #35(feat/verify-package-skill)合入 master 后引入的失败,已 在 master 基线(8823313c)本地复现;本提交在纯 master 基础上修复并连跑 verify_package_test.sh 5/5 通过。 --- .../verify-package/scripts/verify_package.py | 38 +++++++++++-------- .../verify-package/scripts/verify_package.py | 38 +++++++++++-------- .../verify-package/scripts/verify_package.py | 38 +++++++++++-------- .../verify-package/scripts/verify_package.py | 38 +++++++++++-------- tests/scripts/verify_package_test.sh | 7 +++- 5 files changed, 97 insertions(+), 62 deletions(-) diff --git a/.acecode/skills/verify-package/scripts/verify_package.py b/.acecode/skills/verify-package/scripts/verify_package.py index 0aefc17c..b018a920 100644 --- a/.acecode/skills/verify-package/scripts/verify_package.py +++ b/.acecode/skills/verify-package/scripts/verify_package.py @@ -376,23 +376,31 @@ def probe_desktop(report: Report, staged: Path, platform: str, shutil.rmtree(home, ignore_errors=True) return deadline = time.monotonic() + launch_timeout - while time.monotonic() < deadline: - if process.poll() is None: - report.add("desktop launch", "pass", - f"alive after launch (pid {process.pid}); terminating") - process.terminate() - try: - process.wait(timeout=15) - except subprocess.TimeoutExpired: - process.kill() - shutil.rmtree(home, ignore_errors=True) - return + while time.monotonic() < deadline and process.poll() is None: time.sleep(0.25) - if process.poll() == 0: - report.add("desktop launch", "fail", "exited immediately with code 0") + if process.poll() is None: + # Process survived the entire launch window: treat startup as + # successful, then stop it. + report.add("desktop launch", "pass", + f"alive after launch (pid {process.pid}); terminating") + process.terminate() + try: + process.wait(timeout=15) + except subprocess.TimeoutExpired: + process.kill() else: - report.add("desktop launch", "fail", - f"exited with code {process.returncode} during startup") + # Process exited at any point inside the window, including exit code 0. + # Verdicts must wait out the window instead of passing on the first + # poll() is None: right after spawn an immediately-exiting process + # (e.g. an `exit 0` stub) can still report poll() is None and would be + # wrongly marked "alive" (TOCTOU race). Waiting the full window makes + # the immediate-exit detection deterministic. + code = process.returncode + if code == 0: + report.add("desktop launch", "fail", "exited immediately with code 0") + else: + report.add("desktop launch", "fail", + f"exited with code {code} during startup") shutil.rmtree(home, ignore_errors=True) diff --git a/.agents/skills/verify-package/scripts/verify_package.py b/.agents/skills/verify-package/scripts/verify_package.py index 0aefc17c..b018a920 100644 --- a/.agents/skills/verify-package/scripts/verify_package.py +++ b/.agents/skills/verify-package/scripts/verify_package.py @@ -376,23 +376,31 @@ def probe_desktop(report: Report, staged: Path, platform: str, shutil.rmtree(home, ignore_errors=True) return deadline = time.monotonic() + launch_timeout - while time.monotonic() < deadline: - if process.poll() is None: - report.add("desktop launch", "pass", - f"alive after launch (pid {process.pid}); terminating") - process.terminate() - try: - process.wait(timeout=15) - except subprocess.TimeoutExpired: - process.kill() - shutil.rmtree(home, ignore_errors=True) - return + while time.monotonic() < deadline and process.poll() is None: time.sleep(0.25) - if process.poll() == 0: - report.add("desktop launch", "fail", "exited immediately with code 0") + if process.poll() is None: + # Process survived the entire launch window: treat startup as + # successful, then stop it. + report.add("desktop launch", "pass", + f"alive after launch (pid {process.pid}); terminating") + process.terminate() + try: + process.wait(timeout=15) + except subprocess.TimeoutExpired: + process.kill() else: - report.add("desktop launch", "fail", - f"exited with code {process.returncode} during startup") + # Process exited at any point inside the window, including exit code 0. + # Verdicts must wait out the window instead of passing on the first + # poll() is None: right after spawn an immediately-exiting process + # (e.g. an `exit 0` stub) can still report poll() is None and would be + # wrongly marked "alive" (TOCTOU race). Waiting the full window makes + # the immediate-exit detection deterministic. + code = process.returncode + if code == 0: + report.add("desktop launch", "fail", "exited immediately with code 0") + else: + report.add("desktop launch", "fail", + f"exited with code {code} during startup") shutil.rmtree(home, ignore_errors=True) diff --git a/.claude/skills/verify-package/scripts/verify_package.py b/.claude/skills/verify-package/scripts/verify_package.py index 0aefc17c..b018a920 100644 --- a/.claude/skills/verify-package/scripts/verify_package.py +++ b/.claude/skills/verify-package/scripts/verify_package.py @@ -376,23 +376,31 @@ def probe_desktop(report: Report, staged: Path, platform: str, shutil.rmtree(home, ignore_errors=True) return deadline = time.monotonic() + launch_timeout - while time.monotonic() < deadline: - if process.poll() is None: - report.add("desktop launch", "pass", - f"alive after launch (pid {process.pid}); terminating") - process.terminate() - try: - process.wait(timeout=15) - except subprocess.TimeoutExpired: - process.kill() - shutil.rmtree(home, ignore_errors=True) - return + while time.monotonic() < deadline and process.poll() is None: time.sleep(0.25) - if process.poll() == 0: - report.add("desktop launch", "fail", "exited immediately with code 0") + if process.poll() is None: + # Process survived the entire launch window: treat startup as + # successful, then stop it. + report.add("desktop launch", "pass", + f"alive after launch (pid {process.pid}); terminating") + process.terminate() + try: + process.wait(timeout=15) + except subprocess.TimeoutExpired: + process.kill() else: - report.add("desktop launch", "fail", - f"exited with code {process.returncode} during startup") + # Process exited at any point inside the window, including exit code 0. + # Verdicts must wait out the window instead of passing on the first + # poll() is None: right after spawn an immediately-exiting process + # (e.g. an `exit 0` stub) can still report poll() is None and would be + # wrongly marked "alive" (TOCTOU race). Waiting the full window makes + # the immediate-exit detection deterministic. + code = process.returncode + if code == 0: + report.add("desktop launch", "fail", "exited immediately with code 0") + else: + report.add("desktop launch", "fail", + f"exited with code {code} during startup") shutil.rmtree(home, ignore_errors=True) diff --git a/.codex/skills/verify-package/scripts/verify_package.py b/.codex/skills/verify-package/scripts/verify_package.py index 0aefc17c..b018a920 100644 --- a/.codex/skills/verify-package/scripts/verify_package.py +++ b/.codex/skills/verify-package/scripts/verify_package.py @@ -376,23 +376,31 @@ def probe_desktop(report: Report, staged: Path, platform: str, shutil.rmtree(home, ignore_errors=True) return deadline = time.monotonic() + launch_timeout - while time.monotonic() < deadline: - if process.poll() is None: - report.add("desktop launch", "pass", - f"alive after launch (pid {process.pid}); terminating") - process.terminate() - try: - process.wait(timeout=15) - except subprocess.TimeoutExpired: - process.kill() - shutil.rmtree(home, ignore_errors=True) - return + while time.monotonic() < deadline and process.poll() is None: time.sleep(0.25) - if process.poll() == 0: - report.add("desktop launch", "fail", "exited immediately with code 0") + if process.poll() is None: + # Process survived the entire launch window: treat startup as + # successful, then stop it. + report.add("desktop launch", "pass", + f"alive after launch (pid {process.pid}); terminating") + process.terminate() + try: + process.wait(timeout=15) + except subprocess.TimeoutExpired: + process.kill() else: - report.add("desktop launch", "fail", - f"exited with code {process.returncode} during startup") + # Process exited at any point inside the window, including exit code 0. + # Verdicts must wait out the window instead of passing on the first + # poll() is None: right after spawn an immediately-exiting process + # (e.g. an `exit 0` stub) can still report poll() is None and would be + # wrongly marked "alive" (TOCTOU race). Waiting the full window makes + # the immediate-exit detection deterministic. + code = process.returncode + if code == 0: + report.add("desktop launch", "fail", "exited immediately with code 0") + else: + report.add("desktop launch", "fail", + f"exited with code {code} during startup") shutil.rmtree(home, ignore_errors=True) diff --git a/tests/scripts/verify_package_test.sh b/tests/scripts/verify_package_test.sh index 7e9298b6..b9d876cc 100644 --- a/tests/scripts/verify_package_test.sh +++ b/tests/scripts/verify_package_test.sh @@ -197,8 +197,11 @@ EOF chmod +x "$fixture/build/ACECode.app/Contents/MacOS/acecode-daemon" cp "$fixture/assets/models_dev/"* \ "$fixture/build/ACECode.app/Contents/Resources/share/acecode/models_dev/" - cp -R "$fixture/assets/seed" \ - "$fixture/build/ACECode.app/Contents/Resources/share/acecode/seed" + # The destination already exists (created by mkdir -p above), so plain + # `cp -R src dst` nests the tree as dst/seed instead of filling dst, which + # makes the packaged seed layout mismatch assets/seed. Copy the contents. + cp -R "$fixture/assets/seed/." \ + "$fixture/build/ACECode.app/Contents/Resources/share/acecode/seed/" expect_status 0 "darwin bundle flow" "$python_bin" "$verify_script" \ --skip-build --platform darwin --target desktop \ --repo "$fixture" --build-dir "$fixture/build" \ From bbe7320231d23444665c2796f2ecf96884fc8b7b Mon Sep 17 00:00:00 2001 From: slyxyllt Date: Thu, 3 Sep 2026 04:30:15 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(submodule):=20=E5=90=8C=E6=AD=A5=20exte?= =?UTF-8?q?rnal/ftxui=20=E8=87=B3=20f98588b4=EF=BC=8C=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?master=20=E6=9E=84=E5=BB=BA=EF=BC=88EnableMouseHoverMotion?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit master 经 #36 合入 0b97e49b(TUI 超链接增强 5.1-5.3)后,src/main.cpp 调用了 ftxui::App::EnableMouseHoverMotion / EnableSynchronizedOutput 等新 API,但 external/ftxui 的 gitlink 仍停留在 658c942c——该版本 ftxui 无 EnableMouseHoverMotion 成员,导致 linux-x64 CI 在 Build CLI and test binary 步骤失败(error: class ftxui::App has no member named EnableMouseHoverMotion), master 处于构建红态(Merge #36 run 33653685659 同一步失败,为 pre-existing)。 前移至 f98588b4(FTXUI PR #2 hover motion,已确认合入 shaohaozhi286/FTXUI main,任意机器 clone 可及;与 #37 分支 ae67486c 指向一致)。 --- external/ftxui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/ftxui b/external/ftxui index 658c942c..f98588b4 160000 --- a/external/ftxui +++ b/external/ftxui @@ -1 +1 @@ -Subproject commit 658c942c6eaceae88fd7ea4458ffa1f5a7775af7 +Subproject commit f98588b47b097674724100c7a35a3c9b8ec055e3