Files
codex-app-server/deploy/tests/test_deploy.py
host f39ed3654e ci3: lint/mypy 第二阶段清理 + 覆盖率 76% -> 78%(门槛不动)
基底:#251 头 174b631(堆叠于 #242 feat-ci-expand 之上),
基线 origin/main aebc6238;本轨不改 #251 已定语义、不动
workflow 门槛、不删检查、不加 skip。

lint(ruff E,F@120):109 -> 10
- F401 43 -> 0:删未用导入;try/except 双绑定的名字出现
  “打地鼠”(删 except 分支后 try 分支又被标出),两分支
  均已核对正文无引用后清掉;MCP 三模块的 Error 名不删,
  改走 importlib 属性绑定(见下),两路径绑定都保留。
- E702 40 -> 0:全在 host.py main() 的 argparse 段,纯分行,
  tokenize 按分号 token 拆(含 2377 行多行语句只断首个分号);
  一次 5 空格缩进失误已回滚重做并以 ast + 全量 host 测试验证。
- E501 13 -> 0:host.py 5 项随 E702 分行连带清掉;mcp_server
  3 项把描述拆成相邻字面量(AST 比对 24 条工具描述与原值
  完全一致,注意 ruff 按东亚宽度计长,CJK 算 2);test_host
  5 项断开 git commit 参数表。
- E402 8 -> 10:生产侧 2 项清掉(db.py _re、github_app
  _base64 移到顶部,之前无引用、无重名);剩余 10 项全是
  tests/ 的 sys.path 前置 import(6 项旧 + 4 个新测试文件
  沿用同一既定模式),移上去会破坏单文件直跑,有意保留。
- F841 4 -> 0:只去绑定、不删调用(run_task、DB 入队、
  tarfile 上下文、git 提交),各有注释与原测试/新测试覆盖;
  host dispatch_from_branch 新增真实 git 仓库回归测试。
- F811 1 -> 0:server 本地 _row_get 与 peer_messages.row_get
  逐行相同,删本地定义,全部调用点解析到同一 import;
  新增 test_server_row_get(同一对象 + 三路行为)。

mypy(生产范围):24 -> 0,零新增 type: ignore
- no-redef 9 -> 0:MCP 三模块 try/except 两次 from-import
  改 importlib.import_module + 属性绑定(cloud 优先、flat
  copy 回落、内层 ImportError 同样回落,语义与原来一致;
  flat-copy 子进程测试证实回落后 __module__ 为顶层名);
  server _row_get 随 F811 消除。
- arg-type 10 -> 0:harness2_tasks shared 改 TypedDict
  逐键精确类型;CodexProbeAdapter 补 no-op validate_tree
  (_adapter_validate 本就跳过缺失方法,行为一致)。
- var-annotated 3 -> 0:sub2api _SHARED_ADMIN/_login_slots、
  verify-multiuser RESULTS 加精确注解。
- assignment/misc 2 -> 0:marker_tools ZoneInfo 先声明
  Any 再 try import(草稿先验 mypy 通过)。

bandit:总数 286 不变,High 仍 4 项,全在 host.py,本轨不改
(属第 3 项白名单轨范围),只做 triage:
- 1319 run_verify:无插值,整串验测命令,管道/&& 须 shell
  语义(新增 test_host_run_verify 断言管道与 && 真实生效)。
- 1497 notify:模板来自算子配置,三处插值全部 shlex.quote,
  证据为既有 test_notify_failure_quotes_shell_metacharacters
  (; touch 与 $(touch ) 不执行)。
- 2099/2103 goal verify:无插值,任务配置验测串,同 run_verify。
- 无新增 High;pip-audit 0 漏洞。

覆盖率(同口径三套件 coverage 计量):75.96% -> 77.87%
(22392 行/缺 5385 -> 22437 行/缺 4963,显示 76% -> 78%)
- client.py 0% -> 90%(pipe 假子进程 + main 分支)
- run_parallel.py 0% -> 97%、migrate 包装 0% -> 95%
- playwright_mcp 37% -> 62%、imagegen_mcp 47% -> 79%、
  websearch_mcp 51% -> 86%、deploy.py 56% -> 77%
- verify-multiuser 0% -> 27%(仅 check/req helper;main()
  是主机侧真实多用户实弹验证,按门禁不建生产任务、
  不以 mock 冒充,仍为缺口)
