@pikku/cli 0.12.55 → 0.12.56

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.
Files changed (125) hide show
  1. package/cli.schema.json +1 -1
  2. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  3. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  4. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  5. package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
  6. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  7. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  8. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  9. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +14 -0
  11. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +14 -0
  15. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  17. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  18. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  19. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  20. package/dist/.pikku/function/pikku-function-types.gen.d.ts +7 -30
  21. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  22. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  23. package/dist/.pikku/function/pikku-functions-meta.gen.json +24 -5
  24. package/dist/.pikku/function/pikku-functions.gen.js +3 -1
  25. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  26. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  27. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  28. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  29. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  31. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  32. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  34. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  36. package/dist/.pikku/pikku-services.gen.d.ts +4 -2
  37. package/dist/.pikku/pikku-services.gen.js +2 -0
  38. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  39. package/dist/.pikku/pikku-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.json +0 -248
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  45. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  47. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -0
  48. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  49. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  50. package/dist/.pikku/schemas/register.gen.js +5 -1
  51. package/dist/.pikku/schemas/schemas/FabricAddonVerifyInput.schema.json +1 -0
  52. package/dist/.pikku/schemas/schemas/FabricAddonVerifyOutput.schema.json +1 -0
  53. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  54. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  55. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  56. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  57. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  58. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  59. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  60. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  61. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  62. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  63. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  64. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  65. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  66. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  67. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  68. package/dist/bin/pikku-bin.mjs +2 -2
  69. package/dist/src/deploy/analyzer/analyzer.d.ts +6 -0
  70. package/dist/src/deploy/analyzer/analyzer.js +5 -4
  71. package/dist/src/deploy/build-pipeline.d.ts +5 -1
  72. package/dist/src/deploy/build-pipeline.js +5 -5
  73. package/dist/src/deploy/bundler/bun-bundler.d.ts +14 -0
  74. package/dist/src/deploy/bundler/bun-bundler.js +121 -0
  75. package/dist/src/deploy/bundler/bundler.d.ts +25 -30
  76. package/dist/src/deploy/bundler/bundler.interface.d.ts +54 -0
  77. package/dist/src/deploy/bundler/bundler.interface.js +11 -0
  78. package/dist/src/deploy/bundler/bundler.js +120 -190
  79. package/dist/src/deploy/bundler/dep-extractor.d.ts +11 -3
  80. package/dist/src/deploy/bundler/dep-extractor.js +12 -6
  81. package/dist/src/deploy/bundler/index.d.ts +5 -2
  82. package/dist/src/deploy/bundler/index.js +4 -2
  83. package/dist/src/deploy/bundler/node-bundler.d.ts +13 -0
  84. package/dist/src/deploy/bundler/node-bundler.js +80 -0
  85. package/dist/src/fabric/fabric-commands.d.ts +37 -0
  86. package/dist/src/fabric/fabric-commands.js +8 -0
  87. package/dist/src/fabric/functions/addon-verify.function.d.ts +54 -0
  88. package/dist/src/fabric/functions/addon-verify.function.js +153 -0
  89. package/dist/src/fabric/functions/llm-key.function.js +1 -1
  90. package/dist/src/fabric/functions/publish.function.js +8 -3
  91. package/dist/src/functions/commands/deploy-apply.js +3 -1
  92. package/dist/src/functions/commands/deploy-plan.js +3 -1
  93. package/dist/src/functions/commands/dev.js +11 -45
  94. package/dist/src/functions/db/db-codegen.js +14 -0
  95. package/dist/src/functions/wirings/functions/serialize-function-types.js +6 -29
  96. package/dist/src/server/bun-server-runner.d.ts +17 -0
  97. package/dist/src/server/bun-server-runner.js +25 -0
  98. package/dist/src/server/dev-server-runner.interface.d.ts +31 -0
  99. package/dist/src/server/dev-server-runner.interface.js +11 -0
  100. package/dist/src/server/node-server-runner.d.ts +12 -0
  101. package/dist/src/server/node-server-runner.js +30 -0
  102. package/dist/src/services.js +10 -0
  103. package/dist/src/utils/parse-cli-filters.d.ts +1 -0
  104. package/dist/src/utils/parse-cli-filters.js +1 -0
  105. package/dist/tsconfig.tsbuildinfo +1 -1
  106. package/package.json +3 -3
  107. package/skills/pikku-addon/SKILL.md +25 -117
  108. package/skills/pikku-addon/references/addon-package-manifest.md +63 -0
  109. package/skills/pikku-cli/SKILL.md +7 -93
  110. package/skills/pikku-cli/references/complete-example.md +82 -0
  111. package/skills/pikku-concepts/SKILL.md +17 -69
  112. package/skills/pikku-concepts/references/concept-mapping.md +37 -13
  113. package/skills/pikku-concepts/references/packages.md +29 -0
  114. package/skills/pikku-http/SKILL.md +14 -105
  115. package/skills/pikku-http/references/http-options.md +57 -0
  116. package/skills/pikku-middleware/SKILL.md +11 -68
  117. package/skills/pikku-middleware/references/middleware-patterns.md +61 -0
  118. package/skills/pikku-realtime/SKILL.md +56 -105
  119. package/skills/pikku-realtime/references/other-routes.md +23 -0
  120. package/skills/pikku-services/SKILL.md +25 -108
  121. package/skills/pikku-services/references/audit-wire-service.md +34 -0
  122. package/skills/pikku-testing/SKILL.md +51 -359
  123. package/skills/pikku-testing/references/cucumber-bdd-testing.md +176 -0
  124. package/skills/pikku-workflow/SKILL.md +93 -259
  125. package/skills/pikku-workflow/references/workflow-reference.md +63 -0
