"""AUDIT-WP-0010-T04. The envelope tenant-engine actually sends. `AUDIT-IN-0002` invited a correction if the envelope needed a field the emitter does not send. It does — six of eight required fields, one of them absent entirely rather than merely renamed. These tests pin the mismatch rather than describing it, so that the day tenant-engine corrects `envelope_for` the failure here is the signal, and so that nobody quietly relaxes `normalize()` to accept the alternate spellings. """ import pytest from audit_core.ingestion import normalize from audit_core.senders import SenderIdentity IDENTITY = SenderIdentity( name="tenant-engine", tokens=("fixture-only",), sources=frozenset({"tenant-engine"}), evidence_kind="attributive", completeness_trade="outbox drains after commit", ) # Verbatim shape of tenant_engine.audit_core.envelope_for as of 2026-09-10. TENANT_ENGINE_ENVELOPE = { "schema_version": "audit-core.event.v1alpha1", "event_id": "e-1", "observed_at": "2026-09-10T00:00:00+00:00", "tenant": "tenant:acme", "scope": "tenant-engine", "source": "tenant-engine", "actor": "u-1", "action": "role.granted", "resource": "tenant:acme", "outcome": "recorded", "reason": None, "details": {"role": "admin"}, } def test_the_tenant_engine_envelope_is_rejected_today(): """Not a hypothetical: this is the shape on the wire, and it 400s.""" with pytest.raises(ValueError, match="invalid_event"): normalize(TENANT_ENGINE_ENVELOPE, "e-1", IDENTITY) @pytest.mark.parametrize( "required,sent_as", [ ("id", "event_id"), ("type", "action"), ("subject", "resource"), ("occurred_at", "observed_at"), ("data", "details"), ], ) def test_each_renamed_field_is_absent_under_the_name_the_receiver_reads(required, sent_as): assert required not in TENANT_ENGINE_ENVELOPE assert sent_as in TENANT_ENGINE_ENVELOPE def test_correlation_id_is_absent_entirely_and_cannot_be_synthesized(): """The one that is not a rename. A receiver-invented correlation_id would tie an event to an operation audit-core never observed, which is worse than not having one. """ assert "correlation_id" not in TENANT_ENGINE_ENVELOPE corrected = { "id": TENANT_ENGINE_ENVELOPE["event_id"], "type": TENANT_ENGINE_ENVELOPE["action"], "source": TENANT_ENGINE_ENVELOPE["source"], "subject": TENANT_ENGINE_ENVELOPE["resource"], "tenant": TENANT_ENGINE_ENVELOPE["tenant"], "occurred_at": TENANT_ENGINE_ENVELOPE["observed_at"], "data": TENANT_ENGINE_ENVELOPE["details"], } with pytest.raises(ValueError, match="invalid_event"): normalize(corrected, "e-1", IDENTITY) def test_the_corrected_envelope_is_accepted(): """What tenant-engine needs to send. The whole correction, in one place.""" corrected = { "id": "e-1", "type": "role.granted", "source": "tenant-engine", "subject": "tenant:acme", "tenant": "tenant:acme", "correlation_id": "req-9f21", "occurred_at": "2026-09-10T00:00:00+00:00", "data": {"role": "admin", "actor": "u-1"}, } event = normalize(corrected, "e-1", IDENTITY) assert event.event_id == "e-1" assert event.action == "role.granted" assert event.resource == "tenant:acme" assert event.tenant == "tenant:acme" assert event.details["data"]["role"] == "admin" # Derived by the receiver, never taken from the sender. assert event.outcome == "recorded" assert event.actor is None assert event.scope == "tenant" def test_normalize_still_refuses_the_alternate_spellings(): """Guards the decision not to relax the receiver. Accepting event_id/action/resource as aliases would make audit-core choose which sender key means which stored field. The mapping belongs to the sender, which is the party whose assertion the record is. """ required = ("id", "type", "source", "subject", "tenant", "correlation_id", "occurred_at", "data") accepted = { "id": "e-2", "type": "t", "source": "tenant-engine", "subject": "s", "tenant": "tenant:acme", "correlation_id": "c", "occurred_at": "2026-09-10T00:00:00+00:00", "data": {"k": "v"}, } for field in required: broken = dict(accepted) broken.pop(field) with pytest.raises(ValueError, match="invalid_event"): normalize(broken, "e-2", IDENTITY)