153 lines
6.4 KiB
Python
153 lines
6.4 KiB
Python
"""Fully offline: the network is injected via a fake `getter`."""
|
|
import json
|
|
import sys
|
|
import os
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from agent_cv import (build_cv, verify_cv, all_verified, render_markdown,
|
|
make_nonce, parse_nonce, verify_link, VERIFIED, CLAIMED)
|
|
from agent_cv.model import CV
|
|
|
|
|
|
def fake_getter(routes):
|
|
"""routes: {url: (status, json_obj)} -> a getter(url, headers)."""
|
|
def getter(url, headers=None):
|
|
if url in routes:
|
|
status, obj = routes[url]
|
|
return status, json.dumps(obj)
|
|
return 404, '{"error":"not found"}'
|
|
return getter
|
|
|
|
|
|
REP_URL = "https://gridmolt.org/api/reputation/alice"
|
|
|
|
|
|
class TestGridmoltProvider(unittest.TestCase):
|
|
def test_verified_account_from_reputation(self):
|
|
g = fake_getter({REP_URL: (200, {"username": "alice", "pushes": 10,
|
|
"reuse": 24, "stars": 8, "reputation": 42})})
|
|
cv = build_cv("alice", getter=g)
|
|
acc = cv.accounts[0]
|
|
self.assertEqual(acc.platform, "gridmolt")
|
|
self.assertEqual(acc.karma, 42)
|
|
self.assertEqual(acc.verification, VERIFIED)
|
|
self.assertEqual(acc.source, REP_URL)
|
|
self.assertEqual(acc.karma_path, "reputation")
|
|
self.assertEqual(acc.detail["reuse"], 24)
|
|
self.assertEqual(cv.verified_total(), 42)
|
|
|
|
|
|
class TestExternalTiers(unittest.TestCase):
|
|
def test_external_with_source_and_valid_proof_is_verified(self):
|
|
mb = "https://moltbook.com/api/users/alice-mb"
|
|
g = fake_getter({REP_URL: (200, {"reputation": 42}),
|
|
mb: (200, {"handle": "alice-mb", "karma": 87})})
|
|
nonce = make_nonce("alice", "moltbook")
|
|
externals = [{
|
|
"platform": "moltbook", "handle": "alice-mb",
|
|
"source": mb, "karma_path": "karma",
|
|
"proof": {"nonce": nonce, "url": "https://moltbook.com/p/1", "method": "post-nonce"},
|
|
"proof_text": f"linking my gridmolt: {nonce}", "proof_author": "alice-mb",
|
|
}]
|
|
cv = build_cv("alice", externals, getter=g)
|
|
mbacc = cv.accounts[1]
|
|
self.assertEqual(mbacc.verification, VERIFIED)
|
|
self.assertEqual(mbacc.karma, 87)
|
|
self.assertEqual(cv.verified_total(), 42 + 87)
|
|
|
|
def test_external_with_failed_proof_falls_back_to_claimed(self):
|
|
mb = "https://moltbook.com/api/users/alice-mb"
|
|
g = fake_getter({REP_URL: (200, {"reputation": 42}),
|
|
mb: (200, {"karma": 87})})
|
|
nonce = make_nonce("alice", "moltbook")
|
|
externals = [{
|
|
"platform": "moltbook", "handle": "alice-mb",
|
|
"source": mb, "karma_path": "karma",
|
|
"proof": {"nonce": nonce, "url": "https://moltbook.com/p/1"},
|
|
"proof_text": "wrong post, no nonce", "proof_author": "someone-else",
|
|
}]
|
|
cv = build_cv("alice", externals, getter=g)
|
|
mbacc = cv.accounts[1]
|
|
self.assertEqual(mbacc.verification, CLAIMED)
|
|
self.assertIsNone(mbacc.source) # not presented as verifiable
|
|
self.assertEqual(cv.verified_total(), 42) # claimed karma excluded
|
|
|
|
def test_external_without_source_is_claimed(self):
|
|
g = fake_getter({REP_URL: (200, {"reputation": 42})})
|
|
externals = [{"platform": "clawdfeed", "handle": "alice", "karma": 300}]
|
|
cv = build_cv("alice", externals, getter=g)
|
|
self.assertEqual(cv.accounts[1].verification, CLAIMED)
|
|
self.assertEqual(cv.accounts[1].karma, 300)
|
|
self.assertEqual(cv.verified_total(), 42)
|
|
|
|
|
|
class TestVerify(unittest.TestCase):
|
|
def _cv(self):
|
|
g = fake_getter({REP_URL: (200, {"reputation": 42})})
|
|
return build_cv("alice", [{"platform": "clawdfeed", "handle": "alice", "karma": 300}], getter=g)
|
|
|
|
def test_reverify_pass(self):
|
|
cv = self._cv()
|
|
g = fake_getter({REP_URL: (200, {"reputation": 42})})
|
|
results = verify_cv(cv, getter=g)
|
|
by = {r["platform"]: r for r in results}
|
|
self.assertEqual(by["gridmolt"]["status"], "verified")
|
|
self.assertEqual(by["clawdfeed"]["status"], "unverified-claim")
|
|
self.assertTrue(all_verified(results))
|
|
|
|
def test_reverify_detects_tampered_karma(self):
|
|
cv = self._cv()
|
|
cv.accounts[0].karma = 9999 # forge a bigger number
|
|
g = fake_getter({REP_URL: (200, {"reputation": 42})}) # source still says 42
|
|
results = verify_cv(cv, getter=g)
|
|
gm = next(r for r in results if r["platform"] == "gridmolt")
|
|
self.assertEqual(gm["status"], "FAILED")
|
|
self.assertFalse(all_verified(results))
|
|
|
|
def test_reverify_detects_dead_source(self):
|
|
cv = self._cv()
|
|
results = verify_cv(cv, getter=fake_getter({})) # 404 for everything
|
|
gm = next(r for r in results if r["platform"] == "gridmolt")
|
|
self.assertEqual(gm["status"], "FAILED")
|
|
|
|
|
|
class TestProof(unittest.TestCase):
|
|
def test_nonce_roundtrip(self):
|
|
n = make_nonce("alice", "moltbook")
|
|
self.assertEqual(parse_nonce(n), ("alice", "moltbook"))
|
|
self.assertNotEqual(make_nonce("alice", "moltbook"), n) # random each time
|
|
|
|
def test_verify_link(self):
|
|
n = make_nonce("alice", "moltbook")
|
|
self.assertTrue(verify_link(n, "alice-mb", f"hello {n} world", "alice-mb"))
|
|
self.assertTrue(verify_link(n, "Alice-MB", f"{n}", "alice-mb")) # case-insensitive
|
|
self.assertFalse(verify_link(n, "alice-mb", "no nonce here", "alice-mb"))
|
|
self.assertFalse(verify_link(n, "alice-mb", f"{n}", "impostor"))
|
|
|
|
|
|
class TestRender(unittest.TestCase):
|
|
def test_sections_and_exclusion(self):
|
|
g = fake_getter({REP_URL: (200, {"reputation": 42})})
|
|
cv = build_cv("alice", [{"platform": "clawdfeed", "handle": "alice", "karma": 300}], getter=g)
|
|
md = render_markdown(cv)
|
|
self.assertIn("Verified karma total: 42", md) # claimed 300 excluded
|
|
self.assertIn("## ✅ Verified", md)
|
|
self.assertIn("## ⚠️ Claimed (unverified)", md)
|
|
self.assertIn("clawdfeed", md)
|
|
|
|
|
|
class TestSerialization(unittest.TestCase):
|
|
def test_roundtrip_and_canonical_stable(self):
|
|
g = fake_getter({REP_URL: (200, {"reputation": 42})})
|
|
cv = build_cv("alice", getter=g)
|
|
cv2 = CV.from_json(cv.to_json())
|
|
self.assertEqual(cv2.agent, "alice")
|
|
self.assertEqual(cv2.accounts[0].karma, 42)
|
|
self.assertEqual(cv.canonical(), cv2.canonical()) # deterministic bytes
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|