part 6 修复:worktree 根移出 /app——RETEST5 挂载点回归
RETEST5:part 5 的 --bind=<workspace>/.chv-worktrees:/app/work (adc7bf6,排在 --bind-ro=/app 之后)让所有 cell 启动失败: 后挂的 bind 只有在挂载点已存在时才能盖住,挂载点不存在时 nspawn 在只读 /app 下连建都建不起来(Failed to create mount point .../app/work: Read-only file system,全量 73 次, CELL-RC=0 共 0 次),缺陷 1 的 15/15 回退为 0/15。 三选一取 (a) worktree 根移出 /app: - chv-cell 不再发任何指向 /app 之下的 bind,删 --bind=...:/app/work 与 worktrees_dir 参数;WORKTREE_DIR (/home/chv/workspace/.chv-worktrees)改经 --setenv=CHV_WORKTREE_ROOT 传进单元——该路径在已可写绑进 单元的 workspace 之下,挂载点问题按构造即不存在;backing 仍在客机 workspace 下随磁盘持久化,隔离与落盘语义不变, 只是单元内路径从 /app/work/host-worktrees 改为 /home/chv/workspace/.chv-worktrees/host-worktrees。 - host.py 新增 worktree_base():CHV_WORKTREE_ROOT 优先; CHV_CELL=1 但变量未设时回落到同一单元内路径(旧版 chv-cell / 单元内手动跑 host.py 仍不出 /app);单元外沿用 HERE/work/host-worktrees,行为不变。make_worktree 与 dispatch_from_branch(第二个 HERE/work 调用方)同走该函数, goal/fork/依赖链一并修到。 - 不选 (b) 默认 --no-worktree:隔离丢了,且 goal/fork 同样 调 make_worktree,照样撞 Errno 30;不选 (c) 镜像预建空 目录 /app/work:修法耦合镜像构建,某次重建漏建即静默回归, 且单测无法钉住镜像内容。 - 根因 1(39084ce)与根因 3(0203f32)代码未动。 单测:删 part 5 钉 argv 顺序的 4 项(该顺序本身已被真机证伪), 新增 CellPart6WorktreeTest 4 项 + HostWorktreeBaseTest 5 项: argv 里无任何指向 /app 之下的 bind、CHV_WORKTREE_ROOT 经 setenv 进单元、worktree 根只经可写 workspace 绑定可达、 两处同值常量一致、worktree_base 三路选择、make_worktree 真 git worktree 落点、dispatch_from_branch 同根。 全量:cloud/ 下 discover -s tests,2067 项 OK(skipped=9)。 (注:仓库根跑 discover 会把 queue 解析成标准库、致 test_queue_sandbox 导入失败,须在 cloud/ 下跑。)
This commit is contained in:
1 parent
0203f32627
commit
50cae2a0af
3 files changed
+249
-55
No files matched your search
+43
-30
@@ -33,10 +33,14 @@ volatile 空根(part 4):--volatile=yes 下单元根是基本空的
|
|||||||
tmpfs,/app、/etc/resolv.conf、/etc/hosts、/etc/ssl/certs 都须
|
tmpfs,/app、/etc/resolv.conf、/etc/hosts、/etc/ssl/certs 都须
|
||||||
显式绑进去(/app 与 CA 只读绑;resolv/hosts 是 run_cell 按客机
|
显式绑进去(/app 与 CA 只读绑;resolv/hosts 是 run_cell 按客机
|
||||||
有效 DNS 生成的单元专属文件,只读绑;不绑 nsswitch.conf,
|
有效 DNS 生成的单元专属文件,只读绑;不绑 nsswitch.conf,
|
||||||
glibc 默认 files+dns 正是单元需要的)。part 5 补一条:
|
glibc 默认 files+dns 正是单元需要的)。part 6(RETEST5)纠正
|
||||||
/app/work 以可写 bind 盖住(backing 在客机 workspace 下),
|
part 5 的设想:/app/work 以可写 bind 盖住只读 /app 的做法在真机
|
||||||
否则 host.py 的 make_worktree 在只读 /app 下建不了 worktree
|
上不成立——后挂的 bind 只有在挂载点已存在时才能盖住,挂载点
|
||||||
(RETEST4 新发现 2,OSError Errno 30)。
|
不存在时 nspawn 在只读 /app 下连建都建不起来(全量 73 次同一
|
||||||
|
错误,所有 cell 启动失败)。worktree 根改移出 /app:单元内
|
||||||
|
make_worktree 改用 workspace 绑定下已可写的
|
||||||
|
/home/chv/workspace/.chv-worktrees(经 CHV_WORKTREE_ROOT 环境
|
||||||
|
变量传进单元),不再发任何指向 /app 之下的 bind。
|
||||||
|
|
||||||
环境透传:nspawn 不透传调用方环境,本脚本只转发白名单变量(模型
|
环境透传:nspawn 不透传调用方环境,本脚本只转发白名单变量(模型
|
||||||
网关与代理变量)。GITEA_TOKEN 等凭证变量不在白名单里,绝不进单元。
|
网关与代理变量)。GITEA_TOKEN 等凭证变量不在白名单里,绝不进单元。
|
||||||
@@ -73,16 +77,21 @@ CELL_GID = 1000
|
|||||||
WORKSPACE = "/home/chv/workspace"
|
WORKSPACE = "/home/chv/workspace"
|
||||||
CODEX_HOME = "/home/chv/.codex"
|
CODEX_HOME = "/home/chv/.codex"
|
||||||
BROKER_DIR = "/run/chv"
|
BROKER_DIR = "/run/chv"
|
||||||
# part 5:单元内 worktree 的可写落点(RETEST4 新发现 2)。
|
# part 6(RETEST5 新发现):单元内 worktree 根(RETEST4 新发现 2
|
||||||
# host.py 的 make_worktree 把 worktree 建在 HERE/work(单元内
|
# 的旧址是 HERE/work,即单元内 /app/work——/app 是 --bind-ro,
|
||||||
# HERE=/app,而 /app 是 --bind-ro),带 repo 的默认 dispatch
|
# 带 repo 的默认 dispatch 撞 OSError Errno 30;part 5 把该目录以
|
||||||
# 撞 OSError Errno 30。把客机 workspace 下的 .chv-worktrees
|
# 可写 bind 挂到 /app/work 上,真机证明不可行:挂载点不存在时
|
||||||
# 以可写 bind 挂到单元内的 /app/work 上:worktree 的语义、隔离
|
# nspawn 在只读 /app 下连建都建不起来,所有 cell 启动失败)。
|
||||||
# 与落盘位置都不变(仍是 /app/work/host-worktrees/<任务>),
|
# 新址是 workspace 绑定下的 .chv-worktrees:--bind=/home/chv/
|
||||||
# 只是 backing store 落在 bind 出去的 workspace 下、随客机磁盘
|
# workspace 已把整个 workspace 以可写方式挂进单元(目的路径与
|
||||||
# 持久化(跨 cell 的 run/merge 续跑仍找得到 worktree);不碰
|
# 源相同、挂载点即 workspace 自身,挂载时已存在),worktree 根
|
||||||
# /usr 的只读约束。必须排在 --bind-ro=/app 之后:nspawn 按顺序
|
# 落在这里无需任何额外 bind;backing 仍在客机 workspace 下、
|
||||||
# 挂载,后挂的才盖住只读子树下的该子路径。
|
# 随客机磁盘持久化(跨 cell 的 run/merge 续跑仍找得到 worktree),
|
||||||
|
# 隔离与落盘语义不变,只是单元内路径从 /app/work/host-worktrees
|
||||||
|
# 改为 /home/chv/workspace/.chv-worktrees/host-worktrees。
|
||||||
|
# 该值经 --setenv=CHV_WORKTREE_ROOT 传进单元,host.py 的
|
||||||
|
# worktree_base() 读它(与 host.py 的 CELL_WORKTREE_ROOT 同值,
|
||||||
|
# 两处须保持一致)。
|
||||||
WORKTREE_DIR = "/home/chv/workspace/.chv-worktrees"
|
WORKTREE_DIR = "/home/chv/workspace/.chv-worktrees"
|
||||||
CELL_PID_FILE = "/run/chv/cell.pid"
|
CELL_PID_FILE = "/run/chv/cell.pid"
|
||||||
|
|
||||||
@@ -224,17 +233,19 @@ def build_nspawn_argv(command, network="veth",
|
|||||||
workspace=WORKSPACE, codex_home=CODEX_HOME,
|
workspace=WORKSPACE, codex_home=CODEX_HOME,
|
||||||
broker_dir=BROKER_DIR,
|
broker_dir=BROKER_DIR,
|
||||||
playwright_package_dir=None,
|
playwright_package_dir=None,
|
||||||
cell_net=None, extra_ro_binds=None,
|
cell_net=None, extra_ro_binds=None):
|
||||||
worktrees_dir=WORKTREE_DIR):
|
|
||||||
"""产 nspawn 参数表(纯函数,可单测)。command 是单元内要跑的命令
|
"""产 nspawn 参数表(纯函数,可单测)。command 是单元内要跑的命令
|
||||||
(list,如 ["python3", "/app/host.py", ...])。
|
(list,如 ["python3", "/app/host.py", ...])。
|
||||||
cell_net 为 None 时用旧常量身份(argv 子命令与旧测试);
|
cell_net 为 None 时用旧常量身份(argv 子命令与旧测试);
|
||||||
run_cell 一律传 allocate_cell_net() 的唯一身份。
|
run_cell 一律传 allocate_cell_net() 的唯一身份。
|
||||||
extra_ro_binds 是 [(src, dest)],只读绑进单元(如单元专属
|
extra_ro_binds 是 [(src, dest)],只读绑进单元(如单元专属
|
||||||
resolv.conf/hosts;/app 与 /etc/ssl/certs 是固定只读绑定,
|
resolv.conf/hosts;/app 与 /etc/ssl/certs 是固定只读绑定,
|
||||||
直接列在下面)。worktrees_dir 是客机侧 worktree backing
|
直接列在下面)。worktree 根不经 bind:WORKTREE_DIR 在已可写
|
||||||
目录(part 5:以可写 bind 挂到单元内 /app/work 上,见
|
绑进单元的 workspace 之下(part 6,RETEST5),经
|
||||||
WORKTREE_DIR;排在 --bind-ro=/app 之后才能盖住只读子树)。"""
|
--setenv=CHV_WORKTREE_ROOT 传进单元供 host.py 的
|
||||||
|
worktree_base() 用;argv 里绝不出现指向 /app 之下(只读父
|
||||||
|
目录)的 bind——挂载点不存在时 nspawn 在只读父目录下建不
|
||||||
|
了挂载点,单元直接起不来。"""
|
||||||
if not command:
|
if not command:
|
||||||
raise ValueError("chv-cell: 单元内命令不能为空")
|
raise ValueError("chv-cell: 单元内命令不能为空")
|
||||||
if network not in ("veth", "shared"):
|
if network not in ("veth", "shared"):
|
||||||
@@ -286,14 +297,11 @@ def build_nspawn_argv(command, network="veth",
|
|||||||
# 单元内无 resolved/dbus,绑了反而可能挂起;glibc 缺
|
# 单元内无 resolved/dbus,绑了反而可能挂起;glibc 缺
|
||||||
# nsswitch.conf 时默认 files+dns,正是单元需要的。
|
# nsswitch.conf 时默认 files+dns,正是单元需要的。
|
||||||
"--bind-ro=/app",
|
"--bind-ro=/app",
|
||||||
# part 5(RETEST4 新发现 2):/app/work 以可写 bind 盖住
|
# part 6(RETEST5):此处绝不再加 --bind=...:/app/work。
|
||||||
# 只读 /app 下的该子路径——host.py 的 make_worktree 在
|
# part 5 的该绑定已证不可行:/app/work 挂载点在 --bind-ro
|
||||||
# /app/work/host-worktrees 下建目录,绑了才写得进;必须在
|
# 的 /app 镜像里不存在,nspawn 在只读父目录下建不了挂
|
||||||
# --bind-ro=/app 之后(nspawn 按顺序挂载),且必须是
|
# 载点(EROFS),单元启动即失败。worktree 根已移出 /app,
|
||||||
# --bind(可写),不是 --bind-ro。backing 目录在客机
|
# 见 WORKTREE_DIR 与 --setenv=CHV_WORKTREE_ROOT。
|
||||||
# workspace 下(run_cell 起单元前 _ensure_bind_source
|
|
||||||
# 建好),worktree 随客机磁盘持久化。
|
|
||||||
"--bind=%s:/app/work" % worktrees_dir,
|
|
||||||
"--bind-ro=/etc/ssl/certs",
|
"--bind-ro=/etc/ssl/certs",
|
||||||
"--tmpfs=/tmp",
|
"--tmpfs=/tmp",
|
||||||
]
|
]
|
||||||
@@ -303,6 +311,10 @@ def build_nspawn_argv(command, network="veth",
|
|||||||
argv.append("--network-veth")
|
argv.append("--network-veth")
|
||||||
argv += [
|
argv += [
|
||||||
"--setenv=CHV_CELL=1",
|
"--setenv=CHV_CELL=1",
|
||||||
|
# part 6:单元内 worktree 根(host.py worktree_base() 读
|
||||||
|
# 它;值与 WORKTREE_DIR 一致,客机侧与单元内是同一路径,
|
||||||
|
# 因 workspace 以同名可写 bind 进单元)。
|
||||||
|
"--setenv=CHV_WORKTREE_ROOT=%s" % WORKTREE_DIR,
|
||||||
"--setenv=HOME=/home/chv",
|
"--setenv=HOME=/home/chv",
|
||||||
]
|
]
|
||||||
for name in FORWARD_ENV_ALLOWLIST:
|
for name in FORWARD_ENV_ALLOWLIST:
|
||||||
@@ -683,9 +695,10 @@ def run_cell(command, network="veth"):
|
|||||||
_ensure_bind_source(WORKSPACE)
|
_ensure_bind_source(WORKSPACE)
|
||||||
_ensure_bind_source(CODEX_HOME)
|
_ensure_bind_source(CODEX_HOME)
|
||||||
_ensure_bind_source(BROKER_DIR, uid=0, gid=0)
|
_ensure_bind_source(BROKER_DIR, uid=0, gid=0)
|
||||||
# part 5:worktree backing 目录须在 Popen 前存在(nspawn
|
# part 6:worktree 根目录须在 Popen 前存在,且属 chv(单元内
|
||||||
# --bind 的源不存在即起不来),且属 chv(单元内 root 即客机
|
# root 即客机 chv,属主不对就写不进 worktree)。它在 workspace
|
||||||
# chv,属主不对就写不进 worktree)。
|
# 绑定之下,进单元后 host.py 的 makedirs 也能建,这里预建只
|
||||||
|
# 是把属主一次定对;不再是任何 bind 的源(无 /app/work 绑定)。
|
||||||
_ensure_bind_source(WORKTREE_DIR)
|
_ensure_bind_source(WORKTREE_DIR)
|
||||||
net = None
|
net = None
|
||||||
files = None
|
files = None
|
||||||
|
|||||||
+168
-23
@@ -36,6 +36,20 @@ BROWSER = _load_script("chv-browser.py")
|
|||||||
BUILDINFO = _load_script("buildinfo.py")
|
BUILDINFO = _load_script("buildinfo.py")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_host():
|
||||||
|
"""把仓库根的 host.py 当模块加载(只含定义,__main__ 才跑
|
||||||
|
CLI,导入无副作用),供 worktree 根选择逻辑单测。"""
|
||||||
|
path = ROOT / "host.py"
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"chv_host_py", str(path))
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
HOST = _load_host()
|
||||||
|
|
||||||
|
|
||||||
class CellArgvTest(unittest.TestCase):
|
class CellArgvTest(unittest.TestCase):
|
||||||
def test_private_users_maps_cell_root_to_chv(self):
|
def test_private_users_maps_cell_root_to_chv(self):
|
||||||
argv = CELL.build_nspawn_argv(["true"])
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
@@ -854,37 +868,168 @@ class CellPart4ArgvTest(unittest.TestCase):
|
|||||||
self.assertEqual(net.subnet, "10.200.0.0/24")
|
self.assertEqual(net.subnet, "10.200.0.0/24")
|
||||||
|
|
||||||
|
|
||||||
class CellPart5WorktreeTest(unittest.TestCase):
|
class CellPart6WorktreeTest(unittest.TestCase):
|
||||||
"""part 5 修复 2(RETEST4 新发现 2):/app/work 以可写 bind
|
"""part 6 修复(RETEST5 新发现):worktree 根移出 /app。
|
||||||
盖住只读 /app——默认 dispatch 的 make_worktree 才写得进。"""
|
|
||||||
|
|
||||||
def test_worktree_bind_is_writable_under_workspace(self):
|
part 5 的旧测试钉住的是 argv 顺序(/app/work 绑定排在
|
||||||
|
--bind-ro=/app 之后),RETEST5 真机证明该顺序本身不可行:
|
||||||
|
后挂的 bind 只有在挂载点已存在时才能盖住,挂载点不存在时
|
||||||
|
nspawn 在只读 /app 下连建都建不起来(全量 73 次同一错误,
|
||||||
|
CELL-RC=0 共 0 次)。本轮单测不再钉顺序,改钉语义:argv 里
|
||||||
|
不得出现任何指向只读 /app 之下的 bind(挂载点不存在时不发
|
||||||
|
致命 bind),worktree 根改经 CHV_WORKTREE_ROOT 环境变量传
|
||||||
|
进单元,落点在已可写绑进单元的 workspace 之下。"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _bind_dests(argv):
|
||||||
|
"""argv 里全部 bind/bind-ro 的单元内目的路径。"""
|
||||||
|
dests = []
|
||||||
|
for token in argv:
|
||||||
|
for prefix in ("--bind=", "--bind-ro="):
|
||||||
|
if token.startswith(prefix):
|
||||||
|
spec = token[len(prefix):]
|
||||||
|
dests.append(spec.split(":", 1)[1]
|
||||||
|
if ":" in spec else spec)
|
||||||
|
return dests
|
||||||
|
|
||||||
|
def test_no_bind_targets_anything_under_readonly_app(self):
|
||||||
argv = CELL.build_nspawn_argv(["true"])
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
self.assertIn(
|
# RETEST5 的致命绑定本身必须消失。
|
||||||
|
self.assertNotIn(
|
||||||
"--bind=/home/chv/workspace/.chv-worktrees:/app/work",
|
"--bind=/home/chv/workspace/.chv-worktrees:/app/work",
|
||||||
argv)
|
argv)
|
||||||
# 必须是可写 bind,不是只读绑。
|
# 更一般的语义:任何 bind 的目的路径都不得落在只读的
|
||||||
self.assertNotIn(
|
# /app 之下(/app 本身是 --bind-ro 的挂载点,允许;其
|
||||||
"--bind-ro=/home/chv/workspace/.chv-worktrees:/app/work",
|
# 子路径挂载点在镜像里不存在,nspawn 建不了即 EROFS)。
|
||||||
|
for dest in self._bind_dests(argv):
|
||||||
|
self.assertFalse(
|
||||||
|
dest.startswith("/app/"),
|
||||||
|
"bind 目的路径在只读 /app 之下:%s" % dest)
|
||||||
|
|
||||||
|
def test_worktree_root_passed_via_setenv(self):
|
||||||
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
|
self.assertIn(
|
||||||
|
"--setenv=CHV_WORKTREE_ROOT="
|
||||||
|
"/home/chv/workspace/.chv-worktrees",
|
||||||
argv)
|
argv)
|
||||||
|
|
||||||
def test_worktree_bind_stacks_after_readonly_app(self):
|
def test_worktree_dir_reachable_via_writable_workspace_bind(self):
|
||||||
# nspawn 按顺序挂载:work bind 必须在 --bind-ro=/app 之后,
|
# worktree 根在 workspace 之下,且 workspace 本身已以
|
||||||
# 才能盖住只读子树下的 /app/work。
|
# 可写 bind(非 --bind-ro)进单元——根无需额外 bind,
|
||||||
argv = CELL.build_nspawn_argv(["true"])
|
# 挂载点问题按构造即不存在。
|
||||||
self.assertLess(
|
|
||||||
argv.index("--bind-ro=/app"),
|
|
||||||
argv.index(
|
|
||||||
"--bind=/home/chv/workspace/.chv-worktrees:/app/work"))
|
|
||||||
|
|
||||||
def test_worktree_bind_custom_dir(self):
|
|
||||||
argv = CELL.build_nspawn_argv(["true"],
|
|
||||||
worktrees_dir="/tmp/wt")
|
|
||||||
self.assertIn("--bind=/tmp/wt:/app/work", argv)
|
|
||||||
|
|
||||||
def test_worktree_dir_constant_under_workspace(self):
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
CELL.WORKTREE_DIR.startswith(CELL.WORKSPACE + "/"))
|
CELL.WORKTREE_DIR.startswith(CELL.WORKSPACE + "/"))
|
||||||
|
argv = CELL.build_nspawn_argv(["true"])
|
||||||
|
self.assertIn("--bind=%s" % CELL.WORKSPACE, argv)
|
||||||
|
self.assertNotIn("--bind-ro=%s" % CELL.WORKSPACE, argv)
|
||||||
|
# .chv-worktrees 只应出现在 --setenv 里,不应出现在
|
||||||
|
# 任何 bind/bind-ro 参数里。
|
||||||
|
for token in argv:
|
||||||
|
if ".chv-worktrees" in token:
|
||||||
|
self.assertTrue(
|
||||||
|
token.startswith("--setenv=CHV_WORKTREE_ROOT="),
|
||||||
|
"worktree 目录不应再经 bind 进单元:%s" % token)
|
||||||
|
|
||||||
|
def test_worktree_dir_matches_host_cell_fallback(self):
|
||||||
|
# chv-cell 的 WORKTREE_DIR 与 host.py 在 CHV_CELL=1 且
|
||||||
|
# 环境变量未设时的回落值须是同一路径,否则两边各说各话。
|
||||||
|
self.assertEqual(CELL.WORKTREE_DIR, HOST.CELL_WORKTREE_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
class HostWorktreeBaseTest(unittest.TestCase):
|
||||||
|
"""part 6:host.py 的 worktree 根选择(worktree_base)与
|
||||||
|
make_worktree 落点——单元内永不在 /app 之下,单元外不变。"""
|
||||||
|
|
||||||
|
def test_env_override_wins_over_cell_fallback(self):
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"CHV_WORKTREE_ROOT": "/tmp/wt-root",
|
||||||
|
"CHV_CELL": "1"},
|
||||||
|
clear=True):
|
||||||
|
self.assertEqual(
|
||||||
|
HOST.worktree_base(), "/tmp/wt-root/host-worktrees")
|
||||||
|
|
||||||
|
def test_cell_without_env_falls_back_to_workspace(self):
|
||||||
|
with mock.patch.dict(os.environ, {"CHV_CELL": "1"},
|
||||||
|
clear=True):
|
||||||
|
base = HOST.worktree_base()
|
||||||
|
self.assertEqual(
|
||||||
|
base,
|
||||||
|
"/home/chv/workspace/.chv-worktrees/host-worktrees")
|
||||||
|
self.assertFalse(base.startswith("/app/"))
|
||||||
|
|
||||||
|
def test_outside_cell_uses_here_work(self):
|
||||||
|
with mock.patch.dict(os.environ, {}, clear=True):
|
||||||
|
self.assertEqual(
|
||||||
|
HOST.worktree_base(),
|
||||||
|
os.path.join(HOST.HERE, "work", "host-worktrees"))
|
||||||
|
|
||||||
|
def _make_git_repo(self, tmp):
|
||||||
|
repo = os.path.join(tmp, "repo")
|
||||||
|
os.makedirs(repo)
|
||||||
|
for args in (["init", "-q", repo],
|
||||||
|
["-C", repo, "config", "user.email",
|
||||||
|
"test@example.invalid"],
|
||||||
|
["-C", repo, "config", "user.name", "test"]):
|
||||||
|
subprocess.run(["git"] + args, check=True,
|
||||||
|
capture_output=True)
|
||||||
|
with open(os.path.join(repo, "README.md"), "w") as fh:
|
||||||
|
fh.write("hi\n")
|
||||||
|
subprocess.run(["git", "-C", repo, "add", "."],
|
||||||
|
check=True, capture_output=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "-C", repo, "commit", "-qm", "init"],
|
||||||
|
check=True, capture_output=True)
|
||||||
|
return repo
|
||||||
|
|
||||||
|
def test_make_worktree_uses_override_not_app(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
repo = self._make_git_repo(tmp)
|
||||||
|
wt_root = os.path.join(tmp, "wt")
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"CHV_WORKTREE_ROOT": wt_root, "CHV_CELL": ""},
|
||||||
|
clear=False):
|
||||||
|
path, branch = HOST.make_worktree(repo, "t1")
|
||||||
|
self.assertEqual(
|
||||||
|
path, os.path.join(wt_root, "host-worktrees", "t1"))
|
||||||
|
self.assertEqual(branch, "host/t1")
|
||||||
|
self.assertFalse(path.startswith("/app/"))
|
||||||
|
# 真是 git worktree,不是空目录拷贝。
|
||||||
|
proc = subprocess.run(
|
||||||
|
["git", "-C", path, "rev-parse", "--show-toplevel"],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
self.assertEqual(proc.returncode, 0)
|
||||||
|
self.assertEqual(proc.stdout.strip(), path)
|
||||||
|
|
||||||
|
def test_dispatch_from_branch_uses_same_base(self):
|
||||||
|
# 第二个 HERE/work 调用方(依赖派发)须与 make_worktree
|
||||||
|
# 走同一根,否则 goal/fork 的依赖链仍撞只读 /app。
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
repo = self._make_git_repo(tmp)
|
||||||
|
wt_root = os.path.join(tmp, "wt")
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"CHV_WORKTREE_ROOT": wt_root, "CHV_CELL": ""},
|
||||||
|
clear=False):
|
||||||
|
dep_path, _ = HOST.make_worktree(repo, "dep")
|
||||||
|
created = {}
|
||||||
|
|
||||||
|
class _FakeServer:
|
||||||
|
def create_thread(self, path, **kwargs):
|
||||||
|
created["path"] = path
|
||||||
|
return "th-1"
|
||||||
|
|
||||||
|
state = {"tasks": {}}
|
||||||
|
task, err = HOST.dispatch_from_branch(
|
||||||
|
_FakeServer(), state, "child", "prompt", repo,
|
||||||
|
"host/dep", dep_path, "m", "low",
|
||||||
|
"danger-full-access", "ask")
|
||||||
|
self.assertEqual(err, "")
|
||||||
|
self.assertEqual(
|
||||||
|
task["cwd"],
|
||||||
|
os.path.join(wt_root, "host-worktrees", "child"))
|
||||||
|
self.assertFalse(task["cwd"].startswith("/app/"))
|
||||||
|
|
||||||
|
|
||||||
class CellFilesTest(unittest.TestCase):
|
class CellFilesTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -61,6 +61,40 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
DEFAULT_STATE = os.path.join(HERE, "host-state.json")
|
DEFAULT_STATE = os.path.join(HERE, "host-state.json")
|
||||||
|
|
||||||
|
# part 6(RETEST5 新发现):单元内 worktree 根,绝不在 /app 之下。
|
||||||
|
# 单元内 HERE=/app,而 /app 是 chv-cell 的 --bind-ro;part 5 曾
|
||||||
|
# 把客机 workspace 下的 .chv-worktrees 以可写 bind 挂到
|
||||||
|
# /app/work 上(argv 里排在 --bind-ro=/app 之后,指望后挂盖住
|
||||||
|
# 只读子树),RETEST5 真机证明该设想不成立:后挂的 bind 只有在
|
||||||
|
# 挂载点已存在时才能盖住,挂载点不存在时 nspawn 在只读 /app 下
|
||||||
|
# 连建都建不起来(`Failed to create mount point .../app/work:
|
||||||
|
# Read-only file system`,全量 73 次,CELL-RC=0 共 0 次),
|
||||||
|
# 所有 cell 启动失败。修法三选一取 (a) worktree 根移出 /app:
|
||||||
|
# 单元内改用 workspace 绑定下已可写的
|
||||||
|
# /home/chv/workspace/.chv-worktrees(该路径经 --bind 整个
|
||||||
|
# workspace 以可写方式进单元,无需任何额外 bind,挂载点问题
|
||||||
|
# 按构造即不存在);(b) 默认 --no-worktree 不取:隔离丢了,
|
||||||
|
# 且 goal/fork 同样调 make_worktree,照样撞 Errno 30;(c) 镜像
|
||||||
|
# 里预建空目录 /app/work 不取:修法耦合镜像构建,某次重建漏
|
||||||
|
# 建即静默回归,且单测无法钉住镜像内容。
|
||||||
|
# CHV_WORKTREE_ROOT 由 chv-cell 经 --setenv 传进来(与
|
||||||
|
# chv-cell.py 的 WORKTREE_DIR 同值);CHV_CELL=1 但变量未设
|
||||||
|
# 时(旧版 chv-cell / 单元内手动跑 host.py)回落到同一单元内
|
||||||
|
# 路径,单元内 worktree 根永不在 /app 之下;单元外沿用
|
||||||
|
# HERE/work/host-worktrees,行为不变。
|
||||||
|
CHV_WORKTREE_ROOT_ENV = "CHV_WORKTREE_ROOT"
|
||||||
|
CELL_WORKTREE_ROOT = "/home/chv/workspace/.chv-worktrees"
|
||||||
|
|
||||||
|
|
||||||
|
def worktree_base():
|
||||||
|
"""worktree 根目录(<根>/host-worktrees/<任务>)。"""
|
||||||
|
root = os.environ.get(CHV_WORKTREE_ROOT_ENV)
|
||||||
|
if root:
|
||||||
|
return os.path.join(root, "host-worktrees")
|
||||||
|
if os.environ.get("CHV_CELL") == "1":
|
||||||
|
return os.path.join(CELL_WORKTREE_ROOT, "host-worktrees")
|
||||||
|
return os.path.join(HERE, "work", "host-worktrees")
|
||||||
|
|
||||||
|
|
||||||
def _sum_turn_usage(events, thread_id, turn_id):
|
def _sum_turn_usage(events, thread_id, turn_id):
|
||||||
"""从事件窗口里汇总一回合的上游真实用量。
|
"""从事件窗口里汇总一回合的上游真实用量。
|
||||||
@@ -1242,7 +1276,7 @@ def git(args, cwd):
|
|||||||
def make_worktree(repo, task_name):
|
def make_worktree(repo, task_name):
|
||||||
"""一任务一 worktree;不是 git 仓库时退化为独立目录拷贝。"""
|
"""一任务一 worktree;不是 git 仓库时退化为独立目录拷贝。"""
|
||||||
repo = os.path.abspath(repo)
|
repo = os.path.abspath(repo)
|
||||||
base = os.path.join(HERE, "work", "host-worktrees")
|
base = worktree_base()
|
||||||
os.makedirs(base, exist_ok=True)
|
os.makedirs(base, exist_ok=True)
|
||||||
path = os.path.join(base, task_name)
|
path = os.path.join(base, task_name)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
@@ -1774,7 +1808,9 @@ def dispatch_from_branch(server, state, name, prompt, repo, dep_branch,
|
|||||||
c = git(["commit", "-m", f"host: {name} base from {dep_branch}"],
|
c = git(["commit", "-m", f"host: {name} base from {dep_branch}"],
|
||||||
dep_worktree)
|
dep_worktree)
|
||||||
# nothing to commit 时分支里已有全部改动,继续用它当基底
|
# nothing to commit 时分支里已有全部改动,继续用它当基底
|
||||||
base = os.path.join(HERE, "work", "host-worktrees")
|
# part 6:与 make_worktree 同款,worktree 根经 worktree_base()
|
||||||
|
# 选(单元内在 workspace 下,不在只读 /app 之下)。
|
||||||
|
base = worktree_base()
|
||||||
os.makedirs(base, exist_ok=True)
|
os.makedirs(base, exist_ok=True)
|
||||||
path = os.path.join(base, name)
|
path = os.path.join(base, name)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
|
|||||||
Reference in new issue
Block a user