Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(crm): stop silently dropping unsupported eventFilter values

merged#2886CedarCopilot

CedarCopilot wants to merge 4 commits into staging from fix/eventfilter-silent-no-op-and-range-cast

Live on prod, watching, 2 days leftTimeline and evidence
  1. Opened
    Sep 22, 2026, 4:52 PM
  2. Sep 22, 2026, 8:14 PM
  3. Merged
    Sep 22, 2026, 8:15 PM
  4. Live on prod
    Sep 22, 2026, 8:15 PM
  5. Observed 0 hours
    Sep 22, 2026, 8:15 PM
  6. Watching

    Live on prod, watching, 2 days left

    Sep 22, 2026, 8:15 PM
  7. Pipelines steady after this deploy
    Sep 22, 2026, 8:15 PM

Behaviors Libra is checking

CRM conversation lookups fail when eventFilter.count.eventType is unsupported instead of omitting the count condition and reporting an unfiltered successful result.Not checked
prod
CRM conversation lookups fail with an explicit error when eventFilter.count.operator is unsupported instead of constructing a count condition from an undefined operator value.Not checked
prod
CRM conversation lookups fail on an unparseable eventFilter.date instead of logging a warning and silently omitting the date constraint.Not checked
prod
CRM conversation lookups fail when eventFilter.types.include or eventFilter.types.exclude contains an unsupported event type or email direction instead of silently dropping that entry and returning broader results.Not checked
prod
Event filters that provide only date or only dateOperator are rejected as incomplete rather than silently dropping the partial date constraint while applying any remaining filters.Not checked
prod
The CRM event-filter condition builder rejects runtime date operators outside before, after, and on instead of silently producing no date clause.Not checked
prod

Libra has verdicts on 0 of 7 tracked behaviors on prod; 7 are still being checked. Libra checks hourly for 3 days after each deploy.

Summary

Sibling fix to #2882, which fixed conversation-lookup.ts's filters array to throw instead of silently returning null for an unsupported field/operator combination , a bug traced from a real production incident where a chat query ran unfiltered and reported success: true. That PR deliberately scoped out the eventFilter path, which had the identical bug. This PR closes it.

eventFilter fix , buildEventTypeCondition, buildEventTypeDateCondition, buildEventCountCondition in conversation-lookup.ts now throw for anything they were given but couldn't use (an unrecognized event type/direction, an invalid types.include/exclude entry, an unparseable date, a bad count operator), instead of silently dropping it from the WHERE clause. buildEventTypeDateCondition still correctly returns null for the one genuinely empty case , no types, no date, i.e. no real constraint given , so "asked for nothing" and "asked for something invalid" stay distinguishable.

Regression-proofing (the actual ask behind this PR): added const _exhaustive: never = ... compile-time exhaustiveness guards on every switch over a closed union in this file (FilterOperator ×2, eventFilter.dateOperator, eventFilter.count.operator) , verified live, not just written and assumed to work: temporarily added a 13th fake FilterOperator, confirmed tsc failed the build at both call sites with Type '"fakeTestOperator"' is not assignable to type 'never', then reverted. The new test file iterates the real exported VALID_EVENT_TYPES Set rather than a hardcoded copy, so a type added there later is automatically covered without touching the test.

Second bug, found while fixing the first , EventFilterSchema.dateOperator (findCRMConversationsTool.ts) includes 'range' because column_filters mode genuinely implements it (via dateTo, in conversations.ts's own filter builder , untouched by this PR). flexible_query mode never implemented range support at all , LookupEventFilter.dateOperator only ever declared before/after/on , so a 'range' value used to fall through an unhandled switch case silently (zero clauses, false success). The exhaustiveness fix above would have turned that into an uncaught-looking throw from deep inside conversation-lookup.ts instead of a clean tool error. Fixed by rejecting 'range' explicitly in flexible_query mode with a real, actionable error message, before any DB work , narrowed via a local const, no cast, replacing the as 'before' | 'after' | 'on' | undefined cast that previously papered over the mismatch (this repo's NEVER-cast rule).

Known, unfixed, out-of-scope follow-up , conversations.ts (column_filters mode's own filter builder) has three separate hand-copied implementations of this same event-type validation pattern (buildEventTypeCondition for the main event filter, a second copy for latestEventFilter, plus the .filter((c): c is SQL => c !== null) silent-drop this whole PR series exists to eliminate), each with its own VALID_EVENT_TYPES Set , and those Sets have already drifted: conversations.ts has task/linkedin where this file has slack/custom. Same silent-no-op bug class, unfixed, in a different file/call path. Not fixed here , same scoping reasoning #2882 used originally. Isabelle is deciding whether to file a follow-up ticket for it.

Test plan

  • pnpm --filter @zero/server run types , clean, including after the deliberate exhaustiveness-break experiment was reverted
  • pnpm deps:check , 0 violations
  • New test file conversation-lookup.event-filter-conditions.test.ts (20 tests): every throw path asserted by message, the one legitimate null case proven not over-fixed, and a lookupConversations-level integration test proving a bad eventFilter never reaches the database (canary-mocked db.select) and surfaces the real error, not a stand-in
  • 2 new tests in findCRMConversationsTool.test.ts fo
Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/server/src/mastra/tools/conversation/__tests__/findCRMConversationsTool.test.tsno production surface mapped
  • apps/server/src/mastra/tools/conversation/findCRMConversationsTool.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/conversation-lookup.event-filter-conditions.test.tsno production surface mapped
  • apps/server/src/services/crm/conversation-lookup.tsno production surface mapped