342 lines
12 KiB
Markdown
342 lines
12 KiB
Markdown
# Gmail auto-reply — manual test scenarios
|
||
|
||
Use this checklist to send **real emails** into your connected Gmail inbox (from an address that matches your test customer / `CachedOrder` email when you expect order rows). Trigger processing via **webhook** or **poll** as you normally do.
|
||
|
||
**Prerequisites**
|
||
|
||
- Dashboard store is **active**; Gmail integration connected for the user processing mail.
|
||
- Sender is **`@gmail.com`** (non-Gmail senders are skipped by the processor).
|
||
- Saved **email templates** exist for the intent/mood pairs you expect (or you will see **fallback HTML**).
|
||
- **`CachedOrder`** has rows for your test sender email when you expect real order lines in `{{order_summary}}`.
|
||
- **Groq** is available for classification unless you are explicitly testing fallback (`source: fallback` in logs). Intent labels can vary slightly per model run; the **sensitive-topic heuristic** covers many “where’s my package?” cases even if intent is `general`.
|
||
|
||
**How to verify**
|
||
|
||
- Read the **auto-reply body**: check whether it **names a specific order, date, and total** from cache vs. **fixed policy text** asking for an order number or explaining a boundary.
|
||
- Optional: check app logs for flags like `needsOrderNumberFirst`, `hintUnmatched`, `classifySource`, `messageTooSparse`.
|
||
|
||
Replace placeholders:
|
||
|
||
- `YOUR_STORE` — business name from the active store.
|
||
- `YOU@gmail.com` — the inbox that receives mail (merchant).
|
||
- `CUSTOMER@gmail.com` — sender that matches `CachedOrder.email` (or customer JSON email) when testing cache hits.
|
||
|
||
---
|
||
|
||
## 1. Counter-question (no order number, shipping-style message)
|
||
|
||
**Goal:** No CachedOrder rows should appear in the reply; customer is asked to send an order number.
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **From** | `CUSTOMER@gmail.com` (must match cache email if you want to prove orders exist but are hidden) |
|
||
| **Subject** | `Where is my package?` |
|
||
| **Body** | `Hi, I still haven't received anything. Can you check the status for me? Thanks.` |
|
||
|
||
**Expected**
|
||
|
||
- **`{{order_summary}}`** resolves to text like: *no order details shown yet* / *did not see an order number* / *reply with your order number (e.g. #1002)* — **not** real order lines for #1002 or any order.
|
||
- **Template:** `shipping_delay` or `general` mood depending on Groq; if no template, **need-order-id fallback HTML** (`buildNeedOrderIdFallbackHtml`).
|
||
- **Log:** `needsOrderNumberFirst: true` (from intent `shipping_delay` / `refund_request` / `account_billing` **or** `looksLikeSensitiveOrderTopic` on subject+body).
|
||
|
||
---
|
||
|
||
## 2. Counter-question (explicit refund language, no order #)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Refund` |
|
||
| **Body** | `I need a refund on my last purchase. Please process it.` |
|
||
|
||
**Expected**
|
||
|
||
- Same as §1: **no** order block from cache without an order hint; counter-question / policy line only.
|
||
- Often classified `refund_request`; heuristic also matches **refund**.
|
||
|
||
---
|
||
|
||
## 3. Counter-question (billing language, no order #)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Wrong charge` |
|
||
| **Body** | `I see a duplicate charge on my card for your store. Please fix it.` |
|
||
|
||
**Expected**
|
||
|
||
- No order summary from cache until an order number appears; counter-style line.
|
||
- Heuristic may match **wrong charge** / **billing**.
|
||
|
||
---
|
||
|
||
## 4. Order context shown (single clear order hint)
|
||
|
||
**Precondition:** `CachedOrder` includes order **#1002** (or `#1003`) for `CUSTOMER@gmail.com`.
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Order status` |
|
||
| **Body** | `Hi, can you confirm status for order #1002? Thanks.` |
|
||
|
||
**Expected**
|
||
|
||
- **`{{order_summary}}`** contains **real** lines for order **1002** (date, financial/fulfillment, total, items) from cache.
|
||
- **Log:** `orderHints` includes `1002`, `needsOrderNumberFirst: false`, `hintUnmatched: false`.
|
||
|
||
---
|
||
|
||
## 5. Order context shown (hint in subject only)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Re: #1002 shipping` |
|
||
| **Body** | `When will it ship?` |
|
||
|
||
**Expected**
|
||
|
||
- Hints extracted from subject+body → **1002**; summary prefers matching order if present in cache.
|
||
|
||
---
|
||
|
||
## 6. Hint mismatch (order # not on file for this email)
|
||
|
||
**Precondition:** Cache has orders for `CUSTOMER@gmail.com` but **not** `#9999**.
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Order 9999` |
|
||
| **Body** | `I need help with order #9999.` |
|
||
|
||
**Expected**
|
||
|
||
- **No** substitution of unrelated orders (e.g. not “here is #1002” instead).
|
||
- **`{{order_summary}}`** uses **hint-unmatched** copy: cannot match order number(s) to orders on file under this email; ask to double-check / confirm checkout email.
|
||
- **Log:** `hintUnmatched: true`.
|
||
- If no template: **hint-unmatched fallback HTML**.
|
||
|
||
---
|
||
|
||
## 7. Multiple order numbers, not all match cache
|
||
|
||
**Precondition:** Cache has **1002** but not **8888**.
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Body** | `Please check orders #1002 and #8888.` |
|
||
|
||
**Expected**
|
||
|
||
- **hintUnmatched** path (ambiguous / not all hints resolved); **no** listing of 1002 as if everything were OK unless you change code — current behavior withholds mismatched multi-hint summaries.
|
||
|
||
---
|
||
|
||
## 8. Off-topic (no store-related request)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Weather tomorrow` |
|
||
| **Body** | `What is the weather in Paris tomorrow?` |
|
||
|
||
**Expected**
|
||
|
||
- Intent **`off_topic`** (typical with Groq).
|
||
- **`{{order_summary}}`** = fixed **boundary** line (no CachedOrder data).
|
||
- **Template:** `off_topic` + mood, or **off-topic fallback HTML**.
|
||
|
||
---
|
||
|
||
## 9. Product / policy question without order number
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Sizing question` |
|
||
| **Body** | `Does the medium hoodie run true to size?` |
|
||
|
||
**Expected**
|
||
|
||
- Intent **`question`** (typical).
|
||
- **`{{order_summary}}`** = line that **omits order history** (*replying without attaching order history* / send order # if about a purchase).
|
||
- **Log:** `skipOrderCacheForQuestion` effectively via `question` + no hints.
|
||
|
||
---
|
||
|
||
## 10. General thank-you (no shipping/refund/billing wording)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Thanks` |
|
||
| **Body** | `Thank you so much for the fast delivery and great product!` |
|
||
|
||
**Expected**
|
||
|
||
- Intent **`general`**, **`happy`** possible.
|
||
- **No** sensitive heuristic → **with** cache for sender, **`{{order_summary}}`** may show **up to 3 recent orders** (normal path).
|
||
- If you need thanks to **never** show orders, that would require a separate product rule (not implemented).
|
||
|
||
---
|
||
|
||
## 11. Very short body (sparse message)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Subject** | `Hi` |
|
||
| **Body** | `Help` |
|
||
|
||
**Expected**
|
||
|
||
- `plainForAi.trim().length < 12` → **messageTooSparse**.
|
||
- Fixed **`ticket_summary`** about not enough text to look up an order; **`{{order_summary}}`** = **sparse** line (ask for a sentence or two + order # if about an order).
|
||
- **No** cache load for order block.
|
||
|
||
---
|
||
|
||
## 12. Low confidence (general / question only)
|
||
|
||
**Precondition:** Groq returns valid JSON with **`confidence` < `GMAIL_CLASSIFY_CONFIDENCE_MIN`** (default **0.5**) and intent **`general`** or **`question`**, no order hints.
|
||
|
||
**How to test**
|
||
|
||
- Hard to force manually; watch logs for `lowConfidenceSuppressOrders: true` if the model returns low confidence.
|
||
- **Expected:** **`{{order_summary}}`** = *not fully confident how to categorize* / ask for order number if about an order; **no** cache rows for that branch.
|
||
|
||
---
|
||
|
||
## 13. Quoted thread with stale order # in reply chain
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Body** | `Still waiting on my new order.\n\nOn Mon, Jan 1, 2026 at 9:00 AM wrote:\n> Order #9999 from old ticket` |
|
||
|
||
**Precondition:** You only care about **new** content; hints should prefer **strip quoted** portion.
|
||
|
||
**Expected**
|
||
|
||
- Hints extracted **after** `stripQuotedEmailTrailForHints` — **#9999** often **dropped** if the quote block is removed; if no hint remains and the visible line is shipping-like, **counter-question** behavior applies.
|
||
|
||
*(Exact hint list depends on where the regex cuts; this scenario validates quote stripping.)*
|
||
|
||
---
|
||
|
||
## 14. Groq unavailable / bad JSON (classification fallback)
|
||
|
||
**How to test**
|
||
|
||
- Temporarily break **`GROQ_API_KEY`** or block Groq; or rely on parse failure (rare).
|
||
|
||
**Expected**
|
||
|
||
- `classifySource: **fallback**` → `general` + `neutral` by default.
|
||
- If the message still matches **sensitive heuristic** (e.g. “where is my package”) **without** hints → **needsOrderNumberFirst** still true → **no** accidental order dump.
|
||
|
||
---
|
||
|
||
## 15. No CachedOrder for sender (guest / wrong email)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **From** | `other.person@gmail.com` (no rows in `CachedOrder` for this email) |
|
||
| **Subject** | `Order help` |
|
||
| **Body** | `I placed an order last week, can you find it?` |
|
||
|
||
**Expected**
|
||
|
||
- If **needsOrderNumberFirst** or counter path: policy text only.
|
||
- If cache allowed but empty: **`noOrdersFallback`** — generic *no matching orders*; if **sensitiveTopic**, extra line about **different email at checkout**.
|
||
|
||
---
|
||
|
||
## 16. Safety / abuse screen (no auto-reply)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Body** | Content that triggers **`screenInboundEmailForAbuse`** (per `gmailSafety.ts` rules). |
|
||
|
||
**Expected**
|
||
|
||
- **No** customer reply email sent.
|
||
- Email may be stored as **flagged**; marked read per implementation.
|
||
- **Log:** inbound flagged, no auto-reply.
|
||
|
||
*(Use only in a test account; do not send real harmful content from production customers.)*
|
||
|
||
---
|
||
|
||
## 17. Duplicate inbound message (idempotency)
|
||
|
||
**How to test**
|
||
|
||
- Process the same Gmail **message id** twice (e.g. replay webhook or race poll + webhook).
|
||
|
||
**Expected**
|
||
|
||
- Second run: **no second auto-reply**; `GmailInboundAutoReply` unique constraint; log *duplicate / skip*.
|
||
- Message may still be marked read.
|
||
|
||
---
|
||
|
||
## 18. Per-sender rate limit
|
||
|
||
**Precondition:** Default caps — **15** auto-replies / hour and **6** / 10 minutes per `storeId` + recipient (`EmailLog` with triggers `GMAIL_WEBHOOK` / `GMAIL_POLL`).
|
||
|
||
**How to test**
|
||
|
||
- Send many distinct threads from the same `CUSTOMER@gmail.com` quickly until cap hits.
|
||
|
||
**Expected**
|
||
|
||
- Further auto-replies **suppressed** for that sender; log **rate limited**; **no** lease consumed for skipped path (lease is acquired after rate check — verify in code: rate check is **before** lease).
|
||
|
||
---
|
||
|
||
## 19. Non-Gmail sender skipped
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **From** | `customer@company.com` |
|
||
|
||
**Expected**
|
||
|
||
- Processor returns early; **no** auto-reply from this pipeline (by design in `emailProcessor.ts`).
|
||
|
||
---
|
||
|
||
## 20. HTML escaping (security smoke test)
|
||
|
||
| Field | Example |
|
||
| ----- | ------- |
|
||
| **Body** | `Question: <script>alert(1)</script> and order #1002` |
|
||
|
||
**Expected**
|
||
|
||
- Reply is HTML email; **script should not execute** in clients; template variables for HTML templates go through **`escapeTemplateContextForHtml`** — angle brackets in **`ticket_summary`** / snippets should appear **escaped** in source, not as raw tags.
|
||
|
||
---
|
||
|
||
## Quick matrix
|
||
|
||
| # | Scenario | Order # in email? | Expect real order lines in reply? |
|
||
|---|----------|-------------------|-----------------------------------|
|
||
| 1–3 | Counter / sensitive, no hint | No | **No** |
|
||
| 4–5 | Clear hint matching cache | Yes | **Yes** (matching order(s)) |
|
||
| 6–7 | Hint not matching / partial multi-hint | Yes (bad mix) | **No** (policy / unmatched copy) |
|
||
| 8 | Off-topic | — | **No** |
|
||
| 9 | Question, no hint | No | **No** (question-specific line) |
|
||
| 10 | General thanks | No | **Yes** if cache exists (by design) |
|
||
| 11 | Sparse body | No | **No** |
|
||
| 12 | Low confidence g/q | No | **No** |
|
||
| 15 | No cache row | — | **No** (fallback copy) |
|
||
|
||
---
|
||
|
||
## Environment variables (optional tuning)
|
||
|
||
| Variable | Default | Effect |
|
||
| -------- | ------- | ------ |
|
||
| `GMAIL_CLASSIFY_CONFIDENCE_MIN` | `0.5` | Below this, suppress cache for `general` / `question` without hints. |
|
||
| `GMAIL_AUTO_REPLY_MAX_PER_SENDER_PER_HOUR` | `15` | Max automated Gmail replies per hour per store + recipient. |
|
||
| `GMAIL_AUTO_REPLY_BURST_MAX_PER_10MIN` | `6` | Burst cap per 10 minutes. Set to `0` to disable that check’s enforcement (see `gmailAutoReplyGuards.ts`). |
|
||
|
||
---
|
||
|
||
*Aligned with `src/lib/gmail/emailProcessor.ts`, `gmailContext.ts`, `gmailOrderDisclosure.ts`, `gmailGroq.ts`, and `docs/gmail-templates-catalog.md`.*
|