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
Affected Surfaces
Section titled “Affected Surfaces”Sprint 1
Section titled “Sprint 1”apps/ops-mcp/(NEW service) — Hono on Cloud Run, MCP HTTP transportapps/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 registerapps/ops-mcp/**)- BetterStack alert config for
email_send_dlqdepth (terraform / Infisical)
Sprint 2
Section titled “Sprint 2”- 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
Key Interactions to Verify
Section titled “Key Interactions to Verify”Sam’s flow (Sprint 2)
Section titled “Sam’s flow (Sprint 2)”- 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
Mac’s flow (Sprint 1)
Section titled “Mac’s flow (Sprint 1)”- 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
Min’s flow (Sprint 1)
Section titled “Min’s flow (Sprint 1)”- Existing short-supply email pipeline UNTOUCHED (P5 invariant)
- Min reads
list_failed_emailsfrom his Claude surface to triage DLQ if alerts fire
Admin flow (Sprint 1)
Section titled “Admin flow (Sprint 1)”- 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
Critical Paths (must work for ship)
Section titled “Critical Paths (must work for ship)”- Cross-org isolation: CK-scoped token cannot access Genfit-scoped data, ever. Single point of P6 moat collapse if broken.
- Idempotency: Same
order_id+ stage cannot send two emails. Temporal workflow-id dedupe + DB unique constraint. - 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).
- DLQ → alert wiring: BetterStack alert fires within 5 min of DLQ depth > 5 in 1hr.
- Audit completeness: every MCP tool call writes one
audit_logsrow witheventCategory='mcp'.
Edge Cases
Section titled “Edge Cases”- 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 boundary —
expires_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_decisionconcurrently for the same SKU → DB sees both; design decision: last-write-wins OR optimistic concurrency check viaupdated_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
Tests to add (by file)
Section titled “Tests to add (by file)”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_logsrows 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 +
lastUsedAtupdated - 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
Regression Notes
Section titled “Regression Notes”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_logsqueryable for age-based monitoring (any future “no MCP calls in 24hr” alert can be added)- DLQ surface via
list_failed_emailsmakes failures visible to Min/Sam without console access