@acmekit/acmekit 2.13.86 → 2.13.88
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/code-reviewer.md +18 -0
- package/dist/templates/app/.claude/agents/test-writer.md +28 -18
- package/dist/templates/app/.claude/rules/admin-components.md +18 -4
- package/dist/templates/app/.claude/rules/admin-data.md +73 -0
- package/dist/templates/app/.claude/rules/admin-patterns.md +20 -0
- package/dist/templates/app/.claude/rules/api-routes.md +25 -1
- package/dist/templates/app/.claude/rules/modules.md +169 -0
- package/dist/templates/app/.claude/rules/testing.md +96 -16
- package/dist/templates/app/.claude/rules/workflows.md +2 -0
- package/dist/templates/app/.claude/skills/admin-customization/SKILL.md +10 -0
- package/dist/templates/app/.claude/skills/write-test/SKILL.md +7 -0
- package/dist/templates/app/CLAUDE.md +2 -1
- package/dist/templates/plugin/.claude/agents/code-reviewer.md +18 -0
- package/dist/templates/plugin/.claude/agents/test-writer.md +30 -23
- package/dist/templates/plugin/.claude/rules/admin-components.md +18 -4
- package/dist/templates/plugin/.claude/rules/admin-data.md +73 -0
- package/dist/templates/plugin/.claude/rules/admin-patterns.md +20 -0
- package/dist/templates/plugin/.claude/rules/api-routes.md +25 -1
- package/dist/templates/plugin/.claude/rules/modules.md +169 -0
- package/dist/templates/plugin/.claude/rules/testing.md +187 -20
- package/dist/templates/plugin/.claude/rules/workflows.md +2 -0
- package/dist/templates/plugin/.claude/skills/admin-customization/SKILL.md +10 -0
- package/dist/templates/plugin/.claude/skills/write-test/SKILL.md +8 -0
- package/dist/templates/plugin/CLAUDE.md +2 -1
- package/package.json +39 -39
|
@@ -396,6 +396,149 @@ Options flow: host config `providers[].options` → constructor second argument
|
|
|
396
396
|
|
|
397
397
|
Available domain abstracts: `AbstractNotificationProviderService`, `AbstractFileProviderService`, `AbstractAuthModuleProvider`, `AbstractAnalyticsProviderService`, `AbstractModuleProvider<TConfig>` (generic base).
|
|
398
398
|
|
|
399
|
+
### Provider Container Scoping
|
|
400
|
+
|
|
401
|
+
**Providers are module-scoped.** The `container` passed to a provider constructor is the **parent module's** awilix cradle — not the application root container. This is by design: modules are isolated, self-contained units.
|
|
402
|
+
|
|
403
|
+
**Providers CAN access:**
|
|
404
|
+
- Sibling registrations within the parent module (e.g., `logger`, services registered by the module's loaders)
|
|
405
|
+
- Their own `this.options_` (from `providers[].options` in host config)
|
|
406
|
+
|
|
407
|
+
**Providers CANNOT access:**
|
|
408
|
+
- Other modules — a provider under `RELAYER_MODULE` cannot resolve `TRON_MODULE`, `Modules.USER`, or any module outside its parent
|
|
409
|
+
- Application-level container registrations not propagated to the module scope
|
|
410
|
+
|
|
411
|
+
**Attempting to resolve another module from the provider's container silently returns `undefined` or throws at runtime** — there is no compile-time error.
|
|
412
|
+
|
|
413
|
+
#### Cradle vs AcmeKitContainer — Two Different Objects
|
|
414
|
+
|
|
415
|
+
AcmeKit has two container representations. Confusing them causes silent `undefined` at runtime.
|
|
416
|
+
|
|
417
|
+
| | **Cradle** (awilix proxy) | **AcmeKitContainer** |
|
|
418
|
+
|---|---|---|
|
|
419
|
+
| What is it | Proxy object — property access triggers resolution | Full container with `.resolve()` method |
|
|
420
|
+
| Access pattern | `container.logger`, `container[MODULE]` | `container.resolve(MODULE)` |
|
|
421
|
+
| Who gets it | Provider constructors, service constructors | Workflow steps (`{ container }`), `req.scope` |
|
|
422
|
+
| Type | `Record<string, unknown>` (or typed cradle) | `AcmeKitContainer` from `@acmekit/framework/types` |
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// Provider constructor — receives CRADLE (property access)
|
|
426
|
+
constructor(container: Cradle, options: Options) {
|
|
427
|
+
this.logger_ = container.logger // ✅ property access
|
|
428
|
+
this.logger_ = container.resolve("logger") // ❌ cradle has no .resolve()
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Workflow step — receives ACMEKIT CONTAINER (.resolve() method)
|
|
432
|
+
async (input, { container }) => {
|
|
433
|
+
const svc = container.resolve(MY_MODULE) // ✅ .resolve() method
|
|
434
|
+
const svc = container[MY_MODULE] // ❌ undefined — not a cradle
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**When passing the application container to a provider method, the provider must use `.resolve()` — not bracket access:**
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
import type { AcmeKitContainer } from "@acmekit/framework/types"
|
|
442
|
+
|
|
443
|
+
class MyProvider {
|
|
444
|
+
async doWork(request: Request, appContainer?: AcmeKitContainer) {
|
|
445
|
+
// appContainer is from the workflow step — use .resolve()
|
|
446
|
+
const otherService = appContainer?.resolve<IOtherService>(OTHER_MODULE) // ✅
|
|
447
|
+
const otherService = appContainer?.[OTHER_MODULE] // ❌ undefined!
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### Simple Providers — No Cross-Module Dependencies
|
|
453
|
+
|
|
454
|
+
When a provider's job is a single external operation (send email, upload file, call API), it needs only `this.options_` and sibling registrations. No special handling required.
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
class SendGridNotification extends AbstractNotificationProviderService {
|
|
458
|
+
static identifier = "sendgrid"
|
|
459
|
+
|
|
460
|
+
async send(notification) {
|
|
461
|
+
// Only needs this.options_.apiKey — no other modules
|
|
462
|
+
return this.client_.send({ to: notification.to, template: notification.template })
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
#### Complex Providers — Cross-Module Dependencies via Method Parameters
|
|
468
|
+
|
|
469
|
+
Some providers need other modules to do their job — e.g., a chain relayer provider that calls external APIs but also manages chain-specific DB state in a companion module. The provider can't resolve the companion module from `this.container_` (module-scoped), and splitting into workflow steps isn't always possible (the calling workflow belongs to a different plugin/package).
|
|
470
|
+
|
|
471
|
+
**The correct pattern: the workflow step passes the `AcmeKitContainer` through the parent module's service method to the provider. The provider uses `.resolve()` on the passed container — never bracket access.**
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
import type { AcmeKitContainer } from "@acmekit/framework/types"
|
|
475
|
+
|
|
476
|
+
// 1. Workflow step — passes the full application container
|
|
477
|
+
export const executeOnChainStep = createStep(
|
|
478
|
+
{ name: "execute-on-chain-step", noCompensation: true },
|
|
479
|
+
async ({ request }, { container }) => {
|
|
480
|
+
const relayerService = container.resolve<IRelayerModuleService>(RELAYER_MODULE)
|
|
481
|
+
// Pass container (AcmeKitContainer) — provider will .resolve() what it needs
|
|
482
|
+
const result = await relayerService.relay(request, container)
|
|
483
|
+
return new StepResponse(result)
|
|
484
|
+
},
|
|
485
|
+
async () => {}
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
// 2. Parent module service — forwards AcmeKitContainer to the provider
|
|
489
|
+
class RelayerModuleService {
|
|
490
|
+
@InjectManager()
|
|
491
|
+
async relay(
|
|
492
|
+
request: RelayRequest,
|
|
493
|
+
appContainer?: AcmeKitContainer,
|
|
494
|
+
@AcmeKitContext() sharedContext: Context = {}
|
|
495
|
+
) {
|
|
496
|
+
return await this.relay_(request, appContainer, sharedContext)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@InjectTransactionManager()
|
|
500
|
+
protected async relay_(
|
|
501
|
+
request: RelayRequest,
|
|
502
|
+
appContainer?: AcmeKitContainer,
|
|
503
|
+
@AcmeKitContext() sharedContext: Context = {}
|
|
504
|
+
) {
|
|
505
|
+
const provider = this.resolveChainRelayer(request.chain)
|
|
506
|
+
const wallet = await this.selectWallet_(request.chain, sharedContext)
|
|
507
|
+
return await provider.relay({ ...request, wallet }, appContainer)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 3. Provider — uses .resolve() on the passed AcmeKitContainer
|
|
512
|
+
class TronChainRelayer implements IChainRelayer {
|
|
513
|
+
static identifier = "tron-chain-relayer"
|
|
514
|
+
|
|
515
|
+
async relay(request: RelayRequest, appContainer?: AcmeKitContainer): Promise<RelayResult> {
|
|
516
|
+
// .resolve() with optional chaining — NOT bracket access
|
|
517
|
+
const tronService = appContainer?.resolve<ITronModuleService>(TRON_MODULE)
|
|
518
|
+
const delegation = await tronService.createDelegationRecords([...])
|
|
519
|
+
// this.options_ for own config (module-scoped — always available)
|
|
520
|
+
const client = new TronWeb({ fullHost: this.options_.fullNodeUrl })
|
|
521
|
+
const txResult = await client.broadcast(request.payload)
|
|
522
|
+
await tronService.reconcileDelegation(delegation.id, txResult)
|
|
523
|
+
return txResult
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Key constraints:**
|
|
529
|
+
- Provider constructor container (`this.container_`) is a **cradle** — property access only, ONLY for sibling registrations
|
|
530
|
+
- The `AcmeKitContainer` passed as a method parameter uses **`.resolve()`** — never bracket access
|
|
531
|
+
- The `appContainer` parameter is optional (`?`) — the provider interface uses the looser `Record<string, unknown>` type; implementations narrow to `AcmeKitContainer`
|
|
532
|
+
- The workflow step is the only place that has the full `AcmeKitContainer`
|
|
533
|
+
- Import: `import type { AcmeKitContainer } from "@acmekit/framework/types"`
|
|
534
|
+
|
|
535
|
+
**When to use which tier:**
|
|
536
|
+
|
|
537
|
+
| Provider type | Cross-module deps? | Pattern |
|
|
538
|
+
|---|---|---|
|
|
539
|
+
| File storage, notifications, SMS | No | Use `this.options_` and `this.container_` only |
|
|
540
|
+
| Payment processors, chain relayers | Yes — companion modules for state/records | Receive deps as method params from workflow step |
|
|
541
|
+
|
|
399
542
|
### Module() vs ModuleProvider()
|
|
400
543
|
|
|
401
544
|
| | `Module()` | `ModuleProvider()` |
|
|
@@ -590,6 +733,32 @@ class MyProvider {
|
|
|
590
733
|
}
|
|
591
734
|
}
|
|
592
735
|
|
|
736
|
+
// WRONG — provider resolving another module from its own container (cradle)
|
|
737
|
+
// Provider container is scoped to the PARENT module — other modules are not visible
|
|
738
|
+
class TronRelayer implements IChainRelayer {
|
|
739
|
+
async relay(request: RelayRequest) {
|
|
740
|
+
const tronService = this.container_[TRON_MODULE] // ❌ undefined — not in module scope
|
|
741
|
+
await tronService.createDelegationRecords([...]) // crashes
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// WRONG — using bracket access on AcmeKitContainer (it's NOT a cradle)
|
|
746
|
+
class TronRelayer implements IChainRelayer {
|
|
747
|
+
async relay(request: RelayRequest, appContainer?: AcmeKitContainer) {
|
|
748
|
+
const svc = appContainer[TRON_MODULE] // ❌ undefined — wrong access pattern!
|
|
749
|
+
const svc2 = appContainer[TRON_MODULE] as any // ❌ same bug, hidden by cast
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// RIGHT — use .resolve() on AcmeKitContainer passed from workflow step
|
|
754
|
+
class TronRelayer implements IChainRelayer {
|
|
755
|
+
async relay(request: RelayRequest, appContainer?: AcmeKitContainer) {
|
|
756
|
+
const tronService = appContainer?.resolve<ITronModuleService>(TRON_MODULE) // ✅
|
|
757
|
+
await tronService.createDelegationRecords([...])
|
|
758
|
+
return await this.client_.broadcast(request.payload)
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
593
762
|
// WRONG — hardcoding prefix/identifiersKey inline in multiple files
|
|
594
763
|
// A typo between createProvidersLoader and AcmeKitService silently breaks resolution
|
|
595
764
|
createProvidersLoader({ prefix: "cr_", identifiersKey: "chain_relayer_providers_identifier" })
|
|
@@ -8,7 +8,30 @@ paths:
|
|
|
8
8
|
|
|
9
9
|
# Testing Rules
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Pre-Flight Checklist (MANDATORY before writing any test)
|
|
12
|
+
|
|
13
|
+
Complete these steps IN ORDER before writing a single line of test code:
|
|
14
|
+
|
|
15
|
+
1. **Build the plugin.** Run `pnpm build` (or verify `pnpm dev` is running). Stale `.acmekit/server/` output causes cryptic errors like `__joinerConfig is not a function` or missing module services. The test runner loads compiled output from `.acmekit/server/`, not source files.
|
|
16
|
+
|
|
17
|
+
2. **Verify jest config buckets.** Read `jest.config.js` and confirm the test type you need exists:
|
|
18
|
+
```
|
|
19
|
+
integration:plugin → integration-tests/plugin/
|
|
20
|
+
integration:http → integration-tests/http/
|
|
21
|
+
integration:modules → src/modules/*/__tests__/
|
|
22
|
+
unit → src/**/__tests__/*.unit.spec.ts
|
|
23
|
+
```
|
|
24
|
+
If the bucket is missing, add the jest config entry BEFORE writing the test. A test file in a directory with no matching jest config is silently ignored.
|
|
25
|
+
|
|
26
|
+
3. **Read the source code under test.** Read service methods, route handlers, workflow steps, subscriber handlers — understand actual method signatures, return types, and error paths.
|
|
27
|
+
|
|
28
|
+
4. **Read mock interfaces.** When your test depends on auto-injected mocks (`MockEventBusService`, `MockLockingService`, `MockSecretsService`), read the mock source in `@acmekit/test-utils` to verify method names and signatures. Do NOT assume mock methods match the real service 1:1 — mocks have convenience methods (e.g., `setSecret()`, `getEmittedEvents()`) and may omit advanced features.
|
|
29
|
+
|
|
30
|
+
5. **Identify the correct test tier.** Match your test to the right mode and file location (see Test Runner Selection below).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Critical Rules
|
|
12
35
|
|
|
13
36
|
**Single unified runner.** Use `integrationTestRunner` from `@acmekit/test-utils` with a `mode` parameter. The old names (`pluginIntegrationTestRunner`, `moduleIntegrationTestRunner`) are deprecated aliases — NEVER use them.
|
|
14
37
|
|
|
@@ -26,6 +49,8 @@ paths:
|
|
|
26
49
|
|
|
27
50
|
**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
51
|
|
|
52
|
+
**`pluginPath` is MANDATORY in plugin mode.** Always use `pluginPath: process.cwd()`. Omitting it causes `Cannot read properties of undefined` during ConfigStage.
|
|
53
|
+
|
|
29
54
|
---
|
|
30
55
|
|
|
31
56
|
## Test Runner Selection
|
|
@@ -492,34 +517,27 @@ integrationTestRunner({
|
|
|
492
517
|
|
|
493
518
|
## Asserting Domain Events
|
|
494
519
|
|
|
495
|
-
|
|
520
|
+
`MockEventBusService` accumulates all emitted events. Use `.getEmittedEvents()` — no spy needed.
|
|
496
521
|
|
|
497
522
|
```typescript
|
|
498
|
-
|
|
523
|
+
it("should emit greeting.created event", async () => {
|
|
524
|
+
const service = container.resolve(GREETING_MODULE)
|
|
525
|
+
await service.createGreetings([{ message: "Hello" }])
|
|
499
526
|
|
|
500
|
-
it("should capture events", async () => {
|
|
501
|
-
const spy = jest.spyOn(MockEventBusService.prototype, "emit")
|
|
502
527
|
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
{ name: "greeting.created", data: { id: "g1", message: "Hello" } },
|
|
506
|
-
])
|
|
507
|
-
|
|
508
|
-
expect(spy).toHaveBeenCalledTimes(1)
|
|
509
|
-
expect(spy.mock.calls[0][0]).toEqual(
|
|
528
|
+
const events = eventBus.getEmittedEvents()
|
|
529
|
+
expect(events).toEqual(
|
|
510
530
|
expect.arrayContaining([
|
|
511
531
|
expect.objectContaining({
|
|
512
|
-
name: "greeting.created",
|
|
513
|
-
data: expect.objectContaining({ id:
|
|
532
|
+
name: "greeting.created", // NOTE: property is "name", NOT "eventName"
|
|
533
|
+
data: expect.objectContaining({ id: expect.any(String) }),
|
|
514
534
|
}),
|
|
515
535
|
])
|
|
516
536
|
)
|
|
517
|
-
|
|
518
|
-
spy.mockRestore()
|
|
519
537
|
})
|
|
520
538
|
```
|
|
521
539
|
|
|
522
|
-
|
|
540
|
+
> `emitEventStep` maps `eventName` to `name` internally — always assert on `event.name`.
|
|
523
541
|
|
|
524
542
|
---
|
|
525
543
|
|
|
@@ -765,7 +783,9 @@ integrationTestRunner({
|
|
|
765
783
|
|
|
766
784
|
## Configuring Providers in Plugin Tests
|
|
767
785
|
|
|
768
|
-
|
|
786
|
+
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()`.
|
|
787
|
+
|
|
788
|
+
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):
|
|
769
789
|
|
|
770
790
|
```typescript
|
|
771
791
|
integrationTestRunner({
|
|
@@ -775,8 +795,8 @@ integrationTestRunner({
|
|
|
775
795
|
tron: {
|
|
776
796
|
providers: [
|
|
777
797
|
{
|
|
778
|
-
resolve: "
|
|
779
|
-
id: "
|
|
798
|
+
resolve: process.cwd() + "/integration-tests/helpers/mock-energy-provider",
|
|
799
|
+
id: "mock-energy",
|
|
780
800
|
options: { apiKey: "test-key" },
|
|
781
801
|
},
|
|
782
802
|
],
|
|
@@ -786,10 +806,135 @@ integrationTestRunner({
|
|
|
786
806
|
})
|
|
787
807
|
```
|
|
788
808
|
|
|
809
|
+
The helper file must be a real module:
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
// integration-tests/helpers/mock-energy-provider.ts
|
|
813
|
+
import { ModuleProvider } from "@acmekit/framework/utils"
|
|
814
|
+
|
|
815
|
+
class MockEnergyService {
|
|
816
|
+
static identifier = "mock-energy"
|
|
817
|
+
// implement provider interface methods
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export default ModuleProvider("tron", { services: [MockEnergyService] })
|
|
821
|
+
```
|
|
822
|
+
|
|
789
823
|
`pluginModuleOptions` is merged into the discovered module config AFTER `pluginOptions`, so module-specific options override plugin-level ones.
|
|
790
824
|
|
|
791
825
|
---
|
|
792
826
|
|
|
827
|
+
## Auto-Injected Infrastructure Mocks
|
|
828
|
+
|
|
829
|
+
The test runner automatically injects mock implementations for infrastructure modules in plugin and module modes. You do NOT need to manually inject these:
|
|
830
|
+
|
|
831
|
+
| Module | Mock class | Behavior |
|
|
832
|
+
|---|---|---|
|
|
833
|
+
| `Modules.EVENT_BUS` | `MockEventBusService` | Accumulates events — call `.getEmittedEvents()` to inspect |
|
|
834
|
+
| `Modules.LOCKING` | `MockLockingService` | In-memory locking — `execute()` runs job immediately, acquire/release tracked |
|
|
835
|
+
| `Modules.SECRETS` | `MockSecretsService` | In-memory store — pre-populate with `.setSecret(id, value)` |
|
|
836
|
+
|
|
837
|
+
Override any mock via `injectedDependencies` if needed — user overrides take precedence.
|
|
838
|
+
|
|
839
|
+
### Asserting emitted events (no spy needed)
|
|
840
|
+
|
|
841
|
+
```typescript
|
|
842
|
+
// Resolve the mock event bus from the container
|
|
843
|
+
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
844
|
+
|
|
845
|
+
// Run your workflow/operation that emits events
|
|
846
|
+
await createGreetingsWorkflow(container).run({ input: { greetings: [{ message: "Hi" }] } })
|
|
847
|
+
|
|
848
|
+
// Inspect emitted events directly — no jest.spyOn needed
|
|
849
|
+
const events = eventBus.getEmittedEvents()
|
|
850
|
+
const greetingEvent = events.find((e: any) => e.name === "greeting.created")
|
|
851
|
+
expect(greetingEvent).toBeDefined()
|
|
852
|
+
expect(greetingEvent.data).toEqual(expect.objectContaining({ id: expect.any(String) }))
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
> Event objects have `{ name, data, metadata?, options? }`. The property is `name`, NOT `eventName` — `emitEventStep` maps `eventName` to `name` internally.
|
|
856
|
+
|
|
857
|
+
### Pre-populating secrets for tests
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
integrationTestRunner({
|
|
861
|
+
mode: "plugin",
|
|
862
|
+
pluginPath: process.cwd(),
|
|
863
|
+
testSuite: ({ container }) => {
|
|
864
|
+
beforeEach(() => {
|
|
865
|
+
const secrets = container.resolve(Modules.SECRETS)
|
|
866
|
+
secrets.setSecret("prod/wallet", { address: "0xtest123" })
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
it("resolves wallet address from secrets", async () => {
|
|
870
|
+
const addr = await container.resolve(Modules.SECRETS).getKey("prod/wallet", "address")
|
|
871
|
+
expect(addr).toBe("0xtest123")
|
|
872
|
+
})
|
|
873
|
+
},
|
|
874
|
+
})
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
## injectedDependencies vs pluginModuleOptions
|
|
880
|
+
|
|
881
|
+
| | `injectedDependencies` | `pluginModuleOptions` |
|
|
882
|
+
|---|---|---|
|
|
883
|
+
| **Injects into** | Root container | Per-module config |
|
|
884
|
+
| **Use for** | Infrastructure mocks (locking, secrets, custom services) | Provider registration, module-specific config |
|
|
885
|
+
| **Shape** | `{ [Modules.X]: mockInstance }` | `{ moduleName: { providers: [...], options } }` |
|
|
886
|
+
| **Path resolution** | N/A | Must be **absolute** (`process.cwd() + "/path"`) |
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## Client Routes Require API Key
|
|
891
|
+
|
|
892
|
+
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.
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
// WRONG — 401 "Client API key required"
|
|
896
|
+
const response = await api.get("/client/plugin/greetings") // ❌
|
|
897
|
+
|
|
898
|
+
// RIGHT — create client API key and pass header
|
|
899
|
+
const apiKey = await apiKeyModule.createApiKeys({
|
|
900
|
+
title: "Test Client Key",
|
|
901
|
+
type: ApiKeyType.CLIENT,
|
|
902
|
+
created_by: "test",
|
|
903
|
+
})
|
|
904
|
+
const response = await api.get("/client/plugin/greetings", {
|
|
905
|
+
headers: { [CLIENT_API_KEY_HEADER]: apiKey.token },
|
|
906
|
+
})
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
## Known Environment Issues
|
|
912
|
+
|
|
913
|
+
### Stale `.acmekit/server/` Build Output
|
|
914
|
+
|
|
915
|
+
**Symptom:** `__joinerConfig is not a function`, missing module services, `Cannot find module` errors when tests previously worked.
|
|
916
|
+
|
|
917
|
+
**Cause:** Plugin tests load compiled JS from `.acmekit/server/src/`, not TypeScript source. If you edited source files but didn't rebuild, the compiled output is stale.
|
|
918
|
+
|
|
919
|
+
**Fix:** Run `pnpm build` (one-time) or keep `pnpm dev` running (watch mode). After build, re-run the failing test.
|
|
920
|
+
|
|
921
|
+
### Database Teardown Permission Errors
|
|
922
|
+
|
|
923
|
+
**Symptom:** `permission denied for schema public` or `cannot drop schema` during test teardown (afterAll).
|
|
924
|
+
|
|
925
|
+
**Cause:** PostgreSQL user lacks `CREATE`/`DROP` privileges on the `public` schema, or concurrent test processes hold locks.
|
|
926
|
+
|
|
927
|
+
**Fix:**
|
|
928
|
+
```bash
|
|
929
|
+
# Grant full privileges to the test user (run once)
|
|
930
|
+
psql -U postgres -c "GRANT ALL ON SCHEMA public TO <your_user>;"
|
|
931
|
+
psql -U postgres -c "ALTER USER <your_user> CREATEDB;"
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
This is an environment setup issue, not a test code issue. If teardown fails but all tests passed, the test results are still valid.
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
793
938
|
## Anti-Patterns — NEVER Do These
|
|
794
939
|
|
|
795
940
|
```typescript
|
|
@@ -915,4 +1060,26 @@ const re = new RegExp("^(\\*|[0-9]+)(\\/[0-9]+)?$")
|
|
|
915
1060
|
const result = await provider.process(bad)
|
|
916
1061
|
expect(result.success).toBe(false) // ❌ actually throws
|
|
917
1062
|
// RIGHT — read implementation first to check if it throws or returns
|
|
1063
|
+
|
|
1064
|
+
// WRONG — jest virtual mock for providers (require.resolve bypasses jest)
|
|
1065
|
+
jest.mock("../../src/providers/mock-provider", () => ({ ... }), { virtual: true }) // ❌
|
|
1066
|
+
// RIGHT — create a real file and use absolute path
|
|
1067
|
+
// integration-tests/helpers/mock-provider.ts (real file with ModuleProvider export)
|
|
1068
|
+
pluginModuleOptions: { mod: { providers: [{ resolve: process.cwd() + "/integration-tests/helpers/mock-provider" }] } }
|
|
1069
|
+
|
|
1070
|
+
// WRONG — relative provider resolve path (resolved from modules-sdk dist, not test file)
|
|
1071
|
+
{ resolve: "../../src/providers/my-provider" } // ❌
|
|
1072
|
+
// RIGHT
|
|
1073
|
+
{ resolve: process.cwd() + "/src/providers/my-provider" }
|
|
1074
|
+
|
|
1075
|
+
// WRONG — using jest.spyOn to inspect emitted events
|
|
1076
|
+
jest.spyOn(MockEventBusService.prototype, "emit") // ❌ fragile, complex extraction
|
|
1077
|
+
// RIGHT — use built-in event accumulation
|
|
1078
|
+
const eventBus = container.resolve(Modules.EVENT_BUS)
|
|
1079
|
+
const events = eventBus.getEmittedEvents()
|
|
1080
|
+
|
|
1081
|
+
// WRONG — checking event.eventName (emitEventStep maps eventName → name internally)
|
|
1082
|
+
expect(event.eventName).toBe("greeting.created") // ❌ property doesn't exist
|
|
1083
|
+
// RIGHT
|
|
1084
|
+
expect(event.name).toBe("greeting.created")
|
|
918
1085
|
```
|
|
@@ -502,6 +502,8 @@ dismissRemoteLinkStep([{
|
|
|
502
502
|
|
|
503
503
|
Always resolve services fresh from `container`. Never capture from closure in compensation.
|
|
504
504
|
|
|
505
|
+
**Workflow steps have the full application container** — they can resolve ANY module. Module providers and services are scoped to their parent module and cannot resolve other modules. When a provider needs cross-module dependencies, the step resolves them and passes them through the service to the provider as method parameters (see `modules.md` → Provider Container Scoping).
|
|
506
|
+
|
|
505
507
|
```typescript
|
|
506
508
|
// Typed resolution (preferred)
|
|
507
509
|
const service = container.resolve<IBlogModuleService>(BLOG_MODULE)
|
|
@@ -376,6 +376,16 @@ Each widget file exports exactly one default component and one `config`.
|
|
|
376
376
|
|
|
377
377
|
Widgets receive no props. Fetch all data internally using hooks.
|
|
378
378
|
|
|
379
|
+
### Admin Dependencies
|
|
380
|
+
|
|
381
|
+
Form-related packages are provided by acmekit — do NOT install them separately:
|
|
382
|
+
|
|
383
|
+
- `react-hook-form` — import `useForm` from `"react-hook-form"` (add to `peerDependencies` + `devDependencies`)
|
|
384
|
+
- `@hookform/resolvers` — import `zodResolver` from `"@hookform/resolvers/zod"` (add to `peerDependencies` + `devDependencies`)
|
|
385
|
+
- `zod` — import as `import * as zod from "@acmekit/deps/zod"` (NOT `from "zod"`)
|
|
386
|
+
|
|
387
|
+
**Plugin dependency placement:** These packages must be in `peerDependencies` (host provides them), mirrored in `devDependencies` (for local dev). NEVER put them in only `devDependencies`.
|
|
388
|
+
|
|
379
389
|
### Prefer Shared SDK Over Raw Fetch
|
|
380
390
|
|
|
381
391
|
Use `src/admin/lib/sdk.ts` for backend calls so auth, path prefixes, and route typings stay consistent:
|
|
@@ -15,6 +15,13 @@ Generate tests for AcmeKit plugins using `integrationTestRunner` with the correc
|
|
|
15
15
|
- Plugin integration tests: !`ls integration-tests/plugin/*.spec.ts 2>/dev/null || echo "(none)"`
|
|
16
16
|
- HTTP integration tests: !`ls integration-tests/http/*.spec.ts 2>/dev/null || echo "(none)"`
|
|
17
17
|
|
|
18
|
+
## Pre-Flight Checklist (MANDATORY — do these BEFORE writing any test)
|
|
19
|
+
|
|
20
|
+
1. **Build the plugin.** Run `pnpm build` (or verify `pnpm dev` is running). Stale `.acmekit/server/` causes `__joinerConfig is not a function` and missing services.
|
|
21
|
+
2. **Verify jest config buckets.** Read `jest.config.js` — confirm the bucket you need exists (`integration:plugin`, `integration:http`, `integration:modules`, `unit`). If missing, add the entry and create the directory first.
|
|
22
|
+
3. **Read source code under test.** Understand method signatures, return types, error handling.
|
|
23
|
+
4. **Read mock interfaces.** If using auto-injected mocks, read their source to verify method names. Don't guess from real service interfaces.
|
|
24
|
+
|
|
18
25
|
## Critical Gotchas — Every Test Must Get These Right
|
|
19
26
|
|
|
20
27
|
1. **Unified runner only.** `import { integrationTestRunner } from "@acmekit/test-utils"`. NEVER use `pluginIntegrationTestRunner` or `moduleIntegrationTestRunner` — those are deprecated.
|
|
@@ -26,6 +33,7 @@ Generate tests for AcmeKit plugins using `integrationTestRunner` with the correc
|
|
|
26
33
|
7. **HTTP mode requires auth.** `mode: "plugin"` + `http: true` boots the full framework — inline JWT + client API key setup in `beforeEach`.
|
|
27
34
|
8. **Axios throws on 4xx/5xx.** Use `.catch((e: any) => e)` for error assertions in HTTP tests.
|
|
28
35
|
9. **MockEventBusService emit takes arrays.** In plugin container mode, `emit` receives `[{ name, data }]` (array). Real event bus uses `{ name, data }` (single object).
|
|
36
|
+
10. **`pluginPath` is MANDATORY.** Always use `pluginPath: process.cwd()`. Omitting it causes `Cannot read properties of undefined`.
|
|
29
37
|
|
|
30
38
|
## Instructions
|
|
31
39
|
|
|
@@ -8,7 +8,8 @@ You are a senior AcmeKit plugin developer. Build distributable plugins that add
|
|
|
8
8
|
|
|
9
9
|
- Edit files in `src/types/generated/` — overwritten by `npx acmekit generate types`
|
|
10
10
|
- Import from `.acmekit/types/` or `src/types/generated/` directly — use `InferTypeOf` or barrel exports
|
|
11
|
-
- Import `zod`, `awilix`, or `pg` directly — use `@acmekit/framework/zod`, `@acmekit/framework/awilix`, `@acmekit/framework/pg`
|
|
11
|
+
- Import `zod`, `awilix`, or `pg` directly — backend: use `@acmekit/framework/zod`, `@acmekit/framework/awilix`, `@acmekit/framework/pg`; admin UI: use `@acmekit/deps/zod`
|
|
12
|
+
- Add `react-hook-form` or `@hookform/resolvers` to only `devDependencies` — they are provided by acmekit, must be in `peerDependencies` (+ `devDependencies` mirror)
|
|
12
13
|
- Use string literals for container keys — use `ContainerRegistrationKeys.*` and `Modules.*`
|
|
13
14
|
- Call services directly for mutations in routes — use workflows
|
|
14
15
|
- Put business logic in workflow steps — steps are thin wrappers; service methods own orchestration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acmekit/acmekit",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.88",
|
|
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.88"
|
|
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.88",
|
|
56
|
+
"@acmekit/analytics": "2.13.88",
|
|
57
|
+
"@acmekit/analytics-local": "2.13.88",
|
|
58
|
+
"@acmekit/analytics-posthog": "2.13.88",
|
|
59
|
+
"@acmekit/api-key": "2.13.88",
|
|
60
|
+
"@acmekit/auth": "2.13.88",
|
|
61
|
+
"@acmekit/auth-emailpass": "2.13.88",
|
|
62
|
+
"@acmekit/auth-github": "2.13.88",
|
|
63
|
+
"@acmekit/auth-google": "2.13.88",
|
|
64
|
+
"@acmekit/cache-inmemory": "2.13.88",
|
|
65
|
+
"@acmekit/cache-redis": "2.13.88",
|
|
66
|
+
"@acmekit/caching": "2.13.88",
|
|
67
|
+
"@acmekit/caching-redis": "2.13.88",
|
|
68
|
+
"@acmekit/core-flows": "2.13.88",
|
|
69
|
+
"@acmekit/event-bus-local": "2.13.88",
|
|
70
|
+
"@acmekit/event-bus-redis": "2.13.88",
|
|
71
|
+
"@acmekit/file": "2.13.88",
|
|
72
|
+
"@acmekit/file-local": "2.13.88",
|
|
73
|
+
"@acmekit/file-s3": "2.13.88",
|
|
74
|
+
"@acmekit/index": "2.13.88",
|
|
75
|
+
"@acmekit/link-modules": "2.13.88",
|
|
76
|
+
"@acmekit/locking": "2.13.88",
|
|
77
|
+
"@acmekit/locking-postgres": "2.13.88",
|
|
78
|
+
"@acmekit/locking-redis": "2.13.88",
|
|
79
|
+
"@acmekit/notification": "2.13.88",
|
|
80
|
+
"@acmekit/notification-local": "2.13.88",
|
|
81
|
+
"@acmekit/notification-sendgrid": "2.13.88",
|
|
82
|
+
"@acmekit/rbac": "2.13.88",
|
|
83
|
+
"@acmekit/secrets-aws": "2.13.88",
|
|
84
|
+
"@acmekit/secrets-local": "2.13.88",
|
|
85
|
+
"@acmekit/settings": "2.13.88",
|
|
86
|
+
"@acmekit/telemetry": "2.13.88",
|
|
87
|
+
"@acmekit/translation": "2.13.88",
|
|
88
|
+
"@acmekit/user": "2.13.88",
|
|
89
|
+
"@acmekit/workflow-engine-inmemory": "2.13.88",
|
|
90
|
+
"@acmekit/workflow-engine-redis": "2.13.88",
|
|
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.88",
|
|
110
110
|
"@jimsheen/yalc": "^1.2.2",
|
|
111
111
|
"@swc/core": "^1.7.28",
|
|
112
112
|
"posthog-node": "^5.11.0",
|