@acmekit/acmekit 2.13.84 → 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 +72 -0
- package/dist/templates/app/.claude/rules/testing.md +223 -3
- package/dist/templates/app/.claude/skills/write-test/SKILL.md +46 -0
- package/dist/templates/plugin/.claude/agents/test-writer.md +81 -0
- package/dist/templates/plugin/.claude/rules/testing.md +267 -0
- package/dist/templates/plugin/.claude/skills/write-test/SKILL.md +48 -0
- package/package.json +39 -39
|
@@ -382,6 +382,74 @@ integrationTestRunner<IPostModuleService>({
|
|
|
382
382
|
|
|
383
383
|
---
|
|
384
384
|
|
|
385
|
+
## Unit Test Template (No Framework Bootstrap)
|
|
386
|
+
|
|
387
|
+
For providers, utilities, and standalone classes. Uses plain Jest with `jest.mock`.
|
|
388
|
+
|
|
389
|
+
**CRITICAL — jest.mock hoisting:** `jest.mock()` factories are hoisted above `const`/`let` by SWC. Never reference file-level variables inside a factory. Create mocks INSIDE the factory and access via `require()`.
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
// Provider unit test pattern
|
|
393
|
+
jest.mock("external-sdk", () => {
|
|
394
|
+
const mocks = {
|
|
395
|
+
doThing: jest.fn(),
|
|
396
|
+
}
|
|
397
|
+
const MockClient = jest.fn().mockImplementation(() => ({
|
|
398
|
+
doThing: mocks.doThing,
|
|
399
|
+
}))
|
|
400
|
+
return { Client: MockClient, __mocks: mocks }
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
const { __mocks: sdkMocks } = require("external-sdk")
|
|
404
|
+
|
|
405
|
+
import MyProvider from "../my-provider"
|
|
406
|
+
|
|
407
|
+
describe("MyProvider", () => {
|
|
408
|
+
let provider: MyProvider
|
|
409
|
+
const mockContainer = {} as any
|
|
410
|
+
const defaultOptions = { apiKey: "test-key" }
|
|
411
|
+
|
|
412
|
+
beforeEach(() => {
|
|
413
|
+
jest.clearAllMocks()
|
|
414
|
+
provider = new MyProvider(mockContainer, defaultOptions)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
describe("static identifier", () => {
|
|
418
|
+
it("should have correct identifier", () => {
|
|
419
|
+
expect(MyProvider.identifier).toBe("my-provider")
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
describe("validateOptions", () => {
|
|
424
|
+
it("should accept valid options", () => {
|
|
425
|
+
expect(() =>
|
|
426
|
+
MyProvider.validateOptions({ apiKey: "key" })
|
|
427
|
+
).not.toThrow()
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it("should reject missing required option", () => {
|
|
431
|
+
expect(() => MyProvider.validateOptions({})).toThrow()
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
describe("doSomething", () => {
|
|
436
|
+
it("should delegate to SDK", async () => {
|
|
437
|
+
sdkMocks.doThing.mockResolvedValue({ success: true })
|
|
438
|
+
const result = await provider.doSomething({ input: "test" })
|
|
439
|
+
expect(result.success).toBe(true)
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Timer mocking:** If code under test uses `setTimeout` or `sleep()`, use `jest.useFakeTimers()` + `jest.advanceTimersByTimeAsync()` or mock the sleep method.
|
|
446
|
+
|
|
447
|
+
**SWC regex:** Complex regex literals may fail. Use `new RegExp("...")` instead.
|
|
448
|
+
|
|
449
|
+
**Error paths:** Read the implementation to check whether errors are thrown or returned. Don't assume from the return type.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
385
453
|
## Asserting Domain Events (module mode)
|
|
386
454
|
|
|
387
455
|
```typescript
|
|
@@ -486,3 +554,7 @@ it("should emit post.created event", async () => {
|
|
|
486
554
|
- Direct workflow execution: `workflow(getContainer()).run({ input })`
|
|
487
555
|
- `waitSubscribersExecution` promise BEFORE triggering event
|
|
488
556
|
- NEVER use JSDoc blocks or type casts in test files
|
|
557
|
+
- **Always `beforeEach(() => jest.clearAllMocks())`** in unit tests — mock state leaks between describes
|
|
558
|
+
- **Never reference file-level `const`/`let` inside `jest.mock()` factories** — TDZ error
|
|
559
|
+
- **Mock timers or sleep** when code under test has delays — prevents timeouts
|
|
560
|
+
- **Use `new RegExp()` over complex regex literals** — SWC parser has edge cases
|
|
@@ -22,6 +22,10 @@ paths:
|
|
|
22
22
|
|
|
23
23
|
**`.rejects.toThrow()` DOES work for service errors** (e.g., `service.retrievePost("bad-id")`). Services throw real `Error` instances. Only workflow errors are serialized.
|
|
24
24
|
|
|
25
|
+
**jest.mock factories are hoisted.** `jest.mock()` runs BEFORE `const`/`let` declarations. Never reference file-level variables inside a `jest.mock()` factory — see "Unit Tests" section below.
|
|
26
|
+
|
|
27
|
+
**Always add `jest.clearAllMocks()` in `beforeEach`.** Mock state leaks between test blocks. Without explicit cleanup, assertions on mock call counts fail from prior-test contamination.
|
|
28
|
+
|
|
25
29
|
**Inline auth setup.** There is no `createAdminUser` helper. Resolve `Modules.USER`, `Modules.AUTH`, `Modules.API_KEY` from the container and create credentials directly in `beforeEach`.
|
|
26
30
|
|
|
27
31
|
---
|
|
@@ -248,11 +252,10 @@ integrationTestRunner({
|
|
|
248
252
|
### HTTP test lifecycle
|
|
249
253
|
|
|
250
254
|
- `beforeAll`: boots full Express app (resolves plugins, runs migrations, starts HTTP server)
|
|
251
|
-
- `beforeEach`: truncates all tables, re-runs module loaders, runs `createDefaultsWorkflow`
|
|
252
|
-
- `afterEach`: **automatically calls `waitWorkflowExecutions()`** then `dbUtils.teardown()`
|
|
255
|
+
- `beforeEach`: **automatically calls `waitWorkflowExecutions()`** from the previous test, then truncates all tables, re-runs module loaders, runs `createDefaultsWorkflow` (first test skips reset — state is already fresh after setup)
|
|
253
256
|
- `afterAll`: drops DB, shuts down Express
|
|
254
257
|
|
|
255
|
-
**IMPORTANT:** The runner calls `waitWorkflowExecutions()` automatically
|
|
258
|
+
**IMPORTANT:** The runner calls `waitWorkflowExecutions()` automatically during the reset phase (before each subsequent test). You do NOT need to call it in your tests unless you need workflow results BETWEEN two API calls in the same test.
|
|
256
259
|
|
|
257
260
|
---
|
|
258
261
|
|
|
@@ -719,6 +722,188 @@ getContainer().resolve("blogModuleService") // ❌
|
|
|
719
722
|
|
|
720
723
|
---
|
|
721
724
|
|
|
725
|
+
## Unit Tests (No Framework Bootstrap)
|
|
726
|
+
|
|
727
|
+
For providers, utility functions, and standalone classes that don't need the database or AcmeKit container. Uses plain Jest — no `integrationTestRunner`.
|
|
728
|
+
|
|
729
|
+
**File naming:** `src/**/__tests__/<name>.unit.spec.ts` — matches `TEST_TYPE=unit` in `jest.config.js`.
|
|
730
|
+
|
|
731
|
+
### jest.mock Hoisting (Temporal Dead Zone)
|
|
732
|
+
|
|
733
|
+
`jest.mock()` factories are **hoisted above all `const`/`let` declarations** by SWC/Babel. Referencing a file-level `const` inside a `jest.mock()` factory causes `ReferenceError: Cannot access before initialization`.
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
// WRONG — TDZ error: mockSign is not yet initialized when factory runs
|
|
737
|
+
const mockSign = jest.fn()
|
|
738
|
+
jest.mock("tronweb", () => ({
|
|
739
|
+
TronWeb: jest.fn().mockImplementation(() => ({ trx: { sign: mockSign } })),
|
|
740
|
+
}))
|
|
741
|
+
|
|
742
|
+
// RIGHT — create mocks INSIDE the factory, expose via module return
|
|
743
|
+
jest.mock("tronweb", () => {
|
|
744
|
+
const mocks = {
|
|
745
|
+
sign: jest.fn(),
|
|
746
|
+
isAddress: jest.fn().mockReturnValue(true),
|
|
747
|
+
}
|
|
748
|
+
const MockTronWeb = jest.fn().mockImplementation(() => ({
|
|
749
|
+
trx: { sign: mocks.sign },
|
|
750
|
+
}))
|
|
751
|
+
MockTronWeb.isAddress = mocks.isAddress
|
|
752
|
+
return { TronWeb: MockTronWeb, __mocks: mocks }
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
// Access mocks after jest.mock via require()
|
|
756
|
+
const { __mocks: tronMocks } = require("tronweb")
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
**Rule:** All mock state must live INSIDE the `jest.mock()` factory or be accessed via `require()` after the mock is set up. Never reference file-level `const`/`let` from inside a `jest.mock()` factory.
|
|
760
|
+
|
|
761
|
+
### Provider Unit Test Pattern
|
|
762
|
+
|
|
763
|
+
Providers have a specific structure: constructor receives `(container, options)`, static `identifier`, optional static `validateOptions`. Test each part:
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
jest.mock("external-sdk", () => {
|
|
767
|
+
const mocks = {
|
|
768
|
+
doThing: jest.fn(),
|
|
769
|
+
}
|
|
770
|
+
const MockClient = jest.fn().mockImplementation(() => ({
|
|
771
|
+
doThing: mocks.doThing,
|
|
772
|
+
}))
|
|
773
|
+
return { Client: MockClient, __mocks: mocks }
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
const { __mocks: sdkMocks } = require("external-sdk")
|
|
777
|
+
|
|
778
|
+
import MyProvider from "../my-provider"
|
|
779
|
+
|
|
780
|
+
describe("MyProvider", () => {
|
|
781
|
+
let provider: MyProvider
|
|
782
|
+
|
|
783
|
+
const mockContainer = {} as any
|
|
784
|
+
const defaultOptions = { apiKey: "test-key" }
|
|
785
|
+
|
|
786
|
+
beforeEach(() => {
|
|
787
|
+
jest.clearAllMocks()
|
|
788
|
+
provider = new MyProvider(mockContainer, defaultOptions)
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
describe("static identifier", () => {
|
|
792
|
+
it("should have correct identifier", () => {
|
|
793
|
+
expect(MyProvider.identifier).toBe("my-provider")
|
|
794
|
+
})
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
describe("validateOptions", () => {
|
|
798
|
+
it("should accept valid options", () => {
|
|
799
|
+
expect(() =>
|
|
800
|
+
MyProvider.validateOptions({ apiKey: "key" })
|
|
801
|
+
).not.toThrow()
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
it("should reject missing required option", () => {
|
|
805
|
+
expect(() => MyProvider.validateOptions({})).toThrow()
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
describe("doSomething", () => {
|
|
810
|
+
it("should delegate to SDK", async () => {
|
|
811
|
+
sdkMocks.doThing.mockResolvedValue({ success: true })
|
|
812
|
+
const result = await provider.doSomething({ input: "test" })
|
|
813
|
+
expect(result.success).toBe(true)
|
|
814
|
+
expect(sdkMocks.doThing).toHaveBeenCalledWith(
|
|
815
|
+
expect.objectContaining({ input: "test" })
|
|
816
|
+
)
|
|
817
|
+
})
|
|
818
|
+
})
|
|
819
|
+
})
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### Mock Cleanup Between Tests
|
|
823
|
+
|
|
824
|
+
Mock state leaks between `describe` and `it` blocks. **Always add cleanup:**
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
// Recommended: file-level cleanup
|
|
828
|
+
beforeEach(() => {
|
|
829
|
+
jest.clearAllMocks()
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
// Alternative: per-describe when different describes need different setups
|
|
833
|
+
describe("feature A", () => {
|
|
834
|
+
beforeEach(() => {
|
|
835
|
+
jest.clearAllMocks()
|
|
836
|
+
mockFn.mockResolvedValue("A result")
|
|
837
|
+
})
|
|
838
|
+
})
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
Without `jest.clearAllMocks()`, a mock called in one test still shows those calls in the next test:
|
|
842
|
+
|
|
843
|
+
```typescript
|
|
844
|
+
expect(mockSign).not.toHaveBeenCalled() // FAILS — called by prior test
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Testing Code with Timers
|
|
848
|
+
|
|
849
|
+
When code under test calls `setTimeout`, `setInterval`, or a `sleep()` function, tests time out or run slowly.
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
// Option 1: Fake timers (for setTimeout/setInterval)
|
|
853
|
+
beforeEach(() => {
|
|
854
|
+
jest.useFakeTimers()
|
|
855
|
+
})
|
|
856
|
+
afterEach(() => {
|
|
857
|
+
jest.useRealTimers()
|
|
858
|
+
})
|
|
859
|
+
it("should retry after delay", async () => {
|
|
860
|
+
const promise = provider.retryOperation()
|
|
861
|
+
await jest.advanceTimersByTimeAsync(3000)
|
|
862
|
+
const result = await promise
|
|
863
|
+
expect(result).toBeDefined()
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
// Option 2: Mock the sleep method (for custom sleep/delay functions)
|
|
867
|
+
it("should complete without waiting", async () => {
|
|
868
|
+
jest.spyOn(provider as any, "sleep_").mockResolvedValue(undefined)
|
|
869
|
+
const result = await provider.longRunningOperation()
|
|
870
|
+
expect(result).toBeDefined()
|
|
871
|
+
})
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### SWC Regex Limitation
|
|
875
|
+
|
|
876
|
+
SWC's regex parser fails on certain complex regex literals. If you get a `Syntax Error` from SWC on a line with a regex:
|
|
877
|
+
|
|
878
|
+
```typescript
|
|
879
|
+
// WRONG — SWC may fail to parse this
|
|
880
|
+
const pattern = /^(\*|[0-9]+)(\/[0-9]+)?$|^\*\/[0-9]+$/
|
|
881
|
+
|
|
882
|
+
// RIGHT — use RegExp constructor
|
|
883
|
+
const pattern = new RegExp("^(\\*|[0-9]+)(\\/[0-9]+)?$|^\\*\\/[0-9]+$")
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Verifying Error Paths
|
|
887
|
+
|
|
888
|
+
When testing error cases, **read the implementation** to determine whether the method throws or returns an error object. Don't assume from the return type alone.
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
// If the method catches errors and returns { success: false }:
|
|
892
|
+
const result = await provider.process(badInput)
|
|
893
|
+
expect(result.success).toBe(false)
|
|
894
|
+
|
|
895
|
+
// If the method throws (no internal try/catch on that path):
|
|
896
|
+
await expect(provider.process(badInput)).rejects.toThrow("invalid")
|
|
897
|
+
|
|
898
|
+
// If validation runs BEFORE a try/catch block:
|
|
899
|
+
// validateInput() throws → not caught by the try/catch in process()
|
|
900
|
+
await expect(provider.process(badInput)).rejects.toThrow("validation")
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
**Tip:** Look for `try/catch` blocks in the implementation. Code that runs BEFORE or OUTSIDE a `try/catch` throws directly. Code INSIDE a `try/catch` may return an error result instead.
|
|
904
|
+
|
|
905
|
+
---
|
|
906
|
+
|
|
722
907
|
## Anti-Patterns — NEVER Do These
|
|
723
908
|
|
|
724
909
|
```typescript
|
|
@@ -848,4 +1033,39 @@ import { ContainerRegistrationKeys } from "@acmekit/framework/utils" // ❌ if
|
|
|
848
1033
|
|
|
849
1034
|
// WRONG — type casts in tests
|
|
850
1035
|
const filtered = (operations as Array<{ status: string }>).filter(...) // ❌
|
|
1036
|
+
|
|
1037
|
+
// WRONG — referencing file-level const/let inside jest.mock factory (TDZ)
|
|
1038
|
+
const mockFn = jest.fn()
|
|
1039
|
+
jest.mock("lib", () => ({ thing: mockFn })) // ❌ ReferenceError
|
|
1040
|
+
// RIGHT — create mocks inside factory, access via require()
|
|
1041
|
+
jest.mock("lib", () => {
|
|
1042
|
+
const mocks = { thing: jest.fn() }
|
|
1043
|
+
return { ...mocks, __mocks: mocks }
|
|
1044
|
+
})
|
|
1045
|
+
const { __mocks } = require("lib")
|
|
1046
|
+
|
|
1047
|
+
// WRONG — no mock cleanup between tests
|
|
1048
|
+
describe("A", () => { it("calls mock", () => { mockFn() }) })
|
|
1049
|
+
describe("B", () => { it("mock is clean", () => {
|
|
1050
|
+
expect(mockFn).not.toHaveBeenCalled() // ❌ fails — leaked from A
|
|
1051
|
+
}) })
|
|
1052
|
+
// RIGHT — add beforeEach(() => jest.clearAllMocks())
|
|
1053
|
+
|
|
1054
|
+
// WRONG — real timers in unit tests cause timeouts
|
|
1055
|
+
it("should process", async () => {
|
|
1056
|
+
await relay.process(data) // ❌ hangs — code calls sleep(3000) internally
|
|
1057
|
+
})
|
|
1058
|
+
// RIGHT — mock timers or sleep method
|
|
1059
|
+
jest.useFakeTimers()
|
|
1060
|
+
// or: jest.spyOn(relay as any, "sleep_").mockResolvedValue(undefined)
|
|
1061
|
+
|
|
1062
|
+
// WRONG — complex regex literal that SWC can't parse
|
|
1063
|
+
const re = /^(\*|[0-9]+)(\/[0-9]+)?$/ // ❌ SWC Syntax Error
|
|
1064
|
+
// RIGHT
|
|
1065
|
+
const re = new RegExp("^(\\*|[0-9]+)(\\/[0-9]+)?$")
|
|
1066
|
+
|
|
1067
|
+
// WRONG — assuming method returns error without reading implementation
|
|
1068
|
+
const result = await provider.process(bad)
|
|
1069
|
+
expect(result.success).toBe(false) // ❌ actually throws
|
|
1070
|
+
// RIGHT — read implementation first to check if it throws or returns
|
|
851
1071
|
```
|
|
@@ -238,6 +238,47 @@ integrationTestRunner({
|
|
|
238
238
|
})
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
+
#### Unit Test (No Framework Bootstrap)
|
|
242
|
+
|
|
243
|
+
For providers, utilities, and standalone classes. **CRITICAL:** `jest.mock()` factories are hoisted above `const`/`let`. Create mocks INSIDE the factory, access via `require()`.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
jest.mock("external-sdk", () => {
|
|
247
|
+
const mocks = { doThing: jest.fn() }
|
|
248
|
+
const MockClient = jest.fn().mockImplementation(() => ({
|
|
249
|
+
doThing: mocks.doThing,
|
|
250
|
+
}))
|
|
251
|
+
return { Client: MockClient, __mocks: mocks }
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const { __mocks: sdkMocks } = require("external-sdk")
|
|
255
|
+
|
|
256
|
+
import MyProvider from "../my-provider"
|
|
257
|
+
|
|
258
|
+
describe("MyProvider", () => {
|
|
259
|
+
const mockContainer = {} as any
|
|
260
|
+
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
jest.clearAllMocks()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it("should have correct identifier", () => {
|
|
266
|
+
expect(MyProvider.identifier).toBe("my-provider")
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it("should delegate to SDK", async () => {
|
|
270
|
+
sdkMocks.doThing.mockResolvedValue({ success: true })
|
|
271
|
+
const provider = new MyProvider(mockContainer, { apiKey: "key" })
|
|
272
|
+
const result = await provider.doSomething({ input: "test" })
|
|
273
|
+
expect(result.success).toBe(true)
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Timer mocking:** Use `jest.useFakeTimers()` + `jest.advanceTimersByTimeAsync()` or `jest.spyOn(instance, "sleep_").mockResolvedValue(undefined)`.
|
|
279
|
+
|
|
280
|
+
**SWC regex:** Use `new RegExp("...")` instead of complex regex literals.
|
|
281
|
+
|
|
241
282
|
#### Module Integration Test
|
|
242
283
|
|
|
243
284
|
```typescript
|
|
@@ -280,3 +321,8 @@ pnpm test:integration:http # HTTP integration tests
|
|
|
280
321
|
- Call `waitSubscribersExecution` BEFORE triggering the event
|
|
281
322
|
- Import jobs/subscribers directly and call with container
|
|
282
323
|
- Use realistic test data, not "test" or "foo"
|
|
324
|
+
- **Always `beforeEach(() => jest.clearAllMocks())`** in unit tests — mock state leaks between describes
|
|
325
|
+
- **Never reference file-level `const`/`let` inside `jest.mock()` factories** — TDZ error (SWC/Babel hoisting)
|
|
326
|
+
- **Mock timers or sleep** when code under test has delays — prevents test timeouts
|
|
327
|
+
- **Use `new RegExp()` over complex regex literals** — SWC parser fails on some patterns
|
|
328
|
+
- **Read implementation to verify error paths** — check if errors are thrown vs returned
|
|
@@ -258,6 +258,15 @@ integrationTestRunner({
|
|
|
258
258
|
|
|
259
259
|
**Fixtures (container-only):** `container`, `acmekitApp`, `MikroOrmWrapper`, `dbConfig`.
|
|
260
260
|
|
|
261
|
+
**When plugin depends on other plugins:** Add `skipDependencyValidation: true` and mock peer services via `injectedDependencies`.
|
|
262
|
+
|
|
263
|
+
**When plugin has providers needing options:** Use `pluginModuleOptions` keyed by module name:
|
|
264
|
+
```typescript
|
|
265
|
+
pluginModuleOptions: {
|
|
266
|
+
myModule: { providers: [{ resolve: "./src/providers/my-provider", id: "my-id", options: { apiKey: "key" } }] },
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
261
270
|
---
|
|
262
271
|
|
|
263
272
|
## Subscriber Test Template (direct handler invocation)
|
|
@@ -392,6 +401,74 @@ integrationTestRunner<IGreetingModuleService>({
|
|
|
392
401
|
|
|
393
402
|
---
|
|
394
403
|
|
|
404
|
+
## Unit Test Template (No Framework Bootstrap)
|
|
405
|
+
|
|
406
|
+
For providers, utilities, and standalone classes. Uses plain Jest with `jest.mock`.
|
|
407
|
+
|
|
408
|
+
**CRITICAL — jest.mock hoisting:** `jest.mock()` factories are hoisted above `const`/`let` by SWC. Never reference file-level variables inside a factory. Create mocks INSIDE the factory and access via `require()`.
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Provider unit test pattern
|
|
412
|
+
jest.mock("external-sdk", () => {
|
|
413
|
+
const mocks = {
|
|
414
|
+
doThing: jest.fn(),
|
|
415
|
+
}
|
|
416
|
+
const MockClient = jest.fn().mockImplementation(() => ({
|
|
417
|
+
doThing: mocks.doThing,
|
|
418
|
+
}))
|
|
419
|
+
return { Client: MockClient, __mocks: mocks }
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const { __mocks: sdkMocks } = require("external-sdk")
|
|
423
|
+
|
|
424
|
+
import MyProvider from "../my-provider"
|
|
425
|
+
|
|
426
|
+
describe("MyProvider", () => {
|
|
427
|
+
let provider: MyProvider
|
|
428
|
+
const mockContainer = {} as any
|
|
429
|
+
const defaultOptions = { apiKey: "test-key" }
|
|
430
|
+
|
|
431
|
+
beforeEach(() => {
|
|
432
|
+
jest.clearAllMocks()
|
|
433
|
+
provider = new MyProvider(mockContainer, defaultOptions)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
describe("static identifier", () => {
|
|
437
|
+
it("should have correct identifier", () => {
|
|
438
|
+
expect(MyProvider.identifier).toBe("my-provider")
|
|
439
|
+
})
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
describe("validateOptions", () => {
|
|
443
|
+
it("should accept valid options", () => {
|
|
444
|
+
expect(() =>
|
|
445
|
+
MyProvider.validateOptions({ apiKey: "key" })
|
|
446
|
+
).not.toThrow()
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it("should reject missing required option", () => {
|
|
450
|
+
expect(() => MyProvider.validateOptions({})).toThrow()
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
describe("doSomething", () => {
|
|
455
|
+
it("should delegate to SDK", async () => {
|
|
456
|
+
sdkMocks.doThing.mockResolvedValue({ success: true })
|
|
457
|
+
const result = await provider.doSomething({ input: "test" })
|
|
458
|
+
expect(result.success).toBe(true)
|
|
459
|
+
})
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Timer mocking:** If code under test uses `setTimeout` or `sleep()`, use `jest.useFakeTimers()` + `jest.advanceTimersByTimeAsync()` or mock the sleep method.
|
|
465
|
+
|
|
466
|
+
**SWC regex:** Complex regex literals may fail. Use `new RegExp("...")` instead.
|
|
467
|
+
|
|
468
|
+
**Error paths:** Read the implementation to check whether errors are thrown or returned. Don't assume from the return type.
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
395
472
|
## Asserting Domain Events (container-only mode)
|
|
396
473
|
|
|
397
474
|
Plugin mode (without HTTP) injects `MockEventBusService`. Spy on the **prototype**, not an instance.
|
|
@@ -502,3 +579,7 @@ it("should emit greeting.created event", async () => {
|
|
|
502
579
|
- Spy on `MockEventBusService.prototype` — not an instance
|
|
503
580
|
- `jest.restoreAllMocks()` in `afterEach` when spying
|
|
504
581
|
- NEVER use JSDoc blocks or type casts in test files
|
|
582
|
+
- **Always `beforeEach(() => jest.clearAllMocks())`** in unit tests — mock state leaks between describes
|
|
583
|
+
- **Never reference file-level `const`/`let` inside `jest.mock()` factories** — TDZ error
|
|
584
|
+
- **Mock timers or sleep** when code under test has delays — prevents timeouts
|
|
585
|
+
- **Use `new RegExp()` over complex regex literals** — SWC parser has edge cases
|
|
@@ -22,6 +22,10 @@ paths:
|
|
|
22
22
|
|
|
23
23
|
**`.rejects.toThrow()` DOES work for service errors** (e.g., `service.retrievePost("bad-id")`). Services throw real `Error` instances. Only workflow errors are serialized.
|
|
24
24
|
|
|
25
|
+
**jest.mock factories are hoisted.** `jest.mock()` runs BEFORE `const`/`let` declarations. Never reference file-level variables inside a `jest.mock()` factory — see "Unit Tests" section below.
|
|
26
|
+
|
|
27
|
+
**Always add `jest.clearAllMocks()` in `beforeEach`.** Mock state leaks between test blocks. Without explicit cleanup, assertions on mock call counts fail from prior-test contamination.
|
|
28
|
+
|
|
25
29
|
---
|
|
26
30
|
|
|
27
31
|
## Test Runner Selection
|
|
@@ -126,6 +130,8 @@ integrationTestRunner({
|
|
|
126
130
|
| `http` | `boolean` | `false` | Set `true` to boot full Express server for HTTP tests |
|
|
127
131
|
| `additionalModules` | `Record<string, any>` | `{}` | Extra modules to load alongside the plugin |
|
|
128
132
|
| `injectedDependencies` | `Record<string, any>` | `{}` | Mock services to register in the container |
|
|
133
|
+
| `skipDependencyValidation` | `boolean` | `false` | Skip `definePlugin({ dependencies })` validation — use when peer plugins aren't installed |
|
|
134
|
+
| `pluginModuleOptions` | `Record<string, Record<string, any>>` | `{}` | Per-module options keyed by module name (e.g., provider config) |
|
|
129
135
|
| `dbName` | `string` | auto-generated | Override the computed DB name |
|
|
130
136
|
| `schema` | `string` | `"public"` | Postgres schema |
|
|
131
137
|
| `debug` | `boolean` | `false` | Enables DB query logging |
|
|
@@ -534,6 +540,232 @@ container.resolve("auth") // ❌
|
|
|
534
540
|
|
|
535
541
|
---
|
|
536
542
|
|
|
543
|
+
## Unit Tests (No Framework Bootstrap)
|
|
544
|
+
|
|
545
|
+
For providers, utility functions, and standalone classes that don't need the database or AcmeKit container. Uses plain Jest — no `integrationTestRunner`.
|
|
546
|
+
|
|
547
|
+
**File naming:** `src/**/__tests__/<name>.unit.spec.ts` — matches `TEST_TYPE=unit` in `jest.config.js`.
|
|
548
|
+
|
|
549
|
+
### jest.mock Hoisting (Temporal Dead Zone)
|
|
550
|
+
|
|
551
|
+
`jest.mock()` factories are **hoisted above all `const`/`let` declarations** by SWC/Babel. Referencing a file-level `const` inside a `jest.mock()` factory causes `ReferenceError: Cannot access before initialization`.
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// WRONG — TDZ error: mockSign is not yet initialized when factory runs
|
|
555
|
+
const mockSign = jest.fn()
|
|
556
|
+
jest.mock("tronweb", () => ({
|
|
557
|
+
TronWeb: jest.fn().mockImplementation(() => ({ trx: { sign: mockSign } })),
|
|
558
|
+
}))
|
|
559
|
+
|
|
560
|
+
// RIGHT — create mocks INSIDE the factory, expose via module return
|
|
561
|
+
jest.mock("tronweb", () => {
|
|
562
|
+
const mocks = {
|
|
563
|
+
sign: jest.fn(),
|
|
564
|
+
isAddress: jest.fn().mockReturnValue(true),
|
|
565
|
+
}
|
|
566
|
+
const MockTronWeb = jest.fn().mockImplementation(() => ({
|
|
567
|
+
trx: { sign: mocks.sign },
|
|
568
|
+
}))
|
|
569
|
+
MockTronWeb.isAddress = mocks.isAddress
|
|
570
|
+
return { TronWeb: MockTronWeb, __mocks: mocks }
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
// Access mocks after jest.mock via require()
|
|
574
|
+
const { __mocks: tronMocks } = require("tronweb")
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Rule:** All mock state must live INSIDE the `jest.mock()` factory or be accessed via `require()` after the mock is set up. Never reference file-level `const`/`let` from inside a `jest.mock()` factory.
|
|
578
|
+
|
|
579
|
+
### Provider Unit Test Pattern
|
|
580
|
+
|
|
581
|
+
Providers have a specific structure: constructor receives `(container, options)`, static `identifier`, optional static `validateOptions`. Test each part:
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
jest.mock("external-sdk", () => {
|
|
585
|
+
const mocks = {
|
|
586
|
+
doThing: jest.fn(),
|
|
587
|
+
}
|
|
588
|
+
const MockClient = jest.fn().mockImplementation(() => ({
|
|
589
|
+
doThing: mocks.doThing,
|
|
590
|
+
}))
|
|
591
|
+
return { Client: MockClient, __mocks: mocks }
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
const { __mocks: sdkMocks } = require("external-sdk")
|
|
595
|
+
|
|
596
|
+
import MyProvider from "../my-provider"
|
|
597
|
+
|
|
598
|
+
describe("MyProvider", () => {
|
|
599
|
+
let provider: MyProvider
|
|
600
|
+
|
|
601
|
+
const mockContainer = {} as any
|
|
602
|
+
const defaultOptions = { apiKey: "test-key" }
|
|
603
|
+
|
|
604
|
+
beforeEach(() => {
|
|
605
|
+
jest.clearAllMocks()
|
|
606
|
+
provider = new MyProvider(mockContainer, defaultOptions)
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
describe("static identifier", () => {
|
|
610
|
+
it("should have correct identifier", () => {
|
|
611
|
+
expect(MyProvider.identifier).toBe("my-provider")
|
|
612
|
+
})
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
describe("validateOptions", () => {
|
|
616
|
+
it("should accept valid options", () => {
|
|
617
|
+
expect(() =>
|
|
618
|
+
MyProvider.validateOptions({ apiKey: "key" })
|
|
619
|
+
).not.toThrow()
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it("should reject missing required option", () => {
|
|
623
|
+
expect(() => MyProvider.validateOptions({})).toThrow()
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
describe("doSomething", () => {
|
|
628
|
+
it("should delegate to SDK", async () => {
|
|
629
|
+
sdkMocks.doThing.mockResolvedValue({ success: true })
|
|
630
|
+
const result = await provider.doSomething({ input: "test" })
|
|
631
|
+
expect(result.success).toBe(true)
|
|
632
|
+
expect(sdkMocks.doThing).toHaveBeenCalledWith(
|
|
633
|
+
expect.objectContaining({ input: "test" })
|
|
634
|
+
)
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Mock Cleanup Between Tests
|
|
641
|
+
|
|
642
|
+
Mock state leaks between `describe` and `it` blocks. **Always add cleanup:**
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
// Recommended: file-level cleanup
|
|
646
|
+
beforeEach(() => {
|
|
647
|
+
jest.clearAllMocks()
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
// Alternative: per-describe when different describes need different setups
|
|
651
|
+
describe("feature A", () => {
|
|
652
|
+
beforeEach(() => {
|
|
653
|
+
jest.clearAllMocks()
|
|
654
|
+
mockFn.mockResolvedValue("A result")
|
|
655
|
+
})
|
|
656
|
+
})
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Without `jest.clearAllMocks()`, a mock called in one test still shows those calls in the next test:
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
expect(mockSign).not.toHaveBeenCalled() // FAILS — called by prior test
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Testing Code with Timers
|
|
666
|
+
|
|
667
|
+
When code under test calls `setTimeout`, `setInterval`, or a `sleep()` function, tests time out or run slowly.
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// Option 1: Fake timers (for setTimeout/setInterval)
|
|
671
|
+
beforeEach(() => {
|
|
672
|
+
jest.useFakeTimers()
|
|
673
|
+
})
|
|
674
|
+
afterEach(() => {
|
|
675
|
+
jest.useRealTimers()
|
|
676
|
+
})
|
|
677
|
+
it("should retry after delay", async () => {
|
|
678
|
+
const promise = provider.retryOperation()
|
|
679
|
+
await jest.advanceTimersByTimeAsync(3000)
|
|
680
|
+
const result = await promise
|
|
681
|
+
expect(result).toBeDefined()
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
// Option 2: Mock the sleep method (for custom sleep/delay functions)
|
|
685
|
+
it("should complete without waiting", async () => {
|
|
686
|
+
jest.spyOn(provider as any, "sleep_").mockResolvedValue(undefined)
|
|
687
|
+
const result = await provider.longRunningOperation()
|
|
688
|
+
expect(result).toBeDefined()
|
|
689
|
+
})
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### SWC Regex Limitation
|
|
693
|
+
|
|
694
|
+
SWC's regex parser fails on certain complex regex literals. If you get a `Syntax Error` from SWC on a line with a regex:
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
// WRONG — SWC may fail to parse this
|
|
698
|
+
const pattern = /^(\*|[0-9]+)(\/[0-9]+)?$|^\*\/[0-9]+$/
|
|
699
|
+
|
|
700
|
+
// RIGHT — use RegExp constructor
|
|
701
|
+
const pattern = new RegExp("^(\\*|[0-9]+)(\\/[0-9]+)?$|^\\*\\/[0-9]+$")
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Verifying Error Paths
|
|
705
|
+
|
|
706
|
+
When testing error cases, **read the implementation** to determine whether the method throws or returns an error object. Don't assume from the return type alone.
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
// If the method catches errors and returns { success: false }:
|
|
710
|
+
const result = await provider.process(badInput)
|
|
711
|
+
expect(result.success).toBe(false)
|
|
712
|
+
|
|
713
|
+
// If the method throws (no internal try/catch on that path):
|
|
714
|
+
await expect(provider.process(badInput)).rejects.toThrow("invalid")
|
|
715
|
+
|
|
716
|
+
// If validation runs BEFORE a try/catch block:
|
|
717
|
+
// validateInput() throws → not caught by the try/catch in process()
|
|
718
|
+
await expect(provider.process(badInput)).rejects.toThrow("validation")
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**Tip:** Look for `try/catch` blocks in the implementation. Code that runs BEFORE or OUTSIDE a `try/catch` throws directly. Code INSIDE a `try/catch` may return an error result instead.
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## Testing Plugins with Dependencies
|
|
726
|
+
|
|
727
|
+
When your plugin depends on other plugins (declared in `definePlugin({ dependencies })`), the test runner validates that all dependencies are installed. For workspace plugins that depend on other workspace plugins, use `skipDependencyValidation`:
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
integrationTestRunner({
|
|
731
|
+
mode: "plugin",
|
|
732
|
+
pluginPath: process.cwd(),
|
|
733
|
+
skipDependencyValidation: true,
|
|
734
|
+
injectedDependencies: {
|
|
735
|
+
// Mock services your plugin expects from peer plugins
|
|
736
|
+
peerModuleService: { list: jest.fn().mockResolvedValue([]) },
|
|
737
|
+
},
|
|
738
|
+
testSuite: ({ container }) => { ... },
|
|
739
|
+
})
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
## Configuring Providers in Plugin Tests
|
|
743
|
+
|
|
744
|
+
Use `pluginModuleOptions` to pass per-module options (e.g., provider configuration) keyed by module name:
|
|
745
|
+
|
|
746
|
+
```typescript
|
|
747
|
+
integrationTestRunner({
|
|
748
|
+
mode: "plugin",
|
|
749
|
+
pluginPath: process.cwd(),
|
|
750
|
+
pluginModuleOptions: {
|
|
751
|
+
tron: {
|
|
752
|
+
providers: [
|
|
753
|
+
{
|
|
754
|
+
resolve: "./src/providers/energy/own-pool",
|
|
755
|
+
id: "own-pool",
|
|
756
|
+
options: { apiKey: "test-key" },
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
testSuite: ({ container }) => { ... },
|
|
762
|
+
})
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
`pluginModuleOptions` is merged into the discovered module config AFTER `pluginOptions`, so module-specific options override plugin-level ones.
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
537
769
|
## Anti-Patterns — NEVER Do These
|
|
538
770
|
|
|
539
771
|
```typescript
|
|
@@ -624,4 +856,39 @@ import jwt from "jsonwebtoken" // ❌
|
|
|
624
856
|
{ [CLIENT_API_KEY_HEADER]: token }
|
|
625
857
|
|
|
626
858
|
// WRONG — unused imports, JSDoc blocks, type casts in tests
|
|
859
|
+
|
|
860
|
+
// WRONG — referencing file-level const/let inside jest.mock factory (TDZ)
|
|
861
|
+
const mockFn = jest.fn()
|
|
862
|
+
jest.mock("lib", () => ({ thing: mockFn })) // ❌ ReferenceError
|
|
863
|
+
// RIGHT — create mocks inside factory, access via require()
|
|
864
|
+
jest.mock("lib", () => {
|
|
865
|
+
const mocks = { thing: jest.fn() }
|
|
866
|
+
return { ...mocks, __mocks: mocks }
|
|
867
|
+
})
|
|
868
|
+
const { __mocks } = require("lib")
|
|
869
|
+
|
|
870
|
+
// WRONG — no mock cleanup between tests
|
|
871
|
+
describe("A", () => { it("calls mock", () => { mockFn() }) })
|
|
872
|
+
describe("B", () => { it("mock is clean", () => {
|
|
873
|
+
expect(mockFn).not.toHaveBeenCalled() // ❌ fails — leaked from A
|
|
874
|
+
}) })
|
|
875
|
+
// RIGHT — add beforeEach(() => jest.clearAllMocks())
|
|
876
|
+
|
|
877
|
+
// WRONG — real timers in unit tests cause timeouts
|
|
878
|
+
it("should process", async () => {
|
|
879
|
+
await relay.process(data) // ❌ hangs — code calls sleep(3000) internally
|
|
880
|
+
})
|
|
881
|
+
// RIGHT — mock timers or sleep method
|
|
882
|
+
jest.useFakeTimers()
|
|
883
|
+
// or: jest.spyOn(relay as any, "sleep_").mockResolvedValue(undefined)
|
|
884
|
+
|
|
885
|
+
// WRONG — complex regex literal that SWC can't parse
|
|
886
|
+
const re = /^(\*|[0-9]+)(\/[0-9]+)?$/ // ❌ SWC Syntax Error
|
|
887
|
+
// RIGHT
|
|
888
|
+
const re = new RegExp("^(\\*|[0-9]+)(\\/[0-9]+)?$")
|
|
889
|
+
|
|
890
|
+
// WRONG — assuming method returns error without reading implementation
|
|
891
|
+
const result = await provider.process(bad)
|
|
892
|
+
expect(result.success).toBe(false) // ❌ actually throws
|
|
893
|
+
// RIGHT — read implementation first to check if it throws or returns
|
|
627
894
|
```
|
|
@@ -275,6 +275,47 @@ integrationTestRunner({
|
|
|
275
275
|
})
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
+
#### Unit Test (No Framework Bootstrap)
|
|
279
|
+
|
|
280
|
+
For providers, utilities, and standalone classes. **CRITICAL:** `jest.mock()` factories are hoisted above `const`/`let`. Create mocks INSIDE the factory, access via `require()`.
|
|
281
|
+
|
|
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 }
|
|
289
|
+
})
|
|
290
|
+
|
|
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()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it("should have correct identifier", () => {
|
|
303
|
+
expect(MyProvider.identifier).toBe("my-provider")
|
|
304
|
+
})
|
|
305
|
+
|
|
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)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Timer mocking:** Use `jest.useFakeTimers()` + `jest.advanceTimersByTimeAsync()` or `jest.spyOn(instance, "sleep_").mockResolvedValue(undefined)`.
|
|
316
|
+
|
|
317
|
+
**SWC regex:** Use `new RegExp("...")` instead of complex regex literals.
|
|
318
|
+
|
|
278
319
|
#### Module Integration Test (isolated)
|
|
279
320
|
|
|
280
321
|
```typescript
|
|
@@ -315,9 +356,16 @@ pnpm test:integration:http # Plugin HTTP tests
|
|
|
315
356
|
- Always use `pluginPath: process.cwd()` — never hardcode paths
|
|
316
357
|
- Container-only tests: access services via `container.resolve(MODULE_CONSTANT)` — no `api` fixture
|
|
317
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: [...] } }`
|
|
318
361
|
- Pass body directly: `api.post(url, body, headers)` — NOT `{ body: {...} }`
|
|
319
362
|
- Use `.catch((e: any) => e)` for error assertions — axios throws on 4xx/5xx
|
|
320
363
|
- Use `expect.objectContaining()` with `expect.any(String)` for IDs/timestamps
|
|
321
364
|
- Test subscribers by importing handler directly and calling with `{ event, container }`
|
|
322
365
|
- Test jobs by importing function directly and calling with `container`
|
|
323
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",
|