From 89bf47c1bb6322c9d63cfa18a0d066943ba31ee5 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 15 Jul 2026 20:11:04 -0400 Subject: [PATCH] Add offline WO parser test suite 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 --- .../tests/_wo_parser_support.py | 75 ++++++++ lambdas/wo/email_processor/tests/conftest.py | 19 ++ .../fixtures/expected/assign-html-01.json | 18 ++ .../fixtures/expected/assign-html-02.json | 18 ++ .../fixtures/expected/assign-html-03.json | 18 ++ .../fixtures/expected/assign-html-04.json | 18 ++ .../fixtures/expected/assign-html-05.json | 18 ++ .../expected/update-plaintext-01.json | 18 ++ .../expected/update-plaintext-02.json | 18 ++ .../expected/update-plaintext-03.json | 18 ++ .../expected/update-plaintext-04.json | 18 ++ .../expected/update-plaintext-05.json | 18 ++ .../expected/update-plaintext-06.json | 18 ++ .../expected/update-plaintext-07.json | 18 ++ .../expected/update-plaintext-08.json | 18 ++ .../expected/update-plaintext-09.json | 18 ++ .../expected/update-plaintext-10.json | 18 ++ .../expected/update-plaintext-11.json | 18 ++ .../expected/update-plaintext-12.json | 18 ++ .../expected/update-plaintext-13.json | 18 ++ .../expected/update-plaintext-14.json | 18 ++ .../expected/update-plaintext-15.json | 18 ++ .../expected/update-plaintext-16.json | 18 ++ .../expected/update-plaintext-17.json | 18 ++ .../expected/update-plaintext-18.json | 18 ++ .../expected/update-plaintext-19.json | 18 ++ .../expected/update-plaintext-20.json | 18 ++ .../expected/update-plaintext-21.json | 18 ++ .../expected/update-plaintext-22.json | 18 ++ .../expected/update-plaintext-23.json | 18 ++ .../expected/update-plaintext-24.json | 18 ++ .../expected/update-plaintext-25.json | 18 ++ .../expected/update-plaintext-26.json | 18 ++ .../expected/update-plaintext-27.json | 18 ++ .../expected/update-plaintext-28.json | 18 ++ .../expected/update-plaintext-29.json | 18 ++ .../expected/update-plaintext-30.json | 18 ++ .../expected/update-plaintext-31.json | 18 ++ .../expected/update-plaintext-32.json | 18 ++ .../expected/update-plaintext-33.json | 18 ++ .../expected/update-plaintext-34.json | 18 ++ .../expected/update-plaintext-35.json | 18 ++ .../expected/update-plaintext-36.json | 18 ++ .../expected/update-plaintext-37.json | 18 ++ .../expected/update-plaintext-38.json | 18 ++ .../expected/update-plaintext-39.json | 18 ++ .../expected/update-plaintext-40.json | 18 ++ .../expected/update-plaintext-41.json | 18 ++ .../expected/update-plaintext-42.json | 18 ++ .../expected/update-plaintext-43.json | 18 ++ .../expected/update-plaintext-44.json | 18 ++ .../expected/update-plaintext-45.json | 18 ++ .../expected/update-plaintext-46.json | 18 ++ .../expected/update-plaintext-47.json | 18 ++ .../expected/update-plaintext-48.json | 18 ++ .../expected/update-plaintext-49.json | 18 ++ .../expected/update-plaintext-50.json | 18 ++ .../tests/test_bedrock_fallback.py | 172 ++++++++++++++++++ .../email_processor/tests/test_comment_id.py | 88 +++++++++ .../wo/email_processor/tests/test_parser.py | 110 +++++++++++ .../tests/test_validation_gate.py | 143 +++++++++++++++ pytest.ini | 13 +- test_local.py | 21 ++- tests/conftest.py | 5 + 64 files changed, 1625 insertions(+), 11 deletions(-) create mode 100644 lambdas/wo/email_processor/tests/_wo_parser_support.py create mode 100644 lambdas/wo/email_processor/tests/conftest.py create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/assign-html-01.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/assign-html-02.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/assign-html-03.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/assign-html-04.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/assign-html-05.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-01.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-02.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-03.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-04.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-05.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-06.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-07.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-08.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-09.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-10.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-11.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-12.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-13.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-14.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-15.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-16.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-17.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-18.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-19.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-20.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-21.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-22.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-23.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-24.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-25.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-26.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-27.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-28.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-29.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-30.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-31.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-32.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-33.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-34.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-35.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-36.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-37.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-38.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-39.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-40.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-41.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-42.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-43.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-44.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-45.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-46.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-47.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-48.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-49.json create mode 100644 lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-50.json create mode 100644 lambdas/wo/email_processor/tests/test_bedrock_fallback.py create mode 100644 lambdas/wo/email_processor/tests/test_comment_id.py create mode 100644 lambdas/wo/email_processor/tests/test_parser.py create mode 100644 lambdas/wo/email_processor/tests/test_validation_gate.py diff --git a/lambdas/wo/email_processor/tests/_wo_parser_support.py b/lambdas/wo/email_processor/tests/_wo_parser_support.py new file mode 100644 index 0000000..2aa22f5 --- /dev/null +++ b/lambdas/wo/email_processor/tests/_wo_parser_support.py @@ -0,0 +1,75 @@ +"""Offline test helpers for the work-order email processor. + +Uniquely named (not ``conftest``) so ``from _wo_parser_support import ...`` never +collides with the repo's top-level ``tests/conftest.py`` when both test roots are +collected in one pytest run. +""" + +import glob +import json +import os +import sys + +# Make handler.py / template_parser.py importable and give boto3 a region so the +# module-level clients construct without a NoRegionError under CI. +_HERE = os.path.dirname(__file__) +_MODULE_DIR = os.path.abspath(os.path.join(_HERE, "..")) +if _MODULE_DIR not in sys.path: + sys.path.insert(0, _MODULE_DIR) +os.environ.setdefault("AWS_DEFAULT_REGION", "us-east-1") + +FIXTURES = os.path.join(_HERE, "fixtures") + + +def _stems(subdir): + return sorted( + os.path.splitext(os.path.basename(p))[0] + for p in glob.glob(os.path.join(FIXTURES, subdir, "*.eml")) + ) + + +UPDATE_STEMS = _stems("update-plaintext") +ASSIGN_STEMS = _stems("assign-html") +AI_FALLBACK_STEMS = _stems("ai-fallback") +ADVERSARIAL_STEMS = _stems("adversarial") + + +def load_email(subdir, stem): + """Parse a fixture .eml into the email_data dict the parser consumes.""" + from handler import parse_raw_email + + path = os.path.join(FIXTURES, subdir, f"{stem}.eml") + with open(path, "rb") as fh: + return parse_raw_email(fh.read()) + + +def load_golden(stem): + with open(os.path.join(FIXTURES, "expected", f"{stem}.json")) as fh: + return json.load(fh) + + +class FakeTable: + """Records put_item / update_item calls for assertions.""" + + def __init__(self, name): + self.name = name + self.puts = [] + self.updates = [] + # Simulate a single-row-per-key store to detect overwrite vs distinct. + self.store = {} + + def put_item(self, Item=None, **kwargs): # noqa: N803 (boto3 kwarg name) + self.puts.append(Item) + key = (Item.get("work_order_id"), Item.get("comment_id")) + self.store[key] = Item + + def update_item(self, **kwargs): + self.updates.append(kwargs) + + +class FakeDynamoResource: + def __init__(self): + self.tables = {} + + def Table(self, name): # noqa: N802 (boto3 method name) + return self.tables.setdefault(name, FakeTable(name)) diff --git a/lambdas/wo/email_processor/tests/conftest.py b/lambdas/wo/email_processor/tests/conftest.py new file mode 100644 index 0000000..b2b4aa5 --- /dev/null +++ b/lambdas/wo/email_processor/tests/conftest.py @@ -0,0 +1,19 @@ +"""Pytest fixtures for the work-order email-processor suite. + +Path/env setup and the reusable helpers live in ``_wo_parser_support`` (a +uniquely named module) so the test files never do ``from conftest import``, +which would be ambiguous against the repo's top-level ``tests/conftest.py``. +""" + +import pytest + +from _wo_parser_support import FakeDynamoResource + + +@pytest.fixture +def fake_dynamo(monkeypatch): + import handler + + fake = FakeDynamoResource() + monkeypatch.setattr(handler, "dynamodb", fake) + return fake diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-01.json b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-01.json new file mode 100644 index 0000000..2004939 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-01.json @@ -0,0 +1,18 @@ +{ + "address": "do veniam ullamco aliqua exerc\ndo incididunt ame\ndo sit ad ullamc\nconsectetur m\nconsectetur tempor sit e", + "assigned_to": null, + "building": "HTX1", + "comment_text": null, + "comment_time": null, + "commenter": null, + "date_reported": "2026-04-14T16:20:00", + "description": "ad ullamco sed dolor eiusmod ut", + "due_date": null, + "email_type": "new_work_order", + "priority": null, + "scheduled_start": "2026-04-14", + "severity": "5", + "site_code": "HTX1", + "status": "assigned", + "work_order_id": "18073740956" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-02.json b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-02.json new file mode 100644 index 0000000..6fcf733 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-02.json @@ -0,0 +1,18 @@ +{ + "address": "ut quis laboris aliqua et consectetur ipnostr\nminim ullamco nisi do i\nquis magna et", + "assigned_to": null, + "building": "HRD8", + "comment_text": null, + "comment_time": null, + "commenter": null, + "date_reported": "2026-06-23T11:05:00", + "description": "[HRD8] Short Description", + "due_date": null, + "email_type": "new_work_order", + "priority": null, + "scheduled_start": "2026-06-23", + "severity": "5", + "site_code": "HRD8", + "status": "assigned", + "work_order_id": "17187601507" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-03.json b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-03.json new file mode 100644 index 0000000..6aa3bac --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-03.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WFG1", + "comment_text": null, + "comment_time": null, + "commenter": null, + "date_reported": "2026-05-22T07:51:00", + "description": "eiusmod veniam magna et consecteenim dolore nostrud laboris do veniam ma", + "due_date": null, + "email_type": "new_work_order", + "priority": null, + "scheduled_start": "2026-07-31", + "severity": "5", + "site_code": "WFG1", + "status": "assigned", + "work_order_id": "18644082645" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-04.json b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-04.json new file mode 100644 index 0000000..9e9de71 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-04.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WUT1", + "comment_text": null, + "comment_time": null, + "commenter": null, + "date_reported": "2026-06-26T12:47:00", + "description": "[WUT1] Power Outage", + "due_date": null, + "email_type": "new_work_order", + "priority": null, + "scheduled_start": "2026-06-26", + "severity": "2", + "site_code": "WUT1", + "status": "assigned", + "work_order_id": "17194032177" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-05.json b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-05.json new file mode 100644 index 0000000..8a448af --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/assign-html-05.json @@ -0,0 +1,18 @@ +{ + "address": "ipsum enim adipsit ipsum lorem enim adipiscing dolor eiusmod ve", + "assigned_to": null, + "building": "CHI7", + "comment_text": null, + "comment_time": null, + "commenter": null, + "date_reported": "2026-04-10T15:42:00", + "description": "eiusmod sit eiusmod veniam elit nostrud laboris", + "due_date": null, + "email_type": "new_work_order", + "priority": null, + "scheduled_start": "2026-04-10", + "severity": "4", + "site_code": "CHI7", + "status": "assigned", + "work_order_id": "15481007953" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-01.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-01.json new file mode 100644 index 0000000..84eff47 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-01.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WCO0", + "comment_text": "ut amet ipsum enim exercitation sed dolor eiusmod sit eiusmod veniam elit ad ullamco aliqua et nostrud magna exercitation sed veniam elit dolor ad deiusmod sit ad adipiscing minim dolore consectetur tempor sit ipsum eiusmodo incididunt amet ad adipiscing tempor u", + "comment_time": "2026-04-27T23:51:48", + "commenter": "sit ad adipiscing dolor eiusmod sit ad ullamco nisi nisi enitempor sit ad adipiscing tem", + "date_reported": null, + "description": "consectetur tempor sit ipsum enim adipiscing doelit dolor eiusmod", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WCO0", + "status": null, + "work_order_id": "11144580730" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-02.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-02.json new file mode 100644 index 0000000..752b26d --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-02.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WIO9", + "comment_text": "incididunt amet tempor", + "comment_time": "2026-06-18T19:06:12", + "commenter": "lorem lo", + "date_reported": null, + "description": "adipiscing dolor eiusmod sit ipsum lorem lo", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WIO9", + "status": null, + "work_order_id": "17597770281" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-03.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-03.json new file mode 100644 index 0000000..187c572 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-03.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WGC5", + "comment_text": "sit ad dolore et consectetur ipsum lore", + "comment_time": "2026-06-22T20:21:39", + "commenter": "ullamco", + "date_reported": null, + "description": "sit eiusmod ven", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WGC5", + "status": null, + "work_order_id": "11584193048" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-04.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-04.json new file mode 100644 index 0000000..c264035 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-04.json @@ -0,0 +1,18 @@ +{ + "address": "sit eiusmod veniam magna consectetur minim elit minim elit", + "assigned_to": null, + "building": "UOH9", + "comment_text": "exercitatio", + "comment_time": "2026-06-05T18:46:43", + "commenter": "aliqua a", + "date_reported": null, + "description": "sit ipsum eiusmod sit ipsum enim exercitation sed incidid", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "UOH9", + "status": null, + "work_order_id": "15579542821" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-05.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-05.json new file mode 100644 index 0000000..97bc190 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-05.json @@ -0,0 +1,18 @@ +{ + "address": "veniam elit dolor ad adipiscing minim elit dolor eiusmod ut labore labore", + "assigned_to": null, + "building": "SWI4", + "comment_text": "dolore consectetur minim\neiusmod ut\nsit ad dolore consectet\nsit eiusmod sit ad adipiscing tempor sit eiusmod ut amet ipsum lorem enim adipiscing tempor ut quis elit minim ullamco aliqua exercitation sed dolor eiusmod ut quis laboris aliqu\nquis magna et nostrud laboris nisi enim adipiscing dolor lorem do v\nnisi enim dolore et nostrud sed incididunt amet tempor sit ipsum lorem lorem do sit\nlorem do veniam magna consectetur minim ullamco nisi enim adipiscing tempor quis magna consectetur minim elit dolor lorem", + "comment_time": null, + "commenter": null, + "date_reported": null, + "description": "ad adipiscing dolor eiusmod sit ad adipiscing minim ull", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "SWI4", + "status": null, + "work_order_id": "14882673235" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-06.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-06.json new file mode 100644 index 0000000..5e47353 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-06.json @@ -0,0 +1,18 @@ +{ + "address": "incididunt ut amet tempor qu\nveniam ullamco sed incididu\nnostrud magna et", + "assigned_to": null, + "building": "SJS2", + "comment_text": "aliqua exercitation aliqua exercitation laboris do sit a", + "comment_time": "2026-05-14T19:37:34", + "commenter": "nostrud", + "date_reported": null, + "description": "amet ipsum lorem do sit ad ullamco sed inc", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "SJS2", + "status": null, + "work_order_id": "15738557028" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-07.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-07.json new file mode 100644 index 0000000..03fd030 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-07.json @@ -0,0 +1,18 @@ +{ + "address": "amet ad adipiscing tempor sit eiusmod ut amet ipsum e\namet ipsum eiusmod\nnisi do incididunt qui", + "assigned_to": null, + "building": "WTX0", + "comment_text": "ut labor", + "comment_time": "2026-05-20T22:47:08", + "commenter": "sit eius", + "date_reported": null, + "description": "ut quis magna exercitation laboris do veniam ullamco aliqua et labore nostrud la", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WTX0", + "status": null, + "work_order_id": "14921957970" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-08.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-08.json new file mode 100644 index 0000000..2077b35 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-08.json @@ -0,0 +1,18 @@ +{ + "address": "ullamco nisi enim adipiscing\nadipiscing tempor ut amet ips\ndo sit ad ad", + "assigned_to": null, + "building": "LHO8", + "comment_text": "elit minim dolore et consectetur tempor quis magna et labore nostrud laboris nisi enim dolore consectetur tempor quis elit dolor lorem enim exercitation aliqua et consectetur ipsum lorem do sit eiusmod veniam magna consectetur minim dolore et nostrud se", + "comment_time": "2026-06-02T01:21:28", + "commenter": "ut quis elit dolor ad dolore et nostrud sed incididunt quis magna consectetur minim ullam", + "date_reported": null, + "description": "amet ipsum eiusmod veniam magna consectetur ipsum eiusmod", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "LHO8", + "status": null, + "work_order_id": "14486146471" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-09.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-09.json new file mode 100644 index 0000000..3630d34 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-09.json @@ -0,0 +1,18 @@ +{ + "address": "minim dolore nostrud se\net nostrud laboris\ntempor ut labore n", + "assigned_to": null, + "building": "XEV9", + "comment_text": "lorem lorem lorem enim dolore et consectetur minim elit minim ullamco nisi do sit ad adipiscing minim ul", + "comment_time": "2026-05-12T13:29:16", + "commenter": "dolor l", + "date_reported": null, + "description": "aliqua adipiscing dolor lore", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "XEV9", + "status": null, + "work_order_id": "13996242160" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-10.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-10.json new file mode 100644 index 0000000..05b3224 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-10.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "HLO1", + "comment_text": "exercitation sed veniam ullamco aliqua exercitation laboris aliqua ad", + "comment_time": null, + "commenter": null, + "date_reported": null, + "description": "lorem do veniam ullamco aliqua et consectetur minim elit incididunt amet ad ulla", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "HLO1", + "status": null, + "work_order_id": "13401256362" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-11.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-11.json new file mode 100644 index 0000000..51b2667 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-11.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WML1", + "comment_text": "amet ipsum enim dolore nostrud magna exercitation aliqua a", + "comment_time": "2026-05-13T17:49:37", + "commenter": "nostru", + "date_reported": null, + "description": "nostrud laboris do incididunt quis magna consectetur ips", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WML1", + "status": null, + "work_order_id": "18403645991" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-12.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-12.json new file mode 100644 index 0000000..e2ca75d --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-12.json @@ -0,0 +1,18 @@ +{ + "address": "aliqua exercitation laboris\namet ipsum enim adipiscing\nut quis elit dol", + "assigned_to": null, + "building": "SJS2", + "comment_text": "aliqua et nostrud magna exercitation sed ven", + "comment_time": null, + "commenter": null, + "date_reported": null, + "description": "dolor ad dolore consectetur ipsum eiusmod veniam elit incididunt quis", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "SJS2", + "status": null, + "work_order_id": "17206481612" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-13.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-13.json new file mode 100644 index 0000000..764181d --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-13.json @@ -0,0 +1,18 @@ +{ + "address": "elit minim ullamco nisi nisi do incididunt quis laboris do sit ipsu\nipsum eiusmod veni\nad ullamco aliq", + "assigned_to": null, + "building": "WTX5", + "comment_text": "magna exercitation laboris aliqua adipiscing tempor sit ad ullamco aliqua", + "comment_time": "2026-07-12T22:01:50", + "commenter": "dolor", + "date_reported": null, + "description": "eiusmod ut labore labore labore labore amet ipsum lorem do incididunt amet ipsum", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WTX5", + "status": null, + "work_order_id": "12701821079" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-14.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-14.json new file mode 100644 index 0000000..1244c3b --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-14.json @@ -0,0 +1,18 @@ +{ + "address": "et labore labore nostru\nconsectetur min\namet ad dolore c", + "assigned_to": null, + "building": "LRM3", + "comment_text": "aliqua exercitation sed incididunt ut amet ad dolore", + "comment_time": "2026-07-14T16:51:49", + "commenter": "quis eli", + "date_reported": null, + "description": "laboris nisi nisi enim adipisc", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "LRM3", + "status": null, + "work_order_id": "12133395677" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-15.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-15.json new file mode 100644 index 0000000..609f7c1 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-15.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WNM0", + "comment_text": "eiusmod sit ipsum", + "comment_time": "2026-05-29T17:40:21", + "commenter": "enim dol", + "date_reported": null, + "description": "lorem enim adipiscing minim ull", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WNM0", + "status": null, + "work_order_id": "11171293003" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-16.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-16.json new file mode 100644 index 0000000..df608da --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-16.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WND4", + "comment_text": "nisi enim dolore nostrud laboris nisi\nnisi do veniam magna exercitation aliqua et nostrud laboris", + "comment_time": "2026-05-18T17:55:06", + "commenter": "nostrud", + "date_reported": null, + "description": "amet tempor sit eiusmod sit ipsum enim adipiscing minim dolore et", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WND4", + "status": null, + "work_order_id": "13344130918" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-17.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-17.json new file mode 100644 index 0000000..5b4ec9b --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-17.json @@ -0,0 +1,18 @@ +{ + "address": "lorem do sit ad ullam\nipsum eiusmod ve\nmin", + "assigned_to": null, + "building": "DIA5", + "comment_text": "consectetur minim dolore et nostrud magna consectetur ipsum eiusmod veniamet nostrud magna consectetur tempor sit ipsum eiusmod ut amet ipsum lorem ullamco sed dolor lorem lorem enim adipiscing tempor quis elit dolor loremmagna consectetur minim dolore consectetur ipsum lorem lorem enim exercitatempor ut quis elit incididunt ut amet ipsum enim exercitation aliqua adipquis laboris aliqua et nostrud magna et labore nostrud magna consectetur tma\net consectetur ipsum lorem lorem enim exercitation laboris nisi nisi nisi do sit eius", + "comment_time": null, + "commenter": null, + "date_reported": null, + "description": "et nostrud sed dolor eiusmod sit ips", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "DIA5", + "status": null, + "work_order_id": "11742436804" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-18.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-18.json new file mode 100644 index 0000000..d6e0d51 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-18.json @@ -0,0 +1,18 @@ +{ + "address": "nisi enim exercitation aliqua adipiscing minim dolore consectetur tempor", + "assigned_to": null, + "building": "SSC4", + "comment_text": "quis laboris aliqua adipiscing tempor sit a", + "comment_time": "2026-05-08T17:47:44", + "commenter": "eiusmod", + "date_reported": null, + "description": "exercitation sed", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "SSC4", + "status": null, + "work_order_id": "10239975920" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-19.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-19.json new file mode 100644 index 0000000..b2d9a90 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-19.json @@ -0,0 +1,18 @@ +{ + "address": "quis laboris do veniam ullamco aliqua adipiscing mini\nullamco aliqua adi\ndolor ad adipis", + "assigned_to": null, + "building": "WMO7", + "comment_text": "enim adipiscing dolor ad adipiscing tempor sit ipsum enim adipiscing minim dolor", + "comment_time": "2026-04-17T15:58:28", + "commenter": "magna ex", + "date_reported": null, + "description": "adipiscing tempor sit ipsum", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WMO7", + "status": null, + "work_order_id": "15871272648" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-20.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-20.json new file mode 100644 index 0000000..4570884 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-20.json @@ -0,0 +1,18 @@ +{ + "address": "elit minim ullamco nisi enim exercitation aliqua exercitati", + "assigned_to": null, + "building": "UPA0", + "comment_text": "nostrud magna et consectetur tempor sit ad ullamco aliqua et nostrud laboris do sit e", + "comment_time": null, + "commenter": null, + "date_reported": null, + "description": "labore amet ipsum lore", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "UPA0", + "status": null, + "work_order_id": "14192386581" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-21.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-21.json new file mode 100644 index 0000000..e3a584b --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-21.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WLN0", + "comment_text": "dolor eiusmod ut quis magna et nostrud laboris do sit eiusmod ut q", + "comment_time": "2026-07-15T22:55:25", + "commenter": "consecte", + "date_reported": null, + "description": "tempor quis laboris do veniam magna et l", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WLN0", + "status": null, + "work_order_id": "13332034778" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-22.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-22.json new file mode 100644 index 0000000..db32350 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-22.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WMT3", + "comment_text": "dolore consectetur minim\nenim exerc\nipsum enim exercitation\nquis elit dolor lorem enim dolore et nostrud sed incididunt ut amet tempor quis magna et consectetur minim ullamco sed dolor eiusmod sit ad adipiscing\neiusmod ut labore amet ad ullamco nisi enim adipiscing\naliqua et labore amet ad ullamco sed incididunt ut quis elit dolor ad ullamc\nnostrud sed incididunt ut labore labore nostrud laboris aliqua et consectetur minim dolore consectetur minim dolore nostr", + "comment_time": null, + "commenter": null, + "date_reported": null, + "description": "exercitation aliqua et nost", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WMT3", + "status": null, + "work_order_id": "14207299202" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-23.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-23.json new file mode 100644 index 0000000..ab7aaf6 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-23.json @@ -0,0 +1,18 @@ +{ + "address": "exercitation sed dolor lorem lorem do sit eiusmod sit\nipsum lorem lorem lor\nconsectetur", + "assigned_to": null, + "building": "WEE1", + "comment_text": "ad ullamco nisi nisi enim exercit", + "comment_time": "2026-06-29T03:44:30", + "commenter": "eiusmod", + "date_reported": null, + "description": "do incididunt amet ad dolore consectetur m", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WEE1", + "status": null, + "work_order_id": "11649444950" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-24.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-24.json new file mode 100644 index 0000000..7992fc8 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-24.json @@ -0,0 +1,18 @@ +{ + "address": "elit dolor lo\nlabore labore lab\ndolore nostrud ma", + "assigned_to": null, + "building": "USD6", + "comment_text": "et labore amet ad ullamco aliqua exercitation labori", + "comment_time": "2026-05-11T02:23:27", + "commenter": "lorem e", + "date_reported": null, + "description": "sit eiusmod ut quis elit dolor ei", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "USD6", + "status": null, + "work_order_id": "12507150701" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-25.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-25.json new file mode 100644 index 0000000..099077b --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-25.json @@ -0,0 +1,18 @@ +{ + "address": "nisi nisi enim dolore et consectetur minim ullamco ni", + "assigned_to": null, + "building": "USF3", + "comment_text": "magn", + "comment_time": "2026-06-15T22:35:33", + "commenter": "veniam u", + "date_reported": null, + "description": "magna et nostrud magna exercitation aliqua exercitation aliqua exercitat", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "USF3", + "status": null, + "work_order_id": "15849582862" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-26.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-26.json new file mode 100644 index 0000000..b47d6d6 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-26.json @@ -0,0 +1,18 @@ +{ + "address": "adipiscing minim ullamco aliqua adipiscing minim dolore consectetur", + "assigned_to": null, + "building": "HNY6", + "comment_text": "minim", + "comment_time": "2026-05-19T20:08:52", + "commenter": "amet ad", + "date_reported": null, + "description": "ullamco sed incididunt amet ad ullamco aliq", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "HNY6", + "status": null, + "work_order_id": "13456406719" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-27.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-27.json new file mode 100644 index 0000000..7081147 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-27.json @@ -0,0 +1,18 @@ +{ + "address": "labore amet ad dolore et nostrud sed incididunt amet\nadipiscing min\nut labore nostru", + "assigned_to": null, + "building": "WTX7", + "comment_text": "aliqua et consectetur minim elit dolor ad ullamco nisi nisi nisi enim dolore", + "comment_time": "2026-04-29T21:09:55", + "commenter": "exercita", + "date_reported": null, + "description": "do veniam ullamco sed veniam", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WTX7", + "status": null, + "work_order_id": "14072812497" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-28.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-28.json new file mode 100644 index 0000000..8967f28 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-28.json @@ -0,0 +1,18 @@ +{ + "address": "veniam elit dolor lorem enim adipiscing dolor lorem lorem enim ex", + "assigned_to": null, + "building": "HBD0", + "comment_text": "minim elit minim dolore nostrud laboris aliqua exercitation laboris aliqua et consectetur te", + "comment_time": "2026-07-01T19:52:41", + "commenter": "elit mi", + "date_reported": null, + "description": "eiusmod ut quis elit incididunt amet ipsu", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "HBD0", + "status": null, + "work_order_id": "14101235072" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-29.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-29.json new file mode 100644 index 0000000..5ddef95 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-29.json @@ -0,0 +1,18 @@ +{ + "address": "sit eiusmod veniam magn\nquis elit minim\nut labore nostrud sed dol", + "assigned_to": null, + "building": "HDC3", + "comment_text": "amet tempor quis magna et consectetur tempor ut amet tempor quis elit minim dolore nostrud m", + "comment_time": "2026-04-29T15:54:58", + "commenter": "dolor l", + "date_reported": null, + "description": "nisi enim dolore nostrud labori", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "HDC3", + "status": null, + "work_order_id": "16912944806" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-30.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-30.json new file mode 100644 index 0000000..ded43e1 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-30.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WNG3", + "comment_text": "nisi do sit ad adipiscing tempor quis laboris nisi nisi do sit ad ullamco aliqua exercitation lab", + "comment_time": "2026-04-26T17:01:06", + "commenter": "tempor", + "date_reported": null, + "description": "aliqua exercitation labori", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WNG3", + "status": null, + "work_order_id": "19581549387" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-31.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-31.json new file mode 100644 index 0000000..9718ccc --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-31.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WLN0", + "comment_text": "do incididunt amet ipsum enim dolore et nostrud sed dolor lorem do s", + "comment_time": "2026-07-07T14:07:17", + "commenter": "eiusmod", + "date_reported": null, + "description": "exercitation se", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WLN0", + "status": null, + "work_order_id": "11911449322" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-32.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-32.json new file mode 100644 index 0000000..8f554f5 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-32.json @@ -0,0 +1,18 @@ +{ + "address": "incididunt quis magna c\ndo veniam magna e\nut labore amet tempor", + "assigned_to": null, + "building": "SNY9", + "comment_text": "exercitation sed incididunt amet tempor ut labore labore amet ad adipiscinenim exercitation aliqua adipiscing dolor ad dolore consectetur ipsum eiusveniam elit minim ullamco nisi enim dolore nostrud magna exercitation aliqet labore amet tempor ut quis laboris nisi enim dolore nostrud sed incididmin", + "comment_time": "2026-03-16T23:19:40", + "commenter": "amet tem", + "date_reported": null, + "description": "laboris aliqua exercitation laboris do veniam uamet ad ullamco se", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "SNY9", + "status": null, + "work_order_id": "11866176189" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-33.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-33.json new file mode 100644 index 0000000..99b4ef4 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-33.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WBM6", + "comment_text": "elit incididunt ut labore a", + "comment_time": "2026-02-23T19:09:48", + "commenter": "do venia", + "date_reported": null, + "description": "sit ad ullamco nisi nisi do sit ipsum eiusmod veniam ullamco aliqua exercitation", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WBM6", + "status": null, + "work_order_id": "18954083161" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-34.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-34.json new file mode 100644 index 0000000..acda02b --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-34.json @@ -0,0 +1,18 @@ +{ + "address": "enim dolore et labore a\nexercitation sed inc\nconsectetur tempor", + "assigned_to": null, + "building": "SNJ2", + "comment_text": "dolor ad adipiscing tempor q", + "comment_time": "2026-05-26T04:31:11", + "commenter": "exercita", + "date_reported": null, + "description": "aliqua et consectetur ipsum lorem do sit eiusmod ut quis laboris aliqua adipi", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "SNJ2", + "status": null, + "work_order_id": "19667240938" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-35.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-35.json new file mode 100644 index 0000000..5ae9690 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-35.json @@ -0,0 +1,18 @@ +{ + "address": "veniam ullamco nisi do sit eiusmod ut labore labore amet ad dolore nostrud", + "assigned_to": null, + "building": "DNJ9", + "comment_text": "ipsum eiusmod ut amet tempor u", + "comment_time": "2026-04-22T00:36:53", + "commenter": "quis mag", + "date_reported": null, + "description": "tempor ut ame", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "DNJ9", + "status": null, + "work_order_id": "12085878694" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-36.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-36.json new file mode 100644 index 0000000..230e88c --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-36.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WWG0", + "comment_text": "dolore consectetur ipsum eiusmod veniam ullamco sed incidi", + "comment_time": "2026-04-16T00:17:07", + "commenter": "et nost", + "date_reported": null, + "description": "ad dolore et labore amet ad adipis", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WWG0", + "status": null, + "work_order_id": "17956754386" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-37.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-37.json new file mode 100644 index 0000000..a4805cf --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-37.json @@ -0,0 +1,18 @@ +{ + "address": "magna exercitation sed incididunt quis magna et\nnisi enim exercit\nadipiscing minim d", + "assigned_to": null, + "building": "XCV7", + "comment_text": "nisi enim exercitation sed dolor lorem do sit eiusmod sit ad ullamco", + "comment_time": "2026-05-16T16:58:02", + "commenter": "minim", + "date_reported": null, + "description": "veniam elit incididunt amet ipsum lo", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "XCV7", + "status": null, + "work_order_id": "15442394407" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-38.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-38.json new file mode 100644 index 0000000..8de96db --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-38.json @@ -0,0 +1,18 @@ +{ + "address": "amet ipsum enim exercitation laboris nisi nisi do sit eiusmo\nlaboris aliqua exe\nipsum enim adip", + "assigned_to": null, + "building": "HMO1", + "comment_text": "tempo", + "comment_time": "2025-09-25T09:13:39", + "commenter": "do venia", + "date_reported": null, + "description": "ullamco aliqua adipiscing dolor ad adipiscing tempor", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "HMO1", + "status": null, + "work_order_id": "11626686388" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-39.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-39.json new file mode 100644 index 0000000..af73e09 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-39.json @@ -0,0 +1,18 @@ +{ + "address": "ad ullamco sed dolor eiusmod\nconsectetur tempor sit eiusmo\nlabore amet", + "assigned_to": null, + "building": "LHO8", + "comment_text": "lorem lorem do veniam elit minim elit incididunt ut quis elit incididunt ut labore amet tempor", + "comment_time": "2026-06-13T22:15:42", + "commenter": "lorem en", + "date_reported": null, + "description": "et consectetur minim elit incididunt amet ad dolore nostrud magna consectetur mi", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "LHO8", + "status": null, + "work_order_id": "19269813139" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-40.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-40.json new file mode 100644 index 0000000..f25e32e --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-40.json @@ -0,0 +1,18 @@ +{ + "address": "quis laboris nisi do sit eiusmod veniam elit incididu\namet ipsum eiusmo\nincididunt quis elit mi", + "assigned_to": null, + "building": "WPR2", + "comment_text": "exercitation aliqua adipiscing tempor", + "comment_time": "2026-04-22T23:48:00", + "commenter": "nisi do", + "date_reported": null, + "description": "ad a", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WPR2", + "status": null, + "work_order_id": "17107056766" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-41.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-41.json new file mode 100644 index 0000000..cc2fe5b --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-41.json @@ -0,0 +1,18 @@ +{ + "address": "et consectetur minim dolore et nostrud sed incididunt\nconsectetur tempor\nlaboris aliqua adipisc", + "assigned_to": null, + "building": "WTX0", + "comment_text": "nisi do incididunt amet ipsum lorem enim d", + "comment_time": "2026-04-30T16:22:51", + "commenter": "sit eius", + "date_reported": null, + "description": "eiusmod veniam elit dolor eiusmod sit ad ad", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WTX0", + "status": null, + "work_order_id": "15379807062" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-42.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-42.json new file mode 100644 index 0000000..465ba85 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-42.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WNM1", + "comment_text": "dolore et consectetur minim elit dolor eiusmod veniam ullamco aliqua et consectetur tempor quis magna exe", + "comment_time": "2026-07-05T21:24:36", + "commenter": "dolor", + "date_reported": null, + "description": "elit dolor lorem do sit ipsum enim dolore nos", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WNM1", + "status": null, + "work_order_id": "14501248005" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-43.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-43.json new file mode 100644 index 0000000..37a04c9 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-43.json @@ -0,0 +1,18 @@ +{ + "address": "lorem do sit ipsum lorem do incididunt quis laboris d\nenim adipiscing minim\nsit eiusmod ut amet", + "assigned_to": null, + "building": "WUT3", + "comment_text": "laboris nisi nisi nisi nisi enim dolore nostrud laboris d", + "comment_time": "2026-07-08T20:01:49", + "commenter": "magna e", + "date_reported": null, + "description": "tempor ut quis magna consec", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WUT3", + "status": null, + "work_order_id": "15311727998" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-44.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-44.json new file mode 100644 index 0000000..83d421b --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-44.json @@ -0,0 +1,18 @@ +{ + "address": "sit ad adipiscing tempor quis laboris nisi do incididunt amet ipsum\nmagna et labore amet temp\nquis elit inci", + "assigned_to": null, + "building": "WSP0", + "comment_text": "sed incididunt quis elit dolor lorem do veniam magna et nostrud magna consectetur tempor quis elit incididunt amet tempor ut quis lab", + "comment_time": "2026-05-31T12:05:32", + "commenter": "exercit", + "date_reported": null, + "description": "lorem do veniam elit minim elit minim elit minim elit incididunt ut qui", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WSP0", + "status": null, + "work_order_id": "12558588577" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-45.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-45.json new file mode 100644 index 0000000..8758cac --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-45.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "HRC7", + "comment_text": "adipiscing dolor ad ullamco sed dolor ad dolore consectetur tempor sit ipsum enim adipiscing dolor ad ullamco sed veniam elit dolor eiusmod sit", + "comment_time": "2026-06-24T05:24:48", + "commenter": "ut amet", + "date_reported": null, + "description": "ut quis elit dolor eiusmod veniam ullamco nisi nisi nisi nisi nisi d", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "HRC7", + "status": null, + "work_order_id": "10936140019" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-46.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-46.json new file mode 100644 index 0000000..5068848 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-46.json @@ -0,0 +1,18 @@ +{ + "address": "exercitation laboris aliqua et consectetur ipsum eiusmod veniam eli\nad adipiscing\nconsectetur minim u", + "assigned_to": null, + "building": "WID3", + "comment_text": "consectetur tempor sit ad adipiscing dolor lorem do veniam magna cons", + "comment_time": "2026-06-01T18:51:31", + "commenter": "magna e", + "date_reported": null, + "description": "tempor ut amet tempor sit eiusmod ut labore amet", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WID3", + "status": null, + "work_order_id": "18545122745" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-47.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-47.json new file mode 100644 index 0000000..a7bc1d3 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-47.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WNG3", + "comment_text": "incididunt", + "comment_time": "2026-05-07T08:18:09", + "commenter": "ad adi", + "date_reported": null, + "description": "ad adipiscing tempor quis laboris nisi do", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WNG3", + "status": null, + "work_order_id": "17907063707" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-48.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-48.json new file mode 100644 index 0000000..71640ec --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-48.json @@ -0,0 +1,18 @@ +{ + "address": null, + "assigned_to": null, + "building": "WMT9", + "comment_text": "ullamco sed veniam ullamco aliqua et consectetur ipsum lorem enim adipiscing te", + "comment_time": "2026-05-28T15:45:57", + "commenter": "eiusmod", + "date_reported": null, + "description": "eiusmod sit ipsu", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WMT9", + "status": null, + "work_order_id": "12428713709" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-49.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-49.json new file mode 100644 index 0000000..607c120 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-49.json @@ -0,0 +1,18 @@ +{ + "address": "do veniam magna et consectetur ipsum enim adipiscing dolor ad adipiscing mlorem enim exercitatio", + "assigned_to": null, + "building": "HGR0", + "comment_text": "labore amet ipsum eiusmod ut quis magna exercitation sed incididunt amet aincididunt ut quis elit minim elit incididunt amet tempor quis laboris do aliqua et nostrud sed incididunt amet ipsum eiusmod veniam elit dolor ad aconsectetur minim elit incididunt amet tempor ut quis laboris nisi do venialiqua et labore amet ipsum enim exercitation aliqua et labore amet tempordolore et labore amet ad dolore consectetur tempor quis magna et labore amminim elit dolor lorem", + "comment_time": "2026-06-30T14:30:29", + "commenter": "sit ad", + "date_reported": null, + "description": "et nostrud laboris do sit eiusmod veniam ullamcelit minim dolore et nostrud labor", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "HGR0", + "status": null, + "work_order_id": "12123384706" +} diff --git a/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-50.json b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-50.json new file mode 100644 index 0000000..aa50295 --- /dev/null +++ b/lambdas/wo/email_processor/tests/fixtures/expected/update-plaintext-50.json @@ -0,0 +1,18 @@ +{ + "address": "laboris do veniam ullamco aliqua adipiscing dolor lor\nut amet ipsum eiusmod\nlorem do veniam magn", + "assigned_to": null, + "building": "WUT3", + "comment_text": "nisi nisi do incididunt am\nlabore ame\nveniam elit incididunt\nad adipiscing minim dolore et nostrud laboris nisi enim dolore nostrud laboris do sit eiusmod ut labore labore amet tempor quis e\nincididunt ut labore labore amet ipsum eiusmod sit ad dolore\nexercitation aliqua et consectetur minim ullamco nisi enim adipiscing dolor ad ullamco nisi do v\nlorem enim adipiscing dolor eiusmod veniam magna consectetur tempor quis laboris do incididunt amet ipsum lorem lorem do", + "comment_time": null, + "commenter": null, + "date_reported": null, + "description": "elit incididunt ut labore labore nostrud magn", + "due_date": null, + "email_type": "comment", + "priority": null, + "scheduled_start": null, + "severity": null, + "site_code": "WUT3", + "status": null, + "work_order_id": "13995831868" +} diff --git a/lambdas/wo/email_processor/tests/test_bedrock_fallback.py b/lambdas/wo/email_processor/tests/test_bedrock_fallback.py new file mode 100644 index 0000000..4fb4f14 --- /dev/null +++ b/lambdas/wo/email_processor/tests/test_bedrock_fallback.py @@ -0,0 +1,172 @@ +"""End-to-end dispatch tests: the Bedrock AI extractor is invoked ONLY when the +deterministic parse misses, EMF metrics are emitted on both paths, and the +Bedrock response is decoded through the same contract.""" + +import json +import os + +import pytest + +import handler +from _wo_parser_support import FIXTURES + + +def _raw(subdir, stem): + with open(os.path.join(FIXTURES, subdir, f"{stem}.eml"), "rb") as fh: + return fh.read() + + +class FakeBody: + def __init__(self, data): + self._data = data + + def read(self): + return self._data + + +class FakeS3: + def __init__(self, raw): + self._raw = raw + + def get_object(self, Bucket, Key): # noqa: N803 + return {"Body": FakeBody(self._raw)} + + +class FakeBedrock: + def __init__(self, payload): + self.calls = [] + self._payload = payload + + def invoke_model(self, modelId, body): # noqa: N803 + self.calls.append({"modelId": modelId, "body": body}) + text = json.dumps(self._payload) + return {"body": FakeBody(json.dumps({"content": [{"text": text}]}).encode())} + + +AI_17_KEY = { + "email_type": "comment", + "work_order_id": "77777777777", + "description": None, + "status": None, + "site_code": None, + "building": None, + "address": None, + "severity": None, + "priority": None, + "date_reported": None, + "scheduled_start": None, + "due_date": None, + "assigned_to": None, + "commenter": None, + "comment_text": "ai extracted", + "comment_time": None, +} + + +def _event(): + return { + "Records": [ + { + "s3": { + "bucket": {"name": "workorder-ingest-emails-x"}, + "object": {"key": "inbound/o1"}, + } + } + ] + } + + +@pytest.fixture +def metric_spy(monkeypatch): + calls = [] + monkeypatch.setattr( + handler, + "emit_parse_metric", + lambda *a: calls.append(a), + ) + return calls + + +def test_template_path_skips_bedrock(fake_dynamo, metric_spy, monkeypatch): + fake_bedrock = FakeBedrock(AI_17_KEY) + monkeypatch.setattr( + handler, "s3", FakeS3(_raw("update-plaintext", "update-plaintext-01")) + ) + monkeypatch.setattr(handler, "bedrock", fake_bedrock) + + handler.handler(_event(), None) + + assert fake_bedrock.calls == [] # deterministic parse handled it + method, template_id, reason, wo = metric_spy[0] + assert method == "template" + assert template_id == "update_plaintext" + assert reason == "ok" + assert wo == "11144580730" + + +def test_fallback_path_invokes_bedrock(fake_dynamo, metric_spy, monkeypatch): + fake_bedrock = FakeBedrock(AI_17_KEY) + monkeypatch.setattr(handler, "s3", FakeS3(_raw("ai-fallback", "unknown-subject"))) + monkeypatch.setattr(handler, "bedrock", fake_bedrock) + + handler.handler(_event(), None) + + assert len(fake_bedrock.calls) == 1 + # Uses the configured inference profile and the bedrock message contract. + call = fake_bedrock.calls[0] + assert call["modelId"] == handler.BEDROCK_MODEL_ID + body = json.loads(call["body"]) + assert body["anthropic_version"] == "bedrock-2023-05-31" + assert body["max_tokens"] == 1024 + method, _template_id, _reason, wo = metric_spy[0] + assert method == "ai_fallback" + assert wo == "77777777777" + # The AI result was written through to DynamoDB. + wo_table = fake_dynamo.tables[handler.WORK_ORDERS_TABLE] + assert wo_table.updates, "expected a work-order upsert from the AI path" + + +def test_extract_with_bedrock_returns_full_contract(monkeypatch): + fake_bedrock = FakeBedrock(AI_17_KEY) + monkeypatch.setattr(handler, "bedrock", fake_bedrock) + email_data = { + "subject": "x", + "sender": "a", + "to": "b", + "cc": "", + "date": "d", + "body": "body", + } + out = handler.extract_with_bedrock(email_data) + assert set(out.keys()) == set(AI_17_KEY.keys()) + assert len(fake_bedrock.calls) == 1 + + +def test_extract_with_bedrock_strips_markdown_fence(monkeypatch): + class FenceBedrock: + def invoke_model(self, modelId, body): # noqa: N803 + fenced = "```json\n" + json.dumps(AI_17_KEY) + "\n```" + return { + "body": FakeBody(json.dumps({"content": [{"text": fenced}]}).encode()) + } + + monkeypatch.setattr(handler, "bedrock", FenceBedrock()) + out = handler.extract_with_bedrock( + {"subject": "", "sender": "", "to": "", "cc": "", "date": "", "body": ""} + ) + assert out["work_order_id"] == "77777777777" + + +def test_emit_parse_metric_writes_emf(capsys): + handler.emit_parse_metric("template", "update_plaintext", "ok", "123") + line = capsys.readouterr().out.strip() + emf = json.loads(line) + assert emf["ParseMethod"] == "template" + assert emf["TemplateId"] == "update_plaintext" + assert emf["ReasonCode"] == "ok" + assert emf["ParseOutcome"] == 1 + dims = emf["_aws"]["CloudWatchMetrics"][0]["Dimensions"][0] + assert dims == ["ParseMethod", "TemplateId"] + assert ( + emf["_aws"]["CloudWatchMetrics"][0]["Namespace"] == "Seahaven/WorkorderIngest" + ) diff --git a/lambdas/wo/email_processor/tests/test_comment_id.py b/lambdas/wo/email_processor/tests/test_comment_id.py new file mode 100644 index 0000000..8b157ab --- /dev/null +++ b/lambdas/wo/email_processor/tests/test_comment_id.py @@ -0,0 +1,88 @@ +"""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) diff --git a/lambdas/wo/email_processor/tests/test_parser.py b/lambdas/wo/email_processor/tests/test_parser.py new file mode 100644 index 0000000..9b7c720 --- /dev/null +++ b/lambdas/wo/email_processor/tests/test_parser.py @@ -0,0 +1,110 @@ +"""Golden-file tests for the deterministic template parser. + +Positives: every scrubbed real sample must parse via the deterministic path and +match its checked-in golden exactly. Negatives: every adversarial / drift sample +must fail closed to the AI fallback (parsed is None). +""" + +import pytest + +from _wo_parser_support import ( + ADVERSARIAL_STEMS, + AI_FALLBACK_STEMS, + ASSIGN_STEMS, + UPDATE_STEMS, + load_email, + load_golden, +) +from template_parser import try_deterministic_parse + + +@pytest.mark.parametrize("stem", UPDATE_STEMS) +def test_update_plaintext_matches_golden(stem): + email_data = load_email("update-plaintext", stem) + parsed, method, template_id, reason = try_deterministic_parse(email_data) + assert method == "template" + assert template_id == "update_plaintext" + assert reason == "ok" + assert parsed == load_golden(stem) + # Contract invariants for the comment shape. + assert set(parsed.keys()) == set(load_golden(stem).keys()) + assert parsed["email_type"] == "comment" + assert parsed["status"] is None # never clobber a real wo_status on upsert + + +@pytest.mark.parametrize("stem", ASSIGN_STEMS) +def test_assign_html_matches_golden(stem): + email_data = load_email("assign-html", stem) + parsed, method, template_id, reason = try_deterministic_parse(email_data) + assert method == "template" + assert template_id == "assign_html" + assert reason == "ok" + assert parsed == load_golden(stem) + assert parsed["email_type"] == "new_work_order" + assert parsed["status"] == "assigned" + + +@pytest.mark.parametrize("stem", AI_FALLBACK_STEMS) +def test_ai_fallback_samples_return_none(stem): + email_data = load_email("ai-fallback", stem) + parsed, method, template_id, reason = try_deterministic_parse(email_data) + assert parsed is None, f"{stem} should have failed closed, got {parsed}" + assert method == "ai_fallback" + assert reason != "ok" + + +def test_positive_result_matches_ai_contract_keys(): + # The deterministic result must carry EXACTLY the keys the AI fallback + # produces (the EXTRACTION_PROMPT JSON contract) -- no more, no less. + from template_parser import CONTRACT_KEYS + + parsed, *_ = try_deterministic_parse( + load_email("update-plaintext", UPDATE_STEMS[0]) + ) + assert set(parsed.keys()) == set(CONTRACT_KEYS) + + +def test_extractor_exception_fails_closed(monkeypatch): + # Any exception inside the extractor must yield None, never a partial parse. + import template_parser + + monkeypatch.setattr( + template_parser, + "extract_update_plaintext", + lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError("boom")), + ) + parsed, method, _tid, reason = template_parser.try_deterministic_parse( + load_email("update-plaintext", UPDATE_STEMS[0]) + ) + assert parsed is None + assert method == "ai_fallback" + assert reason == "extractor_raised" + + +# --- Adversarial: prompt injection / near-miss subjects --- + + +def test_prompt_injection_in_comment_never_corrupts_other_fields(): + """Injection text in the comment body must either fall back OR parse safely + with the real work_order_id / site_code intact and the payload confined to + comment_text (it must never steer the structured fields).""" + email_data = load_email("adversarial", "prompt-injection-comment") + parsed, method, _tid, _reason = try_deterministic_parse(email_data) + if parsed is None: + assert method == "ai_fallback" + return + # Parsed safely: the subject-derived id wins, injected id/site are ignored. + assert parsed["work_order_id"] == "11144580730" + assert parsed["site_code"] == "WCO0" + assert parsed["email_type"] == "comment" + assert "00000000000" not in (parsed["work_order_id"] or "") + assert parsed["site_code"] != "HACK99" + # The injection payload is confined to the free-text comment field. + assert "Ignore all previous instructions" in parsed["comment_text"] + + +@pytest.mark.parametrize("stem", ADVERSARIAL_STEMS) +def test_adversarial_samples_do_not_raise(stem): + # The parser must be total: adversarial input returns a tuple, never raises. + result = try_deterministic_parse(load_email("adversarial", stem)) + assert isinstance(result, tuple) and len(result) == 4 diff --git a/lambdas/wo/email_processor/tests/test_validation_gate.py b/lambdas/wo/email_processor/tests/test_validation_gate.py new file mode 100644 index 0000000..0625315 --- /dev/null +++ b/lambdas/wo/email_processor/tests/test_validation_gate.py @@ -0,0 +1,143 @@ +"""Fail-closed validation-gate tests. + +Each known drift / adversarial shape must be rejected with the expected reason +code, and direct unit tests exercise each individual gate rule. +""" + +import pytest + +from _wo_parser_support import load_email +from template_parser import ( + CONTRACT_KEYS, + extract_update_plaintext, + try_deterministic_parse, + validate, +) + +# Fixture stem -> expected fail-closed reason code. +EXPECTED_REASONS = { + "unknown-subject": "subject_no_match", + "missing-id": "subject_no_match", + "single-space-work-order": "single_space_work_order", + "nondigit-id": "missing_required_field", + "empty-new-comment": "missing_required_field", + "malformed-site-code": "malformed_site_code", + "label-bleed-comment": "label_bleed", + "unparseable-creation-time": "creation_time_unparseable", + "cancellation-t1": "missing_required_field", + "update-status-t1": "missing_required_field", + "missing-building-and-comment": "missing_required_field", + "t2-wo-id-mismatch": "wo_id_mismatch", + "t2-missing-address": "missing_required_field", + "t2-unparseable-date": "creation_time_unparseable", +} + + +@pytest.mark.parametrize("stem,reason", sorted(EXPECTED_REASONS.items())) +def test_gate_rejects_with_reason(stem, reason): + parsed, method, _tid, got_reason = try_deterministic_parse( + load_email("ai-fallback", stem) + ) + assert parsed is None + assert method == "ai_fallback" + assert got_reason == reason + + +# --- Direct unit tests on validate() --- + + +def _good_t1_email(): + return load_email("update-plaintext", "update-plaintext-01") + + +def _good_candidate(): + return extract_update_plaintext(_good_t1_email()) + + +def test_baseline_candidate_is_valid(): + ok, reason = validate(_good_candidate(), "update_plaintext", _good_t1_email()) + assert ok and reason == "ok" + + +def test_rule1_unknown_template(): + ok, reason = validate(_good_candidate(), "unknown", _good_t1_email()) + assert not ok and reason == "subject_no_match" + + +def test_rule2_extra_key_fails(): + cand = _good_candidate() + cand["surprise"] = "x" + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "key_set_mismatch" + + +def test_rule2_missing_key_fails(): + cand = _good_candidate() + del cand["address"] + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "key_set_mismatch" + + +def test_rule3_nondigit_wo(): + cand = _good_candidate() + cand["work_order_id"] = "12A45" + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "missing_required_field" + + +def test_rule5_wrong_email_type(): + cand = _good_candidate() + cand["email_type"] = "new_work_order" # wrong for a T1 template + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "email_type_mismatch" + + +def test_rule6_bad_site_code(): + cand = _good_candidate() + cand["site_code"] = "workshop" + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "malformed_site_code" + + +def test_rule7_bad_status(): + cand = _good_candidate() + cand["status"] = "frobnicated" + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "malformed_site_code" + + +def test_rule8_empty_comment_text(): + cand = _good_candidate() + cand["comment_text"] = " " + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "missing_required_field" + + +def test_rule9_label_bleed_in_comment(): + cand = _good_candidate() + cand["comment_text"] = "text that leaked Building: WCO0 into the value" + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "label_bleed" + + +def test_rule9_separator_bleed_in_address(): + cand = _good_candidate() + cand["address"] = "123 Main St ________________" + ok, reason = validate(cand, "update_plaintext", _good_t1_email()) + assert not ok and reason == "label_bleed" + + +def test_contract_keys_match_extraction_prompt(): + """The parser's key set must be exactly the AI EXTRACTION_PROMPT contract, so + the deterministic and AI-fallback paths write identical shapes downstream.""" + import re + + import handler + + # The prompt's JSON skeleton uses union-type pseudo-values (not strict JSON) + # and repeats some enum terms in prose bullets, so pull quoted "key": tokens + # and assert every contract key is a field the AI is asked to emit (the + # parser must never invent a key outside the AI contract). + prompt_keys = set(re.findall(r'"([a-z_]+)":', handler.EXTRACTION_PROMPT)) + assert set(CONTRACT_KEYS).issubset(prompt_keys) + assert len(CONTRACT_KEYS) == 16 # current EXTRACTION_PROMPT field count diff --git a/pytest.ini b/pytest.ini index 2c22d60..23a8f27 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,10 @@ [pytest] -; Restrict discovery to tests/. test_local.py at the repo root matches the -; default test_*.py glob but is a manual script that imports a handler (and -; therefore creates boto3 clients) at collection time. -testpaths = tests +; Discovery is restricted to the repo's test roots. test_local.py at the repo +; root matches the default test_*.py glob but is a manual script that imports a +; handler (and therefore creates boto3 clients) at collection time, so it is +; deliberately excluded. The WO email-processor keeps a self-contained, +; offline suite (deterministic template parser + Bedrock fallback + issue #23 +; comment_id) alongside its module. +testpaths = + tests + lambdas/wo/email_processor/tests diff --git a/test_local.py b/test_local.py index 77e8010..cf6332f 100644 --- a/test_local.py +++ b/test_local.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 """ -Local test script - parses sample emails through Claude without AWS. +Local test script - parses sample WO emails through the deterministic template +parser, falling back to Claude on Bedrock for anything the templates don't cover. Usage: - export ANTHROPIC_API_KEY=sk-ant-... + # AWS credentials with bedrock:InvokeModel are only needed if a sample falls + # back to the AI extractor; pure-template samples parse fully offline. python test_local.py """ @@ -13,7 +15,8 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent / "lambdas" / "wo" / "email_processor")) -from handler import parse_raw_email, extract_with_claude +from handler import parse_raw_email, extract_with_bedrock +from template_parser import try_deterministic_parse def main(): @@ -25,9 +28,9 @@ def main(): return for eml_path in eml_files: - print(f"\n{'='*80}") + print(f"\n{'=' * 80}") print(f"FILE: {eml_path.name}") - print(f"{'='*80}") + print(f"{'=' * 80}") raw = eml_path.read_bytes() email_data = parse_raw_email(raw) @@ -38,8 +41,12 @@ def main(): print(f"Body preview: {email_data['body'][:200]}...") print() - print("Sending to Claude for extraction...") - parsed = extract_with_claude(email_data) + parsed, method, template_id, reason = try_deterministic_parse(email_data) + if parsed is None: + print(f"Template miss ({template_id}/{reason}); sending to Bedrock...") + parsed = extract_with_bedrock(email_data) + method = "ai_fallback" + print(f"Parse method: {method} (template={template_id}, reason={reason})") print(json.dumps(parsed, indent=2)) diff --git a/tests/conftest.py b/tests/conftest.py index 8bb84e3..5dfc8e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,6 +29,11 @@ def load_handler(relative_path, module_name): across Lambdas. """ path = REPO_ROOT / relative_path + # Put the handler's own directory on sys.path so its sibling-module imports + # (e.g. the WO handler's ``from template_parser import ...``) resolve. + handler_dir = str(path.parent) + if handler_dir not in sys.path: + sys.path.insert(0, handler_dir) spec = importlib.util.spec_from_file_location(module_name, path) module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module