@@ -23,130 +23,25 @@ Pikku functions are pure business logic — no HTTP, no framework — making the
23
23
  ## Before You Start
24
24
 
25
25
  ```bash
26
- pikku info functions --verbose # See existing functions and their middleware/permissions
27
- pikku info middleware --verbose # See middleware applied
26
+ pikku info functions --verbose # existing functions + their middleware/permissions
27
+ pikku info middleware --verbose # middleware applied
28
28
  ```
29
29
 
30
- ## Personas and Named Data — Never Inline JSON
30
+ ## Cucumber / BDD (feature files)
31
31
 
32
- **Never put JSON, inline tables, or raw values inside `.feature` files.** Feature files are for human-readable scenarios. All test data belongs in typed maps that step definitions look up by name.
33
-
34
- `@pikku/cucumber` exports `PersonaData<T>` for this purpose — a typed map that throws a clear error when a name is missing.
35
-
36
- ### Personas
37
-
38
- A **persona** is a named user: their login credentials plus the session they hold after authenticating. Define all personas in one file:
39
-
40
- ```ts
41
- // tests/tests/support/personas.ts
42
- import { PersonaData } from '@pikku/cucumber'
43
-
44
- export const logins = new PersonaData({
45
- yasser: { email: 'yasser@example.com', password: 'hunter2' },
46
- guest: { email: 'guest@example.com', password: 'guest123' },
47
- })
48
- ```
49
-
50
- A persona step logs in and stores the session in the world so every subsequent call by that persona carries it automatically:
51
-
52
- ```ts
53
- // tests/tests/support/steps/auth.steps.ts
54
- import { Given } from '@cucumber/cucumber'
55
- import { logins } from '../personas.js'
56
-
57
- Given('{string} logs in', async function (name: string) {
58
- await this.call(name, 'auth:login', logins.get(name))
59
- const { token } = this.lastResult as { token: string }
60
- this.setSession(name, { token })
61
- })
62
- ```
63
-
64
- ### Named Domain Data
65
-
66
- Use a separate `PersonaData` map for each domain concept. Name entries after real-world meaning, not technical fields:
67
-
68
- ```ts
69
- // tests/tests/support/data/cards.ts
70
- import { PersonaData } from '@pikku/cucumber'
71
-
72
- export const cards = new PersonaData({
73
- 'writing a blog post': { title: 'Writing a blog post', columnId: 'backlog' },
74
- 'fix the login bug': { title: 'Fix the login bug', columnId: 'in-progress' },
75
- })
76
- ```
77
-
78
- Steps resolve the name and make the call — the feature file never sees raw data:
79
-
80
- ```ts
81
- // tests/tests/support/steps/card.steps.ts
82
- import { When, Then } from '@cucumber/cucumber'
83
- import assert from 'node:assert/strict'
84
- import { cards } from '../data/cards.js'
85
-
86
- When('{string} creates a card for {string}', async function (persona: string, cardName: string) {
87
- await this.call(persona, 'kanban:createCard', cards.get(cardName))
88
- })
89
-
90
- When('{string} gets the card {string}', async function (persona: string, cardName: string) {
91
- const { title } = cards.get(cardName)
92
- await this.call(persona, 'kanban:getCard', { title })
93
- })
94
-
95
- // "the newly created card" — checks the live result against the data map entry
96
- // AND any server-assigned fields (id, createdAt) are present
97
- Then('the result is the newly created card {string}', function (cardName: string) {
98
- const expected = cards.get(cardName)
99
- const result = this.lastResult as typeof expected & { id: string; createdAt: string }
100
- assert.equal(result.title, expected.title)
101
- assert.equal(result.columnId, expected.columnId)
102
- assert.ok(result.id, 'expected server-assigned id')
103
- assert.ok(result.createdAt, 'expected server-assigned createdAt')
104
- })
105
- ```
106
-
107
- The feature file reads naturally:
108
-
109
- ```gherkin
110
- Feature: Card management
111
-
112
- Scenario: Create and retrieve a card
113
- Given 'yasser' logs in
114
- When 'yasser' creates a card for 'writing a blog post'
115
- And 'yasser' gets the card 'writing a blog post'
116
- Then the result is the newly created card 'writing a blog post'
117
- ```
118
-
119
- ### File layout
120
-
121
- ```
122
- tests/tests/support/
123
- personas.ts ← logins PersonaData (one per project)
124
- data/
125
- cards.ts ← cards PersonaData
126
- users.ts ← users PersonaData
127
- steps/
128
- auth.steps.ts ← login / logout steps
129
- card.steps.ts ← card CRUD steps
130
- ```
131
-
132
- Keep one `PersonaData` instance per domain concept. Steps import only what they need — no cross-domain coupling.
32
+ When writing `.feature` files, the golden rule: **never put JSON, inline tables, or raw values inside `.feature` files** all test data goes in typed `PersonaData<T>` maps (from `@pikku/cucumber`) that step definitions look up by name. For personas, named domain data, the support file layout, and the full set of BDD anti-patterns, read `references/cucumber-bdd-testing.md`.
133
33
 
