@pikku/skills 0.12.37 → 0.12.38

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.
@@ -35,13 +35,13 @@ Use this skill as an execution checklist, not reference material.
35
35
  This skill covers writing and running scenarios end to end. Four topics are one level down, and
36
36
  each says when to open it:
37
37
 
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`, 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 |
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 |
45
45
 
46
46
  ## What a scenario is
47
47
 
@@ -245,7 +245,7 @@ suite survives its first redesign:
245
245
  through the actor.
246
246
  - **Steps describe intent, not actions** — `buys the £5 strawberry milkshake`, never
247
247
  `clicks [data-testid=add]`. The clicking lives in plain utilities the step composes.
248
- - **`then` bindings are witnesses, not alternatives** — a `then` runs *every* binding it declares
248
+ - **`then` bindings are witnesses, not alternatives** — a `then` runs _every_ binding it declares
249
249
  and fails when they disagree, because "database right, user still watching a spinner" is the bug
250
250
  nobody catches.
251
251
  - **What language the prose is in** — identifiers are English in every project; `description`,
@@ -265,6 +265,7 @@ whole project — codegen builds the `PersonaId` union from it, materialises one
265
265
  scenario actor per person, and seeds a user row each:
266
266
 
267
267
  ```ts snippet:definePersonas
268
+
268
269
  ```
269
270
 
270
271
  `pikku.config.json` carries the settings around them — nothing about a person:
@@ -5,7 +5,7 @@ A **persona** is a person your product is for; an **actor** is one body that sig
5
5
  - Two people of the same kind are two entries, not one persona with two logins — "you see yours, not theirs" is only testable with two customers.
6
6
  - Never write an email address: each is derived from the persona id and `scenarios.emailDomain`, and a hand-written one signs in as somebody who was never created.
7
7
  - `roles` is typechecked against `defineSystemRole`; an undeclared role is a build error.
8
- - A person who is only ever acted *upon* — the account an admin bans — sets `runnable: false`: declared and seeded, never signed in, because a run as them would race the scenario that acts on them.
8
+ - A person who is only ever acted _upon_ — the account an admin bans — sets `runnable: false`: declared and seeded, never signed in, because a run as them would race the scenario that acts on them.
9
9
  - A persona holds only what is true of that kind of person for the app's whole lifetime (`name`, `jobTitle`, `description`, `personality`, `roles`, `goals`, `disposition`). What someone is trying to get done, and the circumstances they are doing it in, belong to the **scenario**, not to them.
10
10
 
11
11
  ## Declaring personas in TypeScript
@@ -50,6 +50,32 @@ A project that never declares a persona keeps working: a scenario that names no
50
50
  - `environments.<name>.apiUrl` is required. `signInPath` defaults to `/auth/sign-in/actor`, `rpcPath` to `/rpc`.
51
51
  - **`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.
52
52
 
53
+ ## Actors that call a third-party API
54
+
55
+ An addon generated from an upstream API (Dolibarr, a CRM, a calendar) calls it
56
+ with the signed-in user's own credential. An actor has none until one is stored
57
+ for it, so every step that reaches the addon fails with `No <X> session`. Give
58
+ the `actor` plugin a `credentials` option, and each actor carries its upstream
59
+ credential into every session:
60
+
61
+ - `names: ['dolibarr']` — the credentials to carry
62
+ - `store: (name, value, userId) => credentialService.set(name, value, userId)`
63
+ - `remove: (name, userId) => credentialService.delete(name, userId)`
64
+
65
+ At each actor sign-in the plugin reads `ACTOR_CREDENTIAL_<PERSONA>_<NAME>` —
66
+ `dan` + `dolibarr` is `ACTOR_CREDENTIAL_DAN_DOLIBARR` — and stores it with
67
+ `credentialService.set` for that actor. A bare value is stored as `{ token }`
68
+ (what a delegated or bearer credential holds); a JSON object is stored as-is,
69
+ e.g. `{"apiKey":"…"}` for an API-key credential. Unset means the actor has no
70
+ upstream credential: `remove` drops one stored at an earlier sign-in.
71
+
72
+ - **Values live in `.env` (or CI secrets), never in `personas.ts` or code.**
73
+ Use a dedicated upstream test account per persona, not a real person's.
74
+ - `read` defaults to `process.env`; a Worker passes `(key) => variables.get(key)`.
75
+ - A malformed JSON value refuses the sign-in with a 500 that names the variable.
76
+ - A delegated token expires upstream like any other; re-signing the actor
77
+ in re-stores whatever the variable holds now.
78
+
53
79
  ## The same actors sign a human in
54
80
 
55
81
  Declared actors are not only for automated runs. `signInPath` is Better Auth's
@@ -57,8 +83,8 @@ Declared actors are not only for automated runs. `signInPath` is Better Auth's
57
83
  frontend gets a one-click "Sign in as …" switcher over the **same** list, and an
58
84
  app can be reviewed as each kind of user without anyone knowing a seed password.
59
85
 
60
- The sandbox dev server bakes both halves into the frontend from the declared
61
- personas: `VITE_DEV_ACTORS` (the JSON actor list) and `VITE_DEV_ACTOR_SECRETS`
86
+ The dev server bakes both halves into the frontend from the declared
87
+ personas — the sandbox's, or the template's `bun run dev`, never `pikku dev`: `VITE_DEV_ACTORS` (the JSON actor list) and `VITE_DEV_ACTOR_SECRETS`
62
88
  (`{ email: credential }`, one per persona — `SCENARIO_ACTOR_SECRET` itself never
63
89
  goes in a bundle; see **pikku-auth**). Neither var is set in a production
64
90
  build, so the control renders nothing there — but gate the reads on your
@@ -85,3 +111,39 @@ When the switcher is there but signing in fails with `401 Invalid actor
85
111
  secret`, check which server answered before checking the secret: a frontend
86
112
  whose dev proxy (`VITE_API_PROXY`, default `http://localhost:3000`) points at
87
113
  another project's API sends the sign-in there.
114
+
115
+ **A runner of your own that starts vite has to bake them itself**, from the
116
+ generated persona meta (`<outDir>/workflow/personas.gen.json`, which already
117
+ carries the derived `email`):
118
+
119
+ ```js
120
+ const personas = Object.values(JSON.parse(readFileSync(personasPath, 'utf8')))
121
+
122
+ env.VITE_DEV_ACTORS = JSON.stringify(
123
+ personas.map(({ id, email, name, jobTitle }) => ({
124
+ key: id,
125
+ email,
126
+ name,
127
+ jobTitle: jobTitle ?? '',
128
+ }))
129
+ )
130
+ env.VITE_DEV_ACTOR_SECRETS = JSON.stringify(
131
+ Object.fromEntries(
132
+ await Promise.all(
133
+ personas.map(async ({ email }) => [
134
+ email,
135
+ await deriveActorSecret(env.SCENARIO_ACTOR_SECRET, email),
136
+ ])
137
+ )
138
+ )
139
+ )
140
+ ```
141
+
142
+ **Set `SCENARIO_ACTOR_SECRET` yourself**, at least 32 characters, in the
143
+ environment both processes read. Left unset, `pikku dev` mints an ephemeral root
144
+ for its own run that a separately spawned vite cannot see, so the two derive
145
+ from different roots: the switcher renders every persona and each click is
146
+ refused, which reads as a broken login rather than missing configuration. On a
147
+ brand-new project the persona file does not exist until the first `pikku dev`
148
+ codegen, after vite has baked an empty list — watch it and restart the frontend
149
+ when it changes.