Skip to content

test_scan_streaming.py 两处硬编码 E:/code/voyager,导致子进程 ModuleNotFoundError #20

Closed

Description

@SunnyRad

问题

tests/test_scan_streaming.py 里两个并发用例把仓库根路径写死成 E:/code/voyager:

  • test_two_concurrent_streaming_scans_converge
  • test_concurrent_creation_of_a_fresh_index_does_not_fail

它们各自拼一段子进程代码,里面是 sys.path.insert(0, 'E:/code/voyager'),再用 subprocess.Popen([sys.executable, "-c", child], cwd=str(tmp_path)) 起子进程。由于 cwd 是临时目录、且这套测试的 conftest 会把 APPDATA 重定向到沙箱(子进程的 user-site 指向空目录),子进程里根本找不到 voyager:

ModuleNotFoundError: No module named 'voyager'

在没有 E: 盘(或仓库不在这个精确路径)的机器上,这两条必然失败。我在中文 Windows / cp936 本机对当前 main 跑全量时遇到的 2 个失败就是这两条;加 PYTHONPATH=<repo> 后 4 passed,可确认是路径问题而非并发逻辑问题。

为什么单独提,不塞进 PR #19

这两行在 main 上原本就有,不在 PR #19 的 5 个改动文件内。PR #19 正文与 PR #5 回复都承诺「不扩大范围」,把它并进去会自相矛盾;而且这是个独立的、可单独评审的测试可移植性问题。故另开此 issue 并附补丁,由维护者裁定是否合入。

建议改法

对齐仓库既有写法(tests/test_scan_crash.py:24,109 用 REPO_ROOT = Path(__file__).resolve().parents[1],并给子进程传 env["PYTHONPATH"]):

  • 删掉两处 sys.path.insert(0, %r) 及 % str(Path(r"E:/code/voyager"));
  • 两处 Popen(..., env=env),其中 env = {**os.environ, "PYTHONPATH": str(REPO_ROOT)};
  • 用 env 注入仓库根比 sys.path.insert 稳:conftest 重定向 APPDATA 后子进程 user-site 为空,注入 PYTHONPATH 不受影响。

补丁(仅改 tests/test_scan_streaming.py):

--- a/tests/test_scan_streaming.py
+++ b/tests/test_scan_streaming.py
@@ -36,6 +36,7 @@
 from voyager.store import Store
 
 FIXTURES = Path(__file__).parent / "fixtures"
+REPO_ROOT = Path(__file__).resolve().parents[1]
 
 
 # ---------------------------------------------------------------------------
@@ -538,6 +539,7 @@
     Concurrent *creation* of a brand-new index is a different, separately
     covered case (test_concurrent_creation_of_a_fresh_index_does_not_fail).
     """
+    import os
     import subprocess
     import sys
     codex_tree = _codex_tree(tmp_path / "sessions")
@@ -554,7 +556,6 @@
     child = (
         "import sys\n"
         "from pathlib import Path\n"
-        "sys.path.insert(0, %r)\n"
         "from voyager.store import Store\n"
         "from voyager.cli import run_scan\n"
         "import voyager.adapters.codex as m\n"
@@ -566,15 +567,16 @@
         "run_scan(s, providers=['codex'], quiet=True)\n"
         "s.close()\n"
         "print('CHILD_OK')\n"
-    ) % (str(Path(r"E:/code/voyager")), str(codex_tree),
+    ) % (str(codex_tree),
          str(codex_tree.parent / "_absent_history.db"),
          str(codex_tree.parent / "_absent_state.db"),
          str(codex_tree.parent / "_absent_dev.db"),
          str(index))
 
+    env = {**os.environ, "PYTHONPATH": str(REPO_ROOT)}
     procs = [subprocess.Popen([sys.executable, "-c", child],
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE,
-                              text=True, cwd=str(tmp_path))
+                              text=True, cwd=str(tmp_path), env=env)
              for _ in range(2)]
     for p in procs:
         _out, err = p.communicate(timeout=180)
@@ -622,6 +624,7 @@
     read), so the only thing wrong is that a concurrent setup is not tolerated.
     This test drives the real ``Store`` from two real processes.
     """
+    import os
     import subprocess
     import sys
     from voyager.store import Store
@@ -629,20 +632,20 @@
     CHILD = (
         "import sys\n"
         "from pathlib import Path\n"
-        "sys.path.insert(0, %r)\n"
         "from voyager.store import Store\n"
         "s = Store(Path(sys.argv[1]))\n"
         "s.close()\n"
         "print('OK')\n"
-    ) % str(Path(r"E:/code/voyager"))
-
+    )
+
+    env = {**os.environ, "PYTHONPATH": str(REPO_ROOT)}
     # Several trials: the failure is a race, so a single run proves nothing
     # either way.  Ten pairs is enough to catch a ~1-in-6 rate.
     for trial in range(10):
         db = tmp_path / ("fresh%d.db" % trial)
         procs = [subprocess.Popen([sys.executable, "-c", CHILD, str(db)],
                                   stdout=subprocess.PIPE, stderr=subprocess.PIPE,
-                                  text=True, cwd=str(tmp_path))
+                                  text=True, cwd=str(tmp_path), env=env)
                  for _ in range(2)]
         failures = []
         for p in procs:

本机验证

检查 结果
py_compile 0
2 个目标用例(补丁后,不继承 PYTHONPATH、无 E: 盘) 2 passed
变异:去掉两处 env=env、恢复等价硬编码条件 2 failed,复原后 2 passed
全量(含补丁) 1377 passed, 7 skipped, 1 failed

全量那 1 个失败是 test_verification_harness_v2.py::test_a_successful_run_records_evidence(期望 LIVE_VERIFIED、实得 ZERO_TOUCH_LIVE_VERIFIED),单独跑 1 passed,属顺序依赖的验证态泄漏,与本补丁无关(补丁只碰 test_scan_streaming.py)。

环境:中文 Windows / cp936、仓库以 zip 检出(未 pip install),因此未生成 voyager.egg-info,另有 2 个版本用例会报 PackageNotFoundError——同属环境问题,不在此补丁范围内。

来源:PR #19 的本地全量回归副产物。补丁与验证脚本均未推入任何分支,仅作为本 issue 的附件供评审。

Activity

  1. HarryHeYu commented on Nov 11, 2026

    @HarryHeYu
    Owner

    已按建议改法修复,提交 b64e964(在 PR #19 合并之后单独提交,没有塞进贡献者的 PR)。

    • 两处 sys.path.insert(0, 'E:/code/voyager') 移除;
    • 模块级 REPO_ROOT = Path(__file__).resolve().parents[1],与 tests/test_scan_crash.py:24 一致;
    • 两个子进程都通过 env={\"PYTHONPATH\": str(REPO_ROOT)} 继承检出根目录,不再依赖任何绝对路径或 user-site 安装。

    验证:在不设置 ambient PYTHONPATH 的环境里跑 tests/test_scan_streaming.py,15 passed(含那两个并发用例)。CHANGELOG 的 [Unreleased] / Tests 也记了这条。

    感谢把复现、归因和补丁一起写清楚,尤其是明确指出「加 PYTHONPATH=<repo> 后 2 passed,所以不是并发逻辑缺陷」——这个对照省掉了重新定位的时间。这个 issue 先关掉;如果换机器后仍复现,随时重开。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions