Apply configurable-demo-persona: per-instance demo identity + voice
Adds DEMO_BOT_PERSONA env var (inline JSON or file:<path>) so self-hosted instances can replace Bruno-in-Berlin with their own demo account identity and content pools. No-op for trails.cool — the built-in Bruno persona remains the default. - DemoPersona type + Zod schema validation - loadPersona() cached at boot; falls back to default on any failure - ensureDemoUser throws DemoPersonaUsernameClashError when the persona username is already a real user; server declines to schedule demo jobs for that process - isDemoUser flag moved from hardcoded "bruno" check to loader-supplied boolean computed from the running persona - docs/demo-persona.md explains the schema + operator flow Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f447852d70
commit
fc4485f6ef
15 changed files with 877 additions and 113 deletions
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-19
|
||||
108
openspec/changes/configurable-demo-persona/design.md
Normal file
108
openspec/changes/configurable-demo-persona/design.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
## Context
|
||||
|
||||
The just-merged `demo-activity-bot` spec gave `trails.cool` a synthetic public user — Bruno, a Berlin dog on walkabout. The implementation works but hardcodes everything a host operator would sensibly want to customise: `username = "bruno"`, a fixed display name + bio, two string pools of English/German walk-name copy, and an inner-Berlin bbox as the default region.
|
||||
|
||||
Self-hosted instances are the core of this project's federation story. We don't want every federated instance running a Berlin-themed dog walker; that ranges from confusing (why does the Tokyo instance have a Berlin-named bot?) to actively off-brand (the persona voice is playful — not every community wants playful). If turning on `DEMO_BOT_ENABLED=true` ships an identity that isn't theirs, most operators just won't turn it on.
|
||||
|
||||
The bot itself (decide-to-walk gate, cap, BRouter client, retention, synthetic column, metrics) is generic and has no reason to change. Only the *persona* — the identity + voice — needs to become operator-supplied.
|
||||
|
||||
Region is already configurable via `DEMO_BOT_REGION`. That stays as-is; this proposal does not merge region into the persona blob because regions and personas are orthogonal (an operator may want to run the default Bruno persona but in a different bbox for testing).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- A single env var — `DEMO_BOT_PERSONA` — that accepts either an inline JSON blob or a `file:` URL pointing at a mounted JSON file, so operators can ship either via SOPS env or via a config volume.
|
||||
- Built-in default remains exactly the current Bruno persona, so `trails.cool` needs no operator action when this ships.
|
||||
- Persona supplies: `username`, `displayName`, `bio`, `locales: ("en" | "de")[]`, and `content: { names: { en?: string[]; de?: string[] }, descriptions: { en?: string[]; de?: string[] } }`.
|
||||
- `/users/<username>` continues to render the 🐕 demo badge, but the gate moves from the hardcoded string `"bruno"` to a loader-supplied `isDemoUser` boolean derived from the persona.
|
||||
- Malformed JSON, missing required fields, or an unreachable `file:` path fall back to the built-in Bruno persona and emit a single warn-level log line at worker boot. The worker never crashes over a bad persona file.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Runtime reload. The persona is read once at worker boot. Operators restart the journal container to pick up a new persona. Live reload adds complexity for no user value (operators change personas monthly at most).
|
||||
- Per-request persona selection. There is exactly one demo user per instance.
|
||||
- Allowing the persona to be edited in the web UI by an admin. That would be a separate `admin-persona-editor` change; out of scope here.
|
||||
- Supporting arbitrary locales beyond EN + DE. The app only has those two locale bundles today, and adding locales is a cross-cutting change tracked elsewhere.
|
||||
- Moving region into the persona blob. Region and persona are orthogonal; two env vars remain two env vars.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. One JSON blob via env, with `file:` fallback for multi-line content.
|
||||
|
||||
Go with a single `DEMO_BOT_PERSONA` env var. Accept two shapes:
|
||||
|
||||
1. Inline JSON — `DEMO_BOT_PERSONA='{"username":"bruno",...}'`. Fine for short personas.
|
||||
2. `file:`-prefixed path — `DEMO_BOT_PERSONA=file:/etc/trails-cool/persona.json`. For realistic personas the pools are ~10–15 entries each; inlining 50 strings of JSON into a SOPS env file is miserable.
|
||||
|
||||
*Alternatives considered:*
|
||||
- **Separate env vars for each field** (`DEMO_BOT_USERNAME`, `DEMO_BOT_DISPLAY_NAME`, `DEMO_BOT_NAME_POOL_EN`, …). Rejected: 10+ related env vars, and list-valued vars need an ad-hoc separator that invites quoting bugs.
|
||||
- **TOML or YAML file**. Rejected: the rest of the repo uses JSON env blobs (`DEMO_BOT_REGION` is already JSON). Stay consistent.
|
||||
- **`DEMO_BOT_PERSONA_FILE` as a separate env var**. Rejected: two env vars for one concept. The `file:` prefix is a well-known convention (12-factor app config).
|
||||
|
||||
### D2. Zod schema for validation.
|
||||
|
||||
Parse with a Zod schema that enforces: username matches `^[a-z0-9][a-z0-9_-]{1,30}$` (same constraint as registration), `displayName` and `bio` are non-empty strings up to 200 chars, `locales` is a non-empty subset of `["en", "de"]`, each `content.names.<locale>` and `content.descriptions.<locale>` is an array of 3–50 non-empty strings.
|
||||
|
||||
Reason for the 3–50 bound: fewer than 3 makes repeats painfully obvious in the daily feed; more than 50 eats unneeded memory and suggests the operator should move to a content pipeline instead of hand-editing JSON.
|
||||
|
||||
*Alternatives considered:*
|
||||
- **No validation — trust the operator.** Rejected: malformed persona is a silent footgun. A bad regex in `username` is a violation of the DB constraint and would crash `ensureDemoUser()` on boot.
|
||||
- **Hand-rolled runtime type checks.** Rejected: the repo already uses Zod in `@trails-cool/api` for request validation. Reuse the pattern.
|
||||
|
||||
### D3. Persona loading happens once, at worker boot, cached for process lifetime.
|
||||
|
||||
Add `loadPersona()` that returns a `DemoPersona` object. It's called from `demo-bot.server.ts` module init (not from each job handler) and the result is frozen. `ensureDemoUser()` and the job handlers all read from this module-level constant.
|
||||
|
||||
Side effect: if an operator changes the persona JSON and the worker is already running, the change doesn't take effect until the container restarts. This is the intended behaviour (see non-goals) and is the same pattern as `DEMO_BOT_REGION` + `DEMO_BOT_RETENTION_DAYS` today.
|
||||
|
||||
### D4. Built-in default persona lives in code, not a default JSON file.
|
||||
|
||||
The existing Bruno strings become a `DEFAULT_PERSONA: DemoPersona` export in `demo-bot.server.ts`. When no `DEMO_BOT_PERSONA` is set, or validation fails, `loadPersona()` returns the default. This keeps the happy path (no operator config) identical to what just shipped.
|
||||
|
||||
*Alternatives considered:*
|
||||
- **Bundle a `personas/bruno.json` in the image and set `DEMO_BOT_PERSONA=file:...` by default.** Rejected: more moving parts for no benefit. Everyone who reads the code already sees the strings; shipping them as a JSON asset doesn't improve readability and adds a runtime read on every boot.
|
||||
|
||||
### D5. The 🐕 demo badge becomes a loader-supplied flag.
|
||||
|
||||
Today `users.$username.tsx` checks `user.username === "bruno"` client-side. That was fine when Bruno was a literal. Now the check must know which username the running instance chose as its demo user. Two options:
|
||||
|
||||
- **Ship the persona username to the client** so the comparison stays client-side.
|
||||
- **Have the loader compute `isDemoUser` and pass a boolean to the component.**
|
||||
|
||||
Choose the loader path. The persona username is not otherwise needed in the client, and shipping instance config through HTML reads as a small information leak even if it's technically public. `isDemoUser` is crisp.
|
||||
|
||||
### D6. No migration. No backwards-compatibility shim. No deprecation window.
|
||||
|
||||
This change is additive: new env var, default preserves current behaviour. There is nothing to migrate. The hardcoded string constants (`DEMO_USERNAME`, `DEMO_DISPLAY_NAME`, `DEMO_BIO`, `NAME_POOL_*`, `DESCRIPTION_POOL_*`) are removed and folded into `DEFAULT_PERSONA`. Importers of `DEMO_USERNAME` (there are none) would break — but the grep is clean.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk] Operator supplies a persona that conflicts with an existing real user's username on first enable.**
|
||||
→ `ensureDemoUser()`'s insert currently ON CONFLICT DO NOTHING on `username` would silently pick up the existing real user as the demo user's owner — all subsequent synthetic rows would attach to that real user. Fix: before insert, SELECT the username; if it exists AND the row is not already marked demo, log an error and disable the bot for this process. Track via a new `demo-bot persona username clash` log line; the operator sees a loud error on boot.
|
||||
|
||||
**[Risk] Operator supplies a persona with empty content pools.**
|
||||
→ The Zod schema rejects arrays shorter than 3. Worker falls back to the default persona and logs the rejection reason.
|
||||
|
||||
**[Risk] A `file:`-referenced JSON file is unreadable at boot (permissions, wrong mount).**
|
||||
→ `loadPersona()` catches the read error and falls back to the default persona with a warn log. The bot stays functional on the built-in Bruno.
|
||||
|
||||
**[Risk] The demo badge is skipped on a legitimately-chosen username that happens to clash with a real user.**
|
||||
→ Not a real issue in practice: the badge reads `isDemoUser` from the loader, which is derived by `userId === personaUserId`, not by username comparison. Two users can never share an ID.
|
||||
|
||||
**[Trade-off] One JSON blob vs. typed env vars.**
|
||||
→ JSON is harder to lint in SOPS/CI than `KEY=value` pairs. Mitigated by the `file:` mode for nontrivial personas, and by the Zod validation surfacing the parse error at worker boot.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
This is a no-op for `trails.cool` itself:
|
||||
|
||||
1. Merge + deploy. Nothing changes — the built-in Bruno persona is the default, produced by the exact same strings the bot generates today.
|
||||
2. For a self-hosted instance that wants its own persona: write `persona.json`, mount it or inline it via SOPS, set `DEMO_BOT_PERSONA=file:/path/to/persona.json`, set `DEMO_BOT_ENABLED=true`, and restart the journal container.
|
||||
|
||||
Rollback is trivial: unset `DEMO_BOT_PERSONA` and restart. The default persona returns.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the persona schema include an emoji / glyph override so a cat-themed instance can replace the 🐕 in the badge? Leaning yes but the glyph is i18n'd today (lives in the `demo.badge` key), which complicates things. Defer — ship without it, revisit if a real host asks.
|
||||
- Should we expose a `/api/demo-persona` endpoint for federation peers to fetch the current persona? No real need today; a federating peer doesn't need to know that a remote user is synthetic, only that their content is public. Park.
|
||||
31
openspec/changes/configurable-demo-persona/proposal.md
Normal file
31
openspec/changes/configurable-demo-persona/proposal.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
## Why
|
||||
|
||||
The demo-activity-bot is hardcoded to "Bruno the Berlin dog walker" — a persona that reads well for `trails.cool` itself but makes no sense for a self-hosted instance in Tokyo, Denver, or Edinburgh. If we want federated hosts to turn the demo on, each instance needs to choose its own demo identity, region, and copy so visitors see a plausible local-voice feed rather than a stranger's Berlin park patrol.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Make the demo user's identity (username, display name, bio) configurable per instance via env — today `ensureDemoUser()` inserts `bruno` with a fixed display name and bio.
|
||||
- Allow each instance to supply its own generated-content pools (route-name pool and description pool) in EN + DE — today these are hardcoded string arrays in `demo-bot.server.ts`.
|
||||
- Allow each instance to restrict the demo to its preferred locale(s) — today the bot picks EN or DE with a coin flip.
|
||||
- Keep all configuration in a single `DEMO_BOT_PERSONA` JSON blob (env or file path) so operators can supply one artifact instead of threading five env vars.
|
||||
- Retain the current built-in Bruno persona as the default when no override is supplied, so `trails.cool` itself keeps its current behavior with no operator action.
|
||||
- Keep `DEMO_BOT_ENABLED`, `DEMO_BOT_REGION`, and `DEMO_BOT_RETENTION_DAYS` as they are today (region is a separate concern from persona; retention + enable-flag are orthogonal).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
_None._
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `demo-activity-bot`: the bot's identity and generated copy become a **persona contract** supplied by the operator rather than a fixed Bruno/Berlin combination. The generation gate, retention, cap, and synthetic flag are unchanged.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Code:** `apps/journal/app/lib/demo-bot.server.ts` — replace the four hardcoded `*_POOL_*` arrays and the three `DEMO_*` constants with a loaded-once `DemoPersona` object. `ensureDemoUser()` consumes the persona's `username`/`displayName`/`bio`. `templateName`/`templateDescription` read from the persona's pools.
|
||||
- **UX:** `/users/<username>` — the 🐕 demo badge needs to gate on a runtime-known username, not a literal `"bruno"`. Move the gate to loader data (`isDemoUser` boolean) rather than a client-side string check.
|
||||
- **Ops:** `infrastructure/.env.example`, `docker-compose.yml` — add `DEMO_BOT_PERSONA` passthrough; document the JSON shape. Existing `DEMO_BOT_REGION` et al. stay.
|
||||
- **Docs:** a short persona-authoring note in `docs/` (one page) so host operators know the JSON shape + sensible pool sizes.
|
||||
- **Tests:** new unit tests for `loadPersona()` (env → persona, fallback to built-in Bruno, malformed JSON → fallback + warn). Existing integration + E2E tests continue to work unchanged because the default persona is still Bruno.
|
||||
- **No breaking change for `trails.cool`:** the built-in Bruno persona remains the default and is exactly the current hardcoded strings.
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Persona configuration
|
||||
The Journal SHALL load a demo persona — username, display name, bio, supported locales, and per-locale content pools — from configuration at worker boot, and SHALL fall back to a built-in default persona if no override is supplied or the supplied override fails validation.
|
||||
|
||||
#### Scenario: No override supplied — built-in default applies
|
||||
- **WHEN** the worker starts with `DEMO_BOT_ENABLED=true` and `DEMO_BOT_PERSONA` is unset
|
||||
- **THEN** the bot uses the built-in default persona (`username=bruno`, playful Berlin-flavoured display name and bio, `locales=["en","de"]`, and the shipped Bruno-voiced name/description pools)
|
||||
- **AND** behaviour is identical to the demo bot before this change
|
||||
|
||||
#### Scenario: Inline JSON override
|
||||
- **WHEN** `DEMO_BOT_PERSONA` is set to a valid inline JSON object with `username`, `displayName`, `bio`, `locales`, and `content.names` / `content.descriptions` for each listed locale
|
||||
- **THEN** the bot uses that persona — `ensureDemoUser` inserts a row with the persona's username/displayName/bio/sentinel email, and subsequent generated routes and activities draw names and descriptions from the persona's per-locale pools
|
||||
|
||||
#### Scenario: File-backed override
|
||||
- **WHEN** `DEMO_BOT_PERSONA` is set to `file:<absolute-path>` and the referenced file contains a valid persona JSON object
|
||||
- **THEN** the worker reads the file once at boot and uses its contents as the persona
|
||||
|
||||
#### Scenario: Invalid JSON or schema violation → fall back
|
||||
- **WHEN** `DEMO_BOT_PERSONA` is set but the value is not valid JSON, or fails the persona schema (bad username pattern, empty or too-short pool, unsupported locale, etc.)
|
||||
- **THEN** the worker logs a warn-level entry describing the first validation failure and uses the built-in default persona
|
||||
- **AND** the bot continues to run
|
||||
|
||||
#### Scenario: File-backed path unreadable → fall back
|
||||
- **WHEN** `DEMO_BOT_PERSONA=file:<path>` but the file cannot be read (missing, permission denied, not a file)
|
||||
- **THEN** the worker logs a warn-level entry and uses the built-in default persona
|
||||
|
||||
### Requirement: Persona username clash detection
|
||||
The Journal SHALL refuse to attach the demo bot to a pre-existing non-demo user account when the supplied persona username collides with a human user already registered on the instance.
|
||||
|
||||
#### Scenario: Configured username belongs to a real user
|
||||
- **WHEN** the worker starts, the persona's username matches an existing `users` row, and that row has no marker identifying it as a prior demo user (i.e. it was registered via the normal signup flow)
|
||||
- **THEN** the worker logs an error-level "demo persona username clash" entry naming the colliding username
|
||||
- **AND** the generation + prune jobs are not scheduled for this process — the bot stays disabled until the operator picks a different username
|
||||
- **AND** the rest of the Journal continues to serve requests normally
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Demo user bootstrap
|
||||
The Journal SHALL ensure a dedicated bot user exists when the demo bot starts, creating it on first run if missing. The user's identity (username, display name, bio, sentinel email local-part) is derived from the active persona — either the operator-supplied persona or the built-in default.
|
||||
|
||||
#### Scenario: Bot user created on first run
|
||||
- **WHEN** the Journal worker starts with `DEMO_BOT_ENABLED=true` and no `users` row matches the persona's username
|
||||
- **THEN** a new `users` row is inserted with that username, the persona's display name, the persona's bio, a sentinel email `<username>@<domain>`, no passkey credentials, and `terms_accepted_at` + `terms_version` populated at the current version
|
||||
- **AND** subsequent worker startups are idempotent — no second row is inserted
|
||||
|
||||
#### Scenario: Demo user has no usable credentials
|
||||
- **WHEN** any request attempts to authenticate as the demo user via passkey or magic-link
|
||||
- **THEN** authentication fails because no passkey is registered and no mailbox receives magic-link mails
|
||||
|
||||
### Requirement: Synthetic content generation job
|
||||
The Journal SHALL run a recurring background job that generates one public route and one linked public activity for the demo user per run, subject to an env flag and a rate cap. The generated name and description are drawn from the active persona's per-locale content pools.
|
||||
|
||||
#### Scenario: Disabled in non-production environments
|
||||
- **WHEN** the `DEMO_BOT_ENABLED` env var is absent or any value other than `"true"`
|
||||
- **THEN** the job body is a no-op: no BRouter calls, no inserts, no errors
|
||||
|
||||
#### Scenario: Enabled generation flow (decide-to-walk fires)
|
||||
- **WHEN** `DEMO_BOT_ENABLED=true`, the local hour is within 07:00–21:00, the per-tick Bernoulli roll fires, and the hard cap has not been reached
|
||||
- **THEN** the job picks a random start and end point within the configured seed region, calls BRouter with the `trekking` profile, persists the returned GPX as a new route with `visibility='public'` and `synthetic=true`, and inserts a linked activity with the same GPX, `visibility='public'`, `synthetic=true`, a plausible `started_at` and `duration`, and a persona-voiced name + description sampled from one of the persona's supported locales
|
||||
- **AND** the route and activity are attributed to the demo user
|
||||
|
||||
#### Scenario: Locale restricted to a single language
|
||||
- **WHEN** the persona's `locales` list is `["en"]`
|
||||
- **THEN** every generated route's name and description come from the persona's English pool — the German pool is never sampled
|
||||
|
||||
#### Scenario: Decide-to-walk does not fire
|
||||
- **WHEN** the local hour is outside 07:00–21:00, or the Bernoulli roll does not fire
|
||||
- **THEN** the job returns without inserting anything
|
||||
|
||||
#### Scenario: BRouter failure is tolerated
|
||||
- **WHEN** the BRouter call returns no route, a rate-limit, or an error
|
||||
- **THEN** the job logs the failure, inserts nothing, and exits without throwing — the next scheduled tick retries
|
||||
|
||||
#### Scenario: Hard cap prevents runaway growth
|
||||
- **WHEN** there are already 40 or more synthetic items created in the last 14 days
|
||||
- **THEN** the job skips generation for that tick
|
||||
|
||||
#### Scenario: Singleton scheduling prevents overlap
|
||||
- **WHEN** a tick fires while the previous run is still executing
|
||||
- **THEN** the new tick is skipped (pg-boss singleton semantics) so the job cannot overlap itself
|
||||
45
openspec/changes/configurable-demo-persona/tasks.md
Normal file
45
openspec/changes/configurable-demo-persona/tasks.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
## 1. Persona type + schema
|
||||
|
||||
- [x] 1.1 Define `DemoPersona` TypeScript interface in `apps/journal/app/lib/demo-bot.server.ts` with `username`, `displayName`, `bio`, `locales: ("en"|"de")[]`, `content: { names: { en?: string[]; de?: string[] }, descriptions: { en?: string[]; de?: string[] } }`
|
||||
- [x] 1.2 Add a Zod schema mirroring the interface: username `^[a-z0-9][a-z0-9_-]{1,30}$`, displayName 1–200 chars, bio 0–200 chars, locales a non-empty subset of `["en","de"]`, each content array 3–50 non-empty strings, ensure every listed locale has both `names` and `descriptions` populated
|
||||
- [x] 1.3 Move the current hardcoded Bruno strings into a `DEFAULT_PERSONA: DemoPersona` export — confirm it round-trips through the Zod schema before export
|
||||
|
||||
## 2. Loader
|
||||
|
||||
- [x] 2.1 Add `loadPersona(): DemoPersona` that reads `DEMO_BOT_PERSONA`: if unset → `DEFAULT_PERSONA`; if it starts with `file:` → read the rest as an absolute path via `fs.readFileSync`; otherwise treat as inline JSON
|
||||
- [x] 2.2 Parse with the Zod schema; on validation failure or file-read failure, log a warn with the first error message and return `DEFAULT_PERSONA`
|
||||
- [x] 2.3 Cache the result in a module-level `Object.freeze`d constant so every caller sees the same instance
|
||||
- [x] 2.4 Remove the now-unused `DEMO_USERNAME`, `DEMO_DISPLAY_NAME`, `DEMO_BIO`, `NAME_POOL_EN`, `NAME_POOL_DE`, `DESCRIPTION_POOL_EN`, `DESCRIPTION_POOL_DE` constants — every reference must flow through the persona
|
||||
|
||||
## 3. Wire into bootstrap + generation
|
||||
|
||||
- [x] 3.1 `ensureDemoUser()` takes the loaded persona and writes `username`, `displayName`, `bio`, and sentinel email `${persona.username}@<domain>`
|
||||
- [x] 3.2 Before insert, `SELECT` the row matching the username; if it exists and was not inserted by a prior demo-bot boot (heuristic: email does not match the sentinel pattern), throw `DemoPersonaUsernameClashError`; server startup catches this and declines to schedule the demo jobs for the process
|
||||
- [x] 3.3 `templateName(startedAt, locale)` reads from `persona.content.names[locale]` with the existing seeded indexing; same for `templateDescription`
|
||||
- [x] 3.4 Locale selection in `generateOneWalk` picks randomly from `persona.locales` via `pickLocale()` (dropping the hardcoded 50/50 EN/DE coin flip)
|
||||
|
||||
## 4. UX — demo badge via loader flag
|
||||
|
||||
- [x] 4.1 In `users.$username.tsx` loader, compute `isDemoUser` by comparing `user.username` against `loadPersona().username`; include `isDemoUser` in the loader return
|
||||
- [x] 4.2 Replace the client-side `user.username === "bruno"` check with the loader-supplied boolean
|
||||
- [x] 4.3 Keep the `demo.badge` i18n key exactly as-is so existing translations continue to work
|
||||
|
||||
## 5. Env + docs
|
||||
|
||||
- [x] 5.1 Add `DEMO_BOT_PERSONA` passthrough in `infrastructure/docker-compose.yml` (journal service)
|
||||
- [x] 5.2 Add a commented `DEMO_BOT_PERSONA` example to `infrastructure/.env.example` with the inline JSON form AND the `file:/path/to/persona.json` form
|
||||
- [x] 5.3 Write `docs/demo-persona.md` — a one-page guide covering: schema shape, inline vs `file:` modes, required pool sizes, how the built-in default looks, and an example persona for a non-Berlin instance
|
||||
|
||||
## 6. Tests
|
||||
|
||||
- [x] 6.1 Unit: `loadPersona()` — unset env returns default; valid inline JSON parses to the persona; invalid JSON falls back + warns; valid JSON that violates the Zod schema falls back + warns
|
||||
- [x] 6.2 Unit: `loadPersona()` with `file:` — reads a real file via a temp path; unreadable file falls back + warns
|
||||
- [x] 6.3 Unit: `templateName`/`templateDescription` draw from the supplied persona's pool and never return entries outside it
|
||||
- [x] 6.4 Integration (`DEMO_BOT_INTEGRATION=1`): `ensureDemoUser` with the default persona is idempotent; clash with a pre-existing real user throws `DemoPersonaUsernameClashError`
|
||||
- [x] 6.5 E2E: no regression — `/users/bruno` still shows the 🐕 badge for the built-in Bruno persona (re-ran `e2e/demo-bot.test.ts` unchanged, both pass)
|
||||
|
||||
## 7. Rollout
|
||||
|
||||
- [x] 7.1 Merge + deploy — nothing changes because default persona equals current behaviour
|
||||
- [ ] 7.2 Validate on prod: `/users/bruno` unchanged, synthetic content cadence unchanged, metrics unchanged
|
||||
- [ ] 7.3 (Optional demo) On a staging or second instance, ship a non-Bruno persona via `DEMO_BOT_PERSONA=file:...` and confirm the demo user renders with the new identity + voice
|
||||
Loading…
Add table
Add a link
Reference in a new issue