134
34
  ## Coverage-Driven Test Writing
135
35
 
136
36
  When asked to improve or fill test coverage, start with the AI prompt from the coverage command:
137
37
 
138
38
  ```bash
139
- # Run tests and emit an AI-ready prompt listing every uncovered/partial function
140
- pikku tests coverage --ai-out coverage-prompt.md
141
-
142
- # Or skip re-running if you already have fresh coverage data
143
- pikku tests coverage --no-run --ai-out coverage-prompt.md
144
-
145
- # Pipe directly to stdout (e.g. to paste into a chat)
146
- pikku tests coverage --ai-out -
39
+ pikku tests coverage --ai-out coverage-prompt.md # run tests + emit AI-ready prompt of uncovered/partial functions
40
+ pikku tests coverage --no-run --ai-out coverage-prompt.md # skip re-running, use existing coverage data
41
+ pikku tests coverage --ai-out - # pipe to stdout
147
42
  ```
148
43
 
149
- The prompt lists each function that needs work with its status (`uncovered`/`partial`), coverage ratio, missed line numbers, and source file path. Use it as your starting point:
44
+ The prompt lists each function needing work with status (`uncovered`/`partial`), coverage ratio, missed line numbers, and source path. Use it as your starting point:
150
45
 
151
46
  1. Read the prompt to know which functions need Gherkin scenarios.
152
47
  2. Run `pikku meta functions list` or `pikku meta context` to get input/output schemas for those functions.
@@ -157,22 +52,27 @@ See `pikku-concepts` for the core mental model.
157
52
 
158
53
  ## Test Runner Setup
159
54
 
160
- Pikku uses Node.js built-in test runner with tsx for TypeScript:
55
+ Pikku uses the Node.js built-in test runner with tsx for TypeScript:
161
56
 
162
57
  ```bash
163
58
  node --import tsx --test src/**/*.test.ts
164
59
  ```
165
60
 
166
- Standard test file:
167
-
168
61
  ```typescript
169
62
  import { describe, test, beforeEach } from 'node:test'
170
63
  import assert from 'node:assert'
171
64
  ```
172
65
 
66
+ A reusable mock logger / singleton services bag used throughout the examples below:
67
+
68
+ ```typescript
69
+ const mockLogger = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
70
+ const mockSingletonServices = { logger: mockLogger /* + any services your funcs need */ } as any
71
+ ```
72
+
173
73
  ## Level 1: Direct Function Invocation
