Skip to content

Test Plan — CK Harness v2 MCP Server (apps/ops-mcp)

Generated by /plan-eng-review on 2026-05-29 Branch: claude/vigorous-mendeleev-1a67b1 Repo: ERP-Unlocked/ordermatic Design doc: mini-boone-claude-vigorous-mendeleev-1a67b1-design-20260529-113646.md

  • apps/ops-mcp/ (NEW service) — Hono on Cloud Run, MCP HTTP transport
  • apps/webapp/src/temporal/workflows/order-confirmation-email.ts (NEW workflow class)
  • packages/db/src/schema/mcp-tokens.ts (NEW schema)
  • packages/db/src/schema/customer-email-overrides.ts (NEW schema)
  • packages/db/src/schema/email-events.ts (extend with unique constraint on (order_id, stage))
  • apps/webapp/src/pages/api/admin/mcp-tokens/ (NEW admin endpoints)
  • .github/workflows/deploy-ops-mcp.yml (NEW)
  • .github/workflows/deploy-branch-gate.yml (extend to register apps/ops-mcp/**)
  • BetterStack alert config for email_send_dlq depth (terraform / Infisical)
  • 5 new MCP tools in apps/ops-mcp/src/tools/ (inventory_snapshot, open_orders_for_sku, customer_fill_rate, recent_shorts, record_allocation_decision)
  • NEW Postgres silver schemas for allocation analytics (Fabio’s dagster pipeline output)
  • NEW dagster sync jobs for Typesense + Postgres targets
  • Sam opens Claude (Excel plugin or Desktop), connects to ops-mcp with his org-scoped token
  • Asks “what allocation for pickle red onions next week?” — Claude calls 4-5 MCP tools, renders evidence in cells (Excel) or chat (Desktop)
  • Sam decides; AI calls record_allocation_decision; decision logged with rationale
  • Customer’s PO lands in CK’s inbox → server-side Temporal fires OrderConfirmationEmailWorkflow (stage=received)
  • “We received your order” email sent within 5 minutes
  • Order pushed to WhereFour (existing flow) → Temporal fires same workflow (stage=processed)
  • “Your order has been confirmed” email sent within 5 minutes of push
  • Existing short-supply email pipeline UNTOUCHED (P5 invariant)
  • Min reads list_failed_emails from his Claude surface to triage DLQ if alerts fire
  • Issue new org-scoped MCP token via /api/admin/mcp-tokens (Clerk-session-protected)
  • List tokens for an org
  • Revoke a token (sets revoked_at); subsequent uses return 401
  1. Cross-org isolation: CK-scoped token cannot access Genfit-scoped data, ever. Single point of P6 moat collapse if broken.
  2. Idempotency: Same order_id + stage cannot send two emails. Temporal workflow-id dedupe + DB unique constraint.
  3. Email mapping: ≥50% of CK’s last-30-days orders mappable to a deliverable customer email. Below threshold triggers Sprint 1.5 remediation branch (Mac owns the CK-side data master workstream).
  4. DLQ → alert wiring: BetterStack alert fires within 5 min of DLQ depth > 5 in 1hr.
  5. Audit completeness: every MCP tool call writes one audit_logs row with eventCategory='mcp'.
  • Empty result sets in inventory_snapshot (SKU not in WhereFour catalog yet) — return [], don’t 500
  • Customer with zero orders for customer_fill_rate — return {fill_rate: null, sample_size: 0}, don’t divide-by-zero
  • Token at expiry boundaryexpires_at = now() → reject (strict less-than-now check, not less-than-or-equal)
  • Suppression toggle off mid-flight — Temporal workflow checks just before send; if toggled off, no send, audit row records suppression
  • Ambiguous customer email (WhereFour record has multiple email fields populated) — log warning + fall back to override table
  • Markdown template injection<script> tag in template → markdown-it ignores (html:false); DOMPurify strips any residual; email body contains escaped text only
  • Concurrent Sam-Claude sessions — two Excel sessions calling record_allocation_decision concurrently for the same SKU → DB sees both; design decision: last-write-wins OR optimistic concurrency check via updated_at. Defer to Sprint 2 implementation; document in tool docstring.
  • Token revocation race — token revoked mid-tool-call → call completes (it was authorized at start), next call returns 401
  • Sprint 0 spike 2 returns <50% mappable — Sprint 1 redirects per remediation branch; success-gate switches to “Mac engages CK ops on email master within 3 days”
  • Long-running Excel sessions — Claude in Excel has been open for 2 hours; token still valid (no rotation per session); ops-mcp serves requests normally

NEW: apps/ops-mcp/tests/cross-org-isolation.test.ts (3 scenarios)

Section titled “NEW: apps/ops-mcp/tests/cross-org-isolation.test.ts (3 scenarios)”
  • CK token + CK customer_id → rows
  • CK token + Genfit customer_id → zero rows, NOT 403
  • Both calls write audit_logs rows scoped to CK org

NEW: apps/ops-mcp/tests/auth-middleware.test.ts (5 scenarios)

Section titled “NEW: apps/ops-mcp/tests/auth-middleware.test.ts (5 scenarios)”
  • Valid token + matching scope → 200 + lastUsedAt updated
  • Unknown token → 401
  • Expired token (expires_at < now()) → 401
  • Revoked token (revoked_at IS NOT NULL) → 401
  • Valid token + missing scope → 403

NEW: apps/ops-mcp/tests/customer-email-resolution.test.ts (5 scenarios)

Section titled “NEW: apps/ops-mcp/tests/customer-email-resolution.test.ts (5 scenarios)”
  • Tier 1 (WhereFour external_email) returns hit → use it
  • Tier 1 empty, tier 2 (override table) hits → use override
  • Tier 1+2 empty, tier 3 catch-all → use notifications@cleveland-kitchen.com
  • Suppression toggle off → no send, audit row records reason
  • Tier 1 ambiguous (multiple email fields) → audit warning + use override-or-catch-all

NEW: apps/webapp/src/temporal/workflows/order-confirmation-email.test.ts (5 scenarios)

Section titled “NEW: apps/webapp/src/temporal/workflows/order-confirmation-email.test.ts (5 scenarios)”
  • Received-stage start on extracted_order.received → activity invoked → workflow completes
  • Duplicate workflow-id start (same order, same stage) → Temporal rejects
  • Activity transient failure → retry after 1m
  • Activity hard failure (all 3 retries fail) → DLQ row inserted + workflow complete-with-error
  • Processed-stage start on wherefour.order_pushed.succeeded → activity → complete

NEW: apps/ops-mcp/tests/dlq-alert-config.test.ts (1 scenario + manual smoke)

Section titled “NEW: apps/ops-mcp/tests/dlq-alert-config.test.ts (1 scenario + manual smoke)”
  • Verify BetterStack alert resource declared with threshold 5 / 1hr
  • Manual one-time smoke: force 6 DLQ rows, observe BetterStack page

Per-tool tests (baseline, ships with each tool — 12 tools × 3 = ~36 tests)

Section titled “Per-tool tests (baseline, ships with each tool — 12 tools × 3 = ~36 tests)”

For every MCP tool, neighbor .test.ts file asserts:

  • Happy path: valid request → expected response shape
  • Auth failure: missing token → 401
  • Scope failure: token without tool’s required scope → 403

The [email-pipeline-dead-since-april-2026] learning documents an 8-week silent failure on the existing email-worker pipeline because (1) no alerting, (2) no age-based monitoring, (3) the one live customer was processing via manual upload. This new pipeline must NOT repeat the pattern:

  • Alerting wired via BetterStack with explicit test (D14)
  • audit_logs queryable for age-based monitoring (any future “no MCP calls in 24hr” alert can be added)
  • DLQ surface via list_failed_emails makes failures visible to Min/Sam without console access