@pikku/skills 0.12.52 → 0.12.55

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 (112) hide show
  1. package/README.md +1 -1
  2. package/dist/skills.gen.js +2 -2
  3. package/dist/snippets.js +1 -1
  4. package/package.json +1 -1
  5. package/skills/pikku-a11y/SKILL.md +22 -15
  6. package/skills/pikku-addon/SKILL.md +5 -5
  7. package/skills/pikku-addon/references/addon-package-manifest.md +7 -0
  8. package/skills/pikku-addon/references/openapi.md +11 -0
  9. package/skills/pikku-agent/SKILL.md +6 -6
  10. package/skills/pikku-agent/references/agents.md +8 -1
  11. package/skills/pikku-agent/references/runner-vercel.md +2 -2
  12. package/skills/pikku-agent/references/voice.md +7 -1
  13. package/skills/pikku-architect/SKILL.md +2 -2
  14. package/skills/pikku-architect/references/plan-defects.md +9 -7
  15. package/skills/pikku-auth/SKILL.md +15 -36
  16. package/skills/pikku-auth/references/better-auth.md +106 -133
  17. package/skills/pikku-auth/references/jose.md +6 -0
  18. package/skills/pikku-auth/references/machine-auth.md +6 -0
  19. package/skills/pikku-auth/references/sessions.md +6 -0
  20. package/skills/pikku-build/SKILL.md +37 -34
  21. package/skills/pikku-build/references/app.md +113 -99
  22. package/skills/pikku-build/references/design.md +43 -31
  23. package/skills/pikku-build/references/feature.md +17 -3
  24. package/skills/pikku-build/references/mocks.md +29 -0
  25. package/skills/pikku-build/references/multi-app.md +25 -18
  26. package/skills/pikku-build/references/native-app.md +9 -0
  27. package/skills/pikku-build/references/openapi.md +41 -34
  28. package/skills/pikku-build/references/platform.md +23 -14
  29. package/skills/pikku-build/references/post-clone.md +1 -1
  30. package/skills/pikku-build/references/quick.md +76 -39
  31. package/skills/pikku-build/references/scenarios.md +28 -16
  32. package/skills/pikku-build/references/ship.md +19 -16
  33. package/skills/pikku-build/references/theming.md +76 -127
  34. package/skills/pikku-changes/SKILL.md +7 -10
  35. package/skills/pikku-concepts/SKILL.md +34 -44
  36. package/skills/pikku-concepts/references/concept-mapping.md +18 -0
  37. package/skills/pikku-concepts/references/language.md +6 -6
  38. package/skills/pikku-convert-screen/SKILL.md +45 -0
  39. package/skills/pikku-deploy/references/cloudflare.md +8 -0
  40. package/skills/pikku-deploy/references/lambda.md +8 -0
  41. package/skills/pikku-deploy/references/nextjs.md +1 -1
  42. package/skills/pikku-deploy/references/uws.md +1 -1
  43. package/skills/pikku-deploy/references/ws.md +3 -3
  44. package/skills/pikku-emails/SKILL.md +3 -9
  45. package/skills/pikku-guide/SKILL.md +28 -27
  46. package/skills/pikku-i18n/SKILL.md +7 -7
  47. package/skills/pikku-i18n/references/enum-labels.md +9 -0
  48. package/skills/pikku-i18n/references/messages.md +21 -9
  49. package/skills/pikku-i18n/references/rtl.md +18 -38
  50. package/skills/pikku-knowledge/SKILL.md +12 -13
  51. package/skills/pikku-kysely/SKILL.md +3 -21
  52. package/skills/pikku-list-query/SKILL.md +2 -2
  53. package/skills/pikku-meta/references/audit.md +10 -1
  54. package/skills/pikku-meta/references/meta.md +6 -0
  55. package/skills/pikku-meta/references/versioning.md +10 -0
  56. package/skills/pikku-middleware/SKILL.md +7 -16
  57. package/skills/pikku-n8n-import/SKILL.md +9 -1
  58. package/skills/pikku-n8n-import/references/addon-mapping.md +6 -0
  59. package/skills/pikku-n8n-import/references/code-translation.md +7 -0
  60. package/skills/pikku-permissions/SKILL.md +41 -10
  61. package/skills/pikku-permissions/references/scopes.md +59 -0
  62. package/skills/pikku-react/SKILL.md +20 -22
  63. package/skills/pikku-react/references/client.md +2 -70
  64. package/skills/pikku-react/references/react-query.md +11 -0
  65. package/skills/pikku-react/references/workflows.md +11 -0
  66. package/skills/pikku-realtime/SKILL.md +9 -3
  67. package/skills/pikku-report/SKILL.md +17 -16
  68. package/skills/pikku-scenario/SKILL.md +72 -81
  69. package/skills/pikku-scenario/references/browser.md +5 -5
  70. package/skills/pikku-scenario/references/coverage.md +4 -4
  71. package/skills/pikku-scenario/references/steps.md +31 -30
  72. package/skills/pikku-scenario/references/{persona-run.md → virtual-user-run.md} +47 -42
  73. package/skills/pikku-scenario/references/virtual-users.md +114 -0
  74. package/skills/pikku-seo/SKILL.md +12 -16
  75. package/skills/pikku-service-backends/SKILL.md +0 -10
  76. package/skills/pikku-service-backends/references/aws.md +6 -0
  77. package/skills/pikku-service-backends/references/mongodb.md +7 -0
  78. package/skills/pikku-service-backends/references/redis.md +6 -0
  79. package/skills/pikku-services/references/audit-wire-service.md +2 -2
  80. package/skills/pikku-services/references/audit.md +10 -1
  81. package/skills/pikku-services/references/config.md +6 -0
  82. package/skills/pikku-services/references/services.md +9 -2
  83. package/skills/pikku-software-archaeology/SKILL.md +21 -27
  84. package/skills/pikku-software-archaeology/references/blueprint.schema.json +16 -16
  85. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  86. package/skills/pikku-software-archaeology/references/second-opinion.md +13 -1
  87. package/skills/pikku-tailwind/SKILL.md +111 -0
  88. package/skills/pikku-theme/SKILL.md +187 -0
  89. package/skills/pikku-theme/references/from-a-reference.md +44 -0
  90. package/skills/pikku-theme/references/presets.md +40 -0
  91. package/skills/pikku-ui/SKILL.md +41 -0
  92. package/skills/pikku-wiring/SKILL.md +13 -12
  93. package/skills/pikku-wiring/references/channel.md +7 -1
  94. package/skills/pikku-wiring/references/cli.md +7 -1
  95. package/skills/pikku-wiring/references/gateway-slack.md +6 -0
  96. package/skills/pikku-wiring/references/http.md +10 -3
  97. package/skills/pikku-wiring/references/mcp.md +8 -0
  98. package/skills/pikku-wiring/references/queue.md +6 -0
  99. package/skills/pikku-wiring/references/realtime.md +34 -4
  100. package/skills/pikku-wiring/references/rpc.md +6 -0
  101. package/skills/pikku-wiring/references/scheduler.md +6 -0
  102. package/skills/pikku-wiring/references/trigger.md +8 -0
  103. package/skills/pikku-workflow/SKILL.md +9 -7
  104. package/skills/pikku-admin-to-fabric/SKILL.md +0 -212
  105. package/skills/pikku-auth/references/permissions.md +0 -261
  106. package/skills/pikku-blueprint-to-fabric/SKILL.md +0 -377
  107. package/skills/pikku-blueprint-to-fabric/scripts/inventory.mjs +0 -367
  108. package/skills/pikku-fabric/SKILL.md +0 -619
  109. package/skills/pikku-fabric/references/debugging.md +0 -110
  110. package/skills/pikku-mantine/SKILL.md +0 -80
  111. package/skills/pikku-scenario/references/personas.md +0 -117
  112. package/skills/pikku-wiring/references/realtime-other-routes.md +0 -23
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: pikku-scenario
3
3
  description: >-