174
74
 
175
- The simplest approach — call `func` directly with mock services:
75
+ The simplest approach — call `func` directly with mock services. Tests pure business logic: no middleware, permissions, or validation.
176
76
 
177
77
  ```typescript
178
78
  import { describe, test } from 'node:test'
@@ -181,62 +81,31 @@ import assert from 'node:assert'
181
81
  describe('createTodo', () => {
182
82
  test('should create a todo', async () => {
183
83
  const mockServices = {
184
- todoStore: {
185
- add: async (title: string) => ({
186
- id: '1',
187
- title,
188
- completed: false,
189
- }),
190
- },
84
+ todoStore: { add: async (title: string) => ({ id: '1', title, completed: false }) },
191
85
  }
192
-
193
- const result = await createTodo.func(mockServices as any, {
194
- title: 'Buy milk',
195
- })
196
-
86
+ const result = await createTodo.func(mockServices as any, { title: 'Buy milk' })
197
87
  assert.equal(result.title, 'Buy milk')
198
88
  assert.equal(result.completed, false)
199
89
  })
200
90
  })
201
91
  ```
202
92
 
203
- This tests pure business logic — no middleware, no permissions, no validation.
204
-
205
93
  ## Level 2: `runPikkuFunc` (Full Pipeline)
206
94
 
207
- Tests the function through Pikku's middleware, permissions, and schema validation pipeline:
95
+ Tests the function through Pikku's middleware, permissions, and schema-validation pipeline. Always `resetPikkuState()` in `beforeEach`. Register function metadata into `pikkuState(null, 'function', 'meta')`, register the function with `addFunction`, then invoke with `runPikkuFunc`.
208
96
 
209
97
  ```typescript
210
- import { runPikkuFunc } from '@pikku/core'
211
- import { addFunction, addMiddleware, addPermission } from '@pikku/core'
98
+ import { runPikkuFunc, addFunction, addMiddleware, addPermission } from '@pikku/core'
212
99
  import { resetPikkuState, pikkuState } from '@pikku/core'
213
100
 
214
- beforeEach(() => {
215
- resetPikkuState()
216
- })
101
+ beforeEach(() => resetPikkuState())
217
102
 
218
103
  test('should run function with middleware', async () => {
219
- const mockSingletonServices = {
220
- logger: {
221
- info: () => {},
222
- warn: () => {},
223
- error: () => {},
224
- debug: () => {},
225
- },
226
- } as any
227
-
228
- // Register function metadata
229
104
  pikkuState(null, 'function', 'meta')['myFunc'] = {
230
- pikkuFuncId: 'myFunc',
231
- inputSchemaName: null,
232
- outputSchemaName: null,
105
+ pikkuFuncId: 'myFunc', inputSchemaName: null, outputSchemaName: null,
233
106
  }
234
-
235
- // Register the function
236
107
  addFunction('myFunc', {
237
- func: async (services, data) => {
238
- return { greeting: `Hello ${data.name}` }
239
- },
108
+ func: async (services, data) => ({ greeting: `Hello ${data.name}` }),
240
109
  })
241
110
 
242
111
  const result = await runPikkuFunc('rpc', 'test-wire', 'myFunc', {
@@ -253,40 +122,25 @@ test('should run function with middleware', async () => {
253
122
 
254
123
  ### Testing Middleware Execution Order
255
124
 
125
+ Middleware runs: wiring tags → wiring → func tags → func. Register tag middleware with `addMiddleware(tag, [...])`, reference func tags in the meta's `middleware: [{ type: 'tag', tag }]`, pass wiring middleware via `wireMiddleware` and inherited tags via `inheritedMiddleware`.
126
+
256
127
  ```typescript
