mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-10-07 16:08:54 +00:00
Cover the deterministic parser with golden-file tests over 55 real scrubbed .eml fixtures (both comment sub-shapes, username Submitted-By, address present/absent, br+CRLF assign addresses), fail-closed validation-gate rules, adversarial and prompt-injection cases that must route to ai_fallback or parse without corrupting other fields, the issue #23 comment_id idempotency invariants, and the Bedrock-fallback dispatch plus EMF-metric emission with a mocked invoke_model. Extend pytest.ini testpaths to discover the co-located suite, and update tests/conftest.load_handler to put a handler's own directory on sys.path so the WO handler's new `from template_parser import ...` resolves under the existing shared handler tests. Point test_local.py at the new template-first + Bedrock flow. Refs: #23
88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
"""Issue #23: comment_id (WorkOrderComments range key) must be unique per source
|
|
email AND byte-identical across Lambda async retries of the same S3 object, with
|
|
wall-clock now() kept OUT of the key."""
|
|
|
|
import handler
|
|
|
|
|
|
def _parsed(wo="11144580730", comment_time="2026-04-27T23:51:48"):
|
|
return {
|
|
"work_order_id": wo,
|
|
"email_type": "comment",
|
|
"commenter": "jdoe",
|
|
"comment_text": "a comment",
|
|
"comment_time": comment_time,
|
|
}
|
|
|
|
|
|
def _comment_ids(table):
|
|
return [item["comment_id"] for item in table.puts]
|
|
|
|
|
|
def test_same_object_key_retry_is_idempotent(fake_dynamo):
|
|
parsed = _parsed()
|
|
handler.save_event(parsed, "s3://bucket/inbound/obj-a", "inbound/obj-a")
|
|
handler.save_event(parsed, "s3://bucket/inbound/obj-a", "inbound/obj-a")
|
|
table = fake_dynamo.tables[handler.COMMENTS_TABLE]
|
|
ids = _comment_ids(table)
|
|
# Two put_item calls with an identical key -> one logical row (overwrite).
|
|
assert ids[0] == ids[1]
|
|
assert len(table.store) == 1
|
|
|
|
|
|
def test_distinct_emails_same_wo_and_time_are_distinct_rows(fake_dynamo):
|
|
parsed = _parsed()
|
|
handler.save_event(parsed, "s3://bucket/inbound/obj-a", "inbound/obj-a")
|
|
handler.save_event(parsed, "s3://bucket/inbound/obj-b", "inbound/obj-b")
|
|
table = fake_dynamo.tables[handler.COMMENTS_TABLE]
|
|
ids = _comment_ids(table)
|
|
assert ids[0] != ids[1]
|
|
assert len(table.store) == 2
|
|
|
|
|
|
def test_absent_comment_time_is_stable_across_retries(fake_dynamo):
|
|
parsed = _parsed(comment_time=None)
|
|
handler.save_event(parsed, "s3://bucket/inbound/obj-c", "inbound/obj-c")
|
|
handler.save_event(parsed, "s3://bucket/inbound/obj-c", "inbound/obj-c")
|
|
table = fake_dynamo.tables[handler.COMMENTS_TABLE]
|
|
ids = _comment_ids(table)
|
|
assert ids[0] == ids[1] # 'nocomment' segment, not now()
|
|
assert "#nocomment#" in ids[0]
|
|
assert len(table.store) == 1
|
|
|
|
|
|
def test_event_id_independent_of_now(fake_dynamo, monkeypatch):
|
|
"""Freezing / advancing the clock must not change the range key."""
|
|
parsed = _parsed(comment_time=None)
|
|
|
|
class FrozenDT:
|
|
offset = 0
|
|
|
|
@classmethod
|
|
def now(cls, tz=None):
|
|
import datetime as _dt
|
|
|
|
return _dt.datetime(2020, 1, 1, tzinfo=tz) + _dt.timedelta(
|
|
seconds=cls.offset
|
|
)
|
|
|
|
monkeypatch.setattr(handler, "datetime", FrozenDT)
|
|
handler.save_event(parsed, "s3://b/inbound/obj-d", "inbound/obj-d")
|
|
FrozenDT.offset = 999999 # simulate a later retry
|
|
handler.save_event(parsed, "s3://b/inbound/obj-d", "inbound/obj-d")
|
|
table = fake_dynamo.tables[handler.COMMENTS_TABLE]
|
|
ids = _comment_ids(table)
|
|
assert ids[0] == ids[1]
|
|
# created_at (display) may differ, but the KEY must not.
|
|
assert len(table.store) == 1
|
|
|
|
|
|
def test_comment_id_format_shape(fake_dynamo):
|
|
parsed = _parsed()
|
|
handler.save_event(parsed, "s3://bucket/inbound/obj-e", "inbound/obj-e")
|
|
table = fake_dynamo.tables[handler.COMMENTS_TABLE]
|
|
cid = _comment_ids(table)[0]
|
|
wo, time_part, suffix = cid.split("#")
|
|
assert wo == "11144580730"
|
|
assert time_part == "2026-04-27T23:51:48"
|
|
assert len(suffix) == 12 and all(c in "0123456789abcdef" for c in suffix)
|