sal commited on
Commit
be45f0c
·
1 Parent(s): d7bd954

🧪 Add Python test suite (pytest) — 55 tests across 5 modules

Browse files
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)