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.
- package/CHANGELOG.md +38 -0
- package/README.md +8 -0
- package/esm/commands/create-new-app/index.mjs +4 -3
- package/esm/commands/create-new-app/index.mjs.map +1 -1
- package/esm/commands/create-warlock-app/index.mjs +7 -1
- package/esm/commands/create-warlock-app/index.mjs.map +1 -1
- package/esm/features/features-map.mjs +8 -2
- package/esm/features/features-map.mjs.map +1 -1
- package/esm/helpers/app.mjs +1 -0
- package/esm/helpers/app.mjs.map +1 -1
- package/esm/prompts/ask-stack.mjs +1 -1
- package/esm/prompts/ask-stack.mjs.map +1 -1
- package/llms-full.txt +64 -28
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +63 -27
- package/templates/design-system/SOURCES.md +23 -0
- package/templates/design-system/package.json +14 -0
- package/templates/design-system/src/web/components/ui/LICENSE.md +21 -0
- package/templates/design-system/src/web/components/ui/button.tsx +61 -0
- package/templates/design-system/src/web/components/ui/calendar.tsx +178 -0
- package/templates/design-system/src/web/components/ui/checkbox.tsx +28 -0
- package/templates/design-system/src/web/components/ui/command.tsx +161 -0
- package/templates/design-system/src/web/components/ui/dialog.tsx +144 -0
- package/templates/design-system/src/web/components/ui/input.tsx +20 -0
- package/templates/design-system/src/web/components/ui/label.tsx +20 -0
- package/templates/design-system/src/web/components/ui/popover.tsx +73 -0
- package/templates/design-system/src/web/components/ui/radio-group.tsx +44 -0
- package/templates/design-system/src/web/components/ui/switch.tsx +34 -0
- package/templates/design-system/src/web/components/ui/textarea.tsx +17 -0
- package/templates/design-system/src/web/design-system/components/checkbox-input.tsx +19 -0
- package/templates/design-system/src/web/design-system/components/date-picker-input.tsx +59 -0
- package/templates/design-system/src/web/design-system/components/date-time-picker-input.tsx +67 -0
- package/templates/design-system/src/web/design-system/components/email-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/field-frame.tsx +36 -0
- package/templates/design-system/src/web/design-system/components/file-input.tsx +24 -0
- package/templates/design-system/src/web/design-system/components/multi-select-input.tsx +94 -0
- package/templates/design-system/src/web/design-system/components/password-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/radio-input.tsx +41 -0
- package/templates/design-system/src/web/design-system/components/select-input.tsx +94 -0
- package/templates/design-system/src/web/design-system/components/submit-button.tsx +20 -0
- package/templates/design-system/src/web/design-system/components/switch-input.tsx +19 -0
- package/templates/design-system/src/web/design-system/components/text-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/textarea-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/time-picker-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/hooks/use-calendar-day-button.ts +9 -0
- package/templates/design-system/src/web/design-system/hooks/use-control.ts +29 -0
- package/templates/design-system/src/web/design-system/hooks/use-date-control.ts +42 -0
- package/templates/design-system/src/web/design-system/hooks/use-file-control.ts +24 -0
- package/templates/design-system/src/web/design-system/hooks/use-select-control.ts +78 -0
- package/templates/design-system/src/web/design-system/hooks/use-submit-control.ts +13 -0
- package/templates/design-system/src/web/design-system/types/control-props.ts +46 -0
- package/templates/toolchains/oxc/.oxlintrc.json +3 -1
- package/templates/warlock/.oxlintrc.json +3 -1
- package/templates/warlock/AGENTS.md +10 -10
- package/templates/warlock/e2e/home.e2e.ts +19 -0
- package/templates/warlock/package.json +1 -1
- package/templates/warlock/skills/api-design/SKILL.md +180 -57
- package/templates/warlock/skills/code-standards/SKILL.md +48 -39
- package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
- package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
- package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
- package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
- package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
- package/templates/warlock/skills/testing-strategy/SKILL.md +53 -12
- package/templates/warlock/skills/web-code-standards/SKILL.md +195 -60
- package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/login.controller.ts +17 -0
- package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/logout.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/me.controller.ts +7 -0
- package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +12 -0
- package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +6 -0
- package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
- package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
- package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
- package/templates/warlock/src/app/home/controllers/home-page.controller.ts +4 -0
- package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
- package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
- package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +15 -9
- package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
- package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
- package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
- package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
- package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +53 -0
- package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
- package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +13 -1
- package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
- package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
- package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
- package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
- package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
- package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
- package/templates/warlock/src/config/http.ts +6 -0
- package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
- package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
- package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
- package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
- package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
- package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
- package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
- package/templates/warlock/src/web/home/index.page.tsx +1 -1
- package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
- package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
- package/templates/warlock/warlock.config.ts +1 -0
- 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:
|
|
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,
|
|
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.
|
|
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 `
|
|
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 `
|
|
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:
|
|
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 `
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 `
|
|
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 `
|
|
295
|
+
### 7.1 `npm audit` in CI
|
|
295
296
|
|
|
296
|
-
CI runs `
|
|
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
|
-
`
|
|
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 `
|
|
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
|
|
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
|
-
- [ ] `
|
|
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:
|
|
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`,
|
|
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,
|
|
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>("
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 `
|
|
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.
|
|
312
|
+
place-order.service.test.ts ← integration, real DB
|
|
313
313
|
```
|
|
314
314
|
|
|
315
315
|
- `.spec.ts` — default suffix; covers unit tests
|
|
316
|
-
- `.
|
|
317
|
-
-
|
|
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
|
-
- [ ] `
|
|
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
|
+
```
|