- host.py 55% -> 57%(文件 2020 -> 2060 行系 E702 分行
  的计量口径变化,非代码膨胀)
- 未到 85%:门槛保持 75% 不动,不删低覆盖模块出统计口径;
  缺口主力仍是 server.py / worker.py / host.py 大文件,
  下一阶段须针对性深测,非本轨快赢可及。

验证:三套件 1576 -> 1629 项全过(tests 127、cloud/tests
1449 skipped=4、deploy/tests 53);ruff E9 通过;mypy 0;
bandit High 4(同上);pip-audit 0 漏洞;试跑产物已删,
提交后工作树干净。
2026-11-03 02:48:54 +00:00

283 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""deploy.py 的单测:只测纯逻辑与发布包结构,不碰真实主机。
主机侧 host_deploy.sh 的仿真见 test_host_deploy.py(假 systemctl/curl)。
"""
import hashlib
import os
import subprocess
import sys
import tarfile
import tempfile
import unittest
from unittest import mock
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(REPO_ROOT, "deploy"))
import deploy as deploy_mod # noqa: E402
class TestDeployPure(unittest.TestCase):
def test_release_tarball_name_is_commit_scoped(self):
name = deploy_mod.release_tarball_name("abc123")
self.assertEqual(name, "codex-app-server-abc123.tar.gz")
# 绝不再用通用名:不同 commit 绝不可能撞到同一个远端路径。
self.assertNotEqual(
deploy_mod.release_tarball_name("abc123"),
deploy_mod.release_tarball_name("def456"))
def test_parse_release_ok_requires_matching_commit(self):
out = "部署成功\nRELEASE_OK commit=abc sha=dead backup=/tmp/x.tar.gz\n"
self.assertTrue(deploy_mod.parse_release_ok(out, "abc"))
self.assertFalse(deploy_mod.parse_release_ok(out, "other"))
self.assertFalse(deploy_mod.parse_release_ok("部署成功,health 通过\n", "abc"))
def test_sha256_of(self):
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(b"hello")
path = f.name
try:
self.assertEqual(deploy_mod.sha256_of(path),
hashlib.sha256(b"hello").hexdigest())
finally:
os.unlink(path)
def test_tarball_commit_missing_manifest_refuses(self):
with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as f:
path = f.name
try:
# 空 tarball 文件本身是测试前置(tarball_commit 应拒绝):上下文照跑,只去 as 绑定。
with tarfile.open(path, "w:gz"):
pass
with self.assertRaises(SystemExit):
deploy_mod.tarball_commit(path)
finally:
os.unlink(path)
def test_remote_sha_ignores_ssh_warning_lines(self):
# 2026-10-27 新链路首推实测:pty helper 把 ssh 的 Warning 行
# 合并进 stdout 放在 sha 行前面,旧 split()[0] 取到 Warning:
# 导致传输明明成功却被判验真失败、两路都“失败”。
sha = "5d0645b85a65b2d58d0d5812f9e8020bdafe15d60dc440e763cfcc7cd178148b"
fake = subprocess.CompletedProcess(
args=[], returncode=0,
stdout="Warning: Permanently added 'x' (ED25519) to the list of known hosts.\n"
+ sha + " /tmp/x.tar.gz\n", stderr="")
with mock.patch.object(deploy_mod.subprocess, "run",
return_value=fake):
self.assertEqual(deploy_mod.remote_sha("h", "u", "/tmp/x"), sha)
def test_remote_sha_none_without_hash(self):
fake = subprocess.CompletedProcess(args=[], returncode=0,
stdout="Warning: something\n",
stderr="")
with mock.patch.object(deploy_mod.subprocess, "run",
return_value=fake):
self.assertIsNone(deploy_mod.remote_sha("h", "u", "/tmp/x"))
def test_live_commit_ignores_ssh_warning_lines(self):
commit = "32125f7a980e24fd28664a60701d7c1123beb63e"
out = "Warning: Permanently added 'x' (ED25519) to the list of known hosts.\n" \
+ commit + "\n"
self.assertEqual(deploy_mod.live_commit(out), commit)
self.assertIsNone(deploy_mod.live_commit("Warning: something\n"))
class TestGitModeHelpers(unittest.TestCase):
"""--source git 的纯逻辑门禁:完整 sha、SSH 形式、origin/main
归属。主机只能拉到已推的提交,未推的 HEAD 必须在 VM 侧拦住。"""
def test_is_full_sha(self):
self.assertTrue(deploy_mod.is_full_sha("a" * 40))
self.assertFalse(deploy_mod.is_full_sha("a" * 7))
self.assertFalse(deploy_mod.is_full_sha("g" * 40))
self.assertFalse(deploy_mod.is_full_sha(""))
self.assertFalse(deploy_mod.is_full_sha(None))
def test_is_ssh_git_url(self):
self.assertTrue(deploy_mod.is_ssh_git_url(
"git@github.com:openqj/codex-app-server.git"))
self.assertTrue(deploy_mod.is_ssh_git_url(
"ssh://git@github.com/openqj/codex-app-server.git"))
self.assertFalse(deploy_mod.is_ssh_git_url(
"https://github.com/openqj/codex-app-server.git"))
self.assertFalse(deploy_mod.is_ssh_git_url(""))
def test_commit_on_origin_main_uses_merge_base(self):
calls = []
def fake_run(cmd, cwd=None, check=True):
calls.append(cmd)
return subprocess.CompletedProcess(args=cmd, returncode=0,
stdout="", stderr="")
with mock.patch.object(deploy_mod, "run", side_effect=fake_run):
self.assertTrue(deploy_mod.commit_on_origin_main("a" * 40))
self.assertIn("merge-base", calls[0])
self.assertIn("origin/main", calls[0])
def test_transfer_falls_back_to_chunk_key(self):
"""scp 失败后走 chunk_key 分片(密钥版),不依赖 SSH_PASSWORD;
验远端 sha 通过即成功,不应退出。"""
calls = []
def fake_run(cmd, cwd=None, check=True, **kwargs):
calls.append(cmd)
if cmd[0] == "scp":
return subprocess.CompletedProcess(args=cmd, returncode=1,
stdout="", stderr="reset")
return subprocess.CompletedProcess(args=cmd, returncode=0,
stdout="", stderr="")
def fake_helper(name):
return "/helpers/chunk_send_key_explicit.py" \
if name == "chunk_send_key_explicit.py" else None
with mock.patch.object(deploy_mod.subprocess, "run",
side_effect=fake_run):
with mock.patch.object(deploy_mod, "helper_path",
side_effect=fake_helper):
with mock.patch.object(deploy_mod, "ssh_cmd",
return_value=["ssh", "dummy"]):
with mock.patch.object(
deploy_mod, "remote_sha",
return_value="deadbeef"):
deploy_mod.transfer("h", "u", "/tmp/a",
"/tmp/b", "deadbeef")
self.assertTrue(any("chunk_send_key_explicit.py" in x
for c in calls for x in c))
class TestDefaultTestSuites(unittest.TestCase):
"""P2-ops 回归:部署前默认清单必须含 deploy/tests,
且 run_tests() 确实按 discover -s 逐套跑(不是包路径)。"""
def test_default_suites_include_deploy_tests(self):
self.assertIn("deploy/tests", deploy_mod.DEFAULT_TEST_SUITES)
self.assertIn("tests", deploy_mod.DEFAULT_TEST_SUITES)
self.assertIn("cloud/tests", deploy_mod.DEFAULT_TEST_SUITES)
def test_run_tests_discovers_every_suite(self):
calls = []
def fake_run(cmd, cwd=None, check=True):
calls.append(cmd)
return subprocess.CompletedProcess(args=cmd, returncode=0,
stdout="", stderr="")
with mock.patch.object(deploy_mod, "run", side_effect=fake_run):
deploy_mod.run_tests()
discovered = [c[c.index("-s") + 1] for c in calls
if "discover" in c]
self.assertEqual(discovered, list(deploy_mod.DEFAULT_TEST_SUITES))
# 没有任何一套走包路径(deploy.tests 会盖掉 deploy.py)。
for c in calls:
self.assertNotIn("deploy.tests", c)
class TestBuildRelease(unittest.TestCase):
def test_build_from_temp_git_repo(self):
with tempfile.TemporaryDirectory() as tmp:
repo = os.path.join(tmp, "repo")
os.makedirs(repo)
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
subprocess.run(["git", "config", "user.email", "t@t"],
cwd=repo, check=True)
subprocess.run(["git", "config", "user.name", "t"],
cwd=repo, check=True)
with open(os.path.join(repo, "hello.py"), "w") as f:
f.write("x = 1\n")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(["git", "commit", "-qm", "init"],
cwd=repo, check=True)
commit = subprocess.run(
["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True,
text=True, check=True).stdout.strip()
# 把 deploy 模块的 REPO_ROOT 临时指到这个小仓库。
old = deploy_mod.REPO_ROOT
deploy_mod.REPO_ROOT = repo
try:
out_dir = os.path.join(tmp, "out")
tarball, sha, got = deploy_mod.build_release("HEAD", out_dir)
finally:
deploy_mod.REPO_ROOT = old
self.assertEqual(got, commit)
self.assertEqual(deploy_mod.sha256_of(tarball), sha)
self.assertEqual(deploy_mod.tarball_commit(tarball), commit)
with tarfile.open(tarball, "r:gz") as tf:
names = tf.getnames()
self.assertIn("DEPLOYED_COMMIT", names)
self.assertIn("RELEASE.json", names)
self.assertIn("hello.py", names)
class TestSshScpCmd(unittest.TestCase):
"""ssh_cmd/scp_cmd 的三路分发:有密码走 helper、无密码走
密钥直连、无 helper 无代理时直连。2026-10-27 密钥补丁曾只
放在 env-console 工作区未提交、切分支后丢失,本批把它收进
main;测试钉住分发逻辑,不依赖真实主机与真实密钥文件。"""
def setUp(self):
self._env = dict(os.environ)
os.environ.pop("SSH_PASSWORD", None)
os.environ.pop("SSH_PROXY_CMD", None)
os.environ.pop("CODEX_DEPLOY_KEY", None)
def tearDown(self):
os.environ.clear()
os.environ.update(self._env)
def _cmds(self, helper=None, proxy_file=False, key_file=False):
with mock.patch.object(deploy_mod, "helper_path",
return_value=helper):
with mock.patch.object(deploy_mod.os.path, "isfile",
return_value=(proxy_file or key_file)):
# isfile 对 proxy 与 key 都返回同一值:测试只断言
# 分支形状(是否出现 ProxyCommand/-i),不区分两者。
ssh = deploy_mod.ssh_cmd("h", "u", "true")
scp = deploy_mod.scp_cmd("h", "u", "a", "b")
return ssh, scp
def test_password_uses_helper_even_without_proxy(self):
os.environ["SSH_PASSWORD"] = "x"
ssh, scp = self._cmds(helper="/helpers/ssh_run.py")
self.assertEqual(ssh[:4],
["python3", "/helpers/ssh_run.py", "ssh", "h"])
self.assertEqual(scp[:4],
["python3", "/helpers/ssh_run.py", "scp", "h"])
def test_no_password_never_uses_helper(self):
# 无密码时 helper(pty 喂密码)不可用:即使 helper 文件
# 存在也不走它,否则会在密码提示处卡住。
ssh, scp = self._cmds(helper="/helpers/ssh_run.py",
proxy_file=True, key_file=True)
self.assertNotIn("/helpers/ssh_run.py", ssh)
self.assertNotIn("/helpers/ssh_run.py", scp)
# ProxyCommand 与参数拼在一个元素里,查前缀而不是整元素。
self.assertTrue(any(x.startswith("ProxyCommand") for x in ssh))
self.assertIn("-i", ssh)
self.assertIn("-i", scp)
def test_no_password_no_helpers_goes_direct(self):
ssh, scp = self._cmds(helper=None,
proxy_file=False, key_file=False)
self.assertEqual(ssh[0], "ssh")
self.assertNotIn("ProxyCommand", ssh)
self.assertNotIn("-i", ssh)
self.assertEqual(scp[0], "scp")
def test_explicit_proxy_cmd_wins(self):
os.environ["SSH_PROXY_CMD"] = "proxy-cmd %h %p"
ssh, _ = self._cmds(helper=None, proxy_file=False)
self.assertIn("ProxyCommand proxy-cmd %h %p", ssh)
def test_explicit_key_env_wins(self):
os.environ["CODEX_DEPLOY_KEY"] = "/keys/d"
ssh, _ = self._cmds(helper=None, proxy_file=False,
key_file=False)
self.assertIn("/keys/d", ssh)
if __name__ == "__main__":
unittest.main()