feat(plugin-sdk): add executable v0alpha2 contracts
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from missioncore_plugin_sdk import v0alpha2 as sdk
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
NOW = datetime(2026, 7, 16, 12, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def _model() -> sdk.DeviceModelRef:
|
||||
return sdk.DeviceModelRef(
|
||||
plugin_id="nodedc.device.synthetic",
|
||||
plugin_version="0.2.0",
|
||||
model_id="synthetic.sensor",
|
||||
)
|
||||
|
||||
|
||||
def _device() -> sdk.DeviceInstanceRef:
|
||||
return sdk.DeviceInstanceRef(
|
||||
device_id="synthetic.device-001",
|
||||
model=_model(),
|
||||
stability=sdk.IdentityStability.STABLE,
|
||||
basis=sdk.IdentityBasis.PLUGIN_DERIVED,
|
||||
)
|
||||
|
||||
|
||||
def _session_ref() -> sdk.DeviceSessionRef:
|
||||
return sdk.DeviceSessionRef(
|
||||
session_id="session-001",
|
||||
device_id="synthetic.device-001",
|
||||
)
|
||||
|
||||
|
||||
def _header() -> sdk.CanonicalStreamHeader:
|
||||
return sdk.CanonicalStreamHeader(
|
||||
message_id="message-001",
|
||||
session=_session_ref(),
|
||||
acquisition_id="acquisition-001",
|
||||
source_id="lidar-primary",
|
||||
channel_id="points",
|
||||
sequence=1,
|
||||
captured_at=NOW,
|
||||
observed_at=NOW + timedelta(milliseconds=3),
|
||||
clock_domain=sdk.ClockDomain.UTC,
|
||||
compatibility_profile_id="synthetic.fw-1.v1",
|
||||
)
|
||||
|
||||
|
||||
def _inline_payload(data: bytes) -> sdk.InlinePayload:
|
||||
return sdk.InlinePayload(
|
||||
media_type="application/x-pointcloud-struct",
|
||||
byte_length=len(data),
|
||||
sha256=f"sha256:{hashlib.sha256(data).hexdigest()}",
|
||||
data=base64.b64encode(data).decode("ascii"),
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_identity_separates_transport_alias_from_stable_id() -> None:
|
||||
alias = sdk.TransportAlias(
|
||||
transport="ble.corebluetooth",
|
||||
scope_id="agent.macbook-01",
|
||||
value="SYNTHETIC-K1-0001",
|
||||
observed_at=NOW,
|
||||
)
|
||||
provisional = sdk.DeviceInstanceRef(
|
||||
device_id="observation.ble-synthetic-k1-0001",
|
||||
model=_model(),
|
||||
stability=sdk.IdentityStability.PROVISIONAL,
|
||||
basis=sdk.IdentityBasis.TRANSPORT_LOCAL,
|
||||
aliases=(alias,),
|
||||
)
|
||||
|
||||
assert provisional.aliases == (alias,)
|
||||
with pytest.raises(ValidationError, match="transport-local identifier"):
|
||||
sdk.DeviceInstanceRef(
|
||||
device_id="incorrectly-stable",
|
||||
model=_model(),
|
||||
stability=sdk.IdentityStability.STABLE,
|
||||
basis=sdk.IdentityBasis.TRANSPORT_LOCAL,
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_session_tracks_three_independent_state_machines() -> None:
|
||||
context = sdk.DeviceSessionContext(
|
||||
session_id="session-001",
|
||||
device=_device(),
|
||||
execution=sdk.ExecutionBinding(
|
||||
node_id="mission-node.local-01",
|
||||
agent_instance_id="agent-process-001",
|
||||
platform=sdk.ExecutionPlatform.MACOS,
|
||||
),
|
||||
compatibility_profile_id="synthetic.fw-1.v1",
|
||||
opened_at=NOW,
|
||||
)
|
||||
snapshot = sdk.DeviceSessionSnapshot(
|
||||
context=context,
|
||||
revision=4,
|
||||
enrollment=sdk.EnrollmentState.ENROLLED,
|
||||
connectivity=sdk.ConnectivityState.CONNECTED,
|
||||
acquisition=sdk.AcquisitionState.IDLE,
|
||||
observed_at=NOW,
|
||||
active_operation_ids=("operation-health",),
|
||||
)
|
||||
|
||||
assert snapshot.enrollment is sdk.EnrollmentState.ENROLLED
|
||||
assert snapshot.connectivity is sdk.ConnectivityState.CONNECTED
|
||||
assert snapshot.acquisition is sdk.AcquisitionState.IDLE
|
||||
with pytest.raises(ValidationError, match="active operation ids must be unique"):
|
||||
sdk.DeviceSessionSnapshot.model_validate(
|
||||
{
|
||||
**snapshot.model_dump(),
|
||||
"active_operation_ids": ("operation-health", "operation-health"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_operation_ack_is_not_completion() -> None:
|
||||
policy = sdk.OperationPolicy(
|
||||
action_id="acquisition.start",
|
||||
safety_class=sdk.OperationSafetyClass.STATE_CHANGING,
|
||||
idempotency=sdk.IdempotencyMode.IDEMPOTENT_WITH_KEY,
|
||||
timeout_seconds=30,
|
||||
retry_limit=1,
|
||||
preconditions=("device.connected", "calibration.ready"),
|
||||
secret_fields=("device_credential",),
|
||||
)
|
||||
request = sdk.OperationRequest(
|
||||
operation_id="operation-start-001",
|
||||
session=_session_ref(),
|
||||
action_id="acquisition.start",
|
||||
requested_at=NOW,
|
||||
deadline_at=NOW + timedelta(seconds=30),
|
||||
idempotency_key="acquisition.start:001",
|
||||
parameters={"project_id": "project-001"},
|
||||
secret_refs={
|
||||
"device_credential": sdk.SecretReference(
|
||||
provider="missioncore.secret-store",
|
||||
reference="devices/synthetic-001/credential",
|
||||
)
|
||||
},
|
||||
)
|
||||
sdk.validate_operation_request(policy, request)
|
||||
|
||||
adapter = TypeAdapter(sdk.OperationEvent)
|
||||
acknowledgement = adapter.validate_python(
|
||||
{
|
||||
"event_type": "acknowledgement",
|
||||
"operation_id": request.operation_id,
|
||||
"session": request.session.model_dump(),
|
||||
"sequence": 0,
|
||||
"recorded_at": NOW,
|
||||
"disposition": "accepted",
|
||||
}
|
||||
)
|
||||
completion = adapter.validate_python(
|
||||
{
|
||||
"event_type": "completion",
|
||||
"operation_id": request.operation_id,
|
||||
"session": request.session.model_dump(),
|
||||
"sequence": 2,
|
||||
"recorded_at": NOW + timedelta(seconds=1),
|
||||
"result": {"acquisition_id": "acquisition-001"},
|
||||
}
|
||||
)
|
||||
|
||||
assert isinstance(acknowledgement, sdk.OperationAcknowledgement)
|
||||
assert isinstance(completion, sdk.OperationCompletion)
|
||||
assert acknowledgement.event_type != completion.event_type
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_operation_policy_fails_closed() -> None:
|
||||
with pytest.raises(ValidationError, match="automatic retries"):
|
||||
sdk.OperationPolicy(
|
||||
action_id="calibration.start",
|
||||
safety_class=sdk.OperationSafetyClass.SAFETY_CRITICAL,
|
||||
idempotency=sdk.IdempotencyMode.NON_IDEMPOTENT,
|
||||
timeout_seconds=60,
|
||||
retry_limit=1,
|
||||
)
|
||||
|
||||
policy = sdk.OperationPolicy(
|
||||
action_id="camera.catalog.read",
|
||||
safety_class=sdk.OperationSafetyClass.READ_ONLY,
|
||||
idempotency=sdk.IdempotencyMode.IDEMPOTENT,
|
||||
timeout_seconds=5,
|
||||
)
|
||||
request = sdk.OperationRequest(
|
||||
operation_id="operation-camera-001",
|
||||
session=_session_ref(),
|
||||
action_id="camera.preview.start",
|
||||
requested_at=NOW,
|
||||
deadline_at=NOW + timedelta(seconds=5),
|
||||
)
|
||||
with pytest.raises(ValueError, match="action_id"):
|
||||
sdk.validate_operation_request(policy, request)
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_point_cloud_layout_and_payload_are_verified() -> None:
|
||||
payload = _inline_payload(b"\0" * 24)
|
||||
frame = sdk.PointCloudFrame(
|
||||
header=_header(),
|
||||
coordinate_frame="sensor.lidar-primary",
|
||||
width=2,
|
||||
point_count=2,
|
||||
point_step=12,
|
||||
row_step=24,
|
||||
dense=False,
|
||||
fields=(
|
||||
sdk.PointField(name="x", offset=0, data_type=sdk.PointFieldType.FLOAT32),
|
||||
sdk.PointField(name="y", offset=4, data_type=sdk.PointFieldType.FLOAT32),
|
||||
sdk.PointField(name="z", offset=8, data_type=sdk.PointFieldType.FLOAT32),
|
||||
),
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
assert frame.payload.byte_length == frame.point_count * frame.point_step
|
||||
with pytest.raises(ValidationError, match="point_count"):
|
||||
sdk.PointCloudFrame.model_validate({**frame.model_dump(), "point_count": 3})
|
||||
with pytest.raises(ValidationError, match="sha256"):
|
||||
sdk.InlinePayload.model_validate({**payload.model_dump(), "sha256": f"sha256:{'0' * 64}"})
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_evidence_is_immutable_and_lineage_is_explicit() -> None:
|
||||
handle = sdk.EvidenceHandle(
|
||||
evidence_id="evidence.raw-001",
|
||||
store_id="evidence-store.local",
|
||||
media_type="application/vnd.tcpdump.pcap",
|
||||
byte_length=1024,
|
||||
sha256=f"sha256:{'a' * 64}",
|
||||
object_key="experiments/2026-07-16/raw-001.pcap",
|
||||
)
|
||||
record = sdk.EvidenceRecord(
|
||||
handle=handle,
|
||||
plugin_id="nodedc.device.synthetic",
|
||||
session=_session_ref(),
|
||||
created_at=NOW,
|
||||
source_kind="packet-capture",
|
||||
retention=sdk.EvidenceRetention.EXPERIMENT,
|
||||
redaction=sdk.RedactionState.UNREVIEWED,
|
||||
)
|
||||
|
||||
assert record.handle.sha256.startswith("sha256:")
|
||||
with pytest.raises(ValidationError, match="cannot name itself"):
|
||||
sdk.EvidenceRecord.model_validate(
|
||||
{**record.model_dump(), "parent_evidence_ids": [handle.evidence_id]}
|
||||
)
|
||||
with pytest.raises(ValidationError, match="frozen"):
|
||||
record.redaction = sdk.RedactionState.CLEARED # type: ignore[misc]
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_contracts_are_deeply_immutable() -> None:
|
||||
source_parameters = {
|
||||
"pipeline": {
|
||||
"stages": ["capture", "decode"],
|
||||
"options": {"raw_first": True},
|
||||
}
|
||||
}
|
||||
request = sdk.OperationRequest(
|
||||
operation_id="operation-immutable-001",
|
||||
session=_session_ref(),
|
||||
action_id="acquisition.prepare",
|
||||
requested_at=NOW,
|
||||
deadline_at=NOW + timedelta(seconds=30),
|
||||
parameters=source_parameters,
|
||||
)
|
||||
|
||||
pipeline = request.parameters["pipeline"]
|
||||
assert isinstance(pipeline, dict)
|
||||
stages = pipeline["stages"]
|
||||
assert isinstance(stages, list)
|
||||
options = pipeline["options"]
|
||||
assert isinstance(options, dict)
|
||||
|
||||
with pytest.raises(TypeError, match="deeply immutable"):
|
||||
request.parameters["new"] = True
|
||||
with pytest.raises(TypeError, match="deeply immutable"):
|
||||
options["raw_first"] = False
|
||||
with pytest.raises(TypeError, match="deeply immutable"):
|
||||
stages.append("publish")
|
||||
|
||||
source_parameters["pipeline"]["stages"].append("outside-mutation")
|
||||
assert stages == ["capture", "decode"]
|
||||
wire_document = request.model_dump(mode="json")
|
||||
assert wire_document["parameters"] == {
|
||||
"pipeline": {
|
||||
"stages": ["capture", "decode"],
|
||||
"options": {"raw_first": True},
|
||||
}
|
||||
}
|
||||
assert json.loads(json.dumps(wire_document))["parameters"] == wire_document["parameters"]
|
||||
assert "parameters" in sdk.OperationRequest.model_json_schema()["properties"]
|
||||
|
||||
copy_update = {"parameters": {"pipeline": {"stages": ["publish"]}}}
|
||||
copied = request.model_copy(update=copy_update)
|
||||
copied_stages = copied.parameters["pipeline"]["stages"]
|
||||
assert copied_stages == ["publish"]
|
||||
with pytest.raises(TypeError, match="deeply immutable"):
|
||||
copied_stages.append("mutate")
|
||||
copy_update["parameters"]["pipeline"]["stages"].append("outside-mutation")
|
||||
assert copied_stages == ["publish"]
|
||||
with pytest.raises(ValidationError, match="extra"):
|
||||
request.model_copy(update={"undeclared": True})
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_compatibility_blocks_unproven_control() -> None:
|
||||
pass_rule = sdk.CompatibilityRuleResult(
|
||||
rule_id="firmware.exact-match",
|
||||
outcome=sdk.RuleOutcome.PASS,
|
||||
blocking=True,
|
||||
detail="Firmware and protocol fixture match the reviewed profile.",
|
||||
)
|
||||
assessment = sdk.CompatibilityAssessment(
|
||||
assessment_id="compatibility-001",
|
||||
device=_device(),
|
||||
evaluated_at=NOW,
|
||||
observed_firmware="1.0.0",
|
||||
profile_id="synthetic.fw-1.v1",
|
||||
decision=sdk.CompatibilityDecision.COMPATIBLE,
|
||||
permitted_mode=sdk.PermittedMode.ACTIVE_CONTROL,
|
||||
rules=(pass_rule,),
|
||||
supported_actions=("acquisition.start", "acquisition.stop"),
|
||||
)
|
||||
assert assessment.permitted_mode is sdk.PermittedMode.ACTIVE_CONTROL
|
||||
|
||||
with pytest.raises(ValidationError, match="active control requires a compatible decision"):
|
||||
sdk.CompatibilityAssessment.model_validate(
|
||||
{
|
||||
**assessment.model_dump(),
|
||||
"decision": "unknown",
|
||||
"permitted_mode": "active-control",
|
||||
"profile_id": None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_sdk_v0alpha2_exports_closed_json_schemas_and_vocabulary() -> None:
|
||||
schemas = sdk.contract_json_schemas()
|
||||
|
||||
assert {
|
||||
"DeviceInstanceRef",
|
||||
"DeviceSessionSnapshot",
|
||||
"OperationEvent",
|
||||
"CanonicalStreamEnvelope",
|
||||
"EvidenceRecord",
|
||||
"CompatibilityAssessment",
|
||||
}.issubset(schemas)
|
||||
assert "oneOf" in schemas["OperationEvent"]
|
||||
with pytest.raises(ValidationError, match="extra"):
|
||||
sdk.DeviceSessionRef.model_validate(
|
||||
{"session_id": "session-001", "device_id": "device-001", "vendor": "forbidden"}
|
||||
)
|
||||
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
vocabulary = (
|
||||
repository_root / "docs/domain-model/mission-core-experimental-vocabulary-v0alpha2.md"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "acknowledgement is not operation completion" in vocabulary
|
||||
assert "not yet a NODE.DC platform ontology" in vocabulary
|
||||
Reference in New Issue
Block a user