create-warlock 5.34.2 → 5.35.0

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 (106) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +8 -0
  3. package/esm/commands/create-new-app/index.mjs +4 -3
  4. package/esm/commands/create-new-app/index.mjs.map +1 -1
  5. package/esm/commands/create-warlock-app/index.mjs +7 -1
  6. package/esm/commands/create-warlock-app/index.mjs.map +1 -1
  7. package/esm/features/features-map.mjs +8 -2
  8. package/esm/features/features-map.mjs.map +1 -1
  9. package/esm/helpers/app.mjs +1 -0
  10. package/esm/helpers/app.mjs.map +1 -1
  11. package/esm/prompts/ask-stack.mjs +1 -1
  12. package/esm/prompts/ask-stack.mjs.map +1 -1
  13. package/llms-full.txt +64 -28
  14. package/llms.txt +1 -1
  15. package/package.json +2 -2
  16. package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +63 -27
  17. package/templates/design-system/SOURCES.md +23 -0
  18. package/templates/design-system/package.json +14 -0
  19. package/templates/design-system/src/web/components/ui/LICENSE.md +21 -0
  20. package/templates/design-system/src/web/components/ui/button.tsx +61 -0
  21. package/templates/design-system/src/web/components/ui/calendar.tsx +178 -0
  22. package/templates/design-system/src/web/components/ui/checkbox.tsx +28 -0
  23. package/templates/design-system/src/web/components/ui/command.tsx +161 -0
  24. package/templates/design-system/src/web/components/ui/dialog.tsx +144 -0
  25. package/templates/design-system/src/web/components/ui/input.tsx +20 -0
  26. package/templates/design-system/src/web/components/ui/label.tsx +20 -0
  27. package/templates/design-system/src/web/components/ui/popover.tsx +73 -0
  28. package/templates/design-system/src/web/components/ui/radio-group.tsx +44 -0
  29. package/templates/design-system/src/web/components/ui/switch.tsx +34 -0
  30. package/templates/design-system/src/web/components/ui/textarea.tsx +17 -0
  31. package/templates/design-system/src/web/design-system/components/checkbox-input.tsx +19 -0
  32. package/templates/design-system/src/web/design-system/components/date-picker-input.tsx +59 -0
  33. package/templates/design-system/src/web/design-system/components/date-time-picker-input.tsx +67 -0
  34. package/templates/design-system/src/web/design-system/components/email-input.tsx +22 -0
  35. package/templates/design-system/src/web/design-system/components/field-frame.tsx +36 -0
  36. package/templates/design-system/src/web/design-system/components/file-input.tsx +24 -0
  37. package/templates/design-system/src/web/design-system/components/multi-select-input.tsx +94 -0
  38. package/templates/design-system/src/web/design-system/components/password-input.tsx +22 -0
  39. package/templates/design-system/src/web/design-system/components/radio-input.tsx +41 -0
  40. package/templates/design-system/src/web/design-system/components/select-input.tsx +94 -0
  41. package/templates/design-system/src/web/design-system/components/submit-button.tsx +20 -0
  42. package/templates/design-system/src/web/design-system/components/switch-input.tsx +19 -0
  43. package/templates/design-system/src/web/design-system/components/text-input.tsx +22 -0
  44. package/templates/design-system/src/web/design-system/components/textarea-input.tsx +22 -0
  45. package/templates/design-system/src/web/design-system/components/time-picker-input.tsx +22 -0
  46. package/templates/design-system/src/web/design-system/hooks/use-calendar-day-button.ts +9 -0
  47. package/templates/design-system/src/web/design-system/hooks/use-control.ts +29 -0
  48. package/templates/design-system/src/web/design-system/hooks/use-date-control.ts +42 -0
  49. package/templates/design-system/src/web/design-system/hooks/use-file-control.ts +24 -0
  50. package/templates/design-system/src/web/design-system/hooks/use-select-control.ts +78 -0
  51. package/templates/design-system/src/web/design-system/hooks/use-submit-control.ts +13 -0
  52. package/templates/design-system/src/web/design-system/types/control-props.ts +46 -0
  53. package/templates/toolchains/oxc/.oxlintrc.json +3 -1
  54. package/templates/warlock/.oxlintrc.json +3 -1
  55. package/templates/warlock/AGENTS.md +10 -10
  56. package/templates/warlock/e2e/home.e2e.ts +19 -0
  57. package/templates/warlock/package.json +1 -1
  58. package/templates/warlock/skills/api-design/SKILL.md +180 -57
  59. package/templates/warlock/skills/code-standards/SKILL.md +48 -39
  60. package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
  61. package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
  62. package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
  63. package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
  64. package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
  65. package/templates/warlock/skills/testing-strategy/SKILL.md +53 -12
  66. package/templates/warlock/skills/web-code-standards/SKILL.md +195 -60
  67. package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +6 -0
  68. package/templates/warlock/src/app/auth/controllers/login.controller.ts +17 -0
  69. package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +6 -0
  70. package/templates/warlock/src/app/auth/controllers/logout.controller.ts +6 -0
  71. package/templates/warlock/src/app/auth/controllers/me.controller.ts +7 -0
  72. package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +12 -0
  73. package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +6 -0
  74. package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
  75. package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
  76. package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
  77. package/templates/warlock/src/app/home/controllers/home-page.controller.ts +4 -0
  78. package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
  79. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
  80. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +15 -9
  81. package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
  82. package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
  83. package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
  84. package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
  85. package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +53 -0
  86. package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
  87. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +13 -1
  88. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
  89. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
  90. package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
  91. package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
  92. package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
  93. package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
  94. package/templates/warlock/src/config/http.ts +6 -0
  95. package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
  96. package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
  97. package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
  98. package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
  99. package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
  100. package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
  101. package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
  102. package/templates/warlock/src/web/home/index.page.tsx +1 -1
  103. package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
  104. package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
  105. package/templates/warlock/warlock.config.ts +1 -0
  106. package/templates/warlock/src/web/home/components/contact-form-controls.tsx +0 -42
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: observability-and-resilience
3
- description: 'How this project stays debuggable in production and survives flaky dependencies — `log.<level>(module, action, message, context)` from `@warlock.js/logger` with `X-Request-Id` propagation (already shipped via core middleware), structured metrics naming `<area>.<noun>.<unit>`, health endpoints (`/health/live` for liveness vs `/health/ready` for readiness), timeouts on every external call (`@warlock.js/http` `timeout` per Http instance / per request), retries with exponential backoff for idempotent ops only (never auto-retry POST without an idempotency key), idempotency keys for side effects (payments, emails, webhook deliveries), circuit breakers for flaky deps, graceful shutdown via `log.configure({ autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"] })`, N+1 detection, streaming for large payloads. Triggers: making an HTTP / AI / queue / DB call; adding a timeout or retry; designing an idempotency key for a side effect; instrumenting metrics; adding a health check; tracing a request across services; user asks "timeout", "retry policy", "idempotency", "request id", "tracing", "graceful shutdown", "health check", "circuit breaker", "N+1 query", "caching strategy", "metrics naming"; touching `@warlock.js/http` / `@warlock.js/scheduler` / `@warlock.js/ai` call sites. Skip: log call signature (load `skills/code-standards/SKILL.md` § 8); secrets in logs (load `skills/security-baseline/SKILL.md` § 8); pure-function refactors with no external I/O; cache driver setup (load `@warlock.js/cache` skills); HTTP client config in depth (load `@warlock.js/http` skills).'
3
+ description: "Request correlation, structured logs, timeouts, retries, idempotency, health checks, metrics, shutdown and bounded queries. Use when adding external I/O or production instrumentation. Use security-baseline instead for secrets and sensitive data handling."
4
4
  ---
