@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.
@@ -1,225 +1,342 @@
1
1
  ---
2
2
  name: write-test
3
- description: "Generates integration or unit tests using the correct AcmeKit test runner. Auto-detects whether to use pluginIntegrationTestRunner (full plugin tests) or moduleIntegrationTestRunner (module services). Use when adding tests for modules, workflows, API routes, or service methods."
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 test runner and patterns.
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. **Service resolution key = module constant.** `container.resolve(BLOG_MODULE)` where `BLOG_MODULE = "blog"` (the string passed to `Module()`). NEVER guess `"blogModuleService"` — it won't resolve (AwilixResolutionError).
22
- 2. **`moduleName` in `moduleIntegrationTestRunner` = module constant.** Same rule: `moduleName: MY_MODULE` where `MY_MODULE` matches `Module()`. NEVER use `"myModuleService"`.
23
- 3. **`resolve` in `moduleIntegrationTestRunner` = string path from CWD.** Use `resolve: "./src/modules/my-module"`. NEVER pass the imported module object (`resolve: BlogModule` hangs — models not found).
24
- 4. **Workflow errors are plain objects, not Error instances.** NEVER use `.rejects.toThrow()` on workflows — always fails. Use `throwOnError: false` + `errors` array. Error path: `errors[0].error.message` (NOT `errors[0].message`).
25
- 5. **`.rejects.toThrow()` DOES work for service errors** (e.g., `service.retrievePost("bad-id")`). Only workflow errors are serialized.
26
- 6. **Schema sync, not migrations.** `pluginIntegrationTestRunner` and `moduleIntegrationTestRunner` sync schema from entities no migration files needed.
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 | Runner | File location |
34
+ | What to test | Mode | File location |
33
35
  |---|---|---|