4
- Use when writing or running Pikku scenarios, running a persona as a virtual user, or when
4
+ Use when writing or running Pikku scenarios, running a virtual user against a stage, or when
5
5
  asked to test Pikku functions or improve coverage. A scenario (pikkuScenario) drives the app
6
- the way users do — steps run as actors over the real transport against a running server — so a
6
+ the way users do — steps run as virtual users over the real transport against a running server — so a
7
7
  flow doubles as an e2e test and a staged/production health check. Covers scenario.do /
8
8
  expectEventually / expectError / expectService / expectScore, declared steps via
9
9
  pikkuScenarioStep (browser steps driven by @pikku/playwright) written as intent rather than
10
- clicks, personas / actors / environments in pikku.config.json, SCENARIO_ACTOR_SECRET, the
11
- `pikku scenario list|run` and `pikku persona run|list|sync|secret` commands, and live coverage
10
+ clicks, virtual users / virtual users / environments in pikku.config.json, VIRTUAL_USER_SIGNING_KEY, the
11
+ `pikku scenario` and `pikku virtual-user` commands, and live coverage
12
12
  via `pikku dev --coverage`. TRIGGER when: user asks about scenarios, testing a Pikku function,
13
- coverage, e2e flows, browser/UI e2e, health checks, personas, virtual users, or adversarial
13
+ coverage, e2e flows, browser/UI e2e, health checks, virtual users, or adversarial
14
14
  runs against a stage. DO NOT TRIGGER when: user asks about running an existing suite (use
15
15
  Bash) or CI config.
16
16
  installGroups: [core]
@@ -20,38 +20,34 @@ installGroups: [core]
20
20
 
21
21
  ## Agent Operating Procedure
22
22
 
23
- Use this skill as an execution checklist, not reference material.
24
-
25
23
  1. Discover before editing: `pikku scenario list` for what exists, `pikku info functions --verbose` for what a scenario can call.
26
- 2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, or build artifacts.
27
- 3. Make the smallest source change that satisfies the task. Keep generated files generated.
28
- 4. Validate with the narrowest relevant command first, then `pikku all --tsc` when functions, wirings or schemas may have changed.
29
- 5. If validation fails, fix the source cause and rerun. Do not paper over generated errors by editing generated files.
24
+ 2. Edit source files only; never generated output or `.pikku`.
25
+ 3. Validate with the narrowest relevant command first, then `pikku all --tsc` when functions, wirings or schemas may have changed. If it fails, fix the source cause and rerun.
30
26
 
31
27
  **`pikku tests` does not exist.** It was removed in #865 — scenarios own coverage now. Any reference you find to it is stale.
32
28
 
33
29
  ## Pick the reference
34
30
 
35
- This skill covers writing and running scenarios end to end. Four topics are one level down, and
31
+ This skill covers writing and running scenarios end to end. Five topics are one level down, and
36
32
  each says when to open it:
37
33
 
38
- | Read | For |
39
- | --------------------------- | ------------------------------------------------------------------------------------------------------------ |
40
- | `references/steps.md` | Authoring a `pikkuScenarioStep` — intent, witnesses, what a step is given |
41
- | `references/browser.md` | Browser bindings, and locating by message key in a translated app |
42
- | `references/personas.md` | Personas vs actors, `definePersonas`, upstream credentials for actors, and the human "Sign in as …" switcher |
43
- | `references/coverage.md` | Live coverage, filling it, and unit tests for pure logic |
44
- | `references/persona-run.md` | Running a persona as a model-driven virtual user against a stage |
34
+ | Read | For |
35
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
36
+ | `references/steps.md` | Authoring a `pikkuScenarioStep` — intent, witnesses, what a step is given |
37
+ | `references/browser.md` | Browser bindings, and locating by message key in a translated app |
38
+ | `references/virtual-users.md` | Virtual users vs virtual users, `defineVirtualUsers`, upstream credentials for virtual users, and human sign-in as a virtual user |
39
+ | `references/coverage.md` | Live coverage, filling it, and unit tests for pure logic |
40
+ | `references/virtual-user-run.md` | Running a model-driven virtual user against a stage |
45
41
 
46
42
  ## What a scenario is
47
43
 
48
- A scenario is a `pikkuScenario` export that drives the app **as real actors over the real transport**, against a running server. That is what lets one artifact serve as both an e2e test and a staged/production health check.
44
+ A scenario is a `pikkuScenario` export that drives the app **as real virtual users over the real transport**, against a running server. That is what lets one artifact serve as both an e2e test and a staged/production health check.
49
45
 
50
46
  Consequences that matter, and bite if ignored:
51
47
 
52
48
  - **There is no state reset.** A scenario runs against a live server. Scope what you create (unique ids, your own rows) and never assume a clean database.
53
- - **Every effect runs as somebody, or as a declared step.** `scenario.do(...)` without `{ actor }` throws `Scenario tried to run '<rpc>' as an internal step…` — there is no bare internal-RPC step. The other way to do work is `scenario.given/when/then`, which runs a `pikkuScenarioStep`; its actor is optional (setup steps have none) unless it declares `browser: true`.
54
- - **Actors must be configured and signed in**, or the scenario cannot run.
49
+ - **Every effect runs as somebody, or as a declared step.** `scenario.do(...)` without `{ virtualUser }` throws `Scenario tried to run '<rpc>' as an internal step…` — there is no bare internal-RPC step. The other way to do work is `scenario.given/when/then`, which runs a `pikkuScenarioStep`; its virtual user is optional (setup steps have none) unless it declares `browser: true`.
50
+ - **Virtual users must be configured and signed in**, or the scenario cannot run.
55
51
 
