@acmekit/acmekit 2.13.83 → 2.13.85
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 +348 -368
- package/dist/templates/app/.claude/commands/test.md +7 -1
- package/dist/templates/app/.claude/rules/testing.md +711 -844
- package/dist/templates/app/.claude/skills/write-test/SKILL.md +216 -112
- package/dist/templates/plugin/.claude/agents/test-writer.md +405 -265
- package/dist/templates/plugin/.claude/commands/test.md +7 -1
- package/dist/templates/plugin/.claude/rules/testing.md +608 -597
- package/dist/templates/plugin/.claude/skills/write-test/SKILL.md +275 -147
- package/package.json +39 -39
|
@@ -1,225 +1,342 @@
|
|
|
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 (plugin for container/HTTP, 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 (Plugin)
|
|
9
9
|
|
|
10
|
-
Generate tests for AcmeKit plugins using the correct
|
|
11
|
-
|
|
12
|
-
**CRITICAL:** Plugin tests have NO HTTP server. Do NOT generate tests using `api.get()`, `api.post()`, or `acmekitIntegrationTestRunner`. Test services directly through `container.resolve()`.
|
|
10
|
+
Generate tests for AcmeKit plugins using `integrationTestRunner` with the correct mode and patterns.
|
|
13
11
|
|
|
14
12
|
## Current Test Files
|
|
15
13
|
|
|
16
14
|
- Existing tests: !`find src -name "*.spec.ts" -o -name "*.test.ts" 2>/dev/null | head -20 || echo "(none)"`
|
|
17
15
|
- Plugin integration tests: !`ls integration-tests/plugin/*.spec.ts 2>/dev/null || echo "(none)"`
|
|
16
|
+
- HTTP integration tests: !`ls integration-tests/http/*.spec.ts 2>/dev/null || echo "(none)"`
|
|
18
17
|
|
|
19
18
|
## Critical Gotchas — Every Test Must Get These Right
|
|
20
19
|
|
|
21
|
-
1. **
|
|
22
|
-
2.
|
|
23
|
-
3. **`resolve` in
|
|
24
|
-
4. **Workflow errors are plain objects
|
|
25
|
-
5. **`.rejects.toThrow()` DOES work for service errors** (
|
|
26
|
-
6. **
|
|
20
|
+
1. **Unified runner only.** `import { integrationTestRunner } from "@acmekit/test-utils"`. NEVER use `pluginIntegrationTestRunner` or `moduleIntegrationTestRunner` — those are deprecated.
|
|
21
|
+
2. **Service resolution key = module constant.** `container.resolve(GREETING_MODULE)` where `GREETING_MODULE = "greeting"` (the string passed to `Module()`). NEVER guess `"greetingModuleService"`.
|
|
22
|
+
3. **`resolve` in module mode = absolute path.** Use `resolve: process.cwd() + "/src/modules/greeting"`. 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. **Container-only vs HTTP.** Plugin mode without `http: true` has NO `api` fixture. NEVER use `api.get()` or `api.post()` in container-only tests.
|
|
26
|
+
7. **HTTP mode requires auth.** `mode: "plugin"` + `http: true` boots the full framework — inline JWT + client API key setup in `beforeEach`.
|
|
27
|
+
8. **Axios throws on 4xx/5xx.** Use `.catch((e: any) => e)` for error assertions in HTTP tests.
|
|
28
|
+
9. **MockEventBusService emit takes arrays.** In plugin container mode, `emit` receives `[{ name, data }]` (array). Real event bus uses `{ name, data }` (single object).
|
|
27
29
|
|
|
28
30
|
## Instructions
|
|
29
31
|
|
|
30
32
|
### Step 1: Determine Test Type
|
|
31
33
|
|
|
32
|
-
| What to test |
|
|
34
|
+
| What to test | Mode | File location |
|
|
33
35
|
|---|---|---|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
| Pure functions | Plain Jest
|
|
36
|
+
| API routes (HTTP end-to-end) | `mode: "plugin"` + `http: true` | `integration-tests/http/<feature>.spec.ts` |
|
|
37
|
+
| Workflows, subscribers, jobs (container) | `mode: "plugin"` | `integration-tests/plugin/<feature>.spec.ts` |
|
|
38
|
+
| Module service methods (isolated) | `mode: "module"` | `src/modules/<mod>/__tests__/<name>.spec.ts` |
|
|
39
|
+
| Pure functions | Plain Jest | `src/**/__tests__/<name>.unit.spec.ts` |
|
|
38
40
|
|
|
39
41
|
### Step 2: Generate Test File
|
|
40
42
|
|
|
41
|
-
#### Plugin Integration Test
|
|
43
|
+
#### Plugin HTTP Integration Test
|
|
42
44
|
|
|
43
45
|
```typescript
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
47
|
+
import {
|
|
48
|
+
ApiKeyType,
|
|
49
|
+
CLIENT_API_KEY_HEADER,
|
|
50
|
+
ContainerRegistrationKeys,
|
|
51
|
+
generateJwtToken,
|
|
52
|
+
Modules,
|
|
53
|
+
} from "@acmekit/framework/utils"
|
|
54
|
+
import { GREETING_MODULE } from "../../src/modules/greeting"
|
|
55
|
+
|
|
56
|
+
jest.setTimeout(120 * 1000)
|
|
57
|
+
|
|
58
|
+
integrationTestRunner({
|
|
59
|
+
mode: "plugin",
|
|
60
|
+
http: true,
|
|
51
61
|
pluginPath: process.cwd(),
|
|
52
|
-
pluginOptions: {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
pluginOptions: { apiKey: "test-api-key" },
|
|
63
|
+
testSuite: ({ api, container }) => {
|
|
64
|
+
let adminHeaders: Record<string, any>
|
|
65
|
+
let clientHeaders: Record<string, any>
|
|
66
|
+
|
|
67
|
+
beforeEach(async () => {
|
|
68
|
+
const userModule = container.resolve(Modules.USER)
|
|
69
|
+
const authModule = container.resolve(Modules.AUTH)
|
|
70
|
+
const apiKeyModule = container.resolve(Modules.API_KEY)
|
|
71
|
+
|
|
72
|
+
const user = await userModule.createUsers({
|
|
73
|
+
email: "admin@test.js",
|
|
59
74
|
})
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
const authIdentity = await authModule.createAuthIdentities({
|
|
77
|
+
provider_identities: [
|
|
78
|
+
{ provider: "emailpass", entity_id: "admin@test.js" },
|
|
79
|
+
],
|
|
80
|
+
app_metadata: { user_id: user.id },
|
|
64
81
|
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe("BlogModule CRUD", () => {
|
|
68
|
-
let service: any
|
|
69
82
|
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
const config = container.resolve(
|
|
84
|
+
ContainerRegistrationKeys.CONFIG_MODULE
|
|
85
|
+
)
|
|
86
|
+
const { jwtSecret, jwtOptions } = config.projectConfig.http
|
|
87
|
+
|
|
88
|
+
const token = generateJwtToken(
|
|
89
|
+
{
|
|
90
|
+
actor_id: user.id,
|
|
91
|
+
actor_type: "user",
|
|
92
|
+
auth_identity_id: authIdentity.id,
|
|
93
|
+
app_metadata: { user_id: user.id },
|
|
94
|
+
},
|
|
95
|
+
{ secret: jwtSecret, expiresIn: "1d", jwtOptions }
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
adminHeaders = {
|
|
99
|
+
headers: { authorization: `Bearer ${token}` },
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const apiKey = await apiKeyModule.createApiKeys({
|
|
103
|
+
title: "Test Client Key",
|
|
104
|
+
type: ApiKeyType.CLIENT,
|
|
105
|
+
created_by: "test",
|
|
72
106
|
})
|
|
73
107
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
108
|
+
clientHeaders = {
|
|
109
|
+
headers: { [CLIENT_API_KEY_HEADER]: apiKey.token },
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe("POST /admin/plugin/greetings", () => {
|
|
114
|
+
it("should create a greeting", async () => {
|
|
115
|
+
const response = await api.post(
|
|
116
|
+
"/admin/plugin/greetings",
|
|
117
|
+
{ message: "Hello from HTTP" },
|
|
118
|
+
adminHeaders
|
|
119
|
+
)
|
|
120
|
+
expect(response.status).toEqual(200)
|
|
121
|
+
expect(response.data.greeting).toEqual(
|
|
80
122
|
expect.objectContaining({
|
|
81
123
|
id: expect.any(String),
|
|
82
|
-
|
|
124
|
+
message: "Hello from HTTP",
|
|
83
125
|
})
|
|
84
126
|
)
|
|
85
127
|
})
|
|
86
128
|
|
|
87
|
-
it("should
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
it("should reject missing required fields", async () => {
|
|
130
|
+
const { response } = await api
|
|
131
|
+
.post("/admin/plugin/greetings", {}, adminHeaders)
|
|
132
|
+
.catch((e: any) => e)
|
|
133
|
+
expect(response.status).toEqual(400)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe("DELETE /admin/plugin/greetings/:id", () => {
|
|
138
|
+
it("should soft-delete and confirm", async () => {
|
|
139
|
+
const created = (
|
|
140
|
+
await api.post(
|
|
141
|
+
"/admin/plugin/greetings",
|
|
142
|
+
{ message: "Delete me" },
|
|
143
|
+
adminHeaders
|
|
144
|
+
)
|
|
145
|
+
).data.greeting
|
|
146
|
+
|
|
147
|
+
const response = await api.delete(
|
|
148
|
+
`/admin/plugin/greetings/${created.id}`,
|
|
149
|
+
adminHeaders
|
|
150
|
+
)
|
|
151
|
+
expect(response.data).toEqual({
|
|
152
|
+
id: created.id,
|
|
153
|
+
object: "greeting",
|
|
154
|
+
deleted: true,
|
|
155
|
+
})
|
|
91
156
|
})
|
|
157
|
+
})
|
|
92
158
|
|
|
93
|
-
|
|
94
|
-
|
|
159
|
+
describe("Client routes", () => {
|
|
160
|
+
it("GET /client/plugin/greetings with API key", async () => {
|
|
161
|
+
await api.post(
|
|
162
|
+
"/admin/plugin/greetings",
|
|
163
|
+
{ message: "Public" },
|
|
164
|
+
adminHeaders
|
|
165
|
+
)
|
|
166
|
+
const response = await api.get(
|
|
167
|
+
"/client/plugin/greetings",
|
|
168
|
+
clientHeaders
|
|
169
|
+
)
|
|
170
|
+
expect(response.status).toEqual(200)
|
|
171
|
+
expect(response.data.greetings).toHaveLength(1)
|
|
95
172
|
})
|
|
96
173
|
})
|
|
97
174
|
},
|
|
98
175
|
})
|
|
99
176
|
```
|
|
100
177
|
|
|
101
|
-
|
|
178
|
+
#### Plugin Container Integration Test (workflows, subscribers, jobs)
|
|
102
179
|
|
|
103
|
-
|
|
180
|
+
```typescript
|
|
181
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
182
|
+
import { createGreetingsWorkflow } from "../../src/workflows"
|
|
183
|
+
import { GREETING_MODULE } from "../../src/modules/greeting"
|
|
184
|
+
|
|
185
|
+
jest.setTimeout(60 * 1000)
|
|
186
|
+
|
|
187
|
+
integrationTestRunner({
|
|
188
|
+
mode: "plugin",
|
|
189
|
+
pluginPath: process.cwd(),
|
|
190
|
+
pluginOptions: { apiKey: "test-api-key" },
|
|
191
|
+
testSuite: ({ container }) => {
|
|
192
|
+
it("should create via workflow", async () => {
|
|
193
|
+
const { result } = await createGreetingsWorkflow(container).run({
|
|
194
|
+
input: { greetings: [{ message: "Workflow Hello" }] },
|
|
195
|
+
})
|
|
196
|
+
expect(result).toHaveLength(1)
|
|
197
|
+
expect(result[0].message).toBe("Workflow Hello")
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("should reject invalid input", async () => {
|
|
201
|
+
const { errors } = await createGreetingsWorkflow(container).run({
|
|
202
|
+
input: { greetings: [{ message: "", status: "invalid" }] },
|
|
203
|
+
throwOnError: false,
|
|
204
|
+
})
|
|
205
|
+
expect(errors).toHaveLength(1)
|
|
206
|
+
expect(errors[0].error.message).toContain("Invalid")
|
|
207
|
+
})
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### Subscriber Test (direct handler invocation)
|
|
104
213
|
|
|
105
214
|
```typescript
|
|
106
|
-
import {
|
|
107
|
-
import
|
|
215
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
216
|
+
import greetingCreatedHandler from "../../src/subscribers/greeting-created"
|
|
217
|
+
import { GREETING_MODULE } from "../../src/modules/greeting"
|
|
108
218
|
|
|
109
|
-
jest.setTimeout(
|
|
219
|
+
jest.setTimeout(60 * 1000)
|
|
110
220
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
testSuite: ({
|
|
115
|
-
it("should
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
expect(result).toEqual([
|
|
120
|
-
expect.objectContaining({ title: "Quarterly Report" }),
|
|
221
|
+
integrationTestRunner({
|
|
222
|
+
mode: "plugin",
|
|
223
|
+
pluginPath: process.cwd(),
|
|
224
|
+
testSuite: ({ container }) => {
|
|
225
|
+
it("should append ' [notified]' to greeting message", async () => {
|
|
226
|
+
const service: any = container.resolve(GREETING_MODULE)
|
|
227
|
+
const [greeting] = await service.createGreetings([
|
|
228
|
+
{ message: "Hello World" },
|
|
121
229
|
])
|
|
230
|
+
|
|
231
|
+
await greetingCreatedHandler({
|
|
232
|
+
event: {
|
|
233
|
+
data: { id: greeting.id },
|
|
234
|
+
name: "greeting.created",
|
|
235
|
+
},
|
|
236
|
+
container,
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const updated = await service.retrieveGreeting(greeting.id)
|
|
240
|
+
expect(updated.message).toBe("Hello World [notified]")
|
|
122
241
|
})
|
|
123
242
|
},
|
|
124
243
|
})
|
|
125
244
|
```
|
|
126
245
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
#### Unit Test
|
|
246
|
+
#### Job Test (direct invocation)
|
|
130
247
|
|
|
131
248
|
```typescript
|
|
132
|
-
import {
|
|
249
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
250
|
+
import cleanupGreetingsJob from "../../src/jobs/cleanup-greetings"
|
|
251
|
+
import { GREETING_MODULE } from "../../src/modules/greeting"
|
|
133
252
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
253
|
+
jest.setTimeout(60 * 1000)
|
|
254
|
+
|
|
255
|
+
integrationTestRunner({
|
|
256
|
+
mode: "plugin",
|
|
257
|
+
pluginPath: process.cwd(),
|
|
258
|
+
testSuite: ({ container }) => {
|
|
259
|
+
it("should soft-delete greetings with lang='old'", async () => {
|
|
260
|
+
const service: any = container.resolve(GREETING_MODULE)
|
|
261
|
+
|
|
262
|
+
await service.createGreetings([
|
|
263
|
+
{ message: "Old 1", lang: "old" },
|
|
264
|
+
{ message: "Old 2", lang: "old" },
|
|
265
|
+
{ message: "Current", lang: "en" },
|
|
266
|
+
])
|
|
267
|
+
|
|
268
|
+
await cleanupGreetingsJob(container)
|
|
269
|
+
|
|
270
|
+
const remaining = await service.listGreetings()
|
|
271
|
+
expect(remaining).toHaveLength(1)
|
|
272
|
+
expect(remaining[0].lang).toBe("en")
|
|
273
|
+
})
|
|
274
|
+
},
|
|
138
275
|
})
|
|
139
276
|
```
|
|
140
277
|
|
|
141
|
-
####
|
|
278
|
+
#### Unit Test (No Framework Bootstrap)
|
|
142
279
|
|
|
143
|
-
|
|
144
|
-
import { createBlogPostWorkflow } from "../../src/workflows"
|
|
280
|
+
For providers, utilities, and standalone classes. **CRITICAL:** `jest.mock()` factories are hoisted above `const`/`let`. Create mocks INSIDE the factory, access via `require()`.
|
|
145
281
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
282
|
+
```typescript
|
|
283
|
+
jest.mock("external-sdk", () => {
|
|
284
|
+
const mocks = { doThing: jest.fn() }
|
|
285
|
+
const MockClient = jest.fn().mockImplementation(() => ({
|
|
286
|
+
doThing: mocks.doThing,
|
|
287
|
+
}))
|
|
288
|
+
return { Client: MockClient, __mocks: mocks }
|
|
154
289
|
})
|
|
155
290
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
291
|
+
const { __mocks: sdkMocks } = require("external-sdk")
|
|
292
|
+
|
|
293
|
+
import MyProvider from "../my-provider"
|
|
294
|
+
|
|
295
|
+
describe("MyProvider", () => {
|
|
296
|
+
const mockContainer = {} as any
|
|
297
|
+
|
|
298
|
+
beforeEach(() => {
|
|
299
|
+
jest.clearAllMocks()
|
|
160
300
|
})
|
|
161
|
-
expect(errors).toHaveLength(1)
|
|
162
|
-
expect(errors[0].error.message).toContain("title") // errors[0].error.message — NOT errors[0].message
|
|
163
|
-
})
|
|
164
301
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const { errors } = await chargeCardWorkflow(container).run({
|
|
168
|
-
input: { cardToken: "invalid", amount: 100 },
|
|
169
|
-
throwOnError: false,
|
|
302
|
+
it("should have correct identifier", () => {
|
|
303
|
+
expect(MyProvider.identifier).toBe("my-provider")
|
|
170
304
|
})
|
|
171
|
-
expect(errors[0].error.message).toContain("Card declined")
|
|
172
|
-
})
|
|
173
305
|
|
|
174
|
-
it("should
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
306
|
+
it("should delegate to SDK", async () => {
|
|
307
|
+
sdkMocks.doThing.mockResolvedValue({ success: true })
|
|
308
|
+
const provider = new MyProvider(mockContainer, { apiKey: "key" })
|
|
309
|
+
const result = await provider.doSomething({ input: "test" })
|
|
310
|
+
expect(result.success).toBe(true)
|
|
178
311
|
})
|
|
179
|
-
// SkipStep doesn't produce errors — workflow continues
|
|
180
|
-
expect(result).toBeDefined()
|
|
181
312
|
})
|
|
182
313
|
```
|
|
183
314
|
|
|
184
|
-
|
|
315
|
+
**Timer mocking:** Use `jest.useFakeTimers()` + `jest.advanceTimersByTimeAsync()` or `jest.spyOn(instance, "sleep_").mockResolvedValue(undefined)`.
|
|
185
316
|
|
|
186
|
-
|
|
187
|
-
import { MockEventBusService } from "@acmekit/test-utils"
|
|
188
|
-
|
|
189
|
-
let eventBusSpy: jest.SpyInstance
|
|
190
|
-
beforeEach(() => { eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") })
|
|
191
|
-
afterEach(() => { eventBusSpy.mockRestore() })
|
|
192
|
-
|
|
193
|
-
it("should emit blog-post.created event", async () => {
|
|
194
|
-
const service = container.resolve(BLOG_MODULE) // "blog" — from Module() key
|
|
195
|
-
await service.createBlogPosts([{ title: "Event Test" }])
|
|
196
|
-
expect(eventBusSpy).toHaveBeenCalledWith(
|
|
197
|
-
[expect.objectContaining({
|
|
198
|
-
name: "blog-post.created",
|
|
199
|
-
data: expect.objectContaining({ id: expect.any(String) }),
|
|
200
|
-
})],
|
|
201
|
-
{ internal: true }
|
|
202
|
-
)
|
|
203
|
-
})
|
|
204
|
-
```
|
|
317
|
+
**SWC regex:** Use `new RegExp("...")` instead of complex regex literals.
|
|
205
318
|
|
|
206
|
-
####
|
|
319
|
+
#### Module Integration Test (isolated)
|
|
207
320
|
|
|
208
321
|
```typescript
|
|
209
|
-
import {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
322
|
+
import { integrationTestRunner } from "@acmekit/test-utils"
|
|
323
|
+
|
|
324
|
+
jest.setTimeout(30000)
|
|
325
|
+
|
|
326
|
+
integrationTestRunner<IGreetingModuleService>({
|
|
327
|
+
mode: "module",
|
|
328
|
+
moduleName: "greeting",
|
|
329
|
+
resolve: process.cwd() + "/src/modules/greeting",
|
|
330
|
+
testSuite: ({ service }) => {
|
|
331
|
+
it("should create a greeting", async () => {
|
|
332
|
+
const result = await service.createGreetings([
|
|
333
|
+
{ message: "Hello" },
|
|
334
|
+
])
|
|
335
|
+
expect(result[0]).toEqual(
|
|
336
|
+
expect.objectContaining({ message: "Hello" })
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
},
|
|
223
340
|
})
|
|
224
341
|
```
|
|
225
342
|
|
|
@@ -228,16 +345,27 @@ it("should execute subscriber side-effect", async () => {
|
|
|
228
345
|
```bash
|
|
229
346
|
pnpm test:unit # Unit tests
|
|
230
347
|
pnpm test:integration:modules # Module integration tests
|
|
231
|
-
pnpm test:integration:plugin #
|
|
348
|
+
pnpm test:integration:plugin # Plugin container tests
|
|
349
|
+
pnpm test:integration:http # Plugin HTTP tests
|
|
232
350
|
```
|
|
233
351
|
|
|
234
352
|
## Key Patterns
|
|
235
353
|
|
|
354
|
+
- Use `integrationTestRunner` with `mode` — never the old deprecated runner names
|
|
236
355
|
- Match the `jest.config.js` test buckets — don't invent new locations
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
356
|
+
- Always use `pluginPath: process.cwd()` — never hardcode paths
|
|
357
|
+
- Container-only tests: access services via `container.resolve(MODULE_CONSTANT)` — no `api` fixture
|
|
358
|
+
- HTTP tests: full auth setup with `generateJwtToken` + `ApiKeyType.CLIENT` — no `createAdminUser` helper
|
|
359
|
+
- Plugin depends on other plugins: add `skipDependencyValidation: true` + mock peer services via `injectedDependencies`
|
|
360
|
+
- Plugin providers need options: use `pluginModuleOptions: { moduleName: { providers: [...] } }`
|
|
361
|
+
- Pass body directly: `api.post(url, body, headers)` — NOT `{ body: {...} }`
|
|
362
|
+
- Use `.catch((e: any) => e)` for error assertions — axios throws on 4xx/5xx
|
|
241
363
|
- Use `expect.objectContaining()` with `expect.any(String)` for IDs/timestamps
|
|
242
|
-
-
|
|
364
|
+
- Test subscribers by importing handler directly and calling with `{ event, container }`
|
|
365
|
+
- Test jobs by importing function directly and calling with `container`
|
|
243
366
|
- Use realistic test data, not "test" or "foo"
|
|
367
|
+
- **Always `beforeEach(() => jest.clearAllMocks())`** in unit tests — mock state leaks between describes
|
|
368
|
+
- **Never reference file-level `const`/`let` inside `jest.mock()` factories** — TDZ error (SWC/Babel hoisting)
|
|
369
|
+
- **Mock timers or sleep** when code under test has delays — prevents test timeouts
|
|
370
|
+
- **Use `new RegExp()` over complex regex literals** — SWC parser fails on some patterns
|
|
371
|
+
- **Read implementation to verify error paths** — check if errors are thrown vs returned
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acmekit/acmekit",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.85",
|
|
4
4
|
"description": "Generic application bootstrap and loaders for the AcmeKit framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -49,45 +49,45 @@
|
|
|
49
49
|
"test:integration": "../../node_modules/.bin/jest --passWithNoTests --forceExit --testPathPattern=\"src/.*/integration-tests/__tests__/.*\\.ts\""
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@acmekit/framework": "2.13.
|
|
52
|
+
"@acmekit/framework": "2.13.85"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@acmekit/admin-bundler": "2.13.
|
|
56
|
-
"@acmekit/analytics": "2.13.
|
|
57
|
-
"@acmekit/analytics-local": "2.13.
|
|
58
|
-
"@acmekit/analytics-posthog": "2.13.
|
|
59
|
-
"@acmekit/api-key": "2.13.
|
|
60
|
-
"@acmekit/auth": "2.13.
|
|
61
|
-
"@acmekit/auth-emailpass": "2.13.
|
|
62
|
-
"@acmekit/auth-github": "2.13.
|
|
63
|
-
"@acmekit/auth-google": "2.13.
|
|
64
|
-
"@acmekit/cache-inmemory": "2.13.
|
|
65
|
-
"@acmekit/cache-redis": "2.13.
|
|
66
|
-
"@acmekit/caching": "2.13.
|
|
67
|
-
"@acmekit/caching-redis": "2.13.
|
|
68
|
-
"@acmekit/core-flows": "2.13.
|
|
69
|
-
"@acmekit/event-bus-local": "2.13.
|
|
70
|
-
"@acmekit/event-bus-redis": "2.13.
|
|
71
|
-
"@acmekit/file": "2.13.
|
|
72
|
-
"@acmekit/file-local": "2.13.
|
|
73
|
-
"@acmekit/file-s3": "2.13.
|
|
74
|
-
"@acmekit/index": "2.13.
|
|
75
|
-
"@acmekit/link-modules": "2.13.
|
|
76
|
-
"@acmekit/locking": "2.13.
|
|
77
|
-
"@acmekit/locking-postgres": "2.13.
|
|
78
|
-
"@acmekit/locking-redis": "2.13.
|
|
79
|
-
"@acmekit/notification": "2.13.
|
|
80
|
-
"@acmekit/notification-local": "2.13.
|
|
81
|
-
"@acmekit/notification-sendgrid": "2.13.
|
|
82
|
-
"@acmekit/rbac": "2.13.
|
|
83
|
-
"@acmekit/secrets-aws": "2.13.
|
|
84
|
-
"@acmekit/secrets-local": "2.13.
|
|
85
|
-
"@acmekit/settings": "2.13.
|
|
86
|
-
"@acmekit/telemetry": "2.13.
|
|
87
|
-
"@acmekit/translation": "2.13.
|
|
88
|
-
"@acmekit/user": "2.13.
|
|
89
|
-
"@acmekit/workflow-engine-inmemory": "2.13.
|
|
90
|
-
"@acmekit/workflow-engine-redis": "2.13.
|
|
55
|
+
"@acmekit/admin-bundler": "2.13.85",
|
|
56
|
+
"@acmekit/analytics": "2.13.85",
|
|
57
|
+
"@acmekit/analytics-local": "2.13.85",
|
|
58
|
+
"@acmekit/analytics-posthog": "2.13.85",
|
|
59
|
+
"@acmekit/api-key": "2.13.85",
|
|
60
|
+
"@acmekit/auth": "2.13.85",
|
|
61
|
+
"@acmekit/auth-emailpass": "2.13.85",
|
|
62
|
+
"@acmekit/auth-github": "2.13.85",
|
|
63
|
+
"@acmekit/auth-google": "2.13.85",
|
|
64
|
+
"@acmekit/cache-inmemory": "2.13.85",
|
|
65
|
+
"@acmekit/cache-redis": "2.13.85",
|
|
66
|
+
"@acmekit/caching": "2.13.85",
|
|
67
|
+
"@acmekit/caching-redis": "2.13.85",
|
|
68
|
+
"@acmekit/core-flows": "2.13.85",
|
|
69
|
+
"@acmekit/event-bus-local": "2.13.85",
|
|
70
|
+
"@acmekit/event-bus-redis": "2.13.85",
|
|
71
|
+
"@acmekit/file": "2.13.85",
|
|
72
|
+
"@acmekit/file-local": "2.13.85",
|
|
73
|
+
"@acmekit/file-s3": "2.13.85",
|
|
74
|
+
"@acmekit/index": "2.13.85",
|
|
75
|
+
"@acmekit/link-modules": "2.13.85",
|
|
76
|
+
"@acmekit/locking": "2.13.85",
|
|
77
|
+
"@acmekit/locking-postgres": "2.13.85",
|
|
78
|
+
"@acmekit/locking-redis": "2.13.85",
|
|
79
|
+
"@acmekit/notification": "2.13.85",
|
|
80
|
+
"@acmekit/notification-local": "2.13.85",
|
|
81
|
+
"@acmekit/notification-sendgrid": "2.13.85",
|
|
82
|
+
"@acmekit/rbac": "2.13.85",
|
|
83
|
+
"@acmekit/secrets-aws": "2.13.85",
|
|
84
|
+
"@acmekit/secrets-local": "2.13.85",
|
|
85
|
+
"@acmekit/settings": "2.13.85",
|
|
86
|
+
"@acmekit/telemetry": "2.13.85",
|
|
87
|
+
"@acmekit/translation": "2.13.85",
|
|
88
|
+
"@acmekit/user": "2.13.85",
|
|
89
|
+
"@acmekit/workflow-engine-inmemory": "2.13.85",
|
|
90
|
+
"@acmekit/workflow-engine-redis": "2.13.85",
|
|
91
91
|
"@inquirer/checkbox": "^2.3.11",
|
|
92
92
|
"@inquirer/input": "^2.2.9",
|
|
93
93
|
"boxen": "^5.0.1",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
108
108
|
"@acmekit/docs-bundler": "^2.13.42",
|
|
109
|
-
"@acmekit/framework": "2.13.
|
|
109
|
+
"@acmekit/framework": "2.13.85",
|
|
110
110
|
"@jimsheen/yalc": "^1.2.2",
|
|
111
111
|
"@swc/core": "^1.7.28",
|
|
112
112
|
"posthog-node": "^5.11.0",
|