create-warlock 4.2.6 → 4.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -2
  3. package/templates/warlock/.env.example +36 -0
  4. package/templates/warlock/.gitattributes +1 -0
  5. package/templates/warlock/.husky/pre-commit +4 -0
  6. package/templates/warlock/.prettierignore +4 -0
  7. package/templates/warlock/.prettierrc.json +10 -0
  8. package/templates/warlock/.vscode/settings.json +41 -0
  9. package/templates/warlock/README.md +57 -0
  10. package/templates/warlock/_.gitignore +6 -0
  11. package/templates/warlock/docs/new-module.md +551 -0
  12. package/templates/warlock/eslint.config.js +98 -0
  13. package/templates/warlock/package.json +74 -0
  14. package/templates/warlock/public/home.css +523 -0
  15. package/templates/warlock/skills/api-design/SKILL.md +461 -0
  16. package/templates/warlock/skills/code-standards/SKILL.md +595 -0
  17. package/templates/warlock/skills/data-and-persistence/SKILL.md +330 -0
  18. package/templates/warlock/skills/git-workflow/SKILL.md +282 -0
  19. package/templates/warlock/skills/module-boundaries/SKILL.md +283 -0
  20. package/templates/warlock/skills/observability-and-resilience/SKILL.md +306 -0
  21. package/templates/warlock/skills/security-baseline/SKILL.md +352 -0
  22. package/templates/warlock/skills/testing-strategy/SKILL.md +323 -0
  23. package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +28 -0
  24. package/templates/warlock/src/app/auth/controllers/login.controller.ts +22 -0
  25. package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +16 -0
  26. package/templates/warlock/src/app/auth/controllers/logout.controller.ts +16 -0
  27. package/templates/warlock/src/app/auth/controllers/me.controller.ts +13 -0
  28. package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +29 -0
  29. package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +23 -0
  30. package/templates/warlock/src/app/auth/main.ts +9 -0
  31. package/templates/warlock/src/app/auth/models/otp/index.ts +1 -0
  32. package/templates/warlock/src/app/auth/models/otp/migrations/22-12-2025_10-30-20.otp-migration.ts +30 -0
  33. package/templates/warlock/src/app/auth/models/otp/otp.model.ts +69 -0
  34. package/templates/warlock/src/app/auth/requests/guarded.request.ts +10 -0
  35. package/templates/warlock/src/app/auth/routes.ts +22 -0
  36. package/templates/warlock/src/app/auth/schema/login.schema.ts +8 -0
  37. package/templates/warlock/src/app/auth/schema/reset-password.schema.ts +9 -0
  38. package/templates/warlock/src/app/auth/services/auth.service.ts +66 -0
  39. package/templates/warlock/src/app/auth/services/forgot-password.service.ts +28 -0
  40. package/templates/warlock/src/app/auth/services/otp.service.ts +173 -0
  41. package/templates/warlock/src/app/auth/services/reset-password.service.ts +39 -0
  42. package/templates/warlock/src/app/auth/utils/auth-error-code.ts +6 -0
  43. package/templates/warlock/src/app/auth/utils/locales.ts +89 -0
  44. package/templates/warlock/src/app/auth/utils/types.ts +14 -0
  45. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +21 -0
  46. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +30 -0
  47. package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +15 -0
  48. package/templates/warlock/src/app/posts/models/post/post.model.ts +23 -0
  49. package/templates/warlock/src/app/posts/resources/post.resource.ts +14 -0
  50. package/templates/warlock/src/app/posts/routes.ts +8 -0
  51. package/templates/warlock/src/app/posts/schema/create-post.schema.ts +9 -0
  52. package/templates/warlock/src/app/posts/schema/update-post.schema.ts +9 -0
  53. package/templates/warlock/src/app/shared/components/HomePageComponent.tsx +229 -0
  54. package/templates/warlock/src/app/shared/controllers/home-page.controller.ts +18 -0
  55. package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +8 -0
  56. package/templates/warlock/src/app/shared/routes.ts +4 -0
  57. package/templates/warlock/src/app/shared/services/scheduler.service.ts +3 -0
  58. package/templates/warlock/src/app/shared/tests/infrastructure.test.ts +22 -0
  59. package/templates/warlock/src/app/shared/utils/global-columns-schema.ts +8 -0
  60. package/templates/warlock/src/app/shared/utils/locales.ts +766 -0
  61. package/templates/warlock/src/app/shared/utils/router.ts +30 -0
  62. package/templates/warlock/src/app/uploads/controllers/fetch-uploaded-file.controller.ts +33 -0
  63. package/templates/warlock/src/app/uploads/routes.ts +4 -0
  64. package/templates/warlock/src/app/users/commands/hello-world.command.ts +8 -0
  65. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +27 -0
  66. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +12 -0
  67. package/templates/warlock/src/app/users/events/inject-created-by-user.into-model.event.ts +32 -0
  68. package/templates/warlock/src/app/users/events/sync.ts +5 -0
  69. package/templates/warlock/src/app/users/main.ts +5 -0
  70. package/templates/warlock/src/app/users/models/user/index.ts +1 -0
  71. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +15 -0
  72. package/templates/warlock/src/app/users/models/user/user.model.ts +64 -0
  73. package/templates/warlock/src/app/users/repositories/users.repository.ts +23 -0
  74. package/templates/warlock/src/app/users/resources/user.resource.ts +14 -0
  75. package/templates/warlock/src/app/users/routes.ts +8 -0
  76. package/templates/warlock/src/app/users/schema/create-user.schema.ts +11 -0
  77. package/templates/warlock/src/app/users/seeds/users.seed.ts +21 -0
  78. package/templates/warlock/src/app/users/services/get-users.service.ts +5 -0
  79. package/templates/warlock/src/app/users/services/list-users.service.ts +17 -0
  80. package/templates/warlock/src/app/users/services/login-social.ts +19 -0
  81. package/templates/warlock/src/config/app.ts +12 -0
  82. package/templates/warlock/src/config/auth.ts +20 -0
  83. package/templates/warlock/src/config/cache.ts +59 -0
  84. package/templates/warlock/src/config/database.ts +65 -0
  85. package/templates/warlock/src/config/http.ts +23 -0
  86. package/templates/warlock/src/config/log.ts +22 -0
  87. package/templates/warlock/src/config/mail.ts +16 -0
  88. package/templates/warlock/src/config/repository.ts +11 -0
  89. package/templates/warlock/src/config/storage.ts +34 -0
  90. package/templates/warlock/src/config/tests.ts +5 -0
  91. package/templates/warlock/src/config/validation.ts +7 -0
  92. package/templates/warlock/storage/.gitignore +2 -0
  93. package/templates/warlock/tsconfig.json +27 -0
  94. package/templates/warlock/warlock.config.ts +15 -0
  95. package/templates/warlock/yarn.lock +2332 -0