56
52
  Scenarios live in `srcDirectories` like any other function — by convention `*.scenario.ts`.
57
53
 
@@ -68,10 +64,10 @@ export const orderSupportScenario = pikkuScenario<
68
64
  >({
69
65
  title: 'Order support (scenario)',
70
66
  tags: ['scenario'],
71
- func: async ({ logger }, data, { scenario, actors }) => {
72
- if (!actors?.shopper || !actors?.support) {
67
+ func: async ({ logger }, data, { scenario, virtualUsers }) => {
68
+ if (!virtualUsers?.shopper || !virtualUsers?.support) {
73
69
  throw new Error(
74
- 'orderSupportScenario needs run actors (shopper + support) — run via `pikku scenario run <environment>`'
70
+ 'orderSupportScenario needs run virtualUsers (shopper + support) — run via `pikku scenario run <environment>`'
75
71
  )
76
72
  }
77
73
 
@@ -79,7 +75,7 @@ export const orderSupportScenario = pikkuScenario<
79
75
  'shopper doubles their order',
80
76
  'doubleValue',
81
77
  { value: data?.value ?? 21 },
82
- { actor: actors.shopper }
78
+ { virtualUser: virtualUsers.shopper }
83
79
  )
84
80
 
85
81
  const settled = await scenario.expectEventually(
@@ -87,7 +83,7 @@ export const orderSupportScenario = pikkuScenario<
87
83
  'formatMessage',
88
84
  { greeting: 'Hello', name: 'Support' },
89
85
  (out: { message: string }) => out.message.length > 0,
90
- { actor: actors.support, within: '5s', interval: 50 }
86
+ { virtualUser: virtualUsers.support, within: '5s', interval: 50 }
91
87
  )
92
88
 
93
89
  return { doubled: doubled.result, message: settled.message }
@@ -95,19 +91,19 @@ export const orderSupportScenario = pikkuScenario<
95
91
  })
96
92
  ```
97
93
 
98
- A scenario takes the same config fields as a workflow (`title`, `description`, `tags`, `input`/`output`, `auth`, `permissions`, `middleware`, `version`, …). The third argument is the scenario context: `{ scenario, actors }`.
94
+ A scenario takes the same config fields as a workflow (`title`, `description`, `tags`, `input`/`output`, `auth`, `permissions`, `middleware`, `version`, …). The third argument is the scenario context: `{ scenario, virtualUsers }`.
99
95
 
100
96
  ### The scenario API
101
97
 
102
- | Call | Purpose |
103
- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
104
- | `scenario.do(step, rpc, data, { actor })` | Run an RPC as that actor. The step name is what appears in the run output. |
105
- | `scenario.expectEventually(step, rpc, data, predicate, { actor, within, interval })` | Poll until `predicate(out)` passes or `within` elapses. For anything asynchronous — queues, workers, eventual state. |
106
- | `scenario.expectError(step, rpc, data, { actor, matches })` | Assert the call **fails**. For fault injection and negative paths. |
107
- | `scenario.expectService(step, 'service.method', { actor, calledWith })` | Assert a stubbed service was called. Requires the server to run with `--test`. |
108
- | `scenario.expectScore(step, runId, scorer, { atLeast, atMost, reference })` | Grade a finished agent run with a declared scorer and assert the score. See below. |
109
- | `scenario.given(stepName, step, data, { actor })` | Run a declared `pikkuScenarioStep` as setup. `when` is the same call; `then` also makes the step's bindings witnesses. |
110
- | `scenario.runScheduledTask(name)` | Fire a wired scheduler on the target now, rather than waiting for its cron. |
98
+ | Call | Purpose |
99
+ | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
100
+ | `scenario.do(step, rpc, data, { virtualUser })` | Run an RPC as that virtual user. The step name is what appears in the run output. |
101
+ | `scenario.expectEventually(step, rpc, data, predicate, { virtualUser, within, interval })` | Poll until `predicate(out)` passes or `within` elapses. For anything asynchronous — queues, workers, eventual state. |
102
+ | `scenario.expectError(step, rpc, data, { virtualUser, matches })` | Assert the call **fails**. For fault injection and negative paths. |
103
+ | `scenario.expectService(step, 'service.method', { virtualUser, calledWith })` | Assert a stubbed service was called. Requires the server to run with `--test`. |
104
+ | `scenario.expectScore(step, runId, scorer, { atLeast, atMost, reference })` | Grade a finished agent run with a declared scorer and assert the score. See below. |
105
+ | `scenario.given(stepName, step, data, { virtualUser })` | Run a declared `pikkuScenarioStep` as setup. `when` is the same call; `then` also makes the step's bindings witnesses. |
106
+ | `scenario.runScheduledTask(name)` | Fire a wired scheduler on the target now, rather than waiting for its cron. |
111
107
 
112
108
  `expectEventually` is **scenario-only**. Calling it from a `pikkuWorkflowFunc` is a critical inspector error (`PKU675`) pointing you at `pikkuScenario`.
113
109
 
@@ -127,7 +123,7 @@ const { runId } = await scenario.when(
127
123
  {
128
124
  prompt: data.prompt,
129
125
  },
130
- { actor: actors.user }
126
+ { virtualUser: virtualUsers.user }
131
127
  )
132
128
 
133
129
  await scenario.expectScore('answered briefly', runId, 'brevity', {
@@ -156,8 +152,8 @@ the default suite excludes it.
156
152
  A scenario config takes `before` and `after`. Both have the **same signature as `func`** — `(services, data, wire)` — with the return value discarded:
157
153
 
158
154
  ```typescript
159
- const resetsCredentials = async (_services, _data, { actors }) => {
160
- await actors!.admin!.invoke('resetCredentials', {})
155
+ const resetsCredentials = async (_services, _data, { virtualUsers }) => {
156
+ await virtualUsers!.admin!.invoke('resetCredentials', {})
161
157
  }
162
158
 
163
159
  export const credentialScenario = pikkuScenario({
@@ -165,7 +161,7 @@ export const credentialScenario = pikkuScenario({
165
161
  tags: ['scenario', 'credential'],
166
162
  before: resetsCredentials,
167
163
  after: removesInstalledAddon,
168
- func: async (services, data, { scenario, actors }) => {
164
+ func: async (services, data, { scenario, virtualUsers }) => {
169
165
  /* … */
170
166
  },
171
167
  })
@@ -180,18 +176,12 @@ export const credentialScenario = pikkuScenario({
180
176
  | Neither runs when the run is suspended or waiting — teardown only fires at a terminal outcome. |
181
177
  | Hooks are **not** ladder rows. The runner records nothing for them; a failure is labelled by phase. |
182
178
 
183
- A hook reaches the app the same way the body does: through `wire.actors`. If you want cleanup to be _visible_ on the ladder, make it an ordinary `scenario.then(...)` instead.
179
+ A hook reaches the app the same way the body does: through `wire.virtualUsers`. If you want cleanup to be _visible_ on the ladder, make it an ordinary `scenario.then(...)` instead.
184
180
 
185
181
  Hooks are scenario-only. A `before`/`after` on a `pikkuWorkflowFunc` never runs — a workflow is durable and resumable, so a callback that reran on every replay would have no honest meaning.
186
182
 
187
183
  ### Grouping scenarios (`pikkuFeature`)
188
184
 
189
- A feature is also cited by a page of the user guide: `pikku scenario guide`
190
- fills each cited block with the feature's recordings and screenshots, and
191
- nothing else (see **pikku-guide**). A scenario's `title` captions its recording
192
- and a screenshot's `name` captions the still, so write both as copy a user would
193
- read. Mark plumbing features `document: false`.
194
-
195
185
  `pikkuFeature` groups scenarios the way gherkin's `Feature:` groups `Scenario:`. Scenarios are referenced by **imported identifier**, so a renamed or deleted scenario is a compile error rather than a silent skip:
196
186
 
197
187
  ```typescript
@@ -229,11 +219,17 @@ export const credentialFeature = pikkuFeature({
229
219
 
230
220
  The **feature is the run unit**: `--flows` on a scenario whose every feature entry carries `data` errors and names the features containing it, because the feature is what supplies that data. Use `--features` for those. A scenario referenced bare anywhere, or in no feature at all, still runs standalone.
231
221
 
222
+ A feature is also cited by a page of the user guide: `pikku scenario guide`
223
+ fills each cited block with the feature's recordings and screenshots, and
224
+ nothing else (see **pikku-guide**). A scenario's `title` captions its recording
225
+ and a screenshot's `name` captions the still, so write both as copy a user would
226
+ read. Mark plumbing features `document: false`.
227
+
232
228
  ## Steps
233
229
 
234
230
  `scenario.do` can only name an RPC. A **step** — `pikkuScenarioStep` — is a named, typed unit of
235
231
  scenario behaviour whose body is an ordinary pikku function, so it can call several RPCs as its
236
- actor, assert, or drive a browser. It is referenced by its typed string name (checked against the
232
+ virtual user, assert, or drive a browser. It is referenced by its typed string name (checked against the
237
233
  generated step map), its body lives under a surface binding (`default` / `browser` / `cli`, never a
238
234
  `func`), and one row of the report is one step.
239
235
 
@@ -242,7 +238,7 @@ suite survives its first redesign:
242
238
 
243
239
  - **What a step is given** — a step runs in the CLI process, not where the app runs. There is no
244
240
  `kysely`, no `secrets`, and `rpc` is present but throws on purpose. Everything reaches the app
245
- through the actor.
241
+ through the virtual user.
246
242
  - **Steps describe intent, not actions** — `buys the £5 strawberry milkshake`, never
247
243
  `clicks [data-testid=add]`. The clicking lives in plain utilities the step composes.
248
244
  - **`then` bindings are witnesses, not alternatives** — a `then` runs _every_ binding it declares
@@ -260,11 +256,11 @@ score a perfect run while checking nothing.
260
256
 
261
257
  ## Configuration
262
258
 
263
- Personas are declared in TypeScript. One `definePersonas({ … })` call for the
264
- whole project — codegen builds the `PersonaId` union from it, materialises one
265
- scenario actor per person, and seeds a user row each:
259
+ Virtual users are declared in TypeScript. One `defineVirtualUsers({ … })` call for the
260
+ whole project — codegen builds the `VirtualUserId` union from it, materialises one
261
+ scenario virtual user per person, and seeds a user row each:
266
262
 
267
- ```ts snippet:definePersonas
263
+ ```ts snippet:defineVirtualUsers
268
264
 
269
265
  ```
270
266
 
@@ -273,42 +269,40 @@ scenario actor per person, and seeds a user row each:
273
269
  ```json
274
270
  {
275
271
  "scenarios": {
276
- "emailDomain": "actors.example.com",
272
+ "emailDomain": "virtualuser.example.com",
277
273
  "browserDriver": "@pikku/playwright",
278
274
  "model": "claude-sonnet-4-5"
279
275
  },
280
276
  "environments": {
281
277
  "local": {
282
278
  "apiUrl": "http://localhost:4077",
283
- "signInPath": "/api/auth/sign-in/actor"
279
+ "signInPath": "/api/auth/sign-in/virtual-user"
284
280
  }
285
281
  }
286
282
  }
287
283
  ```
288
284
 
289
- - `scenarios.emailDomain` is the mail domain actor addresses are built on.
285
+ - `scenarios.emailDomain` is the mail domain virtual user addresses are built on.
290
286
  - `scenarios.browserDriver` is the package driving `browser` bindings.
291
- - `scenarios.model` is the model a persona thinks with (`actor.converse`, `pikku virtual-user run`).
292
- - `environments.<name>` are the targets a run can point at. The key is the required positional of `pikku scenario run`; `apiUrl` is required and `signInPath`/`rpcPath`/`sessionPath` have defaults.
287
+ - `scenarios.model` is the model a virtual user thinks with (`virtualUser.converse`, `pikku virtual-user run`).
288
+ - `environments.<name>` are the targets a run can point at. The key is the required positional of `pikku scenario run`; `apiUrl` is required; `signInPath` defaults to `/auth/sign-in/virtual-user`, `rpcPath` to `/rpc`, and `sessionPath` has a default too.
293
289
 
294
- **`references/personas.md`** covers the parts that bite: one entry is one
295
- person and one actor, emails are derived and never written, `runnable: false`
296
- for someone only acted upon, `definePersonas` being read from source rather
297
- than evaluated, and the same actor list powering a human "Sign in as …"
298
- switcher.
290
+ **`references/virtual-users.md`** covers the parts that bite: one entry is one
291
+ person and one virtual user, emails are derived and never written, `runnable: false`
292
+ for someone only acted upon, `defineVirtualUsers` being read from source rather
293
+ than evaluated, and the same virtual user list powering a human sign-in.
299
294
 
300
- - `environments.<name>.apiUrl` is required. `signInPath` defaults to `/auth/sign-in/actor`, `rpcPath` to `/rpc`.
301
- - **`SCENARIO_ACTOR_SECRET` is an environment variable and never goes in `pikku.config.json`.** It signs actors in. `pikku scenario run` throws without it; a server auto-building actors warns and runs without them.
295
+ - **The signing key never goes in `pikku.config.json`.** `pikku dev` creates one under `.pikku-runtime/virtual-user/`; a deployed stage's deployer holds `VIRTUAL_USER_SIGNING_KEY`. Each run signs a short-lived statement with it. `pikku scenario run` throws without a key.
302
296
 
303
297
  ## Running
304
298
 
305
299
  ```bash
306
300
  pikku scenario list # features with their scenarios indented, then ungrouped scenarios
307
- SCENARIO_ACTOR_SECRET=… pikku scenario run local
308
- SCENARIO_ACTOR_SECRET=… pikku scenario run local --flows orderSupportScenario
309
- SCENARIO_ACTOR_SECRET=… pikku scenario run local --features credentialFeature
310
- SCENARIO_ACTOR_SECRET=… pikku scenario run local --tags smoke,scenario
311
- SCENARIO_ACTOR_SECRET=… pikku scenario run local --spawn --exclude-tags ai-live
301
+ pikku scenario run local
302
+ pikku scenario run local --flows orderSupportScenario
303
+ pikku scenario run local --features credentialFeature
304
+ pikku scenario run local --tags smoke,scenario
305
+ pikku scenario run local --spawn --exclude-tags ai-live
312
306
  ```
313
307
 
314
308
  `run` takes the environment as a **required positional** — the key from `environments`. Every filter narrows the same plan, so narrowing a feature to two of its five scenarios still runs the feature's hooks exactly once around those two.
@@ -330,7 +324,7 @@ SCENARIO_ACTOR_SECRET=… pikku scenario run local --spawn --exclude-tags ai-liv
330
324
 
331
325
  Output is `PASS <name> (<ms>) → <output>` / `FAIL <name> (<ms>): <error>`, then `N/M scenarios passed against '<env>'`. A scenario inside a feature is named `<Feature> › <scenario> <data>`.
332
326
 
333
- **Exit code is 1** if any scenario fails _or_ if no scenario matched the filter — a typo'd `--flows` is a hard error, not a silent zero-run pass. It throws outright on an unknown environment, an unknown flow name, or a missing `SCENARIO_ACTOR_SECRET`.
327
+ **Exit code is 1** if any scenario fails _or_ if no scenario matched the filter — a typo'd `--flows` is a hard error, not a silent zero-run pass. It throws outright on an unknown environment, an unknown flow name, or a missing signing key.
334
328
 
335
329
  ## Coverage
336
330
 
@@ -340,7 +334,7 @@ derived from unit tests:
340
334
  ```bash
341
335
  pikku enable scenarios # sets scaffold.scenarios = true
342
336
  pikku dev --coverage # V8 precise coverage, in-process
343
- SCENARIO_ACTOR_SECRET=… pikku scenario run local --coverage
337
+ pikku scenario run local --coverage
344
338
  ```
345
339
 
346
340
  That writes `<outDir>/coverage/scenario-coverage.json`. **`references/coverage.md`** has the
@@ -351,21 +345,18 @@ and where a plain unit test is still the right tool.
351
345
 
352
346
  | Smell | Why it's wrong |
353
347
  | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
354
- | `pikku tests …` | Removed in #865. Use `pikku scenario`. |
355
- | `.feature` files / Gherkin for function tests | Scenarios are TypeScript, not Gherkin. The in-process cucumber function world was deleted. |
356
- | `scenario.do(...)` with no `{ actor }` | Throws. Every step runs as somebody. |
348
+ | `.feature` files / Gherkin for function tests | Scenarios are TypeScript, not Gherkin. There is no in-process function world. |
349
+ | `scenario.do(...)` with no `{ virtualUser }` | Throws. Every step runs as somebody. |
357
350
  | A scenario per function | Scenarios are user flows. One flow covers many functions; that is the point. |
358
351
  | Assuming a clean database | There is no state reset — it may be a staging server. Scope what you create. |
359
352
  | `sleep()` before asserting | Use `expectEventually`. |
360
- | A step named `clicksAddToBasket` / `opensThePage` | That is an action, not an intent. Name the step for what the actor wanted; put the clicking in a utility. |
353
+ | A step named `clicksAddToBasket` / `opensThePage` | That is an action, not an intent. Name the step for what the virtual user wanted; put the clicking in a utility. |
361
354
  | A step named `kauftEinenApfel` / a `vorgang` table | Identifiers are English in every project. The German belongs in `description` / `template`, and only when `pikku.config.json` sets `metaLocale`. |
362
355
  | A browser step that assumes it is already on a page | It can then only run mid-flow. Arrive first — check the URL, navigate if needed. |
363
356
  | `getByLabel('Full Name')` in a translated app | Passes only in the base locale, and a copy edit breaks it as an unexplained timeout. Locate by message key. |
364
- | A `browser` binding guarding `if (!browser)` | The binding guarantees it. The guard hides the real error, which is a missing actor (`PKU677`). |
357
+ | A `browser` binding guarding `if (!browser)` | The binding guarantees it. The guard hides the real error, which is a missing virtual user (`PKU677`). |
365
358
  | A step with a `func:` instead of a surface binding | There is no `func` on a step. Bodies live under `default` / `browser` / `cli`; a step with none throws at load. |
366
359
  | `expectEventually` in a `pikkuWorkflowFunc` | `PKU675` — scenario-only. |
367
- | Coverage silently 0 | Server not run with `--coverage`, verbose functions meta not deployed, `scaffold.scenarios` unset, or no actors configured. |
368
-
369
- `@pikku/cucumber` is a **browser/e2e** harness (`Actor`, `BrowserWorld`, `PersonaData`, `DbUtils`) — out of scope here.
360
+ | Coverage silently 0 | Server not run with `--coverage`, verbose functions meta not deployed, `scaffold.scenarios` unset, or no virtual users configured. |
370
361
 
371
362
  See `pikku-concepts` for the core mental model.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Declaring a `browser` binding is the whole switch: inside that binding `wire.browser` is guaranteed present and non-optional, and a step without one never sees a browser at all. There is nothing to null-check.
4
4
 
5
- A `browser` binding gets a session bound to **its actor**, signed in through the same `signInPath` + `SCENARIO_ACTOR_SECRET` path the HTTP actors use, so the browser and the RPC calls are one identity. Calling such a step without an actor is a critical error (`PKU677`).
5
+ A `browser` binding gets a session bound to **its virtual user**, signed in through the same signed-statement path the HTTP virtual users use, so the browser and the RPC calls are one identity. Calling such a step without a virtual user is a critical error (`PKU677`).
6
6
 
7
7
  Browser steps are where **intent, not actions** earns its keep: the step is one intent, the clicking lives in shared utilities, and the step arrives before it acts. Write the mechanics below into utilities and keep the step body to three or four calls that read as a sentence.
8
8
 
@@ -17,8 +17,8 @@ export const opensTheCart = pikkuScenarioStep<
17
17
  await browser.goto(path)
18
18
  return { url: browser.page.url() }
19
19
  },
20
- default: async (_services, _data, { actor }) => ({
21
- url: (await actor.invoke('getCart', {})).url,
20
+ default: async (_services, _data, { virtualUser }) => ({
21
+ url: (await virtualUser.invoke('getCart', {})).url,
22
22
  }),
23
23
  })
24
24
  ```
@@ -54,6 +54,6 @@ await page
54
54
 
55
55
  - Type off `messages/<baseLocale>.json`, **not** the generated Paraglide output — `i18n/paraglide/` is build output, so typing against it makes the tests unbuildable until the app has been built. The JSON is the tracked source.
56
56
  - Fall back to the base locale for a key a locale has not translated. That is what Paraglide does at run time, so a helper that throws instead would disagree with the screen the test is looking at.
57
- - This is not only about locators. A copy literal passed to a **project helper** (`pick('Where would you like to work?', …)`) reaches the DOM the same way, and so does a pane name quoted back in a failure message. `pikku fabric validate` scans every string in a `*.steps.ts` / `*.scenario.ts` against the base catalogue and errors on any verbatim match, wherever it sits — except comments, and the `name` / `description` / `template` declared directly on a `pikkuFeature`, `pikkuScenario` or `pikkuScenarioStep`, which are Console meta written in the project's `locale` rather than app copy.
57
+ - This is not only about locators. A copy literal passed to a **project helper** (`pick('Where would you like to work?', …)`) reaches the DOM the same way, and so does a pane name quoted back in a failure message. `pikku validate` scans every string in a `*.steps.ts` / `*.scenario.ts` against the base catalogue and errors on any verbatim match, wherever it sits — except comments, and the `name` / `description` / `template` declared directly on a `pikkuFeature`, `pikkuScenario` or `pikkuScenarioStep`, which are Console meta written in the project's `locale` rather than app copy.
58
58
  - A regex locator (`{ name: /^Next$/i }`) hides the literal but not the problem. `{ name: t('key'), exact: true }` is both stricter and locale-correct.
59
- - Strings the catalogue does not own — a test id, a fixture filename, a seeded value — stay literal. The catalogue is the test for whether something is copy.
59
+ - Strings the catalogue does not own — a test id, a fixture filename, a seeded value — stay literal. The catalogue is the test for whether something is copy.
@@ -18,7 +18,7 @@ one as a flag would be worse than failing.
18
18
  ```bash
19
19
  pikku dev --coverage # V8 precise coverage, in-process
20
20
  pikku dev --coverage --test # also enable stubs (needed for expectService)
21
- SCENARIO_ACTOR_SECRET=… pikku scenario run local --coverage
21
+ pikku scenario run local --coverage
22
22
  ```
23
23
 
24
24
  The run resets coverage before each scenario and snapshots after, writing **`<outDir>/coverage/scenario-coverage.json`**:
@@ -33,7 +33,7 @@ The run resets coverage before each scenario and snapshots after, writing **`<ou
33
33
  }
34
34
  ```
35
35
 
36
- Coverage is best-effort: it disables itself with a warning if the server is not collecting or the first actor cannot invoke, and it needs at least one configured actor. If you get no coverage, check those first.
36
+ Coverage is best-effort: it disables itself with a warning if the server is not collecting or the first virtual user cannot invoke, and it needs at least one configured virtual user. If you get no coverage, check those first.
37
37
 
38
38
  **There is no AI-prompt output.** The old `--ai-out` flag died with `pikku tests`; nothing replaced it. To find what needs work, read `scenario-coverage.json` yourself and cross-reference `pikku meta functions list` for input/output schemas.
39
39
 
@@ -41,7 +41,7 @@ Coverage is best-effort: it disables itself with a warning if the server is not
41
41
 
42
42
  1. `pikku scenario run <env> --coverage`, then read `<outDir>/coverage/scenario-coverage.json` to see what is unexercised.
43
43
  2. `pikku meta functions list` for those functions' schemas.
44
- 3. Write a `pikkuScenario` that reaches them **through a real user flow** with an actor — not a scenario per function. Scenarios are flows; coverage is a consequence.
44
+ 3. Write a `pikkuScenario` that reaches them **through a real user flow** with a virtual user — not a scenario per function. Scenarios are flows; coverage is a consequence.
45
45
  4. Re-run to confirm.
46
46
 
47
47
  ## Unit tests for pure logic
@@ -67,4 +67,4 @@ describe('createTodo', () => {
67
67
  node --import tsx --test src/**/*.test.ts
68
68
  ```
69
69
 
70
- Services are plain objects — a Pikku function is pure business logic, so a mock is just the shape the function destructures. Build real services via the `pikkuServices` / `pikkuWireServices` factories when a test needs them.
70
+ Services are plain objects — a Pikku function is pure business logic, so a mock is just the shape the function destructures. Build real services via the `pikkuServices` / `pikkuWireServices` factories when a test needs them.
@@ -16,7 +16,7 @@ Browser bindings have their own file: **`references/browser.md`**.
16
16
 
17
17
  ## What a step is
18
18
 
19
- `scenario.do` can only name an RPC. A **step** is a named, typed unit of scenario behaviour whose body is an ordinary pikku function — so it can call several RPCs as its actor, assert, or drive a browser.
19
+ `scenario.do` can only name an RPC. A **step** is a named, typed unit of scenario behaviour whose body is an ordinary pikku function — so it can call several RPCs as its virtual user, assert, or drive a browser.
20
20
 
21
21
  ```typescript
22
22
  import { pikkuScenarioStep } from '#pikku/scenarios'
@@ -28,9 +28,9 @@ export const buysAnApple = pikkuScenarioStep<
28
28
  name: 'buysAnApple',
29
29
  description: 'buys an apple',
30
30
  template: 'buys {qty} apples',
31
- actor: true,
32
- default: async (_services, { qty }, { actor }) => {
33
- return await actor.invoke('placeOrder', { qty })
31
+ virtualUser: true,
32
+ default: async (_services, { qty }, { virtualUser }) => {
33
+ return await virtualUser.invoke('placeOrder', { qty })
34
34
  },
35
35
  })
36
36
  ```
@@ -44,7 +44,7 @@ await scenario.given(
44
44
  'buys an apple',
45
45
  'buysAnApple',
46
46
  { qty: 1 },
47
- { actor: actors.shopper }
47
+ { virtualUser: virtualUsers.shopper }
48
48
  )
49
49
  // reporter renders: Given shopper buys 1 apples ✓ 412ms
50
50
  ```
@@ -53,14 +53,14 @@ Rules that bite:
53
53
 
54
54
  - **The step is referenced by its typed string name, not by importing the const** — exactly like `workflow.do`. The name is the step's `pikkuFuncId` and is checked against the generated step map. A non-literal target is a critical error (`PKU678`).
55
55
  - **Steps are not RPCs.** They are deliberately never network-callable — a browser-driving step must not be.
56
- - **`actor.invoke` is typed over the exposed RPC map**, so the name and the payload are checked and the result comes back narrowed — no cast. `actor.invokeRaw(name, data, { headers })` is the same call reporting `{ status, ok, body }` instead of throwing; use it whenever the refusal _is_ the assertion.
57
- - **A step that runs as somebody declares `actor: true`**, and the runner injects `wire.actor` — non-optional inside every binding, with no guard to write and nothing to unwrap. A `browser` binding implies it, because a window is opened as somebody. Leave it off for a step with no persona to be: an assertion over what an earlier step returned, or one that posts credentials precisely because it must not reuse an actor's session. Dispatching a step that declared it without `{ actor: actors.x }` fails before the body runs (`ScenarioActorRequired`); a step that did not declare it has no `actor` on its wire at all.
56
+ - **`virtualUser.invoke` is typed over the exposed RPC map**, so the name and the payload are checked and the result comes back narrowed — no cast. `virtualUser.invokeRaw(name, data, { headers })` is the same call reporting `{ status, ok, body }` instead of throwing; use it whenever the refusal _is_ the assertion.
57
+ - **A step that runs as somebody declares `virtualUser: true`**, and the runner injects `wire.virtualUser` — non-optional inside every binding, with no guard to write and nothing to unwrap. A `browser` binding implies it, because a window is opened as somebody. Leave it off for a step with no virtual user to be: an assertion over what an earlier step returned, or one that posts credentials precisely because it must not reuse a virtual user's session. Dispatching a step that declared it without `{ virtualUser: virtualUsers.x }` fails before the body runs (`ScenarioVirtualUserRequired`); a step that did not declare it has no `virtualUser` on its wire at all.
58
58
  - **`env` is optional on the wire**, because most steps need nothing from it. Narrow it with `requireScenarioEnv(scenarioStep)` from `#pikku/scenario` rather than a local guard — it names the step and says what to pass. `env` is `{ apiUrl, appUrl? }` from the environment the run targets, and is how a raw-HTTP step learns the target's URL: a step runs in the CLI process, where there is no `variables` service and `process.env` is not the answer.
59
59
  - **Steps default to `retries: 0`**, unlike ordinary workflow steps. Retrying a failed assertion is wrong; pass `retries` explicitly if a step is genuinely flaky-by-nature.
60
60
  - **Step results are persisted**, so return JSON-serialisable data — never a `Locator` or a client object.
61
61
  - **`description` documents the step; `template` is what the report renders.** `template`'s `{placeholders}` are filled from the input the step was called with, so one step reads differently for each call — `sees {state} addon {packageName}` reports as "sees available addon @pikku/addon-stripe". Reflect every input field in the template, and type the values so they read as words (`state?: 'installed' | 'available'`, not `installed?: boolean`). A placeholder with no value renders as nothing and the whitespace collapses.
62
- - **Never write the actor into the prose.** The reporter renders the actor as the sentence's subject, so a step authored as `` `'sam' creates the client` `` run as `{ actor: actors.sam }` reads "Given sam 'sam' creates the client" — and the hardcoded name desyncs the moment the call site changes actor. Write a bare third-person predicate (`creates the {name} client`) and let the actor supply the subject. Prose that opens with its own actor's key — quoted, capitalised or possessive — is `PKU681`; naming someone **else** mid-sentence ("sends nadia an invite") is ordinary prose and is left alone, as is an actor keyed after a role noun used as a noun ("creates the admin client" as `actors.admin`).
63
- - Prose precedence is `options.description` → the step's `template` → the step's own `description` → the positional step name. Repeated names get `#1`, `#2` ordinals, so a `for` loop over a data set is how you write a Scenario Outline. A loop-generated step name is not statically known, so it is matched back to its declaration by step function instead — which works as long as that function's call sites agree on their phase, actor and prose. Two call sites that disagree make the loop step report under its bare runtime name.
62
+ - **Never write the virtual user into the prose.** The reporter renders the virtual user as the sentence's subject, so a step authored as `` `'sam' creates the client` `` run as `{ virtualUser: virtualUsers.sam }` reads "Given sam 'sam' creates the client" — and the hardcoded name desyncs the moment the call site changes virtual user. Write a bare third-person predicate (`creates the {name} client`) and let the virtual user supply the subject. Prose that opens with its own virtual user's key — quoted, capitalised or possessive — is `PKU681`; naming someone **else** mid-sentence ("sends nadia an invite") is ordinary prose and is left alone, as is a virtual user keyed after a role noun used as a noun ("creates the admin client" as `virtualUsers.admin`).
63
+ - Prose precedence is `options.description` → the step's `template` → the step's own `description` → the positional step name. Repeated names get `#1`, `#2` ordinals, so a `for` loop over a data set is how you write a Scenario Outline. A loop-generated step name is not statically known, so it is matched back to its declaration by step function instead — which works as long as that function's call sites agree on their phase, virtual user and prose. Two call sites that disagree make the loop step report under its bare runtime name.
64
64
 
65
65
  ## What a step is given
66
66
 
@@ -82,7 +82,7 @@ not.
82
82
  `guardRpc` whose every member refuses:
83
83
 
84
84
  > Scenario tried to run 'getOrder' as an internal step. Every workflow.do in a
85
- > scenario must carry { actor: actors.x } so it executes against 'local'.
85
+ > scenario must carry { virtualUser: virtualUsers.x } so it executes against 'local'.
86
86
 
87
87
  The same guard covers `rpc.agent.run/stream/resume/approve/interrupt` and
88
88
  `startWorkflow`.
@@ -90,10 +90,10 @@ The same guard covers `rpc.agent.run/stream/resume/approve/interrupt` and
90
90
  This is the design, not a gap: **everything a step touches of the application
91
91
  goes over the wire as somebody.** A test that could reach into the database
92
92
  would be testing a different program from the one a person uses. So there are
93
- exactly three ways in, and they are all through the actor:
93
+ exactly three ways in, and they are all through the virtual user:
94
94
 
95
- - `actor.invoke(name, data)` — typed over the exposed RPC map, carrying the
96
- actor's session. Declare `actor: true` and destructure it off the wire.
95
+ - `virtualUser.invoke(name, data)` — typed over the exposed RPC map, carrying the
96
+ virtual user's session. Declare `virtualUser: true` and destructure it off the wire.
97
97
  - `.invokeRaw(name, data, { headers })` — same call, reporting
98
98
  `{ status, ok, body }`, for when the refusal is the assertion.
99
99
  - a plain `fetch` against `requireScenarioEnv(scenarioStep).apiUrl`, for
@@ -108,9 +108,10 @@ Two consequences follow, and both shape how steps get written:
108
108
  - **`agentRunner` is conditional.** It is built only when the project declares
109
109
  agents, and `createDevAgentRunner` needs a base URL _and_ a key together
110
110
  (`OPENAI_BASE_URL` + `OPENAI_API_KEY`, or the LiteLLM pair). With a key alone
111
- it returns nothing and `agentRunner` is `undefined`, so `actor.converse`
112
- fails before the persona says anything. A suite that would rather own its own
111
+ it returns nothing and `agentRunner` is `undefined`, so `virtualUser.converse`
112
+ fails before the virtual user says anything. A suite that would rather own its own
113
113
  model can pass an `llm` to `runConversation` instead of relying on this one.
114
+
114
115
  ---
115
116
 
116
117
  ## Steps describe intent, not actions
@@ -193,7 +194,7 @@ export const buysTheItem = pikkuScenarioStep<
193
194
  name: 'buysTheItem',
194
195
  description: 'finds one item in the shop and puts it in the basket',
195
196
  template: 'buys the {name}',
196
- // One intent, one implementation per surface an actor can drive it through.
197
+ // One intent, one implementation per surface a virtual user can drive it through.
197
198
  browser: async (_services, { name }, { browser }) => {
198
199
  await ensureOnShop(browser)
199
200
  await searchFor(browser, name)
@@ -215,7 +216,7 @@ await scenario.when(
215
216
  'buys a milkshake',
216
217
  'buysTheItem',
217
218
  { name: '£5 strawberry milkshake' },
218
- { actor: actors.shopper }
219
+ { virtualUser: virtualUsers.shopper }
219
220
  )
220
221
  // reporter renders: When shopper buys the £5 strawberry milkshake ✓ 1.2s
221
222
  ```
@@ -254,7 +255,7 @@ const order = await scenario.do(
254
255
  'Shopper checks out',
255
256
  'createOrder',
256
257
  { basketId, shippingAddress },
257
- { actor: actors.shopper }
258
+ { virtualUser: virtualUsers.shopper }
258
259
  )
259
260
  ```
260
261
 
@@ -282,9 +283,9 @@ export const seesTheOrderConfirmed = pikkuScenarioStep<
282
283
  { status: string }
283
284
  >({
284
285
  name: 'seesTheOrderConfirmed',
285
- // Both bindings run as the persona, so the step declares one and the runner
286
- // injects `wire.actor` — non-optional in every binding.
287
- actor: true,
286
+ // Both bindings run as the virtual user, so the step declares one and the runner
287
+ // injects `wire.virtualUser` — non-optional in every binding.
288
+ virtualUser: true,
288
289
  template: 'sees order {orderId} confirmed',
289
290
  // Both run on `--run browser`. Each returns what it observed, and the runner
290
291
  // compares them — so this fails when the page disagrees with the database.
@@ -293,9 +294,9 @@ export const seesTheOrderConfirmed = pikkuScenarioStep<
293
294
  .locate({ testId: 'order-status', where: { 'data-order': orderId } })
294
295
  .getAttribute('data-status'),
295
296
  }),
296
- // Through the actor, not through a `rpc` service — see "What a step is given".
297
- default: async (_services, { orderId }, { actor }) => ({
298
- status: (await actor.invoke('getOrder', { orderId })).status,
297
+ // Through the virtualUser, not through a `rpc` service — see "What a step is given".
298
+ default: async (_services, { orderId }, { virtualUser }) => ({
299
+ status: (await virtualUser.invoke('getOrder', { orderId })).status,
299
300
  }),
300
301
  })
301
302
  ```
@@ -304,7 +305,7 @@ Three rules follow, and they are the ones that get broken:
304
305
 
305
306
  - **A browser witness must observe on the page.** One that quietly calls an RPC to check the result is worse than no binding at all — it reports a tick for a surface it never looked at.
306
307
  - **Return what you observed, don't just assert.** A witness returning a value lets the runner diff the two. A witness that only throws still works, but it can never disagree with anything, so it proves less. Read structured state with `where` on the test-id selector rather than parsing translated copy.
307
- - **A step with no binding for the run's surface is counted, not excused.** `--run browser` prints `n/m steps ran on browser` over _every_ step, so an action that quietly fell back to the server lowers the number just as an assertion does. A `then` that fell back is additionally named — `--strict` fails on those, because a sentence saying the actor saw something nobody looked at is a different problem from a shortcut. Not being in the UI _is_ the finding: do not add a browser binding that fakes it.
308
+ - **A step with no binding for the run's surface is counted, not excused.** `--run browser` prints `n/m steps ran on browser` over _every_ step, so an action that quietly fell back to the server lowers the number just as an assertion does. A `then` that fell back is additionally named — `--strict` fails on those, because a sentence saying the virtual user saw something nobody looked at is a different problem from a shortcut. Not being in the UI _is_ the finding: do not add a browser binding that fakes it.
308
309
 
309
310
  **Always give a `then` a `default` witness.** It is the floor every run can fall back to, and an assertion with no witness the run can execute is fatal (`ScenarioNoWitness`) — not a coverage gap. The distinction is the point: a `then` checked server-side under `--run browser` did happen, it just wasn't seen where the prose claims; one checked nowhere never happened at all, and without the error it would return `undefined` and render as a tick. A browser-only `then` is therefore a step that fails the fast suite, which is rarely what you want.
310
311
 
@@ -343,9 +344,9 @@ export const buysAnApple = pikkuScenarioStep<
343
344
  name: 'buysAnApple', // identifier — English, always
344
345
  description: 'kauft einen Apfel', // prose — follows locale
345
346
  template: 'kauft {qty} Äpfel', // prose — follows locale
346
- actor: true,
347
- default: async (_services, { qty }, { actor }) =>
348
- await actor.invoke('placeOrder', { qty }),
347
+ virtualUser: true,
348
+ default: async (_services, { qty }, { virtualUser }) =>
349
+ await virtualUser.invoke('placeOrder', { qty }),
349
350
  })
350
351
  ```
351
352
 
@@ -356,11 +357,11 @@ A product with a non-English UI is not on its own a reason to set `metaLocale`
356
357
  is the app's language, not the team's. Ask, or leave it `en`.
357
358
 
358
359
  **Where a non-`en` `metaLocale` still shows English, today.** The reporter composes a
359
- sentence as `<Keyword> <actor> <template>` (`composeStepProse`), and the keyword is
360
+ sentence as `<Keyword> <virtual user> <template>` (`composeStepProse`), and the keyword is
360
361
  an English literal. The Console translates the Given/When/Then keywords into its own
361
362
  UI language; the CLI reporter does not, so `metaLocale: "de"` gives you German step
362
363
  prose inside an English frame — `Given shopper kauft 1 Äpfel`. Write templates that read
363
364
  acceptably in that frame rather than trying to defeat it. A second gap: where a
364
365
  function or scenario declares no `title`, the Console falls back to splitting the
365
366
  **identifier** into an English-looking label (`toEnglishName`), so under a
366
- non-`en` `metaLocale` meta is worth authoring rather than leaving to the fallback.
367
+ non-`en` `metaLocale` meta is worth authoring rather than leaving to the fallback.