@acmekit/acmekit 2.13.83 → 2.13.84
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/dist/templates/app/.claude/agents/test-writer.md +287 -379
- package/dist/templates/app/.claude/commands/test.md +7 -1
- package/dist/templates/app/.claude/rules/testing.md +493 -846
- package/dist/templates/app/.claude/skills/write-test/SKILL.md +175 -117
- package/dist/templates/plugin/.claude/agents/test-writer.md +332 -273
- package/dist/templates/plugin/.claude/commands/test.md +7 -1
- package/dist/templates/plugin/.claude/rules/testing.md +397 -653
- package/dist/templates/plugin/.claude/skills/write-test/SKILL.md +233 -153
- package/package.json +39 -39
|
@@ -1,61 +1,95 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: write-test
|
|
3
|
-
description: "Generates integration or unit tests using the
|
|
3
|
+
description: "Generates integration or unit tests using the unified integrationTestRunner. Auto-detects the correct mode (app for HTTP/workflows, module for isolated services). Use when adding tests for modules, workflows, subscribers, jobs, or API routes."
|
|
4
4
|
argument-hint: "[module-or-feature]"
|
|
5
5
|
allowed-tools: Bash, Read, Write, Edit, Grep, Glob
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Write Test
|
|
9
9
|
|
|
10
|
-
Generate tests for AcmeKit applications using the correct
|
|
10
|
+
Generate tests for AcmeKit applications using `integrationTestRunner` with the correct mode and patterns.
|
|
11
11
|
|
|
12
12
|
## Current Test Files
|
|
13
13
|
|
|
14
14
|
- Existing tests: !`find src -name "*.spec.ts" -o -name "*.test.ts" 2>/dev/null | head -20 || echo "(none)"`
|
|
15
15
|
- HTTP integration tests: !`ls integration-tests/http/*.spec.ts 2>/dev/null || echo "(none)"`
|
|
16
|
+
- App integration tests: !`ls integration-tests/app/*.spec.ts 2>/dev/null || echo "(none)"`
|
|
16
17
|
|
|
17
18
|
## Critical Gotchas — Every Test Must Get These Right
|
|
18
19
|
|
|
19
|
-
1. **
|
|
20
|
-
2.
|
|
21
|
-
3. **`resolve` in
|
|
22
|
-
4. **Workflow errors are plain objects
|
|
23
|
-
5. **`.rejects.toThrow()` DOES work for service errors** (
|
|
24
|
-
6.
|
|
25
|
-
7. **Axios throws on 4xx/5xx.** Use `.catch((e) => e)` for error
|
|
20
|
+
1. **Unified runner only.** `import { integrationTestRunner } from "@acmekit/test-utils"`. NEVER use `acmekitIntegrationTestRunner` or `moduleIntegrationTestRunner` — those are deprecated.
|
|
21
|
+
2. **Service resolution key = module constant.** `getContainer().resolve(BLOG_MODULE)` where `BLOG_MODULE = "blog"` (the string passed to `Module()`). NEVER guess `"blogModuleService"`.
|
|
22
|
+
3. **`resolve` in module mode = absolute path.** Use `resolve: process.cwd() + "/src/modules/post"`. NEVER pass the imported module object.
|
|
23
|
+
4. **Workflow errors are plain objects.** NEVER use `.rejects.toThrow()` on workflows. Use `throwOnError: false` + `errors` array. Error path: `errors[0].error.message`.
|
|
24
|
+
5. **`.rejects.toThrow()` DOES work for service errors** (real `Error` instances). Only workflow errors are serialized.
|
|
25
|
+
6. **Inline auth setup.** There is no `createAdminUser` helper. Resolve `Modules.USER`, `Modules.AUTH`, `Modules.API_KEY` directly in `beforeEach`.
|
|
26
|
+
7. **Axios throws on 4xx/5xx.** Use `.catch((e: any) => e)` for error assertions.
|
|
26
27
|
|
|
27
28
|
## Instructions
|
|
28
29
|
|
|
29
30
|
### Step 1: Determine Test Type
|
|
30
31
|
|
|
31
|
-
| What to test |
|
|
32
|
+
| What to test | Mode | File location |
|
|
32
33
|
|---|---|---|
|
|
33
|
-
| API routes (HTTP
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
| Pure functions | Plain Jest `describe/it` | `src/**/__tests__/<name>.unit.spec.ts` |
|
|
34
|
+
| API routes (HTTP) | `mode: "app"` | `integration-tests/http/<feature>.spec.ts` |
|
|
35
|
+
| Workflows, subscribers, jobs | `mode: "app"` | `integration-tests/app/<feature>.spec.ts` |
|
|
36
|
+
| Module service methods | `mode: "module"` | `src/modules/<mod>/__tests__/<name>.spec.ts` |
|
|
37
|
+
| Pure functions | Plain Jest | `src/**/__tests__/<name>.unit.spec.ts` |
|
|
38
38
|
|
|
39
39
|
### Step 2: Generate Test File
|
|
40
40
|
|
|
41
41
|
#### HTTP Integration Test
|
|
42
42
|
|
|
43
43
|
```typescript
|
|
44
|
-
import {
|
|
44
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
45
|
+
import {
|
|
46
|
+
ApiKeyType,
|
|
47
|
+
CLIENT_API_KEY_HEADER,
|
|
48
|
+
ContainerRegistrationKeys,
|
|
49
|
+
generateJwtToken,
|
|
50
|
+
Modules,
|
|
51
|
+
} from "@acmekit/framework/utils"
|
|
45
52
|
|
|
46
53
|
jest.setTimeout(60 * 1000)
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
55
|
+
integrationTestRunner({
|
|
56
|
+
mode: "app",
|
|
57
|
+
testSuite: ({ api, getContainer }) => {
|
|
58
|
+
let adminHeaders: Record<string, any>
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
const container = getContainer()
|
|
62
|
+
const userModule = container.resolve(Modules.USER)
|
|
63
|
+
const authModule = container.resolve(Modules.AUTH)
|
|
64
|
+
|
|
65
|
+
const user = await userModule.createUsers({ email: "admin@test.js" })
|
|
66
|
+
const authIdentity = await authModule.createAuthIdentities({
|
|
67
|
+
provider_identities: [
|
|
68
|
+
{ provider: "emailpass", entity_id: "admin@test.js" },
|
|
69
|
+
],
|
|
70
|
+
app_metadata: { user_id: user.id },
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const config = container.resolve(ContainerRegistrationKeys.CONFIG_MODULE)
|
|
74
|
+
const { jwtSecret, jwtOptions } = config.projectConfig.http
|
|
75
|
+
|
|
76
|
+
const token = generateJwtToken(
|
|
77
|
+
{
|
|
78
|
+
actor_id: user.id,
|
|
79
|
+
actor_type: "user",
|
|
80
|
+
auth_identity_id: authIdentity.id,
|
|
81
|
+
app_metadata: { user_id: user.id },
|
|
82
|
+
},
|
|
83
|
+
{ secret: jwtSecret, expiresIn: "1d", jwtOptions }
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
adminHeaders = { headers: { authorization: `Bearer ${token}` } }
|
|
87
|
+
})
|
|
51
88
|
|
|
52
|
-
acmekitIntegrationTestRunner({
|
|
53
|
-
testSuite: ({ api, getContainer, utils }) => {
|
|
54
89
|
describe("GET /admin/posts", () => {
|
|
55
90
|
it("should list posts", async () => {
|
|
56
91
|
const response = await api.get("/admin/posts", adminHeaders)
|
|
57
|
-
expect(response.status).
|
|
58
|
-
expect(response.data.posts).toBeDefined()
|
|
92
|
+
expect(response.status).toEqual(200)
|
|
59
93
|
})
|
|
60
94
|
})
|
|
61
95
|
|
|
@@ -63,143 +97,166 @@ acmekitIntegrationTestRunner({
|
|
|
63
97
|
it("should create a post", async () => {
|
|
64
98
|
const response = await api.post(
|
|
65
99
|
"/admin/posts",
|
|
66
|
-
{ title: "Launch Announcement"
|
|
100
|
+
{ title: "Launch Announcement" },
|
|
67
101
|
adminHeaders
|
|
68
102
|
)
|
|
69
|
-
expect(response.status).
|
|
70
|
-
expect(response.data.post
|
|
103
|
+
expect(response.status).toEqual(200)
|
|
104
|
+
expect(response.data.post).toEqual(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
id: expect.any(String),
|
|
107
|
+
title: "Launch Announcement",
|
|
108
|
+
})
|
|
109
|
+
)
|
|
71
110
|
})
|
|
72
111
|
|
|
73
|
-
it("should reject missing required fields
|
|
112
|
+
it("should reject missing required fields", async () => {
|
|
74
113
|
const { response } = await api
|
|
75
114
|
.post("/admin/posts", {}, adminHeaders)
|
|
76
|
-
.catch((e) => e)
|
|
115
|
+
.catch((e: any) => e)
|
|
77
116
|
expect(response.status).toEqual(400)
|
|
78
117
|
})
|
|
79
118
|
})
|
|
80
119
|
|
|
81
120
|
describe("DELETE /admin/posts/:id", () => {
|
|
82
121
|
it("should delete and confirm", async () => {
|
|
83
|
-
const
|
|
84
|
-
"/admin/posts",
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
122
|
+
const created = (
|
|
123
|
+
await api.post("/admin/posts", { title: "To Remove" }, adminHeaders)
|
|
124
|
+
).data.post
|
|
125
|
+
|
|
88
126
|
const response = await api.delete(
|
|
89
|
-
`/admin/posts/${
|
|
127
|
+
`/admin/posts/${created.id}`,
|
|
90
128
|
adminHeaders
|
|
91
129
|
)
|
|
92
|
-
expect(response.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
expect(response.data).toEqual({
|
|
131
|
+
id: created.id,
|
|
132
|
+
object: "post",
|
|
133
|
+
deleted: true,
|
|
134
|
+
})
|
|
96
135
|
})
|
|
97
136
|
})
|
|
98
137
|
},
|
|
99
138
|
})
|
|
100
139
|
```
|
|
101
140
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
#### Module Integration Test
|
|
141
|
+
#### App Integration Test (workflows, subscribers, jobs)
|
|
105
142
|
|
|
106
143
|
```typescript
|
|
107
|
-
import {
|
|
108
|
-
import {
|
|
144
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
145
|
+
import { createPostsWorkflow } from "../../src/workflows/workflows"
|
|
146
|
+
import { POST_MODULE } from "../../src/modules/post"
|
|
109
147
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
])
|
|
119
|
-
expect(result).toHaveLength(1)
|
|
120
|
-
expect(result[0]).toEqual(
|
|
121
|
-
expect.objectContaining({ title: "Quarterly Report" })
|
|
122
|
-
)
|
|
148
|
+
jest.setTimeout(60 * 1000)
|
|
149
|
+
|
|
150
|
+
integrationTestRunner({
|
|
151
|
+
mode: "app",
|
|
152
|
+
testSuite: ({ getContainer }) => {
|
|
153
|
+
it("should create via workflow", async () => {
|
|
154
|
+
const { result } = await createPostsWorkflow(getContainer()).run({
|
|
155
|
+
input: { posts: [{ title: "My Post" }] },
|
|
123
156
|
})
|
|
157
|
+
expect(result[0]).toEqual(
|
|
158
|
+
expect.objectContaining({ title: "My Post" })
|
|
159
|
+
)
|
|
160
|
+
})
|
|
124
161
|
|
|
125
|
-
|
|
126
|
-
|
|
162
|
+
it("should reject invalid input", async () => {
|
|
163
|
+
const { errors } = await createPostsWorkflow(getContainer()).run({
|
|
164
|
+
input: { posts: [{ title: "", status: "bad" }] },
|
|
165
|
+
throwOnError: false,
|
|
127
166
|
})
|
|
167
|
+
expect(errors).toHaveLength(1)
|
|
168
|
+
expect(errors[0].error.message).toContain("Invalid")
|
|
128
169
|
})
|
|
129
170
|
},
|
|
130
171
|
})
|
|
131
172
|
```
|
|
132
173
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
#### Unit Test
|
|
174
|
+
#### Subscriber Test
|
|
136
175
|
|
|
137
176
|
```typescript
|
|
138
|
-
import {
|
|
177
|
+
import { integrationTestRunner, TestEventUtils } from "@acmekit/test-utils"
|
|
178
|
+
import { Modules } from "@acmekit/framework/utils"
|
|
179
|
+
import { POST_MODULE } from "../../src/modules/post"
|
|
180
|
+
|
|
181
|
+
jest.setTimeout(60 * 1000)
|
|
139
182
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
183
|
+
integrationTestRunner({
|
|
184
|
+
mode: "app",
|
|
185
|
+
testSuite: ({ getContainer }) => {
|
|
186
|
+
it("should execute subscriber side-effect", async () => {
|
|
187
|
+
const container = getContainer()
|
|
188
|
+
const service: any = container.resolve(POST_MODULE)
|
|
189
|
+
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
190
|
+
|
|
191
|
+
const [post] = await service.createPosts([
|
|
192
|
+
{ title: "Test", content: "Original" },
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
// CRITICAL: create promise BEFORE emitting
|
|
196
|
+
const subscriberDone = TestEventUtils.waitSubscribersExecution(
|
|
197
|
+
"post.published",
|
|
198
|
+
eventBus
|
|
199
|
+
)
|
|
200
|
+
await eventBus.emit({ name: "post.published", data: { id: post.id } })
|
|
201
|
+
await subscriberDone
|
|
202
|
+
|
|
203
|
+
const updated = await service.retrievePost(post.id)
|
|
204
|
+
expect(updated.content).toBe("Original [notified]")
|
|
205
|
+
})
|
|
206
|
+
},
|
|
144
207
|
})
|
|
145
208
|
```
|
|
146
209
|
|
|
147
|
-
####
|
|
210
|
+
#### Job Test
|
|
148
211
|
|
|
149
212
|
```typescript
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
await utils.waitWorkflowExecutions() // wait for async workflows
|
|
154
|
-
const response = await api.get("/admin/orders", adminHeaders)
|
|
155
|
-
expect(response.data.orders[0].status).toBe("processed")
|
|
156
|
-
})
|
|
213
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
214
|
+
import archiveOldPostsJob from "../../src/jobs/archive-old-posts"
|
|
215
|
+
import { POST_MODULE } from "../../src/modules/post"
|
|
157
216
|
|
|
158
|
-
|
|
159
|
-
it("should reject invalid workflow input", async () => {
|
|
160
|
-
const { errors } = await createPostWorkflow(getContainer()).run({
|
|
161
|
-
input: {},
|
|
162
|
-
throwOnError: false,
|
|
163
|
-
})
|
|
164
|
-
expect(errors).toHaveLength(1)
|
|
165
|
-
expect(errors[0].error.message).toContain("title") // errors[0].error.message — NOT errors[0].message
|
|
166
|
-
})
|
|
217
|
+
jest.setTimeout(60 * 1000)
|
|
167
218
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
219
|
+
integrationTestRunner({
|
|
220
|
+
mode: "app",
|
|
221
|
+
testSuite: ({ getContainer }) => {
|
|
222
|
+
it("should archive old posts", async () => {
|
|
223
|
+
const container = getContainer()
|
|
224
|
+
const service: any = container.resolve(POST_MODULE)
|
|
225
|
+
|
|
226
|
+
await service.createPosts([
|
|
227
|
+
{ title: "Old", status: "archived" },
|
|
228
|
+
{ title: "Active", status: "published" },
|
|
229
|
+
])
|
|
176
230
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
231
|
+
await archiveOldPostsJob(container)
|
|
232
|
+
|
|
233
|
+
const remaining = await service.listPosts()
|
|
234
|
+
expect(remaining).toHaveLength(1)
|
|
235
|
+
expect(remaining[0].title).toBe("Active")
|
|
236
|
+
})
|
|
237
|
+
},
|
|
184
238
|
})
|
|
185
239
|
```
|
|
186
240
|
|
|
187
|
-
####
|
|
241
|
+
#### Module Integration Test
|
|
188
242
|
|
|
189
243
|
```typescript
|
|
190
|
-
import {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
244
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
245
|
+
|
|
246
|
+
jest.setTimeout(30000)
|
|
247
|
+
|
|
248
|
+
integrationTestRunner<IPostModuleService>({
|
|
249
|
+
mode: "module",
|
|
250
|
+
moduleName: "post",
|
|
251
|
+
resolve: process.cwd() + "/src/modules/post",
|
|
252
|
+
testSuite: ({ service }) => {
|
|
253
|
+
it("should create a post", async () => {
|
|
254
|
+
const result = await service.createPosts([{ title: "Test" }])
|
|
255
|
+
expect(result[0]).toEqual(
|
|
256
|
+
expect.objectContaining({ title: "Test" })
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
},
|
|
203
260
|
})
|
|
204
261
|
```
|
|
205
262
|
|
|
@@ -208,17 +265,18 @@ it("should emit post.created event", async () => {
|
|
|
208
265
|
```bash
|
|
209
266
|
pnpm test:unit # Unit tests
|
|
210
267
|
pnpm test:integration:modules # Module integration tests
|
|
268
|
+
pnpm test:integration:app # App integration tests (workflows/subscribers/jobs)
|
|
211
269
|
pnpm test:integration:http # HTTP integration tests
|
|
212
270
|
```
|
|
213
271
|
|
|
214
272
|
## Key Patterns
|
|
215
273
|
|
|
274
|
+
- Use `integrationTestRunner` with `mode` — never the old deprecated runner names
|
|
216
275
|
- Match the `jest.config.js` test buckets — don't invent new locations
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
220
|
-
- Pass body directly to `api.post(url, body, headers)` — NOT `{ body: {...} }`
|
|
221
|
-
- Use `.catch((e) => e)` for error-case assertions — axios throws on 4xx/5xx
|
|
276
|
+
- Inline auth setup in `beforeEach` — no `createAdminUser` helper
|
|
277
|
+
- Pass body directly: `api.post(url, body, headers)` — NOT `{ body: {...} }`
|
|
278
|
+
- Use `.catch((e: any) => e)` for error assertions — axios throws on 4xx/5xx
|
|
222
279
|
- Use `expect.objectContaining()` with `expect.any(String)` for IDs/timestamps
|
|
223
280
|
- Call `waitSubscribersExecution` BEFORE triggering the event
|
|
281
|
+
- Import jobs/subscribers directly and call with container
|
|
224
282
|
- Use realistic test data, not "test" or "foo"
|