H1 worker.py 92%->94% (277->210 miss): 35 real-assertion tests covering goal-signal, tenant/env, followup, poll/wait, process/watchdog and injected defensive branches; prod code 0 changes. H2 sub-90 sweep: approval_timeout 100%, merge_gate_hard 100%, worker_sweep 98% (only __main__ line 174 left); 14 tests. H3 G7 rollback except:pass (server.py:6615-6616) covered by an injected rollback-raising connection; original error still propagates, status stays completed, retry succeeds; no semantics change. H4 caller contract documented only (docstrings in _audit_scorecard endpoint, check_baseline_stale, build_scorecard, baseline store): current_main_head caller-supplied, any string as-is, no format checks, missing/empty treated as not passed with no stale determination at the endpoint, always 200, overall never rewritten; no new validation or rejection, no response-structure change. 3 doc tests pin the wording. H5 design notes (no decisions, no code changes) live in verification/code-to10-gapfill2-design-notes.md, referenced by the batch summary. No bug found by the new tests; no existing assertion touched.
164 lines
6.0 KiB
Python
164 lines
6.0 KiB
Python
"""gapfill2 H3:_merge_task 成功审计段里 rollback 自身的 except: pass。
|
||
|
||
上一批唯一未覆盖分支:G7 新段里 ``conn.rollback()`` 本身抛错时的
|
||
防御分支(基底行号 server.py:6615-6616)。rollback 从不失败,故
|
||
全量覆盖率里该分支一直缺失。本文件只用注入式测试覆盖它,绝不
|
||
改动 rollback 语义、绝不把该分支改成可观测行为:
|
||
|
||
- 注入一个 rollback 会抛错的连接代理(除 rollback 外全部透传
|
||
真实连接),同时让审计失败(沿用 G7 的 RuntimeError 注入),
|
||
使执行流必然走进 ``except Exception: pass``;
|
||
- 断言:rollback 的异常被吞掉(原审计错误原样上抛,不被掩盖)、
|
||
任务状态仍是 completed、审计 0 行、原样重试后成功 200。
|
||
|
||
真实模型 0 轮,不联网;不跑真实 HTTP(handler 直调 +
|
||
真实 AuthStore/DB,审计行直接查库验证)。
|
||
"""
|
||
|
||
import os
|
||
import sqlite3
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
import unittest
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
from unittest import mock
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(REPO_ROOT))
|
||
|
||
import cloud.server as server_mod # noqa: E402
|
||
from cloud.auth import AuthStore # noqa: E402
|
||
from cloud.server import CloudControlPlaneHandler, init_db # noqa: E402
|
||
|
||
NO_BLOCK = {"merge_blocked": False, "release_blocked": False,
|
||
"blocked_by": None, "round": None, "failed_checks": []}
|
||
|
||
HARD_BODY = {
|
||
"files_changed": ["apps/web/a.py"],
|
||
"owned_dir": "apps/web",
|
||
"branch_protection": "protected",
|
||
"ci_status": "success",
|
||
"ci_checks": [{"name": "ci", "status": "success"}],
|
||
"required_checks": ["ci"],
|
||
"contract_version": "v2",
|
||
"contract_frozen": True,
|
||
"coordinator_approved": True,
|
||
"unresolved_review_comments": 0,
|
||
}
|
||
|
||
|
||
class _RollbackFailingConn:
|
||
"""代理真实连接:除 rollback 外全部透传,rollback 计数后抛错。
|
||
|
||
execute / commit / close / row_factory 等一律委托给真实连接,
|
||
只有 rollback 被替换为“计数 + 抛错”,精确模拟“rollback 自身
|
||
失败”这一防御分支的前提,不改动其它任何连接语义。
|
||
"""
|
||
|
||
def __init__(self, real, calls):
|
||
self._real = real
|
||
self._calls = calls
|
||
|
||
def rollback(self):
|
||
self._calls["n"] += 1
|
||
raise RuntimeError("rollback down")
|
||
|
||
def __getattr__(self, name):
|
||
return getattr(self._real, name)
|
||
|
||
|
||
class RollbackFailureSwallowedTestCase(unittest.TestCase):
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.db_path = os.path.join(self.tmp.name, "control.db")
|
||
init_db(self.db_path)
|
||
self.auth = AuthStore(self.db_path)
|
||
self.owner_id, _key = self.auth.create_user("owner")
|
||
conn = sqlite3.connect(self.db_path)
|
||
try:
|
||
conn.execute(
|
||
"INSERT INTO projects(id, name, owner_id, created_at,"
|
||
" repo_url, auto_pr) VALUES(?, ?, ?, ?, ?, 0)",
|
||
("p1", "P", self.owner_id, time.time(), None))
|
||
conn.execute(
|
||
"INSERT INTO tasks(id, project_id, prompt, status,"
|
||
" created_by, created_at, updated_at)"
|
||
" VALUES(?, ?, ?, ?, ?, ?, ?)",
|
||
("t1", "p1", "do work", "completed", self.owner_id,
|
||
time.time(), time.time()))
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
self.auth.create_project("p1", self.owner_id)
|
||
|
||
def tearDown(self):
|
||
try:
|
||
self.auth.conn.close()
|
||
except Exception:
|
||
pass
|
||
self.tmp.cleanup()
|
||
|
||
def _handler(self):
|
||
h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler)
|
||
h.server = SimpleNamespace(db_path=self.db_path)
|
||
sent = []
|
||
h._send_json = lambda status, obj, extra_headers=None: sent.append(
|
||
(status, obj))
|
||
h._read_json = lambda: (dict(HARD_BODY), None)
|
||
h.sent = sent
|
||
return h
|
||
|
||
def _merge(self, h):
|
||
with mock.patch.object(server_mod, "_pushback_block",
|
||
return_value=dict(NO_BLOCK)):
|
||
h._merge_task(self.owner_id, "t1")
|
||
|
||
def _status(self):
|
||
conn = sqlite3.connect(self.db_path)
|
||
try:
|
||
return conn.execute(
|
||
"SELECT status FROM tasks WHERE id = 't1'").fetchone()[0]
|
||
finally:
|
||
conn.close()
|
||
|
||
def _merge_audits(self):
|
||
return [a for a in self.auth.list_audit("p1")
|
||
if a["action"] == "merge_task"]
|
||
|
||
def test_rollback_failure_is_swallowed(self):
|
||
# 审计失败 + rollback 自身失败:rollback 的异常必须被吞掉,
|
||
# 原审计错误原样上抛(不被 rollback 错误掩盖),状态回滚
|
||
# 语义不变(completed、审计 0 行),原样重试后成功。
|
||
h = self._handler()
|
||
real_db = CloudControlPlaneHandler._db
|
||
calls = {"n": 0}
|
||
|
||
def failing_db(handler_self):
|
||
return _RollbackFailingConn(real_db(handler_self), calls)
|
||
|
||
with mock.patch.object(CloudControlPlaneHandler, "_db",
|
||
failing_db):
|
||
with mock.patch.object(
|
||
server_mod, "_audit_on_conn",
|
||
side_effect=RuntimeError("audit down")):
|
||
with self.assertRaises(RuntimeError) as ctx:
|
||
self._merge(h)
|
||
self.assertEqual(str(ctx.exception), "audit down")
|
||
self.assertEqual(calls["n"], 1)
|
||
self.assertEqual(h.sent, [])
|
||
self.assertEqual(self._status(), "completed")
|
||
self.assertEqual(self._merge_audits(), [])
|
||
# 原样重试(审计恢复、连接恢复后)必须成功:状态仍是
|
||
# completed,不是卡在 merged 导致重复合并 409。
|
||
h2 = self._handler()
|
||
self._merge(h2)
|
||
self.assertEqual(h2.sent[-1][0], 200)
|
||
self.assertEqual(self._status(), "merged")
|
||
self.assertEqual(len(self._merge_audits()), 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|