test: code-to10 v2 coverage additions (14 files, production code unchanged)
(cherry picked from commit 473338c35d59bfd50929542e7f08c0a4a22c5987)
This commit is contained in:
1 parent
935b530f84
commit
6e0660e97b
14 files changed
+8249
No files matched your search
@@ -0,0 +1,150 @@
|
||||
"""Coverage batch v2 (E2): cloud/marker_tools.py pure helpers --
|
||||
timezone fallback, first-run parsing, int coercion, spawn-default
|
||||
parsing, heartbeat/progress formatting, todo/task lines.
|
||||
Safety buffer on top of E1; all direct calls.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from cloud.marker_tools import (
|
||||
_as_int,
|
||||
_heartbeat_age_text,
|
||||
_model_from_match,
|
||||
_progress_lines,
|
||||
_shanghai_tz,
|
||||
format_snapshot,
|
||||
format_task,
|
||||
format_todo,
|
||||
parse_first_run_at,
|
||||
parse_spawn_defaults,
|
||||
row_get,
|
||||
)
|
||||
|
||||
|
||||
class FakeMatch:
|
||||
def __init__(self, token):
|
||||
self._token = token
|
||||
|
||||
def group(self, n):
|
||||
return self._token
|
||||
|
||||
def end(self, n):
|
||||
return len(self._token)
|
||||
|
||||
|
||||
class MarkerPureTestCase(unittest.TestCase):
|
||||
def test_shanghai_tz_zoneinfo_raises(self):
|
||||
with mock.patch("cloud.marker_tools.ZoneInfo",
|
||||
side_effect=Exception("no tz")):
|
||||
tz = _shanghai_tz()
|
||||
self.assertEqual(tz.utcoffset(None).total_seconds(), 8 * 3600)
|
||||
|
||||
def test_shanghai_tz_no_zoneinfo(self):
|
||||
with mock.patch("cloud.marker_tools.ZoneInfo", None):
|
||||
tz = _shanghai_tz()
|
||||
self.assertEqual(tz.utcoffset(None).total_seconds(), 8 * 3600)
|
||||
|
||||
def test_first_run_blank(self):
|
||||
self.assertIsNone(parse_first_run_at(" "))
|
||||
|
||||
def test_first_run_negative(self):
|
||||
self.assertIsNone(parse_first_run_at(-5))
|
||||
|
||||
def test_first_run_zero(self):
|
||||
self.assertIsNone(parse_first_run_at(0))
|
||||
|
||||
def test_first_run_valid(self):
|
||||
self.assertEqual(parse_first_run_at(1700000000), 1700000000.0)
|
||||
self.assertIsNotNone(parse_first_run_at("2026-01-01T00:00:00Z"))
|
||||
|
||||
def test_row_get_fallback(self):
|
||||
self.assertIsNone(row_get(None, "x"))
|
||||
self.assertIsNone(row_get({}, "missing"))
|
||||
|
||||
def test_as_int_bool_and_float(self):
|
||||
self.assertEqual(_as_int(True, 7), 7)
|
||||
self.assertEqual(_as_int(3.5, 7), 7)
|
||||
self.assertEqual(_as_int(3.0, 7), 3)
|
||||
self.assertEqual(_as_int(4, 7), 4)
|
||||
|
||||
def test_model_from_match_empty_token(self):
|
||||
self.assertEqual(_model_from_match("x", FakeMatch("."), 1),
|
||||
(None, None))
|
||||
|
||||
def test_model_from_match_effort_word(self):
|
||||
self.assertEqual(_model_from_match("x", FakeMatch("max"), 1),
|
||||
(None, "max"))
|
||||
|
||||
def test_spawn_defaults_empty(self):
|
||||
self.assertEqual(parse_spawn_defaults(""), {})
|
||||
self.assertEqual(parse_spawn_defaults(None), {})
|
||||
|
||||
def test_spawn_defaults_zh(self):
|
||||
out = parse_spawn_defaults("子任务用 gpt-6-luna max")
|
||||
self.assertEqual(out.get("model"), "gpt-6-luna")
|
||||
|
||||
def test_heartbeat_none(self):
|
||||
self.assertEqual(_heartbeat_age_text({}), "")
|
||||
|
||||
def test_heartbeat_bad(self):
|
||||
self.assertEqual(
|
||||
_heartbeat_age_text({"last_heartbeat": "bad"}), "")
|
||||
|
||||
def test_heartbeat_future(self):
|
||||
now = time.time()
|
||||
self.assertEqual(
|
||||
_heartbeat_age_text({"last_heartbeat": now + 100}, now=now),
|
||||
"0 秒前")
|
||||
|
||||
def test_heartbeat_seconds(self):
|
||||
now = time.time()
|
||||
self.assertEqual(
|
||||
_heartbeat_age_text({"last_heartbeat": now - 30}, now=now),
|
||||
"30 秒前")
|
||||
|
||||
def test_progress_lines_full(self):
|
||||
now = time.time()
|
||||
lines = _progress_lines(
|
||||
{"executor": "docker", "actual_model": "m1",
|
||||
"worktree_host": "h1", "last_heartbeat": now - 5,
|
||||
"turn_id": "t9"}, now=now)
|
||||
self.assertEqual(len(lines), 5)
|
||||
|
||||
def test_snapshot_terminal_empty(self):
|
||||
text = format_snapshot(
|
||||
{"id": "t1", "status": "completed", "result_text": "",
|
||||
"error_text": ""}, 100)
|
||||
self.assertIn("结果:(空)", text)
|
||||
|
||||
def test_snapshot_timed_out(self):
|
||||
text = format_snapshot(
|
||||
{"id": "t1", "status": "running", "result_text": "",
|
||||
"error_text": ""}, 100, timed_out=True)
|
||||
self.assertIn("等待超时", text)
|
||||
|
||||
def test_snapshot_cancelled(self):
|
||||
text = format_snapshot(
|
||||
{"id": "t1", "status": "running", "result_text": "",
|
||||
"error_text": ""}, 100, cancelled=True)
|
||||
self.assertIn("被取消", text)
|
||||
|
||||
def test_todo_bad_due_and_task(self):
|
||||
line = format_todo({"status": "open", "title": "t", "id": "d1",
|
||||
"priority": "high", "due_at": "bad",
|
||||
"task_id": "t1"})
|
||||
self.assertIn("task_id=t1", line)
|
||||
self.assertNotIn("due=", line)
|
||||
|
||||
def test_task_bad_created(self):
|
||||
line = format_task({"status": "completed", "id": "t1",
|
||||
"created_by": "u1", "prompt": "do it",
|
||||
"created_at": "bad"})
|
||||
self.assertIn("do it", line)
|
||||
self.assertNotIn("created=", line)
|
||||
@@ -0,0 +1,407 @@
|
||||
"""Coverage batch v2: cloud/audit_scorecard.py branch sweep.
|
||||
|
||||
The baseline left 55 lines uncovered, almost all of them the
|
||||
second/third branches of the seven _score_* scorers (caps, unknown
|
||||
returns, untested kinds) plus the threshold-source fallbacks. These
|
||||
tests drive every branch with real evidence dicts and assert the
|
||||
pinned scores, statuses and overall rule (average is never the pass
|
||||
criterion). Threshold values used here are the module's own test
|
||||
defaults; nothing here presents them as production-approved.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from cloud.audit_scorecard import (
|
||||
DEFAULT_THRESHOLDS,
|
||||
build_scorecard,
|
||||
example_scorecard_current_state,
|
||||
_result,
|
||||
_round1,
|
||||
_score_higher,
|
||||
_score_lower,
|
||||
_t,
|
||||
_thresholds_source,
|
||||
)
|
||||
|
||||
|
||||
def _delivery(**over):
|
||||
ev = {
|
||||
"first_pass_rate": 0.97, "defect_rate": 0.01, "rework_rate": 0.02,
|
||||
"first_pass_known": True, "defect_known": True,
|
||||
"rework_known": True, "attempts_present": True,
|
||||
"evidence_kind": "real",
|
||||
}
|
||||
ev.update(over)
|
||||
return ev
|
||||
|
||||
|
||||
def _gate(**over):
|
||||
ev = {
|
||||
"hard_gate_enforced": True, "gate_items_passed": 100,
|
||||
"gate_items_total": 100, "merge_blocked_verified": True,
|
||||
"loop_end_to_end_verified": True,
|
||||
"sampling_misjudgment_rate": 0.0, "evidence_kind": "real",
|
||||
}
|
||||
ev.update(over)
|
||||
return ev
|
||||
|
||||
|
||||
def _arch(**over):
|
||||
ev = {
|
||||
"contract_frozen": True, "arch_violations": 0,
|
||||
"directory_violations": 0, "import_violations": 0,
|
||||
"stack_changed_without_approval": False,
|
||||
}
|
||||
ev.update(over)
|
||||
return ev
|
||||
|
||||
|
||||
def _test(**over):
|
||||
ev = {
|
||||
"prd_coverage_rate": 1.0, "cross_matrix_coverage_rate": 1.0,
|
||||
"coverage_pct": 0.95, "cross_matrix_kind": "real",
|
||||
"real_pg": True, "real_service": True,
|
||||
"agent_six_complete": True, "lint_items": 0, "mypy_items": 0,
|
||||
"bandit_high": 0,
|
||||
}
|
||||
ev.update(over)
|
||||
return ev
|
||||
|
||||
|
||||
def _security(**over):
|
||||
ev = {
|
||||
"ledger_verify_ok": True, "ledger_coverage_rate": 1.0,
|
||||
"shell_whitelist_pass_rate": 1.0, "branch_protection": "protected",
|
||||
"anchor_check_recent": True,
|
||||
}
|
||||
ev.update(over)
|
||||
return ev
|
||||
|
||||
|
||||
def _cost(**over):
|
||||
ev = {
|
||||
"cost_records_complete": True, "unpriced_as_zero": False,
|
||||
"per_creator_present": True, "quota_cutoff_verified": True,
|
||||
}
|
||||
ev.update(over)
|
||||
return ev
|
||||
|
||||
|
||||
def _ops(**over):
|
||||
ev = {
|
||||
"ci_green_rate": 1.0, "deploy_success_rate": 1.0,
|
||||
"rollback_drill_done": True, "ha_status": "second_host",
|
||||
"parallel_scale_proven": 40,
|
||||
}
|
||||
ev.update(over)
|
||||
return ev
|
||||
|
||||
|
||||
def _all_good():
|
||||
return {
|
||||
"delivery_quality": _delivery(),
|
||||
"gate_audit": _gate(),
|
||||
"arch_contract": _arch(),
|
||||
"test_coverage": _test(),
|
||||
"security_ledger": _security(),
|
||||
"cost_quota": _cost(),
|
||||
"ops_scale": _ops(),
|
||||
}
|
||||
|
||||
|
||||
class HelpersTestCase(unittest.TestCase):
|
||||
def test_thresholds_source_branches(self):
|
||||
self.assertEqual(_thresholds_source(None), "test_default_unapproved")
|
||||
self.assertEqual(_thresholds_source({}), "test_default_unapproved")
|
||||
approved = {"a": {"value": 1, "source": "user_approved"}}
|
||||
self.assertEqual(_thresholds_source(approved), "user_approved")
|
||||
# Non-dict entry: falls into the else branch, and a lone
|
||||
# non-approved source falls through to the final default.
|
||||
self.assertEqual(_thresholds_source({"a": 5}), "test_default_unapproved")
|
||||
mixed = {"a": {"value": 1, "source": "user_approved"},
|
||||
"b": {"value": 2, "source": "test_default_unapproved"}}
|
||||
self.assertEqual(_thresholds_source(mixed), "test_default_unapproved")
|
||||
|
||||
def test_t_branches(self):
|
||||
self.assertEqual(_t(None, "coverage_nine"),
|
||||
DEFAULT_THRESHOLDS["coverage_nine"]["value"])
|
||||
self.assertEqual(_t({"coverage_nine": {"value": 0.77}},
|
||||
"coverage_nine"), 0.77)
|
||||
self.assertEqual(_t({"coverage_nine": 0.66}, "coverage_nine"), 0.66)
|
||||
self.assertEqual(_t({"other": 1}, "coverage_nine"),
|
||||
DEFAULT_THRESHOLDS["coverage_nine"]["value"])
|
||||
|
||||
def test_score_higher_segments(self):
|
||||
self.assertIsNone(_score_higher(None, 0.9, 0.95, 0.5))
|
||||
self.assertEqual(_score_higher(1.0, 0.9, 0.95, 0.5), 10.0)
|
||||
self.assertAlmostEqual(_score_higher(0.92, 0.9, 0.95, 0.5), 9.4)
|
||||
self.assertAlmostEqual(_score_higher(0.7, 0.9, 0.95, 0.5), 7.0)
|
||||
self.assertAlmostEqual(_score_higher(0.2, 0.9, 0.95, 0.5), 2.0)
|
||||
self.assertEqual(_score_higher(-1.0, 0.9, 0.95, 0.0), 0.0)
|
||||
# Degenerate bands still return a number instead of dividing
|
||||
# by zero; the floor-band edge (v == floor) scores exactly 5.
|
||||
self.assertEqual(_score_higher(0.9, 0.9, 0.9, 0.5), 10.0)
|
||||
self.assertEqual(_score_higher(0.5, 0.9, 0.95, 0.5), 5.0)
|
||||
|
||||
def test_score_lower_segments(self):
|
||||
self.assertIsNone(_score_lower(None, 0.02, 0.05, 0.20))
|
||||
self.assertEqual(_score_lower(0.01, 0.02, 0.05, 0.20), 10.0)
|
||||
self.assertAlmostEqual(_score_lower(0.03, 0.02, 0.05, 0.20),
|
||||
9.0 + (0.05 - 0.03) / 0.03)
|
||||
self.assertAlmostEqual(_score_lower(0.10, 0.02, 0.05, 0.20),
|
||||
5.0 + 4.0 * (0.20 - 0.10) / 0.15)
|
||||
self.assertAlmostEqual(_score_lower(0.40, 0.02, 0.05, 0.20), 2.5)
|
||||
self.assertEqual(_score_lower(0.02, 0.05, 0.05, 0.20), 10.0)
|
||||
self.assertEqual(_score_lower(0.05, 0.02, 0.05, 0.20), 9.0)
|
||||
|
||||
def test_result_statuses_and_round1(self):
|
||||
self.assertEqual(_result("x", None, [], ["m"], [])["status"], "unknown")
|
||||
self.assertEqual(_result("x", 9.0, [], [], [])["status"], "pass")
|
||||
self.assertEqual(_result("x", 4.0, [], [], [])["status"], "fail")
|
||||
self.assertEqual(_round1(9.04), 9.0)
|
||||
|
||||
|
||||
class DeliveryTestCase(unittest.TestCase):
|
||||
def _score(self, **over):
|
||||
return build_scorecard({"delivery_quality": _delivery(**over)},
|
||||
{})["dimensions"][0]
|
||||
|
||||
def test_happy(self):
|
||||
dim = self._score()
|
||||
self.assertEqual(dim["score"], 10.0)
|
||||
self.assertEqual(dim["status"], "pass")
|
||||
|
||||
def test_missing_and_untested(self):
|
||||
dim = self._score(first_pass_rate=None, defect_known=False,
|
||||
attempts_present=False)
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertIn("first_pass_rate", dim["missing_items"])
|
||||
self.assertIn("defect_known_not_known", dim["missing_items"])
|
||||
self.assertIn("attempts_present_false_or_missing",
|
||||
dim["missing_items"])
|
||||
dim = self._score(evidence_kind="fixture")
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertTrue(any("fixture" in u for u in dim["untested_items"]))
|
||||
dim = self._score(evidence_kind="estimated")
|
||||
self.assertIsNone(dim["score"])
|
||||
|
||||
def test_mid_scores(self):
|
||||
dim = self._score(first_pass_rate=0.7, defect_rate=0.10,
|
||||
rework_rate=0.30)
|
||||
self.assertLess(dim["score"], 9.0)
|
||||
self.assertEqual(dim["status"], "fail")
|
||||
|
||||
|
||||
class GateTestCase(unittest.TestCase):
|
||||
def _score(self, **over):
|
||||
return build_scorecard({"gate_audit": _gate(**over)},
|
||||
{})["dimensions"][1]
|
||||
|
||||
def test_happy(self):
|
||||
self.assertEqual(self._score()["score"], 10.0)
|
||||
|
||||
def test_unknown_and_conditional(self):
|
||||
dim = self._score(hard_gate_enforced=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertIn("hard_gate_enforced_unknown", dim["missing_items"])
|
||||
dim = self._score(gate_items_passed=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
dim = self._score(audit_verdict="conditional")
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertTrue(any("conditional" in u
|
||||
for u in dim["untested_items"]))
|
||||
dim = self._score(evidence_kind="dry_run")
|
||||
self.assertIsNone(dim["score"])
|
||||
|
||||
def test_caps(self):
|
||||
self.assertEqual(self._score(hard_gate_enforced=False)["score"], 4.0)
|
||||
self.assertEqual(self._score(merge_blocked_verified=False)["score"], 4.0)
|
||||
self.assertEqual(self._score(loop_end_to_end_verified=False)["score"], 4.0)
|
||||
|
||||
def test_misjudgment_unknown_and_scored(self):
|
||||
dim = self._score(sampling_misjudgment_rate=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertIn("sampling_misjudgment_rate_unknown",
|
||||
dim["missing_items"])
|
||||
dim = self._score(sampling_misjudgment_rate=0.10)
|
||||
self.assertLess(dim["score"], 10.0)
|
||||
dim = self._score(gate_items_total=0, gate_items_passed=0,
|
||||
sampling_misjudgment_rate=0.0)
|
||||
self.assertEqual(dim["score"], 0.0)
|
||||
|
||||
|
||||
class ArchTestCase(unittest.TestCase):
|
||||
def _score(self, **over):
|
||||
return build_scorecard({"arch_contract": _arch(**over)},
|
||||
{})["dimensions"][2]
|
||||
|
||||
def test_happy_and_unknown(self):
|
||||
self.assertEqual(self._score()["score"], 10.0)
|
||||
dim = self._score(arch_violations=None, contract_frozen=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertIn("arch_violations_unknown", dim["missing_items"])
|
||||
self.assertIn("contract_frozen_unknown", dim["missing_items"])
|
||||
|
||||
def test_caps(self):
|
||||
self.assertEqual(self._score(contract_frozen=False)["score"], 4.0)
|
||||
self.assertEqual(
|
||||
self._score(stack_changed_without_approval=True)["score"], 4.0)
|
||||
self.assertEqual(self._score(arch_violations=2)["score"], 5.0)
|
||||
self.assertEqual(self._score(directory_violations=1)["score"], 5.0)
|
||||
|
||||
|
||||
class TestDimTestCase(unittest.TestCase):
|
||||
def _score(self, **over):
|
||||
return build_scorecard({"test_coverage": _test(**over)},
|
||||
{})["dimensions"][3]
|
||||
|
||||
def test_happy(self):
|
||||
self.assertEqual(self._score()["score"], 10.0)
|
||||
|
||||
def test_unknown_and_untested(self):
|
||||
dim = self._score(prd_coverage_rate=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
dim = self._score(cross_matrix_kind="viewport_substitute",
|
||||
real_pg=True, real_service=True)
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertTrue(any("viewport_substitute" in u
|
||||
for u in dim["untested_items"]))
|
||||
self.assertTrue(any("cross_matrix_not_real" in u
|
||||
for u in dim["untested_items"]))
|
||||
dim = self._score(cross_matrix_kind="code_review_only")
|
||||
self.assertIsNone(dim["score"])
|
||||
dim = self._score(real_pg=False, real_service=True)
|
||||
self.assertIsNone(dim["score"])
|
||||
dim = self._score(real_service=False, real_pg=True)
|
||||
self.assertIsNone(dim["score"])
|
||||
|
||||
def test_agent_six_cap_and_static(self):
|
||||
self.assertEqual(self._score(agent_six_complete=False)["score"], 4.0)
|
||||
dim = self._score(lint_items=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertIn("lint_items_missing", dim["missing_items"])
|
||||
dim = self._score(lint_items=3)
|
||||
self.assertEqual(dim["score"], 8.0)
|
||||
dim = self._score(bandit_high=1)
|
||||
self.assertEqual(dim["score"], 8.0)
|
||||
|
||||
|
||||
class SecurityTestCase(unittest.TestCase):
|
||||
def _score(self, **over):
|
||||
return build_scorecard({"security_ledger": _security(**over)},
|
||||
{})["dimensions"][4]
|
||||
|
||||
def test_happy_and_unknown(self):
|
||||
self.assertEqual(self._score()["score"], 10.0)
|
||||
dim = self._score(ledger_verify_ok=None, branch_protection="unknown")
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertIn("ledger_verify_ok_unknown", dim["missing_items"])
|
||||
self.assertIn("branch_protection_unknown", dim["missing_items"])
|
||||
dim = self._score(ledger_coverage_rate=None, branch_protection=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
|
||||
def test_caps(self):
|
||||
self.assertEqual(self._score(branch_protection="unprotected")["score"], 4.0)
|
||||
self.assertEqual(self._score(ledger_verify_ok=False)["score"], 4.0)
|
||||
self.assertEqual(self._score(anchor_check_recent=False)["score"], 8.0)
|
||||
|
||||
def test_no_whitelist_rate(self):
|
||||
dim = self._score(shell_whitelist_pass_rate=None)
|
||||
self.assertEqual(dim["score"], 10.0)
|
||||
|
||||
|
||||
class CostTestCase(unittest.TestCase):
|
||||
def _score(self, **over):
|
||||
return build_scorecard({"cost_quota": _cost(**over)},
|
||||
{})["dimensions"][5]
|
||||
|
||||
def test_happy_and_unknown(self):
|
||||
self.assertEqual(self._score()["score"], 10.0)
|
||||
dim = self._score(cost_records_complete=False)
|
||||
self.assertIsNone(dim["score"])
|
||||
dim = self._score(unpriced_as_zero=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
|
||||
def test_caps(self):
|
||||
self.assertEqual(self._score(unpriced_as_zero=True)["score"], 0.0)
|
||||
self.assertEqual(self._score(quota_cutoff_verified=False)["score"], 4.0)
|
||||
self.assertEqual(self._score(per_creator_present=False)["score"], 8.0)
|
||||
|
||||
|
||||
class OpsTestCase(unittest.TestCase):
|
||||
def _score(self, **over):
|
||||
return build_scorecard({"ops_scale": _ops(**over)},
|
||||
{})["dimensions"][6]
|
||||
|
||||
def test_happy_and_unknown(self):
|
||||
self.assertEqual(self._score()["score"], 10.0)
|
||||
dim = self._score(ci_green_rate=None, rollback_drill_done=None,
|
||||
ha_status=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertTrue(any(m.startswith("ci_green_rate_unknown")
|
||||
for m in dim["missing_items"]))
|
||||
self.assertIn("rollback_drill_done_unknown", dim["missing_items"])
|
||||
self.assertIn("ha_status_unknown", dim["missing_items"])
|
||||
|
||||
def test_caps_and_scale(self):
|
||||
self.assertEqual(self._score(rollback_drill_done=False)["score"], 4.0)
|
||||
self.assertEqual(
|
||||
self._score(ha_status="single_host_unaccepted")["score"], 5.0)
|
||||
dim = self._score(parallel_scale_proven=None)
|
||||
self.assertIsNone(dim["score"])
|
||||
self.assertIn("parallel_scale_proven_unknown", dim["missing_items"])
|
||||
dim = self._score(parallel_scale_proven=None,
|
||||
parallel_scale_accepted_alternative=True)
|
||||
self.assertEqual(dim["score"], 10.0)
|
||||
dim = self._score(parallel_scale_proven=18)
|
||||
self.assertEqual(dim["score"], 4.5)
|
||||
dim = self._score(deploy_success_rate=None)
|
||||
self.assertEqual(dim["score"], 10.0)
|
||||
dim = self._score(ha_status="single_host_accepted",
|
||||
parallel_scale_proven=40)
|
||||
self.assertEqual(dim["score"], 10.0)
|
||||
|
||||
|
||||
class OverallTestCase(unittest.TestCase):
|
||||
def test_all_pass(self):
|
||||
card = build_scorecard(_all_good())
|
||||
self.assertEqual(card["overall"], "pass")
|
||||
self.assertTrue(card["overall_pass"])
|
||||
self.assertEqual(card["average_score"], 10.0)
|
||||
self.assertTrue(card["average_is_not_pass_criterion"])
|
||||
self.assertEqual(card["thresholds_source"], "test_default_unapproved")
|
||||
self.assertEqual(set(card["thresholds"].keys()),
|
||||
set(DEFAULT_THRESHOLDS.keys()))
|
||||
|
||||
def test_average_is_not_pass_criterion(self):
|
||||
# Six dims at 10, one at 4: average 9.1 but overall not pass.
|
||||
evidence = _all_good()
|
||||
evidence["cost_quota"] = _cost(quota_cutoff_verified=False)
|
||||
card = build_scorecard(evidence)
|
||||
self.assertGreaterEqual(card["average_score"], 9.0)
|
||||
self.assertEqual(card["overall"], "not_pass")
|
||||
self.assertFalse(card["overall_pass"])
|
||||
|
||||
def test_empty_evidence(self):
|
||||
card = build_scorecard({})
|
||||
self.assertEqual(card["overall"], "not_pass")
|
||||
self.assertIsNone(card["average_score"])
|
||||
self.assertTrue(all(d["score"] is None for d in card["dimensions"]))
|
||||
self.assertGreater(len(card["missing_items"]), 0)
|
||||
card_none = build_scorecard(None)
|
||||
self.assertEqual(card_none["overall"], "not_pass")
|
||||
|
||||
def test_user_approved_thresholds_source(self):
|
||||
approved = {k: {"value": v["value"], "source": "user_approved"}
|
||||
for k, v in DEFAULT_THRESHOLDS.items()}
|
||||
card = build_scorecard(_all_good(), approved)
|
||||
self.assertEqual(card["thresholds_source"], "user_approved")
|
||||
|
||||
def test_example_current_state_is_not_pass(self):
|
||||
card = example_scorecard_current_state()
|
||||
self.assertEqual(card["overall"], "not_pass")
|
||||
self.assertTrue(card["missing_items"] or card["untested_items"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,553 @@
|
||||
"""Coverage batch v2: cloud/server.py handler branches.
|
||||
|
||||
Covers the baseline gaps in _full_auto_branch_protection_ok,
|
||||
_full_auto_github_request, _admin_cost_by_creator,
|
||||
_workers_heartbeat, _admin_backup and _task_trace_stream.
|
||||
|
||||
Stubbing discipline: the method under test is never stubbed. Its
|
||||
sinks and predicate collaborators are: _send_json (captured output),
|
||||
_read_json (request body), _is_global_admin via the real
|
||||
CODEX_GLOBAL_ADMINS env, _user_exists via a real users row,
|
||||
_cost_by_creator_payload (fixed payload / ValueError for the month
|
||||
branch), _registry_heartbeat (ValueError case only), backup_postgres
|
||||
(a real temp file, pg_dump itself not run), _fetch_task /
|
||||
_trace_snapshot / _auth_store for the SSE stream (the stream loop
|
||||
and emit-failure logic under test stays real).
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
import urllib.error
|
||||
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
|
||||
from cloud.server import (
|
||||
CloudControlPlaneHandler,
|
||||
FullAutoError,
|
||||
_full_auto_branch_protection_ok,
|
||||
_full_auto_github_request,
|
||||
init_db,
|
||||
)
|
||||
|
||||
|
||||
def _handler(db_path):
|
||||
h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler)
|
||||
h.server = SimpleNamespace(db_path=db_path)
|
||||
sent = []
|
||||
h._send_json = lambda status, obj, extra_headers=None: sent.append(
|
||||
(status, obj))
|
||||
h.sent = sent
|
||||
return h
|
||||
|
||||
|
||||
def _task_row(status="running", task_id="T1"):
|
||||
return {
|
||||
"id": task_id, "project_id": "p1", "prompt": "do work",
|
||||
"status": status, "idempotency_key": None, "depends_on": "[]",
|
||||
"priority": 0, "created_by": "tester", "created_at": 0.0,
|
||||
"updated_at": 0.0, "last_heartbeat": None, "result_text": None,
|
||||
"result_text_full": None, "error_text": None,
|
||||
"actual_model": None, "started_at": None, "finished_at": None,
|
||||
}
|
||||
|
||||
|
||||
class FullAutoHelpersTestCase(unittest.TestCase):
|
||||
def _valid_protection(self, checks=("verify",)):
|
||||
return {
|
||||
"required_status_checks": {"strict": True,
|
||||
"contexts": list(checks)},
|
||||
"enforce_admins": True,
|
||||
"required_pull_request_reviews": {
|
||||
"required_approving_review_count": 1},
|
||||
"allow_force_pushes": False,
|
||||
"allow_deletions": False,
|
||||
}
|
||||
|
||||
def test_branch_protection_ok(self):
|
||||
self.assertTrue(_full_auto_branch_protection_ok(
|
||||
self._valid_protection(), ["verify"]))
|
||||
self.assertTrue(_full_auto_branch_protection_ok(
|
||||
self._valid_protection(("verify", "lint")),
|
||||
["verify"]))
|
||||
|
||||
def test_branch_protection_failures(self):
|
||||
self.assertFalse(_full_auto_branch_protection_ok(
|
||||
"not-a-dict", ["verify"]))
|
||||
self.assertFalse(_full_auto_branch_protection_ok(
|
||||
self._valid_protection(), []))
|
||||
bad = self._valid_protection()
|
||||
bad["required_status_checks"] = {"strict": False,
|
||||
"contexts": ["verify"]}
|
||||
self.assertFalse(_full_auto_branch_protection_ok(bad, ["verify"]))
|
||||
bad = self._valid_protection()
|
||||
bad["required_status_checks"] = ["verify"]
|
||||
self.assertFalse(_full_auto_branch_protection_ok(bad, ["verify"]))
|
||||
bad = self._valid_protection()
|
||||
bad["required_status_checks"] = {"strict": True,
|
||||
"contexts": ["other"]}
|
||||
self.assertFalse(_full_auto_branch_protection_ok(bad, ["verify"]))
|
||||
bad = self._valid_protection()
|
||||
bad["enforce_admins"] = False
|
||||
self.assertFalse(_full_auto_branch_protection_ok(bad, ["verify"]))
|
||||
bad = self._valid_protection()
|
||||
bad["required_pull_request_reviews"] = None
|
||||
self.assertFalse(_full_auto_branch_protection_ok(bad, ["verify"]))
|
||||
bad = self._valid_protection()
|
||||
bad["allow_force_pushes"] = True
|
||||
self.assertFalse(_full_auto_branch_protection_ok(bad, ["verify"]))
|
||||
bad = self._valid_protection()
|
||||
bad["allow_deletions"] = True
|
||||
self.assertFalse(_full_auto_branch_protection_ok(bad, ["verify"]))
|
||||
|
||||
def test_github_request_fake_client(self):
|
||||
def fake(method, url, headers, body):
|
||||
return 201, {"ok": True}
|
||||
|
||||
self.assertEqual(_full_auto_github_request(
|
||||
fake, "POST", "https://example.invalid/x", {}, None),
|
||||
(201, {"ok": True}))
|
||||
with self.assertRaises(FullAutoError):
|
||||
_full_auto_github_request(
|
||||
lambda m, u, h, b: {"not": "a-tuple"},
|
||||
"GET", "https://example.invalid/x", {}, None)
|
||||
|
||||
def test_github_request_urllib_paths(self):
|
||||
class _Resp:
|
||||
def __init__(self, raw, status=200):
|
||||
self._raw = raw
|
||||
self.status = status
|
||||
|
||||
def read(self):
|
||||
return self._raw
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
with mock.patch("urllib.request.urlopen",
|
||||
return_value=_Resp(b'{"a": 1}', 200)):
|
||||
self.assertEqual(_full_auto_github_request(
|
||||
None, "GET", "https://example.invalid/x", {}, None),
|
||||
(200, {"a": 1}))
|
||||
err = urllib.error.HTTPError(
|
||||
"https://example.invalid/x", 404, "Not Found", {},
|
||||
io.BytesIO(b'{"error": "nope"}'))
|
||||
with mock.patch("urllib.request.urlopen", side_effect=err):
|
||||
self.assertEqual(_full_auto_github_request(
|
||||
None, "GET", "https://example.invalid/x", {}, None),
|
||||
(404, {"error": "nope"}))
|
||||
with mock.patch("urllib.request.urlopen",
|
||||
return_value=_Resp(b'not json', 200)):
|
||||
self.assertEqual(_full_auto_github_request(
|
||||
None, "GET", "https://example.invalid/x", {}, None),
|
||||
(200, {}))
|
||||
with mock.patch("urllib.request.urlopen",
|
||||
return_value=_Resp(b'[1, 2]', 200)):
|
||||
self.assertEqual(_full_auto_github_request(
|
||||
None, "GET", "https://example.invalid/x", {}, None),
|
||||
(200, {}))
|
||||
|
||||
|
||||
class AdminCostTestCase(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)
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY,"
|
||||
" username TEXT UNIQUE, api_key_hash TEXT,"
|
||||
" created_at REAL)")
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO users(id, username, api_key_hash,"
|
||||
" created_at) VALUES('u1', 'u1', '', 0)")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _handler(self):
|
||||
h = _handler(self.db_path)
|
||||
audits = []
|
||||
h._audit_admin = lambda u, a, d: audits.append((u, a, d))
|
||||
h.audits = audits
|
||||
return h
|
||||
|
||||
def test_forbidden_for_non_admin(self):
|
||||
h = self._handler()
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "admin1"},
|
||||
clear=False):
|
||||
h._admin_cost_by_creator("nobody", {"user_id": ["u1"]})
|
||||
self.assertEqual(h.sent[0][0], 403)
|
||||
self.assertTrue(any(a == "admin_quota_denied" for _u, a, _d in h.audits))
|
||||
|
||||
def test_target_validation(self):
|
||||
h = self._handler()
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "admin1"}):
|
||||
h._admin_cost_by_creator("admin1", {})
|
||||
self.assertEqual(h.sent[0][0], 400)
|
||||
h.sent.clear()
|
||||
h._admin_cost_by_creator("admin1", {"user_id": ["ghost"]})
|
||||
self.assertEqual(h.sent[0][0], 404)
|
||||
|
||||
def test_month_value_error_and_success(self):
|
||||
h = self._handler()
|
||||
h._cost_by_creator_payload = mock.Mock(
|
||||
side_effect=ValueError("bad month"))
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "admin1"}):
|
||||
h._admin_cost_by_creator(
|
||||
"admin1", {"user_id": ["u1"], "month": ["2026-13"]})
|
||||
self.assertEqual(h.sent[0][0], 400)
|
||||
self.assertIn("YYYY-MM", h.sent[0][1]["error"])
|
||||
h.sent.clear()
|
||||
h._cost_by_creator_payload = mock.Mock(
|
||||
return_value={"user_id": "u1", "month": "2026-11"})
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "admin1"}):
|
||||
h._admin_cost_by_creator(
|
||||
"admin1", {"user_id": ["u1"], "month": ["2026-11"]})
|
||||
self.assertEqual(h.sent[0][0], 200)
|
||||
self.assertEqual(h.sent[0][1]["user_id"], "u1")
|
||||
|
||||
|
||||
class WorkersHeartbeatTestCase(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)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _call(self, body=None, err=None):
|
||||
h = _handler(self.db_path)
|
||||
h._read_json = lambda: (body, err)
|
||||
h._workers_heartbeat("u1")
|
||||
return h.sent
|
||||
|
||||
def test_read_error(self):
|
||||
sent = self._call(body=None, err="invalid json")
|
||||
self.assertEqual(sent[0][0], 400)
|
||||
|
||||
def test_validation_errors(self):
|
||||
self.assertEqual(self._call({})[0][1]["error"],
|
||||
"worker_id required")
|
||||
self.assertEqual(self._call({"worker_id": "w1",
|
||||
"capacity": "many"})[0][1]["error"],
|
||||
"capacity must be an integer")
|
||||
self.assertEqual(self._call({"worker_id": "w1",
|
||||
"running": "many"})[0][1]["error"],
|
||||
"running must be an integer")
|
||||
self.assertEqual(self._call({"worker_id": "w1",
|
||||
"running": -1})[0][1]["error"],
|
||||
"running must be >= 0")
|
||||
self.assertEqual(self._call({"worker_id": "w1",
|
||||
"host": 123})[0][1]["error"],
|
||||
"host must be a string")
|
||||
self.assertEqual(self._call({"worker_id": "w1",
|
||||
"pid": "12"})[0][1]["error"],
|
||||
"pid must be an integer")
|
||||
self.assertEqual(self._call({"worker_id": "w1",
|
||||
"pid": 0})[0][1]["error"],
|
||||
"pid must be > 0")
|
||||
self.assertEqual(self._call({"worker_id": "w1",
|
||||
"address": 5})[0][1]["error"],
|
||||
"address must be a string")
|
||||
|
||||
def test_registry_value_error(self):
|
||||
h = _handler(self.db_path)
|
||||
h._read_json = lambda: ({"worker_id": "w1"}, None)
|
||||
with mock.patch("cloud.server._registry_heartbeat",
|
||||
side_effect=ValueError("worker_id required")):
|
||||
h._workers_heartbeat("u1")
|
||||
self.assertEqual(h.sent[0][0], 400)
|
||||
self.assertIn("worker_id required", h.sent[0][1]["error"])
|
||||
|
||||
def test_success_registers(self):
|
||||
sent = self._call({"worker_id": "w1", "capacity": 2,
|
||||
"running": 0, "host": "h1", "pid": 123,
|
||||
"address": "10.0.0.1"})
|
||||
self.assertEqual(sent[0][0], 200)
|
||||
self.assertTrue(sent[0][1]["alive"])
|
||||
self.assertEqual(sent[0][1]["worker_id"], "w1")
|
||||
|
||||
|
||||
class AdminBackupTestCase(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.out_root = os.path.join(self.tmp.name, "backups")
|
||||
os.makedirs(self.out_root)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _handler(self):
|
||||
h = _handler(self.db_path)
|
||||
h._audit_admin = lambda u, a, d: None
|
||||
return h
|
||||
|
||||
def test_read_error_and_out_dir_type(self):
|
||||
h = self._handler()
|
||||
h._read_json = lambda: (None, "invalid json")
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "admin1"}):
|
||||
h._admin_backup("admin1")
|
||||
self.assertEqual(h.sent[0][0], 400)
|
||||
h.sent.clear()
|
||||
h._read_json = lambda: ({"out_dir": 123}, None)
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "admin1"}):
|
||||
h._admin_backup("admin1")
|
||||
self.assertEqual(h.sent[0][0], 400)
|
||||
self.assertIn("out_dir must be a string", h.sent[0][1]["error"])
|
||||
|
||||
def test_forbidden_and_outside_root(self):
|
||||
h = self._handler()
|
||||
h._read_json = lambda: ({"out_dir": self.out_root}, None)
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "admin1"}):
|
||||
h._admin_backup("nobody")
|
||||
self.assertEqual(h.sent[0][0], 403)
|
||||
h.sent.clear()
|
||||
h._read_json = lambda: ({"out_dir": "/etc"}, None)
|
||||
with mock.patch.dict(os.environ,
|
||||
{"CODEX_GLOBAL_ADMINS": "admin1",
|
||||
"CLOUD_BACKUP_ROOT": self.out_root}):
|
||||
h._admin_backup("admin1")
|
||||
self.assertEqual(h.sent[0][0], 400)
|
||||
|
||||
def test_sqlite_backup_with_fallback_root(self):
|
||||
# No out_dir and no CLOUD_BACKUP_ROOT: the CLOUD_BACKUP_DIR
|
||||
# fallback root supplies roots[0] as the candidate.
|
||||
h = self._handler()
|
||||
h._read_json = lambda: ({}, None)
|
||||
env = {"CODEX_GLOBAL_ADMINS": "admin1",
|
||||
"CLOUD_BACKUP_DIR": self.out_root}
|
||||
with mock.patch.dict(os.environ, env):
|
||||
os.environ.pop("CLOUD_BACKUP_ROOT", None)
|
||||
os.environ.pop("CLOUD_DB_BACKEND", None)
|
||||
h._admin_backup("admin1")
|
||||
self.assertEqual(h.sent[0][0], 200, h.sent)
|
||||
self.assertTrue(h.sent[0][1]["path"].startswith(self.out_root))
|
||||
|
||||
def test_postgres_backup_branch_with_fake_dump(self):
|
||||
dest = os.path.join(self.out_root, "pg.dump")
|
||||
Path(dest).write_bytes(b"fake-dump")
|
||||
h = self._handler()
|
||||
# _db() resolves the backend from env and refuses PG without
|
||||
# a driver; the backup branch under test is the selection +
|
||||
# success response, so the count query runs on the real
|
||||
# SQLite file through a plain connection here.
|
||||
h._db = lambda: sqlite3.connect(self.db_path)
|
||||
h._read_json = lambda: ({"out_dir": self.out_root}, None)
|
||||
env = {"CODEX_GLOBAL_ADMINS": "admin1",
|
||||
"CLOUD_BACKUP_ROOT": self.out_root,
|
||||
"CLOUD_DB_BACKEND": "postgres"}
|
||||
import cloud.backup as backup_mod
|
||||
with mock.patch.dict(os.environ, env):
|
||||
with mock.patch.object(backup_mod, "backup_postgres",
|
||||
return_value=dest):
|
||||
h._admin_backup("admin1")
|
||||
self.assertEqual(h.sent[0][0], 200, h.sent)
|
||||
self.assertEqual(h.sent[0][1]["backend"], "postgres")
|
||||
self.assertEqual(h.sent[0][1]["path"], dest)
|
||||
|
||||
def test_chmod_failure_swallowed_and_backup_failure_500(self):
|
||||
h = self._handler()
|
||||
h._read_json = lambda: ({"out_dir": self.out_root}, None)
|
||||
env = {"CODEX_GLOBAL_ADMINS": "admin1",
|
||||
"CLOUD_BACKUP_ROOT": self.out_root}
|
||||
with mock.patch.dict(os.environ, env):
|
||||
with mock.patch("os.chmod", side_effect=OSError("chmod down")):
|
||||
h._admin_backup("admin1")
|
||||
self.assertEqual(h.sent[0][0], 200, h.sent)
|
||||
h.sent.clear()
|
||||
import cloud.backup as backup_mod
|
||||
with mock.patch.dict(os.environ, env):
|
||||
with mock.patch.object(backup_mod, "backup_db",
|
||||
side_effect=RuntimeError("dump down")):
|
||||
h._admin_backup("admin1")
|
||||
self.assertEqual(h.sent[0][0], 500)
|
||||
self.assertIn("backup failed", h.sent[0][1]["error"])
|
||||
|
||||
|
||||
class _AuthCtx:
|
||||
def __enter__(self):
|
||||
return SimpleNamespace(check_permission=lambda *args: True)
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
|
||||
class _Writer:
|
||||
def __init__(self, fail_after=None):
|
||||
self.chunks = []
|
||||
self.calls = 0
|
||||
self.fail_after = fail_after
|
||||
|
||||
def write(self, data):
|
||||
self.calls += 1
|
||||
if self.fail_after is not None and self.calls > self.fail_after:
|
||||
raise BrokenPipeError("pipe closed")
|
||||
self.chunks.append(data.decode("utf-8")
|
||||
if isinstance(data, bytes) else data)
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
|
||||
def _trace_handler(db_path, fetch, entries, writer=None,
|
||||
headers_raise=False):
|
||||
h = _handler(db_path)
|
||||
h._fetch_task = fetch
|
||||
h._trace_snapshot = lambda task: (entries, "")
|
||||
h.send_response = lambda *a: (_ for _ in ()).throw(
|
||||
BrokenPipeError("gone")) if headers_raise else None
|
||||
h.send_header = lambda *a: None
|
||||
h.end_headers = lambda: None
|
||||
h.wfile = writer if writer is not None else _Writer()
|
||||
return h
|
||||
|
||||
|
||||
class TraceStreamTestCase(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)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _run(self, handler):
|
||||
with mock.patch("cloud.server._auth_store",
|
||||
lambda db_path: _AuthCtx()):
|
||||
handler._task_trace_stream("u1", "T1")
|
||||
return "".join(handler.wfile.chunks)
|
||||
|
||||
def test_headers_broken_pipe_returns(self):
|
||||
h = _trace_handler(self.db_path, lambda tid: _task_row(),
|
||||
[{"id": "e1"}], headers_raise=True)
|
||||
out = self._run(h)
|
||||
self.assertEqual(out, "")
|
||||
|
||||
def test_snapshot_emit_failure_returns(self):
|
||||
writer = _Writer(fail_after=0)
|
||||
h = _trace_handler(self.db_path, lambda tid: _task_row(),
|
||||
[{"id": "e1"}], writer=writer)
|
||||
out = self._run(h)
|
||||
self.assertEqual(out, "")
|
||||
self.assertEqual(writer.calls, 1)
|
||||
|
||||
def test_poll_timeout(self):
|
||||
h = _trace_handler(self.db_path, lambda tid: _task_row(),
|
||||
[{"id": "e1"}])
|
||||
with mock.patch.object(server_mod, "TRACE_STREAM_MAX_SECONDS",
|
||||
0.05), \
|
||||
mock.patch.object(server_mod, "TRACE_STREAM_POLL_SECONDS",
|
||||
0.01):
|
||||
out = self._run(h)
|
||||
self.assertIn('"type": "snapshot"', out)
|
||||
self.assertIn('"reason": "timeout"', out)
|
||||
|
||||
def test_poll_deleted(self):
|
||||
calls = [0]
|
||||
|
||||
def _fetch(tid):
|
||||
calls[0] += 1
|
||||
return _task_row() if calls[0] == 1 else None
|
||||
|
||||
h = _trace_handler(self.db_path, _fetch, [{"id": "e1"}])
|
||||
with mock.patch.object(server_mod, "TRACE_STREAM_MAX_SECONDS",
|
||||
60), \
|
||||
mock.patch.object(server_mod, "TRACE_STREAM_POLL_SECONDS",
|
||||
0.01):
|
||||
out = self._run(h)
|
||||
self.assertIn('"status": "deleted"', out)
|
||||
|
||||
def test_poll_new_entry_then_terminal(self):
|
||||
calls = [0]
|
||||
|
||||
def _fetch(tid):
|
||||
calls[0] += 1
|
||||
if calls[0] == 1:
|
||||
return _task_row(status="running")
|
||||
return _task_row(status="completed")
|
||||
|
||||
def _snapshot(task):
|
||||
if task["status"] == "completed":
|
||||
return [{"id": "e1"}, {"id": "e2"}], ""
|
||||
return [{"id": "e1"}], ""
|
||||
|
||||
h = _trace_handler(self.db_path, _fetch, [{"id": "e1"}])
|
||||
h._trace_snapshot = _snapshot
|
||||
with mock.patch.object(server_mod, "TRACE_STREAM_MAX_SECONDS",
|
||||
60), \
|
||||
mock.patch.object(server_mod, "TRACE_STREAM_POLL_SECONDS",
|
||||
0.01):
|
||||
out = self._run(h)
|
||||
self.assertIn('"type": "entry"', out)
|
||||
self.assertIn('"status": "completed"', out)
|
||||
|
||||
def test_poll_emit_failure_returns(self):
|
||||
writer = _Writer(fail_after=1)
|
||||
calls = [0]
|
||||
|
||||
def _fetch(tid):
|
||||
calls[0] += 1
|
||||
if calls[0] == 1:
|
||||
return _task_row(status="running")
|
||||
return _task_row(status="completed")
|
||||
|
||||
def _snapshot(task):
|
||||
if task["status"] == "completed":
|
||||
return [{"id": "e1"}, {"id": "e2"}], ""
|
||||
return [{"id": "e1"}], ""
|
||||
|
||||
h = _trace_handler(self.db_path, _fetch, [{"id": "e1"}],
|
||||
writer=writer)
|
||||
h._trace_snapshot = _snapshot
|
||||
with mock.patch.object(server_mod, "TRACE_STREAM_MAX_SECONDS",
|
||||
60), \
|
||||
mock.patch.object(server_mod, "TRACE_STREAM_POLL_SECONDS",
|
||||
0.01):
|
||||
out = self._run(h)
|
||||
# Snapshot write ok, loop entry write fails -> stream ends
|
||||
# without a done event.
|
||||
self.assertIn('"type": "snapshot"', out)
|
||||
self.assertNotIn('"type": "done"', out)
|
||||
|
||||
def test_poll_fetch_broken_pipe_returns(self):
|
||||
calls = [0]
|
||||
|
||||
def _fetch(tid):
|
||||
calls[0] += 1
|
||||
if calls[0] == 1:
|
||||
return _task_row(status="running")
|
||||
raise BrokenPipeError("gone")
|
||||
|
||||
h = _trace_handler(self.db_path, _fetch, [{"id": "e1"}])
|
||||
with mock.patch.object(server_mod, "TRACE_STREAM_MAX_SECONDS",
|
||||
60), \
|
||||
mock.patch.object(server_mod, "TRACE_STREAM_POLL_SECONDS",
|
||||
0.01):
|
||||
out = self._run(h)
|
||||
self.assertIn('"type": "snapshot"', out)
|
||||
self.assertNotIn('"reason": "timeout"', out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,915 @@
|
||||
"""Coverage batch v2 (D2): cloud/server.py handler validation branches.
|
||||
|
||||
Covers baseline gaps in _create_project, _create_environment,
|
||||
_create_upload, _automation_create, _automation_bind,
|
||||
_automation_heartbeat, _merge_task, _loop_run, _audit_loop_confirm,
|
||||
_full_auto_execute and _create_task (gateway / attachment /
|
||||
idempotency).
|
||||
|
||||
Stubbing discipline: the method under test is never stubbed. Its
|
||||
sinks and predicate collaborators are: _send_json (captured output),
|
||||
_read_json (request body), _db (a proxy connection that delegates
|
||||
every statement except the one under test, for the TOCTOU
|
||||
IntegrityError simulations -- the same pattern as the worker retry
|
||||
rowcount-0 test), _validate_sub2api_key / _pushback_block /
|
||||
_loop_verdict / _loop_confirm (collaborator error injection for the
|
||||
handler's own error mapping), and _loop_ledger_store /
|
||||
_loop_create_subtask (close/subtask failure injection). All other
|
||||
logic -- validation, permission checks via the real AuthStore,
|
||||
quota gates, driver result shaping -- runs for real.
|
||||
"""
|
||||
|
||||
import json
|
||||
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
|
||||
from cloud.server import CloudControlPlaneHandler, _auth_store, init_db
|
||||
from cloud.sub2api import Sub2apiError as _Sub2apiError
|
||||
|
||||
|
||||
def _handler(db_path, upload_dir=None):
|
||||
h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler)
|
||||
h.server = SimpleNamespace(db_path=db_path, upload_dir=upload_dir)
|
||||
sent = []
|
||||
h._send_json = lambda status, obj, extra_headers=None: sent.append(
|
||||
(status, obj))
|
||||
h.sent = sent
|
||||
return h
|
||||
|
||||
|
||||
def _conn(db_path):
|
||||
c = sqlite3.connect(db_path)
|
||||
c.row_factory = sqlite3.Row
|
||||
return c
|
||||
|
||||
|
||||
class _ProxyConn:
|
||||
"""Delegate everything except the statements a test targets."""
|
||||
|
||||
def __init__(self, real, fail_insert_prefix=None,
|
||||
fail_insert_exc=None, first_select_none=None,
|
||||
close_exc=None):
|
||||
self._real = real
|
||||
self._fail_insert_prefix = fail_insert_prefix
|
||||
self._fail_insert_exc = fail_insert_exc
|
||||
self._first_select_none = first_select_none
|
||||
self._seen_select = 0
|
||||
self._close_exc = close_exc
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
text = sql.strip()
|
||||
if (self._fail_insert_prefix
|
||||
and text.startswith(self._fail_insert_prefix)):
|
||||
raise self._fail_insert_exc
|
||||
if (self._first_select_none
|
||||
and self._first_select_none in text
|
||||
and text.startswith("SELECT")):
|
||||
self._seen_select += 1
|
||||
if self._seen_select == 1:
|
||||
class _Empty:
|
||||
def fetchone(self):
|
||||
return None
|
||||
|
||||
def fetchall(self):
|
||||
return []
|
||||
return _Empty()
|
||||
return self._real.execute(sql, params)
|
||||
|
||||
def close(self):
|
||||
if self._close_exc is not None:
|
||||
raise self._close_exc
|
||||
return self._real.close()
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
|
||||
|
||||
class _Base(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "test.db")
|
||||
init_db(self.db_path)
|
||||
self.upload_dir = os.path.join(self.tmp.name, "uploads")
|
||||
os.makedirs(self.upload_dir, exist_ok=True)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _h(self, body=None, err=None):
|
||||
h = _handler(self.db_path, upload_dir=self.upload_dir)
|
||||
h._read_json = lambda: (body, err)
|
||||
return h
|
||||
|
||||
def _project(self, pid="p1", owner="u1", repo_url=None):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO projects(id, name, owner_id, created_at,"
|
||||
" repo_url, auto_pr) VALUES(?, ?, ?, ?, ?, 0)",
|
||||
(pid, "P", owner, time.time(), repo_url))
|
||||
c.commit()
|
||||
c.close()
|
||||
with _auth_store(self.db_path) as store:
|
||||
store.create_project(pid, owner)
|
||||
|
||||
def _member(self, pid, uid, role):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO project_members(project_id,"
|
||||
" user_id, role) VALUES(?, ?, ?)", (pid, uid, role))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _task(self, tid="t1", pid="p1", status="completed",
|
||||
created_by="u1", **extra):
|
||||
cols = ["id", "project_id", "prompt", "status", "created_by",
|
||||
"created_at", "updated_at"]
|
||||
vals = [tid, pid, "do work", status, created_by, time.time(),
|
||||
time.time()]
|
||||
for key, val in extra.items():
|
||||
cols.append(key)
|
||||
vals.append(val)
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO tasks(%s) VALUES(%s)"
|
||||
% (", ".join(cols), ", ".join("?" * len(cols))), vals)
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _last(self, h):
|
||||
self.assertTrue(h.sent, "handler sent no response")
|
||||
return h.sent[-1]
|
||||
|
||||
|
||||
class ProjectEnvUploadTestCase(_Base):
|
||||
def test_create_project_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._create_project("u1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_create_project_name_required(self):
|
||||
h = self._h(body={})
|
||||
h._create_project("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("name required", payload["error"])
|
||||
|
||||
def test_create_project_id_must_be_string(self):
|
||||
h = self._h(body={"name": "P", "id": 42})
|
||||
h._create_project("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("id must be a string", payload["error"])
|
||||
|
||||
def test_create_project_auto_pr_bool(self):
|
||||
# Bool branch (True -> 1): the 3351 line.
|
||||
h = self._h(body={"name": "P", "auto_pr": True})
|
||||
h._create_project("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 201)
|
||||
self.assertEqual(payload["auto_pr"], 1)
|
||||
|
||||
def test_create_project_existing_409(self):
|
||||
self._project()
|
||||
h = self._h(body={"name": "P2", "id": "p1"})
|
||||
h._create_project("u1")
|
||||
self.assertEqual(self._last(h)[0], 409)
|
||||
|
||||
def test_create_project_integrity_error_409(self):
|
||||
# TOCTOU simulation: SELECT finds nothing, INSERT races and
|
||||
# loses; the handler must map it to the same 409. Each _db()
|
||||
# call gets a fresh proxy so closing one connection cannot
|
||||
# poison the next.
|
||||
h = self._h(body={"name": "P", "id": "p9"})
|
||||
h._db = lambda: _ProxyConn(
|
||||
_conn(self.db_path),
|
||||
fail_insert_prefix="INSERT INTO projects",
|
||||
fail_insert_exc=sqlite3.IntegrityError("race"))
|
||||
h._create_project("u1")
|
||||
self.assertEqual(self._last(h)[0], 409)
|
||||
|
||||
def test_create_environment_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._create_environment("u1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_create_environment_project_id_required(self):
|
||||
h = self._h(body={})
|
||||
h._create_environment("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("project_id required", payload["error"])
|
||||
|
||||
def test_create_environment_name_too_long(self):
|
||||
self._project(repo_url="https://example.com/r.git")
|
||||
h = self._h(body={"project_id": "p1", "name": "x" * 101})
|
||||
h._create_environment("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("name too long", payload["error"])
|
||||
|
||||
def test_create_environment_integrity_error_409(self):
|
||||
self._project(repo_url="https://example.com/r.git")
|
||||
h = self._h(body={"project_id": "p1", "name": "env1"})
|
||||
h._db = lambda: _ProxyConn(
|
||||
_conn(self.db_path),
|
||||
fail_insert_prefix="INSERT INTO environments",
|
||||
fail_insert_exc=sqlite3.IntegrityError("race"))
|
||||
h._create_environment("u1")
|
||||
self.assertEqual(self._last(h)[0], 409)
|
||||
|
||||
def _multipart(self, parts):
|
||||
boundary = "BOUNDARY"
|
||||
out = b""
|
||||
for headers, data in parts:
|
||||
out += ("--%s\r\n" % boundary).encode()
|
||||
for key, val in headers:
|
||||
out += ("%s: %s\r\n" % (key, val)).encode()
|
||||
out += b"\r\n" + data + b"\r\n"
|
||||
out += ("--%s--\r\n" % boundary).encode()
|
||||
return out, "multipart/form-data; boundary=" + boundary
|
||||
|
||||
def test_create_upload_content_length_value_error(self):
|
||||
h = self._h()
|
||||
h.headers = {"Content-Type": "multipart/form-data; boundary=X",
|
||||
"Content-Length": "not-a-number"}
|
||||
h._read_body = lambda: b""
|
||||
h._create_upload("u1")
|
||||
code, payload = self._last(h)
|
||||
# Length falls back to 0, then the empty body fails parsing.
|
||||
self.assertEqual(code, 400)
|
||||
|
||||
def test_create_upload_too_large(self):
|
||||
from cloud.uploads import MAX_RAW_BYTES
|
||||
h = self._h()
|
||||
h.headers = {"Content-Type": "multipart/form-data; boundary=X",
|
||||
"Content-Length": str(MAX_RAW_BYTES + 1)}
|
||||
h._read_body = lambda: b"x"
|
||||
h._create_upload("u1")
|
||||
self.assertEqual(self._last(h)[0], 413)
|
||||
|
||||
def test_create_upload_parse_error(self):
|
||||
raw, ctype = self._multipart([])
|
||||
h = self._h()
|
||||
h.headers = {"Content-Type": ctype,
|
||||
"Content-Length": str(len(b"garbage"))}
|
||||
h._read_body = lambda: b"garbage"
|
||||
h._create_upload("u1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_create_upload_no_files(self):
|
||||
# Zero parts: fields and files are both empty, so the paths
|
||||
# check passes vacuously and the no-files branch fires.
|
||||
raw, ctype = self._multipart([])
|
||||
h = self._h()
|
||||
h.headers = {"Content-Type": ctype,
|
||||
"Content-Length": str(len(raw))}
|
||||
h._read_body = lambda: raw
|
||||
h._create_upload("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("no files", payload["error"])
|
||||
|
||||
def test_create_upload_paths_mismatch(self):
|
||||
raw, ctype = self._multipart([
|
||||
([("Content-Disposition",
|
||||
'form-data; name="f1"; filename="a.txt"'),
|
||||
("Content-Type", "text/plain")], b"hello"),
|
||||
([("Content-Disposition",
|
||||
'form-data; name="f2"; filename="b.txt"'),
|
||||
("Content-Type", "text/plain")], b"world"),
|
||||
([("Content-Disposition", 'form-data; name="paths"')],
|
||||
b'["a.txt"]')])
|
||||
h = self._h()
|
||||
h.headers = {"Content-Type": ctype,
|
||||
"Content-Length": str(len(raw))}
|
||||
h._read_body = lambda: raw
|
||||
h._create_upload("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("数量不一致", payload["error"])
|
||||
|
||||
|
||||
class AutomationTestCase(_Base):
|
||||
def _auto(self, name="auto1", project_id="p1", prompt="do it"):
|
||||
from cloud.automations import create_automation
|
||||
c = _conn(self.db_path)
|
||||
auto = create_automation(c, project_id, name, prompt, "u1")
|
||||
c.close()
|
||||
return auto
|
||||
|
||||
def _bind(self, aid, tid, mode="heartbeat"):
|
||||
from cloud.lifecycle import bind_automation
|
||||
c = _conn(self.db_path)
|
||||
binding = bind_automation(c, aid, "p1", tid, mode)
|
||||
c.close()
|
||||
return binding
|
||||
|
||||
def test_automation_create_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._automation_create("u1", "p1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_automation_create_interval_none_defaults(self):
|
||||
self._project()
|
||||
h = self._h(body={"name": "a1", "prompt": "p",
|
||||
"interval_seconds": None})
|
||||
h._automation_create("u1", "p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 201)
|
||||
self.assertEqual(payload["interval_seconds"], 86400)
|
||||
|
||||
def test_automation_create_bad_first_run(self):
|
||||
self._project()
|
||||
h = self._h(body={"name": "a1", "prompt": "p",
|
||||
"first_run_at": "not-a-time"})
|
||||
h._automation_create("u1", "p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("first_run_at", payload["error"])
|
||||
|
||||
def test_automation_create_run_once_must_be_bool(self):
|
||||
self._project()
|
||||
h = self._h(body={"name": "a1", "prompt": "p",
|
||||
"run_once": "yes"})
|
||||
h._automation_create("u1", "p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("run_once", payload["error"])
|
||||
|
||||
def test_automation_create_generic_error_502(self):
|
||||
self._project()
|
||||
h = self._h(body={"name": "a1", "prompt": "p"})
|
||||
with mock.patch("cloud.server._auto_create",
|
||||
side_effect=RuntimeError("db boom")):
|
||||
h._automation_create("u1", "p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 502)
|
||||
self.assertIn("db boom", payload["error"])
|
||||
|
||||
def test_automation_bind_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._automation_bind("u1", "p1", "a1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_automation_bind_missing_404(self):
|
||||
h = self._h(body={"task_id": "t1"})
|
||||
h._automation_bind("u1", "p1", "nope")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_automation_bind_cancelled_409(self):
|
||||
auto = self._auto()
|
||||
c = _conn(self.db_path)
|
||||
c.execute("UPDATE automations SET status='cancelled' WHERE id=?",
|
||||
(auto["id"],))
|
||||
c.commit()
|
||||
c.close()
|
||||
self._task()
|
||||
h = self._h(body={"task_id": "t1"})
|
||||
h._automation_bind("u1", "p1", auto["id"])
|
||||
self.assertEqual(self._last(h)[0], 409)
|
||||
|
||||
def test_automation_bind_target_missing_404(self):
|
||||
auto = self._auto()
|
||||
h = self._h(body={"task_id": "ghost"})
|
||||
h._automation_bind("u1", "p1", auto["id"])
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_automation_bind_target_cross_project_404(self):
|
||||
auto = self._auto()
|
||||
self._project(pid="p2", owner="u2")
|
||||
self._task(tid="t2", pid="p2", created_by="u2")
|
||||
h = self._h(body={"task_id": "t2"})
|
||||
h._automation_bind("u1", "p1", auto["id"])
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_automation_bind_bad_mode_400(self):
|
||||
auto = self._auto()
|
||||
self._task()
|
||||
h = self._h(body={"task_id": "t1", "mode": "bogus"})
|
||||
h._automation_bind("u1", "p1", auto["id"])
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("heartbeat or cron", payload["error"])
|
||||
|
||||
def test_automation_heartbeat_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._automation_heartbeat("u1", "p1", "a1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_automation_heartbeat_missing_404(self):
|
||||
h = self._h(body={})
|
||||
h._automation_heartbeat("u1", "p1", "nope")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_automation_heartbeat_no_binding_409(self):
|
||||
auto = self._auto()
|
||||
h = self._h(body={})
|
||||
h._automation_heartbeat("u1", "p1", auto["id"])
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 409)
|
||||
self.assertIn("no heartbeat binding", payload["error"])
|
||||
|
||||
def test_automation_heartbeat_cron_binding_409(self):
|
||||
auto = self._auto()
|
||||
self._task()
|
||||
self._bind(auto["id"], "t1", mode="cron")
|
||||
h = self._h(body={})
|
||||
h._automation_heartbeat("u1", "p1", auto["id"])
|
||||
self.assertEqual(self._last(h)[0], 409)
|
||||
|
||||
def test_automation_heartbeat_bound_task_missing_404(self):
|
||||
auto = self._auto()
|
||||
self._task()
|
||||
self._bind(auto["id"], "t1")
|
||||
c = _conn(self.db_path)
|
||||
c.execute("DELETE FROM tasks WHERE id='t1'")
|
||||
c.commit()
|
||||
c.close()
|
||||
h = self._h(body={})
|
||||
h._automation_heartbeat("u1", "p1", auto["id"])
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_automation_heartbeat_bound_task_cross_project_404(self):
|
||||
auto = self._auto()
|
||||
self._task()
|
||||
self._bind(auto["id"], "t1")
|
||||
c = _conn(self.db_path)
|
||||
c.execute("UPDATE tasks SET project_id='p2' WHERE id='t1'")
|
||||
c.commit()
|
||||
c.close()
|
||||
h = self._h(body={})
|
||||
h._automation_heartbeat("u1", "p1", auto["id"])
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_automation_heartbeat_quota_reject_returns(self):
|
||||
# The bare return after the quota gate: quota lookup failure
|
||||
# makes the gate answer 503, and heartbeat must return
|
||||
# without delivering anything.
|
||||
auto = self._auto()
|
||||
self._task()
|
||||
self._bind(auto["id"], "t1")
|
||||
h = self._h(body={})
|
||||
h._quota_status = mock.Mock(side_effect=RuntimeError("down"))
|
||||
h._automation_heartbeat("u1", "p1", auto["id"])
|
||||
self.assertEqual(len(h.sent), 1)
|
||||
self.assertEqual(h.sent[0][0], 503)
|
||||
|
||||
def test_automation_heartbeat_message_must_be_string(self):
|
||||
auto = self._auto()
|
||||
self._task()
|
||||
self._bind(auto["id"], "t1")
|
||||
h = self._h(body={"message": 123})
|
||||
h._automation_heartbeat("u1", "p1", auto["id"])
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("message required", payload["error"])
|
||||
|
||||
def test_automation_heartbeat_message_too_long(self):
|
||||
auto = self._auto()
|
||||
self._task()
|
||||
self._bind(auto["id"], "t1")
|
||||
h = self._h(body={"message": "x" * 20001})
|
||||
h._automation_heartbeat("u1", "p1", auto["id"])
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("message too long", payload["error"])
|
||||
|
||||
|
||||
class MergeLoopTestCase(_Base):
|
||||
def _state(self):
|
||||
return SimpleNamespace(
|
||||
module_id="m", contract_version="v1", pushback_round=0,
|
||||
failure_count=0, audit_fail_count=0,
|
||||
acceptance_fail_count=0, rebuild_no=0, targeted_used=False,
|
||||
status="open", pending_confirmation=False, retained_tests=[],
|
||||
retained_passed_items=[], previous_artifacts=[], history=[])
|
||||
|
||||
def _loop_result(self, subtask=None):
|
||||
return {
|
||||
"next_state": self._state(), "action_kind": "verdict",
|
||||
"reason": "r", "merge_blocked": False,
|
||||
"release_blocked": False, "requires_reaudit": False,
|
||||
"escalation_grants_user_acceptance": False,
|
||||
"notification": None, "subtask": subtask}
|
||||
|
||||
def _confirm_result(self, subtask=None):
|
||||
result = self._loop_result(subtask)
|
||||
result.pop("requires_reaudit")
|
||||
result.pop("escalation_grants_user_acceptance")
|
||||
return result
|
||||
|
||||
def _closing_db(self):
|
||||
# First _db() call serves _fetch_task (real); later calls
|
||||
# serve the loop connection whose close() raises, so the
|
||||
# finally-block except-pass lines run.
|
||||
calls = {"n": 0}
|
||||
|
||||
def _db():
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
return _conn(self.db_path)
|
||||
return _ProxyConn(_conn(self.db_path),
|
||||
close_exc=RuntimeError("close boom"))
|
||||
return _db
|
||||
|
||||
def _closing_store(self):
|
||||
class _Store:
|
||||
def close(self):
|
||||
raise RuntimeError("store close boom")
|
||||
return _Store()
|
||||
|
||||
def test_merge_read_error(self):
|
||||
self._project()
|
||||
self._task(status="completed")
|
||||
h = self._h(err="bad json")
|
||||
h._merge_task("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_merge_files_changed_must_be_list(self):
|
||||
self._project()
|
||||
self._task(status="completed")
|
||||
h = self._h(body={"files_changed": "x"})
|
||||
h._merge_task("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("files_changed must be a list", payload["error"])
|
||||
|
||||
def test_merge_pushback_unavailable_409(self):
|
||||
self._project()
|
||||
self._task(status="completed")
|
||||
h = self._h(body={})
|
||||
with mock.patch.dict(os.environ,
|
||||
{"CLOUD_MERGE_GATE_MODE": "off"}), \
|
||||
mock.patch("cloud.server._pushback_block",
|
||||
side_effect=RuntimeError("chain down")):
|
||||
h._merge_task("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 409)
|
||||
self.assertEqual(payload["error"],
|
||||
"audit pushback state unavailable")
|
||||
self.assertTrue(payload["audit_pushback"]["merge_blocked"])
|
||||
|
||||
def test_merge_pushback_unavailable_audit_fails_too(self):
|
||||
# The audit best-effort except-pass: even when the audit
|
||||
# write itself raises, the handler still answers the same 409.
|
||||
self._project()
|
||||
self._task(status="completed")
|
||||
h = self._h(body={})
|
||||
real_auth = server_mod._auth_store
|
||||
calls = {"n": 0}
|
||||
|
||||
def _auth_store(db_path):
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
return real_auth(db_path)
|
||||
raise RuntimeError("audit down")
|
||||
with mock.patch.dict(os.environ,
|
||||
{"CLOUD_MERGE_GATE_MODE": "off"}), \
|
||||
mock.patch("cloud.server._pushback_block",
|
||||
side_effect=RuntimeError("chain down")), \
|
||||
mock.patch("cloud.server._auth_store",
|
||||
side_effect=_auth_store):
|
||||
h._merge_task("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 409)
|
||||
self.assertEqual(payload["error"],
|
||||
"audit pushback state unavailable")
|
||||
|
||||
def test_loop_run_task_missing_404(self):
|
||||
h = self._h()
|
||||
h._loop_run("u1", "ghost", {}, "verdict")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_loop_run_forbidden_403(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._loop_run("u9", "t1", {}, "verdict")
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
def test_loop_run_verdict_body_400(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._loop_run("u1", "t1", {}, "verdict")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("verdict dict", payload["error"])
|
||||
|
||||
def test_loop_run_fix_body_400(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._loop_run("u1", "t1", {}, "fix")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_loop_run_correction_body_400(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._loop_run("u1", "t1", {}, "correction")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_loop_run_unknown_kind_400(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._loop_run("u1", "t1", {}, "bogus")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("unknown loop kind", payload["error"])
|
||||
|
||||
def test_loop_run_driver_value_error_400(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._loop_verdict",
|
||||
side_effect=ValueError("driver nope")):
|
||||
h._loop_run("u1", "t1", {"verdict": {"verdict": "fail"}},
|
||||
"verdict")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_loop_run_driver_exception_503(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._loop_verdict",
|
||||
side_effect=RuntimeError("ledger down")):
|
||||
h._loop_run("u1", "t1", {"verdict": {"verdict": "fail"}},
|
||||
"verdict")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 503)
|
||||
self.assertEqual(payload["error"], "audit_loop_unavailable")
|
||||
|
||||
def test_loop_run_close_failures_swallowed_200(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._db = self._closing_db()
|
||||
h._loop_ledger_store = lambda: self._closing_store()
|
||||
with mock.patch("cloud.server._loop_verdict",
|
||||
return_value=self._loop_result()):
|
||||
h._loop_run("u1", "t1", {"verdict": {"verdict": "fail"}},
|
||||
"verdict")
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
def test_loop_run_subtask_error_503(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._loop_create_subtask = mock.Mock(
|
||||
side_effect=RuntimeError("insert boom"))
|
||||
with mock.patch("cloud.server._loop_verdict",
|
||||
return_value=self._loop_result(
|
||||
subtask={"kind": "rebuild"})):
|
||||
h._loop_run("u1", "t1", {"verdict": {"verdict": "fail"}},
|
||||
"verdict")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 503)
|
||||
self.assertIn("subtask_error", payload)
|
||||
self.assertIn("subtask_note", payload)
|
||||
|
||||
def test_confirm_task_missing_404(self):
|
||||
h = self._h(body={})
|
||||
h._audit_loop_confirm("u1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_confirm_forbidden_403(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(body={})
|
||||
h._audit_loop_confirm("u9", "t1")
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
def test_confirm_read_error(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(err="bad json")
|
||||
h._audit_loop_confirm("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_confirm_driver_value_error_400(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(body={"user_confirmed": True, "confirmed_by": "u1"})
|
||||
with mock.patch("cloud.server._loop_confirm",
|
||||
side_effect=ValueError("driver nope")):
|
||||
h._audit_loop_confirm("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_confirm_driver_exception_503(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(body={"user_confirmed": True, "confirmed_by": "u1"})
|
||||
with mock.patch("cloud.server._loop_confirm",
|
||||
side_effect=RuntimeError("ledger down")):
|
||||
h._audit_loop_confirm("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 503)
|
||||
self.assertEqual(payload["error"], "audit_loop_unavailable")
|
||||
|
||||
def test_confirm_close_failures_swallowed_200(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(body={"user_confirmed": True, "confirmed_by": "u1"})
|
||||
h._db = self._closing_db()
|
||||
h._loop_ledger_store = lambda: self._closing_store()
|
||||
with mock.patch("cloud.server._loop_confirm",
|
||||
return_value=self._confirm_result()):
|
||||
h._audit_loop_confirm("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
def test_confirm_subtask_error_503(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(body={"user_confirmed": True, "confirmed_by": "u1"})
|
||||
h._loop_create_subtask = mock.Mock(
|
||||
side_effect=RuntimeError("insert boom"))
|
||||
with mock.patch("cloud.server._loop_confirm",
|
||||
return_value=self._confirm_result(
|
||||
subtask={"kind": "rebuild"})):
|
||||
h._audit_loop_confirm("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 503)
|
||||
self.assertIn("subtask_error", payload)
|
||||
|
||||
|
||||
class FullAutoCreateTaskTestCase(_Base):
|
||||
def _full_auto_h(self, body, err=None, admin=False):
|
||||
h = self._h(body, err)
|
||||
h._audit_admin = lambda *args: None
|
||||
return h
|
||||
|
||||
def test_full_auto_read_error(self):
|
||||
h = self._full_auto_h(None, err="bad json")
|
||||
h._full_auto_execute("u1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_full_auto_plan_error(self):
|
||||
h = self._full_auto_h({})
|
||||
h._full_auto_execute("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("repo must be", payload["error"])
|
||||
|
||||
def test_full_auto_not_admin_denied(self):
|
||||
h = self._full_auto_h({"repo": "o/r"})
|
||||
with mock.patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop("CODEX_GLOBAL_ADMINS", None)
|
||||
h._full_auto_execute("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 403)
|
||||
self.assertEqual(payload["reason"], "not_global_admin")
|
||||
|
||||
def test_full_auto_action_must_be_string(self):
|
||||
h = self._full_auto_h({"repo": "o/r", "action": 123})
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "u1"}):
|
||||
h._full_auto_execute("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("action must be a string", payload["error"])
|
||||
|
||||
def test_full_auto_disabled_denied(self):
|
||||
h = self._full_auto_h({"repo": "o/r"})
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "u1"}):
|
||||
h._full_auto_execute("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 403)
|
||||
self.assertEqual(payload["reason"], "full_auto_disabled")
|
||||
|
||||
def _install(self, repo="o/r"):
|
||||
from cloud.github_app import get_app_config
|
||||
c = _conn(self.db_path)
|
||||
get_app_config(c)
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO github_app_config(id, app_id, slug,"
|
||||
" client_id, webhook_secret, pem, html_url, created_at)"
|
||||
" VALUES(1, '1', 'app', 'cid', 'sec', NULL, NULL, ?)",
|
||||
(time.time(),))
|
||||
from cloud.github_app import _ensure_installations_table
|
||||
_ensure_installations_table(c)
|
||||
c.execute(
|
||||
"INSERT INTO github_app_installations(installation_id,"
|
||||
" repo_full_name, created_at) VALUES(?, ?, ?)",
|
||||
(123, repo, time.time()))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def test_full_auto_token_missing_denied(self):
|
||||
self._install()
|
||||
h = self._full_auto_h({"repo": "o/r"})
|
||||
h.server.full_auto_token_resolver = lambda db_path, repo: None
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "u1",
|
||||
"FULL_AUTO_ENABLED": "1"}):
|
||||
h._full_auto_execute("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 403)
|
||||
self.assertEqual(payload["reason"], "token_missing")
|
||||
|
||||
def test_full_auto_protection_request_error_denied(self):
|
||||
self._install()
|
||||
h = self._full_auto_h({"repo": "o/r"})
|
||||
h.server.full_auto_token_resolver = lambda db_path, repo: "tok"
|
||||
from cloud.full_auto import FullAutoError
|
||||
h.server.full_auto_http_client = mock.Mock(
|
||||
side_effect=FullAutoError("net down", reason="net"))
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "u1",
|
||||
"FULL_AUTO_ENABLED": "1"}):
|
||||
h._full_auto_execute("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 403)
|
||||
self.assertEqual(payload["reason"],
|
||||
"branch_protection_unverified")
|
||||
|
||||
def test_full_auto_protection_not_200_denied(self):
|
||||
self._install()
|
||||
h = self._full_auto_h({"repo": "o/r"})
|
||||
h.server.full_auto_token_resolver = lambda db_path, repo: "tok"
|
||||
h.server.full_auto_http_client = lambda m, u, hd, b: (404, {})
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "u1",
|
||||
"FULL_AUTO_ENABLED": "1"}):
|
||||
h._full_auto_execute("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 403)
|
||||
self.assertEqual(payload["reason"],
|
||||
"branch_protection_unverified")
|
||||
|
||||
def test_create_task_gateway_sub2api_error_502(self):
|
||||
self._project()
|
||||
h = self._h(body={"project_id": "p1", "prompt": "hello"})
|
||||
with mock.patch.dict(os.environ, {"CLOUD_GATEWAY": "sub2api"}), \
|
||||
mock.patch("cloud.server._validate_sub2api_key",
|
||||
side_effect=_Sub2apiError("key dead")):
|
||||
h._create_task("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 502)
|
||||
self.assertIn("key dead", payload["error"])
|
||||
|
||||
def test_create_task_gateway_generic_error_502(self):
|
||||
self._project()
|
||||
h = self._h(body={"project_id": "p1", "prompt": "hello"})
|
||||
with mock.patch.dict(os.environ, {"CLOUD_GATEWAY": "sub2api"}), \
|
||||
mock.patch("cloud.server._validate_sub2api_key",
|
||||
side_effect=RuntimeError("net down")):
|
||||
h._create_task("u1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def _upload_row(self, uid="up1"):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO uploads(id, created_by, created_at, status)"
|
||||
" VALUES(?, ?, ?, 'pending')",
|
||||
(uid, "u1", time.time()))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def test_create_task_attachment_dir_missing_400(self):
|
||||
self._project()
|
||||
self._upload_row()
|
||||
h = self._h(body={"project_id": "p1", "prompt": "hello",
|
||||
"upload_id": "up1"})
|
||||
h._create_task("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("暂存目录缺失", payload["error"])
|
||||
|
||||
def test_create_task_attachment_empty_400(self):
|
||||
self._project()
|
||||
self._upload_row()
|
||||
os.makedirs(os.path.join(self.upload_dir, "up1"), exist_ok=True)
|
||||
h = self._h(body={"project_id": "p1", "prompt": "hello",
|
||||
"upload_id": "up1"})
|
||||
h._create_task("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("附件包为空", payload["error"])
|
||||
|
||||
def test_create_task_idempotency_race_returns_existing(self):
|
||||
self._project()
|
||||
self._task(tid="t-old", idempotency_key="k1")
|
||||
h = self._h(body={"project_id": "p1", "prompt": "hello",
|
||||
"idempotency_key": "k1"})
|
||||
h._db = lambda: _ProxyConn(
|
||||
_conn(self.db_path),
|
||||
fail_insert_prefix="INSERT INTO tasks",
|
||||
fail_insert_exc=sqlite3.IntegrityError("race"),
|
||||
first_select_none="idempotency_key = ?")
|
||||
h._create_task("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(payload["id"], "t-old")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,496 @@
|
||||
"""Coverage batch v2 (D4): cloud/server.py small handler clusters.
|
||||
|
||||
Same stubbing discipline as D2/D3: the method under test is never
|
||||
stubbed; _send_json / _read_json are captured input, store classes
|
||||
are faked only where the test target is the close-swallow itself
|
||||
(the fake still performs the business call), and collaborator
|
||||
error injection covers only the handler's own error mapping.
|
||||
"""
|
||||
|
||||
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))
|
||||
|
||||
from cloud.server import (
|
||||
CloudControlPlaneHandler,
|
||||
_auth_store,
|
||||
init_db,
|
||||
)
|
||||
|
||||
|
||||
def _handler(db_path, upload_dir=None):
|
||||
h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler)
|
||||
h.server = SimpleNamespace(db_path=db_path, upload_dir=upload_dir)
|
||||
sent = []
|
||||
h._send_json = lambda status, obj, extra_headers=None: sent.append(
|
||||
(status, obj))
|
||||
h.sent = sent
|
||||
return h
|
||||
|
||||
|
||||
def _conn(db_path):
|
||||
c = sqlite3.connect(db_path)
|
||||
c.row_factory = sqlite3.Row
|
||||
return c
|
||||
|
||||
|
||||
class _Base(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "test.db")
|
||||
init_db(self.db_path)
|
||||
self.upload_dir = os.path.join(self.tmp.name, "uploads")
|
||||
os.makedirs(self.upload_dir, exist_ok=True)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _h(self, body=None, err=None):
|
||||
h = _handler(self.db_path, upload_dir=self.upload_dir)
|
||||
h._read_json = lambda: (body, err)
|
||||
return h
|
||||
|
||||
def _project(self, pid="p1", owner="u1", repo_url=None):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO projects(id, name, owner_id, created_at,"
|
||||
" repo_url, auto_pr) VALUES(?, ?, ?, ?, ?, 0)",
|
||||
(pid, "P", owner, time.time(), repo_url))
|
||||
c.commit()
|
||||
c.close()
|
||||
with _auth_store(self.db_path) as store:
|
||||
store.create_project(pid, owner)
|
||||
|
||||
def _member(self, pid, uid, role):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO project_members(project_id,"
|
||||
" user_id, role) VALUES(?, ?, ?)", (pid, uid, role))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _task(self, tid="t1", pid="p1", status="completed",
|
||||
created_by="u1", **extra):
|
||||
cols = ["id", "project_id", "prompt", "status", "created_by",
|
||||
"created_at", "updated_at"]
|
||||
vals = [tid, pid, "do work", status, created_by, time.time(),
|
||||
time.time()]
|
||||
for key, val in extra.items():
|
||||
cols.append(key)
|
||||
vals.append(val)
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO tasks(%s) VALUES(%s)"
|
||||
% (", ".join(cols), ", ".join("?" * len(cols))), vals)
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _env(self, eid="e1", pid="p1"):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO environments(id, project_id, name, branch,"
|
||||
" executor, status, error_text, repo_url, created_by,"
|
||||
" created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?,"
|
||||
" ?, ?, ?)",
|
||||
(eid, pid, "env", "main", "local", "ready", None,
|
||||
"https://example.com/r.git", "u1", time.time(),
|
||||
time.time()))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _last(self, h):
|
||||
self.assertTrue(h.sent, "handler sent no response")
|
||||
return h.sent[-1]
|
||||
|
||||
|
||||
class _ClosingConn:
|
||||
def __init__(self):
|
||||
self.conn = mock.Mock()
|
||||
self.conn.close.side_effect = RuntimeError("close down")
|
||||
|
||||
|
||||
class _ClosingStore:
|
||||
def __init__(self):
|
||||
self.conn = _ClosingConn().conn
|
||||
|
||||
def close(self):
|
||||
raise RuntimeError("close down")
|
||||
|
||||
|
||||
class EnvProjectMemberTestCase(_Base):
|
||||
def test_get_environment_check_error(self):
|
||||
self._project()
|
||||
self._env()
|
||||
h = self._h()
|
||||
h._get_environment("u9", "e1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 403)
|
||||
|
||||
def test_archive_environment_missing_404(self):
|
||||
h = self._h()
|
||||
h._archive_environment("u1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_archive_environment_check_error(self):
|
||||
self._project()
|
||||
self._env()
|
||||
h = self._h()
|
||||
h._archive_environment("u9", "e1")
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
def test_update_project_read_error(self):
|
||||
self._project()
|
||||
h = self._h(err="bad json")
|
||||
h._update_project("u1", "p1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_resolve_invite_user_id_must_be_string(self):
|
||||
h = self._h()
|
||||
target, status, err = h._resolve_invite_target({"user_id": 42})
|
||||
self.assertEqual(status, 400)
|
||||
self.assertIsNone(target)
|
||||
|
||||
def test_resolve_invite_username_must_be_string(self):
|
||||
h = self._h()
|
||||
target, status, err = h._resolve_invite_target({"username": 42})
|
||||
self.assertEqual(status, 400)
|
||||
self.assertIsNone(target)
|
||||
|
||||
def test_member_invite_project_missing_404(self):
|
||||
h = self._h(body={})
|
||||
h._project_member_invite("u1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_member_invite_read_error(self):
|
||||
self._project()
|
||||
h = self._h(err="bad json")
|
||||
h._project_member_invite("u1", "p1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_member_patch_project_missing_404(self):
|
||||
h = self._h(body={})
|
||||
h._project_member_patch("u1", "ghost", "u2")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_member_patch_read_error(self):
|
||||
self._project()
|
||||
h = self._h(err="bad json")
|
||||
h._project_member_patch("u1", "p1", "u2")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_member_patch_unknown_role_400(self):
|
||||
self._project()
|
||||
h = self._h(body={"role": "superuser"})
|
||||
h._project_member_patch("u1", "p1", "u2")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("unknown role", payload["error"])
|
||||
|
||||
def test_push_sub_delete_wrong_method_404(self):
|
||||
h = self._h()
|
||||
h._project_push_subscription_delete("u1", "GET", "p1",
|
||||
["p1", "push",
|
||||
"subscriptions", "s1"])
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def _sub(self, sid="s1", pid="p1", uid="u2"):
|
||||
from cloud.push import init_push_db
|
||||
init_push_db(self.db_path)
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO push_subscriptions(id, project_id, user_id,"
|
||||
" channel, target, preference, created_at)"
|
||||
" VALUES(?, ?, ?, ?, ?, ?, ?)",
|
||||
(sid, pid, uid, "webhook", "https://example.com/hook",
|
||||
"all", time.time()))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def test_push_sub_delete_missing_404(self):
|
||||
self._project()
|
||||
h = self._h()
|
||||
h._project_push_subscription_delete("u1", "DELETE", "p1",
|
||||
["p1", "push",
|
||||
"subscriptions", "ghost"])
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_push_sub_delete_forbidden_403(self):
|
||||
self._project(owner="owner1")
|
||||
self._member("p1", "u1", "contributor")
|
||||
self._sub()
|
||||
h = self._h()
|
||||
h._project_push_subscription_delete("u1", "DELETE", "p1",
|
||||
["p1", "push",
|
||||
"subscriptions", "s1"])
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
|
||||
class MemoryIdentityTestCase(_Base):
|
||||
def test_memory_create_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._memory_create("u1", "p1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_memory_create_goal_required(self):
|
||||
h = self._h(body={"conclusion": "done"})
|
||||
h._memory_create("u1", "p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("goal required", payload["error"])
|
||||
|
||||
def test_memory_create_close_failure_swallowed(self):
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = _ClosingConn().conn
|
||||
|
||||
def add_summary(self, *a, **k):
|
||||
return 99
|
||||
h = self._h(body={"goal": "g", "conclusion": "c"})
|
||||
with mock.patch("cloud.server.MemoryStore", _Store):
|
||||
h._memory_create("u1", "p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 201)
|
||||
self.assertEqual(payload["id"], 99)
|
||||
|
||||
def test_memory_list_close_failure_swallowed(self):
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = _ClosingConn().conn
|
||||
|
||||
def list_summaries(self, project_id):
|
||||
return []
|
||||
|
||||
def build_prompt_context(self, project_id, top_k=5):
|
||||
return ""
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server.MemoryStore", _Store):
|
||||
h._memory_list("p1", {})
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
def test_memory_supersede_close_failure_swallowed(self):
|
||||
from cloud.server import MemoryStore
|
||||
store = MemoryStore(self.db_path)
|
||||
try:
|
||||
mid = store.add_summary("p1", "", "g", "c")
|
||||
finally:
|
||||
store.conn.close()
|
||||
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = _ClosingConn().conn
|
||||
|
||||
def deprecate(self, mid):
|
||||
pass
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server.MemoryStore", _Store):
|
||||
h._memory_supersede("u1", "p1", str(mid))
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(payload["status"], "deprecated")
|
||||
|
||||
def test_identity_close_swallow(self):
|
||||
h = self._h()
|
||||
h._identity_close(_ClosingStore())
|
||||
h._auto_memory_close(_ClosingStore())
|
||||
|
||||
def test_identity_profile_upsert_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._identity_profile_upsert("u1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_identity_profile_upsert_bad_preferences_400(self):
|
||||
h = self._h(body={"preferences": "not-an-object"})
|
||||
h._identity_profile_upsert("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("must be an object", payload["error"])
|
||||
|
||||
def test_identity_user_memory_create_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._identity_user_memory_create("u1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_identity_user_memory_create_bad_kind_400(self):
|
||||
h = self._h(body={"kind": "bogus", "content": "x"})
|
||||
h._identity_user_memory_create("u1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_identity_user_memory_supersede_read_error_body_404(self):
|
||||
# Read error does not abort supersede: body defaults to {}
|
||||
# (4294) and the missing-memory check still answers 404.
|
||||
h = self._h(err="bad json")
|
||||
h._identity_user_memory_supersede("u1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_identity_user_memory_supersede_key_error_404(self):
|
||||
# Row check passes, then the supersede itself loses a race
|
||||
# (KeyError) -- the handler maps it to the same 404.
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = mock.Mock()
|
||||
|
||||
def get_memory(self, mem_id):
|
||||
return {"scope": "user", "owner_user_id": "u1"}
|
||||
|
||||
def supersede_memory(self, *a, **k):
|
||||
raise KeyError("gone")
|
||||
h = self._h(body={})
|
||||
with mock.patch("cloud.server.IdentityStore", _Store):
|
||||
h._identity_user_memory_supersede("u1", "m1")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_identity_user_memory_delete_key_error_404(self):
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = mock.Mock()
|
||||
|
||||
def get_memory(self, mem_id):
|
||||
return {"scope": "user", "owner_user_id": "u1"}
|
||||
|
||||
def delete_memory(self, mem_id, user_id):
|
||||
raise KeyError("gone")
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server.IdentityStore", _Store):
|
||||
h._identity_user_memory_delete("u1", "m1")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_identity_user_memory_supersede_bad_kind_400(self):
|
||||
h = self._h(body={"kind": "fact", "content": "likes tea"})
|
||||
h._identity_user_memory_create("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 201)
|
||||
mem_id = payload["id"]
|
||||
h2 = self._h(body={"replacement_kind": "bogus",
|
||||
"replacement_content": "x"})
|
||||
h2._identity_user_memory_supersede("u1", mem_id)
|
||||
self.assertEqual(self._last(h2)[0], 400)
|
||||
|
||||
def test_identity_user_memory_delete_missing_404(self):
|
||||
h = self._h()
|
||||
h._identity_user_memory_delete("u1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_identity_user_memory_delete_forbidden_404(self):
|
||||
h = self._h(body={"kind": "fact", "content": "likes tea"})
|
||||
h._identity_user_memory_create("u1")
|
||||
mem_id = self._last(h)[1]["id"]
|
||||
h2 = self._h()
|
||||
h2._identity_user_memory_delete("u2", mem_id)
|
||||
self.assertEqual(self._last(h2)[0], 404)
|
||||
|
||||
def test_identity_context_with_project(self):
|
||||
self._project()
|
||||
h = self._h()
|
||||
h._identity_context("u1", {"project_id": ["p1"]})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(payload["project_id"], "p1")
|
||||
|
||||
def test_identity_context_close_failure_swallowed(self):
|
||||
self._project()
|
||||
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = _ClosingConn().conn
|
||||
|
||||
def build_prompt_context(self, project_id):
|
||||
return ""
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server.MemoryStore", _Store):
|
||||
h._identity_context("u1", {"project_id": ["p1"]})
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
def test_auto_memory_settings_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._auto_memory_settings_upsert("u1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_auto_memory_settings_body_must_be_object(self):
|
||||
h = self._h(body=["not", "an", "object"])
|
||||
h._auto_memory_settings_upsert("u1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("must be an object", payload["error"])
|
||||
|
||||
def test_auto_memory_log_bad_limit(self):
|
||||
h = self._h()
|
||||
h._auto_memory_log("u1", {"limit": ["abc"]})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("limit must be an integer", payload["error"])
|
||||
|
||||
def test_task_auto_memory_flags_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._task_auto_memory_flags("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_task_auto_memory_flags_body_must_be_object(self):
|
||||
h = self._h(body=["x"])
|
||||
h._task_auto_memory_flags("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_task_auto_memory_flags_check_error(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(body={})
|
||||
h._task_auto_memory_flags("u9", "t1")
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
def test_project_identity_memory_create_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._project_identity_memory_create("u1", "p1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_project_identity_memories_list_bad_top_k(self):
|
||||
h = self._h()
|
||||
h._project_identity_memories_list("p1", {"top_k": ["abc"]})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("top_k must be an integer", payload["error"])
|
||||
|
||||
def test_project_identity_memory_supersede_none_body_404(self):
|
||||
h = self._h(body=None)
|
||||
h._project_identity_memory_supersede("u1", "p1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_project_identity_memory_supersede_read_error_body_404(self):
|
||||
# Same read-error defaulting as the user-memory variant.
|
||||
h = self._h(err="bad json")
|
||||
h._project_identity_memory_supersede("u1", "p1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_project_identity_memory_supersede_key_error_404(self):
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = mock.Mock()
|
||||
|
||||
def get_memory(self, mem_id):
|
||||
return {"scope": "project", "project_id": "p1"}
|
||||
|
||||
def supersede_memory(self, *a, **k):
|
||||
raise PermissionError("not yours")
|
||||
h = self._h(body={})
|
||||
with mock.patch("cloud.server.IdentityStore", _Store):
|
||||
h._project_identity_memory_supersede("u1", "p1", "m1")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_project_identity_memory_supersede_bad_kind_400(self):
|
||||
self._project()
|
||||
h = self._h(body={"kind": "fact", "content": "likes tea"})
|
||||
h._project_identity_memory_create("u1", "p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 201)
|
||||
mem_id = payload["id"]
|
||||
h2 = self._h(body={"replacement_kind": "bogus",
|
||||
"replacement_content": "x"})
|
||||
h2._project_identity_memory_supersede("u1", "p1", mem_id)
|
||||
self.assertEqual(self._last(h2)[0], 400)
|
||||
@@ -0,0 +1,648 @@
|
||||
"""Coverage batch v2 (D4b): cloud/server.py small handler clusters,
|
||||
second half -- costs/relay, task lists, followups, threads, files,
|
||||
browser helpers, peer messages, reviews, skills/apps/MCP lists.
|
||||
|
||||
Same stubbing discipline as D2-D4: the method under test is never
|
||||
stubbed; host-CLI collaborators (_run_host_cli, _extract_*) are
|
||||
stubbed only to drive the handler's own error mapping.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
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))
|
||||
|
||||
from cloud.server import (
|
||||
CloudControlPlaneHandler,
|
||||
_auth_store,
|
||||
init_db,
|
||||
)
|
||||
from cloud.sub2api import Sub2apiError as _Sub2apiError
|
||||
|
||||
|
||||
def _handler(db_path, upload_dir=None, tenants_root=None):
|
||||
h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler)
|
||||
h.server = SimpleNamespace(db_path=db_path, upload_dir=upload_dir,
|
||||
tenants_root=tenants_root)
|
||||
sent = []
|
||||
h._send_json = lambda status, obj, extra_headers=None: sent.append(
|
||||
(status, obj))
|
||||
h.sent = sent
|
||||
return h
|
||||
|
||||
|
||||
def _conn(db_path):
|
||||
c = sqlite3.connect(db_path)
|
||||
c.row_factory = sqlite3.Row
|
||||
return c
|
||||
|
||||
|
||||
class _ProxyConn:
|
||||
def __init__(self, real, fail_select_substr=None, fail_exc=None):
|
||||
self._real = real
|
||||
self._fail_select_substr = fail_select_substr
|
||||
self._fail_exc = fail_exc
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
text = sql.strip()
|
||||
if (self._fail_select_substr and text.startswith("SELECT")
|
||||
and self._fail_select_substr in text):
|
||||
raise self._fail_exc
|
||||
return self._real.execute(sql, params)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
|
||||
|
||||
class _Base(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "test.db")
|
||||
init_db(self.db_path)
|
||||
self.upload_dir = os.path.join(self.tmp.name, "uploads")
|
||||
os.makedirs(self.upload_dir, exist_ok=True)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _h(self, body=None, err=None):
|
||||
h = _handler(self.db_path, upload_dir=self.upload_dir,
|
||||
tenants_root=self.tmp.name)
|
||||
h._read_json = lambda: (body, err)
|
||||
return h
|
||||
|
||||
def _project(self, pid="p1", owner="u1", repo_url=None):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO projects(id, name, owner_id, created_at,"
|
||||
" repo_url, auto_pr) VALUES(?, ?, ?, ?, ?, 0)",
|
||||
(pid, "P", owner, time.time(), repo_url))
|
||||
c.commit()
|
||||
c.close()
|
||||
with _auth_store(self.db_path) as store:
|
||||
store.create_project(pid, owner)
|
||||
|
||||
def _member(self, pid, uid, role):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO project_members(project_id,"
|
||||
" user_id, role) VALUES(?, ?, ?)", (pid, uid, role))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _task(self, tid="t1", pid="p1", status="completed",
|
||||
created_by="u1", **extra):
|
||||
cols = ["id", "project_id", "prompt", "status", "created_by",
|
||||
"created_at", "updated_at"]
|
||||
vals = [tid, pid, "do work", status, created_by, time.time(),
|
||||
time.time()]
|
||||
for key, val in extra.items():
|
||||
cols.append(key)
|
||||
vals.append(val)
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO tasks(%s) VALUES(%s)"
|
||||
% (", ".join(cols), ", ".join("?" * len(cols))), vals)
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _last(self, h):
|
||||
self.assertTrue(h.sent, "handler sent no response")
|
||||
return h.sent[-1]
|
||||
|
||||
|
||||
class _ClosingConn:
|
||||
def __init__(self):
|
||||
self.conn = mock.Mock()
|
||||
self.conn.close.side_effect = RuntimeError("close down")
|
||||
|
||||
|
||||
class CostRelayTestCase(_Base):
|
||||
def test_kb_search_q_required(self):
|
||||
h = self._h()
|
||||
h._kb_search("p1", {})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("q required", payload["error"])
|
||||
|
||||
def test_kb_search_bad_top_k(self):
|
||||
h = self._h()
|
||||
h._kb_search("p1", {"q": ["x"], "top_k": ["abc"]})
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_kb_search_close_failure_swallowed(self):
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = _ClosingConn().conn
|
||||
|
||||
def search(self, project_id, q, top_k=5):
|
||||
return []
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server.KBStore", _Store):
|
||||
h._kb_search("p1", {"q": ["x"]})
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
def test_project_cost_bad_days(self):
|
||||
h = self._h()
|
||||
h._project_cost("p1", {"days": ["abc"]})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("days must be an integer", payload["error"])
|
||||
|
||||
def test_project_cost_close_failure_swallowed(self):
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self._conn = mock.Mock()
|
||||
self._conn.close.side_effect = RuntimeError("down")
|
||||
|
||||
def dual_column_report_by_project(self, project_id):
|
||||
return {}
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server.CostStore", _Store):
|
||||
h._project_cost("p1", {})
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
def test_task_cost_close_failure_swallowed(self):
|
||||
self._project()
|
||||
self._task()
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self._conn = mock.Mock()
|
||||
self._conn.close.side_effect = RuntimeError("down")
|
||||
|
||||
def dual_column_report_by_task(self, task_id):
|
||||
return {}
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server.CostStore", _Store):
|
||||
h._task_cost("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
def test_relay_models_sub2api_generic_error_502(self):
|
||||
h = self._h()
|
||||
with mock.patch.dict(os.environ, {"CLOUD_GATEWAY": "sub2api"}), \
|
||||
mock.patch("cloud.server._validate_sub2api_key",
|
||||
side_effect=RuntimeError("net down")):
|
||||
h._relay_models("u1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_relay_models_sub2api_list_error_502(self):
|
||||
h = self._h()
|
||||
with mock.patch.dict(os.environ, {"CLOUD_GATEWAY": "sub2api"}), \
|
||||
mock.patch("cloud.server._validate_sub2api_key",
|
||||
return_value=("ok", "key", "sub", "kid")), \
|
||||
mock.patch("cloud.server._list_sub2api_models",
|
||||
side_effect=RuntimeError("gw down")):
|
||||
h._relay_models("u1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_models_overview_no_memberships_200(self):
|
||||
self._project()
|
||||
h = self._h()
|
||||
h._models_overview("u9")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(payload["model_stats"], [])
|
||||
|
||||
def test_models_overview_close_failure_swallowed(self):
|
||||
self._project()
|
||||
conn = mock.Mock()
|
||||
conn.execute.return_value.fetchall.return_value = []
|
||||
conn.close.side_effect = RuntimeError("down")
|
||||
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self._conn = conn
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server.CostStore", _Store):
|
||||
h._models_overview("u1")
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
|
||||
class ListsFollowupsTestCase(_Base):
|
||||
def test_list_tasks_project_required(self):
|
||||
h = self._h()
|
||||
h._list_tasks("u1", {})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("project_id required", payload["error"])
|
||||
|
||||
def test_list_tasks_executor_filter_skips(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._list_tasks("u1", {"project_id": ["p1"],
|
||||
"executor": ["docker"]})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(payload["tasks"], [])
|
||||
|
||||
def test_goal_convert_queued_none(self):
|
||||
self._project()
|
||||
self._task(status="completed")
|
||||
h = self._h()
|
||||
self.assertIsNone(h._goal_convert_queued("t1", "做目标"))
|
||||
|
||||
def test_goal_revive_terminal_none(self):
|
||||
self._project()
|
||||
self._task(status="completed", resume_message="old")
|
||||
h = self._h()
|
||||
self.assertIsNone(h._goal_revive_terminal("t1", "做目标"))
|
||||
|
||||
def test_task_agents_remote_host_200(self):
|
||||
self._project()
|
||||
self._task(worktree_host="other-host")
|
||||
h = self._h()
|
||||
h._task_agents("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(payload["agents"], [])
|
||||
|
||||
def test_create_followup_convert_race_409(self):
|
||||
self._project()
|
||||
self._task(status="queued")
|
||||
h = self._h(body={"body": "/goal 做目标"})
|
||||
h._goal_convert_queued = lambda tid, target: None
|
||||
h._create_followup("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 409)
|
||||
self.assertIn("状态已变化", payload["error"])
|
||||
|
||||
def test_create_followup_revive_race_409(self):
|
||||
self._project()
|
||||
self._task(status="completed")
|
||||
h = self._h(body={"body": "/goal 做目标"})
|
||||
h._goal_revive_terminal = lambda tid, target: None
|
||||
h._create_followup("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 409)
|
||||
self.assertIn("状态已变化", payload["error"])
|
||||
|
||||
def test_append_direct_trace_remote_host_returns(self):
|
||||
h = self._h()
|
||||
h._append_direct_trace({"id": "t1", "project_id": "p1",
|
||||
"worktree_host": "other-host"},
|
||||
"fu1", "hi")
|
||||
|
||||
def test_append_direct_trace_tenant_dir(self):
|
||||
tenant_dir = os.path.join(self.tmp.name, "p1", "work",
|
||||
"host-traces")
|
||||
os.makedirs(tenant_dir, exist_ok=True)
|
||||
open(os.path.join(tenant_dir, "t1.jsonl"), "w").close()
|
||||
h = self._h()
|
||||
h._append_direct_trace({"id": "t1", "project_id": "p1"},
|
||||
"fu1", "hello tenant")
|
||||
with open(os.path.join(tenant_dir, "t1.jsonl")) as f:
|
||||
content = f.read()
|
||||
self.assertIn("followup-direct", content)
|
||||
|
||||
def test_append_direct_trace_write_failure_swallowed(self):
|
||||
h = self._h()
|
||||
with mock.patch("os.makedirs",
|
||||
side_effect=OSError("ro fs")):
|
||||
h._append_direct_trace({"id": "t1", "project_id": "p1"},
|
||||
"fu1", "hi")
|
||||
|
||||
def test_list_followups_missing_404(self):
|
||||
h = self._h()
|
||||
h._list_followups("u1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_list_followups_forbidden_403(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._list_followups("u9", "t1")
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
def test_task_timeline_forbidden_403(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._task_timeline("u9", "t1")
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
def test_task_files_200_cap(self):
|
||||
self._project()
|
||||
self._task()
|
||||
worktree = os.path.join(self.tmp.name, "p1", "work",
|
||||
"host-worktrees", "t1")
|
||||
os.makedirs(worktree, exist_ok=True)
|
||||
for i in range(201):
|
||||
with open(os.path.join(worktree, "f%03d.txt" % i), "w") as f:
|
||||
f.write("x")
|
||||
h = self._h()
|
||||
h._task_files("u1", "t1", {})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(len(payload["entries"]), 200)
|
||||
|
||||
def test_browser_worktree_for_cloud_409(self):
|
||||
self._project()
|
||||
self._task(executor="cloud")
|
||||
h = self._h()
|
||||
worktree, err = h._browser_worktree_for("u1", "t1")
|
||||
self.assertIsNone(worktree)
|
||||
self.assertEqual(err[0], 409)
|
||||
|
||||
def test_browser_body_dict_none_and_bad(self):
|
||||
h = self._h()
|
||||
self.assertIsNone(h._browser_body_dict(None))
|
||||
self.assertIsNone(h._browser_body_dict(b"not json"))
|
||||
self.assertEqual(h._browser_body_dict({"a": 1}), {"a": 1})
|
||||
|
||||
def test_browser_input_worktree_error(self):
|
||||
h = self._h()
|
||||
h._browser_worktree_for = lambda *a: (None, (404, "nope"))
|
||||
h._browser_input("u1", "t1", {})
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_browser_input_state_error(self):
|
||||
h = self._h()
|
||||
h._browser_worktree_for = lambda *a: (self.tmp.name, None)
|
||||
with mock.patch("cloud.playwright_remote.read_state",
|
||||
return_value=(None, (502, "no session"))):
|
||||
h._browser_input("u1", "t1", {})
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_browser_input_write_os_error_503(self):
|
||||
h = self._h()
|
||||
h._browser_worktree_for = lambda *a: (self.tmp.name, None)
|
||||
with mock.patch("cloud.playwright_remote.read_state",
|
||||
return_value=({"url": ""}, None)), \
|
||||
mock.patch("cloud.playwright_remote.effective_holder",
|
||||
return_value="user"), \
|
||||
mock.patch("cloud.playwright_remote.append_input",
|
||||
side_effect=OSError("unreachable")):
|
||||
h._browser_input("u1", "t1", {"type": "type", "text": "hi"})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 503)
|
||||
self.assertIn("browser input unreachable", payload["error"])
|
||||
|
||||
def test_chat_history_forbidden_403(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._task_chat_history("u9", "t1")
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
def test_chat_history_legacy_query_failure_none(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._trace_snapshot = lambda task: ([], None)
|
||||
h._db = lambda: _ProxyConn(
|
||||
_conn(self.db_path),
|
||||
fail_select_substr="FROM audit_log",
|
||||
fail_exc=RuntimeError("audit down"))
|
||||
h._task_chat_history("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIsNone(payload["legacy_continue_count"])
|
||||
|
||||
def test_peer_message_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._create_peer_message("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_peer_message_sender_forbidden_403(self):
|
||||
self._project()
|
||||
self._project(pid="p2", owner="u2")
|
||||
self._task(tid="t1", pid="p1")
|
||||
self._task(tid="t2", pid="p2", created_by="u2")
|
||||
h = self._h(body={"from_task_id": "t2", "body": "hi"})
|
||||
h._create_peer_message("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 403)
|
||||
|
||||
def test_peer_message_body_too_long(self):
|
||||
self._project()
|
||||
self._task(tid="t1", pid="p1")
|
||||
self._task(tid="t2", pid="p1")
|
||||
h = self._h(body={"from_task_id": "t2", "body": "x" * 20001})
|
||||
h._create_peer_message("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("body too long", payload["error"])
|
||||
|
||||
def test_list_peer_messages_missing_404(self):
|
||||
h = self._h()
|
||||
h._list_peer_messages("u1", "ghost")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_create_review_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._create_review("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_update_review_findings_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._update_review_findings("u1", "t1", "r1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_update_review_findings_missing_404(self):
|
||||
h = self._h(body={})
|
||||
h._update_review_findings("u1", "ghost", "r1")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
|
||||
class ThreadListsTestCase(_Base):
|
||||
def test_thread_guard_docker_409(self):
|
||||
h = self._h()
|
||||
code, msg = h._thread_task_guard({"executor": "docker"})
|
||||
self.assertEqual(code, 409)
|
||||
|
||||
def test_thread_guard_remote_host_409(self):
|
||||
h = self._h()
|
||||
code, msg = h._thread_task_guard(
|
||||
{"executor": None, "worktree_host": "other-host"})
|
||||
self.assertEqual(code, 409)
|
||||
|
||||
def test_list_threads_no_tenant_home_502(self):
|
||||
h = self._h()
|
||||
threads, home, cwd, err, provider = h._list_task_threads(
|
||||
{"id": "t1", "project_id": None})
|
||||
self.assertEqual(err[0], 502)
|
||||
|
||||
def test_list_threads_extract_none_502(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(True, "garbage")), \
|
||||
mock.patch("cloud.server._extract_thread_list",
|
||||
return_value=None):
|
||||
threads, home, cwd, err, provider = h._list_task_threads(
|
||||
{"id": "t1", "project_id": "p1"})
|
||||
self.assertEqual(err[0], 502)
|
||||
self.assertIn("no thread list", err[1])
|
||||
|
||||
def test_task_thread_guard_409(self):
|
||||
self._project()
|
||||
self._task(executor="docker")
|
||||
h = self._h()
|
||||
h._task_thread("u1", "t1", {})
|
||||
self.assertEqual(self._last(h)[0], 409)
|
||||
|
||||
def test_task_thread_cli_failure_502(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._resolve_task_thread = lambda task: (
|
||||
{"id": "th1"}, "/tmp", "/tmp", None, None)
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(False, "host down")):
|
||||
h._task_thread("u1", "t1", {})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 502)
|
||||
self.assertIn("host down", payload["error"])
|
||||
|
||||
def test_task_thread_no_thread_502(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._resolve_task_thread = lambda task: (
|
||||
{"id": "th1"}, "/tmp", "/tmp", None, None)
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(True, "garbage")):
|
||||
h._task_thread("u1", "t1", {})
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 502)
|
||||
self.assertIn("no thread", payload["error"])
|
||||
|
||||
def test_task_thread_mutate_task_error(self):
|
||||
h = self._h()
|
||||
h._task_thread_mutate("u1", "ghost", ["archive"],
|
||||
"thread_archived", "detail")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_task_thread_rename_read_error(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(err="bad json")
|
||||
h._task_thread_rename("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_task_thread_rename_name_too_long(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(body={"name": "x" * 201})
|
||||
h._task_thread_rename("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("name too long", payload["error"])
|
||||
|
||||
def test_project_host_home_none(self):
|
||||
h = self._h()
|
||||
self.assertEqual(h._project_host_home(None), (None, None))
|
||||
|
||||
def _list_case(self, method):
|
||||
h = self._h()
|
||||
method(h, None)
|
||||
return h
|
||||
|
||||
def test_skills_list_home_none_502(self):
|
||||
h = self._h()
|
||||
h._skills_list(None)
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_skills_list_cli_failure_502(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(False, "host down")):
|
||||
h._skills_list("p1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_skills_list_no_list_502(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(True, "garbage")):
|
||||
h._skills_list("p1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_apps_list_home_none_502(self):
|
||||
h = self._h()
|
||||
h._apps_list(None)
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_apps_list_cli_failure_502(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(False, "host down")):
|
||||
h._apps_list("p1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_apps_list_no_list_502(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(True, "garbage")):
|
||||
h._apps_list("p1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_mcp_list_home_none_502(self):
|
||||
h = self._h()
|
||||
h._mcp_servers_list(None)
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_mcp_list_cli_failure_502(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(False, "host down")):
|
||||
h._mcp_servers_list("p1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_mcp_list_no_list_502(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(True, "garbage")):
|
||||
h._mcp_servers_list("p1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_mcp_list_next_cursor(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(True, {"servers": [],
|
||||
"nextCursor": "n1"})):
|
||||
h._mcp_servers_list("p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(payload["nextCursor"], "n1")
|
||||
|
||||
def test_mcp_reload_home_none_502(self):
|
||||
h = self._h()
|
||||
h._mcp_servers_reload("u1", None)
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_mcp_reload_cli_failure_502(self):
|
||||
h = self._h()
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(False, "host down")):
|
||||
h._mcp_servers_reload("u1", "p1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
|
||||
def test_mcp_oauth_login_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._mcp_server_oauth_login("u1", "p1", "srv")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_mcp_oauth_login_timeout_argv(self):
|
||||
h = self._h(body={"timeoutSecs": 30})
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(True, {"authorizationUrl": "u",
|
||||
"loginId": "l"})) as cli:
|
||||
h._mcp_server_oauth_login("u1", "p1", "srv")
|
||||
argv = cli.call_args[0][0]
|
||||
self.assertIn("--timeout-secs", argv)
|
||||
self.assertEqual(self._last(h)[0], 200)
|
||||
|
||||
def test_mcp_oauth_login_cli_failure_502(self):
|
||||
h = self._h(body={})
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(False, "host down")):
|
||||
h._mcp_server_oauth_login("u1", "p1", "srv")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,615 @@
|
||||
"""Coverage batch v2 (D7): cloud/server.py module-level helpers and
|
||||
last small handler branches -- rate limiter, heartbeat, quota helpers,
|
||||
plan-params parsing, kill requests, gate validation, thread guards,
|
||||
oauth/github-app leftovers, todos/automation error mappings.
|
||||
|
||||
Discipline as before: the function under test is never stubbed.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from collections import deque
|
||||
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))
|
||||
|
||||
from cloud.server import (
|
||||
CloudControlPlaneHandler,
|
||||
RateLimiter,
|
||||
_apply_default_quota,
|
||||
_auth_store,
|
||||
_backfill_default_quotas,
|
||||
_classify_goal_followup,
|
||||
_deployed_commit,
|
||||
_default_executor,
|
||||
_full_auto_installation_verified,
|
||||
_full_auto_parse_plan_params,
|
||||
_make_rate_limiter,
|
||||
_read_agent_entries,
|
||||
_read_agent_entries_multi,
|
||||
_resolve_shared_home_mcp,
|
||||
_run_host_cli,
|
||||
_task_to_dict,
|
||||
_upload_dir_for,
|
||||
_validate_hard_gate_body,
|
||||
_worker_heartbeat_status,
|
||||
init_db,
|
||||
request_kill,
|
||||
start_rate_limit_sweeper,
|
||||
sweep_rate_limit_hits,
|
||||
DbRateLimiter,
|
||||
)
|
||||
from cloud.server import _auth_store as _auth_store_cm
|
||||
|
||||
|
||||
def _handler(db_path, upload_dir=None, tenants_root=None):
|
||||
h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler)
|
||||
h.server = SimpleNamespace(db_path=db_path, upload_dir=upload_dir,
|
||||
tenants_root=tenants_root)
|
||||
sent = []
|
||||
h._send_json = lambda status, obj, extra_headers=None: sent.append(
|
||||
(status, obj))
|
||||
html = []
|
||||
h._send_html = lambda status, body: html.append((status, body))
|
||||
h.sent = sent
|
||||
h.html = html
|
||||
return h
|
||||
|
||||
|
||||
def _conn(db_path):
|
||||
c = sqlite3.connect(db_path)
|
||||
c.row_factory = sqlite3.Row
|
||||
return c
|
||||
|
||||
|
||||
class _Base(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "test.db")
|
||||
init_db(self.db_path)
|
||||
self.upload_dir = os.path.join(self.tmp.name, "uploads")
|
||||
os.makedirs(self.upload_dir, exist_ok=True)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _h(self, body=None, err=None):
|
||||
h = _handler(self.db_path, upload_dir=self.upload_dir,
|
||||
tenants_root=self.tmp.name)
|
||||
h._read_json = lambda: (body, err)
|
||||
return h
|
||||
|
||||
def _project(self, pid="p1", owner="u1", repo_url=None):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO projects(id, name, owner_id, created_at,"
|
||||
" repo_url, auto_pr) VALUES(?, ?, ?, ?, ?, 0)",
|
||||
(pid, "P", owner, time.time(), repo_url))
|
||||
c.commit()
|
||||
c.close()
|
||||
with _auth_store(self.db_path) as store:
|
||||
store.create_project(pid, owner)
|
||||
|
||||
def _task(self, tid="t1", pid="p1", status="completed",
|
||||
created_by="u1", **extra):
|
||||
cols = ["id", "project_id", "prompt", "status", "created_by",
|
||||
"created_at", "updated_at"]
|
||||
vals = [tid, pid, "do work", status, created_by, time.time(),
|
||||
time.time()]
|
||||
for key, val in extra.items():
|
||||
cols.append(key)
|
||||
vals.append(val)
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO tasks(%s) VALUES(%s)"
|
||||
% (", ".join(cols), ", ".join("?" * len(cols))), vals)
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _last(self, h):
|
||||
self.assertTrue(h.sent, "handler sent no response")
|
||||
return h.sent[-1]
|
||||
|
||||
|
||||
class HelpersTestCase(_Base):
|
||||
def test_run_host_cli_timeout(self):
|
||||
fake = os.path.join(self.tmp.name, "host.py")
|
||||
with open(fake, "w") as f:
|
||||
f.write("# fake\n")
|
||||
import subprocess as _sp
|
||||
with mock.patch("cloud.server._host_py_path",
|
||||
return_value=fake), \
|
||||
mock.patch("cloud.server.subprocess.run",
|
||||
side_effect=_sp.TimeoutExpired("host.py", 5)):
|
||||
ok, err = _run_host_cli(["x"], "/tmp")
|
||||
self.assertFalse(ok)
|
||||
self.assertEqual(err, "host.py timed out")
|
||||
|
||||
def test_run_host_cli_os_error(self):
|
||||
fake = os.path.join(self.tmp.name, "host.py")
|
||||
with open(fake, "w") as f:
|
||||
f.write("# fake\n")
|
||||
with mock.patch("cloud.server._host_py_path",
|
||||
return_value=fake), \
|
||||
mock.patch("cloud.server.subprocess.run",
|
||||
side_effect=OSError("no exec")):
|
||||
ok, err = _run_host_cli(["x"], "/tmp")
|
||||
self.assertFalse(ok)
|
||||
self.assertIn("cannot run host.py", err)
|
||||
|
||||
def test_shared_home_tenants_root_raises(self):
|
||||
with mock.patch("cloud.server._local_worker_codex_home",
|
||||
return_value=None), \
|
||||
mock.patch("cloud.server._tenants_root_for",
|
||||
side_effect=RuntimeError("no root")):
|
||||
self.assertIsNone(_resolve_shared_home_mcp(None))
|
||||
|
||||
def test_shared_home_tenants_root_empty(self):
|
||||
with mock.patch("cloud.server._local_worker_codex_home",
|
||||
return_value=None), \
|
||||
mock.patch("cloud.server._tenants_root_for",
|
||||
return_value=""):
|
||||
self.assertIsNone(_resolve_shared_home_mcp(None))
|
||||
|
||||
def test_read_agent_entries_mixed_lines(self):
|
||||
path = os.path.join(self.tmp.name, "agents.jsonl")
|
||||
with open(path, "w") as f:
|
||||
f.write("\n")
|
||||
f.write("not json\n")
|
||||
f.write("[1, 2]\n")
|
||||
f.write('{"thread": "t", "nope": 1}\n')
|
||||
f.write('{"thread": "t", "id": "a", "state": "start"}\n')
|
||||
f.write('{"thread": "t", "id": "a", "state": "done"}\n')
|
||||
entries = _read_agent_entries(path)
|
||||
self.assertEqual(len(entries), 1)
|
||||
self.assertEqual(entries[0]["state"], "done")
|
||||
self.assertEqual(_read_agent_entries(
|
||||
os.path.join(self.tmp.name, "missing.jsonl")), [])
|
||||
|
||||
def test_read_agent_entries_multi_overlap(self):
|
||||
p1 = os.path.join(self.tmp.name, "a1.jsonl")
|
||||
p2 = os.path.join(self.tmp.name, "a2.jsonl")
|
||||
with open(p1, "w") as f:
|
||||
f.write('{"thread": "t", "id": "a", "v": 1}\n')
|
||||
with open(p2, "w") as f:
|
||||
f.write('{"thread": "t", "id": "a", "v": 2}\n')
|
||||
entries = _read_agent_entries_multi([p1, p2])
|
||||
self.assertEqual(len(entries), 1)
|
||||
self.assertEqual(entries[0]["v"], 2)
|
||||
|
||||
def test_rate_limiter_key_cleanup(self):
|
||||
limiter = RateLimiter(per_minute=100, window_seconds=60,
|
||||
time_fn=lambda: 1000.0)
|
||||
limiter._hits = {("k%d" % i): deque() for i in range(10001)}
|
||||
self.assertTrue(limiter.allow("new"))
|
||||
self.assertEqual(len(limiter._hits), 10001)
|
||||
|
||||
def test_db_limiter_ensure_twice(self):
|
||||
limiter = DbRateLimiter(self.db_path)
|
||||
c = _conn(self.db_path)
|
||||
limiter._ensure_table(c)
|
||||
limiter._ensure_table(c)
|
||||
c.close()
|
||||
self.assertTrue(limiter._ensured)
|
||||
|
||||
def test_db_limiter_ensure_inner_return(self):
|
||||
# Inner early return needs a thread that passed the outer
|
||||
# check, then blocks on the lock while the main thread flips
|
||||
# _ensured: deterministic, no timing luck beyond the block.
|
||||
limiter = DbRateLimiter(self.db_path)
|
||||
limiter._ensured = False
|
||||
c = _conn(self.db_path)
|
||||
limiter._ensure_lock.acquire()
|
||||
done = []
|
||||
thread = threading.Thread(
|
||||
target=lambda: done.append(limiter._ensure_table(c)))
|
||||
thread.start()
|
||||
time.sleep(0.2)
|
||||
limiter._ensured = True
|
||||
limiter._ensure_lock.release()
|
||||
thread.join(timeout=5)
|
||||
self.assertFalse(thread.is_alive())
|
||||
c.close()
|
||||
|
||||
def test_make_rate_limiter_postgres(self):
|
||||
with mock.patch.dict(os.environ, {"CLOUD_DB_BACKEND": "postgres"}):
|
||||
limiter = _make_rate_limiter(self.db_path)
|
||||
self.assertIsInstance(limiter, DbRateLimiter)
|
||||
|
||||
def test_sweep_postgres_no_lock(self):
|
||||
conn = mock.Mock()
|
||||
conn.execute.return_value.fetchone.return_value = (0,)
|
||||
with mock.patch.dict(os.environ, {"CLOUD_DB_BACKEND": "postgres"}), \
|
||||
mock.patch("cloud.server._db_connect",
|
||||
return_value=conn):
|
||||
self.assertFalse(sweep_rate_limit_hits(self.db_path, 60))
|
||||
|
||||
def test_sweeper_loop_runs_and_dies(self):
|
||||
limiter = DbRateLimiter(self.db_path)
|
||||
with mock.patch("time.sleep",
|
||||
side_effect=[None, RuntimeError("stop")]), \
|
||||
mock.patch("cloud.server.sweep_rate_limit_hits",
|
||||
side_effect=RuntimeError("sweep down")):
|
||||
thread = start_rate_limit_sweeper(limiter, self.db_path)
|
||||
self.assertIsNotNone(thread)
|
||||
thread.join(timeout=5)
|
||||
self.assertFalse(thread.is_alive())
|
||||
|
||||
def test_deployed_commit_missing_file(self):
|
||||
self.assertIsNone(_deployed_commit())
|
||||
|
||||
def test_deployed_commit_success_path(self):
|
||||
# Success path credits 1088/1091 in coverage's line map;
|
||||
# the file lives in the repo root only for this test.
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
target = root / "DEPLOYED_COMMIT"
|
||||
self.assertFalse(target.exists(),
|
||||
"DEPLOYED_COMMIT must not pre-exist")
|
||||
try:
|
||||
target.write_text(" abc123\n", encoding="utf-8")
|
||||
self.assertEqual(_deployed_commit(), "abc123")
|
||||
finally:
|
||||
target.unlink(missing_ok=True)
|
||||
|
||||
def test_worker_heartbeat_bad_override(self):
|
||||
hb = os.path.join(self.tmp.name, "hb.json")
|
||||
with open(hb, "w") as f:
|
||||
json.dump({"last_seen": time.time()}, f)
|
||||
server = SimpleNamespace(
|
||||
db_path=None, worker_heartbeat_max_age_seconds="bogus",
|
||||
worker_heartbeat_file=hb)
|
||||
age, ok = _worker_heartbeat_status(server)
|
||||
self.assertTrue(ok)
|
||||
|
||||
def test_worker_heartbeat_bad_env(self):
|
||||
hb = os.path.join(self.tmp.name, "hb.json")
|
||||
with open(hb, "w") as f:
|
||||
json.dump({"last_seen": time.time()}, f)
|
||||
server = SimpleNamespace(db_path=None, worker_heartbeat_file=hb)
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"CLOUD_WORKER_HEARTBEAT_MAX_AGE_SECONDS": "bogus"}):
|
||||
age, ok = _worker_heartbeat_status(server)
|
||||
self.assertTrue(ok)
|
||||
|
||||
def test_upload_dir_fallback(self):
|
||||
d = _upload_dir_for(SimpleNamespace())
|
||||
self.assertIsInstance(d, str)
|
||||
self.assertTrue(d)
|
||||
|
||||
def test_default_executor_local(self):
|
||||
with mock.patch.dict(os.environ,
|
||||
{"CLOUD_DEFAULT_EXECUTOR": "local"}):
|
||||
self.assertEqual(_default_executor(), "local")
|
||||
|
||||
def test_apply_default_quota_commit_failure(self):
|
||||
c = _conn(self.db_path)
|
||||
|
||||
class _CommitFail:
|
||||
def __init__(self, real):
|
||||
self._real = real
|
||||
|
||||
def execute(self, *a, **k):
|
||||
return self._real.execute(*a, **k)
|
||||
|
||||
def commit(self):
|
||||
raise RuntimeError("commit down")
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
self.assertTrue(_apply_default_quota(_CommitFail(c), "u1"))
|
||||
c.close()
|
||||
|
||||
def test_installation_verified_config_none(self):
|
||||
c = _conn(self.db_path)
|
||||
with mock.patch("cloud.server.get_app_config",
|
||||
return_value=None):
|
||||
self.assertFalse(_full_auto_installation_verified(c))
|
||||
c.close()
|
||||
|
||||
def test_installation_verified_config_error(self):
|
||||
c = _conn(self.db_path)
|
||||
with mock.patch("cloud.server.get_app_config",
|
||||
side_effect=RuntimeError("config down")):
|
||||
self.assertFalse(_full_auto_installation_verified(c))
|
||||
c.close()
|
||||
|
||||
def test_installation_verified_repo(self):
|
||||
c = _conn(self.db_path)
|
||||
with mock.patch("cloud.server.get_app_config",
|
||||
return_value={"id": 1}), \
|
||||
mock.patch("cloud.server.find_installation_for_repo",
|
||||
return_value={"id": 9}):
|
||||
self.assertTrue(
|
||||
_full_auto_installation_verified(c, "o/r"))
|
||||
c.close()
|
||||
|
||||
def test_installation_verified_any(self):
|
||||
c = _conn(self.db_path)
|
||||
with mock.patch("cloud.server.get_app_config",
|
||||
return_value={"id": 1}), \
|
||||
mock.patch("cloud.server.list_installations",
|
||||
return_value=[{"id": 9}]):
|
||||
self.assertTrue(_full_auto_installation_verified(c))
|
||||
c.close()
|
||||
|
||||
def test_plan_params_checks_string(self):
|
||||
params, err = _full_auto_parse_plan_params(
|
||||
{}, {"repo": ["o/r"], "required_checks": ["a, b"]})
|
||||
self.assertIsNone(err)
|
||||
self.assertEqual(params["required_checks"], ("a", "b"))
|
||||
|
||||
def test_plan_params_checks_list(self):
|
||||
params, err = _full_auto_parse_plan_params(
|
||||
{"repo": "o/r", "required_checks": ["x", "y"]}, {})
|
||||
self.assertIsNone(err)
|
||||
self.assertEqual(params["required_checks"], ("x", "y"))
|
||||
|
||||
def test_plan_params_checks_bad_type(self):
|
||||
params, err = _full_auto_parse_plan_params(
|
||||
{"repo": "o/r", "required_checks": 5}, {})
|
||||
self.assertIsNone(params)
|
||||
self.assertIn("must be a list or string", err)
|
||||
|
||||
def test_plan_params_org_bad_type(self):
|
||||
params, err = _full_auto_parse_plan_params(
|
||||
{"repo": "o/r", "org": 123}, {})
|
||||
self.assertIsNone(params)
|
||||
self.assertIn("org must be a string", err)
|
||||
|
||||
def test_backfill_rollback_failure_zero(self):
|
||||
c = _conn(self.db_path)
|
||||
|
||||
class _Failing:
|
||||
def __init__(self, real):
|
||||
self._real = real
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
if "INSERT OR IGNORE INTO user_quotas" in sql:
|
||||
raise RuntimeError("insert down")
|
||||
return self._real.execute(sql, params)
|
||||
|
||||
def rollback(self):
|
||||
raise RuntimeError("rollback down")
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
self.assertEqual(_backfill_default_quotas(_Failing(c)), 0)
|
||||
c.close()
|
||||
|
||||
def test_auth_store_close_failure_swallowed(self):
|
||||
class _FakeStore:
|
||||
def __init__(self, db_path):
|
||||
self.conn = mock.Mock()
|
||||
self.conn.close.side_effect = RuntimeError("down")
|
||||
with mock.patch("cloud.server.AuthStore", _FakeStore):
|
||||
with _auth_store_cm(self.db_path) as store:
|
||||
self.assertIsNotNone(store)
|
||||
|
||||
def _row(self, depends_on):
|
||||
return {
|
||||
"id": "t1", "project_id": "p1", "prompt": "p",
|
||||
"status": "completed", "idempotency_key": None,
|
||||
"depends_on": depends_on, "priority": 0,
|
||||
"created_by": "u1", "created_at": 1.0, "updated_at": 2.0,
|
||||
"last_heartbeat": None,
|
||||
}
|
||||
|
||||
def test_task_to_dict_bad_depends_on(self):
|
||||
d = _task_to_dict(self._row("not json"))
|
||||
self.assertEqual(d["depends_on"], [])
|
||||
|
||||
def test_task_to_dict_non_list_depends_on(self):
|
||||
d = _task_to_dict(self._row('{"a": 1}'))
|
||||
self.assertEqual(d["depends_on"], [{"a": 1}])
|
||||
|
||||
def test_request_kill_connect_failure(self):
|
||||
with mock.patch("cloud.server._connect",
|
||||
side_effect=RuntimeError("db down")):
|
||||
self.assertFalse(request_kill(self.db_path, "t1", "u1"))
|
||||
|
||||
def test_request_kill_insert_failure(self):
|
||||
c = _conn(self.db_path)
|
||||
|
||||
class _KillConn:
|
||||
def __init__(self, real):
|
||||
self._real = real
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
if "INSERT INTO kill_requests" in sql:
|
||||
raise RuntimeError("insert down")
|
||||
return self._real.execute(sql, params)
|
||||
|
||||
def rollback(self):
|
||||
raise RuntimeError("rollback down")
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
with mock.patch("cloud.server._connect",
|
||||
return_value=_KillConn(c)):
|
||||
self.assertFalse(request_kill(self.db_path, "t1", "u1"))
|
||||
c.close()
|
||||
|
||||
def test_gate_body_bad_entry(self):
|
||||
self.assertIn("ci_checks entries must be objects",
|
||||
_validate_hard_gate_body(
|
||||
{"ci_checks": [{"name": 1}]}))
|
||||
|
||||
def test_gate_body_bad_bool(self):
|
||||
self.assertIn("must be a boolean",
|
||||
_validate_hard_gate_body({"contract_frozen": "x"}))
|
||||
|
||||
def test_gate_body_bad_version(self):
|
||||
self.assertIn("must be a string",
|
||||
_validate_hard_gate_body({"contract_version": 3}))
|
||||
|
||||
def test_gate_body_bad_expected(self):
|
||||
self.assertIn("must be a string",
|
||||
_validate_hard_gate_body(
|
||||
{"contract_expected_version": 4}))
|
||||
|
||||
def test_classify_goal_followup_paused(self):
|
||||
action, code, payload = _classify_goal_followup(
|
||||
{"status": "paused"}, "做目标")
|
||||
self.assertEqual(action, "error")
|
||||
self.assertEqual(code, 409)
|
||||
|
||||
|
||||
class HandlerLeftoversTestCase(_Base):
|
||||
def test_kb_index_close_failure_swallowed(self):
|
||||
h = self._h(body={"path": "a.md", "content": "hi"})
|
||||
|
||||
class _Store:
|
||||
def __init__(self, db_path):
|
||||
self.conn = mock.Mock()
|
||||
self.conn.close.side_effect = RuntimeError("down")
|
||||
|
||||
def index_file(self, project_id, path, content, kind="file"):
|
||||
return "d1"
|
||||
with mock.patch("cloud.server.KBStore", _Store):
|
||||
h._kb_index("u1", "p1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 201)
|
||||
self.assertEqual(payload["id"], "d1")
|
||||
|
||||
def test_create_followup_read_error(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h(err="bad json")
|
||||
h._create_followup("u1", "t1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_thread_guard_cloud(self):
|
||||
h = self._h()
|
||||
code, msg = h._thread_task_guard({"executor": "cloud"})
|
||||
self.assertEqual(code, 409)
|
||||
|
||||
def test_thread_mutate_resolve_error(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._resolve_task_thread = lambda task, **k: (
|
||||
None, None, None, (404, "no thread"), None)
|
||||
h._task_thread_mutate("u1", "t1", ["archive"],
|
||||
"thread_archived", "archived %s")
|
||||
self.assertEqual(self._last(h)[0], 404)
|
||||
|
||||
def test_mcp_oauth_login_home_none(self):
|
||||
h = self._h(body={})
|
||||
h._mcp_server_oauth_login("u1", None, "srv")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 502)
|
||||
self.assertIn("cannot resolve tenant codex-home",
|
||||
payload["error"])
|
||||
|
||||
def test_thread_read_no_thread_note(self):
|
||||
self._project()
|
||||
self._task()
|
||||
h = self._h()
|
||||
h._resolve_task_thread = lambda task, **k: (
|
||||
{"id": "th1"}, "/tmp", "/tmp", None, None)
|
||||
with mock.patch("cloud.server._run_host_cli",
|
||||
return_value=(True, "garbage")):
|
||||
h._task_thread_read("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIn("no thread", str(payload))
|
||||
|
||||
def test_release_hard_gate_error(self):
|
||||
self._project()
|
||||
self._task(status="merged")
|
||||
h = self._h(body={"files_changed": [], "ci_checks": "x"})
|
||||
with mock.patch.dict(os.environ, {"CODEX_GLOBAL_ADMINS": "u1"}):
|
||||
h._release_task("u1", "t1")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("must be a list", payload["error"])
|
||||
|
||||
def test_coordinator_reserve_single_part(self):
|
||||
h = self._h()
|
||||
self.assertTrue(h._check_coordinator_reserve("u1", 1))
|
||||
|
||||
def test_oauth_login_close_failure_swallowed(self):
|
||||
from cloud.server import OAuthStateStore as _RealStore
|
||||
|
||||
class _ClosingProxy:
|
||||
def __init__(self, real):
|
||||
self._real = real
|
||||
|
||||
def close(self):
|
||||
raise RuntimeError("close down")
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
|
||||
class _Store(_RealStore):
|
||||
def __init__(self, db_path):
|
||||
super().__init__(db_path)
|
||||
self.conn = _ClosingProxy(self.conn)
|
||||
h = self._h()
|
||||
h.path = ("/oauth/github/login?client_id=c"
|
||||
"&redirect_uri=https://x.test/cb")
|
||||
with mock.patch("cloud.server.OAuthStateStore", _Store):
|
||||
h._oauth_login("/oauth/github/login")
|
||||
# Either 200 (login started) or 400 (no client config): both
|
||||
# pass through the close-failure swallow in the finally.
|
||||
self.assertIn(self._last(h)[0], (200, 400))
|
||||
|
||||
def test_oauth_bind_initiate_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._authenticate = lambda: "u1"
|
||||
h._oauth_bind_initiate("github")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_github_app_setup_page_org_action(self):
|
||||
from cloud.github_app import SetupStateStore
|
||||
store = SetupStateStore(self.db_path)
|
||||
try:
|
||||
state = store.create_state(issued_by="u1")
|
||||
finally:
|
||||
store.conn.close()
|
||||
h = self._h()
|
||||
h.path = ("/github-app/setup?state=%s&org=acme" % state)
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"GITHUB_APP_PUBLIC_BASE": "https://api.example.test"}):
|
||||
h._github_app_setup_page()
|
||||
self.assertEqual(h.html[0][0], 200)
|
||||
self.assertIn("organizations/acme", h.html[0][1])
|
||||
|
||||
def test_todos_create_read_error(self):
|
||||
h = self._h(err="bad json")
|
||||
h._todos_create("u1", "p1")
|
||||
self.assertEqual(self._last(h)[0], 400)
|
||||
|
||||
def test_todos_delete_ghost_404(self):
|
||||
h = self._h()
|
||||
h._todos_delete("u1", "p1", "ghost")
|
||||
code, payload = self._last(h)
|
||||
self.assertEqual(code, 404)
|
||||
self.assertIn("todo not found", payload["error"])
|
||||
|
||||
def test_automation_update_generic_failure_502(self):
|
||||
h = self._h(body={"name": "new"})
|
||||
|
||||
class _FailAll:
|
||||
def __init__(self, real):
|
||||
self._real = real
|
||||
|
||||
def execute(self, *a, **k):
|
||||
raise RuntimeError("db down")
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
h._db = lambda: _FailAll(_conn(self.db_path))
|
||||
h._automation_update("u1", "p1", "a1")
|
||||
self.assertEqual(self._last(h)[0], 502)
|
||||
@@ -0,0 +1,204 @@
|
||||
"""Coverage batch v2 (D8): GitHub webhook /fix-review branches --
|
||||
project with no owner, task-INSERT IntegrityError duplicate read,
|
||||
and IntegrityError re-raise.
|
||||
|
||||
The webhook handler runs end to end with a real HMAC signature
|
||||
(env secret), a real delivery claim and a real fix-review claim;
|
||||
only the task-table INSERT is faulted, via a counting _db proxy,
|
||||
to drive the handler's own IntegrityError mapping.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import io
|
||||
import json
|
||||
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))
|
||||
|
||||
from cloud.server import (
|
||||
CloudControlPlaneHandler,
|
||||
init_db,
|
||||
)
|
||||
|
||||
SECRET = "test-secret"
|
||||
|
||||
|
||||
def _handler(db_path):
|
||||
h = CloudControlPlaneHandler.__new__(CloudControlPlaneHandler)
|
||||
h.server = SimpleNamespace(db_path=db_path, upload_dir=None,
|
||||
tenants_root=None)
|
||||
sent = []
|
||||
h._send_json = lambda status, obj, extra_headers=None: sent.append(
|
||||
(status, obj))
|
||||
h.sent = sent
|
||||
return h
|
||||
|
||||
|
||||
def _conn(db_path):
|
||||
c = sqlite3.connect(db_path)
|
||||
c.row_factory = sqlite3.Row
|
||||
return c
|
||||
|
||||
|
||||
class _RowCursor:
|
||||
def __init__(self, row):
|
||||
self._row = row
|
||||
|
||||
def fetchone(self):
|
||||
return self._row
|
||||
|
||||
def fetchall(self):
|
||||
return [self._row] if self._row else []
|
||||
|
||||
|
||||
class _WebhookConn:
|
||||
"""Delegates everything except the task INSERT (raises
|
||||
IntegrityError) and the idempotency re-read (faked row)."""
|
||||
|
||||
def __init__(self, real, existing_row):
|
||||
self._real = real
|
||||
self._existing_row = existing_row
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
text = sql.strip()
|
||||
if text.startswith("INSERT INTO tasks"):
|
||||
raise sqlite3.IntegrityError("duplicate delivery")
|
||||
if "SELECT id FROM tasks WHERE project_id" in text:
|
||||
return _RowCursor(self._existing_row)
|
||||
return self._real.execute(sql, params)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
|
||||
|
||||
class WebhookFixReviewTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "test.db")
|
||||
init_db(self.db_path)
|
||||
from cloud.cost import CostStore
|
||||
CostStore(self.db_path)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _project_row(self, owner):
|
||||
c = _conn(self.db_path)
|
||||
c.execute(
|
||||
"INSERT INTO projects(id, name, owner_id, created_at,"
|
||||
" repo_url, auto_pr) VALUES(?, ?, ?, ?, ?, 0)",
|
||||
("p1", "o/r", owner, time.time(), None))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
def _payload(self):
|
||||
return {
|
||||
"action": "created",
|
||||
"comment": {
|
||||
"id": 99,
|
||||
"body": "/fix-review please fix this",
|
||||
"user": {"login": "alice",
|
||||
"author_association": "MEMBER"},
|
||||
},
|
||||
"issue": {"number": 7, "user": {"login": "bob"}},
|
||||
"repository": {"full_name": "o/r"},
|
||||
}
|
||||
|
||||
def _h(self, raw, delivery="d1"):
|
||||
h = _handler(self.db_path)
|
||||
sig = "sha256=" + hmac.new(
|
||||
SECRET.encode(), raw, hashlib.sha256).hexdigest()
|
||||
h.rfile = io.BytesIO(raw)
|
||||
h.headers = {
|
||||
"Content-Length": str(len(raw)),
|
||||
"X-Hub-Signature-256": sig,
|
||||
"X-GitHub-Event": "issue_comment",
|
||||
"X-GitHub-Delivery": delivery,
|
||||
}
|
||||
return h
|
||||
|
||||
def test_project_no_owner(self):
|
||||
self._project_row(None)
|
||||
raw = json.dumps(self._payload()).encode()
|
||||
h = self._h(raw)
|
||||
with mock.patch.dict(os.environ,
|
||||
{"GITHUB_WEBHOOK_SECRET": SECRET}):
|
||||
h._handle_github_webhook()
|
||||
code, payload = h.sent[-1]
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIn("no owner for quota attribution",
|
||||
payload["error"])
|
||||
|
||||
def _proxy_db(self, existing_row):
|
||||
calls = {"n": 0}
|
||||
|
||||
def _db():
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 2:
|
||||
return _WebhookConn(_conn(self.db_path), existing_row)
|
||||
return _conn(self.db_path)
|
||||
return _db
|
||||
|
||||
def test_insert_conflict_reads_duplicate(self):
|
||||
self._project_row("u1")
|
||||
raw = json.dumps(self._payload()).encode()
|
||||
h = self._h(raw)
|
||||
h._db = self._proxy_db(("existing-task",))
|
||||
with mock.patch.dict(os.environ,
|
||||
{"GITHUB_WEBHOOK_SECRET": SECRET}):
|
||||
h._handle_github_webhook()
|
||||
code, payload = h.sent[-1]
|
||||
self.assertEqual(code, 200)
|
||||
self.assertTrue(payload["duplicate"])
|
||||
self.assertEqual(payload["task_id"], "existing-task")
|
||||
|
||||
def test_insert_conflict_reraises(self):
|
||||
self._project_row("u1")
|
||||
raw = json.dumps(self._payload()).encode()
|
||||
h = self._h(raw)
|
||||
h._db = self._proxy_db(None)
|
||||
with mock.patch.dict(os.environ,
|
||||
{"GITHUB_WEBHOOK_SECRET": SECRET}):
|
||||
with self.assertRaises(sqlite3.IntegrityError):
|
||||
h._handle_github_webhook()
|
||||
|
||||
|
||||
class MainHarnessTestCase(unittest.TestCase):
|
||||
def _fake_server(self):
|
||||
server = SimpleNamespace(
|
||||
rate_limiter=object(), server_address=("127.0.0.1", 18765))
|
||||
|
||||
def _serve_forever():
|
||||
raise KeyboardInterrupt
|
||||
|
||||
server.serve_forever = _serve_forever
|
||||
return server
|
||||
|
||||
def test_main_shared_home_synced(self):
|
||||
from cloud.server import main
|
||||
with mock.patch("cloud.server.create_server",
|
||||
return_value=self._fake_server()), \
|
||||
mock.patch("cloud.server._resolve_shared_home_mcp",
|
||||
return_value="/tmp/x"), \
|
||||
mock.patch("cloud.server._sync_shared_home_mcp",
|
||||
return_value={"copied": ["f"],
|
||||
"refreshed": []}):
|
||||
main(["--port", "18765"])
|
||||
|
||||
def test_main_shared_home_failure_swallowed(self):
|
||||
from cloud.server import main
|
||||
with mock.patch("cloud.server.create_server",
|
||||
return_value=self._fake_server()), \
|
||||
mock.patch("cloud.server._resolve_shared_home_mcp",
|
||||
side_effect=RuntimeError("resolve down")):
|
||||
main(["--port", "18765"])
|
||||
@@ -0,0 +1,636 @@
|
||||
"""Coverage batch v2: check_transplant.py in-process tests.
|
||||
|
||||
Why this file exists: the existing test_skill_transplant_prep.py
|
||||
drives the script through subprocess, which coverage cannot see --
|
||||
so the tool measured 11.4% (31/271) despite being behaviour-tested.
|
||||
These tests import the module in-process and call the same code
|
||||
paths directly, with real assertions on drafts and error strings
|
||||
(the subprocess tests stay untouched and still pass).
|
||||
|
||||
Fixtures are the same construction as test_skill_transplant_prep
|
||||
(built from the script's own fragment constants, not copies of them),
|
||||
plus small variants that trip each loud-failure branch.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SKILL_DIR = REPO_ROOT / "cloud" / "skills" / "software-engineering"
|
||||
SCRIPT = SKILL_DIR / "tools" / "check_transplant.py"
|
||||
|
||||
|
||||
def _load_script():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"check_transplant_v2cov", str(SCRIPT))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
CT = _load_script()
|
||||
|
||||
|
||||
def _source_fixture():
|
||||
m = CT
|
||||
parts = [
|
||||
"---\nname: software-engineering\n---\n",
|
||||
"## 角色线程矩阵\n",
|
||||
"| 角色 | 线程数 | 负责目录 | 产出 |",
|
||||
"| QA / 测试 | 1 | `tests/` | 测试用例 |",
|
||||
"| 独立审计 | 1 | 只写审计记录 | 审计结论 |",
|
||||
"| 验收 / 发布 | 协调员兼任 | `docs/release/` | 验收清单 |",
|
||||
"## 失败、续跑与成本\n",
|
||||
"- 成本控制:分阶段本身就是成本控制。",
|
||||
"## 独立审计与自动推回\n",
|
||||
"本节是审计加固第 3 步的技能机制,与代码 `cloud/audit_pushback.py`",
|
||||
"(推回决策纯函数)双轨落地,只写文档不算落地。只补审计机制,",
|
||||
"不改上面已有规则的语义:G5 缺陷回路、合并拆分、G6 用户验收",
|
||||
"的原意都不变。" + m.AUDIT_WARNING,
|
||||
"### 独立审计角色:不写代码,只独立重跑、抽查证据、单独签字\n",
|
||||
"- 审计线程不写业务代码、不改待办、不给合并决定、不执行合并。",
|
||||
"### 自动推回:审计不过关,系统自动推回,不等人工点头\n",
|
||||
"- 推回上限与升级:推回修复最多三轮" + m.PUSHBACK_LIMIT_PAREN + "。",
|
||||
"- 与已有回路的关系:G5 缺陷回路复用为修复执行通道,推回派生的修复子任务",
|
||||
" 就按 G5 方式执行,只补“自动触发 + 合并发布阻塞”这两块缺失;",
|
||||
m.G7_FULLAUTO_BLOCK,
|
||||
"## 推倒重做:超过三次后的升级分支(不是并行回路)\n",
|
||||
"- 与上一节自动推回的关系再写一遍以免混淆:自动推回(1–3 轮修补)",
|
||||
" 在前、推倒重做(超过 3 次)在后,是同一链路的前后两段;",
|
||||
" 与 G5 缺陷回路的关系是复用修复执行通道(重做任务本身仍按",
|
||||
" 阶段派生、仍走 G5 回归复核)" + m.REBUILD_TAIL,
|
||||
"## 三条收紧:分流、保留资产、审计的审计\n",
|
||||
"- 收紧一:阈值不能死板,第三次失败先判根因。",
|
||||
m.TIGHTEN3_NOTIFY,
|
||||
"## QA 全链路:测试环节跑通各个和所有业务链路\n",
|
||||
"本节是“独立审计与 QA”部分的补充验收标准,与自动推回、推倒重做、",
|
||||
"三条收紧并列,不覆盖已有规则。" + m.QA_INTRO,
|
||||
"### 每个客户端交付前自测:三项缺一不算自测完\n",
|
||||
"- 单元测试 + 对照契约的集成测试 + 手工冒烟清单,三项缺一不算自测完。",
|
||||
"## 诚实边界:原生客户端\n",
|
||||
"- 在只有 Linux 主机的环境里,真原生 iOS 需要 Xcode。",
|
||||
"## 校验(协调员每过一个门禁自查一次)\n",
|
||||
"- 分阶段走了吗?G3.5 没过就开了 G4b / G4c 吗?",
|
||||
"- 审计与推回走了吗?审计签字人是否独立于执行人、协调员批准人、",
|
||||
" QA 测试人?安全签字是不是无条件通过?审计 fail 后有没有自动推回",
|
||||
" 并阻塞合并/发布?模块失败超过 3 次后有没有按推倒重做分支处理",
|
||||
" (先判根因、只推倒该模块、旧实现归档、避坑清单带进新包),",
|
||||
" 而不是无限修补或悄悄放行?",
|
||||
"- QA 全链路走了吗?每端自测三项齐了吗?PRD 验收标准全有对应用例吗?",
|
||||
" agent 专属六项齐了吗?跨端矩阵覆盖全部已选端了吗,未测端写明原因了吗?",
|
||||
" 未编译端是不是只写了“代码评审 + Web 核心验证”?QA 矩阵签字与",
|
||||
" 独立审计签字两个签字位都在且不是同一人吗?推回包里失败链路齐了吗,",
|
||||
" 重审重跑失败链路了吗?",
|
||||
"有一项为否,就停在当前门禁,不进入下一阶段。",
|
||||
"## 附录:角色模板(播种随附版)\n",
|
||||
"下面是 `templates/roles/` 的随附全文。",
|
||||
"---",
|
||||
"## 附录角色:08-shared-core(来源 templates/roles/08-shared-core.md)\n",
|
||||
"# 角色模板:共享核心\n",
|
||||
"- 不在共享逻辑里写某端专属代码。",
|
||||
"---",
|
||||
"## 附录角色:09-auditor(独立审计,新增)\n",
|
||||
"# 角色模板:独立审计(1 个线程)\n",
|
||||
"你是本项目的独立审计线程。",
|
||||
]
|
||||
return "\n".join(parts) + "\n"
|
||||
|
||||
|
||||
def _target_fixture(target_id):
|
||||
markers = " ".join(CT.MARKERS[target_id])
|
||||
last = ("## 附录角色:10-iteration(来源 templates/roles/10-iteration.md,v0.5 新增)"
|
||||
if target_id == "v05"
|
||||
else "## 附录角色:08-shared-core(来源 templates/roles/08-shared-core.md)")
|
||||
parts = [
|
||||
"# software-engineering:多客户端软件工程编排\n",
|
||||
"## 角色线程矩阵\n",
|
||||
"| 角色 | 线程数 | 负责目录 | 产出 |",
|
||||
"| QA / 测试 | 1 | `tests/` | 测试用例 |",
|
||||
"| 验收 / 发布 | 协调员兼任 | `docs/release/` | 验收清单 |",
|
||||
"## 失败、续跑与成本\n",
|
||||
"- 成本控制:分阶段本身就是成本控制。",
|
||||
"## 诚实边界:原生客户端\n",
|
||||
"- 在只有 Linux 主机的环境里,真原生 iOS 需要 Xcode。",
|
||||
"## 校验(协调员每过一个门禁自查一次)\n",
|
||||
"- 分阶段走了吗?G3.5 没过就开了 G4b / G4c 吗?",
|
||||
"- 标记行:" + markers,
|
||||
"| G1 | PRD 冻结 | PRD 按 `templates/artifacts/prd-outline.md` 写完,用户故事有验收标准 | 回产品线程改 |",
|
||||
"问卷模板在 `templates/artifacts/intake-questions.md`。最关键的六问:",
|
||||
"- 验收清单模板在 `templates/artifacts/acceptance-checklist.md`。",
|
||||
"按 `templates/artifacts/prd-outline.md` 写一份 PRD,至少包含:",
|
||||
" 协议模板见 `templates/artifacts/` 下的 agent 任务契约模板。",
|
||||
"1. 按 `templates/artifacts/acceptance-checklist.md` 生成本项目的验收清单,",
|
||||
"有一项为否,就停在当前门禁,不进入下一阶段。",
|
||||
"## 附录:角色模板(播种随附版)\n",
|
||||
last + "\n",
|
||||
"# 角色模板:共享核心\n",
|
||||
"- 不在共享逻辑里写某端专属代码。",
|
||||
]
|
||||
return "\n".join(parts) + "\n"
|
||||
|
||||
|
||||
def _main_lines_ending_with_tighten3():
|
||||
"""Main lines whose last paragraph is the tighten-3 notify text.
|
||||
|
||||
Exercises transform_main's v04 else-branch (notify text without a
|
||||
trailing newline), which the standard fixture cannot reach because
|
||||
the QA section follows the notify paragraph there. Built as a
|
||||
lines list directly: transform_main only needs main lines, while
|
||||
extract_source would also demand matrix/checklist/appendix.
|
||||
"""
|
||||
m = CT
|
||||
return [
|
||||
"## 独立审计与自动推回",
|
||||
"前言。" + m.AUDIT_WARNING,
|
||||
"### 独立审计角色:不写代码,只独立重跑、抽查证据、单独签字",
|
||||
"### 自动推回:审计不过关,系统自动推回,不等人工点头",
|
||||
"上限" + m.PUSHBACK_LIMIT_PAREN + "。",
|
||||
m.G7_FULLAUTO_BLOCK,
|
||||
"## 推倒重做:超过三次后的升级分支(不是并行回路)",
|
||||
"关系" + m.REBUILD_TAIL,
|
||||
"## 三条收紧:分流、保留资产、审计的审计",
|
||||
m.QA_INTRO,
|
||||
m.TIGHTEN3_NOTIFY,
|
||||
]
|
||||
|
||||
|
||||
class ExtractTestCase(unittest.TestCase):
|
||||
def test_extract_happy(self):
|
||||
hunks = CT.extract_source(_source_fixture())
|
||||
self.assertEqual(hunks["matrix_row"], "| 独立审计 | 1 | 只写审计记录 | 审计结论 |")
|
||||
self.assertEqual(hunks["main"][0], "## 独立审计与自动推回")
|
||||
self.assertTrue(hunks["audit_item"][0].startswith("- 审计与推回走了吗?"))
|
||||
self.assertTrue(hunks["audit_item"][-1].endswith("悄悄放行?"))
|
||||
self.assertTrue(hunks["qa_item"][0].startswith("- QA 全链路走了吗?"))
|
||||
self.assertTrue(hunks["qa_item"][-1].endswith("重审重跑失败链路了吗?"))
|
||||
self.assertEqual(hunks["appendix_body"][0], "# 角色模板:独立审计(1 个线程)")
|
||||
|
||||
def test_extract_matrix_zero_and_two(self):
|
||||
base = _source_fixture()
|
||||
no_matrix = base.replace("| 独立审计 | 1 | 只写审计记录 | 审计结论 |\n", "")
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
CT.extract_source(no_matrix)
|
||||
self.assertIn("auditor matrix row", str(ctx.exception))
|
||||
two = base.replace("| 验收 / 发布 |",
|
||||
"| 独立审计 | 1 | 只写审计记录 | 审计结论 |\n| 验收 / 发布 |")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.extract_source(two)
|
||||
|
||||
def test_extract_main_after_honesty(self):
|
||||
base = _source_fixture()
|
||||
# Move the single honesty heading to before the main heading:
|
||||
# delete the original, then insert one copy ahead of main, so
|
||||
# both _find_exact calls still find exactly one hit and only
|
||||
# the order check can fire.
|
||||
moved = base.replace("## 诚实边界:原生客户端\n", "", 1)
|
||||
moved = moved.replace("## 独立审计与自动推回\n",
|
||||
"## 诚实边界:原生客户端\n## 独立审计与自动推回\n", 1)
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
CT.extract_source(moved)
|
||||
self.assertIn("does not precede", str(ctx.exception))
|
||||
|
||||
def test_extract_missing_headings_and_unterminated_item(self):
|
||||
base = _source_fixture()
|
||||
with self.assertRaises(ValueError):
|
||||
CT.extract_source(base.replace("## 独立审计与自动推回\n", "## 其它节\n", 1))
|
||||
with self.assertRaises(ValueError):
|
||||
CT.extract_source(base.replace("## 附录角色:09-auditor(独立审计,新增)\n", "", 1))
|
||||
with self.assertRaises(ValueError):
|
||||
CT.extract_source(base.replace("- 审计与推回走了吗?", "- 审计走了吗?", 1))
|
||||
with self.assertRaises(ValueError):
|
||||
CT.extract_source(base.replace("- QA 全链路走了吗?", "- QA 走了吗?", 1))
|
||||
unterminated = base.replace("而不是无限修补或悄悄放行?",
|
||||
"而不是无限修补。")
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
CT.extract_source(unterminated)
|
||||
self.assertIn("never ends", str(ctx.exception))
|
||||
|
||||
def test_extract_trims_trailing_blank_lines_of_main(self):
|
||||
# Main slice ending in blank lines: the while-pop loop runs
|
||||
# and the returned main has no trailing blank.
|
||||
base = _source_fixture()
|
||||
variant = base.replace("## 诚实边界:原生客户端\n",
|
||||
"\n\n## 诚实边界:原生客户端\n", 1)
|
||||
hunks = CT.extract_source(variant)
|
||||
self.assertNotEqual(hunks["main"][-1], "")
|
||||
self.assertEqual(hunks["main"][0], "## 独立审计与自动推回")
|
||||
|
||||
def test_find_exact_and_lines_and_fail(self):
|
||||
self.assertEqual(CT._lines("a\nb"), ["a", "b"])
|
||||
lines = ["x", "y", "x"]
|
||||
with self.assertRaises(ValueError):
|
||||
CT._find_exact(lines, lambda l: l == "z", "missing")
|
||||
with self.assertRaises(ValueError):
|
||||
CT._find_exact(lines, lambda l: l == "x", "duplicated")
|
||||
err = io.StringIO()
|
||||
with contextlib.redirect_stderr(err):
|
||||
self.assertEqual(CT.fail("boom"), 1)
|
||||
self.assertIn("FAIL: boom", err.getvalue())
|
||||
|
||||
|
||||
class TransformTestCase(unittest.TestCase):
|
||||
def _main(self, source=None):
|
||||
return CT.extract_source(source or _source_fixture())["main"]
|
||||
|
||||
def test_transform_v03(self):
|
||||
lines, stopped = CT.transform_main(self._main(), "v03")
|
||||
text = "\n".join(lines)
|
||||
self.assertEqual(stopped, [])
|
||||
self.assertNotIn("G7 热修复回路并行", text)
|
||||
self.assertNotIn("全自动 CI 自动修复", text)
|
||||
self.assertIn("验收失败导致的推倒要先通知用户", text)
|
||||
self.assertIn("## QA 全链路", text)
|
||||
self.assertNotIn("移植警告", text)
|
||||
self.assertIn("推回上限独立于其它修复回路", text)
|
||||
self.assertTrue(text.rstrip().endswith("。") or "三项缺一不算自测完" in text)
|
||||
|
||||
def test_transform_v04(self):
|
||||
lines, stopped = CT.transform_main(self._main(), "v04")
|
||||
text = "\n".join(lines)
|
||||
self.assertEqual(len(stopped), 1)
|
||||
self.assertIn("收紧三", stopped[0])
|
||||
self.assertNotIn("G7 热修复回路并行", text)
|
||||
self.assertNotIn("验收失败导致的推倒要先通知用户", text)
|
||||
self.assertIn("全自动 CI 自动修复并行", text)
|
||||
self.assertIn("## QA 全链路", text)
|
||||
|
||||
def test_transform_v05(self):
|
||||
lines, stopped = CT.transform_main(self._main(), "v05")
|
||||
text = "\n".join(lines)
|
||||
self.assertEqual(len(stopped), 2)
|
||||
self.assertNotIn("## QA 全链路", text)
|
||||
self.assertNotIn("验收失败导致的推倒要先通知用户", text)
|
||||
self.assertNotIn("移植警告", text)
|
||||
self.assertIn("## 三条收紧", text)
|
||||
|
||||
def test_transform_v04_tighten3_without_trailing_newline(self):
|
||||
lines, stopped = CT.transform_main(
|
||||
_main_lines_ending_with_tighten3(), "v04")
|
||||
text = "\n".join(lines)
|
||||
self.assertEqual(len(stopped), 1)
|
||||
self.assertNotIn("验收失败导致的推倒要先通知用户", text)
|
||||
self.assertNotIn("G7 热修复回路并行", text)
|
||||
|
||||
def test_transform_unknown_target(self):
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(self._main(), "v99")
|
||||
|
||||
def test_transform_fragment_count_errors(self):
|
||||
base = _source_fixture()
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(CT.extract_source(
|
||||
base.replace(CT.G7_FULLAUTO_BLOCK, ""))["main"], "v03")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(CT.extract_source(
|
||||
base.replace(CT.PUSHBACK_LIMIT_PAREN, ""))["main"], "v03")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(CT.extract_source(
|
||||
base.replace(CT.REBUILD_TAIL, ""))["main"], "v03")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(CT.extract_source(
|
||||
base.replace(CT.G7_FULLAUTO_BLOCK, ""))["main"], "v04")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(CT.extract_source(
|
||||
base.replace(CT.AUDIT_WARNING, ""))["main"], "v03")
|
||||
|
||||
def test_transform_v05_qa_missing_and_tighten3_missing(self):
|
||||
base = _source_fixture()
|
||||
no_qa = base.replace("## QA 全链路:测试环节跑通各个和所有业务链路\n", "", 1)
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(CT.extract_source(no_qa)["main"], "v05")
|
||||
no_tighten = base.replace(CT.TIGHTEN3_NOTIFY, "")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(CT.extract_source(no_tighten)["main"], "v05")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.transform_main(CT.extract_source(no_tighten)["main"], "v04")
|
||||
|
||||
|
||||
class ApplyTestCase(unittest.TestCase):
|
||||
def test_apply_all_targets_in_process(self):
|
||||
for target_id in ("v03", "v04", "v05"):
|
||||
with self.subTest(target_id=target_id):
|
||||
draft, report = CT.apply_transplant(
|
||||
_source_fixture(), _target_fixture(target_id), target_id)
|
||||
self.assertNotIn("templates/artifacts", draft)
|
||||
self.assertEqual(draft.count("| 独立审计 |"), 1)
|
||||
self.assertEqual(
|
||||
CT.check_transplanted(_target_fixture(target_id),
|
||||
draft, target_id), [])
|
||||
number = "11" if target_id == "v05" else "09"
|
||||
self.assertIn("## 附录角色:%s-auditor" % number, draft)
|
||||
self.assertTrue(any("matrix row" in line for line in report))
|
||||
if target_id == "v03":
|
||||
self.assertIn("验收失败导致的推倒要先通知用户", draft)
|
||||
self.assertNotIn("G7 热修复回路并行", draft)
|
||||
if target_id == "v04":
|
||||
self.assertIn("STOPPED", "\n".join(report))
|
||||
if target_id == "v05":
|
||||
self.assertNotIn("## QA 全链路", draft)
|
||||
self.assertNotIn("09-auditor", draft)
|
||||
|
||||
def test_apply_error_branches(self):
|
||||
base_t = _target_fixture("v03")
|
||||
base_s = _source_fixture()
|
||||
with self.assertRaises(ValueError):
|
||||
CT.apply_transplant(base_s, base_t.replace(
|
||||
"| QA / 测试 |", "| QA |"), "v03")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.apply_transplant(base_s, base_t.replace(
|
||||
"## 诚实边界:原生客户端", "## 诚实分界线", 1), "v03")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.apply_transplant(base_s, base_t.replace(
|
||||
"有一项为否", "有一项不对", 1), "v03")
|
||||
already = base_t.replace(
|
||||
"## 附录:角色模板(播种随附版)",
|
||||
"## 附录角色:09-auditor(来源 x)\n## 附录:角色模板(播种随附版)")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.apply_transplant(base_s, already, "v03")
|
||||
duplicated = base_t.replace(
|
||||
"问卷模板在 `templates/artifacts/intake-questions.md`。",
|
||||
"问卷模板在 `templates/artifacts/intake-questions.md`。"
|
||||
"问卷模板在 `templates/artifacts/intake-questions.md`。")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.apply_transplant(base_s, duplicated, "v03")
|
||||
missing_artifact = base_t.replace(
|
||||
"问卷模板在 `templates/artifacts/intake-questions.md`。",
|
||||
"问卷模板已删。")
|
||||
with self.assertRaises(ValueError):
|
||||
CT.apply_transplant(base_s, missing_artifact, "v03")
|
||||
|
||||
|
||||
class AnchorsTestCase(unittest.TestCase):
|
||||
def _anchors(self, source, target, target_id):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
errors = CT.check_anchors(source, target, target_id)
|
||||
return errors, buf.getvalue()
|
||||
|
||||
def test_anchors_happy_all_targets(self):
|
||||
for target_id in ("v03", "v04", "v05"):
|
||||
with self.subTest(target_id=target_id):
|
||||
errors, out = self._anchors(_source_fixture(),
|
||||
_target_fixture(target_id),
|
||||
target_id)
|
||||
self.assertEqual(errors, [])
|
||||
self.assertIn("anchors:", out)
|
||||
|
||||
def test_anchors_source_unextractable(self):
|
||||
errors, _out = self._anchors("no hunks here",
|
||||
_target_fixture("v03"), "v03")
|
||||
self.assertEqual(len(errors), 1)
|
||||
self.assertIn("not extractable", errors[0])
|
||||
|
||||
def test_anchors_each_error(self):
|
||||
base_t = _target_fixture("v03")
|
||||
base_s = _source_fixture()
|
||||
cases = [
|
||||
(base_t.replace("| QA / 测试 |", "| QA |"),
|
||||
"target QA matrix row"),
|
||||
(base_t.replace("## 诚实边界:原生客户端", "## 诚实分界线", 1),
|
||||
"target honesty heading"),
|
||||
(base_t.replace("有一项为否", "有一项不对", 1),
|
||||
"target checklist end"),
|
||||
(base_t.replace("## 附录:角色模板(播种随附版)",
|
||||
"## 附录模板", 1),
|
||||
"target appendix intro"),
|
||||
(base_t.replace("## 附录角色:08-shared-core", "## 附录角色:07-x", 1),
|
||||
"lacks last chapter"),
|
||||
(base_t.replace("## 附录:角色模板(播种随附版)",
|
||||
"## 附录角色:09-auditor(来源 x)\n"
|
||||
"## 附录:角色模板(播种随附版)"),
|
||||
"already contains"),
|
||||
(base_t.replace("单端复核", "单端检查", 1),
|
||||
"preserved marker missing"),
|
||||
]
|
||||
for target, needle in cases:
|
||||
with self.subTest(needle=needle):
|
||||
errors, _out = self._anchors(base_s, target, "v03")
|
||||
self.assertTrue(any(needle in e for e in errors),
|
||||
errors)
|
||||
|
||||
|
||||
class CheckTransplantedTestCase(unittest.TestCase):
|
||||
def _draft(self, target_id):
|
||||
draft, _report = CT.apply_transplant(
|
||||
_source_fixture(), _target_fixture(target_id), target_id)
|
||||
return draft
|
||||
|
||||
def test_check_happy_all_targets(self):
|
||||
for target_id in ("v03", "v04", "v05"):
|
||||
with self.subTest(target_id=target_id):
|
||||
self.assertEqual(
|
||||
CT.check_transplanted(_target_fixture(target_id),
|
||||
self._draft(target_id),
|
||||
target_id), [])
|
||||
|
||||
def test_check_marker_and_matrix_errors(self):
|
||||
draft = self._draft("v03")
|
||||
target = _target_fixture("v03")
|
||||
dropped = "\n".join(l for l in draft.split("\n")
|
||||
if not l.startswith("- 标记行:"))
|
||||
errors = CT.check_transplanted(target, dropped, "v03")
|
||||
self.assertTrue(any("preserved marker lost" in e for e in errors))
|
||||
no_row = draft.replace("| 独立审计 | 1 | 只写审计记录 | 审计结论 |\n", "")
|
||||
errors = CT.check_transplanted(target, no_row, "v03")
|
||||
self.assertTrue(any("auditor matrix row" in e for e in errors))
|
||||
two_rows = draft.replace("| 独立审计 | 1 |",
|
||||
"| 独立审计 | 1 |\n| 独立审计 | 1 |", 1)
|
||||
errors = CT.check_transplanted(target, two_rows, "v03")
|
||||
self.assertTrue(any("auditor matrix row" in e for e in errors))
|
||||
|
||||
def test_check_main_header_missing(self):
|
||||
draft = self._draft("v03")
|
||||
target = _target_fixture("v03")
|
||||
for header in CT.MAIN_HEADERS:
|
||||
with self.subTest(header=header):
|
||||
mutated = draft.replace(header + "\n", "", 1)
|
||||
errors = CT.check_transplanted(target, mutated, "v03")
|
||||
self.assertTrue(any("main header missing" in e
|
||||
for e in errors), errors)
|
||||
|
||||
def test_check_stopped_paragraphs_per_target(self):
|
||||
# v03: QA must be present, G7/full-auto absent, tighten3 present.
|
||||
draft = self._draft("v03")
|
||||
target = _target_fixture("v03")
|
||||
no_qa = draft.replace("## QA 全链路:测试环节跑通各个和所有业务链路\n", "", 1)
|
||||
self.assertTrue(any("QA section missing" in e for e in
|
||||
CT.check_transplanted(target, no_qa, "v03")))
|
||||
with_g7 = draft + "\nG7 热修复回路并行\n"
|
||||
self.assertTrue(any("STOPPED G7" in e for e in
|
||||
CT.check_transplanted(target, with_g7, "v03")))
|
||||
with_fullauto = draft + "\n全自动 CI 自动修复\n"
|
||||
self.assertTrue(any("STOPPED full-auto" in e for e in
|
||||
CT.check_transplanted(target, with_fullauto, "v03")))
|
||||
no_tighten = draft.replace(CT.TIGHTEN3_NOTIFY, "")
|
||||
self.assertTrue(any("tighten3 notify missing" in e for e in
|
||||
CT.check_transplanted(target, no_tighten, "v03")))
|
||||
# v04: QA present, G7/tighten3 absent.
|
||||
draft4 = self._draft("v04")
|
||||
target4 = _target_fixture("v04")
|
||||
no_qa4 = draft4.replace("## QA 全链路:测试环节跑通各个和所有业务链路\n", "", 1)
|
||||
self.assertTrue(any("QA section missing" in e for e in
|
||||
CT.check_transplanted(target4, no_qa4, "v04")))
|
||||
with_tighten4 = draft4 + "\n" + CT.TIGHTEN3_NOTIFY + "\n"
|
||||
self.assertTrue(any("STOPPED tighten3" in e for e in
|
||||
CT.check_transplanted(target4, with_tighten4, "v04")))
|
||||
with_g7_4 = draft4 + "\nG7 热修复回路并行\n"
|
||||
self.assertTrue(any("STOPPED G7" in e for e in
|
||||
CT.check_transplanted(target4, with_g7_4, "v04")))
|
||||
# v05: QA section/item and tighten3 must all be absent.
|
||||
draft5 = self._draft("v05")
|
||||
target5 = _target_fixture("v05")
|
||||
with_qa5 = draft5 + "\n## QA 全链路:测试环节跑通各个和所有业务链路\n"
|
||||
self.assertTrue(any("STOPPED QA section" in e for e in
|
||||
CT.check_transplanted(target5, with_qa5, "v05")))
|
||||
with_qa_item5 = draft5 + "\n- QA 全链路走了吗?\n"
|
||||
self.assertTrue(any("STOPPED QA checklist" in e for e in
|
||||
CT.check_transplanted(target5, with_qa_item5, "v05")))
|
||||
with_tighten5 = draft5 + "\n" + CT.TIGHTEN3_NOTIFY + "\n"
|
||||
self.assertTrue(any("STOPPED tighten3" in e for e in
|
||||
CT.check_transplanted(target5, with_tighten5, "v05")))
|
||||
|
||||
def test_check_appendix_stale_artifacts_and_no_overwrite(self):
|
||||
draft = self._draft("v03")
|
||||
target = _target_fixture("v03")
|
||||
no_heading = draft.replace(
|
||||
"## 附录角色:09-auditor(来源 templates/roles/09-auditor.md)",
|
||||
"## 附录角色:09-auditor(来源 templates/roles/09-auditor2.md)")
|
||||
self.assertTrue(any("appendix heading" in e for e in
|
||||
CT.check_transplanted(target, no_heading, "v03")))
|
||||
old_form = draft + "\n09-auditor(独立审计,新增)\n"
|
||||
self.assertTrue(any("old appendix heading" in e for e in
|
||||
CT.check_transplanted(target, old_form, "v03")))
|
||||
for stale in ("移植警告", "以生产 v0.2 技能为准", "技能正文 diff 全是加法"):
|
||||
with self.subTest(stale=stale):
|
||||
mutated = draft + "\n%s\n" % stale
|
||||
self.assertTrue(any("stale transplant warning" in e for e in
|
||||
CT.check_transplanted(target, mutated, "v03")))
|
||||
with_artifact = draft + "\n见 `templates/artifacts/x.md`。\n"
|
||||
self.assertTrue(any("templates/artifacts" in e for e in
|
||||
CT.check_transplanted(target, with_artifact, "v03")))
|
||||
# No-overwrite: drop one target line that no other check pins.
|
||||
dropped_honesty = draft.replace("## 诚实边界:原生客户端\n", "", 1)
|
||||
self.assertTrue(any("no-overwrite" in e for e in
|
||||
CT.check_transplanted(target, dropped_honesty, "v03")))
|
||||
lines = draft.split("\n")
|
||||
idx_a = lines.index("## 失败、续跑与成本")
|
||||
idx_b = lines.index("## 诚实边界:原生客户端")
|
||||
lines[idx_a], lines[idx_b] = lines[idx_b], lines[idx_a]
|
||||
reordered = "\n".join(lines)
|
||||
self.assertTrue(any("no-overwrite" in e for e in
|
||||
CT.check_transplanted(target, reordered, "v03")))
|
||||
|
||||
|
||||
class MainCliTestCase(unittest.TestCase):
|
||||
def _write(self, tmp, source, target, target_id="v03"):
|
||||
src = Path(tmp) / "source.md"
|
||||
tgt = Path(tmp) / "target.md"
|
||||
src.write_text(source, encoding="utf-8")
|
||||
tgt.write_text(target, encoding="utf-8")
|
||||
return src, tgt
|
||||
|
||||
def _run(self, argv):
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
||||
code = CT.main(argv)
|
||||
return code, out.getvalue(), err.getvalue()
|
||||
|
||||
def test_main_anchors_ok_and_failed(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src, tgt = self._write(tmp, _source_fixture(),
|
||||
_target_fixture("v03"))
|
||||
code, out, _err = self._run(["anchors", "--source244", str(src),
|
||||
"--target", str(tgt),
|
||||
"--target-id", "v03"])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("anchors OK", out)
|
||||
bad_tgt = Path(tmp) / "bad.md"
|
||||
bad_tgt.write_text(_target_fixture("v03").replace(
|
||||
"| QA / 测试 |", "| QA |"), encoding="utf-8")
|
||||
code, _out, err = self._run(["anchors", "--source244", str(src),
|
||||
"--target", str(bad_tgt),
|
||||
"--target-id", "v03"])
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("FAILED", err)
|
||||
|
||||
def test_main_apply_out_and_stdout(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src, tgt = self._write(tmp, _source_fixture(),
|
||||
_target_fixture("v04"))
|
||||
out_path = Path(tmp) / "draft.md"
|
||||
code, _out, err = self._run(["apply", "--source244", str(src),
|
||||
"--target", str(tgt),
|
||||
"--target-id", "v04",
|
||||
"--out", str(out_path)])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("STOPPED", err)
|
||||
self.assertIn("## 附录角色:09-auditor", out_path.read_text())
|
||||
code, out, _err = self._run(["apply", "--source244", str(src),
|
||||
"--target", str(tgt),
|
||||
"--target-id", "v04"])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("## 附录角色:09-auditor", out)
|
||||
|
||||
def test_main_check_transplanted_ok_and_failed(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src, tgt = self._write(tmp, _source_fixture(),
|
||||
_target_fixture("v05"))
|
||||
draft, _report = CT.apply_transplant(_source_fixture(),
|
||||
_target_fixture("v05"), "v05")
|
||||
draft_path = Path(tmp) / "draft.md"
|
||||
draft_path.write_text(draft, encoding="utf-8")
|
||||
code, out, _err = self._run(["check-transplanted",
|
||||
"--source244", str(src),
|
||||
"--target", str(tgt),
|
||||
"--target-id", "v05",
|
||||
"--transplanted", str(draft_path)])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("check-transplanted OK", out)
|
||||
draft_path.write_text(draft + "\n见 `templates/artifacts/x.md`。\n",
|
||||
encoding="utf-8")
|
||||
code, _out, err = self._run(["check-transplanted",
|
||||
"--source244", str(src),
|
||||
"--target", str(tgt),
|
||||
"--target-id", "v05",
|
||||
"--transplanted", str(draft_path)])
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("FAILED", err)
|
||||
|
||||
def test_main_read_error_and_value_error_are_code_2(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src, tgt = self._write(tmp, _source_fixture(),
|
||||
_target_fixture("v03"))
|
||||
code, _out, err = self._run(["anchors", "--source244",
|
||||
str(Path(tmp) / "missing.md"),
|
||||
"--target", str(tgt),
|
||||
"--target-id", "v03"])
|
||||
self.assertEqual(code, 2)
|
||||
self.assertIn("cannot read input", err)
|
||||
bad_src = Path(tmp) / "bad.md"
|
||||
bad_src.write_text("no hunks here", encoding="utf-8")
|
||||
code, _out, _err = self._run(["apply", "--source244", str(bad_src),
|
||||
"--target", str(tgt),
|
||||
"--target-id", "v03"])
|
||||
self.assertEqual(code, 2)
|
||||
|
||||
def test_main_argparse_errors_exit_2(self):
|
||||
# argparse writes usage to stderr itself; swallow it so the
|
||||
# full-suite log stays readable.
|
||||
with contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
CT.main(["apply", "--source244", "s", "--target", "t",
|
||||
"--target-id", "v99"])
|
||||
self.assertEqual(ctx.exception.code, 2)
|
||||
with contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
CT.main([])
|
||||
self.assertEqual(ctx.exception.code, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,240 @@
|
||||
"""Coverage batch v2 (E1): cloud/wait_ops.py pure helpers -- row
|
||||
access, handoff validation, pr-check parsing, wait polling cores,
|
||||
diagnostics assembly. All direct calls; no production code touched.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from cloud.wait_ops import (
|
||||
_normalize_json_checks,
|
||||
_row_dict,
|
||||
build_pr_diagnostics,
|
||||
create_handoff,
|
||||
decode_wait_cursor,
|
||||
detect_significant_change,
|
||||
get_handoff,
|
||||
handoff_to_dict,
|
||||
interruptible_sleep,
|
||||
parse_pr_checks,
|
||||
poll_for_first_change,
|
||||
poll_task_statuses,
|
||||
report_handoff,
|
||||
row_get,
|
||||
significant_now,
|
||||
worktree_status,
|
||||
)
|
||||
|
||||
|
||||
class RowHelpersTestCase(unittest.TestCase):
|
||||
def test_row_get_fallbacks(self):
|
||||
self.assertIsNone(row_get(None, "x"))
|
||||
self.assertIsNone(row_get({}, "missing"))
|
||||
self.assertEqual(row_get({"a": 1}, "a"), 1)
|
||||
|
||||
def test_row_dict_none_and_tuple(self):
|
||||
self.assertIsNone(_row_dict(None, ["id", "x"]))
|
||||
self.assertEqual(_row_dict(("a", "b"), ["id", "x"]),
|
||||
{"id": "a", "x": "b"})
|
||||
|
||||
def test_handoff_progress_non_int_zero(self):
|
||||
d = handoff_to_dict({"id": "h", "progress": "abc"})
|
||||
self.assertEqual(d["progress"], 0)
|
||||
|
||||
def test_handoff_progress_clamped(self):
|
||||
d = handoff_to_dict({"id": "h", "progress": 150})
|
||||
self.assertEqual(d["progress"], 100)
|
||||
|
||||
|
||||
class HandoffValidationTestCase(unittest.TestCase):
|
||||
def test_create_target_too_long(self):
|
||||
with self.assertRaises(ValueError):
|
||||
create_handoff(None, "t", "p", "x" * 201, "u")
|
||||
|
||||
def test_create_detail_not_string(self):
|
||||
with self.assertRaises(ValueError):
|
||||
create_handoff(None, "t", "p", "host", "u", detail=123)
|
||||
|
||||
def test_create_detail_too_long(self):
|
||||
with self.assertRaises(ValueError):
|
||||
create_handoff(None, "t", "p", "host", "u",
|
||||
detail="x" * 2001)
|
||||
|
||||
def test_create_valid(self):
|
||||
conn = sqlite3.connect(":memory:")
|
||||
d = create_handoff(conn, "t1", "p1", " host-b ", "u1")
|
||||
self.assertEqual(d["status"], "pending")
|
||||
self.assertEqual(d["target_host"], "host-b")
|
||||
conn.close()
|
||||
|
||||
def test_get_missing_none(self):
|
||||
conn = sqlite3.connect(":memory:")
|
||||
self.assertIsNone(get_handoff(conn, "ghost"))
|
||||
conn.close()
|
||||
|
||||
def test_report_bad_status(self):
|
||||
ok, err = report_handoff(None, "h", "bogus")
|
||||
self.assertFalse(ok)
|
||||
self.assertIn("status must be one of", err)
|
||||
|
||||
def test_report_progress_not_int(self):
|
||||
ok, err = report_handoff(None, "h", "running", progress=True)
|
||||
self.assertFalse(ok)
|
||||
self.assertIn("must be an integer", err)
|
||||
|
||||
def test_report_detail_not_string(self):
|
||||
ok, err = report_handoff(None, "h", "running", detail=123)
|
||||
self.assertFalse(ok)
|
||||
self.assertIn("must be a string", err)
|
||||
|
||||
def test_report_detail_too_long(self):
|
||||
ok, err = report_handoff(None, "h", "running",
|
||||
detail="x" * 2001)
|
||||
self.assertFalse(ok)
|
||||
self.assertIn("too long", err)
|
||||
|
||||
|
||||
class ParseChecksTestCase(unittest.TestCase):
|
||||
def test_parse_header_single_part_nomatch_fallback(self):
|
||||
checks, all_pass, failed, pending = parse_pr_checks(
|
||||
"name\tstatus\nonly\t\nhello world\npass\nci\tpassing")
|
||||
names = [(c["name"], c["state"]) for c in checks]
|
||||
self.assertIn(("pass", "pass"), names)
|
||||
self.assertIn(("ci", "pass"), names)
|
||||
self.assertTrue(all_pass)
|
||||
|
||||
def test_worktree_status_unknown(self):
|
||||
s = worktree_status("paused", "/tmp/x", False, False)
|
||||
self.assertEqual(s["status"], "unknown")
|
||||
|
||||
|
||||
class WaitCoresTestCase(unittest.TestCase):
|
||||
def test_sleep_stop_event(self):
|
||||
event = threading.Event()
|
||||
event.set()
|
||||
slept, interrupted = interruptible_sleep(5, event)
|
||||
self.assertEqual(slept, 0.0)
|
||||
self.assertTrue(interrupted)
|
||||
|
||||
def test_sleep_full(self):
|
||||
slept, interrupted = interruptible_sleep(0.05)
|
||||
self.assertAlmostEqual(slept, 0.05, places=2)
|
||||
self.assertFalse(interrupted)
|
||||
|
||||
def test_poll_statuses_bad_timeout(self):
|
||||
all_terminal, statuses, waited, timed_out = poll_task_statuses(
|
||||
lambda: {}, timeout_seconds="bad")
|
||||
self.assertTrue(all_terminal)
|
||||
|
||||
def test_cursor_empty(self):
|
||||
with self.assertRaises(ValueError):
|
||||
decode_wait_cursor("")
|
||||
|
||||
def test_cursor_not_dict(self):
|
||||
cur = base64.urlsafe_b64encode(b"[1]").decode()
|
||||
with self.assertRaises(ValueError):
|
||||
decode_wait_cursor(cur)
|
||||
|
||||
def test_cursor_value_not_dict(self):
|
||||
cur = base64.urlsafe_b64encode(
|
||||
json.dumps({"a": 1}).encode()).decode()
|
||||
with self.assertRaises(ValueError):
|
||||
decode_wait_cursor(cur)
|
||||
|
||||
def test_significant_dirty_numbers(self):
|
||||
self.assertIsNone(significant_now(
|
||||
{"status": "running", "pending_approvals": "x",
|
||||
"pending_input": "y", "pending_followups": "z"}))
|
||||
|
||||
def test_significant_needs_approval(self):
|
||||
self.assertEqual(significant_now(
|
||||
{"status": "running", "pending_approvals": 1,
|
||||
"pending_input": 0, "pending_followups": 0}),
|
||||
"needs_approval")
|
||||
|
||||
def test_significant_all_clear(self):
|
||||
self.assertIsNone(significant_now(
|
||||
{"status": "running", "pending_approvals": 0,
|
||||
"pending_input": 0, "pending_followups": 0}))
|
||||
|
||||
def test_detect_non_dict_current(self):
|
||||
self.assertIsNone(detect_significant_change(
|
||||
None, {"t1": "notadict"}, ["t1"]))
|
||||
|
||||
def test_detect_needs_approval(self):
|
||||
base = {"t1": {"status": "running", "pending_approvals": 0,
|
||||
"pending_input": 0, "pending_followups": 0}}
|
||||
cur = {"t1": {"status": "running", "pending_approvals": 1,
|
||||
"pending_input": 0, "pending_followups": 0}}
|
||||
task_id, kind, detail = detect_significant_change(
|
||||
base, cur, ["t1"])
|
||||
self.assertEqual((task_id, kind), ("t1", "needs_approval"))
|
||||
|
||||
def test_detect_needs_continue(self):
|
||||
base = {"t1": {"status": "running", "pending_approvals": 0,
|
||||
"pending_input": 0, "pending_followups": 0}}
|
||||
cur = {"t1": {"status": "running", "pending_approvals": 0,
|
||||
"pending_input": 0, "pending_followups": 2}}
|
||||
task_id, kind, detail = detect_significant_change(
|
||||
base, cur, ["t1"])
|
||||
self.assertEqual((task_id, kind), ("t1", "needs_continue"))
|
||||
|
||||
def test_poll_first_bad_timeout_immediate(self):
|
||||
fetch = lambda: {"t1": {"status": "completed",
|
||||
"pending_approvals": 0,
|
||||
"pending_input": 0,
|
||||
"pending_followups": 0}}
|
||||
changed, summaries, waited, timed_out = poll_for_first_change(
|
||||
fetch, ["t1"], timeout_seconds="bad")
|
||||
self.assertEqual(changed["kind"], "terminal")
|
||||
self.assertFalse(timed_out)
|
||||
|
||||
def test_poll_first_loop_finds_change(self):
|
||||
seq = [{"t1": {"status": "running", "pending_approvals": 0,
|
||||
"pending_input": 0, "pending_followups": 0}},
|
||||
{"t1": {"status": "completed", "pending_approvals": 0,
|
||||
"pending_input": 0, "pending_followups": 0}}]
|
||||
it = iter(seq)
|
||||
fetch = lambda: next(it, seq[-1])
|
||||
changed, summaries, waited, timed_out = poll_for_first_change(
|
||||
fetch, ["t1"], timeout_seconds=5, poll_seconds=0.01)
|
||||
self.assertEqual(changed["kind"], "terminal")
|
||||
self.assertFalse(timed_out)
|
||||
|
||||
|
||||
class DiagnosticsTestCase(unittest.TestCase):
|
||||
def test_normalize_non_list(self):
|
||||
self.assertEqual(_normalize_json_checks("x"), [])
|
||||
|
||||
def test_normalize_skips_nameless(self):
|
||||
checks = _normalize_json_checks(
|
||||
[{"state": "pass"}, {"name": "ci", "state": "pass"}])
|
||||
self.assertEqual([c["name"] for c in checks], ["ci"])
|
||||
|
||||
def test_build_both_routes_fail(self):
|
||||
def run(cmd):
|
||||
return (1, "", "boom")
|
||||
with self.assertRaises(RuntimeError):
|
||||
build_pr_diagnostics(run, 7)
|
||||
|
||||
def test_build_check_runs_fetch_failed(self):
|
||||
def run(cmd):
|
||||
if "--json" in cmd:
|
||||
return (0, json.dumps(
|
||||
[{"name": "ci", "state": "fail"}]), "")
|
||||
return (0, "", "")
|
||||
with mock.patch("cloud.wait_ops._fetch_failed_check_runs",
|
||||
side_effect=RuntimeError("api down")):
|
||||
diag = build_pr_diagnostics(run, 7, owner_repo="o/r",
|
||||
head_sha="deadbeef")
|
||||
self.assertFalse(diag["check_runs"]["available"])
|
||||
self.assertIn("api down", diag["check_runs"]["note"])
|
||||
@@ -0,0 +1,706 @@
|
||||
"""Coverage batch v2: cloud/worker.py _deliver_marker_tools branches.
|
||||
|
||||
Baseline left 93 lines of _deliver_marker_tools uncovered: quota /
|
||||
DB exception paths, automation bind/create failures, control
|
||||
fallbacks, todo/task/cost/quota/files branches, poll/wait
|
||||
exceptions and the peer-message delivery error. The harness mirrors
|
||||
test_marker_tools.WorkerDeliverTestCase (temp SQLite via init_db,
|
||||
sender PM created by tester/maintainer).
|
||||
|
||||
Fault injection notes (honest about what is stubbed and why):
|
||||
- Collaborator helpers (_ensure_result_columns, _todo_list_db,
|
||||
_poll_task_snapshot, _wait_task_targets, _db_connect, CostStore,
|
||||
_marker_ancestry, get_task, cancel, _audit) are patched to
|
||||
simulate a failure the real code must report, never to fake the
|
||||
deliver report itself; every assertion reads the real followup
|
||||
body or the real DB row afterwards.
|
||||
- The quota owner-from-parent fallback (sender without created_by)
|
||||
is defensive code: the real _marker_auth returns False for a
|
||||
creator-less sender, so the fallback is only reachable when auth
|
||||
is stubbed True. Those two tests say so in their docstrings.
|
||||
- Root bypasses file permission bits, so the dir-list and file-read
|
||||
failure branches are reached with a selective patch (listdir /
|
||||
open raising for exactly the target path), not chmod.
|
||||
"""
|
||||
|
||||
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))
|
||||
|
||||
from cloud.server import init_db
|
||||
from cloud.todos import create_todo as _create_todo
|
||||
from cloud.worker import Worker
|
||||
|
||||
FAKE_ENV = {"XCM_API_KEY": "test-placeholder"}
|
||||
|
||||
|
||||
def _base_result(**over):
|
||||
result = {
|
||||
"status": "completed",
|
||||
"task_creates": [], "task_polls": [], "wait_threads": [],
|
||||
"task_cancels": [], "task_retries": [], "todo_lists": [],
|
||||
"todo_creates": [], "todo_updates": [], "task_lists": [],
|
||||
"task_costs": [], "quota_status": [], "task_files": [],
|
||||
"goal_converts": [], "automation_creates": [],
|
||||
}
|
||||
result.update(over)
|
||||
return result
|
||||
|
||||
|
||||
class DeliverBase(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._insert("PM", status="running", created_by="tester")
|
||||
self._member("tester", "maintainer")
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _insert(self, task_id, status="queued", created_by="tester",
|
||||
project_id="p1", parent=None, **cols):
|
||||
now = time.time()
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO tasks(id, project_id, prompt, status,"
|
||||
" idempotency_key, depends_on, priority, created_by,"
|
||||
" created_at, updated_at, last_heartbeat)"
|
||||
" VALUES(?, ?, ?, ?, NULL, '[]', 0, ?, ?, ?, NULL)",
|
||||
(task_id, project_id, "do work", status, created_by,
|
||||
now, now))
|
||||
if parent is not None:
|
||||
conn.execute(
|
||||
"UPDATE tasks SET parent_task_id = ? WHERE id = ?",
|
||||
(parent, task_id))
|
||||
for key, value in cols.items():
|
||||
conn.execute(
|
||||
"UPDATE tasks SET %s = ? WHERE id = ?" % key,
|
||||
(value, task_id))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _member(self, user_id, role):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY,"
|
||||
" username TEXT UNIQUE, api_key_hash TEXT,"
|
||||
" created_at REAL)")
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS project_members("
|
||||
"project_id TEXT, user_id TEXT, role TEXT,"
|
||||
" PRIMARY KEY(project_id, user_id))")
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO project_members"
|
||||
"(project_id, user_id, role) VALUES('p1', ?, ?)",
|
||||
(user_id, role))
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO users(id, username, api_key_hash,"
|
||||
" created_at) VALUES(?, ?, '', 0)", (user_id, user_id))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _worker(self, **kwargs):
|
||||
return Worker(self.db_path, "/fake/host.py", env=FAKE_ENV,
|
||||
**kwargs)
|
||||
|
||||
def _sender(self, worker, task_id="PM"):
|
||||
return worker.get_task(task_id)
|
||||
|
||||
def _followup_bodies(self, task_id="PM"):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT body FROM task_followups"
|
||||
" WHERE task_id = ?", (task_id,)).fetchall()
|
||||
return [row[0] for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _status(self, task_id):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT status, result_text FROM tasks WHERE id = ?",
|
||||
(task_id,)).fetchone()
|
||||
return row[0], row[1]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _deliver(self, worker, sender, result, task_id="PM"):
|
||||
worker._deliver_marker_tools(sender, result)
|
||||
return self._followup_bodies(task_id)
|
||||
|
||||
|
||||
class CreatesAndAutomationTestCase(DeliverBase):
|
||||
def test_creates_quota_check_exception(self):
|
||||
worker = self._worker()
|
||||
sender = self._sender(worker)
|
||||
with mock.patch.object(worker, "_quota_exceeded",
|
||||
side_effect=RuntimeError("boom")):
|
||||
bodies = self._deliver(worker, sender, _base_result(
|
||||
task_creates=[{"name": "E1", "prompt": "do e1",
|
||||
"model": None, "effort": None,
|
||||
"executor": None}]))
|
||||
self.assertTrue(any("quota_check_failed" in b for b in bodies))
|
||||
|
||||
def test_creates_db_exception(self):
|
||||
worker = self._worker()
|
||||
sender = self._sender(worker)
|
||||
with mock.patch.object(worker, "_ensure_result_columns",
|
||||
side_effect=RuntimeError("db down")):
|
||||
bodies = self._deliver(worker, sender, _base_result(
|
||||
task_creates=[{"name": "E1", "prompt": "do e1",
|
||||
"model": None, "effort": None,
|
||||
"executor": None}]))
|
||||
self.assertTrue(any("创建失败" in b for b in bodies))
|
||||
|
||||
def test_automation_first_run_past_skipped(self):
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
automation_creates=[{"name": "auto1", "prompt": "do auto",
|
||||
"interval_seconds": 3600,
|
||||
"first_run_at": time.time() - 3600}]))
|
||||
self.assertTrue(any("首次运行时刻已过去" in b for b in bodies))
|
||||
|
||||
def test_automation_duplicate_name_second_fails(self):
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
automation_creates=[
|
||||
{"name": "dup", "prompt": "do auto",
|
||||
"interval_seconds": 3600},
|
||||
{"name": "dup", "prompt": "do auto again",
|
||||
"interval_seconds": 3600}]))
|
||||
joined = "\n".join(bodies)
|
||||
self.assertIn("已创建自动化", joined)
|
||||
self.assertIn("自动化创建失败", joined)
|
||||
self.assertIn("already exists", joined)
|
||||
|
||||
def test_automation_bind_bad_mode_reports_but_counts(self):
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
automation_creates=[{"name": "b1", "prompt": "do auto",
|
||||
"interval_seconds": 3600,
|
||||
"bind": "bogus"}]))
|
||||
joined = "\n".join(bodies)
|
||||
self.assertIn("绑定到本会话失败", joined)
|
||||
self.assertIn("定义本身仍有效", joined)
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT name FROM automations WHERE name = 'b1'"
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
self.assertIsNotNone(row)
|
||||
|
||||
def test_automation_bind_heartbeat_success(self):
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
automation_creates=[{"name": "hb1", "prompt": "do auto",
|
||||
"interval_seconds": 3600,
|
||||
"bind": "heartbeat"}]))
|
||||
self.assertTrue(any("已绑到本会话唤醒" in b for b in bodies))
|
||||
|
||||
def test_automation_quota_exception(self):
|
||||
worker = self._worker()
|
||||
sender = self._sender(worker)
|
||||
with mock.patch.object(worker, "_quota_exceeded",
|
||||
side_effect=RuntimeError("boom")):
|
||||
bodies = self._deliver(worker, sender, _base_result(
|
||||
automation_creates=[{"name": "q1", "prompt": "do auto",
|
||||
"interval_seconds": 3600}]))
|
||||
joined = "\n".join(bodies)
|
||||
self.assertIn("自动化创建跳过", joined)
|
||||
self.assertIn("quota_check_failed", joined)
|
||||
|
||||
|
||||
class ControlFallbackTestCase(DeliverBase):
|
||||
def test_cancel_target_read_failure(self):
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
worker = self._worker()
|
||||
sender = self._sender(worker)
|
||||
# _was_cancelled also calls get_task, so only the target read
|
||||
# itself may fail; everything else delegates to the real one.
|
||||
orig_get = worker.get_task
|
||||
|
||||
def _flaky_get(task_id):
|
||||
if task_id == "E1":
|
||||
raise RuntimeError("db down")
|
||||
return orig_get(task_id)
|
||||
|
||||
with mock.patch.object(worker, "get_task",
|
||||
side_effect=_flaky_get):
|
||||
bodies = self._deliver(worker, sender, _base_result(
|
||||
task_cancels=[{"task_id": "E1"}]))
|
||||
self.assertTrue(any("读取失败" in b for b in bodies))
|
||||
self.assertEqual(self._status("E1")[0], "running")
|
||||
|
||||
def test_cancel_ancestry_failure_falls_back_to_other(self):
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
worker = self._worker()
|
||||
with mock.patch("cloud.worker._marker_ancestry",
|
||||
side_effect=RuntimeError("db down")):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_cancels=[{"task_id": "E1"}]))
|
||||
self.assertTrue(any("拒绝" in b for b in bodies))
|
||||
self.assertEqual(self._status("E1")[0], "running")
|
||||
|
||||
def test_cancel_not_done_reports(self):
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
worker = self._worker()
|
||||
with mock.patch.object(worker, "cancel", return_value=False):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_cancels=[{"task_id": "E1"}]))
|
||||
self.assertTrue(any("未执行" in b for b in bodies))
|
||||
self.assertEqual(self._status("E1")[0], "running")
|
||||
|
||||
def test_cancel_execution_exception(self):
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
worker = self._worker()
|
||||
with mock.patch.object(worker, "cancel",
|
||||
side_effect=RuntimeError("boom")):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_cancels=[{"task_id": "E1"}]))
|
||||
self.assertTrue(any("执行失败" in b for b in bodies))
|
||||
|
||||
def test_retry_not_done_when_rowcount_zero(self):
|
||||
self._insert("E1", status="failed", parent="PM")
|
||||
worker = self._worker()
|
||||
real_connect = sqlite3.connect
|
||||
|
||||
class _CursorProxy:
|
||||
def __init__(self, real):
|
||||
self._real = real
|
||||
self.rowcount = 0
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
|
||||
class _ConnProxy:
|
||||
def __init__(self, real):
|
||||
self._real = real
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
if "UPDATE tasks SET status = 'queued'" in sql:
|
||||
return _CursorProxy(self._real.execute(sql, params))
|
||||
return self._real.execute(sql, params)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
|
||||
import cloud.worker as worker_mod
|
||||
real_db_connect = worker_mod._db_connect
|
||||
|
||||
def _connect(path):
|
||||
return _ConnProxy(real_db_connect(path))
|
||||
|
||||
with mock.patch("cloud.worker._db_connect", side_effect=_connect):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_retries=[{"task_id": "E1"}]))
|
||||
# The proxy cursor reports rowcount 0, so deliver must report
|
||||
# "not executed" -- but the real UPDATE still ran through the
|
||||
# proxy's delegate, so the test asserts the report, not the
|
||||
# row, for this TOCTOU simulation.
|
||||
self.assertTrue(any("未执行" in b for b in bodies))
|
||||
|
||||
def test_retry_execution_exception_via_audit(self):
|
||||
self._insert("E1", status="failed", parent="PM")
|
||||
worker = self._worker()
|
||||
sender = self._sender(worker)
|
||||
orig_audit = worker._audit
|
||||
calls = [0]
|
||||
|
||||
def _flaky_audit(project_id, action, detail=""):
|
||||
calls[0] += 1
|
||||
if calls[0] == 1:
|
||||
raise RuntimeError("audit down")
|
||||
return orig_audit(project_id, action, detail)
|
||||
|
||||
with mock.patch.object(worker, "_audit", side_effect=_flaky_audit):
|
||||
bodies = self._deliver(worker, sender, _base_result(
|
||||
task_retries=[{"task_id": "E1"}]))
|
||||
self.assertTrue(any("执行失败" in b for b in bodies))
|
||||
|
||||
|
||||
class TodoTestCase(DeliverBase):
|
||||
def test_todo_create_write_denied(self):
|
||||
self._member("tester", "viewer")
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
todo_creates=[{"title": "T1"}]))
|
||||
self.assertTrue(any("创建待办" in b and "拒绝" in b for b in bodies))
|
||||
|
||||
def test_todo_create_cross_project_task(self):
|
||||
self._insert("X1", status="running", project_id="p2")
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
todo_creates=[{"title": "T1", "task_id": "X1"}]))
|
||||
self.assertTrue(any("属于别的项目" in b for b in bodies))
|
||||
|
||||
def test_todo_create_validation_failure(self):
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
todo_creates=[{"title": ""}]))
|
||||
self.assertTrue(any("创建待办" in b and "失败" in b for b in bodies))
|
||||
|
||||
def test_todo_update_cross_project_task(self):
|
||||
self._insert("X1", status="running", project_id="p2")
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
todo = _create_todo(conn, "p1", "tester", "T1")
|
||||
finally:
|
||||
conn.close()
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
todo_updates=[{"id": todo["id"],
|
||||
"fields": {"task_id": "X1"}}]))
|
||||
self.assertTrue(any("属于别的项目" in b for b in bodies))
|
||||
|
||||
def test_todo_update_failure(self):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
todo = _create_todo(conn, "p1", "tester", "T1")
|
||||
finally:
|
||||
conn.close()
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
todo_updates=[{"id": todo["id"],
|
||||
"fields": {"status": "bogus"}}]))
|
||||
self.assertTrue(any("更新待办" in b and "失败" in b for b in bodies))
|
||||
|
||||
def test_todo_list_failure(self):
|
||||
worker = self._worker()
|
||||
with mock.patch("cloud.worker._todo_list_db",
|
||||
side_effect=RuntimeError("db down")):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(todo_lists=[{}]))
|
||||
self.assertTrue(any("查待办失败" in b for b in bodies))
|
||||
|
||||
def test_todo_list_truncation(self):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
for i in range(21):
|
||||
_create_todo(conn, "p1", "tester", "T%d" % i)
|
||||
finally:
|
||||
conn.close()
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(todo_lists=[{}]))
|
||||
self.assertTrue(any("还有 1 条未列出" in b for b in bodies))
|
||||
|
||||
def test_task_list_failure(self):
|
||||
worker = self._worker()
|
||||
sender = self._sender(worker)
|
||||
# _was_cancelled (first _db_connect use) and the final
|
||||
# followup insert must still work; only the task_lists query
|
||||
# itself (second use) fails.
|
||||
import cloud.worker as worker_mod
|
||||
real_connect = worker_mod._db_connect
|
||||
calls = [0]
|
||||
|
||||
def _flaky_connect(path):
|
||||
calls[0] += 1
|
||||
if calls[0] == 2:
|
||||
raise RuntimeError("db down")
|
||||
return real_connect(path)
|
||||
|
||||
with mock.patch("cloud.worker._db_connect",
|
||||
side_effect=_flaky_connect):
|
||||
bodies = self._deliver(worker, sender,
|
||||
_base_result(task_lists=[{"limit": 10}]))
|
||||
self.assertTrue(any("查任务列表失败" in b for b in bodies))
|
||||
|
||||
|
||||
class CostQuotaFilesTestCase(DeliverBase):
|
||||
def test_task_cost_read_denied(self):
|
||||
# Viewer still has read permission, so read denial needs a
|
||||
# sender with no project membership at all.
|
||||
self._insert("OUT", status="running", created_by="outsider")
|
||||
self._insert("E1", status="completed", parent="PM")
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker, "OUT"),
|
||||
_base_result(
|
||||
task_costs=[{"task_id": "E1"}]),
|
||||
task_id="OUT")
|
||||
self.assertTrue(any("创建人无 read 权限" in b for b in bodies))
|
||||
|
||||
def test_task_cost_relation_reject(self):
|
||||
self._insert("S1", status="running", created_by="other")
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
task_costs=[{"task_id": "S1"}]))
|
||||
self.assertTrue(any("不在派生链内且创建人不同" in b for b in bodies))
|
||||
|
||||
def test_task_cost_ancestry_failure(self):
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
worker = self._worker()
|
||||
with mock.patch("cloud.worker._marker_ancestry",
|
||||
side_effect=RuntimeError("db down")):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_costs=[{"task_id": "E1"}]))
|
||||
self.assertTrue(any("不在派生链内且创建人不同" in b for b in bodies))
|
||||
|
||||
def test_task_cost_store_close_failure_swallowed(self):
|
||||
self._insert("E1", status="completed", parent="PM",
|
||||
result_text="done")
|
||||
worker = self._worker()
|
||||
|
||||
class _FakeConn:
|
||||
def close(self):
|
||||
raise RuntimeError("close down")
|
||||
|
||||
class _FakeStore:
|
||||
def __init__(self, db_path):
|
||||
self._conn = _FakeConn()
|
||||
|
||||
def report_by_task(self, task_id):
|
||||
return {"usd_micros": 1234567, "total_tokens": 100,
|
||||
"actual_tokens": 80, "estimated_tokens": 20,
|
||||
"turns": 2, "unpriced_actual_tokens": 5}
|
||||
|
||||
with mock.patch("cloud.worker.CostStore", _FakeStore):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_costs=[{"task_id": "E1"}]))
|
||||
joined = "\n".join(bodies)
|
||||
self.assertIn("成本:$1.2346", joined)
|
||||
self.assertIn("无价 actual token 5", joined)
|
||||
|
||||
def test_task_cost_report_failure(self):
|
||||
self._insert("E1", status="completed", parent="PM")
|
||||
worker = self._worker()
|
||||
|
||||
class _FailingStore:
|
||||
def __init__(self, db_path):
|
||||
self._conn = SimpleNamespace(
|
||||
close=lambda: None)
|
||||
|
||||
def report_by_task(self, task_id):
|
||||
raise RuntimeError("cost down")
|
||||
|
||||
with mock.patch("cloud.worker.CostStore", _FailingStore):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_costs=[{"task_id": "E1"}]))
|
||||
self.assertTrue(any("成本失败" in b for b in bodies))
|
||||
|
||||
def test_quota_owner_from_parent_via_stubbed_auth(self):
|
||||
# Defensive fallback only: the real _marker_auth returns
|
||||
# False for a creator-less sender, so this branch is only
|
||||
# reachable when auth is stubbed True. The test pins the
|
||||
# fallback logic itself (owner resolved from the parent).
|
||||
self._insert("ORPH", status="running", created_by=None,
|
||||
parent="PM")
|
||||
worker = self._worker()
|
||||
worker._marker_auth = lambda task, action: (True, "")
|
||||
sender = self._sender(worker, "ORPH")
|
||||
bodies = self._deliver(worker, sender,
|
||||
_base_result(quota_status=[{}]),
|
||||
task_id="ORPH")
|
||||
# Owner resolved from the parent (tester, no explicit quota
|
||||
# row) so the report shows the default quota, not "unlimited".
|
||||
self.assertTrue(any("配额" in b and "总额 $10.0000" in b
|
||||
for b in bodies), bodies)
|
||||
|
||||
def test_quota_no_owner_via_stubbed_auth(self):
|
||||
self._insert("ORPH2", status="running", created_by=None)
|
||||
worker = self._worker()
|
||||
worker._marker_auth = lambda task, action: (True, "")
|
||||
sender = self._sender(worker, "ORPH2")
|
||||
bodies = self._deliver(worker, sender,
|
||||
_base_result(quota_status=[{}]),
|
||||
task_id="ORPH2")
|
||||
self.assertTrue(any("发送方无创建人" in b for b in bodies))
|
||||
|
||||
def test_quota_unlimited_explicit_null_row(self):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS user_quotas("
|
||||
"user_id TEXT PRIMARY KEY, quota_usd_micros BIGINT,"
|
||||
" updated_at DOUBLE PRECISION)")
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO user_quotas"
|
||||
"(user_id, quota_usd_micros, updated_at)"
|
||||
" VALUES('tester', NULL, 0)")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(quota_status=[{}]))
|
||||
self.assertTrue(any("不限额" in b for b in bodies))
|
||||
|
||||
def test_quota_limit_failure(self):
|
||||
worker = self._worker()
|
||||
with mock.patch.object(worker, "_quota_limit_and_used",
|
||||
side_effect=RuntimeError("quota down")):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(quota_status=[{}]))
|
||||
self.assertTrue(any("查配额失败" in b for b in bodies))
|
||||
|
||||
def test_task_files_target_missing(self):
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
task_files=[{"task_id": "NOPE", "path": ""}]))
|
||||
self.assertTrue(any("不存在(或属于别的项目)" in b for b in bodies))
|
||||
|
||||
def test_task_files_ancestry_failure(self):
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
worker = self._worker()
|
||||
with mock.patch("cloud.worker._marker_ancestry",
|
||||
side_effect=RuntimeError("db down")):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_files=[{"task_id": "E1",
|
||||
"path": ""}]))
|
||||
self.assertTrue(any("不在派生链内且创建人不同" in b for b in bodies))
|
||||
|
||||
|
||||
class TaskFilesWorktreeTestCase(DeliverBase):
|
||||
def _worktree_worker(self):
|
||||
worker = self._worker(tenants_root=self.tmp.name)
|
||||
worktree = os.path.join(self.tmp.name, "p1", "work",
|
||||
"host-worktrees", "E1")
|
||||
os.makedirs(worktree)
|
||||
return worker, worktree
|
||||
|
||||
def test_task_files_dir_and_file_happy(self):
|
||||
worker, worktree = self._worktree_worker()
|
||||
Path(worktree, "a.txt").write_text("hello-file", encoding="utf-8")
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
task_files=[{"task_id": "E1", "path": ""},
|
||||
{"task_id": "E1", "path": "a.txt"}]))
|
||||
joined = "\n".join(bodies)
|
||||
self.assertIn("a.txt", joined)
|
||||
self.assertIn("hello-file", joined)
|
||||
|
||||
def test_task_files_dir_list_failure(self):
|
||||
worker, worktree = self._worktree_worker()
|
||||
Path(worktree, "a.txt").write_text("x", encoding="utf-8")
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
with mock.patch("os.listdir", side_effect=OSError("list down")):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_files=[{"task_id": "E1",
|
||||
"path": ""}]))
|
||||
self.assertTrue(any("列任务" in b and "目录失败" in b
|
||||
for b in bodies))
|
||||
|
||||
def test_task_files_file_read_failure(self):
|
||||
worker, worktree = self._worktree_worker()
|
||||
target = Path(worktree, "a.txt")
|
||||
target.write_text("secret", encoding="utf-8")
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
real_open = open
|
||||
|
||||
def _selective_open(path, *args, **kwargs):
|
||||
if os.fspath(path) == str(target):
|
||||
raise OSError("read down")
|
||||
return real_open(path, *args, **kwargs)
|
||||
|
||||
with mock.patch("builtins.open", side_effect=_selective_open):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_files=[{"task_id": "E1",
|
||||
"path": "a.txt"}]))
|
||||
self.assertTrue(any("读任务" in b and "失败" in b for b in bodies))
|
||||
|
||||
def test_task_files_binary_rejected(self):
|
||||
worker, worktree = self._worktree_worker()
|
||||
Path(worktree, "a.bin").write_bytes(b"\x00\x01\x02abc")
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
task_files=[{"task_id": "E1", "path": "a.bin"}]))
|
||||
self.assertTrue(any("二进制文件" in b for b in bodies))
|
||||
|
||||
def test_task_files_invalid_utf8_rejected(self):
|
||||
worker, worktree = self._worktree_worker()
|
||||
Path(worktree, "bad.bin").write_bytes(b"\xff\xfe\x00abc")
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
task_files=[{"task_id": "E1", "path": "bad.bin"}]))
|
||||
self.assertTrue(any("二进制文件" in b for b in bodies))
|
||||
|
||||
def test_task_files_truncation(self):
|
||||
worker, worktree = self._worktree_worker()
|
||||
Path(worktree, "big.txt").write_text("x" * 21000, encoding="utf-8")
|
||||
self._insert("E1", status="running", parent="PM")
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
task_files=[{"task_id": "E1", "path": "big.txt"}]))
|
||||
self.assertTrue(any("已截断前 20000 字" in b for b in bodies))
|
||||
|
||||
|
||||
class PollWaitGoalPeerTestCase(DeliverBase):
|
||||
def test_poll_and_wait_exceptions(self):
|
||||
worker = self._worker()
|
||||
with mock.patch.object(worker, "_poll_task_snapshot",
|
||||
side_effect=RuntimeError("poll down")), \
|
||||
mock.patch.object(worker, "_wait_task_targets",
|
||||
side_effect=RuntimeError("wait down")):
|
||||
bodies = self._deliver(worker, self._sender(worker),
|
||||
_base_result(
|
||||
task_polls=[{"task_id": "E1",
|
||||
"interval_seconds": 5,
|
||||
"timeout_seconds": 0,
|
||||
"chars": 2000}],
|
||||
wait_threads=[{"task_ids": ["E1"],
|
||||
"interval_seconds": 5,
|
||||
"timeout_seconds": 0,
|
||||
"chars": 2000}]))
|
||||
joined = "\n".join(bodies)
|
||||
self.assertIn("轮询任务 E1 失败", joined)
|
||||
self.assertIn("等待任务失败", joined)
|
||||
|
||||
def test_goal_converts_queued_after_report(self):
|
||||
worker = self._worker()
|
||||
bodies = self._deliver(worker, self._sender(worker), _base_result(
|
||||
goal_converts=[{"goal": "做目标"}]))
|
||||
self.assertEqual(bodies, ["/goal 做目标"])
|
||||
|
||||
def test_peer_messages_delivery_error(self):
|
||||
worker = self._worker()
|
||||
sender = self._sender(worker)
|
||||
with mock.patch("cloud.worker._deliver_peer_message",
|
||||
side_effect=RuntimeError("peer down")):
|
||||
worker._deliver_peer_messages(sender, {
|
||||
"status": "completed",
|
||||
"task_messages": [{"to_task_id": "E1", "body": "hi",
|
||||
"mode": "followup"}]})
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
tables = [r[0] for r in conn.execute(
|
||||
"SELECT name FROM sqlite_master"
|
||||
" WHERE type = 'table'").fetchall()]
|
||||
if "audit_log" in tables:
|
||||
rows = conn.execute(
|
||||
"SELECT detail FROM audit_log"
|
||||
" WHERE action = 'worker_peer_messages'").fetchall()
|
||||
self.assertTrue(any("rejected=1" in (r[0] or "")
|
||||
for r in rows), rows)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,229 @@
|
||||
"""Coverage batch v2: host.py shell scanner + classify branches.
|
||||
|
||||
Baseline left 48 lines of _scan_shell_command uncovered (NUL,
|
||||
dquote escapes/backtick, bare $, brace, #-in-word, [-glob, !,
|
||||
&> / &>>, pending-redirect errors on && / | / ;, >> / >| / <)
|
||||
plus three classify_verify_command lines (cwd missing, cwd escape
|
||||
via symlink, script path unvalidated without cwd). Each case below
|
||||
asserts the real segments/features/denial reason, not just that the
|
||||
scanner did not crash.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
import host
|
||||
from host import _scan_shell_command, classify_verify_command
|
||||
|
||||
|
||||
def _scan(cmd):
|
||||
return _scan_shell_command(cmd)
|
||||
|
||||
|
||||
def _denied(cmd):
|
||||
segments, features, denied = _scan(cmd)
|
||||
return denied
|
||||
|
||||
|
||||
class ScanDenialsTestCase(unittest.TestCase):
|
||||
def test_nul_and_newlines(self):
|
||||
self.assertEqual(_denied("echo a\x00b"), "命令含 NUL 字节")
|
||||
self.assertIn("换行", _denied("echo a\nb"))
|
||||
self.assertIn("换行", _denied("echo a\rb"))
|
||||
|
||||
def test_unclosed_and_command_substitution(self):
|
||||
self.assertEqual(_denied("echo 'abc"), "引号未闭合")
|
||||
self.assertEqual(_denied('echo "abc'), "引号未闭合")
|
||||
self.assertIn("$(...)", _denied("echo $(whoami)"))
|
||||
self.assertIn("反引号", _denied("echo `whoami`"))
|
||||
self.assertIn("反引号", _denied('echo "a`whoami`"'))
|
||||
self.assertIn("${ 未闭合", _denied("echo ${FOO"))
|
||||
self.assertIn("子 shell", _denied("echo (foo)"))
|
||||
self.assertIn("花括号", _denied("echo {a,b}"))
|
||||
self.assertIn("花括号", _denied("echo }"))
|
||||
self.assertIn("注释", _denied("# echo hi"))
|
||||
self.assertIn("取反", _denied("! echo hi"))
|
||||
self.assertIn("后台", _denied("pytest &"))
|
||||
self.assertIn("here-doc", _denied("cat << EOF"))
|
||||
|
||||
def test_pending_redirect_errors(self):
|
||||
self.assertEqual(_denied("pytest >"), "重定向后缺目标")
|
||||
self.assertEqual(_denied("pytest > && pytest"), "重定向后缺目标")
|
||||
self.assertEqual(_denied("pytest > | pytest"), "重定向后缺目标")
|
||||
self.assertEqual(_denied("pytest > ; pytest"), "重定向后缺目标")
|
||||
self.assertEqual(_denied("pytest > || pytest"), "重定向后缺目标")
|
||||
|
||||
|
||||
class ScanWordsTestCase(unittest.TestCase):
|
||||
def test_squote_is_literal(self):
|
||||
segments, features, denied = _scan("echo 'a$b`c'")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["words"][1]["text"], "a$b`c")
|
||||
self.assertFalse(segments[0]["words"][1]["expand"])
|
||||
self.assertNotIn("env", features)
|
||||
|
||||
def test_dquote_escape_and_dollar(self):
|
||||
segments, features, denied = _scan('echo "a\\$b"')
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["words"][1]["text"], "a$b")
|
||||
segments, features, denied = _scan('echo "$FOO"')
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("env", features)
|
||||
self.assertTrue(segments[0]["words"][1]["expand"])
|
||||
segments, features, denied = _scan('echo "${FOO}"')
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["words"][1]["text"], "${FOO}")
|
||||
self.assertIn("env", features)
|
||||
|
||||
def test_normal_backslash_escape(self):
|
||||
segments, _features, denied = _scan("echo a\\ b")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual([w["text"] for w in segments[0]["words"]],
|
||||
["echo", "a b"])
|
||||
|
||||
def test_bare_dollar_is_literal(self):
|
||||
segments, _features, denied = _scan("echo $%")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["words"][1]["text"], "$%")
|
||||
segments, _features, denied = _scan("echo $")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["words"][1]["text"], "$")
|
||||
|
||||
def test_hash_inside_word_is_literal(self):
|
||||
segments, _features, denied = _scan("echo foo#bar")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["words"][1]["text"], "foo#bar")
|
||||
|
||||
def test_bracket_glob_vs_test_command(self):
|
||||
segments, features, denied = _scan("echo foo[ab]")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("glob", features)
|
||||
self.assertTrue(segments[0]["words"][1]["glob"])
|
||||
self.assertEqual(segments[0]["words"][1]["text"], "foo[ab]")
|
||||
segments, features, denied = _scan("[ -f x ]")
|
||||
self.assertIsNone(denied)
|
||||
self.assertNotIn("glob", features)
|
||||
self.assertEqual(segments[0]["words"][0]["text"], "[")
|
||||
|
||||
def test_bang_inside_word_is_literal(self):
|
||||
segments, _features, denied = _scan("echo foo!bar")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["words"][1]["text"], "foo!bar")
|
||||
|
||||
|
||||
class ScanOperatorsTestCase(unittest.TestCase):
|
||||
def test_dollar_error_inside_dquote(self):
|
||||
self.assertIn("$(...)", _denied('echo "$(whoami)"'))
|
||||
|
||||
def test_plain_and_pipe_and_semicolon(self):
|
||||
segments, features, denied = _scan("pytest && pytest")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("compound", features)
|
||||
self.assertEqual(len(segments), 2)
|
||||
segments, features, denied = _scan("pytest | pytest")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("pipe", features)
|
||||
self.assertEqual(len(segments), 2)
|
||||
segments, features, denied = _scan("pytest ; pytest")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("compound", features)
|
||||
self.assertEqual(len(segments), 2)
|
||||
|
||||
def test_compound_or(self):
|
||||
segments, features, denied = _scan("pytest || pytest")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("compound", features)
|
||||
self.assertEqual(len(segments), 2)
|
||||
|
||||
def test_and_redirect_and_append_and_clobber(self):
|
||||
segments, features, denied = _scan("echo hi &> out.txt")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("redirect", features)
|
||||
self.assertEqual(segments[0]["redirects"][0]["target"], "out.txt")
|
||||
self.assertFalse(segments[0]["redirects"][0]["fd"])
|
||||
segments, features, denied = _scan("echo hi &>> out.txt")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("redirect", features)
|
||||
self.assertEqual(segments[0]["redirects"][0]["target"], "out.txt")
|
||||
segments, features, denied = _scan("pytest >> out.txt")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["redirects"][0]["target"], "out.txt")
|
||||
segments, features, denied = _scan("pytest >| out.txt")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["redirects"][0]["target"], "out.txt")
|
||||
segments, features, denied = _scan("cat < in.txt")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["redirects"][0]["target"], "in.txt")
|
||||
self.assertFalse(segments[0]["redirects"][0]["fd"])
|
||||
|
||||
def test_fd_redirect(self):
|
||||
segments, features, denied = _scan("pytest 2>&1")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("redirect", features)
|
||||
self.assertEqual(segments[0]["redirects"][0]["target"], "1")
|
||||
self.assertTrue(segments[0]["redirects"][0]["fd"])
|
||||
segments, features, denied = _scan("pytest 2> err.txt")
|
||||
self.assertIsNone(denied)
|
||||
self.assertEqual(segments[0]["redirects"][0]["target"], "err.txt")
|
||||
self.assertFalse(segments[0]["redirects"][0]["fd"])
|
||||
|
||||
def test_tilde_and_glob_features(self):
|
||||
_segments, features, denied = _scan("echo ~/x")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("env", features)
|
||||
_segments, features, denied = _scan("echo *.py")
|
||||
self.assertIsNone(denied)
|
||||
self.assertIn("glob", features)
|
||||
|
||||
|
||||
class ClassifyCwdTestCase(unittest.TestCase):
|
||||
def test_script_path_unvalidated_without_cwd(self):
|
||||
result = classify_verify_command("./run.sh", cwd=None)
|
||||
self.assertTrue(any("未校验" in note for note in result["notes"]),
|
||||
result)
|
||||
|
||||
def test_script_path_cwd_missing(self):
|
||||
result = classify_verify_command("./run.sh",
|
||||
cwd="/nonexistent-xyz-abc")
|
||||
self.assertEqual(result["kind"], "denied")
|
||||
self.assertIn("cwd 不存在", result["reason"])
|
||||
|
||||
def test_script_path_escapes_cwd_via_symlink(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
outside = os.path.join(tmp, "outside")
|
||||
os.mkdir(outside)
|
||||
target = os.path.join(outside, "x.sh")
|
||||
Path(target).write_text("#!/bin/sh\n")
|
||||
os.chmod(target, 0o755)
|
||||
work = os.path.join(tmp, "work")
|
||||
os.mkdir(work)
|
||||
os.symlink(outside, os.path.join(work, "link"))
|
||||
result = classify_verify_command("link/x.sh", cwd=work)
|
||||
self.assertEqual(result["kind"], "denied")
|
||||
self.assertIn("越出 cwd", result["reason"])
|
||||
|
||||
def test_script_path_inside_cwd_executable_passes(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
script = os.path.join(tmp, "run.sh")
|
||||
Path(script).write_text("#!/bin/sh\n")
|
||||
os.chmod(script, 0o755)
|
||||
result = classify_verify_command("./run.sh", cwd=tmp)
|
||||
self.assertNotEqual(result["kind"], "denied")
|
||||
|
||||
def test_absolute_and_dotdot_paths_denied(self):
|
||||
self.assertEqual(classify_verify_command("/bin/pytest")["kind"],
|
||||
"denied")
|
||||
self.assertIn("绝对路径",
|
||||
classify_verify_command("/bin/pytest")["reason"])
|
||||
self.assertEqual(classify_verify_command("../run.sh")["kind"],
|
||||
"denied")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user