5
5
 
6
6
  # Observability & resilience
@@ -53,7 +53,7 @@ Set a default at the `Http` instance level; override per request when the endpoi
53
53
  // ✅ instance-level default
54
54
  const stripeClient = new Http({
55
55
  baseURL: "https://api.stripe.com",
56
- timeout: 10_000, // 10s — enough for most calls, short enough to fail fast
56
+ timeout: 10_000, // 10s — enough for most calls, short enough to fail fast
57
57
  });
58
58
 
59
59
  // ✅ per-request override for a known-slow call
@@ -132,12 +132,14 @@ Persist `key → result` with a TTL longer than your retry window (24h is typica
132
132
 
133
133
  ### 4.4 Pattern
134
134
 
135
+ This simplified service sketch also needs an atomic reservation and provider-side idempotency to prevent concurrent duplicate charges. At the HTTP boundary, require `Idempotency-Key` and run `middleware.idempotency()` after auth as described in the `api-design` skill.
136
+
135
137
  ```typescript
136
138
  // ✅
137
139
  export async function chargePaymentService(input, idempotencyKey) {
138
- const cached = await idempotencyRepository.find({ key: idempotencyKey });
140
+ const cached = await idempotencyRepository.findBy("key", idempotencyKey);
139
141
  if (cached) {
140
- return cached.result;
142
+ return cached.get("result");
141
143
  }
142
144
 
143
145
  const result = await stripeClient.post("/v1/charges", input);
@@ -261,11 +263,11 @@ These belong here because performance failures present as resilience failures
261
263
 
262
264
  A list endpoint that fires one query per item is a bug. Either eager-load relations (Cascade's `.with("relation")`) or batch-fetch the related ids in a single query and join in-process.
263
265
 
264
- A code review that lets an N+1 through is doing its job poorly. Add it to the review checklist (already in `skills/api-design/SKILL.md`).
266
+ A code review that lets an N+1 through is doing its job poorly. Add it to the review checklist (already in the `api-design` skill).
265
267
 
266
268
  ### 9.2 Pagination always
267
269
 
268
- See `skills/api-design/SKILL.md` § 3. No unbounded list ever, even if today's dataset is 12 rows.
270
+ See the `api-design` skill § 3. No unbounded list ever, even if today's dataset is 12 rows.
269
271
 
270
272
  ### 9.3 Stream large payloads
271
273
 
@@ -1,6 +1,6 @@
1
1
  ---
2
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.'
3
+ description: "Input schemas, authentication, authorization, secrets, safe queries, rate limits, password handling and log redaction. Use when code accepts external input or handles identity or credentials. Use api-design instead for HTTP response contracts."
4
4
  ---
5
5
 
6
6
  # Security baseline
@@ -10,7 +10,7 @@ description: 'Project-level security floor for Warlock.js apps — input validat
10
10
 
11
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
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.
13
+ > **Sub-agent rule:** Before writing any code that handles external input, identity, or secrets, read this file. Code-style rules live in the `code-standards` skill — this skill is about what the code must do, not how it must look.
14
14
 
15
15
  ---
16
16
 
@@ -27,13 +27,13 @@ Anything that crosses the process boundary:
27
27
  - Webhook payloads
28
28
  - Queue messages
29
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)
30
+ - Database rows from a _different_ service (treat them as external if you don't own the writer)
31
31
 
32
32
  ### 1.2 Where validation runs
33
33
 
34
34
  At the boundary, exactly once. Never inside services.
35
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.
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 the `api-design` skill § 6 for the full controller shape.
37
37
 
38
38
  ```typescript
39
39
  // ✅ schema/create-order.schema.ts
@@ -56,10 +56,10 @@ export type CreateOrderSchema = Infer<typeof createOrderSchema>;
56
56
  import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
57
57
  import { type CreateOrderSchema, createOrderSchema } from "../schema/create-order.schema";
58
58
 
59
- export const createOrderController: GuardedRequestHandler<CreateOrderSchema> = async (
59
+ export const createOrderController: GuardedRequestHandler<CreateOrderSchema> = async ({
60
60
  request,
61
61
  response,
62
- ) => {
62
+ }) => {
63
63
  const order = await placeOrderService(request.validated()); // already typed + safe
64
64
  return response.success({ order });
65
65
  };
@@ -80,7 +80,7 @@ The framework returns `422 Unprocessable Entity` with structured field errors au
80
80
 
81
81
  - **Verify the signature first**, schema-validate second. A signature failure aborts before any other work.
82
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.
83
+ - Idempotency-key the handler (see the `observability-and-resilience` skill § 4) — providers retry.
84
84
 
85
85
  ---
86
86
 
@@ -92,18 +92,18 @@ AuthN proves who you are. AuthZ decides what you can do. Conflating them is how
92
92
 
93
93
  ```typescript
94
94
  // ❌ trusting the client
95
- const order = await ordersRepository.find({ id, userId: request.input("userId") });
95
+ const order = await Order.findOne({ publicId: id, userId: request.input("userId") });
96
96
 
97
97
  // ✅ identity from the authenticated session
98
98
  const user = request.locals.user;
99
- const order = await ordersRepository.find({ id, userId: user.id });
99
+ const order = await Order.findOne({ publicId: id, userId: user.id });
100
100
  ```
101
101
 
102
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
103
 
104
104
  ### 2.2 Authentication via `@warlock.js/auth`
105
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:
106
+ The project's auth stack: JWT access tokens and optional refresh tokens, configured in `src/config/auth.ts`. The starter uses `NO_EXPIRATION` for access tokens and disables refresh tokens; choose finite lifetimes and explicitly enable refresh when the application needs sessions. Password hashing via `bcryptjs`. The framework's `authService` (`@warlock.js/auth`) is the single entry point:
107
107
 
108
108
  ```typescript
109
109
  // ✅ thin wrapper service — the framework does the work
@@ -124,13 +124,13 @@ Pattern: at the start of every mutating service, check ownership and role:
124
124
  ```typescript
125
125
  // ✅
126
126
  export async function updateOrderService(orderId: string, input, user) {
127
- const order = await ordersRepository.find({ id: orderId });
127
+ const order = await Order.findOne({ publicId: orderId });
128
128
 
129
129
  if (!order) {
130
130
  throw new OrderNotFoundError(orderId);
131
131
  }
132
132
 
133
- if (order.userId !== user.id && !user.isAdmin) {
133
+ if (order.get("userId") !== user.id && !user.isAdmin) {
134
134
  throw new ForbiddenError("not your order");
135
135
  }
136
136
 
@@ -168,7 +168,7 @@ const authConfigurations: AuthConfigurations = {
168
168
  jwt: {
169
169
  secret: env("JWT_SECRET"),
170
170
  expiresIn: "1h",
171
- refresh: { /* ... */ },
171
+ refresh: {/* ... */},
172
172
  },
173
173
  };
174
174
  ```
@@ -184,10 +184,10 @@ The logger redaction layer is the safety net, but the first line of defence is n
184
184
  log.info("auth", "login", "User authenticated", { user, password, token });
185
185
 
186
186
  // ✅
187
- log.info("auth", "login", "User authenticated", { userId: user.id });
187
+ log.info("auth", "login", "User authenticated", { userId: user.get("publicId") });
188
188
  ```
189
189
 
190
- Configure `log.configure({ redact: { paths: ["password", "token", "*.secret", "*.apiKey"] } })` at startup — see `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md`.
190
+ Configure `log.configure({ redact: { paths: ["password", "token", "*.secret", "*.apiKey"] } })` at startup — see the `@warlock.js/logger` skill's `redact-sensitive-log-fields` topic.
191
191
 
192
192
  ### 3.4 Token lifetimes
193
193
 
@@ -213,7 +213,7 @@ const orders = await ordersRepository.raw(`status = '${input.status}'`);
213
213
 
214
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
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).
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 the `api-design` skill § 3.5).
217
217
 
218
218
  This is a real reason § 1 is non-negotiable.
219
219
 
@@ -281,6 +281,7 @@ For `/auth/login` and `/auth/reset-password`, key the limiter on the **email or
281
281
  ### 6.4 Progressive lockout
282
282
 
283
283
  After N failed login attempts:
284
+
284
285
  - 3 failures → 1-minute delay
285
286
  - 5 failures → 5-minute delay + email notification
286
287
  - 10 failures → temporary account lock + manual review
@@ -291,13 +292,13 @@ Never silently succeed-and-fail (returning 200 with no body to hide whether the
291
292
 
292
293
  ## 7. Dependency hygiene
293
294
 
294
- ### 7.1 `yarn audit` in CI
295
+ ### 7.1 `npm audit` in CI
295
296
 
296
- 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.
297
+ CI runs `npm audit --audit-level=high` on every PR. High and critical vulnerabilities block merge. Moderate goes on the backlog with a fix-by date.
297
298
 
298
299
  ### 7.2 Lockfile committed
299
300
 
300
- `yarn.lock` is checked in. Always. CI installs with `--frozen-lockfile`.
301
+ `package-lock.json` is checked in. CI installs with `npm ci`.
301
302
 
302
303
  ### 7.3 No `latest` in `package.json`
303
304
 
@@ -311,7 +312,7 @@ Every dependency is future risk. Before adding a 5-line utility, ask whether it'
311
312
 
312
313
  ## 8. Logging and PII
313
314
 
314
- See `skills/code-standards/SKILL.md` § 8 for the call signature. This is the **what not to log** list:
315
+ See the `code-standards` skill § 8 for the call signature. This is the **what not to log** list:
315
316
 
316
317
  ### 8.1 Never log
317
318
 
@@ -340,7 +341,7 @@ log.configure({
340
341
  });
341
342
  ```
342
343
 
343
- See `@warlock.js/logger/redact-sensitive-log-fields/SKILL.md` for the additive per-channel layer.
344
+ See the `@warlock.js/logger` skill's `redact-sensitive-log-fields` topic for the additive per-channel layer.
344
345
 
345
346
  ---
346
347
 
@@ -356,7 +357,7 @@ Before merging a change that touches input, identity, secrets, or persistence:
356
357
  - [ ] All DB queries go through Cascade — no raw string-built queries
357
358
  - [ ] Rate limit appropriate for the route family (default for general, aggressive for auth)
358
359
  - [ ] No `.env` or secrets file in the commit
359
- - [ ] `yarn audit --level=high` passes
360
+ - [ ] `npm audit --audit-level=high` passes
360
361
  - [ ] Logger redaction config covers any new sensitive field introduced
361
362
  - [ ] File uploads cap size, sniff MIME, server-generate the stored filename
362
363
  - [ ] Webhooks verify signature before any other work
@@ -1,6 +1,6 @@
1
1
  ---
2
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.'
3
+ description: "Vitest test scope, co-located service and endpoint specs when testing is enabled, isolation, boundary mocks, integration tests and flaky-test policy. Use when planning or reviewing test coverage. Use git-workflow instead for CI and merge gates."
4
4
  ---
5
5
 
6
6
  # Testing strategy
@@ -21,7 +21,7 @@ This skill governs projects that have **opted into testing**. Testing is **enabl
21
21
  - `package.json` has a `test` (or `test:*`) script, **or**
22
22
  - a `vitest.config.*` / `vite.config.*` file exists at the project root.
23
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).
24
+ When the `test` feature is selected at scaffold time, both land automatically (Vitest, `@vitest/coverage-v8`, and separate unit/integration projects with a per-file runtime by default).
25
25
 
26
26
  ### 0.1 If testing is enabled, tests are mandatory — not optional
27
27
 
@@ -32,7 +32,7 @@ Write the spec in the **same change** that adds the service or controller. A rev
32
32
 
33
33
  ### 0.2 If testing is NOT enabled
34
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.
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
36
 
37
37
  ---
38
38
 
@@ -95,7 +95,7 @@ Not enforced numerically. Enforced culturally — when a reviewer sees ten new e
95
95
  ### 2.2 Don't test
96
96
 
97
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
98
+ - **Trivial getters / setters** — `cart.totalCents` returning `this.get<number>("totalCents", 0)` doesn't need a test
99
99
  - **One-line passthroughs** — `loginService(...)` just calling `authService.login(...)` doesn't need a unit test (an integration test of the wiring is better)
100
100
  - **Generated code** — migrations, resource scaffolds
101
101
 
@@ -119,7 +119,7 @@ Each test sets up and tears down its own world. No `let users = []` at the top o
119
119
 
120
120
  Use transactions that roll back, or use testcontainers with a fresh container per file. Never share rows across tests.
121
121
 
122
- The project's testcontainers setup makes "fresh DB per test file" cheap — use it.
122
+ Provide the database fixture in global setup; the framework does not create testcontainers automatically. Give each worker its own database/schema before resetting rows.
123
123
 
124
124
  ### 3.4 Time and randomness
125
125
 
@@ -149,7 +149,7 @@ const result = await summarizeContentService(input, { provider: ai });
149
149
  expect(result).toBe("mocked response");
150
150
  ```
151
151
 
152
- See `@warlock.js/ai/skills/run-ai-agent/SKILL.md` for the full mock surface.
152
+ See the `@warlock.js/ai` skill's `run-ai-agent` topic for the full mock surface.
153
153
 
154
154
  ### 4.3 Database — real, via testcontainers
155
155
 
@@ -179,7 +179,7 @@ In the test environment `setupTest()` installs fakes for mail, queue, cache, sto
179
179
  ```typescript
180
180
  import { mailFake, queueFake } from "@warlock.js/core/tests";
181
181
 
182
- const order = { id: 42 }; // what the code under test placed
182
+ const order = { id: "550e8400-e29b-41d4-a716-446655440000" }; // what the code under test placed
183
183
 
184
184
  mailFake.assertSent({ to: "ada@example.test" });
185
185
  queueFake.assertPushed("invoices.send", { orderId: order.id });
@@ -235,7 +235,7 @@ Low coverage = guaranteed gaps. High coverage = no guarantee of correctness. Tre
235
235
 
236
236
  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.
237
237
 
238
- ### 6.4 The `yarn test:coverage` command
238
+ ### 6.4 The `npm run test:coverage` command
239
239
 
240
240
  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.
241
241
 
@@ -309,12 +309,12 @@ Co-located with the integration: `src/app/payments/clients/stripe-client.contrac
309
309
  src/app/orders/services/
310
310
  place-order.service.ts
311
311
  place-order.service.spec.ts ← unit test, mocks repository
312
- place-order.service.integration.spec.ts ← integration, real DB
312
+ place-order.service.test.ts ← integration, real DB
313
313
  ```
314
314
 
315
315
  - `.spec.ts` — default suffix; covers unit tests
316
- - `.integration.spec.ts` — when the test needs real I/O (DB, queue)
317
- - `.e2e.spec.ts` — full-system journey tests (sparse; one per critical flow)
316
+ - `.test.ts` or a spec inside `tests/` — integration, real I/O (DB, queue)
317
+ - Full-system journeys also use `.test.ts` in the integration project.
318
318
 
319
319
  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.
320
320
 
@@ -337,4 +337,45 @@ Before merging a change that should include tests (testing enabled — see § 0)
337
337
  - [ ] Test names describe behaviour, not implementation
338
338
  - [ ] No `.only`, no `.skip` without a tracking ticket
339
339
  - [ ] Snapshots reviewed line-by-line if updated
340
- - [ ] `yarn test --run` passes; `yarn test:coverage` doesn't drop the floor
340
+ - [ ] `npm run test --run` passes; `npm run test:coverage` doesn't drop the floor
341
+
342
+ ## Runtime cost and release gates
343
+
344
+ With `--features=test`, create-warlock delegates to `warlock add test` to generate
345
+ `vite.config.ts`: the `unit` project runs `src/**/*.spec.ts` outside `tests/`
346
+ without framework or HTTP setup; `integration` runs `*.test.ts` and specs under
347
+ `tests/` with `src/test-setup.ts`, global HTTP setup and `src/test-runner.ts`.
348
+ Prefer `npx vitest run --project unit` during the pure service loop.
349
+
350
+ When a unit needs config values, call
351
+ `setupTest({ mode: "unit", importModule: (file) => import(file) })` from
352
+ `@warlock.js/core/tests` and pair it with `afterAll(teardownTest)`. Config loads
353
+ once per module graph, without hooks, connectors, native loader or fakes.
354
+ Most unit specs need no setup call.
355
+
356
+ Integration setup defaults to `tests.lifetime: "file"`. For reuse, set
357
+ `lifetime: "worker"` in `src/config/tests.ts`, change the integration project to
358
+ `pool: "forks", isolate: false`, keep the generated runner, and use `vitest run`.
359
+ Watch, threads and isolated graphs are refused. Keep `afterAll(teardownTest)`:
360
+ it marks file boundaries while the runner closes connectors at worker exit.
361
+ Set `tests.resetBetweenFiles` to an async application-owned cleanup callback;
362
+ it runs before every file, including the first, after selected fakes, clock and
363
+ IDs reset. Each worker needs a separate database/schema. Module singletons and
364
+ mocks survive; reset application state and restore mocks explicitly. Worker
365
+ storage is private and does not mirror the shared HTTP server's fake files.
366
+
367
+ Use `tests.profileSetup: true` or
368
+ `WARLOCK_TEST_PROFILE=1 npx vitest run --project integration` for file timings and
369
+ summaries per worker. Pass `file: expect.getState().testPath` in the setup call.
370
+ `getTestRuntimeStats()` exposes bootstrap and file counts. Generated configs use
371
+ `recommendedTestWorkers()` from `@warlock.js/core/vite` to respect visible cgroup
372
+ v2 CPU quotas; reduce `maxWorkers` further for database or memory contention.
373
+
374
+ Release gate:
375
+
376
+ ```bash
377
+ npx warlock test --changed --since "$LAST_RELEASE_REF"
378
+ npx vitest run --project integration
379
+ # Nightly:
380
+ npx vitest run
381
+ ```