@@ -0,0 +1,352 @@
1
+ ---
2
+ name: security-baseline
3
+ description: 'Project-level security floor for Warlock.js apps — input validation at every boundary via `@warlock.js/seal` schemas, authentication via `@warlock.js/auth` (JWT + refresh) with `userType` config, server-side authorization never trusting client-supplied `userId`/`organizationId`, secrets only via `env(...)` + typed config files (never `process.env.X` scattered), parameterized queries through Cascade ORM (never raw concatenation), rate limiting via `@fastify/rate-limit` configured in `config/http.ts`, PII / token redaction in logs (`log.configure({ redact })`), `bcryptjs` for password hashing, `@mongez/encryption` for symmetric encryption, dependency vulnerability scanning (`yarn audit`). Triggers: writing a route handler / controller that accepts external input; touching auth, login, signup, session, token, JWT, password, refresh-token; reading secrets / API keys / env vars; writing a Cascade query or repository filter; handling file uploads, webhook payloads, queue messages; setting rate limits; user asks "is this secure", "input validation", "SQL / NoSQL injection", "XSS", "CSRF", "rate limiting", "secrets management", "authentication vs authorization", "where does X env var go"; importing `@warlock.js/seal` or `@warlock.js/auth` or `bcryptjs`. Skip: pure UI / styling that handles no data; code-style questions (load `skills/code-standards/SKILL.md`); HTTP envelope shape (load `skills/api-design/SKILL.md`); cryptography internals / writing your own crypto — stop and use a vetted lib; advanced auth flows (OAuth, SAML) — load `@warlock.js/social-auth` skill.'
4
+ ---
5
+
6
+ # Security baseline
7
+
8
+ **Status:** Stable
9
+ **Applies to:** Every code path that touches external input, identity, secrets, persistence, or the public network.
10
+
11
+ The non-negotiable security floor for any project on this template. Each rule maps to a real class of incident — most production breaches trace back to one of these eight being absent or sloppy.
12
+
13
+ > **Sub-agent rule:** Before writing any code that handles external input, identity, or secrets, read this file. Code-style rules live in `skills/code-standards/SKILL.md` — this skill is about what the code must do, not how it must look.
14
+
15
+ ---
16
+
17
+ ## 1. Input trust model
18
+
19
+ TypeScript types are runtime lies. The only thing standing between a malicious payload and your service is a runtime validator. Use `@warlock.js/seal`.
20
+
21
+ ### 1.1 What counts as untrusted input
22
+
23
+ Anything that crosses the process boundary:
24
+
25
+ - Request bodies, query params, path params, headers
26
+ - File uploads (the binary AND the filename AND the MIME type)
27
+ - Webhook payloads
28
+ - Queue messages
29
+ - Env vars (validated once at boot — see § 3)
30
+ - Database rows from a *different* service (treat them as external if you don't own the writer)
31
+
32
+ ### 1.2 Where validation runs
33
+
34
+ At the boundary, exactly once. Never inside services.
35
+
36
+ The Warlock pattern: schema lives in the module's `schema/` folder, the controller types itself with the schema and attaches it via `controller.validation.schema`. See `skills/api-design/SKILL.md` § 6 for the full controller shape.
37
+
38
+ ```typescript
39
+ // ✅ schema/create-order.schema.ts
40
+ import { v, type Infer } from "@warlock.js/seal";
41
+
42
+ export const createOrderSchema = v.object({
43
+ cartId: v.string(),
44
+ paymentToken: v.string(),
45
+ shippingAddress: v.object({
46
+ street: v.string().min(1),
47
+ city: v.string().min(1),
48
+ postalCode: v.string().min(1),
49
+ country: v.string().min(2).max(2),
50
+ }),
51
+ });
52
+
53
+ export type CreateOrderSchema = Infer<typeof createOrderSchema>;
54
+
55
+ // ✅ controller — framework rejects invalid input with 422 before the handler runs
56
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
57
+ import { type CreateOrderSchema, createOrderSchema } from "../schema/create-order.schema";
58
+
59
+ export const createOrderController: GuardedRequestHandler<CreateOrderSchema> = async (
60
+ request,
61
+ response,
62
+ ) => {
63
+ const order = await placeOrderService(request.validated()); // already typed + safe
64
+ return response.success({ order });
65
+ };
66
+
67
+ createOrderController.validation = { schema: createOrderSchema };
68
+ ```
69
+
70
+ The framework returns `422 Unprocessable Entity` with structured field errors automatically. **Never** catch validation errors in the controller — the boundary handles them.
71
+
72
+ ### 1.3 File uploads
73
+
74
+ - Cap the size at the route level (`fileUploadLimit` in `config/http.ts` is the project floor; tighten per-route).
75
+ - Sniff the real MIME type from the bytes; don't trust the `Content-Type` header.
76
+ - Never use the client-supplied filename as a path component — generate a server-side identifier.
77
+ - Quarantine until processed; scan if user-visible.
78
+
79
+ ### 1.4 Webhook payloads
80
+
81
+ - **Verify the signature first**, schema-validate second. A signature failure aborts before any other work.
82
+ - Reject requests older than the provider's replay window (typically 5 minutes).
83
+ - Idempotency-key the handler (see `skills/observability-and-resilience/SKILL.md` § 4) — providers retry.
84
+
85
+ ---
86
+
87
+ ## 2. Authentication vs authorization
88
+
89
+ AuthN proves who you are. AuthZ decides what you can do. Conflating them is how incidents start.
90
+
91
+ ### 2.1 Identity comes from the session, never the request body
92
+
93
+ ```typescript
94
+ // ❌ trusting the client
95
+ const order = await ordersRepository.find({ id, userId: request.input("userId") });
96
+
97
+ // ✅ identity from the authenticated session
98
+ const user = request.user;
99
+ const order = await ordersRepository.find({ id, userId: user.id });
100
+ ```
101
+
102
+ Any field whose value would determine permission (`userId`, `organizationId`, `roleId`, `isAdmin`) is read from the authenticated session — never from the request body, query, or header.
103
+
104
+ ### 2.2 Authentication via `@warlock.js/auth`
105
+
106
+ The project's auth stack: JWT access tokens (1h) + refresh tokens (30d), configured in `config/auth.ts`. Password hashing via `bcryptjs`. The framework's `authService` (`@warlock.js/auth`) is the single entry point:
107
+
108
+ ```typescript
109
+ // ✅ thin wrapper service — the framework does the work
110
+ import { authService } from "@warlock.js/auth";
111
+ import { User } from "app/users/models/user/user.model";
112
+
113
+ export async function loginService(credentials, deviceInfo) {
114
+ return authService.login(User, credentials, deviceInfo);
115
+ }
116
+ ```
117
+
118
+ ### 2.3 Authorization happens server-side, every endpoint, every time
119
+
120
+ A `guarded(() => { ... })` wrapper around routes enforces authentication. **Authorization is separate** — being authenticated proves identity, not permission.
121
+
122
+ Pattern: at the start of every mutating service, check ownership and role:
123
+
124
+ ```typescript
125
+ // ✅
126
+ export async function updateOrderService(orderId: string, input, user) {
127
+ const order = await ordersRepository.find({ id: orderId });
128
+
129
+ if (!order) {
130
+ throw new OrderNotFoundError(orderId);
131
+ }
132
+
133
+ if (order.userId !== user.id && !user.isAdmin) {
134
+ throw new ForbiddenError("not your order");
135
+ }
136
+
137
+ // ... mutate
138
+ }
139
+ ```
140
+
141
+ Resource ownership checks live in the service, not the controller. The controller only orchestrates.
142
+
143
+ ### 2.4 Role checks
144
+
145
+ Use centralized role helpers, never inline string comparisons (`user.role === "admin"` is a typo waiting to happen). Define roles as an enum in `app/shared/utils/enums.ts`.
146
+
147
+ ---
148
+
149
+ ## 3. Secrets management
150
+
151
+ ### 3.1 `.env` is never committed
152
+
153
+ - `.env.local` and `.env` are in `.gitignore` from day one.
154
+ - `.env.example` is committed and documents every variable the app needs.
155
+ - Production secrets come from the deployment platform's secret manager (Doppler, AWS Secrets Manager, Kubernetes secrets), not files on disk.
156
+
157
+ ### 3.2 Secrets enter through validated config
158
+
159
+ The pattern is already established in `src/config/*.ts`. Every config file reads `env(...)` and exports a typed object. Never read `process.env.X` outside `config/`.
160
+
161
+ ```typescript
162
+ // ✅ config/auth.ts — the project's actual pattern
163
+ import { type AuthConfigurations } from "@warlock.js/auth";
164
+ import { env } from "@warlock.js/core";
165
+
166
+ const authConfigurations: AuthConfigurations = {
167
+ userType: { user: User },
168
+ jwt: {
169
+ secret: env("JWT_SECRET"),
170
+ expiresIn: "1h",
171
+ refresh: { /* ... */ },
172
+ },
173
+ };
174
+ ```
175
+
176
+ A missing critical env var should fail loudly at boot. Don't silently fall back to a default for security-relevant values (`JWT_SECRET` has no sensible default).
177
+
178
+ ### 3.3 Secrets never appear in logs
179
+
180
+ The logger redaction layer is the safety net, but the first line of defence is not putting them in context at all.
181
+
182
+ ```typescript
183
+ // ❌
184
+ log.info("auth", "login", "User authenticated", { user, password, token });
185
+
186
+ // ✅
187
+ log.info("auth", "login", "User authenticated", { userId: user.id });
188
+ ```
189
+
190
+ Configure `log.configure({ redact: { paths: ["password", "token", "*.secret", "*.apiKey"] } })` at startup — see `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md`.
191
+
192
+ ### 3.4 Token lifetimes
193
+
194
+ Short-lived access tokens (≤ 1h), longer-lived refresh tokens (≤ 30d) with rotation on every refresh, revocation on logout. The default in `config/auth.ts` matches this — don't extend access-token lifetime to "make it easier" for the frontend.
195
+
196
+ ---
197
+
198
+ ## 4. Injection prevention
199
+
200
+ ### 4.1 Cascade ORM — always parameterized
201
+
202
+ Never compose query strings from user input. Cascade's repository and query builder are parameterized by default; never reach for a raw query mode.
203
+
204
+ ```typescript
205
+ // ✅
206
+ const orders = await ordersRepository.list({ status: input.status });
207
+
208
+ // ❌ string-built filter with user value
209
+ const orders = await ordersRepository.raw(`status = '${input.status}'`);
210
+ ```
211
+
212
+ ### 4.2 NoSQL operator injection
213
+
214
+ If a request body is `{ status: { $ne: null } }`, a naive `find({ status: input.status })` becomes a `$ne: null` match — returns every row. The seal schema is what prevents this: declaring `status: v.string()` rejects an object value at the boundary.
215
+
216
+ This applies to **query strings too**, not just bodies — a `?status[$ne]=` smuggled through the query is the same attack. That's why list endpoints validate the query with a schema and never pass raw `request.all()` to the repository (see `skills/api-design/SKILL.md` § 3.5).
217
+
218
+ This is a real reason § 1 is non-negotiable.
219
+
220
+ ### 4.3 Command execution
221
+
222
+ - Avoid `exec` / `spawn` with user input entirely.
223
+ - If unavoidable, use the array-args form (`spawn(cmd, [arg1, arg2])`), never the string form.
224
+ - Validate the input against a strict allowlist.
225
+
226
+ ### 4.4 Path traversal
227
+
228
+ - Never concatenate user input into a file path.
229
+ - Canonicalize (`path.resolve`) and verify the result starts with the expected prefix.
230
+ - File uploads land under a server-generated path, never a client-supplied one.
231
+
232
+ ---
233
+
234
+ ## 5. Output safety
235
+
236
+ ### 5.1 HTML escaping
237
+
238
+ For any user-controlled string rendered into HTML (email templates, server-rendered pages), escape at the template engine level. React Email handles this by default; if you reach for a raw string, you've taken on the responsibility.
239
+
240
+ ### 5.2 JSON responses
241
+
242
+ The framework sets `Content-Type: application/json` for `response.success(...)` automatically. Don't override unless you genuinely mean to return a different type.
243
+
244
+ ### 5.3 Open redirects
245
+
246
+ If you implement a "redirect back to where the user came from" feature, validate the destination URL against a whitelist of internal paths. Never blindly follow a user-supplied URL.
247
+
248
+ ### 5.4 CORS
249
+
250
+ `config/http.ts` controls the CORS policy. The template default `origin: "*"` is **fine for early development** — but before production:
251
+
252
+ - Replace `*` with the explicit frontend origin(s).
253
+ - If credentials cross the boundary (cookies, auth headers), `*` is no longer allowed by the spec anyway.
254
+
255
+ ---
256
+
257
+ ## 6. Rate limiting and abuse
258
+
259
+ ### 6.1 Default limiter
260
+
261
+ `config/http.ts` ships with `rateLimit: { max: 260, duration: 60_000 }` — 260 requests per minute per IP. That's the project floor for all routes.
262
+
263
+ ### 6.2 Aggressive limiter on `/auth/*`
264
+
265
+ Login, signup, password-reset, and refresh-token endpoints get a much tighter limit (e.g. 10/min/IP, 5/hour/email). Brute-force is the cheapest attack; defending it is cheap too.
266
+
267
+ ### 6.3 Account-targeted limits
268
+
269
+ For `/auth/login` and `/auth/reset-password`, key the limiter on the **email or user-id** in the payload as well as the IP — otherwise an attacker rotating IPs has free rein on a target account.
270
+
271
+ ### 6.4 Progressive lockout
272
+
273
+ After N failed login attempts:
274
+ - 3 failures → 1-minute delay
275
+ - 5 failures → 5-minute delay + email notification
276
+ - 10 failures → temporary account lock + manual review
277
+
278
+ Never silently succeed-and-fail (returning 200 with no body to hide whether the account exists) — that's a UX disaster and doesn't actually slow attackers. Honest `401` + rate limiter is the right answer.
279
+
280
+ ---
281
+
282
+ ## 7. Dependency hygiene
283
+
284
+ ### 7.1 `yarn audit` in CI
285
+
286
+ CI runs `yarn audit --level=high` on every PR. High and critical vulnerabilities block merge. Moderate goes on the backlog with a fix-by date.
287
+
288
+ ### 7.2 Lockfile committed
289
+
290
+ `yarn.lock` is checked in. Always. CI installs with `--frozen-lockfile`.
291
+
292
+ ### 7.3 No `latest` in `package.json`
293
+
294
+ Every dependency is pinned or carets-pinned. Never `"some-dep": "latest"` — that's a supply-chain attack waiting to happen.
295
+
296
+ ### 7.4 Prefer framework / std lib
297
+
298
+ Every dependency is future risk. Before adding a 5-line utility, ask whether it's already covered by `@warlock.js/*`, `@mongez/*`, or the standard library.
299
+
300
+ ---
301
+
302
+ ## 8. Logging and PII
303
+
304
+ See `skills/code-standards/SKILL.md` § 8 for the call signature. This is the **what not to log** list:
305
+
306
+ ### 8.1 Never log
307
+
308
+ - Passwords (raw or hashed), API keys, JWT tokens, session ids, refresh tokens
309
+ - Authorization headers
310
+ - OTP codes
311
+ - Payment data — card numbers, CVVs, card-holder data of any kind
312
+ - Cookies
313
+
314
+ ### 8.2 Redact unless required
315
+
316
+ - Personal identifiers: email, phone, full name, address
317
+ - IP address (legal in some jurisdictions, regulated in others — check)
318
+ - Device identifiers
319
+
320
+ When a debugging incident genuinely needs personal data, route it through a separate, access-controlled channel — never the default log stream.
321
+
322
+ ### 8.3 Configure once
323
+
324
+ ```typescript
325
+ log.configure({
326
+ redact: {
327
+ paths: ["password", "token", "*.secret", "*.apiKey", "authorization", "cookie"],
328
+ },
329
+ channels: [/* ... */],
330
+ });
331
+ ```
332
+
333
+ See `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md` for the additive per-channel layer.
334
+
335
+ ---
336
+
337
+ ## 9. Review checklist
338
+
339
+ Before merging a change that touches input, identity, secrets, or persistence:
340
+
341
+ - [ ] Every untrusted input has a `@warlock.js/seal` schema attached at the controller
342
+ - [ ] No identity field (`userId`, `organizationId`, role) is read from the request body
343
+ - [ ] Ownership / authorization check runs in the service, not the controller
344
+ - [ ] No `process.env.X` outside the `config/` folder
345
+ - [ ] No secret value appears in any log call
346
+ - [ ] All DB queries go through Cascade — no raw string-built queries
347
+ - [ ] Rate limit appropriate for the route family (default for general, aggressive for auth)
348
+ - [ ] No `.env` or secrets file in the commit
349
+ - [ ] `yarn audit --level=high` passes
350
+ - [ ] Logger redaction config covers any new sensitive field introduced
351
+ - [ ] File uploads cap size, sniff MIME, server-generate the stored filename
352
+ - [ ] Webhooks verify signature before any other work
@@ -0,0 +1,323 @@
1
+ ---
2
+ name: testing-strategy
3
+ description: 'Test pyramid posture for Warlock.js apps — Vitest co-located (`foo.ts` → `foo.spec.ts`); **when testing is enabled in the project (a `test`/`test:*` script in package.json OR a `vitest.config.*` / `vite.config.*` file), it is mandatory that every service ships a co-located `.service.spec.ts` and every HTTP controller / request handler ships a test — a new service or endpoint without a spec is incomplete**; unit tests dominate (~70%), integration tests via `@testcontainers/mongodb` + `@testcontainers/postgresql` (~25%), e2e for critical user journeys only (~5%); coverage is a smoke detector not a target (`yarn test:coverage` via `@vitest/coverage-v8`); flaky-test zero-tolerance (quarantine in 24h, fix or delete in 7); test isolation (no shared state, no order dependency); AAA structure with blank-line phases; mock at the boundary not deep inside; `MockSDK` from `@warlock.js/ai` for AI calls; real DB via testcontainers for integration; snapshot tests only for small deterministic output; contract tests for external integrations. Triggers: deciding whether to write a unit / integration / e2e test for a change; setting up a new `.spec.ts`; debugging a flaky test; reviewing the test plan in a PR; user asks "should I unit test this", "what is our coverage policy", "this test keeps failing intermittently", "what should I mock", "test pyramid", "integration test vs unit test", "snapshot tests", "how do I test a service that hits the DB", "how do I test AI code". Skip: Vitest assertion API mechanics — that''s the testing framework''s docs; language / runtime questions; load testing / performance benchmarking; framework-package internals — load the relevant `@warlock.js/*` skill.'
4
+ ---
5
+
6
+ # Testing strategy
7
+
8
+ **Status:** Stable
9
+ **Applies to:** Every test in the project, and every code change that should but doesn't have one.
10
+
11
+ Tests are insurance, not ceremony. The right tests prevent the right bugs at the right cost. Wrong-shape tests slow the team down and rot until everyone ignores them.
12
+
13
+ > **Sub-agent rule:** Before writing tests, read this file to decide what kind of test belongs here.
14
+
15
+ ---
16
+
17
+ ## 0. When this skill applies — and the mandate
18
+
19
+ This skill governs projects that have **opted into testing**. Testing is **enabled** when **either** is true:
20
+
21
+ - `package.json` has a `test` (or `test:*`) script, **or**
22
+ - a `vitest.config.*` / `vite.config.*` file exists at the project root.
23
+
24
+ When the `test` feature is selected at scaffold time, both land automatically (Vitest, `@vitest/coverage-v8`, testcontainers, and a per-worker DB/cache test setup).
25
+
26
+ ### 0.1 If testing is enabled, tests are mandatory — not optional
27
+
28
+ - **Every service** ships a co-located unit spec: `place-order.service.ts` → `place-order.service.spec.ts`. Cover the happy path, every branch, and every error path. Mock the repository — the service's logic is the unit under test.
29
+ - **Every HTTP controller / request handler** ships a test: a unit test with mocked services for its branch / validation / authorization logic, **or** an integration test through the route when the wiring is the point. A new endpoint without a test is an incomplete endpoint.
30
+
31
+ Write the spec in the **same change** that adds the service or controller. A reviewer treats a missing spec as a blocking defect, not a follow-up.
32
+
33
+ ### 0.2 If testing is NOT enabled
34
+
35
+ No `test` script and no vitest/vite config — e.g. a scaffold created without the `test` feature. **Do not** add tests or testing dependencies unprompted, and don't invent a `vitest.config`. If the project is outgrowing prototype stage, *suggest* enabling the `test` feature — but never scaffold a suite the project didn't ask for.
36
+
37
+ ---
38
+
39
+ ## 1. The pyramid
40
+
41
+ ```
42
+ /\
43
+ / \ e2e (~5%) — slow, full system, critical journeys only
44
+ /----\
45
+ / \ integration (~25%) — real DB / queue, exercises wiring
46
+ /--------\
47
+ / \ unit (~70%) — fast, isolated, pure logic
48
+ /____________\
49
+ ```
50
+
51
+ ### 1.1 Unit — most of the suite
52
+
53
+ - Services with mocked repositories
54
+ - Utility functions (pure)
55
+ - Model getters, validators, computed fields
56
+ - Resource mappers
57
+ - Error class construction
58
+
59
+ Fast (< 50ms each), isolated (no I/O), pinned to a single function or class.
60
+
61
+ ### 1.2 Integration — real wiring
62
+
63
+ - Services + real DB (via testcontainers)
64
+ - Repository queries against a real DB
65
+ - Queue producer + consumer wired together
66
+ - Auth flow through the framework's middleware
67
+
68
+ Medium (100ms–2s each), real dependencies, fewer than unit tests.
69
+
70
+ ### 1.3 E2E — critical journeys
71
+
72
+ - "User can sign up and place an order"
73
+ - "User can log in, hit rate-limit, recover"
74
+ - Payment + webhook handshake
75
+
76
+ Slow (5–30s each), small number (10–30 per app), only for journeys whose breakage is a business-impacting incident.
77
+
78
+ ### 1.4 The 70 / 25 / 5 split
79
+
80
+ Not enforced numerically. Enforced culturally — when a reviewer sees ten new e2e tests for one feature, they push back.
81
+
82
+ ---
83
+
84
+ ## 2. What to test, what not to
85
+
86
+ ### 2.1 Test
87
+
88
+ - **Public surface of a module** — services, controllers, resources, models' public methods
89
+ - **Branches and edge cases** — every `if` / `else` worth its salt has a test
90
+ - **Error paths** — what happens when the dependency fails / the input is invalid / the user lacks permission
91
+ - **Security-sensitive logic** — auth, authorization, input validation, rate limiting
92
+ - **Money and time arithmetic** — these break silently; tests are the only safety net
93
+ - **Business rules** — anything where "the spec says X" matters more than "the code does Y"
94
+
95
+ ### 2.2 Don't test
96
+
97
+ - **The framework** — `@warlock.js/*` has its own tests. Don't re-test that `response.success()` returns 200.
98
+ - **Trivial getters / setters** — `cart.totalCents` returning `this.get<number>("total_cents", 0)` doesn't need a test
99
+ - **One-line passthroughs** — `loginService(...)` just calling `authService.login(...)` doesn't need a unit test (an integration test of the wiring is better)
100
+ - **Generated code** — migrations, resource scaffolds
101
+
102
+ ### 2.3 The rule of thumb
103
+
104
+ If a future change to this function could silently break behaviour someone cares about, write a test. If a future change would either be obviously broken or obviously fine, skip it.
105
+
106
+ ---
107
+
108
+ ## 3. Test isolation — non-negotiable
109
+
110
+ ### 3.1 No shared mutable state
111
+
112
+ Each test sets up and tears down its own world. No `let users = []` at the top of the file mutated across tests.
113
+
114
+ ### 3.2 Test order independence
115
+
116
+ `vitest --shuffle` should still pass. If it doesn't, you have shared state somewhere — fix it before adding more tests.
117
+
118
+ ### 3.3 Database tests
119
+
120
+ Use transactions that roll back, or use testcontainers with a fresh container per file. Never share rows across tests.
121
+
122
+ The project's testcontainers setup makes "fresh DB per test file" cheap — use it.
123
+
124
+ ### 3.4 Time and randomness
125
+
126
+ Stub `Date.now()` / `Math.random()` / UUID generation when the test depends on the value. `vi.useFakeTimers()` and `vi.spyOn()` are your tools.
127
+
128
+ ---
129
+
130
+ ## 4. Mocking philosophy
131
+
132
+ ### 4.1 Mock at the boundary
133
+
134
+ Mock the HTTP client, not the internal functions that use it. Mock the AI provider, not the service that calls it. The further from the boundary you mock, the more your tests describe implementation rather than behaviour.
135
+
136
+ ### 4.2 AI calls — `MockSDK`
137
+
138
+ Never hit a real provider in tests. Use `MockSDK` from `@warlock.js/ai`:
139
+
140
+ ```typescript
141
+ // ✅
142
+ import { MockSDK } from "@warlock.js/ai";
143
+
144
+ const ai = MockSDK({
145
+ responses: [{ text: "mocked response" }],
146
+ });
147
+
148
+ const result = await summarizeContentService(input, { provider: ai });
149
+ expect(result).toBe("mocked response");
150
+ ```
151
+
152
+ See `@warlock.js/ai/skills/run-ai-agent/SKILL.md` for the full mock surface.
153
+
154
+ ### 4.3 Database — real, via testcontainers
155
+
156
+ For integration tests, run a real Mongo or Postgres container via `@testcontainers/mongodb` or `@testcontainers/postgresql`. The cost (a few seconds at suite start) is worth the fidelity (catching real-world query issues, real index usage, real type coercion).
157
+
158
+ For pure-unit tests of services, mock the repository — the service's logic is the unit under test, not the DB.
159
+
160
+ ### 4.4 Time — stub, don't sleep
161
+
162
+ ```typescript
163
+ // ❌
164
+ await sleep(1000);
165
+
166
+ // ✅
167
+ vi.useFakeTimers();
168
+ vi.advanceTimersByTime(1000);
169
+ ```
170
+
171
+ A test that sleeps is a test that's slow and flaky. Stub the clock.
172
+
173
+ ---
174
+
175
+ ## 5. AAA structure
176
+
177
+ Every test has three phases, separated by blank lines:
178
+
179
+ ```typescript
180
+ it("denies cart updates after checkout", async () => {
181
+ // Arrange
182
+ const cart = await cartFactory.create({ status: CartStatus.CHECKED_OUT });
183
+ const update = { totalCents: 99999 };
184
+
185
+ // Act
186
+ const promise = updateCartService(cart.id, update, mockUser);
187
+
188
+ // Assert
189
+ await expect(promise).rejects.toThrow(CartLockedError);
190
+ });
191
+ ```
192
+
193
+ ### 5.1 One assertion target per test
194
+
195
+ Multiple `expect` calls are fine if they all verify one outcome. A test that asserts five unrelated things hides which one broke.
196
+
197
+ ### 5.2 Descriptive `it(...)` names
198
+
199
+ The name describes the expected behaviour, not the implementation. "rejects cart updates after checkout" beats "throws CartLockedError when status is CHECKED_OUT" (the implementation detail is irrelevant to the reader scanning the suite).
200
+
201
+ ---
202
+
203
+ ## 6. Coverage policy
204
+
205
+ ### 6.1 Coverage is a smoke detector
206
+
207
+ Low coverage = guaranteed gaps. High coverage = no guarantee of correctness. Treat it as a "definitely look here" signal, not a quality metric.
208
+
209
+ ### 6.2 Suggested floors
210
+
211
+ - `src/app/**/services/**` — ≥ 70% line coverage
212
+ - `src/app/**/utils/**` — ≥ 80% (pure code, easy to test)
213
+ - `src/app/**/resources/**` — no floor (output-only, low risk)
214
+ - `src/app/**/models/**` — no floor (mostly getters; framework tests cover the rest)
215
+ - `src/app/**/types/**` — no floor (no runtime behaviour)
216
+
217
+ ### 6.3 Aim for "the meaningful paths are tested", not 100%
218
+
219
+ Chasing the last 5% of coverage produces brittle tests over edge-case code that nobody cares about. Spend the time on tests that catch real regressions.
220
+
221
+ ### 6.4 The `yarn test:coverage` command
222
+
223
+ Runs Vitest with `@vitest/coverage-v8` (already in dependencies). Add coverage as a CI gate per the floors above once the suite is large enough.
224
+
225
+ ---
226
+
227
+ ## 7. Flaky test policy — zero tolerance
228
+
229
+ A test that fails intermittently is a bug, not a nuisance.
230
+
231
+ ### 7.1 The deadline
232
+
233
+ - **24 hours** to quarantine (mark `.skip` with a tracking ticket).
234
+ - **7 days** to fix or delete.
235
+
236
+ ### 7.2 Why the deadline matters
237
+
238
+ Once a team accepts flakes, the suite dies. Every flake makes the next failure easier to dismiss ("probably flaky"). The signal-to-noise ratio collapses, real failures get ignored, the suite stops being trusted, the team stops writing tests.
239
+
240
+ Guard against this actively — flake reports are first-priority bugs.
241
+
242
+ ### 7.3 Common causes
243
+
244
+ - Shared state between tests (see § 3)
245
+ - Real I/O without proper teardown
246
+ - Timing assumptions (`setTimeout`, polling)
247
+ - Test order dependency
248
+ - Race conditions in the code under test (test exposed a real bug — fix the code)
249
+
250
+ ---
251
+
252
+ ## 8. Snapshot tests
253
+
254
+ ### 8.1 Use for
255
+
256
+ - Small structured output that's deterministic
257
+ - Error response shapes
258
+ - Resource serialization output (small ones)
259
+
260
+ ### 8.2 Don't use for
261
+
262
+ - Large objects (the diff becomes unreadable; reviewers rubber-stamp)
263
+ - Non-deterministic output (timestamps, generated ids — stub them first or omit)
264
+ - HTML / large strings (use targeted string assertions instead)
265
+
266
+ ### 8.3 Update with care
267
+
268
+ `vitest -u` regenerates snapshots. **Read every line of the diff before committing the update.** A blind `-u` is how silent regressions land.
269
+
270
+ ---
271
+
272
+ ## 9. Contract tests
273
+
274
+ ### 9.1 What and why
275
+
276
+ For any external integration whose shape might drift (third-party API, partner webhook):
277
+
278
+ - **Provider contract** — what we send, what we expect back
279
+ - **Consumer contract** — what they send us, what we expect to handle
280
+
281
+ Run against a recorded fixture in CI (fast, deterministic). Run against the real endpoint in a nightly job — if their API drifts, you catch it within 24h instead of in production.
282
+
283
+ ### 9.2 Where they live
284
+
285
+ Co-located with the integration: `src/app/payments/clients/stripe-client.contract.spec.ts`.
286
+
287
+ ---
288
+
289
+ ## 10. File layout
290
+
291
+ ```
292
+ src/app/orders/services/
293
+ place-order.service.ts
294
+ place-order.service.spec.ts ← unit test, mocks repository
295
+ place-order.service.integration.spec.ts ← integration, real DB
296
+ ```
297
+
298
+ - `.spec.ts` — default suffix; covers unit tests
299
+ - `.integration.spec.ts` — when the test needs real I/O (DB, queue)
300
+ - `.e2e.spec.ts` — full-system journey tests (sparse; one per critical flow)
301
+
302
+ Tests live next to the file under test, always. Never in a separate `__tests__/` or `test/` folder — colocation makes the link obvious and survives refactors.
303
+
304
+ ---
305
+
306
+ ## 11. Review checklist
307
+
308
+ Before merging a change that should include tests (testing enabled — see § 0):
309
+
310
+ - [ ] Every new/changed **service** has a co-located `.service.spec.ts`
311
+ - [ ] Every new/changed **HTTP controller / request handler** has a test (unit with mocked services, or integration through the route)
312
+ - [ ] New public function has at least one test
313
+ - [ ] Branches / error paths / edge cases covered
314
+ - [ ] No real provider API hit in tests (use `MockSDK` for AI, mock HTTP clients otherwise)
315
+ - [ ] Integration tests use testcontainers, not mocked DBs
316
+ - [ ] AAA structure, blank-line phase separation
317
+ - [ ] One assertion target per test (where practical)
318
+ - [ ] No `await sleep(...)` — fake timers instead
319
+ - [ ] No shared mutable state between tests
320
+ - [ ] Test names describe behaviour, not implementation
321
+ - [ ] No `.only`, no `.skip` without a tracking ticket
322
+ - [ ] Snapshots reviewed line-by-line if updated
323
+ - [ ] `yarn test --run` passes; `yarn test:coverage` doesn't drop the floor