From 822a7aa19f93f8572d2e2a124ef97d26476a0154 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 01:18:24 +0000 Subject: [PATCH 1/4] fix(tests): assert the full fail-closed enrich envelope in gate_egress test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_request_enrichment_fails_closed_without_gate_url compared the whole result dict against a three-key literal that omitted idempotency_key, so it failed against the implementation it was introduced alongside (#259). engine/gate_egress.py documents the authoritative contract: "one attempt per call; retry is the caller's decision and requires the idempotency key returned in the result". All three return paths of request_enrichment honour that, the sibling success-path test already asserts result["idempotency_key"], and engine/health/enrichment_trigger.py forwards the whole envelope to its caller, so the key is load-bearing on the fail-closed path too. The test encoded the wrong envelope, so the test is corrected. Strict whole-dict equality is kept — the envelope stays exactly pinned, with the expected key derived from the module's public enrichment_idempotency_key helper rather than a hardcoded digest. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3 --- tests/unit/test_gate_egress.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_gate_egress.py b/tests/unit/test_gate_egress.py index 922080c7..6818c78b 100644 --- a/tests/unit/test_gate_egress.py +++ b/tests/unit/test_gate_egress.py @@ -82,7 +82,15 @@ async def test_request_enrichment_fails_closed_without_gate_url(monkeypatch: pyt fake = _FakeClient(response=_response_packet()) monkeypatch.setattr(gate_egress, "get_gate_client", lambda: fake) result = await request_enrichment(tenant="acme", entity_id="ent-1", domain="plasticos", target_fields=["polymer"]) - assert result == {"status": "failed", "error": "gate_not_configured", "action": "enrich"} + assert result == { + "status": "failed", + "error": "gate_not_configured", + "action": "enrich", + # The module contract returns the idempotency key on every path, + # including fail-closed, because retry is the caller's decision and + # requires the key (see engine/gate_egress.py docstring). + "idempotency_key": enrichment_idempotency_key("acme", "ent-1", ["polymer"]), + } assert fake.calls == [], "no direct fallback: nothing may be sent when Gate is not configured" From a1980c8e1064a1df28b6bdbe8874e12b2cdc9556 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 01:25:13 +0000 Subject: [PATCH 2/4] fix(compliance): stop the phone pattern matching inside opaque identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handle_match builds query_id as `q_` + uuid4().hex[:12]. The PHONE value pattern was the only one in _PII_PATTERNS without boundary guards, so any 10-digit run inside that token matched it. ComplianceEngine.redact_response then removed the field outright (PIIHandler.redact pops the key), so the match response silently lost query_id. Measured on 200,000 generated ids: 1.572% were classified as a phone number. That is a live response-shape defect, and it is what made tests/test_handlers.py::test_match_returns_structure fail intermittently — the assertion is correct, the engine was dropping the key. `\b` is not usable here: the pattern can start with `+` or `(`, which are not word characters, so a leading `\b` would break `+1 (555) 123-4567`. SSN and IP_ADDRESS can use `\b` because they start with a digit. Lookarounds for [0-9A-Za-z_] give the same protection without that constraint. Verified: 0/200,000 false positives after the change, while `555-123-4567`, `+1 (555) 123-4567`, `5551234567` and `call 555.123.4567 now` are all still detected, as is detection by field name. Both directions are pinned by new regression tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3 --- engine/compliance/pii.py | 8 +++++++- tests/compliance/test_hipaa.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/engine/compliance/pii.py b/engine/compliance/pii.py index c336340c..4d60343b 100644 --- a/engine/compliance/pii.py +++ b/engine/compliance/pii.py @@ -59,8 +59,14 @@ class PIISensitivity(StrEnum): PIICategory.EMAIL: re.compile( r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}", ), + # The guards keep a 10-digit run from matching inside a longer alphanumeric + # token — opaque identifiers such as `q_5c9382647927` (handle_match's + # query_id) otherwise read as phone numbers and get redacted out of the + # response. `\b` cannot be used here because a leading `+` or `(` is not a + # word character, so it would break `+1 (555) 123-4567`. SSN and IP_ADDRESS + # below rely on `\b` for the same reason, their patterns start with a digit. PIICategory.PHONE: re.compile( - r"(?:\+?1[\-\s.]?)?\(?\d{3}\)?[\-\s.]?\d{3}[\-\s.]?\d{4}", + r"(? None: assert detections[0].category == PIICategory.SSN assert detections[0].detected_by == "pattern_match" + def test_phone_pattern_does_not_match_inside_opaque_identifier(self) -> None: + """A digit run inside a longer token is not a phone number. + + Regression: handle_match builds query_id as `q_<12 hex chars>`. Roughly + 1.6% of those contain a 10-digit run, which the unguarded phone pattern + matched — ComplianceEngine.redact_response then deleted query_id from + the match response for those requests. + """ + handler = PIIHandler() + + for opaque in ("q_5c9382647927", "q_a90333567456", "q_6281353751e8"): + assert handler.detect_pii({"query_id": opaque}) == [], f"{opaque} must not be treated as PII" + + def test_detect_phone_by_pattern_still_matches_real_numbers(self) -> None: + """Guarding the phone pattern must not cost real phone detection.""" + handler = PIIHandler() + + for number in ("555-123-4567", "+1 (555) 123-4567", "5551234567", "call 555.123.4567 now"): + detections = handler.detect_pii({"notes": number}) + assert [d.category for d in detections] == [PIICategory.PHONE], f"{number} must be detected as a phone" + def test_detect_nested_pii(self) -> None: """PII detected in nested dicts.""" handler = PIIHandler() From be1580933e2c31b806f87b06a3fe8aa9aed0e169 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 01:31:31 +0000 Subject: [PATCH 3/4] revert(tests): drop the gate_egress fix already carried by open PR #264 The overlap gate blocked publication: tests/unit/test_gate_egress.py textually conflicts with PR #264 ("fix(test): repair the fail-closed enrichment assertion that is red on main") and PR #262. PR #264 already contains the same correction, derived independently and byte-identical in the block it touches, and additionally asserts the idempotency key on the SDK-error path. That PR owns this file, so carrying a duplicate here would only add an add/add conflict for whichever landed second. This branch keeps only the compliance repair, which neither #264 nor #262 touches. This PR therefore stacks on #264's head so the enrichment assertion is inherited rather than duplicated. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3 --- tests/unit/test_gate_egress.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/unit/test_gate_egress.py b/tests/unit/test_gate_egress.py index 6818c78b..922080c7 100644 --- a/tests/unit/test_gate_egress.py +++ b/tests/unit/test_gate_egress.py @@ -82,15 +82,7 @@ async def test_request_enrichment_fails_closed_without_gate_url(monkeypatch: pyt fake = _FakeClient(response=_response_packet()) monkeypatch.setattr(gate_egress, "get_gate_client", lambda: fake) result = await request_enrichment(tenant="acme", entity_id="ent-1", domain="plasticos", target_fields=["polymer"]) - assert result == { - "status": "failed", - "error": "gate_not_configured", - "action": "enrich", - # The module contract returns the idempotency key on every path, - # including fail-closed, because retry is the caller's decision and - # requires the key (see engine/gate_egress.py docstring). - "idempotency_key": enrichment_idempotency_key("acme", "ent-1", ["polymer"]), - } + assert result == {"status": "failed", "error": "gate_not_configured", "action": "enrich"} assert fake.calls == [], "no direct fallback: nothing may be sent when Gate is not configured" From 568493bfbf3f1e6cdf2ea29a25b4814b757d7f33 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 01:40:41 +0000 Subject: [PATCH 4/4] fix(tests): port the fail-closed enrich assertion fix from #264 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The enrichment assertion is red on main and takes seven checks down with it on this PR: Test Suite, Pre-commit Hooks (via the pytest hook), Coverage (Codecov), Quality Gate, Baseline Ratchet (Required Tests + Verdict, which records it as an unledgered finding), and the CI Gate rollup that blocks merge. The fix exists in open PRs #264 and #262, which make byte-identical changes to this hunk. Porting it rather than waiting: it no-ops once main carries it. This is the exact hunk both PRs carry, deliberately with no edits of my own. An earlier attempt here rewrote the same assertion with an explanatory comment inside the dict, which made it textually different and so a genuine add/add conflict — that is what the overlap gate caught, and it was right to. Identical text on both sides merges cleanly in a three-way merge, so this port conflicts with neither PR. engine/gate_egress.py documents the contract this restores: "retry is the caller's decision and requires the idempotency key returned in the result". All three return paths of request_enrichment honour it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3 --- tests/unit/test_gate_egress.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_gate_egress.py b/tests/unit/test_gate_egress.py index 922080c7..55d68ae6 100644 --- a/tests/unit/test_gate_egress.py +++ b/tests/unit/test_gate_egress.py @@ -82,7 +82,12 @@ async def test_request_enrichment_fails_closed_without_gate_url(monkeypatch: pyt fake = _FakeClient(response=_response_packet()) monkeypatch.setattr(gate_egress, "get_gate_client", lambda: fake) result = await request_enrichment(tenant="acme", entity_id="ent-1", domain="plasticos", target_fields=["polymer"]) - assert result == {"status": "failed", "error": "gate_not_configured", "action": "enrich"} + assert result == { + "status": "failed", + "error": "gate_not_configured", + "action": "enrich", + "idempotency_key": enrichment_idempotency_key("acme", "ent-1", ["polymer"]), + } assert fake.calls == [], "no direct fallback: nothing may be sent when Gate is not configured"