mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-10-03 22:23:11 +00:00
Some checks are pending
Deploy / deploy (push) Waiting to run
Both email-processor God-handlers split along the seams that already work in the flat-sibling pattern established by lambdas/shared/, so bare-name imports keep working under the existing bundling glob. PO (5-way split): handler.py keeps only the event loop, fail-closed auth, and email_type routing. extraction.py holds extract_with_claude and _EMAIL_TAG_RE, importing EXTRACTION_PROMPT from prompts.py and parse_raw_email from shared/email_parsing.py rather than recreating a PO-local copy. enrichment.py is a pure code move of enrich_parsed and pad_zip (PO-only; WO has no enrichment stage) with zero behavior change. telemetry.py holds the EMF ParseMethod emit wrappers. persistence.py holds _write_fields/_merge_update/save_*, collapsing the byte-identical save_new_po/save_revision bodies into one _save_merge helper that both now call through, preserving the sticky Cancelled ConditionExpression guard for both callers; save_cancellation stays separate. WO (5 concerns, no enrichment stage): the handler loop keeps validate_ai_fallback and the re.fullmatch(r"[0-9]+", work_order_id) key guard ahead of both save_work_order and save_event, since the guard protects the DynamoDB partition key and the '#'-delimited comment_id range-key segment. _header_date_iso and comment_id determinism stay colocated with persistence.py's save_event for the retry-idempotent event_id key. EXTRACTION_PROMPT (PO) moves to prompts.py with cross-reference headers to derived_fields.py's authoritative trade/site/fiscal rule tables; handler.py re-exports it (from prompts import EXTRACTION_PROMPT) since four tests dereference handler.EXTRACTION_ PROMPT directly. WO's prompt moves the same way. I/O modules (extraction.py's bedrock client, persistence.py's dynamodb resource, handler.py's s3 client) get lazy cached boto3 accessors; pure modules (enrichment.py, prompts.py, telemetry.py) import no boto3. Test monkeypatch surfaces move to the module that now owns the client (e.g. persistence.dynamodb) everywhere tests patch it, and the moto-before-handler-import ordering in _po_parser_support.py is preserved so the moto-backed suites don't hit real AWS. Behavior-preservation pins, verified with tests: PO still emits ParseMethod=ai_fallback before the Bedrock call, with ai_fallback_rejected as the additive second datapoint on rejection. WO still emits after its gate with mutually-exclusive ai_fallback / ai_fallback_rejected. Shadow DerivedFieldAgreement telemetry stays ai_fallback-only. derived_fields.py is untouched (diff against feature/phase-3-shared-extraction is empty). handler(event, context) signatures and the save_* public contract are unchanged on both pipelines; goldens unchanged. PO_EXPECTED_TOP_LEVEL_MODULES and its WO equivalent in tests/test_bundle_consistency.py are updated for the new sibling modules so the AST bundle-consistency test still fails on an unshipped or uncommented-out sibling.
72 lines
2.8 KiB
Python
72 lines
2.8 KiB
Python
"""Deploy-guard healthcheck branch (Phase 0).
|
|
|
|
The handler must answer a top-level direct-invoke ``{"healthcheck": true}``
|
|
probe immediately -- BEFORE any S3 fetch, SES sender-auth gate, or Records
|
|
iteration -- and must do so without touching AWS or emitting any telemetry
|
|
that could trip the ``sender_auth_rejected`` metric-filter alarm.
|
|
|
|
These tests import the WO ``handler`` via the ``_wo_parser_support`` sys.path
|
|
loader idiom (region + module dir set on import); no fake_dynamo/S3 wiring is
|
|
needed because a correct healthcheck returns before any client is used.
|
|
"""
|
|
|
|
import handler
|
|
import persistence
|
|
from _wo_parser_support import FakeDynamoResource
|
|
|
|
|
|
class _ExplodingS3:
|
|
"""Any attribute access is a test failure: the healthcheck branch must
|
|
return before the handler ever reaches the S3 client."""
|
|
|
|
def get_object(self, *a, **k): # noqa: N803
|
|
raise AssertionError("healthcheck branch touched S3")
|
|
|
|
|
|
def test_healthcheck_returns_ok():
|
|
assert handler.handler({"healthcheck": True}, None) == {"healthcheck": "ok"}
|
|
|
|
|
|
def test_healthcheck_precedes_s3_and_auth(monkeypatch):
|
|
# If the early-return were missing or misplaced, the handler would call
|
|
# s3.get_object / authenticate_inbound_email; both are booby-trapped.
|
|
monkeypatch.setattr(handler, "s3", _ExplodingS3())
|
|
# Phase 5: the dynamodb cache lives on persistence.py; handler has no
|
|
# dynamodb attribute to patch. (s3 stays on handler -- the loop owns it.)
|
|
monkeypatch.setattr(persistence, "dynamodb", FakeDynamoResource())
|
|
|
|
def _boom_auth(*a, **k):
|
|
raise AssertionError("healthcheck branch reached SES auth")
|
|
|
|
monkeypatch.setattr(handler, "authenticate_inbound_email", _boom_auth)
|
|
|
|
assert handler.handler({"healthcheck": True}, None) == {"healthcheck": "ok"}
|
|
|
|
|
|
def test_healthcheck_emits_no_metric(monkeypatch):
|
|
# No EMF / ParseOutcome emission on the healthcheck path.
|
|
def _boom_metric(*a, **k):
|
|
raise AssertionError("healthcheck branch emitted a metric")
|
|
|
|
monkeypatch.setattr(handler, "emit_parse_metric", _boom_metric)
|
|
|
|
assert handler.handler({"healthcheck": True}, None) == {"healthcheck": "ok"}
|
|
|
|
|
|
def test_healthcheck_only_on_literal_true():
|
|
# A truthy-but-not-True value must NOT trigger the branch: it falls through
|
|
# to the (empty) Records loop and returns the normal 200 envelope. This
|
|
# keeps the trigger to the exact direct-invoke contract.
|
|
assert handler.handler({"healthcheck": "yes"}, None) == {
|
|
"statusCode": 200,
|
|
"body": "OK",
|
|
}
|
|
|
|
|
|
def test_records_event_ignores_healthcheck_lookalike():
|
|
# A real S3 event shape has no top-level healthcheck key; an empty Records
|
|
# list is processed normally without hitting the early return.
|
|
assert handler.handler({"Records": []}, None) == {
|
|
"statusCode": 200,
|
|
"body": "OK",
|
|
}
|