@pikku/cli 0.12.25 → 0.12.27
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/console-app/assets/index-BERGDBO9.js +228 -0
- package/console-app/assets/{index-C52h1B_L.css → index-CQ29NRyR.css} +1 -1
- package/console-app/index.html +2 -2
- 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-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 +9 -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 +1 -1
- 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 +140 -125
- 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 +1 -1
- 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.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 +11 -10
- 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 +11 -9
- package/dist/.pikku/schemas/schemas/DbAuditInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuTestsCoverageInput.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/cli.wiring.js +8 -0
- package/dist/src/fabric/functions/validate-core.js +6 -6
- package/dist/src/fabric/functions/validate.function.js +1 -1
- package/dist/src/functions/commands/db-audit.d.ts +1 -0
- package/dist/src/functions/commands/db-audit.js +67 -0
- package/dist/src/functions/commands/db-migrate.js +5 -8
- package/dist/src/functions/commands/db-reset.js +9 -8
- package/dist/src/functions/commands/db-seed.js +9 -8
- package/dist/src/functions/commands/db-shared.d.ts +2 -4
- package/dist/src/functions/commands/db-shared.js +15 -5
- package/dist/src/functions/commands/dev.js +14 -8
- package/dist/src/functions/commands/new-addon.js +2 -2
- package/dist/src/functions/commands/tests-coverage.d.ts +3 -0
- package/dist/src/functions/commands/tests-coverage.js +34 -0
- package/dist/src/functions/db/annotation-parser.d.ts +31 -0
- package/dist/src/functions/db/annotation-parser.js +93 -0
- package/dist/src/functions/db/db-codegen.d.ts +24 -0
- package/dist/src/functions/db/db-codegen.js +276 -0
- package/dist/src/functions/db/db-introspector.d.ts +15 -0
- package/dist/src/functions/db/db-introspector.js +1 -0
- package/dist/src/functions/db/db-migrator.d.ts +32 -0
- package/dist/src/functions/db/db-migrator.js +65 -0
- package/dist/src/functions/db/local-db.d.ts +26 -32
- package/dist/src/functions/db/local-db.js +100 -53
- package/dist/src/functions/db/postgres/postgres-introspector.d.ts +10 -0
- package/dist/src/functions/db/postgres/postgres-introspector.js +54 -0
- package/dist/src/functions/db/postgres/postgres-migrator.d.ts +9 -0
- package/dist/src/functions/db/postgres/postgres-migrator.js +32 -0
- package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +9 -0
- package/dist/src/functions/db/sqlite/sqlite-introspector.js +35 -0
- package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +10 -0
- package/dist/src/functions/db/sqlite/sqlite-migrator.js +36 -0
- package/dist/src/functions/validate/workspace-validate.js +7 -3
- package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +2 -1
- package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -4
- package/dist/src/functions/wirings/functions/serialize-addon-types.js +1 -1
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/services.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -4
- package/skills/pikku-middleware/SKILL.md +283 -0
- package/skills/pikku-permissions/SKILL.md +165 -0
- package/skills/pikku-security/SKILL.md +38 -177
- package/skills/pikku-tag-middleware/SKILL.md +13 -0
- package/skills/pikku-testing/SKILL.md +208 -0
- package/console-app/assets/index-D4DgafuS.js +0 -232
- package/dist/src/functions/db/sql-migrator.d.ts +0 -26
- package/dist/src/functions/db/sql-migrator.js +0 -104
- package/dist/src/functions/db/sqlite-codegen.d.ts +0 -45
- package/dist/src/functions/db/sqlite-codegen.js +0 -294
- /package/dist/src/functions/db/{seed.d.ts → sqlite/seed.d.ts} +0 -0
- /package/dist/src/functions/db/{seed.js → sqlite/seed.js} +0 -0
- /package/dist/src/functions/db/{sqlite-kysely.d.ts → sqlite/sqlite-kysely.d.ts} +0 -0
- /package/dist/src/functions/db/{sqlite-kysely.js → sqlite/sqlite-kysely.js} +0 -0
- /package/dist/src/functions/db/{sqlite-runtime-bun.d.ts → sqlite/sqlite-runtime-bun.d.ts} +0 -0
- /package/dist/src/functions/db/{sqlite-runtime-bun.js → sqlite/sqlite-runtime-bun.js} +0 -0
- /package/dist/src/functions/db/{sqlite-runtime-node.d.ts → sqlite/sqlite-runtime-node.d.ts} +0 -0
- /package/dist/src/functions/db/{sqlite-runtime-node.js → sqlite/sqlite-runtime-node.js} +0 -0
- /package/dist/src/functions/db/{sqlite-runtime.d.ts → sqlite/sqlite-runtime.d.ts} +0 -0
- /package/dist/src/functions/db/{sqlite-runtime.js → sqlite/sqlite-runtime.js} +0 -0
|
@@ -1,43 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-security
|
|
3
|
-
description: 'Use when adding authentication
|
|
4
|
-
TRIGGER when:
|
|
5
|
-
DO NOT TRIGGER when: user asks about
|
|
3
|
+
description: 'Use when adding authentication or session management to a Pikku app — pikkuAuth, session lifecycle (setSession/clearSession), built-in auth strategies (authBearer, authCookie, authAPIKey), or JWT setup.
|
|
4
|
+
TRIGGER when: user asks about login, logout, session, bearer tokens, cookie auth, API keys, or JWT.
|
|
5
|
+
DO NOT TRIGGER when: user asks about middleware (use pikku-middleware), permissions/authorization checks (use pikku-permissions), or secrets/env vars (use pikku-config).'
|
|
6
6
|
installGroups: [core]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# Pikku Security (
|
|
9
|
+
# Pikku Security (Authentication & Sessions)
|
|
10
10
|
|
|
11
11
|
## Agent Operating Procedure
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
1. Discover before editing. Run `pikku info middleware --verbose` and `pikku info functions --verbose` to understand existing auth setup.
|
|
14
|
+
2. Auth strategies live in wirings files — do not put `addHTTPMiddleware` calls inside function bodies.
|
|
15
|
+
3. Validate with `pikku tsc` after changes; run `pikku all` if wirings changed.
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
18
|
-
4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
|
|
19
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
17
|
+
For **middleware** (including tag middleware and service-to-service bearer auth) see `pikku-middleware`.
|
|
18
|
+
For **permissions** (pikkuPermission, pikkuAuth, per-function authorization) see `pikku-permissions`.
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## Before You Start
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
pikku info middleware --verbose # See existing middleware and where it's applied
|
|
27
|
-
pikku info permissions --verbose # See existing permissions
|
|
28
|
-
pikku info functions --verbose # See which functions have auth/permissions
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
See `pikku-concepts` for the core mental model.
|
|
32
|
-
|
|
33
|
-
## API Reference
|
|
34
|
-
|
|
35
|
-
### Session Management
|
|
36
|
-
|
|
37
|
-
Functions access session via the wire object:
|
|
20
|
+
## Session Management
|
|
38
21
|
|
|
39
22
|
```typescript
|
|
40
|
-
//
|
|
23
|
+
// Read session in pikkuFunc (session guaranteed to exist)
|
|
41
24
|
const getProfile = pikkuFunc({
|
|
42
25
|
func: async ({ db }, _data, { session }) => {
|
|
43
26
|
return await db.getUser(session.userId)
|
|
@@ -62,78 +45,19 @@ const logout = pikkuFunc({
|
|
|
62
45
|
})
|
|
63
46
|
```
|
|
64
47
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Use for authentication gates that only need the session (no request data).
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
import { pikkuAuth } from '#pikku'
|
|
71
|
-
|
|
72
|
-
// Receives (services, session)
|
|
73
|
-
export const isAuthenticated = pikkuAuth(
|
|
74
|
-
async (_services, session) => !!session
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
export const isAdmin = pikkuAuth(
|
|
78
|
-
async (_services, session) => session?.role === 'admin'
|
|
79
|
-
)
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### `pikkuPermission(fn)` — Data-Aware Checks
|
|
83
|
-
|
|
84
|
-
Use when authorization depends on the actual request data.
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
import { pikkuPermission } from '#pikku'
|
|
88
|
-
|
|
89
|
-
// Receives (services, data, wire)
|
|
90
|
-
export const isOwner = pikkuPermission(
|
|
91
|
-
async ({ db }, { bookId }, { session }) => {
|
|
92
|
-
const book = await db.getBook(bookId)
|
|
93
|
-
return book?.authorId === session?.userId
|
|
94
|
-
}
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
export const hasBookAccess = pikkuPermission(
|
|
98
|
-
async ({ db }, { bookId }, { session }) => {
|
|
99
|
-
return await db.hasAccess(session?.userId, bookId)
|
|
100
|
-
}
|
|
101
|
-
)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Permission Groups (OR/AND Logic)
|
|
48
|
+
## Built-in Auth Strategies
|
|
105
49
|
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
admin: isAdmin, // OR: admins can access
|
|
109
|
-
owner: isOwner, // OR: owners can access
|
|
110
|
-
reviewer: [isAuthenticated, hasBookAccess], // AND: both must pass
|
|
111
|
-
}
|
|
112
|
-
// Logic: admin OR owner OR (isAuthenticated AND hasBookAccess)
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### `pikkuMiddleware(fn)` — Before/After Wrapping
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
import { pikkuMiddleware } from '#pikku'
|
|
119
|
-
|
|
120
|
-
const logRequest = pikkuMiddleware(async ({ logger }, wire, next) => {
|
|
121
|
-
logger.info('Before')
|
|
122
|
-
await next()
|
|
123
|
-
logger.info('After')
|
|
124
|
-
})
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Built-in Auth Middleware
|
|
50
|
+
Apply these via `addHTTPMiddleware` in a wirings file:
|
|
128
51
|
|
|
129
52
|
```typescript
|
|
130
53
|
import { authBearer, authCookie, authAPIKey } from '@pikku/core/middleware'
|
|
54
|
+
import { addHTTPMiddleware } from '@pikku/core/http'
|
|
131
55
|
|
|
132
56
|
// JWT bearer token — reads Authorization header
|
|
133
|
-
addHTTPMiddleware([authBearer()])
|
|
57
|
+
addHTTPMiddleware('*', [authBearer()])
|
|
134
58
|
|
|
135
59
|
// Cookie-based sessions — auto-refreshes JWT
|
|
136
|
-
addHTTPMiddleware([
|
|
60
|
+
addHTTPMiddleware('*', [
|
|
137
61
|
authCookie({
|
|
138
62
|
name: 'session',
|
|
139
63
|
expiresIn: { value: 30, unit: 'day' },
|
|
@@ -141,48 +65,8 @@ addHTTPMiddleware([
|
|
|
141
65
|
}),
|
|
142
66
|
])
|
|
143
67
|
|
|
144
|
-
// API key — from x-api-key header or ?apiKey= query
|
|
145
|
-
addHTTPMiddleware([authAPIKey({ source: 'all' })])
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Scoping: Where to Apply
|
|
149
|
-
|
|
150
|
-
Four levels of scoping, from broadest to narrowest:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
// 1. Global: all HTTP routes
|
|
154
|
-
addHTTPMiddleware('*', [authBearer()])
|
|
155
|
-
|
|
156
|
-
// 2. Prefix-based: URL pattern
|
|
157
|
-
addHTTPMiddleware('/admin/*', [auditLog])
|
|
158
|
-
addHTTPPermission('/admin/*', { admin: requireAdmin })
|
|
159
|
-
|
|
160
|
-
// 3. Tag-based: any wiring with matching tag
|
|
161
|
-
addMiddleware('api', [rateLimiter])
|
|
162
|
-
addPermission('api', { auth: requireAuth })
|
|
163
|
-
|
|
164
|
-
// 4. Inline: per-wiring
|
|
165
|
-
wireHTTP({
|
|
166
|
-
route: '/books/:id',
|
|
167
|
-
func: getBook,
|
|
168
|
-
middleware: [cacheControl],
|
|
169
|
-
permissions: { owner: requireOwnership },
|
|
170
|
-
})
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Per-Function Permissions
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
export const deleteBook = pikkuFunc({
|
|
177
|
-
func: async ({ db }, { bookId }) => {
|
|
178
|
-
await db.deleteBook(bookId)
|
|
179
|
-
},
|
|
180
|
-
permissions: {
|
|
181
|
-
admin: isAdmin,
|
|
182
|
-
owner: isOwner,
|
|
183
|
-
reviewer: [isAuthenticated, hasBookAccess],
|
|
184
|
-
},
|
|
185
|
-
})
|
|
68
|
+
// API key — from x-api-key header or ?apiKey= query param
|
|
69
|
+
addHTTPMiddleware('*', [authAPIKey({ source: 'all' })])
|
|
186
70
|
```
|
|
187
71
|
|
|
188
72
|
## Complete Example
|
|
@@ -191,53 +75,30 @@ export const deleteBook = pikkuFunc({
|
|
|
191
75
|
// permissions.ts
|
|
192
76
|
import { pikkuAuth, pikkuPermission } from '#pikku'
|
|
193
77
|
|
|
194
|
-
export const isAuthenticated = pikkuAuth(
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
export const isAdmin = pikkuAuth(
|
|
199
|
-
async (_services, session) => session?.role === 'admin'
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
export const isBookOwner = pikkuPermission(
|
|
203
|
-
async ({ db }, { bookId }, { session }) => {
|
|
204
|
-
const book = await db.getBook(bookId)
|
|
205
|
-
return book?.authorId === session?.userId
|
|
206
|
-
}
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
// middleware.ts
|
|
210
|
-
import { pikkuMiddleware } from '#pikku'
|
|
211
|
-
|
|
212
|
-
export const auditLog = pikkuMiddleware(async ({ logger, db }, wire, next) => {
|
|
213
|
-
const start = Date.now()
|
|
214
|
-
await next()
|
|
215
|
-
await db.createAuditLog({
|
|
216
|
-
duration: Date.now() - start,
|
|
217
|
-
})
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
// wirings/security.wiring.ts
|
|
221
|
-
import { authBearer } from '@pikku/core/middleware'
|
|
222
|
-
import { addHTTPMiddleware, addHTTPPermission } from '#pikku'
|
|
78
|
+
export const isAuthenticated = pikkuAuth(async (_services, session) => !!session)
|
|
79
|
+
export const isAdmin = pikkuAuth(async (_services, session) => session?.role === 'admin')
|
|
223
80
|
|
|
224
|
-
//
|
|
225
|
-
|
|
81
|
+
// wirings/auth.wiring.ts
|
|
82
|
+
import { authCookie } from '@pikku/core/middleware'
|
|
83
|
+
import { addHTTPMiddleware } from '@pikku/core/http'
|
|
226
84
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
85
|
+
addHTTPMiddleware('*', [
|
|
86
|
+
authCookie({ name: 'session', expiresIn: { value: 30, unit: 'day' } }),
|
|
87
|
+
])
|
|
230
88
|
|
|
231
|
-
// functions/
|
|
232
|
-
export const
|
|
233
|
-
|
|
234
|
-
func: async ({ db }, {
|
|
235
|
-
await db.
|
|
236
|
-
|
|
89
|
+
// functions/auth.functions.ts
|
|
90
|
+
export const login = pikkuFunc({
|
|
91
|
+
auth: false,
|
|
92
|
+
func: async ({ jwt, db }, { email, password }, { setSession }) => {
|
|
93
|
+
const user = await db.verifyCredentials(email, password)
|
|
94
|
+
setSession({ userId: user.id, role: user.role })
|
|
95
|
+
return { token: jwt.sign({ userId: user.id }) }
|
|
237
96
|
},
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
export const logout = pikkuFunc({
|
|
100
|
+
func: async ({}, _data, { clearSession }) => {
|
|
101
|
+
clearSession()
|
|
241
102
|
},
|
|
242
103
|
})
|
|
243
104
|
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pikku-tag-middleware
|
|
3
|
+
description: 'Deprecated — use pikku-middleware instead. Tag middleware (addTagMiddleware) is now documented as a section within the pikku-middleware skill, alongside global HTTP middleware, execution order, and the service-to-service bearer auth pattern.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deprecated: use `pikku-middleware`
|
|
7
|
+
|
|
8
|
+
Tag middleware is covered in the **`pikku-middleware`** skill, which also covers:
|
|
9
|
+
- `addHTTPMiddleware` (global / prefix-based)
|
|
10
|
+
- `addTagMiddleware` (tag-scoped)
|
|
11
|
+
- Middleware execution order and priority
|
|
12
|
+
- Service-to-service bearer auth pattern
|
|
13
|
+
- Session-setting middleware pattern
|
|
@@ -27,6 +27,132 @@ pikku info functions --verbose # See existing functions and their middleware/p
|
|
|
27
27
|
pikku info middleware --verbose # See middleware applied
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Personas and Named Data — Never Inline JSON
|
|
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.
|
|
133
|
+
|
|
134
|
+
## Coverage-Driven Test Writing
|
|
135
|
+
|
|
136
|
+
When asked to improve or fill test coverage, start with the AI prompt from the coverage command:
|
|
137
|
+
|
|
138
|
+
```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 -
|
|
147
|
+
```
|
|
148
|
+
|
|
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:
|
|
150
|
+
|
|
151
|
+
1. Read the prompt to know which functions need Gherkin scenarios.
|
|
152
|
+
2. Run `pikku meta functions list` or `pikku meta context` to get input/output schemas for those functions.
|
|
153
|
+
3. Write `.feature` files under `tests/tests/features/` — one feature per domain, one scenario per case.
|
|
154
|
+
4. Re-run `pikku tests coverage` to confirm coverage improved.
|
|
155
|
+
|
|
30
156
|
See `pikku-concepts` for the core mental model.
|
|
31
157
|
|
|
32
158
|
## Test Runner Setup
|
|
@@ -426,3 +552,85 @@ describe('createTodo', () => {
|
|
|
426
552
|
})
|
|
427
553
|
})
|
|
428
554
|
```
|
|
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
|
+
```
|