257
128
  test('middleware runs in order: wiring tags -> wiring -> func tags -> func', async () => {
258
- const mockSingletonServices = {
259
- logger: {
260
- info: () => {},
261
- warn: () => {},
262
- error: () => {},
263
- debug: () => {},
264
- },
265
- } as any
266
-
267
129
  const order: string[] = []
268
-
269
130
  const createMiddleware =
270
131
  (name: string) => async (services: any, wire: any, next: Function) => {
271
- order.push(name)
272
- await next()
132
+ order.push(name); await next()
273
133
  }
274
134
 
275
135
  addMiddleware('apiTag', [createMiddleware('apiTag')])
276
136
  addMiddleware('funcTag', [createMiddleware('funcTag')])
277
137
 
278
138
  pikkuState(null, 'function', 'meta')['myFunc'] = {
279
- pikkuFuncId: 'myFunc',
280
- inputSchemaName: null,
281
- outputSchemaName: null,
139
+ pikkuFuncId: 'myFunc', inputSchemaName: null, outputSchemaName: null,
282
140
  middleware: [{ type: 'tag', tag: 'funcTag' }],
283
141
  }
284
-
285
142
  addFunction('myFunc', {
286
- func: async () => {
287
- order.push('main')
288
- return 'ok'
289
- },
143
+ func: async () => { order.push('main'); return 'ok' },
290
144
  middleware: [createMiddleware('funcMiddleware')],
291
145
  tags: ['funcTag'],
292
146
  })
@@ -301,40 +155,22 @@ test('middleware runs in order: wiring tags -> wiring -> func tags -> func', asy
301
155
  wire: {},
302
156
  })
303
157
 
304
- assert.deepEqual(order, [
305
- 'apiTag',
306
- 'wiringMiddleware',
307
- 'funcTag',
308
- 'funcMiddleware',
309
- 'main',
310
- ])
158
+ assert.deepEqual(order, ['apiTag', 'wiringMiddleware', 'funcTag', 'funcMiddleware', 'main'])
311
159
  })
312
160
  ```
313
161
 
314
162
  ### Testing Permissions
315
163
 
164
+ Register a denying permission with `addPermission(tag, [...])` and reference it in the meta's `permissions`.
165
+
316
166
  ```typescript
