diff --git a/.github/workflows/offer-approval-quality.yml b/.github/workflows/offer-approval-quality.yml new file mode 100644 index 000000000..1ecff719a --- /dev/null +++ b/.github/workflows/offer-approval-quality.yml @@ -0,0 +1,57 @@ +name: Offer Approval Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/offer-approval/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/offer-approval-quality.yml" + - "docs/adr/0017-governed-offer-approval.md" + - "docs/doctoring/offer-approval-references.md" + - "docs/traceability/offer-approval.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: offer-approval-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Offer approval contract and 100% coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Prove exact candidate checkout + env: + ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + check-latest: false + - name: Install reviewed test toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + python -m pip check + - name: Compile offer approval package + run: python -m compileall -q packages/offer-approval/src packages/offer-approval/tests + - name: Test offer approval with exact statement and branch coverage + env: + PYTHONPATH: packages/offer-approval/src + COVERAGE_FILE: /tmp/orgmetra-offer-approval.coverage + run: python -m pytest -c packages/offer-approval/pyproject.toml packages/offer-approval/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md new file mode 100644 index 000000000..c662a0429 --- /dev/null +++ b/docs/adr/0017-governed-offer-approval.md @@ -0,0 +1,37 @@ +# ADR 0017: Governed offer approval evidence + +- Status: Proposed — active PR only +- Date: 2026-08-19 +- Scope: Talent Acquisition offer review + +## Context + +Protected `develop` can govern candidate, requisition, selection, and employment evidence, but it does not yet expose a bounded pre-send contract proving that a proposed offer is tied to the selected candidate, authoritative Job/optional Position, reviewed selection decision, compensation-package provenance, offer-terms provenance, and accountable human approval. + +Offer review is high-impact employment workflow. A governance envelope must not become an alternate decision authority, a salary-value cache, or a channel that lets generated/model material masquerade as an approved offer. Different opaque requester/approver references also do not prove that the authoritative actor boundary resolves them to different people, and UUID syntax does not prove that the referenced candidate, requisition, Job/Position, selection decision, compensation package, or offer terms belong to the packet tenant. Packet-owned UUIDv1 references also carry timestamp/node-derived correlation metadata. The authoritative tenant identifier is different: it is issued by Orgmetra core, so this leaf package must accept the canonical non-sentinel operational UUID contract owned by that boundary rather than silently imposing a second version policy. + +ISO 30405:2023 provides current recruitment guidance across planning, assessment, employment, stakeholder management, and review. EEOC guidance on tests and selection procedures emphasizes job-related use and employer responsibility for selection procedures. Those sources support a conservative evidence-and-human-review boundary; they do not by themselves certify this package or decide the legality of any offer. + +## Decision + +Orgmetra will expose `OfferApprovalPacket` as value-free review evidence only. + +`tenant_record_id` must be canonical and non-sentinel under Orgmetra's authoritative operational UUID contract. Tenant UUID generation/version/privacy policy remains owned by the core HRIS boundary. Packet-owned opaque candidate profile, requisition, Job, optional Position, selection decision, compensation package, offer terms, and accountable actor references separately require canonical non-sentinel UUIDv4 plus their expected namespace. UUIDv1 and other non-v4 suffixes fail closed for those packet-owned references. Decision, package, and terms artifacts are independently SHA-256 bound. Before approval, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary and reject approval if any reference belongs to another tenant or cannot be authoritatively resolved. Identical requester/approver references are rejected as an early syntactic guard; after tenant-scoped resolution, the host must prove their resolved actor identities are distinct. Reference inequality alone is not separation-of-duties evidence. + +The packet must not contain candidate PII, compensation values, assessment scores, or free-form model output. The `reason_code` field is closed to the reviewed value-free `selected_candidate_offer_review` code. Direct construction and `dataclasses.replace(...)` revalidate all trust-bearing invariants. + +Every packet is fixed to purpose `offer_approval_review`, reviewed reason `selected_candidate_offer_review`, bounded positive integer `evidence_version` (default `1`) included in canonical JSON/SHA-256, `human_confirmation_required=True`, decision authority `human_approval_only`, review state `requires_human_approval`, and delivery state `not_authorized_to_send`. + +`evidence_version` accepts only real integers from `1` through `2147483647`; booleans, text, zero, negative values, and overflow values fail closed. It versions the immutable pre-send evidence envelope and does not itself prove source-version resolution, approval, or delivery. + +Canonical JSON and SHA-256 are audit-correlation evidence only. The packet does not approve, communicate, send, execute, persist an offer, or prove authoritative reference/actor identity. + +## Consequences + +A buyer can review one deterministic, PII-minimized envelope before an offer moves to the authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, while Orgmetra keeps exact provenance references, evidence version, and human accountability. Packet-owned UUIDv1/non-v4 references fail closed before serialization without making this leaf package incompatible with authoritative Orgmetra tenant UUIDs. Cross-tenant evidence mixing is fail-closed at the host approval boundary because every packet reference must resolve in the exact tenant. Requester/approver separation is proven only after tenant-scoped authoritative actor resolution. New offer-review reason categories require an explicit contract change and regression evidence rather than accepting arbitrary caller text. + +Downstream offer persistence/execution must independently enforce authorization, tenant-scoped source-evidence resolution, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains proposed active-PR truth until integrated into protected `develop`. + +## References + +See `docs/doctoring/offer-approval-references.md`. diff --git a/docs/doctoring/offer-approval-references.md b/docs/doctoring/offer-approval-references.md new file mode 100644 index 000000000..b0de5316a --- /dev/null +++ b/docs/doctoring/offer-approval-references.md @@ -0,0 +1,22 @@ +# Offer approval references + +Retrieved August 19, 2026. + +International Organization for Standardization. (2023). *ISO 30405:2023 Human resource +management—Guidelines on recruitment* (2nd ed.). https://www.iso.org/standard/79488.html + +U.S. Equal Employment Opportunity Commission. (2007, December 1). *Employment tests and +selection procedures*. https://www.eeoc.gov/laws/guidance/employment-tests-and-selection-procedures + +U.S. Equal Employment Opportunity Commission, U.S. Department of Justice, U.S. Department of +Labor, U.S. Office of Personnel Management, & U.S. Department of the Treasury. (1979, March 1). +*Questions and answers to clarify and provide a common interpretation of the Uniform Guidelines +on Employee Selection Procedures*. https://www.eeoc.gov/laws/guidance/questions-and-answers-clarify-and-provide-common-interpretation-uniform-guidelines + +## Applied boundary + +These sources support recruitment-process governance, job-related selection responsibility, +stakeholder review, and documented use of selection evidence. This package makes no +certification or legal-compliance claim. It deliberately keeps candidate and compensation values +outside the governance envelope and requires accountable human approval before any offer may be +communicated or executed. diff --git a/docs/traceability/offer-approval.md b/docs/traceability/offer-approval.md new file mode 100644 index 000000000..5104cfb8d --- /dev/null +++ b/docs/traceability/offer-approval.md @@ -0,0 +1,21 @@ +# Governed offer approval traceability + +Status: **active PR / proposed capability**, not protected-main truth. + +| Buyer requirement | Executable evidence | Contract outcome | +| --- | --- | --- | +| Exact selected-candidate scope | `test_rejects_bad_opaque_references`; canonical JSON test | Candidate is correlated only by a bounded opaque `candidate_profile:` reference. | +| Authoritative tenant interoperability and packet-reference privacy | `test_tenant_identity_privacy.py`; `test_rejects_uuid1_trust_references_through_direct_and_replace` | `tenant_record_id` follows the canonical non-sentinel Orgmetra core operational-UUID contract; packet-owned namespaced references require canonical non-sentinel UUIDv4 and reject UUIDv1/non-v4 suffixes through construction and replacement paths. | +| Separate Job and Position | valid packet + optional-Position test | Job is mandatory; Position is separately named and optional rather than collapsed into Job. | +| Reviewed selection evidence | digest/reference validation tests | Selection decision identity and SHA-256 evidence are required. | +| Compensation/terms provenance without value duplication | value-free canonical JSON test; digest/reference validation tests | Package and terms are exact reference+digest pairs; salary/benefit values are absent. | +| Value-free reason metadata | `test_rejects_value_bearing_reason_codes_through_direct_and_replace` | `reason_code` is closed to reviewed `selected_candidate_offer_review`; arbitrary lower-snake-case candidate, compensation, or offer-term text fails closed. | +| Human accountability and separation of duties | same-reference rejection plus `test_actor_separation.py` | Requester/approver references differ locally, and approval requires tenant-scoped authoritative resolution proving distinct resolved actor identities. | +| High-impact evidence versioning | `test_evidence_version.py` | Bounded positive `evidence_version` is in canonical JSON, changes correlation SHA-256 across versions, and revalidates through mutation-by-copy. | +| No premature offer delivery | direct-constructor/replace fail-closed tests | State remains `requires_human_approval` and `not_authorized_to_send`. | +| Deterministic audit correlation | canonical JSON, fractional-second, timezone, evidence-version, SHA-256 tests | Canonical evidence is precision-preserving, versioned, and deterministic. | +| Public API readability | module/class/function docstrings | Beginner-readable contract boundary is documented in source and package README. | + +UUIDv4 is an opacity constraint for packet-owned trust references, not tenant authority. Tenant UUID generation/version/privacy policy remains owned by the authoritative HRIS boundary. Before approval, every packet reference must still resolve authoritatively inside the exact `tenant_record_id`; requester/approver identity separation must be proven after that resolution. + +The SHA-256 packet digest proves only the exact canonical envelope bytes. It does not prove that referenced evidence is substantively correct, that requester/approver resolve to different identities, that compensation is lawful/fair, that a human approved the offer, or that an offer was delivered. Authoritative actor and source-evidence resolution remain outside this evidence packet and are required pre-approval host checks. diff --git a/packages/offer-approval/CHANGELOG.md b/packages/offer-approval/CHANGELOG.md new file mode 100644 index 000000000..626500f50 --- /dev/null +++ b/packages/offer-approval/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## Unreleased + +- Add a governed, value-free pre-send offer approval packet. +- Require separate requester and approver identities and exact human approval. +- Bind selected-candidate, Job/optional Position, selection-decision, compensation-package, and offer-terms provenance without copying candidate or compensation values. +- Follow Orgmetra's authoritative canonical non-sentinel operational UUID contract for `tenant_record_id`, while packet-owned trust-reference suffixes remain canonical non-sentinel UUIDv4 and reject UUIDv1/non-v4 identity forms. +- Close `reason_code` to the reviewed value-free `selected_candidate_offer_review` contract so arbitrary candidate, compensation, or offer-term text cannot enter canonical evidence. +- Bind a bounded positive `evidence_version` into canonical JSON and SHA-256 correlation evidence so high-impact offer-review evidence versions are explicit and fail closed. +- Keep every packet `requires_human_approval` and `not_authorized_to_send`. diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md new file mode 100644 index 000000000..9fb659a14 --- /dev/null +++ b/packages/offer-approval/README.md @@ -0,0 +1,40 @@ +# Orgmetra governed offer approval + +This package creates a **value-free pre-send offer approval packet**. It is a governance envelope, not an offer engine and not an employment decision. + +The packet binds one selected candidate to the exact requisition and authoritative Job, an optional exact Position, the reviewed selection-decision digest, compensation-package provenance, offer-terms provenance, and two accountable actor references. Identical requester and approver references are rejected as an early syntactic guard. + +The envelope intentionally excludes candidate names, email addresses, demographic values, assessment scores, salary/benefit amounts, credentials, and free-form model output. `candidate_profile_reference` remains sensitive correlating metadata even though it is opaque. `tenant_record_id` follows Orgmetra's authoritative canonical non-sentinel operational UUID contract rather than imposing a second UUID-version rule in this leaf package. Packet-owned namespaced references remain canonical non-sentinel UUIDv4 values and require their expected namespace. UUIDv1 and other non-v4 reference suffixes are rejected so timestamp/node correlation metadata, names, compensation values, offer terms, and actor identities cannot be smuggled into packet-owned governance references. `reason_code` is likewise closed to the reviewed, value-free `selected_candidate_offer_review` code; arbitrary lower-snake-case text is rejected so the reason field cannot become a side channel for candidate, compensation, or offer-term values. + +Every packet also carries a bounded positive integer `evidence_version` (default `1`). It is serialized into canonical JSON, so changing the governed evidence version changes the packet SHA-256 digest. Zero, negative, boolean, textual, and values above `2147483647` fail closed. The field versions this immutable pre-send evidence envelope; it is not approval, delivery, or proof that referenced source versions were authoritatively resolved. + +A valid packet always remains `requires_human_approval` and `not_authorized_to_send`. Before approval, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary so valid references from a foreign tenant cannot be mixed into the approval envelope. It must specifically re-resolve `requester_reference` and `approver_reference` and prove their resolved actor identities are distinct; opaque-reference inequality alone is not separation-of-duties evidence. The host must then verify Job/Position scope, selected-candidate evidence, compensation-package provenance, and offer-terms provenance before recording accountable human approval through the authoritative offer workflow and before communicating or executing the offer. UUIDv4 is only an opacity constraint for packet-owned references; tenant UUID generation/privacy policy remains owned by the authoritative HRIS boundary. + +Canonical JSON and its SHA-256 digest support immutable audit correlation. They do not prove that the referenced evidence is true, that all references belong to the packet tenant, that actor identities are distinct, that compensation is lawful or fair, that an offer was approved, or that an offer was communicated. + +## Example + +```python +from datetime import datetime, timezone +from orgmetra_offer_approval import build_offer_approval_packet + +packet = build_offer_approval_packet( + tenant_record_id="11111111-1111-4111-8111-111111111111", + offer_approval_reference="offer_approval:10000000-0000-4000-8000-000000000001", + candidate_profile_reference="candidate_profile:10000000-0000-4000-8000-000000000002", + requisition_reference="requisition:10000000-0000-4000-8000-000000000003", + job_profile_reference="job_profile:10000000-0000-4000-8000-000000000004", + position_record_reference="position_record:10000000-0000-4000-8000-000000000005", + selection_decision_reference="selection_decision:10000000-0000-4000-8000-000000000006", + selection_decision_digest="a" * 64, + compensation_package_reference="compensation_package:10000000-0000-4000-8000-000000000007", + compensation_package_digest="b" * 64, + offer_terms_reference="offer_terms:10000000-0000-4000-8000-000000000008", + offer_terms_digest="c" * 64, + requester_reference="actor:10000000-0000-4000-8000-000000000009", + approver_reference="actor:10000000-0000-4000-8000-00000000000a", + purpose_code="offer_approval_review", + reason_code="selected_candidate_offer_review", + generated_at=datetime.now(timezone.utc), +) +``` diff --git a/packages/offer-approval/pyproject.toml b/packages/offer-approval/pyproject.toml new file mode 100644 index 000000000..5fdf2f8df --- /dev/null +++ b/packages/offer-approval/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-offer-approval" +version = "0.1.0" +description = "Governed human offer-approval evidence for Orgmetra." +requires-python = ">=3.12" + +[project.optional-dependencies] +test = ["pytest>=8.3", "pytest-cov>=5.0"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "--cov=orgmetra_offer_approval", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/offer-approval/src/orgmetra_offer_approval/__init__.py b/packages/offer-approval/src/orgmetra_offer_approval/__init__.py new file mode 100644 index 000000000..d8f99a53b --- /dev/null +++ b/packages/offer-approval/src/orgmetra_offer_approval/__init__.py @@ -0,0 +1,5 @@ +"""Public governed offer-approval contract.""" + +from .packet import OfferApprovalPacket, build_offer_approval_packet + +__all__ = ["OfferApprovalPacket", "build_offer_approval_packet"] diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py new file mode 100644 index 000000000..3a7363c62 --- /dev/null +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -0,0 +1,265 @@ +"""Governed, value-free human offer-approval evidence. + +The packet binds one selected candidate to an authoritative requisition and Job, an +optional exact Position, the reviewed selection decision, compensation-package +provenance, offer-terms provenance, and accountable human actors. The opaque candidate +reference remains sensitive correlating metadata. Candidate PII, compensation values, +assessment scores, and free-form model output remain outside this envelope. +""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +from hashlib import sha256 +import json +import re +from uuid import UUID + +_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$") +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_REFERENCE_PATTERN = re.compile( + r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$" +) +_PURPOSE_CODE = "offer_approval_review" +_ALLOWED_REASON_CODES = frozenset({"selected_candidate_offer_review"}) +_DECISION_AUTHORITY = "human_approval_only" +_REVIEW_STATE = "requires_human_approval" +_DELIVERY_STATE = "not_authorized_to_send" +_NEXT_ACTION = ( + "Within tenant_record_id, re-resolve every packet reference through its authoritative " + "boundary; specifically re-resolve requester_reference and approver_reference and verify " + "their resolved actor identities are distinct; then verify authoritative Job/Position " + "scope, selected-candidate evidence, compensation-package provenance, and offer-terms " + "provenance before recording accountable human approval through the authoritative offer " + "workflow and before communicating or executing the offer." +) + + +def _validate_operational_uuid(value: str, field_name: str) -> None: + """Require canonical non-sentinel UUID text owned by the authoritative HRIS.""" + try: + parsed = UUID(value) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(f"{field_name} must be canonical UUID text") from exc + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") + + +def _validate_code(value: str, field_name: str) -> None: + """Require a bounded descriptive lower snake_case governance code.""" + if not isinstance(value, str) or len(value) > 64 or not _CODE_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be bounded two-or-more-word lower snake_case") + + +def _validate_reference(value: str, prefix: str, field_name: str) -> None: + """Require an expected namespace plus a canonical opaque UUIDv4 suffix.""" + error_message = f"{field_name} must be an opaque {prefix}: reference" + if ( + not isinstance(value, str) + or len(value) > 160 + or not _REFERENCE_PATTERN.fullmatch(value) + or not value.startswith(f"{prefix}:") + ): + raise ValueError(error_message) + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(error_message) from exc + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + raise ValueError(error_message) + + +def _validate_digest(value: str, field_name: str) -> None: + """Require lowercase SHA-256 hexadecimal evidence.""" + if not isinstance(value, str) or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _canonical_timestamp(value: datetime) -> str: + """Render an aware instant as precision-preserving UTC RFC 3339 text.""" + if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None: + raise ValueError("generated_at must be timezone-aware") + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _validate_evidence_version(value: int) -> None: + """Require a bounded positive integer version for high-impact offer-review evidence.""" + if type(value) is not int or value < 1 or value > 2_147_483_647: + raise ValueError("evidence_version must be an integer from 1 through 2147483647") + + +@dataclass(frozen=True, slots=True, repr=False) +class OfferApprovalPacket: + """Immutable value-free offer review packet awaiting accountable approval.""" + + tenant_record_id: str + offer_approval_reference: str + candidate_profile_reference: str + requisition_reference: str + job_profile_reference: str + position_record_reference: str | None + selection_decision_reference: str + selection_decision_digest: str + compensation_package_reference: str + compensation_package_digest: str + offer_terms_reference: str + offer_terms_digest: str + requester_reference: str + approver_reference: str + purpose_code: str + reason_code: str + generated_at: datetime + evidence_version: int = 1 + contains_candidate_pii: bool = False + contains_compensation_values: bool = False + human_confirmation_required: bool = True + decision_authority: str = _DECISION_AUTHORITY + review_state: str = _REVIEW_STATE + delivery_state: str = _DELIVERY_STATE + next_action: str = _NEXT_ACTION + + def __repr__(self) -> str: + """Return a representation that never emits candidate or compensation evidence.""" + return "OfferApprovalPacket()" + + def __post_init__(self) -> None: + """Fail closed when direct construction drifts from the governed contract.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference( + self.offer_approval_reference, + "offer_approval", + "offer_approval_reference", + ) + _validate_reference( + self.candidate_profile_reference, + "candidate_profile", + "candidate_profile_reference", + ) + _validate_reference(self.requisition_reference, "requisition", "requisition_reference") + _validate_reference(self.job_profile_reference, "job_profile", "job_profile_reference") + if self.position_record_reference is not None: + _validate_reference( + self.position_record_reference, + "position_record", + "position_record_reference", + ) + _validate_reference( + self.selection_decision_reference, + "selection_decision", + "selection_decision_reference", + ) + _validate_digest(self.selection_decision_digest, "selection_decision_digest") + _validate_reference( + self.compensation_package_reference, + "compensation_package", + "compensation_package_reference", + ) + _validate_digest(self.compensation_package_digest, "compensation_package_digest") + _validate_reference(self.offer_terms_reference, "offer_terms", "offer_terms_reference") + _validate_digest(self.offer_terms_digest, "offer_terms_digest") + _validate_reference(self.requester_reference, "actor", "requester_reference") + _validate_reference(self.approver_reference, "actor", "approver_reference") + if self.requester_reference == self.approver_reference: + raise ValueError("approver_reference must identify a different accountable actor") + _validate_code(self.purpose_code, "purpose_code") + if self.purpose_code != _PURPOSE_CODE: + raise ValueError("purpose_code must remain offer_approval_review") + _validate_code(self.reason_code, "reason_code") + if self.reason_code not in _ALLOWED_REASON_CODES: + raise ValueError("reason_code must use a reviewed non-sensitive offer reason") + _canonical_timestamp(self.generated_at) + _validate_evidence_version(self.evidence_version) + if self.contains_candidate_pii is not False: + raise ValueError("offer approval packet must not contain candidate PII") + if self.contains_compensation_values is not False: + raise ValueError("offer approval packet must not contain compensation values") + if self.human_confirmation_required is not True: + raise ValueError("human confirmation is mandatory before offer approval") + if self.decision_authority != _DECISION_AUTHORITY: + raise ValueError("decision_authority must remain human_approval_only") + if self.review_state != _REVIEW_STATE: + raise ValueError("review_state must remain requires_human_approval") + if self.delivery_state != _DELIVERY_STATE: + raise ValueError("delivery_state must remain not_authorized_to_send") + if self.next_action != _NEXT_ACTION: + raise ValueError("next_action must remain the governed offer-approval instruction") + + def canonical_json(self) -> str: + """Return deterministic canonical JSON for immutable audit correlation.""" + payload = { + "approver_reference": self.approver_reference, + "candidate_profile_reference": self.candidate_profile_reference, + "compensation_package_digest": self.compensation_package_digest, + "compensation_package_reference": self.compensation_package_reference, + "contains_candidate_pii": self.contains_candidate_pii, + "contains_compensation_values": self.contains_compensation_values, + "decision_authority": self.decision_authority, + "delivery_state": self.delivery_state, + "evidence_version": self.evidence_version, + "generated_at": _canonical_timestamp(self.generated_at), + "human_confirmation_required": self.human_confirmation_required, + "job_profile_reference": self.job_profile_reference, + "next_action": self.next_action, + "offer_approval_reference": self.offer_approval_reference, + "offer_terms_digest": self.offer_terms_digest, + "offer_terms_reference": self.offer_terms_reference, + "position_record_reference": self.position_record_reference, + "purpose_code": self.purpose_code, + "reason_code": self.reason_code, + "requester_reference": self.requester_reference, + "requisition_reference": self.requisition_reference, + "review_state": self.review_state, + "selection_decision_digest": self.selection_decision_digest, + "selection_decision_reference": self.selection_decision_reference, + "tenant_record_id": self.tenant_record_id, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical UTF-8 offer-approval packet.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_offer_approval_packet( + *, + tenant_record_id: str, + offer_approval_reference: str, + candidate_profile_reference: str, + requisition_reference: str, + job_profile_reference: str, + position_record_reference: str | None, + selection_decision_reference: str, + selection_decision_digest: str, + compensation_package_reference: str, + compensation_package_digest: str, + offer_terms_reference: str, + offer_terms_digest: str, + requester_reference: str, + approver_reference: str, + purpose_code: str, + reason_code: str, + generated_at: datetime, + evidence_version: int = 1, +) -> OfferApprovalPacket: + """Build value-free offer-approval evidence pending accountable human approval.""" + return OfferApprovalPacket( + tenant_record_id=tenant_record_id, + offer_approval_reference=offer_approval_reference, + candidate_profile_reference=candidate_profile_reference, + requisition_reference=requisition_reference, + job_profile_reference=job_profile_reference, + position_record_reference=position_record_reference, + selection_decision_reference=selection_decision_reference, + selection_decision_digest=selection_decision_digest, + compensation_package_reference=compensation_package_reference, + compensation_package_digest=compensation_package_digest, + offer_terms_reference=offer_terms_reference, + offer_terms_digest=offer_terms_digest, + requester_reference=requester_reference, + approver_reference=approver_reference, + purpose_code=purpose_code, + reason_code=reason_code, + generated_at=generated_at, + evidence_version=evidence_version, + ) diff --git a/packages/offer-approval/tests/test_actor_separation.py b/packages/offer-approval/tests/test_actor_separation.py new file mode 100644 index 000000000..00fbb40c8 --- /dev/null +++ b/packages/offer-approval/tests/test_actor_separation.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest + +from orgmetra_offer_approval import build_offer_approval_packet + + +def _build(**overrides): + """Build a valid offer-approval packet, allowing one field to be varied by a regression.""" + values = { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "offer_approval_reference": "offer_approval:22222222-2222-4222-8222-222222222222", + "candidate_profile_reference": "candidate_profile:33333333-3333-4333-8333-333333333333", + "requisition_reference": "requisition:44444444-4444-4444-8444-444444444444", + "job_profile_reference": "job_profile:55555555-5555-4555-8555-555555555555", + "position_record_reference": None, + "selection_decision_reference": "selection_decision:66666666-6666-4666-8666-666666666666", + "selection_decision_digest": "a" * 64, + "compensation_package_reference": "compensation_package:77777777-7777-4777-8777-777777777777", + "compensation_package_digest": "b" * 64, + "offer_terms_reference": "offer_terms:88888888-8888-4888-8888-888888888888", + "offer_terms_digest": "c" * 64, + "requester_reference": "actor:99999999-9999-4999-8999-999999999999", + "approver_reference": "actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "purpose_code": "offer_approval_review", + "reason_code": "selected_candidate_offer_review", + "generated_at": datetime(2026, 8, 19, 2, 15, tzinfo=timezone.utc), + } + values.update(overrides) + return build_offer_approval_packet(**values) + + +def test_requester_and_approver_require_authoritative_actor_separation() -> None: + """Require distinct actor references plus authoritative identity separation before approval.""" + with pytest.raises(ValueError, match="different accountable actor"): + _build(approver_reference="actor:99999999-9999-4999-8999-999999999999") + + normalized_next_action = _build().next_action.lower() + assert "requester_reference and approver_reference" in normalized_next_action + assert "resolved actor identities are distinct" in normalized_next_action + + +def test_approval_requires_every_reference_to_resolve_in_the_exact_tenant() -> None: + """Prevent cross-tenant offer evidence mixing behind syntactically valid UUID references.""" + action = _build().next_action + tenant_clause = "Within tenant_record_id, re-resolve every packet reference" + actor_clause = "verify their resolved actor identities are distinct" + scope_clause = "verify authoritative Job/Position scope" + approval_clause = "accountable human approval" + + assert tenant_clause in action + assert action.index(tenant_clause) < action.index(actor_clause) + assert action.index(actor_clause) < action.index(scope_clause) + assert action.index(scope_clause) < action.index(approval_clause) diff --git a/packages/offer-approval/tests/test_evidence_version.py b/packages/offer-approval/tests/test_evidence_version.py new file mode 100644 index 000000000..5ddc2a5e4 --- /dev/null +++ b/packages/offer-approval/tests/test_evidence_version.py @@ -0,0 +1,58 @@ +"""Regression coverage for high-impact offer-review evidence versioning.""" + +from dataclasses import replace +from datetime import datetime, timezone +import json + +import pytest + +from orgmetra_offer_approval import build_offer_approval_packet + + +def _build(evidence_version: object = 1): + """Build a valid offer-review packet while varying only its evidence version.""" + return build_offer_approval_packet( + tenant_record_id="11111111-1111-4111-8111-111111111111", + offer_approval_reference="offer_approval:10000000-0000-4000-8000-000000000001", + candidate_profile_reference="candidate_profile:10000000-0000-4000-8000-000000000002", + requisition_reference="requisition:10000000-0000-4000-8000-000000000003", + job_profile_reference="job_profile:10000000-0000-4000-8000-000000000004", + position_record_reference="position_record:10000000-0000-4000-8000-000000000005", + selection_decision_reference="selection_decision:10000000-0000-4000-8000-000000000006", + selection_decision_digest="a" * 64, + compensation_package_reference="compensation_package:10000000-0000-4000-8000-000000000007", + compensation_package_digest="b" * 64, + offer_terms_reference="offer_terms:10000000-0000-4000-8000-000000000008", + offer_terms_digest="c" * 64, + requester_reference="actor:10000000-0000-4000-8000-000000000009", + approver_reference="actor:10000000-0000-4000-8000-00000000000a", + purpose_code="offer_approval_review", + reason_code="selected_candidate_offer_review", + generated_at=datetime(2026, 8, 19, 5, 10, 0, 123456, tzinfo=timezone.utc), + evidence_version=evidence_version, + ) + + +def test_evidence_version_is_bound_to_offer_correlation_evidence() -> None: + """Changing evidence version must change canonical JSON and its correlation digest.""" + first = _build(1) + second = _build(2) + + assert first.evidence_version == 1 + assert json.loads(first.canonical_json())["evidence_version"] == 1 + assert second.evidence_version == 2 + assert first.canonical_json() != second.canonical_json() + assert first.sha256_digest() != second.sha256_digest() + + +@pytest.mark.parametrize("evidence_version", [0, -1, True, "1", 2_147_483_648]) +def test_evidence_version_fails_closed(evidence_version: object) -> None: + """Reject non-integer, non-positive, or overflow evidence versions.""" + with pytest.raises(ValueError, match="evidence_version"): + _build(evidence_version) + + +def test_replace_cannot_bypass_evidence_version_validation() -> None: + """Mutation-by-copy must revalidate the immutable evidence version invariant.""" + with pytest.raises(ValueError, match="evidence_version"): + replace(_build(), evidence_version=False) diff --git a/packages/offer-approval/tests/test_packet.py b/packages/offer-approval/tests/test_packet.py new file mode 100644 index 000000000..c27e5832a --- /dev/null +++ b/packages/offer-approval/tests/test_packet.py @@ -0,0 +1,360 @@ +from __future__ import annotations + +from dataclasses import FrozenInstanceError, replace +from datetime import datetime, timedelta, timezone, tzinfo +from hashlib import sha256 +import json + +import pytest + +from orgmetra_offer_approval import ( + OfferApprovalPacket, + build_offer_approval_packet, +) + + +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +OFFER_ID = "10000000-0000-4000-8000-000000000001" +CANDIDATE_ID = "10000000-0000-4000-8000-000000000002" +REQUISITION_ID = "10000000-0000-4000-8000-000000000003" +JOB_ID = "10000000-0000-4000-8000-000000000004" +POSITION_ID = "10000000-0000-4000-8000-000000000005" +DECISION_ID = "10000000-0000-4000-8000-000000000006" +COMPENSATION_ID = "10000000-0000-4000-8000-000000000007" +TERMS_ID = "10000000-0000-4000-8000-000000000008" +REQUESTER_ID = "10000000-0000-4000-8000-000000000009" +APPROVER_ID = "10000000-0000-4000-8000-00000000000a" +UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + + +def valid_kwargs() -> dict[str, object]: + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "offer_approval_reference": f"offer_approval:{OFFER_ID}", + "candidate_profile_reference": f"candidate_profile:{CANDIDATE_ID}", + "requisition_reference": f"requisition:{REQUISITION_ID}", + "job_profile_reference": f"job_profile:{JOB_ID}", + "position_record_reference": f"position_record:{POSITION_ID}", + "selection_decision_reference": f"selection_decision:{DECISION_ID}", + "selection_decision_digest": DIGEST_A, + "compensation_package_reference": f"compensation_package:{COMPENSATION_ID}", + "compensation_package_digest": DIGEST_B, + "offer_terms_reference": f"offer_terms:{TERMS_ID}", + "offer_terms_digest": DIGEST_C, + "requester_reference": f"actor:{REQUESTER_ID}", + "approver_reference": f"actor:{APPROVER_ID}", + "purpose_code": "offer_approval_review", + "reason_code": "selected_candidate_offer_review", + "generated_at": datetime(2026, 8, 19, 5, 10, 0, 123456, tzinfo=timezone.utc), + } + + +def build_valid() -> OfferApprovalPacket: + return build_offer_approval_packet(**valid_kwargs()) + + +def test_builds_value_free_human_offer_approval_packet() -> None: + packet = build_valid() + + assert packet.contains_candidate_pii is False + assert packet.contains_compensation_values is False + assert packet.human_confirmation_required is True + assert packet.decision_authority == "human_approval_only" + assert packet.review_state == "requires_human_approval" + assert packet.delivery_state == "not_authorized_to_send" + assert "authoritative offer workflow" in packet.next_action + assert "communicating or executing the offer" in packet.next_action + + +def test_position_reference_is_optional_without_collapsing_job_scope() -> None: + kwargs = valid_kwargs() + kwargs["position_record_reference"] = None + packet = build_offer_approval_packet(**kwargs) + payload = json.loads(packet.canonical_json()) + + assert packet.job_profile_reference == f"job_profile:{JOB_ID}" + assert payload["position_record_reference"] is None + + +def test_canonical_json_and_digest_are_deterministic_and_value_free() -> None: + packet = build_valid() + payload = json.loads(packet.canonical_json()) + + assert payload["generated_at"] == "2026-08-19T05:10:00.123456Z" + assert payload["candidate_profile_reference"] == f"candidate_profile:{CANDIDATE_ID}" + assert "candidate_name" not in payload + assert "candidate_email" not in payload + assert "salary" not in payload + assert "compensation_value" not in payload + assert "assessment_score" not in payload + assert "model_output" not in payload + assert packet.sha256_digest() == sha256(packet.canonical_json().encode("utf-8")).hexdigest() + + +def test_repr_redacts_candidate_compensation_and_actor_correlation() -> None: + packet = build_valid() + rendered = repr(packet) + + assert rendered == "OfferApprovalPacket()" + assert packet.tenant_record_id not in rendered + assert packet.candidate_profile_reference not in rendered + assert packet.compensation_package_reference not in rendered + assert packet.compensation_package_digest not in rendered + assert packet.requester_reference not in rendered + assert packet.approver_reference not in rendered + + +def test_fractional_seconds_remain_distinct_evidence() -> None: + first = build_valid() + second = replace(first, generated_at=first.generated_at + timedelta(microseconds=1)) + + assert first.canonical_json() != second.canonical_json() + assert first.sha256_digest() != second.sha256_digest() + + +@pytest.mark.parametrize( + ("field_name", "value"), + [ + ("tenant_record_id", "not-a-uuid"), + ("tenant_record_id", "00000000-0000-0000-0000-000000000000"), + ("tenant_record_id", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"), + ("tenant_record_id", None), + ], +) +def test_rejects_nonoperational_tenant_identity(field_name: str, value: object) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match="tenant_record_id"): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("offer_approval_reference", f"offer:{OFFER_ID}", "offer_approval"), + ("candidate_profile_reference", f"candidate:{CANDIDATE_ID}", "candidate_profile"), + ("requisition_reference", f"request:{REQUISITION_ID}", "requisition"), + ("job_profile_reference", f"job:{JOB_ID}", "job_profile"), + ("position_record_reference", f"position:{POSITION_ID}", "position_record"), + ("selection_decision_reference", f"decision:{DECISION_ID}", "selection_decision"), + ("compensation_package_reference", f"compensation:{COMPENSATION_ID}", "compensation_package"), + ("offer_terms_reference", f"terms:{TERMS_ID}", "offer_terms"), + ("requester_reference", f"person:{REQUESTER_ID}", "actor"), + ("approver_reference", f"reviewer:{APPROVER_ID}", "actor"), + ("requester_reference", "actor:", "actor"), + ("requester_reference", 1, "actor"), + ("requester_reference", "actor:" + "a" * 155, "actor"), + ], +) +def test_rejects_bad_opaque_references( + field_name: str, + value: object, + message: str, +) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "prefix"), + [ + ("offer_approval_reference", "offer_approval"), + ("candidate_profile_reference", "candidate_profile"), + ("requisition_reference", "requisition"), + ("job_profile_reference", "job_profile"), + ("position_record_reference", "position_record"), + ("selection_decision_reference", "selection_decision"), + ("compensation_package_reference", "compensation_package"), + ("offer_terms_reference", "offer_terms"), + ("requester_reference", "actor"), + ("approver_reference", "actor"), + ], +) +def test_rejects_uuid1_trust_references_through_direct_and_replace( + field_name: str, + prefix: str, +) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = f"{prefix}:{UUID1_ID}" + with pytest.raises(ValueError, match=f"opaque {prefix}"): + OfferApprovalPacket(**kwargs) + + packet = build_valid() + with pytest.raises(ValueError, match=f"opaque {prefix}"): + replace(packet, **{field_name: f"{prefix}:{UUID1_ID}"}) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("candidate_profile_reference", "candidate_profile:Jane-Doe", "opaque candidate_profile"), + ("compensation_package_reference", "compensation_package:120000", "opaque compensation_package"), + ("offer_terms_reference", "offer_terms:remote-two-days", "opaque offer_terms"), + ("requester_reference", "actor:seonghobae", "opaque actor"), + ("candidate_profile_reference", "candidate_profile:00000000-0000-0000-0000-000000000000", "opaque candidate_profile"), + ("candidate_profile_reference", "candidate_profile:FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", "opaque candidate_profile"), + ], +) +def test_rejects_value_bearing_or_noncanonical_reference_suffixes( + field_name: str, + value: object, + message: str, +) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + build_offer_approval_packet(**kwargs) + + packet = build_valid() + with pytest.raises(ValueError, match=message): + replace(packet, **{field_name: value}) + + +@pytest.mark.parametrize( + "field_name", + [ + "selection_decision_digest", + "compensation_package_digest", + "offer_terms_digest", + ], +) +@pytest.mark.parametrize("value", ["A" * 64, "a" * 63, 1]) +def test_rejects_malformed_digests(field_name: str, value: object) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match="lowercase SHA-256"): + build_offer_approval_packet(**kwargs) + + +def test_approver_must_be_distinct_from_requester() -> None: + kwargs = valid_kwargs() + kwargs["approver_reference"] = kwargs["requester_reference"] + with pytest.raises(ValueError, match="different accountable actor"): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("purpose_code", "selection_review", "offer_approval_review"), + ("purpose_code", "OfferApprovalReview", "lower snake_case"), + ("purpose_code", "a_" + "b" * 64, "lower snake_case"), + ("purpose_code", 1, "lower snake_case"), + ("reason_code", "offer", "lower snake_case"), + ("reason_code", "Offer_Review", "lower snake_case"), + ("reason_code", 1, "lower snake_case"), + ], +) +def test_rejects_bad_governance_codes( + field_name: str, + value: object, + message: str, +) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + "value", + [ + "jane_doe", + "salary_120000", + "remote_two_days", + ], +) +def test_rejects_value_bearing_reason_codes_through_direct_and_replace(value: str) -> None: + kwargs = valid_kwargs() + kwargs["reason_code"] = value + with pytest.raises(ValueError, match="reviewed non-sensitive"): + OfferApprovalPacket(**kwargs) + + packet = build_valid() + with pytest.raises(ValueError, match="reviewed non-sensitive"): + replace(packet, reason_code=value) + + +class NullOffsetTz(tzinfo): + def utcoffset(self, dt: datetime | None) -> None: + return None + + def dst(self, dt: datetime | None) -> None: + return None + + def tzname(self, dt: datetime | None) -> str: + return "NULL" + + +@pytest.mark.parametrize( + "value", + [ + datetime(2026, 8, 19, 5, 10), + "2026-08-19T05:10:00Z", + 1, + datetime(2026, 8, 19, 5, 10).replace(tzinfo=NullOffsetTz()), + ], +) +def test_rejects_nonaware_generation_time(value: object) -> None: + kwargs = valid_kwargs() + kwargs["generated_at"] = value + with pytest.raises(ValueError, match="timezone-aware"): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("contains_candidate_pii", True, "candidate PII"), + ("contains_candidate_pii", 0, "candidate PII"), + ("contains_compensation_values", True, "compensation values"), + ("contains_compensation_values", 0, "compensation values"), + ("human_confirmation_required", False, "human confirmation"), + ("human_confirmation_required", 1, "human confirmation"), + ("decision_authority", "automated", "human_approval_only"), + ("review_state", "approved", "requires_human_approval"), + ("delivery_state", "ready_to_send", "not_authorized_to_send"), + ("next_action", "Send the offer.", "governed offer-approval instruction"), + ], +) +def test_direct_constructor_and_replace_fail_closed( + field_name: str, + value: object, + message: str, +) -> None: + direct_kwargs = valid_kwargs() + direct_kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + OfferApprovalPacket(**direct_kwargs) + + packet = build_valid() + with pytest.raises(ValueError, match=message): + replace(packet, **{field_name: value}) + + +def test_frozen_packet_rejects_mutation() -> None: + packet = build_valid() + with pytest.raises(FrozenInstanceError): + packet.review_state = "approved" + + +def test_timezone_is_normalized_without_losing_precision() -> None: + kwargs = valid_kwargs() + kwargs["generated_at"] = datetime( + 2026, + 8, + 19, + 14, + 10, + 0, + 654321, + tzinfo=timezone(timedelta(hours=9)), + ) + packet = build_offer_approval_packet(**kwargs) + + payload = json.loads(packet.canonical_json()) + assert payload["generated_at"] == "2026-08-19T05:10:00.654321Z" diff --git a/packages/offer-approval/tests/test_tenant_identity_privacy.py b/packages/offer-approval/tests/test_tenant_identity_privacy.py new file mode 100644 index 000000000..05a916973 --- /dev/null +++ b/packages/offer-approval/tests/test_tenant_identity_privacy.py @@ -0,0 +1,42 @@ +"""Privacy and interoperability regression for offer-approval tenant identity.""" +from dataclasses import replace +from datetime import datetime, timezone + +from orgmetra_offer_approval import build_offer_approval_packet + +UUID7_TENANT = "10000000-0000-7000-8000-000000000001" + + +def _valid_kwargs() -> dict[str, object]: + """Return one valid offer-approval packet input mapping.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "offer_approval_reference": "offer_approval:10000000-0000-4000-8000-000000000001", + "candidate_profile_reference": "candidate_profile:10000000-0000-4000-8000-000000000002", + "requisition_reference": "requisition:10000000-0000-4000-8000-000000000003", + "job_profile_reference": "job_profile:10000000-0000-4000-8000-000000000004", + "position_record_reference": "position_record:10000000-0000-4000-8000-000000000005", + "selection_decision_reference": "selection_decision:10000000-0000-4000-8000-000000000006", + "selection_decision_digest": "a" * 64, + "compensation_package_reference": "compensation_package:10000000-0000-4000-8000-000000000007", + "compensation_package_digest": "b" * 64, + "offer_terms_reference": "offer_terms:10000000-0000-4000-8000-000000000008", + "offer_terms_digest": "c" * 64, + "requester_reference": "actor:10000000-0000-4000-8000-000000000009", + "approver_reference": "actor:10000000-0000-4000-8000-00000000000a", + "purpose_code": "offer_approval_review", + "reason_code": "selected_candidate_offer_review", + "generated_at": datetime(2026, 8, 19, 5, 10, tzinfo=timezone.utc), + } + + +def test_authoritative_uuid7_tenant_identity_is_accepted_by_builder_and_replace() -> None: + """Accept tenant UUIDs already valid at the authoritative Orgmetra core boundary.""" + kwargs = _valid_kwargs() + kwargs["tenant_record_id"] = UUID7_TENANT + + packet = build_offer_approval_packet(**kwargs) + replaced = replace(build_offer_approval_packet(**_valid_kwargs()), tenant_record_id=UUID7_TENANT) + + assert packet.tenant_record_id == UUID7_TENANT + assert replaced.tenant_record_id == UUID7_TENANT