Skip to content

Commit 7f92b7e

Browse files
committed
Add contract model serialization and validation tests
1 parent 97bf89a commit 7f92b7e

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

tests/test_contracts.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
from sourceos_syncd.contracts import (
9+
ActorRecord,
10+
AgentObjectTransactionRecord,
11+
ConflictRecord,
12+
ContractError,
13+
DeviceTrustRecord,
14+
IntegrityEventRecord,
15+
PolicyDecisionRecord,
16+
ProfileRecord,
17+
SchemaContractRecord,
18+
SourceObjectRecord,
19+
SyncPlanRecord,
20+
to_json_dict,
21+
validate_contract,
22+
)
23+
24+
EXAMPLE_DIR = Path("examples/contracts")
25+
26+
EXPECTED_SCHEMAS = {
27+
"sourceos.profile-record/v1alpha1",
28+
"sourceos.device-record/v1alpha1",
29+
"sourceos.actor-record/v1alpha1",
30+
"sourceos.schema-record/v1alpha1",
31+
"sourceos.object-record/v1alpha1",
32+
"sourceos.sync-plan/v1alpha1",
33+
"sourceos.conflict-record/v1alpha1",
34+
"sourceos.policy-decision/v1alpha1",
35+
"sourceos.integrity-event/v1alpha1",
36+
"sourceos.agent-object-transaction/v1alpha1",
37+
}
38+
39+
40+
def load_example(name: str) -> dict:
41+
return json.loads((EXAMPLE_DIR / name).read_text(encoding="utf-8"))
42+
43+
44+
def test_all_contract_fixtures_validate() -> None:
45+
fixtures = sorted(EXAMPLE_DIR.glob("*.json"))
46+
assert fixtures, "expected contract fixtures"
47+
48+
seen_schemas: set[str] = set()
49+
for path in fixtures:
50+
record = json.loads(path.read_text(encoding="utf-8"))
51+
validated = validate_contract(record)
52+
assert validated == record
53+
seen_schemas.add(record["schema"])
54+
55+
assert EXPECTED_SCHEMAS <= seen_schemas
56+
57+
58+
def test_contracts_serialize_cleanly_to_json() -> None:
59+
actor = ActorRecord.from_dict(load_example("actor.agent-one.json"))
60+
obj = SourceObjectRecord.from_dict(load_example("object.alpha.json"))
61+
event = IntegrityEventRecord.from_dict(load_example("event.object-alpha-created.json"))
62+
63+
actor_json = to_json_dict(actor)
64+
object_json = to_json_dict(obj)
65+
event_json = to_json_dict(event)
66+
67+
assert actor_json["schema"] == "sourceos.actor-record/v1alpha1"
68+
assert object_json["schema"] == "sourceos.object-record/v1alpha1"
69+
assert event_json["schema"] == "sourceos.integrity-event/v1alpha1"
70+
json.dumps(actor_json)
71+
json.dumps(object_json)
72+
json.dumps(event_json)
73+
74+
75+
def test_each_contract_type_round_trips_from_fixture() -> None:
76+
mapping = {
77+
"profile.local-dev.json": ProfileRecord,
78+
"device.local.json": DeviceTrustRecord,
79+
"actor.agent-one.json": ActorRecord,
80+
"schema.source-object-v1.json": SchemaContractRecord,
81+
"object.alpha.json": SourceObjectRecord,
82+
"sync-plan.alpha.json": SyncPlanRecord,
83+
"conflict.alpha.json": ConflictRecord,
84+
"policy.decision-review.json": PolicyDecisionRecord,
85+
"event.object-alpha-created.json": IntegrityEventRecord,
86+
"agent-transaction.alpha.json": AgentObjectTransactionRecord,
87+
}
88+
89+
for fixture, contract_type in mapping.items():
90+
record = load_example(fixture)
91+
model = contract_type.from_dict(record)
92+
assert model.to_dict() == record
93+
94+
95+
def test_required_field_validation_rejects_missing_actor_id() -> None:
96+
record = load_example("actor.agent-one.json")
97+
record.pop("actor_id")
98+
99+
with pytest.raises(ContractError, match="missing required fields"):
100+
validate_contract(record)
101+
102+
103+
def test_controlled_value_validation_rejects_invalid_actor_type() -> None:
104+
record = load_example("actor.agent-one.json")
105+
record["actor_type"] = "anonymous_script"
106+
107+
with pytest.raises(ContractError, match="actor_type"):
108+
validate_contract(record)
109+
110+
111+
def test_actor_capability_validation_rejects_unknown_capability() -> None:
112+
record = load_example("actor.agent-one.json")
113+
record["capabilities"] = ["read", "root_everything"]
114+
115+
with pytest.raises(ContractError, match="capabilities"):
116+
validate_contract(record)
117+
118+
119+
def test_policy_effect_validation_rejects_unknown_effect() -> None:
120+
record = load_example("policy.decision-review.json")
121+
record["effect"] = "maybe"
122+
123+
with pytest.raises(ContractError, match="effect"):
124+
validate_contract(record)
125+
126+
127+
def test_conflict_severity_validation_rejects_unknown_severity() -> None:
128+
record = load_example("conflict.alpha.json")
129+
record["severity"] = "panic"
130+
131+
with pytest.raises(ContractError, match="severity"):
132+
validate_contract(record)
133+
134+
135+
def test_unsupported_schema_is_rejected() -> None:
136+
record = {"schema": "sourceos.unknown/v1alpha1"}
137+
138+
with pytest.raises(ContractError, match="unsupported contract schema"):
139+
validate_contract(record)

0 commit comments

Comments
 (0)