34
- | Full plugin (all resources loaded) | `pluginIntegrationTestRunner` | `integration-tests/plugin/<feature>.spec.ts` |
35
- | Module service methods (isolated) | `moduleIntegrationTestRunner` | `src/modules/<mod>/__tests__/<name>.spec.ts` |
36
- | Workflows (through container) | `pluginIntegrationTestRunner` | `integration-tests/plugin/<feature>.spec.ts` |
37
- | Pure functions | Plain Jest `describe/it` | `src/**/__tests__/<name>.unit.spec.ts` |
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 { pluginIntegrationTestRunner } from "@acmekit/test-utils"
45
- import { plugin } from "../../src/plugin"
46
- import { BLOG_MODULE } from "../../src/modules/blog" // BLOG_MODULE = "blog" — must match Module() key
47
-
48
- jest.setTimeout(60 * 1000)
49
-
50
- pluginIntegrationTestRunner({
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
- apiKey: "test-api-key",
54
- },
55
- testSuite: ({ container, acmekitApp }) => {
56
- describe("Plugin loading", () => {
57
- it("should load plugin modules", () => {
58
- expect(acmekitApp.modules).toBeDefined()
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
- it("should resolve typed plugin options", () => {
62
- const options = plugin.resolveOptions(container)
63
- expect(options.apiKey).toBe("test-api-key")
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
- beforeEach(() => {
71
- service = container.resolve(BLOG_MODULE) // resolves via module key
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
- it("should create a blog post", async () => {
75
- const result = await service.createBlogPosts([
76
- { title: "Launch Announcement" },
77
- ])
78
- expect(result).toHaveLength(1)
79
- expect(result[0]).toEqual(
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
- title: "Launch Announcement",
124
+ message: "Hello from HTTP",
83
125
  })
84
126
  )
85
127
  })
86
128
 
87
- it("should list and count", async () => {
88
- await service.createBlogPosts([{ title: "A" }, { title: "B" }])
89
- const [posts, count] = await service.listAndCountBlogPosts()
90
- expect(count).toBe(2)
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
- it("should throw on missing required field", async () => {
94
- await expect(service.createBlogPosts([{}])).rejects.toThrow()
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
- **Fixtures:** `container` (proxy), `acmekitApp`, `MikroOrmWrapper`, `dbConfig`.
178
+ #### Plugin Container Integration Test (workflows, subscribers, jobs)
102
179
 
103
- #### Module Integration Test (Isolated)
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 { moduleIntegrationTestRunner } from "@acmekit/test-utils"
107
- import { BLOG_MODULE } from "../../src/modules/blog" // BLOG_MODULE = "blog" — must match Module() key
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(30000)
219
+ jest.setTimeout(60 * 1000)
110
220
 
111
- moduleIntegrationTestRunner<IBlogModuleService>({
112
- moduleName: BLOG_MODULE, // must match Module() key
113
- resolve: "./src/modules/blog", // string path from CWD — NEVER pass imported module object
114
- testSuite: ({ service }) => {
115
- it("should create a blog post", async () => {
116
- const result = await service.createBlogPosts([
117
- { title: "Quarterly Report" },
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
- **Fixtures:** `service` (proxy), `MikroOrmWrapper`, `acmekitApp`, `dbConfig`.
128
-
129
- #### Unit Test
246
+ #### Job Test (direct invocation)
130
247
 
131
248
  ```typescript
132
- import { formatSlug } from "../format-slug"
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
- describe("formatSlug", () => {
135
- it("should kebab-case the title", () => {
136
- expect(formatSlug("Hello World")).toBe("hello-world")
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
- #### Testing Workflows Through Container
278
+ #### Unit Test (No Framework Bootstrap)
142
279
 
143
- ```typescript
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
- // Direct execution
147
- it("should execute the workflow", async () => {
148
- const { result } = await createBlogPostWorkflow(container).run({
149
- input: { title: "Workflow Test", author: "Admin" },
150
- })
151
- expect(result.blogPost).toEqual(
152
- expect.objectContaining({ title: "Workflow Test" })
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
- it("should reject invalid workflow input", async () => {
157
- const { errors } = await createBlogPostWorkflow(container).run({
158
- input: {},
159
- throwOnError: false,
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
- // Typed errors — PermanentFailure stops retries, SkipStep skips gracefully
166
- it("should permanently fail on invalid card", async () => {
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 skip optional step when disabled", async () => {
175
- const { result } = await processOrderWorkflow(container).run({
176
- input: { items: [{ id: "item-1" }] },
177
- throwOnError: false,
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
- #### Testing Domain Events
315
+ **Timer mocking:** Use `jest.useFakeTimers()` + `jest.advanceTimersByTimeAsync()` or `jest.spyOn(instance, "sleep_").mockResolvedValue(undefined)`.
185
316
 
186
- ```typescript
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
- #### Testing Subscribers
319
+ #### Module Integration Test (isolated)
207
320
 
208
321
  ```typescript
209
- import { TestEventUtils } from "@acmekit/test-utils"
210
- import { Modules } from "@acmekit/framework/utils"
211
-
212
- it("should execute subscriber side-effect", async () => {
213
- const eventBus = container.resolve(Modules.EVENT_BUS)
214
- // IMPORTANT: capture promise BEFORE triggering event
215
- const subscriberExecution = TestEventUtils.waitSubscribersExecution(
216
- "blog-post.created",
217
- eventBus
218
- )
219
- const service = container.resolve(BLOG_MODULE) // "blog" — from Module() key
220
- await service.createBlogPosts([{ title: "Trigger Subscriber" }])
221
- await subscriberExecution
222
- // assert subscriber side-effect
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 # Full plugin integration tests
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
- - All integration tests require `NODE_OPTIONS=--experimental-vm-modules`
238
- - Runners handle DB setup/teardown automatically — no manual cleanup needed
239
- - NEVER use `api.get()` or `api.post()` — plugin tests have no HTTP server
240
- - Always use `pluginPath: process.cwd()` unless the repo structure requires a different path
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
- - Call `waitSubscribersExecution` BEFORE triggering the event
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.83",
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.83"
52
+ "@acmekit/framework": "2.13.85"
53
53
  },
54
54
  "dependencies": {
55
- "@acmekit/admin-bundler": "2.13.83",
56
- "@acmekit/analytics": "2.13.83",
57
- "@acmekit/analytics-local": "2.13.83",
58
- "@acmekit/analytics-posthog": "2.13.83",
59
- "@acmekit/api-key": "2.13.83",
60
- "@acmekit/auth": "2.13.83",
61
- "@acmekit/auth-emailpass": "2.13.83",
62
- "@acmekit/auth-github": "2.13.83",
63
- "@acmekit/auth-google": "2.13.83",
64
- "@acmekit/cache-inmemory": "2.13.83",
65
- "@acmekit/cache-redis": "2.13.83",
66
- "@acmekit/caching": "2.13.83",
67
- "@acmekit/caching-redis": "2.13.83",
68
- "@acmekit/core-flows": "2.13.83",
69
- "@acmekit/event-bus-local": "2.13.83",
70
- "@acmekit/event-bus-redis": "2.13.83",
71
- "@acmekit/file": "2.13.83",
72
- "@acmekit/file-local": "2.13.83",
73
- "@acmekit/file-s3": "2.13.83",
74
- "@acmekit/index": "2.13.83",
75
- "@acmekit/link-modules": "2.13.83",
76
- "@acmekit/locking": "2.13.83",
77
- "@acmekit/locking-postgres": "2.13.83",
78
- "@acmekit/locking-redis": "2.13.83",
79
- "@acmekit/notification": "2.13.83",
80
- "@acmekit/notification-local": "2.13.83",
81
- "@acmekit/notification-sendgrid": "2.13.83",
82
- "@acmekit/rbac": "2.13.83",
83
- "@acmekit/secrets-aws": "2.13.83",
84
- "@acmekit/secrets-local": "2.13.83",
85
- "@acmekit/settings": "2.13.83",
86
- "@acmekit/telemetry": "2.13.83",
87
- "@acmekit/translation": "2.13.83",
88
- "@acmekit/user": "2.13.83",
89
- "@acmekit/workflow-engine-inmemory": "2.13.83",
90
- "@acmekit/workflow-engine-redis": "2.13.83",
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.83",
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",