chore: initial monorepo scaffold + WDS Phase 1+2 artifacts
- Nx 22.7 monorepo (pnpm 11.1, TypeScript 5.9, Node 24) - apps/api: NestJS 11 (CJS conforme CODING-RULES.md PGD-DB-004) - apps/web: React 19 + Vite 8 (ESM) - libs/shared/api-interface: Zod contract base - Docker Compose dev: Postgres 18, Valkey 8, MinIO, Mailpit - WDS artifacts: - design-artifacts/A-Product-Brief/ (5 docs canônicos + 16 dialogs) - design-artifacts/B-Trigger-Map/ (hub + 4 personas + feature impact) - Stack canon: STACK.md v2.2 + CODING-RULES.md v2.0 + brand.md - AGENTS.md + README.md como entrada para devs/agentes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# WebhookTimeoutError and Debugging
|
||||
|
||||
## Principle
|
||||
|
||||
`WebhookTimeoutError` is thrown when `waitFor` or `waitForCount` does not find a matching webhook within the configured timeout. It carries a snapshot of received webhooks from the last polling cycle — truncated to the last 10 entries — so you can inspect what arrived vs. what was expected. The full count of all received webhooks is available in `totalReceived`.
|
||||
|
||||
## Error Properties
|
||||
|
||||
```typescript
|
||||
class WebhookTimeoutError extends Error {
|
||||
readonly name = 'WebhookTimeoutError';
|
||||
readonly templateName: string; // from webhookTemplate('...')
|
||||
readonly timeoutMs: number; // the timeout that was exceeded
|
||||
readonly totalReceived: number; // total webhooks seen in polling window
|
||||
readonly receivedWebhooks: ReceivedWebhook[]; // last ≤10 received webhooks
|
||||
readonly matcherDetails: string[]; // human-readable matcher summary
|
||||
|
||||
toJSON(): Record<string, unknown>; // serialize all fields for CI logs
|
||||
}
|
||||
```
|
||||
|
||||
`receivedWebhooks` is capped at the last 10 entries. If more than 10 webhooks arrived, `totalReceived` shows the full count but `receivedWebhooks` contains only the most recent 10.
|
||||
|
||||
## Reading the Error
|
||||
|
||||
The error message format:
|
||||
|
||||
```
|
||||
Webhook "movie.deleted" not received within 15000ms.
|
||||
3 webhook(s) were received but none matched.
|
||||
Matchers: field(event="movie.deleted"), field(data.id=42).
|
||||
```
|
||||
|
||||
Use `matcherDetails` to confirm the matchers were configured correctly. Use `receivedWebhooks` to inspect actual payloads — compare field paths and values against what the matchers expect.
|
||||
|
||||
## Validating the Error Shape in Tests
|
||||
|
||||
```typescript
|
||||
import { WebhookTimeoutError, webhookTemplate } from '@seontechnologies/playwright-utils/webhook';
|
||||
|
||||
const neverArrivingTemplate = webhookTemplate('never.arrives')
|
||||
.matchField('event', 'event.that.never.happens')
|
||||
.withTimeout(500)
|
||||
.withInterval(100)
|
||||
.build();
|
||||
|
||||
const [waitResult] = await Promise.allSettled([webhookRegistry.waitFor(neverArrivingTemplate)]);
|
||||
|
||||
expect(waitResult.status).toBe('rejected');
|
||||
if (waitResult.status !== 'rejected') {
|
||||
throw new Error('Expected webhook wait to reject with WebhookTimeoutError');
|
||||
}
|
||||
|
||||
const error = waitResult.reason as WebhookTimeoutError;
|
||||
expect(error).toBeInstanceOf(WebhookTimeoutError);
|
||||
expect(error.templateName).toBe('never.arrives');
|
||||
expect(error.timeoutMs).toBe(500);
|
||||
expect(error.toJSON()).toMatchObject({
|
||||
name: 'WebhookTimeoutError',
|
||||
templateName: 'never.arrives',
|
||||
timeoutMs: 500,
|
||||
totalReceived: expect.any(Number),
|
||||
matcherDetails: ['field(event="event.that.never.happens")'],
|
||||
});
|
||||
```
|
||||
|
||||
## Inspecting receivedWebhooks
|
||||
|
||||
When a webhook arrives but doesn't match, `receivedWebhooks` shows you what actually came in:
|
||||
|
||||
```typescript
|
||||
// Wait for create webhook first — puts it in the journal
|
||||
await webhookRegistry.waitFor(movieCreated(movieId));
|
||||
|
||||
// Wait for delete webhook that will never arrive — no delete was called
|
||||
const undeliveredDelete = webhookTemplate<{
|
||||
event: string;
|
||||
data: { id: number };
|
||||
}>('movie.deleted.not.delivered')
|
||||
.matchField('event', 'movie.deleted')
|
||||
.matchField('data.id', movieId)
|
||||
.withTimeout(2_000)
|
||||
.withInterval(200)
|
||||
.build();
|
||||
|
||||
const [waitResult] = await Promise.allSettled([webhookRegistry.waitFor(undeliveredDelete)]);
|
||||
|
||||
expect(waitResult.status).toBe('rejected');
|
||||
if (waitResult.status !== 'rejected') {
|
||||
throw new Error('Expected webhook wait to reject with WebhookTimeoutError');
|
||||
}
|
||||
|
||||
const error = waitResult.reason as WebhookTimeoutError;
|
||||
expect(error).toBeInstanceOf(WebhookTimeoutError);
|
||||
expect(error.totalReceived).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// The movie.created webhook that did arrive is visible in the error
|
||||
const createdWebhook = error.receivedWebhooks.find((w) => (w.body as { data: { id: number } }).data.id === movieId);
|
||||
expect(createdWebhook).toBeDefined();
|
||||
expect((createdWebhook!.body as { event: string }).event).toBe('movie.created');
|
||||
```
|
||||
|
||||
## Common Failure Patterns
|
||||
|
||||
| What you see | Likely cause | Fix |
|
||||
| -------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `totalReceived: 0` | Webhook not delivered; wrong URL or event not firing | Check application event publishing and webhook routing |
|
||||
| `totalReceived > 0`, none match | Webhooks arriving but matchers not matching | Inspect `receivedWebhooks[0].body` — check field paths and values |
|
||||
| `matcherDetails` shows wrong path | Template factory misconfigured | Print `error.toJSON()` and compare paths against actual payload |
|
||||
| `totalReceived: 0` with `matched-only` | Another worker claimed and deleted the webhook first | Ensure template is scoped by entity ID |
|
||||
| Parse error in body | Webhook body is not valid JSON | Check `receivedWebhooks[n].parseError` and `rawBody` |
|
||||
|
||||
## matcherDetails Format per Matcher Type
|
||||
|
||||
| Matcher | matcherDetails string |
|
||||
| ------------------------------- | --------------------- |
|
||||
| `matchField('event', 'x')` | `field(event="x")` |
|
||||
| `matchPartial({ a: 1 })` | `partial({"a":1})` |
|
||||
| `matchPredicate('my desc', fn)` | `predicate(my desc)` |
|
||||
|
||||
## Import
|
||||
|
||||
```typescript
|
||||
import { WebhookTimeoutError } from '@seontechnologies/playwright-utils/webhook';
|
||||
```
|
||||
|
||||
## Related Fragments
|
||||
|
||||
- `webhook-template-matchers.md` — matcherDetails string format per matcher type
|
||||
- `webhook-waiting-querying.md` — waitFor and waitForCount throw this error on timeout
|
||||
Reference in New Issue
Block a user