317
167
  test('should reject when permission fails', async () => {
318
- const mockSingletonServices = {
319
- logger: {
320
- info: () => {},
321
- warn: () => {},
322
- error: () => {},
323
- debug: () => {},
324
- },
325
- } as any
326
-
327
- addPermission('admin', [
328
- async () => false, // Always deny
329
- ])
168
+ addPermission('admin', [async () => false]) // always deny
330
169
 
331
170
  pikkuState(null, 'function', 'meta')['adminFunc'] = {
332
- pikkuFuncId: 'adminFunc',
333
- inputSchemaName: null,
334
- outputSchemaName: null,
171
+ pikkuFuncId: 'adminFunc', inputSchemaName: null, outputSchemaName: null,
335
172
  permissions: [{ type: 'tag', tag: 'admin' }],
336
173
  }
337
-
338
174
  addFunction('adminFunc', { func: async () => 'secret' })
339
175
 
340
176
  await assert.rejects(
@@ -352,46 +188,30 @@ test('should reject when permission fails', async () => {
352
188
 
353
189
  ## Level 3: Integration Testing (HTTP)
354
190
 
355
- Test the full HTTP stack using the `fetch` export:
191
+ Test the full HTTP stack using the `fetch` export. Set singleton services and factories into state, register route metadata + function, then `wireHTTP`.
356
192
 
357
193
  ```typescript
358
194
  import { fetch, wireHTTP } from '@pikku/core/http'
359
195
  import { resetPikkuState, pikkuState, addFunction } from '@pikku/core'
360
196
 
361
- const mockSingletonServices = {
362
- logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
363
- } as any
364
-
365
- const listTodos = {
366
- func: async () => ({ todos: [{ id: '1', title: 'Test todo' }] }),
367
- }
197
+ const listTodos = { func: async () => ({ todos: [{ id: '1', title: 'Test todo' }] }) }
368
198
 
369
199
  beforeEach(() => {
370
200
  resetPikkuState()
371
-
372
- // Set up singleton services in state
373
201
  pikkuState(null, 'package', 'singletonServices', mockSingletonServices)
374
- pikkuState(null, 'package', 'factories', {
375
- createWireServices: async () => ({}),
376
- })
202
+ pikkuState(null, 'package', 'factories', { createWireServices: async () => ({}) })
377
203
  })
378
204
 
379
205
  test('GET /todos returns todo list', async () => {
380
- // Register route metadata and function
381
- pikkuState(null, 'http', 'meta')['get'] =
382
- pikkuState(null, 'http', 'meta')['get'] || {}
206
+ pikkuState(null, 'http', 'meta')['get'] = pikkuState(null, 'http', 'meta')['get'] || {}
383
207
  pikkuState(null, 'http', 'meta')['get']['/todos'] = {
384
- pikkuFuncId: 'listTodos',
385
- method: 'get',
386
- route: '/todos',
208
+ pikkuFuncId: 'listTodos', method: 'get', route: '/todos',
387
209
  }
388
210
  addFunction('listTodos', listTodos)
389
211
  wireHTTP({ method: 'get', route: '/todos', func: listTodos })
390
212
 
391
- const request = new Request('http://localhost/todos')
392
- const response = await fetch(request)
213
+ const response = await fetch(new Request('http://localhost/todos'))
393
214
  const data = await response.json()
394
-
395
215
  assert.equal(response.status, 200)
396
216
  assert.ok(Array.isArray(data.todos))
397
217
  })
@@ -410,7 +230,6 @@ describe('LocalVariablesService', () => {
410
230
  test('should get and set variables', () => {
411
231
  const service = new LocalVariablesService({ API_KEY: 'test-key' })
412
232
  assert.equal(service.get('API_KEY'), 'test-key')
413
-
414
233
  service.set('NEW_KEY', 'value')
415
234
  assert.equal(service.get('NEW_KEY'), 'value')
416
235
  })
@@ -419,7 +238,7 @@ describe('LocalVariablesService', () => {
419
238
 
420
239
  ## Testing with Real Services (Verifier Pattern)
421
240
 
422
- For integration testing with a running server:
241
+ For integration testing with a running server, build real services via `pikkuServices`/`pikkuWireServices` and bootstrap a server:
423
242
 
424
243
  ```typescript
425
244
  // services.ts — real service setup for tests
@@ -431,7 +250,6 @@ export const createSingletonServices = pikkuServices(async (config) => {
431
250
  const secrets = new LocalSecretService(variables)
432
251
  return { config, variables, secrets, logger: new ConsoleLogger() }
433
252
  })
434
-
435
253
  export const createWireServices = pikkuWireServices(async () => ({}))
436
254
  ```
437
255
 
@@ -442,51 +260,22 @@ import { createSingletonServices, createWireServices } from './services.js'
442
260
 
443
261
  const config = {}
444
262
  const singletonServices = await createSingletonServices(config)
445
- const server = new PikkuFastifyServer(
446
- config,
447
- singletonServices,
448
- createWireServices
449
- )
263
+ const server = new PikkuFastifyServer(config, singletonServices, createWireServices)
450
264
  await server.init()
451
265
  await server.start()
452
266
  ```
453
267
 
454
268
  ## Common Patterns
455
269
 
456
- ### Mock Logger
457
-
458
- ```typescript
459
- const mockLogger = {
460
- info: () => {},
461
- warn: () => {},
462
- error: () => {},
463
- debug: () => {},
464
- }
465
- ```
466
-
467
- ### Mock Singleton Services
468
-
469
- ```typescript
470
- const mockSingletonServices = {
471
- logger: mockLogger,
472
- todoStore: new InMemoryTodoStore(),
473
- // Add whatever services your functions need
474
- } as any
475
- ```
476
-
477
- ### Reset State Between Tests
478
-
479
- Always reset pikku state in `beforeEach` to isolate tests:
270
+ - **Mock logger / singleton services** — see the reusable bag defined under "Test Runner Setup".
271
+ - **Reset state between tests** — always `resetPikkuState()` in `beforeEach` to isolate tests.
480
272
 
481
273
  ```typescript
482
274
  import { resetPikkuState } from '@pikku/core'
483
-
484
- beforeEach(() => {
485
- resetPikkuState()
486
- })
275
+ beforeEach(() => resetPikkuState())
487
276
  ```
488
277
 
489
- ### Async Error Assertions
278
+ - **Async error assertions**:
490
279
 
491
280
  ```typescript
492
281
  await assert.rejects(
@@ -503,9 +292,7 @@ export const createTodo = pikkuSessionlessFunc({
503
292
  description: 'Create a todo',
504
293
  input: z.object({ title: z.string().min(1) }),
505
294
  output: z.object({ id: z.string(), title: z.string() }),
506
- func: async ({ todoStore }, { title }) => {
507
- return todoStore.add(title)
508
- },
295
+ func: async ({ todoStore }, { title }) => todoStore.add(title),
509
296
  })
510
297
 
511
298
  // functions/todos.test.ts
@@ -514,123 +301,28 @@ import assert from 'node:assert'
514
301
 
515
302
  class MockTodoStore {
516
303
  private todos: any[] = []
517
-
518
304
  async add(title: string) {
519
305
  const todo = { id: String(this.todos.length + 1), title, completed: false }
520
306
  this.todos.push(todo)
521
307
  return todo
522
308
  }
523
-
524
- async list() {
525
- return this.todos
526
- }
309
+ async list() { return this.todos }
527
310
  }
528
311
 
529
312
  describe('createTodo', () => {
530
313
  let todoStore: MockTodoStore
531
-
532
- beforeEach(() => {
533
- todoStore = new MockTodoStore()
534
- })
314
+ beforeEach(() => { todoStore = new MockTodoStore() })
535
315
 
536
316
  test('creates a todo with the given title', async () => {
537
- const result = await createTodo.func({ todoStore } as any, {
538
- title: 'Buy milk',
539
- })
540
-
317
+ const result = await createTodo.func({ todoStore } as any, { title: 'Buy milk' })
541
318
  assert.equal(result.id, '1')
542
319
  assert.equal(result.title, 'Buy milk')
543
320
  })
544
321
 
545
322
  test('increments IDs', async () => {
546
323
  await createTodo.func({ todoStore } as any, { title: 'First' })
547
- const second = await createTodo.func({ todoStore } as any, {
548
- title: 'Second',
549
- })
550
-
324
+ const second = await createTodo.func({ todoStore } as any, { title: 'Second' })
551
325
  assert.equal(second.id, '2')
552
326
  })
553
327
  })
554
328
  ```
555
-
556
- ## Anti-Patterns
557
-
558
- ### Inline data in feature files
559
-
560
- **Wrong** — raw values and JSON in `.feature` files make scenarios brittle and unreadable:
561
-
562
- ```gherkin
563
- When I call 'kanban:createCard' with {"title": "My card", "columnId": "backlog"}
564
- And I call 'kanban:getCard' with {"title": "My card"}
565
- Then the result title is "My card"
566
- ```
567
-
568
- **Right** — named references resolved by step definitions:
569
-
570
- ```gherkin
571
- When 'yasser' creates a card for 'writing a blog post'
572
- And 'yasser' gets the card 'writing a blog post'
573
- Then the result is the newly created card 'writing a blog post'
574
- ```
575
-
576
- ### Feature-coupled step definitions
577
-
578
- Steps tied to one feature can't be reused and cause duplication. Organise by **domain concept**, not by feature:
579
-
580
- ```
581
- Wrong: Right:
582
- steps/
583
- edit_work_experience.ts → steps/
584
- edit_languages.ts → auth.steps.ts
585
- edit_education.ts → profile.steps.ts
586
- card.steps.ts
587
- ```
588
-
589
- Name step files after the domain they cover. A login step belongs in `auth.steps.ts` regardless of which feature needs it.
590
-
591
- ### Conjunction steps
592
-
593
- Don't combine multiple actions into a single step — it makes reuse impossible:
594
-
595
- ```gherkin
596
- # Wrong — two actions in one step
597
- Given 'yasser' is logged in and has created a card
598
- ```
599
-
600
- ```gherkin
601
- # Right — atomic steps, composable via And
602
- Given 'yasser' logs in
603
- And 'yasser' creates a card for 'writing a blog post'
604
- ```
605
-
606
- Use `And` / `But` for a reason: each step should do exactly one thing.
607
-
608
- ### Asserting in When steps
609
-
610
- `When` steps perform actions; `Then` steps assert outcomes. Mixing them hides intent:
611
-
612
- ```gherkin
613
- # Wrong
614
- When 'yasser' creates a card and the title is 'writing a blog post'
615
-
616
- # Right
617
- When 'yasser' creates a card for 'writing a blog post'
618
- Then the call succeeds
619
- ```
620
-
621
- ### Hard-coding persona data in step definitions
622
-
623
- Credentials and test inputs embedded in step code can't be reused across scenarios and break when data changes:
624
-
625
- ```ts
626
- // Wrong
627
- Given('{string} logs in', async function (name: string) {
628
- await this.call(name, 'auth:login', { email: 'yasser@example.com', password: 'hunter2' })
629
- })
630
-
631
- // Right — look up from PersonaData
632
- Given('{string} logs in', async function (name: string) {
633
- await this.call(name, 'auth:login', logins.get(name))
634
- this.setSession(name, (this.lastResult as { token: string }))
635
- })
636
- ```