@acmekit/acmekit 2.13.85 → 2.13.87
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 +10 -14
- package/dist/templates/app/.claude/rules/testing.md +53 -15
- package/dist/templates/plugin/.claude/agents/test-writer.md +13 -20
- package/dist/templates/plugin/.claude/rules/testing.md +160 -21
- package/dist/templates/plugin/.claude/skills/write-test/SKILL.md +1 -1
- package/package.json +39 -39
|
@@ -450,29 +450,23 @@ describe("MyProvider", () => {
|
|
|
450
450
|
|
|
451
451
|
---
|
|
452
452
|
|
|
453
|
-
## Asserting Domain Events
|
|
453
|
+
## Asserting Domain Events
|
|
454
454
|
|
|
455
|
-
|
|
456
|
-
import { MockEventBusService } from "@acmekit/test-utils"
|
|
457
|
-
|
|
458
|
-
let eventBusSpy: jest.SpyInstance
|
|
459
|
-
|
|
460
|
-
beforeEach(() => {
|
|
461
|
-
eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
|
462
|
-
})
|
|
455
|
+
`MockEventBusService` accumulates emitted events — use `.getEmittedEvents()` directly, no spy needed.
|
|
463
456
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
})
|
|
457
|
+
```typescript
|
|
458
|
+
import { Modules } from "@acmekit/framework/utils"
|
|
467
459
|
|
|
468
460
|
it("should emit post.created event", async () => {
|
|
469
461
|
await service.createPosts([{ title: "Event Test" }])
|
|
470
462
|
|
|
471
|
-
|
|
463
|
+
// Access events directly from the mock event bus — no spy needed
|
|
464
|
+
const eventBus = getContainer().resolve(Modules.EVENT_BUS)
|
|
465
|
+
const events = eventBus.getEmittedEvents()
|
|
472
466
|
expect(events).toEqual(
|
|
473
467
|
expect.arrayContaining([
|
|
474
468
|
expect.objectContaining({
|
|
475
|
-
name: "post.created",
|
|
469
|
+
name: "post.created", // NOTE: property is "name", NOT "eventName"
|
|
476
470
|
data: expect.objectContaining({ id: expect.any(String) }),
|
|
477
471
|
}),
|
|
478
472
|
])
|
|
@@ -480,6 +474,8 @@ it("should emit post.created event", async () => {
|
|
|
480
474
|
})
|
|
481
475
|
```
|
|
482
476
|
|
|
477
|
+
> `emitEventStep` maps `eventName` to `name` internally — always assert on `event.name`.
|
|
478
|
+
|
|
483
479
|
---
|
|
484
480
|
|
|
485
481
|
## What to Test
|
|
@@ -520,29 +520,18 @@ expect(response.data).toEqual({
|
|
|
520
520
|
|
|
521
521
|
## Asserting Domain Events
|
|
522
522
|
|
|
523
|
-
|
|
523
|
+
`MockEventBusService` accumulates all emitted events. Use `.getEmittedEvents()` — no spy needed.
|
|
524
524
|
|
|
525
525
|
```typescript
|
|
526
|
-
import { MockEventBusService } from "@acmekit/test-utils"
|
|
527
|
-
|
|
528
|
-
let eventBusSpy: jest.SpyInstance
|
|
529
|
-
|
|
530
|
-
beforeEach(() => {
|
|
531
|
-
eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
afterEach(() => {
|
|
535
|
-
eventBusSpy.mockClear()
|
|
536
|
-
})
|
|
537
|
-
|
|
538
526
|
it("should emit post.created event", async () => {
|
|
539
527
|
await service.createPosts([{ title: "Event Test" }])
|
|
540
528
|
|
|
541
|
-
const
|
|
529
|
+
const eventBus = getContainer().resolve(Modules.EVENT_BUS)
|
|
530
|
+
const events = eventBus.getEmittedEvents()
|
|
542
531
|
expect(events).toEqual(
|
|
543
532
|
expect.arrayContaining([
|
|
544
533
|
expect.objectContaining({
|
|
545
|
-
name: "post.created",
|
|
534
|
+
name: "post.created", // NOTE: property is "name", NOT "eventName"
|
|
546
535
|
data: expect.objectContaining({ id: expect.any(String) }),
|
|
547
536
|
}),
|
|
548
537
|
])
|
|
@@ -550,6 +539,8 @@ it("should emit post.created event", async () => {
|
|
|
550
539
|
})
|
|
551
540
|
```
|
|
552
541
|
|
|
542
|
+
> `emitEventStep` maps `eventName` to `name` internally — always assert on `event.name`.
|
|
543
|
+
|
|
553
544
|
---
|
|
554
545
|
|
|
555
546
|
## Waiting for Subscribers
|
|
@@ -904,6 +895,42 @@ await expect(provider.process(badInput)).rejects.toThrow("validation")
|
|
|
904
895
|
|
|
905
896
|
---
|
|
906
897
|
|
|
898
|
+
## Auto-Injected Infrastructure Mocks
|
|
899
|
+
|
|
900
|
+
The test runner automatically injects mock implementations for infrastructure modules in plugin and module modes. You do NOT need to manually inject these:
|
|
901
|
+
|
|
902
|
+
| Module | Mock class | Behavior |
|
|
903
|
+
|---|---|---|
|
|
904
|
+
| `Modules.EVENT_BUS` | `MockEventBusService` | Accumulates events — call `.getEmittedEvents()` to inspect |
|
|
905
|
+
| `Modules.LOCKING` | `MockLockingService` | In-memory locking — `execute()` runs job immediately, acquire/release tracked |
|
|
906
|
+
| `Modules.SECRETS` | `MockSecretsService` | In-memory store — pre-populate with `.setSecret(id, value)` |
|
|
907
|
+
|
|
908
|
+
Override any mock via `injectedDependencies` if needed — user overrides take precedence.
|
|
909
|
+
|
|
910
|
+
### Asserting emitted events (no spy needed)
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
const eventBus = getContainer().resolve(Modules.EVENT_BUS)
|
|
914
|
+
|
|
915
|
+
await createPostWorkflow(getContainer()).run({ input: { title: "Test" } })
|
|
916
|
+
|
|
917
|
+
// Inspect emitted events directly — no jest.spyOn needed
|
|
918
|
+
const events = eventBus.getEmittedEvents()
|
|
919
|
+
const postEvent = events.find((e: any) => e.name === "post.created")
|
|
920
|
+
expect(postEvent).toBeDefined()
|
|
921
|
+
expect(postEvent.data).toEqual(expect.objectContaining({ id: expect.any(String) }))
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
> Event objects have `{ name, data, metadata?, options? }`. The property is `name`, NOT `eventName` — `emitEventStep` maps `eventName` to `name` internally.
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## Client Routes Require API Key
|
|
929
|
+
|
|
930
|
+
Routes under `/client/*` use `AcmeKitTypedRequest` (non-authenticated), but they are NOT fully public. AcmeKit middleware enforces a client API key header on all `/client/*` routes. "Public" means "no user authentication" — a client API key is still required.
|
|
931
|
+
|
|
932
|
+
---
|
|
933
|
+
|
|
907
934
|
## Anti-Patterns — NEVER Do These
|
|
908
935
|
|
|
909
936
|
```typescript
|
|
@@ -1068,4 +1095,15 @@ const re = new RegExp("^(\\*|[0-9]+)(\\/[0-9]+)?$")
|
|
|
1068
1095
|
const result = await provider.process(bad)
|
|
1069
1096
|
expect(result.success).toBe(false) // ❌ actually throws
|
|
1070
1097
|
// RIGHT — read implementation first to check if it throws or returns
|
|
1098
|
+
|
|
1099
|
+
// WRONG — using jest.spyOn to inspect emitted events
|
|
1100
|
+
jest.spyOn(MockEventBusService.prototype, "emit") // ❌ fragile, complex extraction
|
|
1101
|
+
// RIGHT — use built-in event accumulation
|
|
1102
|
+
const eventBus = getContainer().resolve(Modules.EVENT_BUS)
|
|
1103
|
+
const events = eventBus.getEmittedEvents()
|
|
1104
|
+
|
|
1105
|
+
// WRONG — checking event.eventName (emitEventStep maps eventName → name internally)
|
|
1106
|
+
expect(event.eventName).toBe("post.created") // ❌ property doesn't exist
|
|
1107
|
+
// RIGHT
|
|
1108
|
+
expect(event.name).toBe("post.created")
|
|
1071
1109
|
```
|
|
@@ -258,7 +258,7 @@ integrationTestRunner({
|
|
|
258
258
|
|
|
259
259
|
**Fixtures (container-only):** `container`, `acmekitApp`, `MikroOrmWrapper`, `dbConfig`.
|
|
260
260
|
|
|
261
|
-
**When plugin depends on other plugins:**
|
|
261
|
+
**When plugin depends on other plugins:** Use `additionalPlugins` to load real peer plugins with their modules, migrations, and services. Only fall back to `skipDependencyValidation: true` + `injectedDependencies` mocks when peer plugins can't be installed.
|
|
262
262
|
|
|
263
263
|
**When plugin has providers needing options:** Use `pluginModuleOptions` keyed by module name:
|
|
264
264
|
```typescript
|
|
@@ -471,31 +471,22 @@ describe("MyProvider", () => {
|
|
|
471
471
|
|
|
472
472
|
## Asserting Domain Events (container-only mode)
|
|
473
473
|
|
|
474
|
-
|
|
474
|
+
`MockEventBusService` accumulates emitted events — use `.getEmittedEvents()` directly, no spy needed.
|
|
475
475
|
|
|
476
476
|
```typescript
|
|
477
|
-
import {
|
|
478
|
-
|
|
479
|
-
let eventBusSpy: jest.SpyInstance
|
|
480
|
-
|
|
481
|
-
beforeEach(() => {
|
|
482
|
-
eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
afterEach(() => {
|
|
486
|
-
eventBusSpy.mockClear()
|
|
487
|
-
})
|
|
477
|
+
import { Modules } from "@acmekit/framework/utils"
|
|
488
478
|
|
|
489
479
|
it("should emit greeting.created event", async () => {
|
|
490
480
|
const service: any = container.resolve(GREETING_MODULE)
|
|
491
481
|
await service.createGreetings([{ message: "Event Test" }])
|
|
492
482
|
|
|
493
|
-
//
|
|
494
|
-
const
|
|
483
|
+
// Access events directly from the mock event bus — no spy needed
|
|
484
|
+
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
485
|
+
const events = eventBus.getEmittedEvents()
|
|
495
486
|
expect(events).toEqual(
|
|
496
487
|
expect.arrayContaining([
|
|
497
488
|
expect.objectContaining({
|
|
498
|
-
name: "greeting.created",
|
|
489
|
+
name: "greeting.created", // NOTE: property is "name", NOT "eventName"
|
|
499
490
|
data: expect.objectContaining({ id: expect.any(String) }),
|
|
500
491
|
}),
|
|
501
492
|
])
|
|
@@ -503,6 +494,8 @@ it("should emit greeting.created event", async () => {
|
|
|
503
494
|
})
|
|
504
495
|
```
|
|
505
496
|
|
|
497
|
+
> `emitEventStep` maps `eventName` to `name` internally — always assert on `event.name`.
|
|
498
|
+
|
|
506
499
|
---
|
|
507
500
|
|
|
508
501
|
## What to Test
|
|
@@ -538,8 +531,8 @@ it("should emit greeting.created event", async () => {
|
|
|
538
531
|
- Auth: 401 without JWT, 400 without client API key
|
|
539
532
|
|
|
540
533
|
**Events (container mode):**
|
|
541
|
-
-
|
|
542
|
-
-
|
|
534
|
+
- Use `container.resolve(Modules.EVENT_BUS).getEmittedEvents()` — no spy needed
|
|
535
|
+
- Event objects have `{ name, data }` — property is `name`, NOT `eventName`
|
|
543
536
|
|
|
544
537
|
---
|
|
545
538
|
|
|
@@ -576,8 +569,8 @@ it("should emit greeting.created event", async () => {
|
|
|
576
569
|
- Use realistic test data ("Launch Announcement", "Quarterly Report") not "test", "foo"
|
|
577
570
|
- Pass body directly: `api.post(url, body, headers)` — NOT `{ body: {...} }`
|
|
578
571
|
- Runners handle DB setup/teardown — no manual cleanup needed
|
|
579
|
-
-
|
|
580
|
-
- `
|
|
572
|
+
- Use `eventBus.getEmittedEvents()` for event assertions — no spy ceremony
|
|
573
|
+
- Event property is `name`, NOT `eventName` (`emitEventStep` maps internally)
|
|
581
574
|
- NEVER use JSDoc blocks or type casts in test files
|
|
582
575
|
- **Always `beforeEach(() => jest.clearAllMocks())`** in unit tests — mock state leaks between describes
|
|
583
576
|
- **Never reference file-level `const`/`let` inside `jest.mock()` factories** — TDZ error
|
|
@@ -128,9 +128,10 @@ integrationTestRunner({
|
|
|
128
128
|
| `pluginPath` | `string` | **(required)** | Path to plugin root — always use `process.cwd()` |
|
|
129
129
|
| `pluginOptions` | `Record<string, unknown>` | `{}` | Simulates host app plugin config |
|
|
130
130
|
| `http` | `boolean` | `false` | Set `true` to boot full Express server for HTTP tests |
|
|
131
|
+
| `additionalPlugins` | `Array<{ resolve, options? }>` | `[]` | Peer plugins to load alongside the plugin under test — real modules, migrations, services |
|
|
131
132
|
| `additionalModules` | `Record<string, any>` | `{}` | Extra modules to load alongside the plugin |
|
|
132
133
|
| `injectedDependencies` | `Record<string, any>` | `{}` | Mock services to register in the container |
|
|
133
|
-
| `skipDependencyValidation` | `boolean` | `false` |
|
|
134
|
+
| `skipDependencyValidation` | `boolean` | `false` | Escape hatch: skip `definePlugin({ dependencies })` validation when peer plugins can't be installed |
|
|
134
135
|
| `pluginModuleOptions` | `Record<string, Record<string, any>>` | `{}` | Per-module options keyed by module name (e.g., provider config) |
|
|
135
136
|
| `dbName` | `string` | auto-generated | Override the computed DB name |
|
|
136
137
|
| `schema` | `string` | `"public"` | Postgres schema |
|
|
@@ -491,34 +492,27 @@ integrationTestRunner({
|
|
|
491
492
|
|
|
492
493
|
## Asserting Domain Events
|
|
493
494
|
|
|
494
|
-
|
|
495
|
+
`MockEventBusService` accumulates all emitted events. Use `.getEmittedEvents()` — no spy needed.
|
|
495
496
|
|
|
496
497
|
```typescript
|
|
497
|
-
|
|
498
|
+
it("should emit greeting.created event", async () => {
|
|
499
|
+
const service = container.resolve(GREETING_MODULE)
|
|
500
|
+
await service.createGreetings([{ message: "Hello" }])
|
|
498
501
|
|
|
499
|
-
it("should capture events", async () => {
|
|
500
|
-
const spy = jest.spyOn(MockEventBusService.prototype, "emit")
|
|
501
502
|
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
{ name: "greeting.created", data: { id: "g1", message: "Hello" } },
|
|
505
|
-
])
|
|
506
|
-
|
|
507
|
-
expect(spy).toHaveBeenCalledTimes(1)
|
|
508
|
-
expect(spy.mock.calls[0][0]).toEqual(
|
|
503
|
+
const events = eventBus.getEmittedEvents()
|
|
504
|
+
expect(events).toEqual(
|
|
509
505
|
expect.arrayContaining([
|
|
510
506
|
expect.objectContaining({
|
|
511
|
-
name: "greeting.created",
|
|
512
|
-
data: expect.objectContaining({ id:
|
|
507
|
+
name: "greeting.created", // NOTE: property is "name", NOT "eventName"
|
|
508
|
+
data: expect.objectContaining({ id: expect.any(String) }),
|
|
513
509
|
}),
|
|
514
510
|
])
|
|
515
511
|
)
|
|
516
|
-
|
|
517
|
-
spy.mockRestore()
|
|
518
512
|
})
|
|
519
513
|
```
|
|
520
514
|
|
|
521
|
-
|
|
515
|
+
> `emitEventStep` maps `eventName` to `name` internally — always assert on `event.name`.
|
|
522
516
|
|
|
523
517
|
---
|
|
524
518
|
|
|
@@ -724,7 +718,28 @@ await expect(provider.process(badInput)).rejects.toThrow("validation")
|
|
|
724
718
|
|
|
725
719
|
## Testing Plugins with Dependencies
|
|
726
720
|
|
|
727
|
-
When your plugin depends on other plugins (declared in `definePlugin({ dependencies })`),
|
|
721
|
+
When your plugin depends on other plugins (declared in `definePlugin({ dependencies })`), use `additionalPlugins` to load them alongside the plugin under test. This boots real modules, runs real migrations, and registers real services — the same as production:
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
integrationTestRunner({
|
|
725
|
+
mode: "plugin",
|
|
726
|
+
pluginPath: process.cwd(),
|
|
727
|
+
additionalPlugins: [
|
|
728
|
+
{ resolve: "@acmekit/plugin-reviews" },
|
|
729
|
+
{ resolve: "@acmekit/plugin-loyalty", options: { tier: "gold" } },
|
|
730
|
+
],
|
|
731
|
+
testSuite: ({ container }) => {
|
|
732
|
+
// container has REAL services from all plugins
|
|
733
|
+
it("uses review service", async () => {
|
|
734
|
+
const reviewService = container.resolve(REVIEW_MODULE)
|
|
735
|
+
const reviews = await reviewService.listReviews()
|
|
736
|
+
expect(reviews).toBeDefined()
|
|
737
|
+
})
|
|
738
|
+
},
|
|
739
|
+
})
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Escape hatch:** When peer plugins genuinely can't be installed (e.g., CI without optional deps), use `skipDependencyValidation` to bypass validation and mock the services manually:
|
|
728
743
|
|
|
729
744
|
```typescript
|
|
730
745
|
integrationTestRunner({
|
|
@@ -739,9 +754,13 @@ integrationTestRunner({
|
|
|
739
754
|
})
|
|
740
755
|
```
|
|
741
756
|
|
|
757
|
+
> **Prefer `additionalPlugins` over `skipDependencyValidation` + mocks.** Mocked services can silently drift from the real API — tests pass but production breaks.
|
|
758
|
+
|
|
742
759
|
## Configuring Providers in Plugin Tests
|
|
743
760
|
|
|
744
|
-
|
|
761
|
+
Providers CANNOT be mocked with `jest.mock({ virtual: true })`. The module loader uses `require.resolve()` which bypasses jest. Providers must be **real files on disk** exporting `ModuleProvider()`.
|
|
762
|
+
|
|
763
|
+
Use `pluginModuleOptions` to register providers keyed by module name. Provider `resolve` paths must be **absolute** (the loader resolves from `modules-sdk` dist, not your test file):
|
|
745
764
|
|
|
746
765
|
```typescript
|
|
747
766
|
integrationTestRunner({
|
|
@@ -751,8 +770,8 @@ integrationTestRunner({
|
|
|
751
770
|
tron: {
|
|
752
771
|
providers: [
|
|
753
772
|
{
|
|
754
|
-
resolve: "
|
|
755
|
-
id: "
|
|
773
|
+
resolve: process.cwd() + "/integration-tests/helpers/mock-energy-provider",
|
|
774
|
+
id: "mock-energy",
|
|
756
775
|
options: { apiKey: "test-key" },
|
|
757
776
|
},
|
|
758
777
|
],
|
|
@@ -762,10 +781,108 @@ integrationTestRunner({
|
|
|
762
781
|
})
|
|
763
782
|
```
|
|
764
783
|
|
|
784
|
+
The helper file must be a real module:
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
// integration-tests/helpers/mock-energy-provider.ts
|
|
788
|
+
import { ModuleProvider } from "@acmekit/framework/utils"
|
|
789
|
+
|
|
790
|
+
class MockEnergyService {
|
|
791
|
+
static identifier = "mock-energy"
|
|
792
|
+
// implement provider interface methods
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export default ModuleProvider("tron", { services: [MockEnergyService] })
|
|
796
|
+
```
|
|
797
|
+
|
|
765
798
|
`pluginModuleOptions` is merged into the discovered module config AFTER `pluginOptions`, so module-specific options override plugin-level ones.
|
|
766
799
|
|
|
767
800
|
---
|
|
768
801
|
|
|
802
|
+
## Auto-Injected Infrastructure Mocks
|
|
803
|
+
|
|
804
|
+
The test runner automatically injects mock implementations for infrastructure modules in plugin and module modes. You do NOT need to manually inject these:
|
|
805
|
+
|
|
806
|
+
| Module | Mock class | Behavior |
|
|
807
|
+
|---|---|---|
|
|
808
|
+
| `Modules.EVENT_BUS` | `MockEventBusService` | Accumulates events — call `.getEmittedEvents()` to inspect |
|
|
809
|
+
| `Modules.LOCKING` | `MockLockingService` | In-memory locking — `execute()` runs job immediately, acquire/release tracked |
|
|
810
|
+
| `Modules.SECRETS` | `MockSecretsService` | In-memory store — pre-populate with `.setSecret(id, value)` |
|
|
811
|
+
|
|
812
|
+
Override any mock via `injectedDependencies` if needed — user overrides take precedence.
|
|
813
|
+
|
|
814
|
+
### Asserting emitted events (no spy needed)
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
// Resolve the mock event bus from the container
|
|
818
|
+
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
819
|
+
|
|
820
|
+
// Run your workflow/operation that emits events
|
|
821
|
+
await createGreetingsWorkflow(container).run({ input: { greetings: [{ message: "Hi" }] } })
|
|
822
|
+
|
|
823
|
+
// Inspect emitted events directly — no jest.spyOn needed
|
|
824
|
+
const events = eventBus.getEmittedEvents()
|
|
825
|
+
const greetingEvent = events.find((e: any) => e.name === "greeting.created")
|
|
826
|
+
expect(greetingEvent).toBeDefined()
|
|
827
|
+
expect(greetingEvent.data).toEqual(expect.objectContaining({ id: expect.any(String) }))
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
> Event objects have `{ name, data, metadata?, options? }`. The property is `name`, NOT `eventName` — `emitEventStep` maps `eventName` to `name` internally.
|
|
831
|
+
|
|
832
|
+
### Pre-populating secrets for tests
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
integrationTestRunner({
|
|
836
|
+
mode: "plugin",
|
|
837
|
+
pluginPath: process.cwd(),
|
|
838
|
+
testSuite: ({ container }) => {
|
|
839
|
+
beforeEach(() => {
|
|
840
|
+
const secrets = container.resolve(Modules.SECRETS)
|
|
841
|
+
secrets.setSecret("prod/wallet", { address: "0xtest123" })
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
it("resolves wallet address from secrets", async () => {
|
|
845
|
+
const addr = await container.resolve(Modules.SECRETS).getKey("prod/wallet", "address")
|
|
846
|
+
expect(addr).toBe("0xtest123")
|
|
847
|
+
})
|
|
848
|
+
},
|
|
849
|
+
})
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
---
|
|
853
|
+
|
|
854
|
+
## injectedDependencies vs pluginModuleOptions
|
|
855
|
+
|
|
856
|
+
| | `injectedDependencies` | `pluginModuleOptions` |
|
|
857
|
+
|---|---|---|
|
|
858
|
+
| **Injects into** | Root container | Per-module config |
|
|
859
|
+
| **Use for** | Infrastructure mocks (locking, secrets, custom services) | Provider registration, module-specific config |
|
|
860
|
+
| **Shape** | `{ [Modules.X]: mockInstance }` | `{ moduleName: { providers: [...], options } }` |
|
|
861
|
+
| **Path resolution** | N/A | Must be **absolute** (`process.cwd() + "/path"`) |
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
## Client Routes Require API Key
|
|
866
|
+
|
|
867
|
+
Routes under `/client/*` use `AcmeKitTypedRequest` (non-authenticated), but they are NOT fully public. AcmeKit middleware enforces a client API key header on all `/client/*` routes.
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
// WRONG — 401 "Client API key required"
|
|
871
|
+
const response = await api.get("/client/plugin/greetings") // ❌
|
|
872
|
+
|
|
873
|
+
// RIGHT — create client API key and pass header
|
|
874
|
+
const apiKey = await apiKeyModule.createApiKeys({
|
|
875
|
+
title: "Test Client Key",
|
|
876
|
+
type: ApiKeyType.CLIENT,
|
|
877
|
+
created_by: "test",
|
|
878
|
+
})
|
|
879
|
+
const response = await api.get("/client/plugin/greetings", {
|
|
880
|
+
headers: { [CLIENT_API_KEY_HEADER]: apiKey.token },
|
|
881
|
+
})
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
769
886
|
## Anti-Patterns — NEVER Do These
|
|
770
887
|
|
|
771
888
|
```typescript
|
|
@@ -891,4 +1008,26 @@ const re = new RegExp("^(\\*|[0-9]+)(\\/[0-9]+)?$")
|
|
|
891
1008
|
const result = await provider.process(bad)
|
|
892
1009
|
expect(result.success).toBe(false) // ❌ actually throws
|
|
893
1010
|
// RIGHT — read implementation first to check if it throws or returns
|
|
1011
|
+
|
|
1012
|
+
// WRONG — jest virtual mock for providers (require.resolve bypasses jest)
|
|
1013
|
+
jest.mock("../../src/providers/mock-provider", () => ({ ... }), { virtual: true }) // ❌
|
|
1014
|
+
// RIGHT — create a real file and use absolute path
|
|
1015
|
+
// integration-tests/helpers/mock-provider.ts (real file with ModuleProvider export)
|
|
1016
|
+
pluginModuleOptions: { mod: { providers: [{ resolve: process.cwd() + "/integration-tests/helpers/mock-provider" }] } }
|
|
1017
|
+
|
|
1018
|
+
// WRONG — relative provider resolve path (resolved from modules-sdk dist, not test file)
|
|
1019
|
+
{ resolve: "../../src/providers/my-provider" } // ❌
|
|
1020
|
+
// RIGHT
|
|
1021
|
+
{ resolve: process.cwd() + "/src/providers/my-provider" }
|
|
1022
|
+
|
|
1023
|
+
// WRONG — using jest.spyOn to inspect emitted events
|
|
1024
|
+
jest.spyOn(MockEventBusService.prototype, "emit") // ❌ fragile, complex extraction
|
|
1025
|
+
// RIGHT — use built-in event accumulation
|
|
1026
|
+
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
1027
|
+
const events = eventBus.getEmittedEvents()
|
|
1028
|
+
|
|
1029
|
+
// WRONG — checking event.eventName (emitEventStep maps eventName → name internally)
|
|
1030
|
+
expect(event.eventName).toBe("greeting.created") // ❌ property doesn't exist
|
|
1031
|
+
// RIGHT
|
|
1032
|
+
expect(event.name).toBe("greeting.created")
|
|
894
1033
|
```
|
|
@@ -356,7 +356,7 @@ pnpm test:integration:http # Plugin HTTP tests
|
|
|
356
356
|
- Always use `pluginPath: process.cwd()` — never hardcode paths
|
|
357
357
|
- Container-only tests: access services via `container.resolve(MODULE_CONSTANT)` — no `api` fixture
|
|
358
358
|
- HTTP tests: full auth setup with `generateJwtToken` + `ApiKeyType.CLIENT` — no `createAdminUser` helper
|
|
359
|
-
- Plugin depends on other plugins:
|
|
359
|
+
- Plugin depends on other plugins: use `additionalPlugins: [{ resolve: "@acmekit/plugin-name" }]` to load real peers. Fall back to `skipDependencyValidation: true` + `injectedDependencies` only when peers can't be installed
|
|
360
360
|
- Plugin providers need options: use `pluginModuleOptions: { moduleName: { providers: [...] } }`
|
|
361
361
|
- Pass body directly: `api.post(url, body, headers)` — NOT `{ body: {...} }`
|
|
362
362
|
- Use `.catch((e: any) => e)` for error assertions — axios throws on 4xx/5xx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acmekit/acmekit",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.87",
|
|
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.87"
|
|
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.87",
|
|
56
|
+
"@acmekit/analytics": "2.13.87",
|
|
57
|
+
"@acmekit/analytics-local": "2.13.87",
|
|
58
|
+
"@acmekit/analytics-posthog": "2.13.87",
|
|
59
|
+
"@acmekit/api-key": "2.13.87",
|
|
60
|
+
"@acmekit/auth": "2.13.87",
|
|
61
|
+
"@acmekit/auth-emailpass": "2.13.87",
|
|
62
|
+
"@acmekit/auth-github": "2.13.87",
|
|
63
|
+
"@acmekit/auth-google": "2.13.87",
|
|
64
|
+
"@acmekit/cache-inmemory": "2.13.87",
|
|
65
|
+
"@acmekit/cache-redis": "2.13.87",
|
|
66
|
+
"@acmekit/caching": "2.13.87",
|
|
67
|
+
"@acmekit/caching-redis": "2.13.87",
|
|
68
|
+
"@acmekit/core-flows": "2.13.87",
|
|
69
|
+
"@acmekit/event-bus-local": "2.13.87",
|
|
70
|
+
"@acmekit/event-bus-redis": "2.13.87",
|
|
71
|
+
"@acmekit/file": "2.13.87",
|
|
72
|
+
"@acmekit/file-local": "2.13.87",
|
|
73
|
+
"@acmekit/file-s3": "2.13.87",
|
|
74
|
+
"@acmekit/index": "2.13.87",
|
|
75
|
+
"@acmekit/link-modules": "2.13.87",
|
|
76
|
+
"@acmekit/locking": "2.13.87",
|
|
77
|
+
"@acmekit/locking-postgres": "2.13.87",
|
|
78
|
+
"@acmekit/locking-redis": "2.13.87",
|
|
79
|
+
"@acmekit/notification": "2.13.87",
|
|
80
|
+
"@acmekit/notification-local": "2.13.87",
|
|
81
|
+
"@acmekit/notification-sendgrid": "2.13.87",
|
|
82
|
+
"@acmekit/rbac": "2.13.87",
|
|
83
|
+
"@acmekit/secrets-aws": "2.13.87",
|
|
84
|
+
"@acmekit/secrets-local": "2.13.87",
|
|
85
|
+
"@acmekit/settings": "2.13.87",
|
|
86
|
+
"@acmekit/telemetry": "2.13.87",
|
|
87
|
+
"@acmekit/translation": "2.13.87",
|
|
88
|
+
"@acmekit/user": "2.13.87",
|
|
89
|
+
"@acmekit/workflow-engine-inmemory": "2.13.87",
|
|
90
|
+
"@acmekit/workflow-engine-redis": "2.13.87",
|
|
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.87",
|
|
110
110
|
"@jimsheen/yalc": "^1.2.2",
|
|
111
111
|
"@swc/core": "^1.7.28",
|
|
112
112
|
"posthog-node": "^5.11.0",
|