Spaces:
Sleeping
Sleeping
sal commited on
Commit ·
be45f0c
1
Parent(s): d7bd954
🧪 Add Python test suite (pytest) — 55 tests across 5 modules
Browse files- pytest.ini +3 -0
- tests/__init__.py +1 -0
- tests/test_api.py +142 -0
- tests/test_guardian.py +148 -0
- tests/test_inference.py +73 -0
- tests/test_simulation.py +54 -0
- tests/test_stellar_client.py +65 -0
pytest.ini
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pytest]
|
| 2 |
+
testpaths = tests
|
| 3 |
+
addopts = -v --tb=short
|
tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# ZeroSense test suite
|
tests/test_api.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for FastAPI endpoints (api/main.py).
|
| 2 |
+
|
| 3 |
+
Uses FastAPI TestClient — no server startup needed.
|
| 4 |
+
"""
|
| 5 |
+
import pytest
|
| 6 |
+
from fastapi.testclient import TestClient
|
| 7 |
+
from api.main import app
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@pytest.fixture(scope="module")
|
| 11 |
+
def c():
|
| 12 |
+
return TestClient(app)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ── Root ──
|
| 16 |
+
class TestRoot:
|
| 17 |
+
def test_200(self, c): assert c.get("/").status_code == 200
|
| 18 |
+
def test_project(self, c): assert c.get("/").json()["project"] == "ZeroSense"
|
| 19 |
+
def test_status(self, c): assert c.get("/").json()["status"] == "live"
|
| 20 |
+
def test_hackathon(self, c): assert "Stellar Hacks" in c.get("/").json()["hackathon"]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# ── Generate Proof ──
|
| 24 |
+
class TestGenerateProof:
|
| 25 |
+
def _req(self, c, pixels=None, task_id=None):
|
| 26 |
+
body = {
|
| 27 |
+
"robot_id": "robot-001",
|
| 28 |
+
"sensor_frames": [{"pixels": pixels or [0.8]*64, "frame_id": 0}],
|
| 29 |
+
}
|
| 30 |
+
if task_id:
|
| 31 |
+
body["task_id"] = task_id
|
| 32 |
+
return c.post("/generate-proof", json=body)
|
| 33 |
+
|
| 34 |
+
def test_200(self, c): assert self._req(c).status_code == 200
|
| 35 |
+
def test_has_task_id(self, c): assert "task_id" in self._req(c).json()
|
| 36 |
+
def test_has_proof_hex(self, c): assert "proof_hex" in self._req(c).json()
|
| 37 |
+
def test_has_confidence(self, c): assert "confidence" in self._req(c).json()
|
| 38 |
+
def test_has_action_label(self, c): assert "action_label" in self._req(c).json()
|
| 39 |
+
def test_has_input_hash(self, c): assert "input_hash" in self._req(c).json()
|
| 40 |
+
def test_confidence_range(self, c):
|
| 41 |
+
assert 0 <= self._req(c).json()["confidence"] <= 100
|
| 42 |
+
def test_action_label_valid(self, c):
|
| 43 |
+
assert self._req(c).json()["action_label"] in ("task_complete","obstacle_detected","incident")
|
| 44 |
+
def test_explicit_task_id(self, c):
|
| 45 |
+
r = self._req(c, task_id="mytask00000000000000000000000001")
|
| 46 |
+
assert r.json()["task_id"] == "mytask00000000000000000000000001"
|
| 47 |
+
def test_mock_proof_512_chars(self, c):
|
| 48 |
+
assert len(self._req(c).json()["proof_hex"]) == 512
|
| 49 |
+
def test_multi_frame(self, c):
|
| 50 |
+
r = c.post("/generate-proof", json={
|
| 51 |
+
"robot_id": "robot-001",
|
| 52 |
+
"sensor_frames": [
|
| 53 |
+
{"pixels": [0.8]*64, "frame_id": 0},
|
| 54 |
+
{"pixels": [0.9]*64, "frame_id": 1},
|
| 55 |
+
]
|
| 56 |
+
})
|
| 57 |
+
assert r.status_code == 200
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# ── Verify Proof ──
|
| 61 |
+
class TestVerifyProof:
|
| 62 |
+
def _verify(self, c, confidence=97, task_id="task_v_001"):
|
| 63 |
+
return c.post("/verify-proof", json={
|
| 64 |
+
"robot_id": "robot-001", "task_id": task_id,
|
| 65 |
+
"proof_hex": "0"*512, "model_hash": "mobilenet_v2_int8",
|
| 66 |
+
"confidence": confidence, "action_type": 0,
|
| 67 |
+
})
|
| 68 |
+
|
| 69 |
+
def test_200(self, c): assert self._verify(c).status_code == 200
|
| 70 |
+
def test_status_verified(self, c): assert self._verify(c).json()["status"] == "verified"
|
| 71 |
+
def test_auto_pay_high_conf(self, c): assert self._verify(c, 98, "task_v_002").json()["auto_payment"] is True
|
| 72 |
+
def test_no_auto_pay_low_conf(self, c): assert self._verify(c, 80, "task_v_003").json()["auto_payment"] is False
|
| 73 |
+
def test_has_stellar_tx(self, c): assert self._verify(c, task_id="task_v_004").json()["stellar_tx"] is not None
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# ── Claim Payment ──
|
| 77 |
+
class TestClaimPayment:
|
| 78 |
+
def test_200(self, c):
|
| 79 |
+
assert c.post("/claim-payment", json={"task_id":"task_c_001","confidence":96}).status_code == 200
|
| 80 |
+
def test_paid_status(self, c):
|
| 81 |
+
assert c.post("/claim-payment", json={"task_id":"task_c_002","confidence":97}).json()["status"] == "paid"
|
| 82 |
+
def test_has_stellar_tx(self, c):
|
| 83 |
+
assert "stellar_tx" in c.post("/claim-payment", json={"task_id":"task_c_003","confidence":95}).json()
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# ── Insurance Claim ──
|
| 87 |
+
class TestInsurance:
|
| 88 |
+
def test_200(self, c):
|
| 89 |
+
r = c.post("/file-insurance-claim", json={
|
| 90 |
+
"robot_id":"robot-001","incident_proof_hash":"abc"*21+"d","claim_amount":1_000_000
|
| 91 |
+
})
|
| 92 |
+
assert r.status_code == 200
|
| 93 |
+
def test_filed_status(self, c):
|
| 94 |
+
r = c.post("/file-insurance-claim", json={
|
| 95 |
+
"robot_id":"robot-001","incident_proof_hash":"def"*21+"g","claim_amount":500_000
|
| 96 |
+
})
|
| 97 |
+
assert r.json()["status"] == "filed"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ── Robot Identity ──
|
| 101 |
+
class TestIdentity:
|
| 102 |
+
def test_200(self, c):
|
| 103 |
+
r = c.post("/robot/register-identity", json={
|
| 104 |
+
"robot_id":"robot-001","sensor_noise_sample":[0.01]*64
|
| 105 |
+
})
|
| 106 |
+
assert r.status_code == 200
|
| 107 |
+
def test_has_identity_hash(self, c):
|
| 108 |
+
r = c.post("/robot/register-identity", json={
|
| 109 |
+
"robot_id":"robot-002","sensor_noise_sample":[0.02]*64
|
| 110 |
+
})
|
| 111 |
+
assert len(r.json()["identity_hash"]) == 64
|
| 112 |
+
def test_different_robots_different_hashes(self, c):
|
| 113 |
+
r1 = c.post("/robot/register-identity", json={"robot_id":"rA","sensor_noise_sample":[0.01]*64})
|
| 114 |
+
r2 = c.post("/robot/register-identity", json={"robot_id":"rB","sensor_noise_sample":[0.99]*64})
|
| 115 |
+
assert r1.json()["identity_hash"] != r2.json()["identity_hash"]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# ── Robot Status + Fleet ──
|
| 119 |
+
class TestStatus:
|
| 120 |
+
def test_robot_status_200(self, c): assert c.get("/robot/robot-001/status").status_code == 200
|
| 121 |
+
def test_robot_has_fields(self, c):
|
| 122 |
+
d = c.get("/robot/robot-001/status").json()
|
| 123 |
+
assert "robot_id" in d and "zrep_balance" in d
|
| 124 |
+
def test_fleet_200(self, c): assert c.get("/fleet/report").status_code == 200
|
| 125 |
+
def test_fleet_has_fields(self, c):
|
| 126 |
+
d = c.get("/fleet/report").json()
|
| 127 |
+
assert "total_tasks_verified" in d and "guardian_status" in d
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# ── Guardian ──
|
| 131 |
+
class TestGuardian:
|
| 132 |
+
def test_start_200(self, c): assert c.post("/guardian/start").status_code == 200
|
| 133 |
+
def test_start_status(self, c):
|
| 134 |
+
assert c.post("/guardian/start").json()["status"] in ("started","already_running")
|
| 135 |
+
def test_stop_200(self, c): assert c.post("/guardian/stop").status_code == 200
|
| 136 |
+
def test_stop_status(self, c): assert c.post("/guardian/stop").json()["status"] == "stopped"
|
| 137 |
+
def test_agents_count(self, c):
|
| 138 |
+
c.post("/guardian/stop")
|
| 139 |
+
r = c.post("/guardian/start")
|
| 140 |
+
if "agents" in r.json():
|
| 141 |
+
assert len(r.json()["agents"]) == 7
|
| 142 |
+
c.post("/guardian/stop")
|
tests/test_guardian.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for ZeroSense Guardian v2 — all 7 autonomous agents."""
|
| 2 |
+
import asyncio
|
| 3 |
+
import pytest
|
| 4 |
+
from unittest.mock import AsyncMock, MagicMock
|
| 5 |
+
from api.agents.guardian import (
|
| 6 |
+
ZeroSenseGuardianV2, PaymentAgent, AnomalyAgent,
|
| 7 |
+
InsuranceAgent, ReputationAgent,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@pytest.fixture
|
| 12 |
+
def stellar():
|
| 13 |
+
m = MagicMock()
|
| 14 |
+
m.get_recent_contract_events = AsyncMock(return_value=[])
|
| 15 |
+
m.claim_payment = AsyncMock(return_value="tx_mock_paid")
|
| 16 |
+
m.file_insurance_claim = AsyncMock(return_value=42)
|
| 17 |
+
return m
|
| 18 |
+
|
| 19 |
+
@pytest.fixture
|
| 20 |
+
def guardian(stellar):
|
| 21 |
+
return ZeroSenseGuardianV2(stellar_client=stellar)
|
| 22 |
+
|
| 23 |
+
def run(coro):
|
| 24 |
+
return asyncio.get_event_loop().run_until_complete(coro)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class TestGuardianInit:
|
| 28 |
+
def test_not_running(self, guardian): assert guardian.running is False
|
| 29 |
+
def test_has_7_agents(self, guardian): assert len(guardian.agents) == 7
|
| 30 |
+
def test_has_payment(self, guardian): assert "payment" in guardian.agents
|
| 31 |
+
def test_has_anomaly(self, guardian): assert "anomaly" in guardian.agents
|
| 32 |
+
def test_has_insurance(self, guardian): assert "insurance" in guardian.agents
|
| 33 |
+
def test_has_reputation(self, guardian): assert "reputation" in guardian.agents
|
| 34 |
+
def test_has_learning(self, guardian): assert "learning" in guardian.agents
|
| 35 |
+
def test_has_oracle(self, guardian): assert "oracle" in guardian.agents
|
| 36 |
+
def test_has_assistant(self, guardian): assert "assistant" in guardian.agents
|
| 37 |
+
def test_status_returns_all(self, guardian):
|
| 38 |
+
assert set(guardian.status().keys()) == {
|
| 39 |
+
"payment","anomaly","insurance","reputation","learning","oracle","assistant"
|
| 40 |
+
}
|
| 41 |
+
def test_initial_counts_zero(self, guardian):
|
| 42 |
+
for info in guardian.status().values():
|
| 43 |
+
assert info["processed"] == 0
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class TestPaymentAgent:
|
| 47 |
+
def test_init(self, stellar):
|
| 48 |
+
a = PaymentAgent(stellar)
|
| 49 |
+
assert a.threshold == 0.95 and len(a.paid_tasks) == 0
|
| 50 |
+
|
| 51 |
+
def test_no_events_no_pay(self, stellar):
|
| 52 |
+
a = PaymentAgent(stellar)
|
| 53 |
+
run(a.tick())
|
| 54 |
+
assert a.processed_count == 0
|
| 55 |
+
|
| 56 |
+
def test_high_conf_pays(self, stellar):
|
| 57 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 58 |
+
return_value=[{"task_id":"t001","confidence":97}]
|
| 59 |
+
)
|
| 60 |
+
a = PaymentAgent(stellar)
|
| 61 |
+
run(a.tick())
|
| 62 |
+
assert a.processed_count == 1
|
| 63 |
+
stellar.claim_payment.assert_called_once()
|
| 64 |
+
|
| 65 |
+
def test_low_conf_skips(self, stellar):
|
| 66 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 67 |
+
return_value=[{"task_id":"t002","confidence":80}]
|
| 68 |
+
)
|
| 69 |
+
a = PaymentAgent(stellar)
|
| 70 |
+
run(a.tick())
|
| 71 |
+
assert a.processed_count == 0
|
| 72 |
+
stellar.claim_payment.assert_not_called()
|
| 73 |
+
|
| 74 |
+
def test_no_duplicate_pay(self, stellar):
|
| 75 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 76 |
+
return_value=[{"task_id":"t003","confidence":98}]
|
| 77 |
+
)
|
| 78 |
+
a = PaymentAgent(stellar)
|
| 79 |
+
run(a.tick())
|
| 80 |
+
run(a.tick()) # second tick — same task
|
| 81 |
+
assert stellar.claim_payment.call_count == 1
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class TestAnomalyAgent:
|
| 85 |
+
def test_high_conf_no_count(self, stellar):
|
| 86 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 87 |
+
return_value=[{"robot_id":"r1","confidence":97}]
|
| 88 |
+
)
|
| 89 |
+
a = AnomalyAgent(stellar)
|
| 90 |
+
run(a.tick())
|
| 91 |
+
assert a.low_confidence_counts.get("r1", 0) == 0
|
| 92 |
+
|
| 93 |
+
def test_low_conf_increments(self, stellar):
|
| 94 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 95 |
+
return_value=[{"robot_id":"r1","confidence":70}]
|
| 96 |
+
)
|
| 97 |
+
a = AnomalyAgent(stellar)
|
| 98 |
+
run(a.tick())
|
| 99 |
+
assert a.low_confidence_counts["r1"] == 1
|
| 100 |
+
|
| 101 |
+
def test_killswitch_at_threshold(self, stellar):
|
| 102 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 103 |
+
return_value=[{"robot_id":"r-bad","confidence":50}]
|
| 104 |
+
)
|
| 105 |
+
a = AnomalyAgent(stellar, anomaly_threshold=3)
|
| 106 |
+
for _ in range(3):
|
| 107 |
+
run(a.tick())
|
| 108 |
+
assert a.processed_count >= 1
|
| 109 |
+
|
| 110 |
+
def test_good_reading_resets(self, stellar):
|
| 111 |
+
a = AnomalyAgent(stellar)
|
| 112 |
+
a.low_confidence_counts["r1"] = 2
|
| 113 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 114 |
+
return_value=[{"robot_id":"r1","confidence":98}]
|
| 115 |
+
)
|
| 116 |
+
run(a.tick())
|
| 117 |
+
assert a.low_confidence_counts["r1"] == 0
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class TestInsuranceAgent:
|
| 121 |
+
def test_files_claim(self, stellar):
|
| 122 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 123 |
+
return_value=[{"proof_hash":"deadbeef"*8,"robot_id":"r1","estimated_claim":1_000_000}]
|
| 124 |
+
)
|
| 125 |
+
a = InsuranceAgent(stellar)
|
| 126 |
+
run(a.tick())
|
| 127 |
+
assert a.processed_count == 1
|
| 128 |
+
stellar.file_insurance_claim.assert_called_once()
|
| 129 |
+
|
| 130 |
+
def test_no_duplicate_claim(self, stellar):
|
| 131 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 132 |
+
return_value=[{"proof_hash":"cafebabe"*8,"robot_id":"r1","estimated_claim":500_000}]
|
| 133 |
+
)
|
| 134 |
+
a = InsuranceAgent(stellar)
|
| 135 |
+
run(a.tick())
|
| 136 |
+
run(a.tick())
|
| 137 |
+
assert stellar.file_insurance_claim.call_count == 1
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class TestReputationAgent:
|
| 141 |
+
def test_processes_payment_event(self, stellar):
|
| 142 |
+
stellar.get_recent_contract_events = AsyncMock(
|
| 143 |
+
return_value=[{"task_id":"trep1","robot_id":"r1","confidence":97}]
|
| 144 |
+
)
|
| 145 |
+
a = ReputationAgent(stellar)
|
| 146 |
+
run(a.tick())
|
| 147 |
+
assert a.processed_count == 1
|
| 148 |
+
assert "trep1" in a.processed_tasks
|
tests/test_inference.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for AI inference engine (model/inference.py).
|
| 2 |
+
|
| 3 |
+
All 10 tests run in mock mode — no ONNX file required.
|
| 4 |
+
"""
|
| 5 |
+
import sys, os
|
| 6 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
| 7 |
+
|
| 8 |
+
import pytest
|
| 9 |
+
from model.inference import RobotInferenceEngine, ACTION_LABELS
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@pytest.fixture
|
| 13 |
+
def engine():
|
| 14 |
+
return RobotInferenceEngine()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class TestRobotInferenceEngine:
|
| 18 |
+
|
| 19 |
+
def test_engine_initializes(self, engine):
|
| 20 |
+
assert engine is not None
|
| 21 |
+
|
| 22 |
+
def test_model_hash_set(self, engine):
|
| 23 |
+
assert engine.model_hash is not None
|
| 24 |
+
assert len(engine.model_hash) > 0
|
| 25 |
+
|
| 26 |
+
def test_run_inference_returns_triple(self, engine):
|
| 27 |
+
frames = [[0.5] * 64]
|
| 28 |
+
result = engine.run_inference(frames)
|
| 29 |
+
assert len(result) == 3
|
| 30 |
+
|
| 31 |
+
def test_action_in_valid_range(self, engine):
|
| 32 |
+
action, _, _ = engine.run_inference([[0.5] * 64])
|
| 33 |
+
assert action in (0, 1, 2)
|
| 34 |
+
|
| 35 |
+
def test_confidence_in_valid_range(self, engine):
|
| 36 |
+
_, confidence, _ = engine.run_inference([[0.5] * 64])
|
| 37 |
+
assert 0 <= confidence <= 100
|
| 38 |
+
|
| 39 |
+
def test_input_hash_is_sha256(self, engine):
|
| 40 |
+
_, _, h = engine.run_inference([[0.5] * 64])
|
| 41 |
+
assert len(h) == 64
|
| 42 |
+
|
| 43 |
+
def test_high_avg_gives_task_complete(self, engine):
|
| 44 |
+
action, confidence, _ = engine.run_inference([[0.9] * 64])
|
| 45 |
+
assert action == 0 # task_complete
|
| 46 |
+
assert confidence >= 90
|
| 47 |
+
|
| 48 |
+
def test_mid_avg_gives_obstacle(self, engine):
|
| 49 |
+
action, _, _ = engine.run_inference([[0.55] * 64])
|
| 50 |
+
assert action == 1 # obstacle_detected
|
| 51 |
+
|
| 52 |
+
def test_low_avg_gives_low_confidence(self, engine):
|
| 53 |
+
_, confidence, _ = engine.run_inference([[0.2] * 64])
|
| 54 |
+
assert confidence < 80
|
| 55 |
+
|
| 56 |
+
def test_empty_frames_returns_incident(self, engine):
|
| 57 |
+
action, _, _ = engine.run_inference([])
|
| 58 |
+
assert action == 2 # incident
|
| 59 |
+
|
| 60 |
+
def test_hash_is_deterministic(self, engine):
|
| 61 |
+
_, _, h1 = engine.run_inference([[0.5] * 64])
|
| 62 |
+
_, _, h2 = engine.run_inference([[0.5] * 64])
|
| 63 |
+
assert h1 == h2
|
| 64 |
+
|
| 65 |
+
def test_different_inputs_different_hashes(self, engine):
|
| 66 |
+
_, _, h1 = engine.run_inference([[0.1] * 64])
|
| 67 |
+
_, _, h2 = engine.run_inference([[0.9] * 64])
|
| 68 |
+
assert h1 != h2
|
| 69 |
+
|
| 70 |
+
def test_action_labels_complete(self):
|
| 71 |
+
assert ACTION_LABELS[0] == "task_complete"
|
| 72 |
+
assert ACTION_LABELS[1] == "obstacle_detected"
|
| 73 |
+
assert ACTION_LABELS[2] == "incident"
|
tests/test_simulation.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for warehouse robot simulation (simulation/robot_sim.py)."""
|
| 2 |
+
import math
|
| 3 |
+
import pytest
|
| 4 |
+
from simulation.robot_sim import WarehouseRobot
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@pytest.fixture
|
| 8 |
+
def robot():
|
| 9 |
+
return WarehouseRobot(robot_id="test-robot-001")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class TestWarehouseRobot:
|
| 13 |
+
def test_init(self, robot):
|
| 14 |
+
assert robot.robot_id == "test-robot-001"
|
| 15 |
+
assert robot.position == [0.0, 0.0]
|
| 16 |
+
assert robot.task_count == 0
|
| 17 |
+
|
| 18 |
+
def test_sensor_frame_64_values(self, robot):
|
| 19 |
+
assert len(robot.capture_sensor_frame()) == 64
|
| 20 |
+
|
| 21 |
+
def test_sensor_values_in_range(self, robot):
|
| 22 |
+
for v in robot.capture_sensor_frame():
|
| 23 |
+
assert 0.0 <= v <= 1.0
|
| 24 |
+
|
| 25 |
+
def test_generate_task_data_num_frames(self, robot):
|
| 26 |
+
assert len(robot.generate_task_sensor_data(num_frames=5)) == 5
|
| 27 |
+
|
| 28 |
+
def test_generate_task_data_frame_shape(self, robot):
|
| 29 |
+
for frame in robot.generate_task_sensor_data(num_frames=3):
|
| 30 |
+
assert len(frame) == 64
|
| 31 |
+
|
| 32 |
+
def test_navigate_already_at_target(self, robot):
|
| 33 |
+
robot.position = [0.0, 0.0]
|
| 34 |
+
assert robot.navigate_to_task((0.05, 0.05)) is True
|
| 35 |
+
|
| 36 |
+
def test_navigate_moves_toward_target(self, robot):
|
| 37 |
+
robot.position = [0.0, 0.0]
|
| 38 |
+
robot.navigate_to_task((5.0, 0.0))
|
| 39 |
+
assert robot.position[0] > 0.0
|
| 40 |
+
|
| 41 |
+
def test_navigate_eventually_reaches(self, robot):
|
| 42 |
+
robot.position = [0.0, 0.0]
|
| 43 |
+
reached = any(robot.navigate_to_task((1.0, 0.0)) for _ in range(100))
|
| 44 |
+
assert reached
|
| 45 |
+
|
| 46 |
+
def test_heading_updates(self, robot):
|
| 47 |
+
robot.position = [0.0, 0.0]
|
| 48 |
+
robot.navigate_to_task((0.0, 5.0))
|
| 49 |
+
assert abs(robot.heading - math.pi / 2) < 0.01
|
| 50 |
+
|
| 51 |
+
def test_frames_have_noise(self, robot):
|
| 52 |
+
f1, f2 = robot.capture_sensor_frame(), robot.capture_sensor_frame()
|
| 53 |
+
diffs = sum(1 for a, b in zip(f1, f2) if abs(a-b) > 0.001)
|
| 54 |
+
assert diffs > 0
|
tests/test_stellar_client.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for Stellar SDK client (api/stellar/client.py).
|
| 2 |
+
|
| 3 |
+
Runs in mock/dev mode — no real Stellar keys needed.
|
| 4 |
+
"""
|
| 5 |
+
import asyncio
|
| 6 |
+
import pytest
|
| 7 |
+
from api.stellar.client import StellarClient
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@pytest.fixture
|
| 11 |
+
def client():
|
| 12 |
+
return StellarClient(secret_key="", network="testnet")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TestStellarClientInit:
|
| 16 |
+
|
| 17 |
+
def test_client_initializes(self, client):
|
| 18 |
+
assert client is not None
|
| 19 |
+
assert client.network == "testnet"
|
| 20 |
+
|
| 21 |
+
def test_testnet_urls(self, client):
|
| 22 |
+
assert "testnet" in client.horizon_url
|
| 23 |
+
assert "testnet" in client.soroban_url
|
| 24 |
+
|
| 25 |
+
def test_public_key_length(self, client):
|
| 26 |
+
assert len(client.keypair.public_key) == 56
|
| 27 |
+
|
| 28 |
+
def test_public_key_starts_with_G(self, client):
|
| 29 |
+
assert client.keypair.public_key.startswith("G")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class TestStellarClientAsync:
|
| 33 |
+
|
| 34 |
+
def _run(self, coro):
|
| 35 |
+
return asyncio.get_event_loop().run_until_complete(coro)
|
| 36 |
+
|
| 37 |
+
def test_verify_proof_returns_tx(self, client):
|
| 38 |
+
tx = self._run(client.verify_proof_on_chain(
|
| 39 |
+
proof_hex="00" * 256, robot_id="robot-001",
|
| 40 |
+
task_id="task_test_verify", model_hash="mobilenet_v2_int8",
|
| 41 |
+
confidence=97, action_type=0,
|
| 42 |
+
))
|
| 43 |
+
assert "verified" in tx
|
| 44 |
+
|
| 45 |
+
def test_claim_payment_returns_tx(self, client):
|
| 46 |
+
tx = self._run(client.claim_payment(task_id="task_pay_001", confidence=95))
|
| 47 |
+
assert "paid" in tx
|
| 48 |
+
|
| 49 |
+
def test_insurance_claim_returns_id(self, client):
|
| 50 |
+
cid = self._run(client.file_insurance_claim(
|
| 51 |
+
robot_id="robot-001", proof_hash="abc" * 20, claim_amount=1_000_000
|
| 52 |
+
))
|
| 53 |
+
assert isinstance(cid, int) and cid >= 1
|
| 54 |
+
|
| 55 |
+
def test_zrep_balance_returns_int(self, client):
|
| 56 |
+
b = self._run(client.get_zrep_balance("robot-001"))
|
| 57 |
+
assert isinstance(b, int)
|
| 58 |
+
|
| 59 |
+
def test_mint_soulbound_returns_tx(self, client):
|
| 60 |
+
tx = self._run(client.mint_soulbound_identity_token("robot-001", "deadbeef" * 8))
|
| 61 |
+
assert "identity" in tx
|
| 62 |
+
|
| 63 |
+
def test_events_returns_list(self, client):
|
| 64 |
+
events = self._run(client.get_recent_contract_events("RobotActionVerified"))
|
| 65 |
+
assert isinstance(events, list)
|