import json import os import shutil import subprocess import sys import tempfile import unittest HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) sys.path.insert(0, os.path.join(ROOT, "fixtures")) sys.path.insert(0, os.path.join(ROOT, "verifier")) import serve_fixture # noqa: E402 import proof_of_done # noqa: E402 VERIFIER = os.path.join(ROOT, "verifier", "proof_of_done.py") def run(*args): p = subprocess.run([sys.executable, VERIFIER, "--json"] + list(args), capture_output=True, text=True, timeout=60) try: data = json.loads(p.stdout) if p.stdout.strip() else None except ValueError: data = None return p.returncode, data, p.stderr class FixtureCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.srv, cls.port = serve_fixture.start() cls.base = "http://127.0.0.1:%d" % cls.port @classmethod def tearDownClass(cls): cls.srv.shutdown() cls.srv.server_close() class TestScenarios(FixtureCase): def test_all_scenarios(self): with open(os.path.join(ROOT, "fixtures", "scenarios.json")) as f: sc = json.load(f) self.assertEqual(sc["fixture_file_sha256"], serve_fixture.FILE_SHA256) cats = {s["category"] for s in sc["scenarios"]} self.assertEqual(cats, {"LIVE", "CODE_ONLY"}) for s in sc["scenarios"]: with self.subTest(s["id"]): args = [a.replace("{base}", self.base) for a in s["args"]] code, data, _ = run("--url", self.base + s["path"], "--allow-local", *args) self.assertEqual(code, s["expected_exit"]) self.assertEqual(data["status"], s["expected_label"]) if s["category"] == "CODE_ONLY": self.assertNotEqual(data["status"], "LIVE_VERIFIED") class TestVerifier(FixtureCase): def test_live_has_evidence_and_timestamp(self): code, d, _ = run("--url", self.base + "/ok", "--allow-local") self.assertEqual(code, 0) self.assertEqual(d["status"], "LIVE_VERIFIED") self.assertTrue(d["checked_at_utc"].endswith("Z")) self.assertEqual(d["hops"][0]["status"], 200) self.assertTrue(all("evidence" in c for c in d["checks"])) def test_404_fails(self): code, d, _ = run("--url", self.base + "/missing", "--allow-local") self.assertEqual(code, 1) self.assertEqual(d["status"], "FAIL") def test_expect_404_passes(self): code, d, _ = run("--url", self.base + "/missing", "--allow-local", "--expect-status", "404") self.assertEqual(code, 0) def test_redirect_hops_reported(self): code, d, _ = run("--url", self.base + "/redirect", "--allow-local", "--expect-redirect-to", self.base + "/ok") self.assertEqual(code, 0) self.assertEqual([h["status"] for h in d["hops"]], [301, 200]) self.assertEqual(d["final_url"], self.base + "/ok") def test_redirect_to_wrong_place_fails(self): code, d, _ = run("--url", self.base + "/redirect", "--allow-local", "--expect-redirect-to", self.base + "/elsewhere") self.assertEqual(code, 1) def test_redirect_loop_capped(self): code, d, _ = run("--url", self.base + "/loop", "--allow-local", "--expect-redirect-to", self.base + "/ok") self.assertEqual(code, 1) self.assertEqual(len(d["hops"]), proof_of_done.MAX_HOPS + 1) self.assertFalse(all(c["ok"] for c in d["checks"])) def test_checksum_match_and_mismatch(self): code, d, _ = run("--url", self.base + "/file.bin", "--allow-local", "--sha256", serve_fixture.FILE_SHA256.upper()) self.assertEqual(code, 0) code, d, _ = run("--url", self.base + "/file.bin", "--allow-local", "--sha256", "ab" * 32) self.assertEqual(code, 1) self.assertEqual(d["status"], "FAIL") def test_sha256_file_argument(self): with tempfile.NamedTemporaryFile("w", suffix=".sha256", delete=False) as f: f.write(serve_fixture.FILE_SHA256 + " file.bin\n") try: code, d, _ = run("--url", self.base + "/file.bin", "--allow-local", "--sha256-file", f.name) self.assertEqual(code, 0) finally: os.unlink(f.name) def test_bad_sha_is_usage_error(self): code, d, err = run("--url", self.base + "/ok", "--allow-local", "--sha256", "xyz") self.assertEqual(code, 2) def test_expect_text(self): self.assertEqual(run("--url", self.base + "/ok", "--allow-local", "--expect-text", "Carehab fixture OK")[0], 0) self.assertEqual(run("--url", self.base + "/ok", "--allow-local", "--expect-text", "nope-absent")[0], 1) def test_timeout(self): code, d, _ = run("--url", self.base + "/slow", "--allow-local", "--timeout", "1") self.assertEqual(code, 1) self.assertEqual(d["status"], "CODE_ONLY") self.assertIn("timeout", d["checks"][0]["evidence"]) def test_slow_passes_with_enough_timeout(self): code, d, _ = run("--url", self.base + "/slow", "--allow-local", "--timeout", "10") self.assertEqual(code, 0) def test_connection_refused_is_code_only(self): s = serve_fixture.Server(("127.0.0.1", 0), serve_fixture.Handler) port = s.server_address[1] s.server_close() code, d, _ = run("--url", "http://127.0.0.1:%d/" % port, "--allow-local", "--timeout", "2") self.assertEqual(code, 1) self.assertEqual(d["status"], "CODE_ONLY") class TestRefusals(FixtureCase): def test_bad_schemes(self): for u in ("ftp://example.com/", "file:///etc/passwd", "gopher://x/"): with self.subTest(u): code, d, _ = run("--url", u, "--allow-local") self.assertEqual(code, 2) self.assertEqual(d["status"], "ERROR") def test_credentials_refused(self): code, d, _ = run("--url", "https://user:pw@example.com/", "--allow-local") self.assertEqual(code, 2) self.assertIn("credentials", d["error"]) def test_local_blocked_without_flag(self): code, d, _ = run("--url", self.base + "/ok") self.assertEqual(code, 2) self.assertEqual(d["status"], "ERROR") self.assertIn("non-public", d["error"]) def test_private_literals_blocked(self): for h in ("10.0.0.1", "169.254.169.254", "192.168.1.1", "[::1]"): with self.subTest(h): self.assertEqual(run("--url", "http://%s/" % h)[0], 2) def test_no_tls_bypass_flag(self): ap = proof_of_done.build_parser() opts = {o for a in ap._actions for o in a.option_strings} self.assertFalse({o for o in opts if "insecure" in o or "verify" in o or "no-check" in o}) self.assertEqual(run("--url", "https://example.com/", "--insecure")[0], 2) @unittest.skipUnless(shutil.which("openssl"), "openssl not installed; cannot create self-signed cert") class TestTLS(unittest.TestCase): def setUp(self): self.tmp = tempfile.mkdtemp() cert, key = os.path.join(self.tmp, "c.pem"), os.path.join(self.tmp, "k.pem") subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", key, "-out", cert, "-days", "1", "-subj", "/CN=127.0.0.1", "-addext", "subjectAltName=IP:127.0.0.1"], check=True, capture_output=True) self.srv, self.port = serve_fixture.start(cert, key) def tearDown(self): self.srv.shutdown() self.srv.server_close() shutil.rmtree(self.tmp, ignore_errors=True) def test_self_signed_rejected(self): code, d, _ = run("--url", "https://127.0.0.1:%d/ok" % self.port, "--allow-local", "--timeout", "5") self.assertEqual(code, 1) self.assertEqual(d["status"], "CODE_ONLY") self.assertIn("TLS", d["checks"][0]["evidence"]) class TestSkillPackaging(unittest.TestCase): def test_frontmatter_and_name_matches_dir(self): d = os.path.join(ROOT, "skills", "proof-of-done") with open(os.path.join(d, "SKILL.md")) as f: text = f.read() self.assertTrue(text.startswith("---\n")) fm, _, body = text[4:].partition("\n---\n") keys = {} for line in fm.splitlines(): k, _, v = line.partition(":") keys[k.strip()] = v.strip() self.assertEqual(set(keys), {"name", "description"}) self.assertEqual(keys["name"], os.path.basename(d)) self.assertTrue(len(keys["description"]) > 20) self.assertIn("not an enforcement gate", body) for label in ("CODE_ONLY", "LIVE", "BLOCKED"): self.assertIn(label, body) def test_manifest_consistency(self): with open(os.path.join(ROOT, "manifest.json")) as f: m = json.load(f) self.assertEqual(m["name"], "proof-of-done") self.assertEqual(m["tools_permissions"]["skill_allowed_tools"], ["Bash"]) self.assertTrue(m["install"]["scope"].startswith("Claude Code")) for p in [m["parts"]["skill"], m["parts"]["verifier"]] + m["parts"]["operator_artifacts"] + m["parts"]["fixtures"]: self.assertTrue(os.path.exists(os.path.join(ROOT, p)), p) if __name__ == "__main__": unittest.main()