@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.
- package/cli.schema.json +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +14 -0
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +14 -0
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +7 -30
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +24 -5
- package/dist/.pikku/function/pikku-functions.gen.js +3 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +4 -2
- package/dist/.pikku/pikku-services.gen.js +2 -0
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.json +0 -248
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -0
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +5 -1
- package/dist/.pikku/schemas/schemas/FabricAddonVerifyInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricAddonVerifyOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/deploy/analyzer/analyzer.d.ts +6 -0
- package/dist/src/deploy/analyzer/analyzer.js +5 -4
- package/dist/src/deploy/build-pipeline.d.ts +5 -1
- package/dist/src/deploy/build-pipeline.js +5 -5
- package/dist/src/deploy/bundler/bun-bundler.d.ts +14 -0
- package/dist/src/deploy/bundler/bun-bundler.js +121 -0
- package/dist/src/deploy/bundler/bundler.d.ts +25 -30
- package/dist/src/deploy/bundler/bundler.interface.d.ts +54 -0
- package/dist/src/deploy/bundler/bundler.interface.js +11 -0
- package/dist/src/deploy/bundler/bundler.js +120 -190
- package/dist/src/deploy/bundler/dep-extractor.d.ts +11 -3
- package/dist/src/deploy/bundler/dep-extractor.js +12 -6
- package/dist/src/deploy/bundler/index.d.ts +5 -2
- package/dist/src/deploy/bundler/index.js +4 -2
- package/dist/src/deploy/bundler/node-bundler.d.ts +13 -0
- package/dist/src/deploy/bundler/node-bundler.js +80 -0
- package/dist/src/fabric/fabric-commands.d.ts +37 -0
- package/dist/src/fabric/fabric-commands.js +8 -0
- package/dist/src/fabric/functions/addon-verify.function.d.ts +54 -0
- package/dist/src/fabric/functions/addon-verify.function.js +153 -0
- package/dist/src/fabric/functions/llm-key.function.js +1 -1
- package/dist/src/fabric/functions/publish.function.js +8 -3
- package/dist/src/functions/commands/deploy-apply.js +3 -1
- package/dist/src/functions/commands/deploy-plan.js +3 -1
- package/dist/src/functions/commands/dev.js +11 -45
- package/dist/src/functions/db/db-codegen.js +14 -0
- package/dist/src/functions/wirings/functions/serialize-function-types.js +6 -29
- package/dist/src/server/bun-server-runner.d.ts +17 -0
- package/dist/src/server/bun-server-runner.js +25 -0
- package/dist/src/server/dev-server-runner.interface.d.ts +31 -0
- package/dist/src/server/dev-server-runner.interface.js +11 -0
- package/dist/src/server/node-server-runner.d.ts +12 -0
- package/dist/src/server/node-server-runner.js +30 -0
- package/dist/src/services.js +10 -0
- package/dist/src/utils/parse-cli-filters.d.ts +1 -0
- package/dist/src/utils/parse-cli-filters.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/skills/pikku-addon/SKILL.md +25 -117
- package/skills/pikku-addon/references/addon-package-manifest.md +63 -0
- package/skills/pikku-cli/SKILL.md +7 -93
- package/skills/pikku-cli/references/complete-example.md +82 -0
- package/skills/pikku-concepts/SKILL.md +17 -69
- package/skills/pikku-concepts/references/concept-mapping.md +37 -13
- package/skills/pikku-concepts/references/packages.md +29 -0
- package/skills/pikku-http/SKILL.md +14 -105
- package/skills/pikku-http/references/http-options.md +57 -0
- package/skills/pikku-middleware/SKILL.md +11 -68
- package/skills/pikku-middleware/references/middleware-patterns.md +61 -0
- package/skills/pikku-realtime/SKILL.md +56 -105
- package/skills/pikku-realtime/references/other-routes.md +23 -0
- package/skills/pikku-services/SKILL.md +25 -108
- package/skills/pikku-services/references/audit-wire-service.md +34 -0
- package/skills/pikku-testing/SKILL.md +51 -359
- package/skills/pikku-testing/references/cucumber-bdd-testing.md +176 -0
- package/skills/pikku-workflow/SKILL.md +93 -259
- 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 #
|
|
27
|
-
pikku info middleware --verbose #
|
|
26
|
+
pikku info functions --verbose # existing functions + their middleware/permissions
|
|
27
|
+
pikku info middleware --verbose # middleware applied
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Cucumber / BDD (feature files)
|
|
31
31
|
|
|
32
|
-
**
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|