backend-manager 5.2.18 → 5.3.0
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/CHANGELOG.md +35 -0
- package/CLAUDE.md +6 -4
- package/bin/backend-manager +10 -1
- package/docs/admin-post-route.md +2 -0
- package/docs/ai-library.md +29 -2
- package/docs/cli-output.md +122 -0
- package/docs/common-mistakes.md +1 -1
- package/docs/environment-detection.md +87 -3
- package/docs/marketing-campaigns.md +1 -1
- package/docs/payment-system.md +1 -1
- package/docs/testing.md +64 -17
- package/package.json +1 -1
- package/src/cli/commands/base-command.js +4 -0
- package/src/cli/commands/install.js +2 -2
- package/src/cli/commands/setup-tests/bem-config.js +22 -9
- package/src/cli/commands/setup-tests/helpers/merge-line-files.js +22 -168
- package/src/cli/commands/setup.js +65 -33
- package/src/cli/commands/test.js +1 -1
- package/src/cli/index.js +72 -39
- package/src/cli/utils/ui.js +270 -0
- package/src/defaults/CLAUDE.md +2 -2
- package/src/defaults/test/_init.js +14 -0
- package/src/manager/functions/_legacy/actions/create-post-handler.js +1 -1
- package/src/manager/functions/core/actions/api/admin/edit-post.js +1 -1
- package/src/manager/functions/core/actions/api/general/send-email.js +1 -1
- package/src/manager/functions/core/actions/api/user/delete.js +1 -1
- package/src/manager/functions/core/actions/api/user/oauth2.js +1 -1
- package/src/manager/helpers/analytics.js +3 -1
- package/src/manager/helpers/assistant.js +43 -38
- package/src/manager/index.js +76 -12
- package/src/manager/libraries/ai/index.js +33 -0
- package/src/manager/libraries/ai/providers/openai.js +105 -8
- package/src/manager/libraries/content/ghostii.js +76 -16
- package/src/manager/libraries/email/data/disposable-domains.json +24 -0
- package/src/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
- package/src/manager/libraries/email/generators/newsletter.js +16 -2
- package/src/manager/libraries/payment/discount-codes.js +1 -0
- package/src/manager/routes/admin/post/put.js +1 -1
- package/src/manager/routes/handler/post/post.js +1 -1
- package/src/manager/routes/payments/intent/processors/test.js +1 -1
- package/src/manager/routes/user/delete.js +1 -1
- package/src/manager/routes/user/signup/post.js +4 -2
- package/src/test/runner.js +142 -20
- package/src/test/test-accounts.js +159 -102
- package/src/utils/merge-line-files.js +16 -5
- package/templates/_.env +1 -0
- package/test/events/payments/journey-payments-cancel-endpoint.js +6 -2
- package/test/events/payments/journey-payments-cancel.js +6 -3
- package/test/events/payments/journey-payments-failure.js +6 -3
- package/test/events/payments/journey-payments-plan-change.js +6 -3
- package/test/events/payments/journey-payments-refund-webhook.js +6 -2
- package/test/events/payments/journey-payments-suspend.js +6 -3
- package/test/events/payments/journey-payments-trial-cancel.js +6 -3
- package/test/events/payments/journey-payments-trial.js +10 -4
- package/test/events/payments/journey-payments-uid-resolution.js +6 -2
- package/test/events/payments/journey-payments-upgrade.js +6 -3
- package/test/helpers/content/ghostii-blocks.js +134 -0
- package/test/helpers/environment.js +230 -0
- package/test/helpers/merge-line-files.js +271 -0
- package/test/routes/marketing/webhook-forward.js +14 -7
- package/test/routes/payments/cancel.js +4 -1
- package/test/routes/payments/dispute-alert.js +9 -6
- package/test/routes/payments/intent.js +55 -14
- package/test/routes/payments/portal.js +4 -1
- package/test/routes/payments/refund.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
# [5.3.0] - 2026-06-02
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`Manager.AI(assistant).image({ prompt, ... })`** — image generation via OpenAI's `gpt-image-2`. Separate method from `request()` (return type is bytes, not text; bypasses moderation/token-accounting/schema/prompt-normalization, none of which apply to image gen). Returns `{ buffer, b64, mime, revisedPrompt, model, size, quality, raw }` for a single image, or an array when `n > 1`. Options: `prompt` (required), `model` (default `gpt-image-2`), `size` (`1024x1024` default), `quality` (`medium` default), `background`, `n`, `timeout` (default 5min — image gen is slow). Only `openai` implements it. Lives on `OpenAI.prototype.image()` + dispatched via `AI.prototype.image()`. See [docs/ai-library.md](docs/ai-library.md).
|
|
21
|
+
- **AI tools passthrough (nested) — `ai.request({ tools: { list, choice } })`.** `tools.list` is an array of tool definitions passed to the OpenAI Responses API verbatim — built-in hosted tools (e.g. `{ type: 'web_search' }`) OR custom function tools (`{ type: 'function', name, parameters }`); `tools.choice` *(optional)* maps to `tool_choice`. Opt-in — omitted/empty means no tools and identical behavior to a plain request. Primary use is OpenAI's built-in **web search** so the model finds and cites real, currently-live URLs instead of hallucinating them. When tools are active the response `output` may carry tool-call items (e.g. `web_search_call`) + `url_citation` annotations; the message-text extractor ignores non-message items so `r.content` is unaffected. See [docs/ai-library.md](docs/ai-library.md).
|
|
22
|
+
- **`image-illustrator.js` — newsletter section illustrations now generated as flat-vector PNGs via `gpt-image-2` by default**, replacing the SVG-author-then-rasterize approach as the default. Clean flat 2D vector style (Stripe / Linear / undraw.co aesthetic) built from the brand palette (`content.theme.{primary,secondary,accent}Color`), white background, no text. The legacy `svg-illustrator.js` method is still available per-brand via `marketing.beehiiv.content.method.image = 'svg'`. Both return the same `{ png: Buffer, fallback, meta }` contract, so `image-host.js` / `uploadAssets` are unchanged. Validated end-to-end against live `gpt-image-2` with Somiibo's real config. See [docs/marketing-campaigns.md](docs/marketing-campaigns.md).
|
|
23
|
+
- **Full emulator-Firestore flush before every test run.** `deleteTestUsers()` now calls `flushEmulatorFirestore()` — `listCollections()` + `recursiveDelete()` on the entire emulator DB — before recreating test accounts. The emulator DB is 100% test data, so a full flush is the simplest correct clean slate; there are no per-collection allowlists to maintain. Guarded to run ONLY when `FIRESTORE_EMULATOR_HOST` is set, so it can never touch a real project.
|
|
24
|
+
- **`test/_init.js` pre-test lifecycle hook.** The test runner loads an optional `test/_init.js` from BOTH test roots (BEM core + consumer project) and runs it before any test (it is not run as a test itself). The module **must export a function** — `module.exports = (ctx) => ({ ... })` — called with `{ config, Manager }` and returning the hook object. It may declare `accounts` (array of extra test accounts `{ id, uid, email, properties }`, created/fetched/deleted on the same path as the built-ins so a project has a user per lifecycle) and `async setup({ admin, config, accounts, Manager, assistant })` (reseed fixtures into the freshly-flushed DB, after account creation). There is **no `cleanup` hook** — the whole DB is flushed each run and each test cleans up after itself. A default boilerplate `test/_init.js` now ships via `src/defaults/` (copied into consumers on first `npx mgr setup`, never overwriting an existing one). Mirrored across all four OMEGA frameworks. See `docs/testing.md`.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **Environment detection consolidated onto the Manager as SSOT.** `getEnvironment()` returns exactly one of `development | testing | production` (mutually exclusive, testing wins), read **live** from `process.env` on every call (no caching). `assistant.isDevelopment/isProduction/isTesting/getEnvironment` forward to the Manager. Fixes a bug where a cached environment made `getApiUrl()` resolve to the production URL inside the test runner.
|
|
28
|
+
- **Install-command parity.** `npx mgr install` accepts the unified alias set across all four frameworks — `dev|d|development|local|l` for the local source install and `live|prod|p|production` for the published version; docs advertise the canonical `dev` + `live`.
|
|
29
|
+
- **`copyDefaults` no longer skips `_`-prefixed filenames.** The archive-skip rule now only applies to `_`-prefixed *directory* segments (e.g. `_legacy/`); a `_`-prefixed *filename* like `test/_init.js` ships verbatim. The `_.env` / `_.gitignore` dotfile rename is unchanged.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- **Test-account creation race.** A late `auth:on-delete` could clobber the fresh `auth:on-create` doc, leaving accounts without `api.clientId`/`privateKey` and cascading into dozens of auth/payment test failures. `createAccount()` now verifies the API keys landed and repairs (recreate) if a stale delete clobbered the doc, making the suite deterministic.
|
|
33
|
+
- **`meta/stats` seed ordering.** `ensureMetaStats()` ran before the emulator flush (so it was wiped) and skipped when the doc already existed (so the notification on-write trigger could leave it without a `users` field). It now merges the `users` baseline AFTER the flush, fixing flaky admin-stats tests.
|
|
34
|
+
- **Flaky payment/dispute tests.** The trial journey now skips cleanly when no paid product has `trial.days > 0` (mirrors trial-cancel) instead of timing out; the dispute-alert dedup test seeds a deterministic in-flight doc instead of racing a live webhook to `failed`.
|
|
35
|
+
|
|
36
|
+
# [5.2.19] - 2026-05-29
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Shared CLI styling module (`src/cli/utils/ui.js`)** — the SSOT for BEM console output, matching the OMEGA Manager look: a `🚀` banner, 70-char `━` dividers, indented tree output, dimmed `Label:` fields, timestamps, a consistent set of status symbols (`→ ✓ ✗ ⊘ ⚠ ✅ + ↻`), and a `Summary` block (green `✅` / yellow `⚠`). Exposed on every command as `this.ui` (wired in `base-command.js`) and adoptable incrementally by `serve`/`deploy`/`test`/`emulator`. See `docs/cli-output.md`.
|
|
41
|
+
- **Regression test for the `.env`/`.gitignore` merge** (`test/helpers/merge-line-files.js`). Covers key-based alignment when key order drifts (the original scramble), Custom→Default promotion when the template adopts a key, Default→Custom migration of unknown keys, value preservation, quote normalization, idempotency, and that the setup-test helper is the same function as the canonical impl (SSOT guard).
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **`npx mgr setup` now renders in the OMEGA style.** The old `---- RUNNING SETUP ----` / `[1] name: passed` / bare missing-keys dump is replaced with a banner, a divider-wrapped project header (brand name + Firebase console URL + `Project`/`API` fields), and `[DEFAULTS]` / `[CHECKS]` / `[STATS]` sections. Each check prints `[N] ✓ name`, with `⚠ … — fixing…` → `✓ fixed` on auto-fix and `✗ Could not fix: …` on hard failure. The run ends with a `Summary` block (checks count, duration, passed/failed, and any failing checks with detail lines). The `bem-config` check lists the missing `backend-manager-config.json` keys as a bulleted list and surfaces a compact version in the summary.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **No more `UnhandledPromiseRejection` crash on setup failure.** Previously an unfixable check (e.g. missing config keys) rejected a promise with no reason, which bubbled out of the un-`catch`'d `bin/backend-manager` IIFE as Node's raw `UnhandledPromiseRejection: undefined` dump (exit non-zero, lost message). Setup now exits cleanly via `haltSetup()` → `process.exit(1)` after printing the styled summary, and `bin/backend-manager` wraps the run in a `try/catch` that prints a one-line `✗ <message>` backstop. The early-exit guards (missing `functions/package.json`, wrong directory) now print styled errors and exit `1` instead of `0`.
|
|
50
|
+
- **`.env` merge no longer scrambles keys under the wrong headers.** The `has correct .env file` / `has correct .gitignore` setup checks (`env-file.js`, `gitignore.js`) imported a SECOND, **positional** merge implementation in `src/cli/commands/setup-tests/helpers/merge-line-files.js` that zipped comment lines and value lines by index — so any drift between the consumer's key order and the template shifted every value down a slot (the last key of each group landed under the next group's header), dropped newly-added template keys, and duplicated keys. That duplicate is **deleted**; the helper is now a thin SSOT shim re-exporting the canonical key-based merge in `src/utils/merge-line-files.js`. The canonical merge also now **promotes** a key from the user's Custom section up into Default (with its value) when the framework template adopts that key, instead of emitting an empty Default line and leaving the value stranded in Custom.
|
|
51
|
+
|
|
17
52
|
# [5.2.17] - 2026-05-29
|
|
18
53
|
|
|
19
54
|
### Added
|
package/CLAUDE.md
CHANGED
|
@@ -34,7 +34,7 @@ All `npx mgr <cmd>` aliases work: `npx bm <cmd>`, `npx bem <cmd>`, `npx backend-
|
|
|
34
34
|
1. `npm install` — install BEM's own deps
|
|
35
35
|
2. `npm run prepare` — build once: copies `src/` → `dist/` via prepare-package
|
|
36
36
|
3. `npm run prepare:watch` — watch mode
|
|
37
|
-
4. Test in a consumer project: from inside the consumer, run `npx mgr install dev` to swap BEM to this local repo — required whenever you edit the framework source and want the consumer to pick up the changes (the consumer otherwise keeps its installed `node_modules/backend-manager`). Reverse with `npx mgr install
|
|
37
|
+
4. Test in a consumer project: from inside the consumer, run `npx mgr install dev` to swap BEM to this local repo — required whenever you edit the framework source and want the consumer to pick up the changes (the consumer otherwise keeps its installed `node_modules/backend-manager`). Reverse with `npx mgr install live`. If `npx mgr` then errors with "could not determine executable to run", the local install skipped bin-linking — re-run `npm install` to relink, or call `node node_modules/backend-manager/bin/backend-manager <cmd>` directly.
|
|
38
38
|
|
|
39
39
|
## Architecture
|
|
40
40
|
|
|
@@ -79,6 +79,7 @@ See [docs/cli-firestore-auth.md](docs/cli-firestore-auth.md) and [docs/cli-logs.
|
|
|
79
79
|
- **Routes receive whitespace-trimmed data; HTML is preserved.** Sanitize at the HTML-insertion site via `utilities.sanitize()`. Opt into middleware-level HTML strip per-route with `{ sanitize: true }`. See [docs/sanitization.md](docs/sanitization.md).
|
|
80
80
|
- **Match schema names to route names** — if route is `myEndpoint`, schema is `myEndpoint`.
|
|
81
81
|
- **Always use `assistant.respond()` for responses** — do NOT use `res.send()` directly.
|
|
82
|
+
- **Always use `Manager.getApiUrl()` for the API URL** — never read the cached `Manager.project.apiUrl` property. The getter is the SSOT and auto-resolves to the local emulator in dev AND test (and production otherwise), so it's safe everywhere without passing an env arg. See [docs/environment-detection.md](docs/environment-detection.md).
|
|
82
83
|
- **Add Firestore composite indexes** for any compound query (`where` + `orderBy`, or multiple `where`s) to `src/cli/commands/setup-tests/helpers/required-indexes.js` (the SSOT). Without the index, queries crash with `FAILED_PRECONDITION` in production.
|
|
83
84
|
|
|
84
85
|
See [docs/code-patterns.md](docs/code-patterns.md) for code-pattern detail, [docs/common-mistakes.md](docs/common-mistakes.md) for the full anti-pattern checklist, and [docs/file-naming.md](docs/file-naming.md) for the naming table (routes / schemas / API commands / events / cron jobs / hooks).
|
|
@@ -106,7 +107,8 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
|
|
|
106
107
|
- [docs/file-naming.md](docs/file-naming.md) — naming table for routes, schemas, API commands, events, cron jobs, hooks
|
|
107
108
|
- [docs/common-mistakes.md](docs/common-mistakes.md) — anti-pattern checklist (don't modify Manager internals, always await, increment-before-update, etc.)
|
|
108
109
|
- [docs/key-files.md](docs/key-files.md) — quick lookup for the most-touched files (Manager, helpers, auth events, cron, payment processors, CLI commands)
|
|
109
|
-
- [docs/
|
|
110
|
+
- [docs/cli-output.md](docs/cli-output.md) — shared CLI styling module (`src/cli/utils/ui.js`): OMEGA-style banner/dividers/sections/status symbols + the `Summary` block; used by `setup`, adoptable by other commands
|
|
111
|
+
- [docs/environment-detection.md](docs/environment-detection.md) — `getEnvironment()` returns `'development' | 'testing' | 'production'` (mutually exclusive); gate side effects on the INTENTIONAL check (`isProduction()` for prod-only, `isDevelopment() || isTesting()` for local-or-test) — never `!isDevelopment()`. Plus the URL helper convention (always `Manager.getApiUrl()` — auto-resolves local in dev+test, never read `project.apiUrl`)
|
|
110
112
|
- [docs/response-headers.md](docs/response-headers.md) — automatic `bm-properties` header
|
|
111
113
|
|
|
112
114
|
### Building Routes & Components
|
|
@@ -128,12 +130,12 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
|
|
|
128
130
|
### Subsystems & Libraries
|
|
129
131
|
|
|
130
132
|
- [docs/usage-rate-limiting.md](docs/usage-rate-limiting.md) — usage tracking, monthly/daily caps, `setUser()` + mirrors for proxy usage, reset schedule
|
|
131
|
-
- [docs/ai-library.md](docs/ai-library.md) — `Manager.AI()` unified entry for OpenAI + Anthropic
|
|
133
|
+
- [docs/ai-library.md](docs/ai-library.md) — `Manager.AI()` unified entry for OpenAI + Anthropic (text via `.request()`, images via `.image()` → `gpt-image-2`)
|
|
132
134
|
- [docs/marketing-fields.md](docs/marketing-fields.md) — adding custom fields to SendGrid + Beehiiv via the BEM/OMEGA SSOT pair
|
|
133
135
|
- [docs/stripe-webhook-forwarding.md](docs/stripe-webhook-forwarding.md) — auto-started Stripe CLI forwarding for local dev
|
|
134
136
|
|
|
135
137
|
### Testing & CLI
|
|
136
138
|
|
|
137
|
-
- [docs/testing.md](docs/testing.md) — running, filtering, log files, test types (standalone/suite/group), context object, assertions, auth levels. **All cleanup runs at the START of every run, never at the end
|
|
139
|
+
- [docs/testing.md](docs/testing.md) — running, filtering, log files, test types (standalone/suite/group), context object, assertions, auth levels. **NEVER mock — test against the real emulator.** No `mockManager`/`mockAdmin`/fake `firestore`/stubbed `assistant`; every `run()` gets the real `Manager`/`assistant`/`firestore`/`http`/`accounts` — use them. Pure functions (zero I/O) are the only thing you call directly; anything touching Firestore or an external API runs for real. Real external APIs (OpenAI/PayPal/GitHub/SendGrid/Stripe) are gated behind `TEST_EXTENDED_MODE` in-source (not mocked), and anything an extended test creates externally must be cleaned up by the test. **Each test file `module.exports` a `{ description, type, tests }` object — NOT raw Mocha (`describe`/`it`/`beforeEach`); those globals are not injected and the file fails to load. Split tests one-file-per-concern under `test/<area>/`, never one giant `test/test.js`.** **All cleanup runs at the START of every run, never at the end** — the runner flushes the ENTIRE emulator Firestore before every run, so there's nothing to register; seed any needed fixtures in `test/_init.js`'s `setup()`, and never add a trailing cleanup step. Marketing providers (SendGrid/Beehiiv) don't need a special exception — `_test.*` emails are blocked at the validation layer so test signups never reach providers. The `_test.allow_*` carve-out exists only for the live-provider lifecycle test (`test/marketing/consent-lifecycle.js`), which manages its own teardown.
|
|
138
140
|
- [docs/cli-firestore-auth.md](docs/cli-firestore-auth.md) — `npx mgr firestore:*` and `auth:*` commands, shared flags, examples
|
|
139
141
|
- [docs/cli-logs.md](docs/cli-logs.md) — `npx mgr logs:read` / `logs:tail` with full flag reference and built-in Cloud Function names
|
package/bin/backend-manager
CHANGED
|
@@ -2,5 +2,14 @@
|
|
|
2
2
|
let Main = new (require('../src/cli/index.js'))(process.argv);
|
|
3
3
|
(async function() {
|
|
4
4
|
'use strict';
|
|
5
|
-
|
|
5
|
+
try {
|
|
6
|
+
await Main.process(process.argv);
|
|
7
|
+
} catch (e) {
|
|
8
|
+
// Print a clean one-line error instead of Node's raw UnhandledPromiseRejection
|
|
9
|
+
// dump. Commands that intend a hard stop should `process.exit(1)` themselves
|
|
10
|
+
// (e.g. setup's haltSetup); this is the catch-all backstop.
|
|
11
|
+
const chalk = require('chalk').default;
|
|
12
|
+
console.error(chalk.red(`\n✗ ${e && e.message ? e.message : e}`));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
6
15
|
}());
|
package/docs/admin-post-route.md
CHANGED
|
@@ -4,6 +4,8 @@ The `POST /admin/post` route creates blog posts via GitHub's API. It handles ima
|
|
|
4
4
|
|
|
5
5
|
**Consumers.** Besides direct admin/blogger HTTP calls, this route is the publish target for the **Ghostii article engine** (`src/manager/libraries/content/ghostii.js` → `publishArticle()`), which is used by two paths: the standalone daily `ghostii-auto-publisher.js` cron (off by default), and the newsletter generator's linked-article flow (`marketing.beehiiv.content.article.enabled`). Both POST `title`/`url`/`description`/`headerImageURL`/`body`/`author`/`categories`/`tags`/`postPath`/`githubUser`/`githubRepo` with a `backendManagerKey`. Note: `headerImageURL` MUST resolve to a `.jpg` (the downloader rejects non-`.jpg` headers); Ghostii's `unsplash` hero satisfies this. See [docs/marketing-campaigns.md](marketing-campaigns.md).
|
|
6
6
|
|
|
7
|
+
**Ghostii is unopinionated about BEM.** Its `/write/article` response is a generic article — a `json` block array (`[{ name, content }]` where name ∈ `heading-1..6`/`image`/`paragraph`/`blockquote`/`list`) plus top-level `title`/`description`/`headerImageUrl`/`images`/`categories`/`keywords`. BEM owns the transform into this route's shape: `blocksToPost(article.json)` extracts the `heading-1` as the title, the first `image` block as `headerImageURL`, and joins every remaining block as the `body` (content only — NO title, NO header image embedded, since this route adds those itself; section images stay in the body and are extracted normally). Older Ghostii responses without `json` fall back to the flat `article.{title,body,headerImageUrl}` fields.
|
|
8
|
+
|
|
7
9
|
## Image Processing Flow
|
|
8
10
|
|
|
9
11
|
1. Receives markdown body with external image URLs (e.g., ``)
|
package/docs/ai-library.md
CHANGED
|
@@ -14,16 +14,43 @@ Return shape (same for all providers): `{ content, output, tokens, raw }`.
|
|
|
14
14
|
|
|
15
15
|
API keys: `BACKEND_MANAGER_OPENAI_API_KEY`, `BACKEND_MANAGER_ANTHROPIC_API_KEY` (process.env or config).
|
|
16
16
|
|
|
17
|
+
## Image generation (OpenAI)
|
|
18
|
+
|
|
19
|
+
`Manager.AI(assistant).image({ prompt, ... })` generates an image via OpenAI's image model (`gpt-image-2` by default). Separate from `request()` because the return type is bytes, not text — it bypasses moderation, token accounting, schema, and prompt-normalization (none apply to image gen).
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const ai = Manager.AI(assistant);
|
|
23
|
+
const { buffer, b64, mime, revisedPrompt } = await ai.image({
|
|
24
|
+
prompt: 'Minimal flat vector illustration of a rocket, undraw.co style, blue + white, no text.',
|
|
25
|
+
size: '1024x1024', // 1024x1024 | 1536x1024 | 1024x1536 | auto (default 1024x1024)
|
|
26
|
+
quality: 'medium', // low | medium | high | auto (default medium)
|
|
27
|
+
background: 'opaque', // transparent | opaque | auto (opt-in)
|
|
28
|
+
n: 1, // default 1; n > 1 returns an array
|
|
29
|
+
});
|
|
30
|
+
// buffer is a PNG Buffer; b64 is the same data base64-encoded.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Return shape (single image): `{ buffer, b64, mime, revisedPrompt, model, size, quality, raw }`. With `n > 1` it returns an array of those.
|
|
34
|
+
|
|
35
|
+
`gpt-image-2` always returns base64 (no URL round-trip). Generation is slow — `medium`/`1024²` ≈ 40-50s; the default request timeout is 5 minutes. Only `openai` implements `image()`; calling it on another provider throws.
|
|
36
|
+
|
|
37
|
+
API key resolution is the same as `request()` — `BACKEND_MANAGER_OPENAI_API_KEY` / `OPENAI_API_KEY` (process.env or config).
|
|
38
|
+
|
|
17
39
|
## Tools / web search (OpenAI)
|
|
18
40
|
|
|
19
|
-
|
|
41
|
+
Tools are nested under `options.tools` and opt-in — when omitted, no tools are sent and behavior is identical to a plain request:
|
|
42
|
+
|
|
43
|
+
- `tools.list` — array of tool definitions passed to the OpenAI Responses API verbatim. Built-in hosted tools (e.g. `{ type: 'web_search' }`, `{ type: 'code_interpreter' }`) OR custom function tools (`{ type: 'function', name, parameters }`).
|
|
44
|
+
- `tools.choice` *(optional)* — maps to `tool_choice` (`'auto'` | `'required'` | `'none'`, or a specific tool). Omit to let OpenAI default to `auto`.
|
|
45
|
+
|
|
46
|
+
The most common use is OpenAI's built-in **web search** so the model finds and cites real, currently-live URLs instead of hallucinating them:
|
|
20
47
|
|
|
21
48
|
```js
|
|
22
49
|
const r = await ai.request({
|
|
23
50
|
model: 'gpt-5.4',
|
|
24
51
|
response: 'json',
|
|
25
52
|
reasoning: { effort: 'medium' },
|
|
26
|
-
tools: [{ type: 'web_search' }],
|
|
53
|
+
tools: { list: [{ type: 'web_search' }] },
|
|
27
54
|
prompt: { path: '.../research/system.md', settings },
|
|
28
55
|
message: { path: '.../research/user.md', settings },
|
|
29
56
|
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# CLI Output Styling (`src/cli/utils/ui.js`)
|
|
2
|
+
|
|
3
|
+
BEM's CLI shares a single styling module so every command renders with the same
|
|
4
|
+
look as the **OMEGA Manager** (`omega-manager`): `🚀` banner, 70-char `━`
|
|
5
|
+
dividers, indented tree output, dimmed labels, timestamps, and a consistent set
|
|
6
|
+
of status symbols. This is the **SSOT for console output** — commands should pull
|
|
7
|
+
helpers from here instead of hand-rolling `chalk` + `console.log`.
|
|
8
|
+
|
|
9
|
+
The module is exposed on every command as `this.ui` (wired in
|
|
10
|
+
[`src/cli/commands/base-command.js`](../src/cli/commands/base-command.js)) and can
|
|
11
|
+
also be `require('../utils/ui')`'d directly (e.g. from setup-test files).
|
|
12
|
+
|
|
13
|
+
## Conventions
|
|
14
|
+
|
|
15
|
+
| Aspect | Value |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Divider | `━` × 70 (`ui.RULE_WIDTH`, `ui.RULE_CHAR`) |
|
|
18
|
+
| Indentation | 2 spaces per level (`ui.indent(level)`) |
|
|
19
|
+
| Section label | `[UPPERCASE]` in bold magenta, at indent level 1 |
|
|
20
|
+
| Section items | one level deeper than the label (level 2) |
|
|
21
|
+
| Timestamp | `new Date().toLocaleTimeString()` (e.g. `7:47:12 PM`) |
|
|
22
|
+
| Banner | bold cyan, prefixed with `🚀` |
|
|
23
|
+
|
|
24
|
+
### Status symbols (`ui.SYMBOLS`)
|
|
25
|
+
|
|
26
|
+
| Key | Symbol | Color | Meaning |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| `running` | `→` | dim | step in progress / hint |
|
|
29
|
+
| `pass` | `✓` | green | success |
|
|
30
|
+
| `fail` | `✗` | red | failure |
|
|
31
|
+
| `skip` | `⊘` | dim | skipped / no-op |
|
|
32
|
+
| `warn` | `⚠` | yellow | warning |
|
|
33
|
+
| `done` | `✅` | green | final success (summary) |
|
|
34
|
+
| `add` | `+` | green | created a file/record |
|
|
35
|
+
| `change` | `↻` | yellow | modified a file/record |
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
const ui = require('../utils/ui'); // or this.ui inside a command
|
|
41
|
+
|
|
42
|
+
ui.banner('Backend Manager v5.2.18'); // 🚀 bold-cyan banner + blank lines
|
|
43
|
+
ui.header('Somiibo', { subtitle: url }); // ━ divider / title @ time / ━ divider
|
|
44
|
+
ui.section('Checks'); // blank line + bold-magenta [CHECKS]
|
|
45
|
+
ui.field('Project', 'somiibo-91d13', { pad: 9 });// dimmed "Label:" + value (pad aligns columns)
|
|
46
|
+
ui.status('pass', 'Stats fetched', { level: 2 });// <symbol> <text> at an indent level
|
|
47
|
+
ui.note('All defaults up to date', 2); // dimmed line at a level
|
|
48
|
+
ui.blank(); // blank line
|
|
49
|
+
ui.rule(); // a bare 70-char rule string (cyan)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`ui.header(title, opts)`:
|
|
53
|
+
- `opts.subtitle` — text after the title (default cyan), e.g. a URL.
|
|
54
|
+
- `opts.subtitleColor` — chalk fn for the subtitle.
|
|
55
|
+
- `opts.time` — append `@ <time>` (default `true`).
|
|
56
|
+
- `opts.color` — chalk fn for the rules (default cyan).
|
|
57
|
+
|
|
58
|
+
`ui.field(label, value, opts)`:
|
|
59
|
+
- `opts.level` — indent level (default 1).
|
|
60
|
+
- `opts.pad` — pad the label (incl. colon) to this width for column alignment.
|
|
61
|
+
- `opts.valueColor` — chalk fn for the value.
|
|
62
|
+
|
|
63
|
+
`ui.status(kind, text, opts)`:
|
|
64
|
+
- `kind` — one of `running|pass|fail|skip|warn|add|change` (picks symbol + color).
|
|
65
|
+
- `opts.level` — indent level (default 1).
|
|
66
|
+
- `opts.detail` — dimmed trailing detail string.
|
|
67
|
+
|
|
68
|
+
### `ui.Summary`
|
|
69
|
+
|
|
70
|
+
Collects pass/fail outcomes and prints an OMEGA-style summary block (green `✅`
|
|
71
|
+
when all passed, yellow `⚠` otherwise).
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const summary = new ui.Summary().start();
|
|
75
|
+
summary.pass(); // record a pass
|
|
76
|
+
summary.fail('check name', detailsArr);// record a fail with pre-formatted detail lines
|
|
77
|
+
summary.print({ hint: 'Fix the above, then run npx mgr setup again.' });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`fail()`'s second arg is an array of already-styled lines shown indented under the
|
|
81
|
+
failing check in the summary block.
|
|
82
|
+
|
|
83
|
+
## How `setup` uses it
|
|
84
|
+
|
|
85
|
+
[`src/cli/commands/setup.js`](../src/cli/commands/setup.js) builds the whole run
|
|
86
|
+
from these helpers:
|
|
87
|
+
|
|
88
|
+
1. `ui.banner(...)` → `ui.header(brand, { subtitle: consoleUrl })` → `ui.field('Project'/'API', ...)`.
|
|
89
|
+
2. `ui.section('Defaults')` then `ui.status('add'|'change', ...)` per scaffolded file.
|
|
90
|
+
3. `ui.section('Checks')` then the per-check status lines (printed by the test
|
|
91
|
+
runner — see below), with `✓ fixed` / `✗ Could not fix` sub-lines.
|
|
92
|
+
4. `ui.section('Stats')` then a pass/skip/warn line.
|
|
93
|
+
5. `self.setupSummary.print()` on success.
|
|
94
|
+
|
|
95
|
+
### Test runner (`Main.prototype.test` in `src/cli/index.js`)
|
|
96
|
+
|
|
97
|
+
Each setup check prints ` [N] <symbol> <name>`. A check can:
|
|
98
|
+
- **pass** → `✓` (recorded via `setupSummary.pass()`).
|
|
99
|
+
- **fail then auto-fix** → `⚠ … — fixing…` then `✓ fixed`.
|
|
100
|
+
- **fail unfixably** → `✗ Could not fix: <message>`, recorded via
|
|
101
|
+
`setupSummary.fail(name, details)`, then `haltSetup()` prints the summary and
|
|
102
|
+
`process.exit(1)`.
|
|
103
|
+
|
|
104
|
+
A failing check's `fix()` may attach `error.summaryDetails` (an array of styled
|
|
105
|
+
lines) to surface a compact version in the summary block — see
|
|
106
|
+
[`setup-tests/bem-config.js`](../src/cli/commands/setup-tests/bem-config.js),
|
|
107
|
+
which lists the missing `backend-manager-config.json` keys.
|
|
108
|
+
|
|
109
|
+
`--continue` records the failure but keeps going instead of halting.
|
|
110
|
+
|
|
111
|
+
> **No more `UnhandledPromiseRejection`.** Hard failures exit cleanly via
|
|
112
|
+
> `haltSetup()` / `process.exit(1)`, and `bin/backend-manager` wraps the run in a
|
|
113
|
+
> `try/catch` that prints a one-line `✗ <message>` instead of Node's raw rejection
|
|
114
|
+
> dump.
|
|
115
|
+
|
|
116
|
+
## Adopting it in other commands
|
|
117
|
+
|
|
118
|
+
`serve`, `deploy`, `test`, `emulator`, etc. can migrate to the same look
|
|
119
|
+
incrementally: replace ad-hoc `console.log(chalk...)` with `this.ui.section(...)`,
|
|
120
|
+
`this.ui.status(...)`, and `this.ui.field(...)`. The legacy
|
|
121
|
+
`log/logError/logSuccess/logWarning` helpers on `BaseCommand` still work for
|
|
122
|
+
simple one-off lines.
|
package/docs/common-mistakes.md
CHANGED
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
7. **Use short-circuit returns** — Return early from error conditions
|
|
10
10
|
8. **Increment usage before update** — Call `usage.increment()` then `usage.update()`
|
|
11
11
|
9. **Add Firestore composite indexes for new compound queries** — Any new Firestore query using multiple `.where()` clauses or `.where()` + `.orderBy()` requires a composite index. Add it to `src/cli/commands/setup-tests/helpers/required-indexes.js` (the SSOT). Consumer projects pick these up via `npx mgr setup`, which syncs them into `firestore.indexes.json`. Without the index, the query will crash with `FAILED_PRECONDITION` in production.
|
|
12
|
-
10. **Don't put test data cleanup at the END of a test** — End-of-test cleanup doesn't fire when the previous run was killed mid-execution, so the next run inherits stale state. ALL test-data cleanup belongs in the runner's pre-test phase ([../src/test/test-accounts.js](../src/test/test-accounts.js) `deleteTestUsers()` + [../src/test/runner.js](../src/test/runner.js) `setupAccounts()`)
|
|
12
|
+
10. **Don't put test data cleanup at the END of a test** — End-of-test cleanup doesn't fire when the previous run was killed mid-execution, so the next run inherits stale state. ALL test-data cleanup belongs in the runner's pre-test phase ([../src/test/test-accounts.js](../src/test/test-accounts.js) `deleteTestUsers()` + [../src/test/runner.js](../src/test/runner.js) `setupAccounts()`), which **flushes the entire emulator Firestore before every run** — so there's nothing to register when you add a test that writes data. Seed any required fixtures in `test/_init.js`'s `setup()` (runs after the flush). The only acceptable trailing cleanup is within-run state isolation (one test removes a doc so the NEXT test in the same run sees a clean slate) — that's not preparing the next run, it's intra-run housekeeping. See [testing.md](testing.md) "Test Data Cleanup".
|
|
@@ -1,7 +1,91 @@
|
|
|
1
1
|
# Environment Detection
|
|
2
2
|
|
|
3
|
+
`getEnvironment()` returns exactly ONE of three mutually-exclusive, exhaustive values:
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
Manager.getEnvironment() // 'development' | 'testing' | 'production'
|
|
7
|
+
|
|
8
|
+
Manager.isDevelopment() // true ONLY in development
|
|
9
|
+
Manager.isTesting() // true ONLY in testing
|
|
10
|
+
Manager.isProduction() // true ONLY in production
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**The Manager is the single source of truth.** `getEnvironment()` is the ONLY function that reads the raw signals (`BEM_TESTING` / `ENVIRONMENT` / `FUNCTIONS_EMULATOR` / `TERM_PROGRAM`). The three `is*()` checks **derive** from it live on every call — they never read raw signals themselves, so they can never disagree with `getEnvironment()`.
|
|
14
|
+
|
|
15
|
+
**The assistant forwards to the Manager.** Request handlers receive an `assistant`, so the same methods are exposed there and return identical results — call whichever is in scope:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
assistant.getEnvironment() // === Manager.getEnvironment() — a thin forward
|
|
19
|
+
assistant.isTesting() // === Manager.isTesting()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
(An assistant always has a Manager — `init()` throws without one. The `assistant.meta.environment` field is still populated for code that reads it, but the `is*()` checks no longer depend on that snapshot.)
|
|
23
|
+
|
|
24
|
+
**Resolution order:** testing wins first, then production, else development. The three checks are mutually exclusive — exactly one is true. `isDevelopment()` is **false** during testing, and `isProduction()` is a real positive check (it is NOT `!isDevelopment()`).
|
|
25
|
+
|
|
26
|
+
## Available helpers
|
|
27
|
+
|
|
28
|
+
| Helper | Returns |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `getEnvironment()` | `'development' \| 'testing' \| 'production'` — the SSOT resolver; the only reader of raw signals. |
|
|
31
|
+
| `isDevelopment()` | `true` ONLY in development (local Firebase emulator / dev), and NOT testing. Derives from `getEnvironment()`. |
|
|
32
|
+
| `isTesting()` | `true` ONLY in testing (`BEM_TESTING === 'true'`). **Takes precedence** — a test run is not development. |
|
|
33
|
+
| `isProduction()` | `true` ONLY in production (deployed Cloud Functions). A **real positive check** — NOT `!isDevelopment()`. |
|
|
34
|
+
|
|
35
|
+
## Gating side effects — use the INTENTIONAL check
|
|
36
|
+
|
|
37
|
+
Because there are three environments, never gate a side effect on a two-value assumption. State what you mean:
|
|
38
|
+
|
|
3
39
|
```javascript
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
40
|
+
// Production-only (skip real emails/analytics/Sentry/webhooks in dev AND testing):
|
|
41
|
+
if (isProduction()) { /* do the real thing */ }
|
|
42
|
+
if (!isProduction()) { /* skip / use the safe local behavior */ }
|
|
43
|
+
|
|
44
|
+
// Local-or-test (anything that should run in BOTH dev and testing):
|
|
45
|
+
if (isDevelopment() || isTesting()) { /* localhost URL, console logging, etc. */ }
|
|
7
46
|
```
|
|
47
|
+
|
|
48
|
+
**Avoid** `if (!isDevelopment())` or `if (env !== 'development')` to gate production behavior — those wrongly include `testing` as production and leak real side effects (emails, analytics, Sentry) during test runs. This is the bug class that motivated the 3-value model.
|
|
49
|
+
|
|
50
|
+
## URL helpers
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
Manager.getApiUrl() // this brand's API URL — the SSOT for calling the BEM API
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**`Manager.getApiUrl()` is the one and only way to get the API URL.** It resolves to the **local** hosting emulator (`http://localhost:5002`) in development OR testing, and to production (`https://api.{domain}`) otherwise. Always call `getApiUrl()` directly — do NOT read the cached `Manager.project.apiUrl` property (it's a boot-time snapshot kept only for internal env-var export; the getter is the SSOT and always fresh). Build full endpoints by appending the path: `` `${Manager.getApiUrl()}/backend-manager/admin/post` ``.
|
|
57
|
+
|
|
58
|
+
Resolving local in test mode is required because tests hit the local emulator — without it, internal BEM→BEM calls (and tests calling `getApiUrl()`) would leak to the live production server. Pass an explicit `env` arg (`getApiUrl('production')`) only to force a specific environment regardless of the current one — rarely needed, and mainly used by tests to pin a specific environment's mapping.
|
|
59
|
+
|
|
60
|
+
> `getFunctionsUrl()` (raw Cloud Functions URL) exists for the ONE internal case that must name a specific deployed function by its raw address (`assistant.tryUrl()`). Application/route code should never need it — use `getApiUrl()`.
|
|
61
|
+
|
|
62
|
+
**Exception — parent helpers stay live:** `Manager.getParentApiUrl()` / `getParentUrl()` ALWAYS return the live production URL, even in dev/test. The parent BEM is a real remote server with no localhost equivalent, so cross-brand parent calls are never redirected to localhost.
|
|
63
|
+
|
|
64
|
+
## Where they live
|
|
65
|
+
|
|
66
|
+
Source: [src/manager/index.js](../src/manager/index.js). BEM has a single Manager (no multi-context mixin like EM/UJM/BXM), so `getEnvironment()` + `is*()` + the URL helpers live directly on the Manager. The `assistant` exposes the same methods and forwards each to its Manager (`assistant.isTesting()` → `Manager.isTesting()`), so request handlers can call whichever object is in scope.
|
|
67
|
+
|
|
68
|
+
## How detection works
|
|
69
|
+
|
|
70
|
+
`getEnvironment()` resolves in this precedence order:
|
|
71
|
+
|
|
72
|
+
1. **Testing** — `process.env.BEM_TESTING === 'true'` (set by the test runner / emulator). A test run is a test run regardless of any other signal.
|
|
73
|
+
2. **Production** — `process.env.ENVIRONMENT === 'production'`.
|
|
74
|
+
3. **Development** — `process.env.ENVIRONMENT === 'development'`, or `FUNCTIONS_EMULATOR` is set, or `TERM_PROGRAM` is `Apple_Terminal` / `vscode` (running locally).
|
|
75
|
+
4. **Default** — production. BEM's deployed *runtime* can legitimately lack a dev signal (a live Cloud Function has no `FUNCTIONS_EMULATOR`), so "no signal" IS the normal production state. (Contrast UJM/BXM, whose deployed artifacts always carry their signal baked in, so they default to **development** — a bare context there is just build tooling. EM defaults to production for the same reason as BEM.)
|
|
76
|
+
|
|
77
|
+
## Adding a new helper
|
|
78
|
+
|
|
79
|
+
If you need a new environment-derived helper, add it next to the others on the Manager in [src/manager/index.js](../src/manager/index.js), and forward it from the assistant if request handlers need it. Don't read `process.env` ad-hoc elsewhere — derive from `getEnvironment()` so there is one source of truth and no chance of drift.
|
|
80
|
+
|
|
81
|
+
## Why this matters
|
|
82
|
+
|
|
83
|
+
**One signal, used everywhere.** The test runner sets `BEM_TESTING=true`; every piece of code that calls `isTesting()` (framework or consumer) then sees `true` — no need to invent a per-module env var.
|
|
84
|
+
|
|
85
|
+
**Sub-modules check the same signal.** When framework code (an analytics flush, a webhook fan-out) needs to skip side effects in tests, it checks `isTesting()` — the same answer the consumer's own code gets. No drift.
|
|
86
|
+
|
|
87
|
+
**`is*()` can never disagree with `getEnvironment()`.** Because the checks derive from the single resolver instead of reading raw signals, there is exactly one definition of "what environment is this," and a wrong-but-confident gate (leaking real emails during a test run) is structurally impossible.
|
|
88
|
+
|
|
89
|
+
## See also
|
|
90
|
+
|
|
91
|
+
- [testing.md](testing.md) — `BEM_TESTING` is set automatically by the test runner; `TEST_EXTENDED_MODE` gates real external APIs.
|
|
@@ -96,7 +96,7 @@ Campaigns reference segments by SSOT key: `segments: ['subscription_free']`. Aut
|
|
|
96
96
|
Pipeline:
|
|
97
97
|
1. Fetch sources: `GET {parentUrl}/newsletter/sources?category=X&claimFor=brandId` (atomic claim)
|
|
98
98
|
2. **structure.js** — Generic dispatcher. Resolves the active template, merges `BASE_SCHEMA` (universal fields: subject, preheader, signoff, citations) with the template's own `schema` fragment, calls the template's `buildPrompt({brand, newsletterConfig, sources})` to get the AI brief, runs the AI call, and normalizes the result via the template's optional `normalize()`. Default provider: `openai` (override per-run only via `NEWSLETTER_PROVIDER_STRUCTURE` env).
|
|
99
|
-
3. **
|
|
99
|
+
3. **image-illustrator.js** (default) — One flat-vector PNG per section in parallel (`Promise.all`), generated directly via `Manager.AI(assistant).image()` → `gpt-image-2`. Iterates `structure.sections` — templates whose content shape isn't section-based (e.g. field-report uses `dispatches`) populate `sections` in their `normalize()` step so this loop keeps working unchanged. The prompt enforces a clean flat 2D vector style (Stripe / Linear / undraw.co aesthetic) built from the brand palette (`content.theme.{primary,secondary,accent}Color`), on a white background, no text. **Legacy method:** set `marketing.beehiiv.content.method.image = 'svg'` to use the older `svg-illustrator.js` (AI authors an `<svg>`, rasterized via `@resvg/resvg-js`). Both methods return the same `{ png: Buffer, fallback, meta }` contract.
|
|
100
100
|
4. **mjml-template.js** — Resolves the template by name from `templates/index.js`, calls `template.build({structure, imagePaths, theme, ...})` for the MJML, compiles to email-safe HTML via the `mjml` package. Brand-domain links get UTM-tagged via the existing `tagLinks()` utility.
|
|
101
101
|
5. Mark used: `PUT {parentUrl}/newsletter/sources` per source
|
|
102
102
|
|
package/docs/payment-system.md
CHANGED
|
@@ -180,7 +180,7 @@ module.exports = async function ({ before, after, uid, userDoc, admin, assistant
|
|
|
180
180
|
|
|
181
181
|
1. Add detection logic in `transitions/index.js` (in priority order)
|
|
182
182
|
2. Create handler file in `transitions/{category}/{name}.js`
|
|
183
|
-
3. Handler receives full context — use `assistant.log()` for logging, `Manager.
|
|
183
|
+
3. Handler receives full context — use `assistant.log()` for logging, `Manager.getApiUrl()` for API calls
|
|
184
184
|
|
|
185
185
|
## Processor Interface
|
|
186
186
|
|
package/docs/testing.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
+
## 🚫 NEVER mock — test against the real emulator (HARD RULE)
|
|
4
|
+
|
|
5
|
+
Tests run against a **real Firebase emulator** (real Firestore/Auth). **Do NOT hand-roll fake/stub/mock objects** — no `mockManager`, `mockAdmin`, `makeManager()`, fake `firestore()`/`admin`, stubbed `assistant`, or fake HTTP. Every test `run()` receives the **real** booted `Manager`, `assistant`, `firestore`, `http`, and `accounts` (see [the test context](#test-context)). Use them.
|
|
6
|
+
|
|
7
|
+
- Call routes over `http.as(...)`; call handlers/helpers with the real `Manager`/`assistant` from context; read/write/verify with the real `firestore` helper.
|
|
8
|
+
- **Pure functions are the only exception** — a function with zero I/O can be `require()`d and called with plain inputs (nothing to mock). The instant it touches Firestore or any external system, it must run for real against the emulator.
|
|
9
|
+
- **Real external APIs (OpenAI, PayPal, GitHub, SendGrid, Beehiiv, Stripe) are gated behind `TEST_EXTENDED_MODE` in the source, NOT mocked** — see [Extended Mode](#extended-mode-test_extended_mode). Normal mode skips them; extended mode runs them for real.
|
|
10
|
+
- **Anything an extended test creates in an external system must be cleaned up by the test** (delete the GitHub file, cancel the PayPal invoice, etc.) — the runner's pre-test wipe only covers local Firestore/Auth.
|
|
11
|
+
|
|
12
|
+
If you're writing `const mockX = {...}` to satisfy a function under test, STOP and pass the real context object instead.
|
|
13
|
+
|
|
14
|
+
### The ONLY two exceptions where a stub is allowed
|
|
15
|
+
|
|
16
|
+
Mock **nothing** by default. There are exactly two narrow cases where the real dependency genuinely cannot run in the test environment — and even then, stub the *smallest possible seam*, never a whole `Manager`/`assistant`:
|
|
17
|
+
|
|
18
|
+
1. **A side effect that would destroy the test run itself.** If invoking the real method would kill or corrupt the harness — e.g. a process-exit, an `app.quit()`, a destructive filesystem wipe, a recursive re-invocation of the test/build command — you may stub *that one call* to a no-op, assert the surrounding logic, then restore it. You are not faking behavior; you are preventing the harness from terminating mid-assertion.
|
|
19
|
+
2. **Cross-project fan-out that needs infrastructure you can't run locally.** Some routes fan out to *other* BEM backends (parent → child brand servers). Only **one** BEM emulator runs locally, so the real cross-project call has no second backend to hit. A unit test may hand-roll the minimal inputs (`makeManager`/`makeAdminMock`/mocked `wonderful-fetch`) to exercise the *fan-out logic* in isolation — but a companion integration test MUST still verify the real route's gate/wiring against the emulator. (Example: `test/helpers/webhook-forward.js`.)
|
|
20
|
+
|
|
21
|
+
**Rules for both exceptions:** stub the narrowest seam (one method / one module), restore it immediately, and add a comment stating *why the real thing can't run here*. If you can run it for real, you must.
|
|
22
|
+
|
|
3
23
|
## Running Tests
|
|
4
24
|
|
|
5
25
|
```bash
|
|
@@ -18,12 +38,11 @@ npx mgr test
|
|
|
18
38
|
What the runner wipes pre-test (in [src/test/test-accounts.js](../src/test/test-accounts.js) `deleteTestUsers()` and [src/test/runner.js](../src/test/runner.js) `setupAccounts()`):
|
|
19
39
|
|
|
20
40
|
1. **`meta/stats`** doc ensured (required for on-create batch writes).
|
|
21
|
-
2.
|
|
22
|
-
3. **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
5. **Realtime Database** — the `_test` namespace removed in full (`admin.database().ref('_test').remove()`).
|
|
41
|
+
2. **The ENTIRE emulator Firestore** — every top-level collection, flushed recursively (`flushEmulatorFirestore()` → `listCollections()` + `recursiveDelete()`). The emulator DB is 100% test data, so a full flush is the simplest correct clean slate — no per-collection allowlist to maintain. **SAFETY: it only runs when `FIRESTORE_EMULATOR_HOST` is set** (which the test command always sets); if absent it is a no-op, so it can never wipe a real project.
|
|
42
|
+
3. **Firebase Auth test users** — all `TEST_ACCOUNTS` uids deleted (Auth is a separate store from Firestore, so this still runs explicitly).
|
|
43
|
+
4. **Realtime Database** — the `_test` namespace removed in full (`admin.database().ref('_test').remove()`, guarded — RTDB is optional).
|
|
44
|
+
|
|
45
|
+
After the flush, `test/_init.js`'s `setup()` reseeds fixtures into the empty DB.
|
|
27
46
|
|
|
28
47
|
### Marketing-provider cleanup
|
|
29
48
|
|
|
@@ -35,18 +54,44 @@ All cleanup follows the start-only rule. No trailing-cleanup exception.
|
|
|
35
54
|
|
|
36
55
|
### When adding a new test that writes data
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
|---|---|
|
|
40
|
-
| A new Firestore collection (mixed test + prod data) | Add the collection name to `testDataCollections` in `src/test/test-accounts.js`. |
|
|
41
|
-
| A new Firestore collection (test-only data) | Add it to `testOnlyCollections` in `src/test/test-accounts.js`. |
|
|
42
|
-
| Realtime Database | Use a path under `_test/...` — already wiped pre-test. |
|
|
43
|
-
| Anywhere else | Use the `_test-` doc-id prefix so id-keyed pass-2 catches it. |
|
|
57
|
+
Nothing to register — the **entire emulator Firestore is flushed before every run**, so any collection a test writes starts empty next run automatically. If a test needs a fixture to exist (e.g. a brand doc) before the suite runs, seed it in `test/_init.js`'s `setup()` (it runs after the flush). Realtime Database test data should live under `_test/...` (that namespace is wiped pre-run).
|
|
44
58
|
|
|
45
59
|
### Within-run state isolation is different
|
|
46
60
|
|
|
47
|
-
Per-test cleanup is still appropriate when a test sets up DB state that would pollute a **later test in the same run** — e.g.
|
|
61
|
+
Per-test cleanup is still appropriate when a test sets up DB state that would pollute a **later test in the same run** — e.g. a `try/finally` that removes a fixture so the next sibling test sees a clean slate. Those stay in the test. They are *intra-run* state management, not *next-run* cleanup, and the distinction matters.
|
|
62
|
+
|
|
63
|
+
The rule: **never put cleanup at the END of a test file or suite for the purpose of preparing the next run** — for LOCAL state. The pre-test full flush already guarantees a clean slate. (The third-party provider exception lives in the runner's post-suite hook, not in individual tests.)
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
## `test/_init.js` — pre-test lifecycle hook
|
|
66
|
+
|
|
67
|
+
The runner loads an optional `test/_init.js` from **both** test roots — BEM core (`<bem>/test/_init.js`) and the consumer project (`<projectDir>/test/_init.js`) — and runs it before any test (it is NOT itself run as a test). Same contract for both roots, so framework and consumer authors write the identical file. Because the entire emulator Firestore is flushed each run, there are **no collection lists to declare** — `_init.js` only declares accounts and reseeds fixtures.
|
|
68
|
+
|
|
69
|
+
The module **must export a function** — `module.exports = (ctx) => ({ ... })` — called with `{ config, Manager }` and returning the hook object. (The function form lets a project compute its accounts/fixtures from config.) It may declare:
|
|
70
|
+
|
|
71
|
+
- `accounts` — array of extra test accounts to create alongside the built-in ones (admin/basic/premium-*/journey-*), so this project has a user for each lifecycle it exercises. Each entry is `{ id, uid, email, properties }` (email may use the `{domain}` placeholder, `properties` is merged into the user doc after `auth:on-create`). These accounts are created, fetched (privateKeys), and deleted on the same path as the built-ins, and show up in the `accounts` map that tests and `setup()` receive. A project account may override a built-in one by reusing its `id`.
|
|
72
|
+
- `async setup({ admin, config, accounts, Manager, assistant })` — seed fixtures (e.g. a brand doc) into the freshly-flushed DB, AFTER the clean slate + account creation. `accounts` is available so fixtures can reference a test uid. Use real ids that mirror production shape (no `_test-` prefix needed — the whole DB is wiped each run).
|
|
73
|
+
|
|
74
|
+
There is **no `cleanup` hook**: the entire emulator Firestore is flushed before every run and each test cleans up after itself, so there is nothing project-level to tear down.
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
// <projectDir>/test/_init.js
|
|
78
|
+
module.exports = ({ config }) => ({
|
|
79
|
+
// One account per lifecycle this project needs. Created alongside the built-ins.
|
|
80
|
+
accounts: [
|
|
81
|
+
{ id: 'shop-owner', uid: '_test-shop-owner', email: '_test.shop-owner@{domain}', properties: { roles: {} } },
|
|
82
|
+
],
|
|
83
|
+
|
|
84
|
+
// The entire emulator Firestore is flushed before each run, so just reseed.
|
|
85
|
+
async setup({ admin, accounts }) {
|
|
86
|
+
await admin.firestore().doc('brands/ultimate-jekyll').set({
|
|
87
|
+
id: 'ultimate-jekyll',
|
|
88
|
+
brand: { id: 'ultimate-jekyll', name: 'Ultimate Jekyll', url: 'https://ultimate-jekyll.itwcreativeworks.com' },
|
|
89
|
+
owner: accounts['shop-owner'].uid,
|
|
90
|
+
sponsorships: { prices: { 'guest-post': 50, 'link-insertion': 30 } },
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
50
95
|
|
|
51
96
|
## Extended Mode (`TEST_EXTENDED_MODE`)
|
|
52
97
|
|
|
@@ -118,13 +163,15 @@ npx mgr test user/ admin/ # Multiple paths
|
|
|
118
163
|
|
|
119
164
|
## Test Locations
|
|
120
165
|
|
|
121
|
-
- **BEM core tests:** `test/`
|
|
122
|
-
- **Project tests:** `
|
|
166
|
+
- **BEM core tests:** `test/` (in the framework repo)
|
|
167
|
+
- **Project tests:** the consumer project's repo-root `test/` directory (NOT inside `functions/`)
|
|
123
168
|
|
|
124
|
-
Use `bem:` or `project:` prefix to filter by source.
|
|
169
|
+
Use `bem:` or `project:` prefix to filter by source. **Mirror the source path so a test reads like what it tests.** Route tests live under `test/routes/<route-path>/<concern>.js`, mirroring `functions/routes/<route-path>/` — e.g. `functions/routes/write/article/` → `test/routes/write/article/generate.js`, `functions/routes/sponsorship/post.js` → `test/routes/sponsorship/post.js`. Split each route into **one file per concern** under its mirrored dir (`test/routes/sponsorship/post.js`, `.../manual-validation.js`), never one giant `test/test.js`. The runner discovers files by directory, so the split also drives the `project:<path>` filter: `npx mgr test project:routes/write` runs a whole route's tests, `project:routes/write/markdown` runs one concern.
|
|
125
170
|
|
|
126
171
|
## Test Types
|
|
127
172
|
|
|
173
|
+
> **The runner reads each file's `module.exports` object — it does NOT inject Mocha/Jest globals.** A test file that calls `describe`/`it`/`before`/`beforeEach`/`after` at top level throws `ReferenceError: beforeEach is not defined` and shows as `Failed to load`. There is no global `assert` either — use the `assert` passed into `run({ assert })`. Every test file MUST export one of the shapes below.
|
|
174
|
+
|
|
128
175
|
| Type | Use When | Behavior |
|
|
129
176
|
|------|----------|----------|
|
|
130
177
|
| Standalone | Single logical test | Runs once |
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ const { confirm } = require('@inquirer/prompts');
|
|
|
3
3
|
const { execSync, spawn } = require('child_process');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const jetpack = require('fs-jetpack');
|
|
6
|
+
const ui = require('../utils/ui');
|
|
6
7
|
|
|
7
8
|
class BaseCommand {
|
|
8
9
|
constructor(main) {
|
|
@@ -10,6 +11,9 @@ class BaseCommand {
|
|
|
10
11
|
this.firebaseProjectPath = main.firebaseProjectPath;
|
|
11
12
|
this.argv = main.argv;
|
|
12
13
|
this.options = main.options;
|
|
14
|
+
// Shared OMEGA-style CLI styling helpers (dividers, headers, status lines).
|
|
15
|
+
// See src/cli/utils/ui.js. Use `this.ui.*` in any command for consistent output.
|
|
16
|
+
this.ui = ui;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
async execute() {
|
|
@@ -8,9 +8,9 @@ const wonderfulVersion = require('wonderful-version');
|
|
|
8
8
|
|
|
9
9
|
class InstallCommand extends BaseCommand {
|
|
10
10
|
async execute(type) {
|
|
11
|
-
if (
|
|
11
|
+
if (['local', 'l', 'dev', 'd', 'development'].includes(type)) {
|
|
12
12
|
await this.installLocal();
|
|
13
|
-
} else if (
|
|
13
|
+
} else if (['live', 'prod', 'p', 'production'].includes(type)) {
|
|
14
14
|
await this.installLive();
|
|
15
15
|
}
|
|
16
16
|
}
|