Skip to content

Commit 7098aca

Browse files
feat(core,verify,cli): bootStack mounts the always-on slate and builds each provider from the app's configuration — item 1 gap of #22301 (#22747)
Part of #22301 Clause-②: yes (narrowing: a configuration whose mail or SMS settings, or the OS_EMAIL_* / OS_SMS_* environment, name a transport that cannot deliver, now fails bootStack where it booted; widening: bootStack mounts the always-on slate objectstack serve mounts for every app and hands each provider the app's configuration, so the app's analytics cubes now reach the registry, where it mounted fewer and built them with defaults) Item 1 of #22301: the remaining composition gap. The stage-2 contract review on PR #22381 recorded it (`6075048879`, judgment 4). It falls under ruling `6070767186` (A): for one configuration, `bootStack` composes what `objectstack serve` composes. Claim: `6099249975`. Items 6 and 7 are not addressed here, and #22301 remains open for them. **Status: draft.** This PR does not leave draft until the full dogfood suite has run on CI. The contract review at `CONTRACT_REVIEW_TIER` is owed before enqueue. ## What changes 1. **One rule for what a served boot mounts, in `@objectstack/core`.** It lives in `capability-composition.ts`, beside `capability-providers.ts`, and both `serve` and `bootStack` read it: - `resolveServedCapabilities(declaredTokens, { preset, hostDefaults })` answers **which tokens** get a provider, in order: - the declared tokens, which each boot reads with its own `stackDeclaredCapabilities` call; - `email` for a declared `auth`; - the host's own defaults; - the always-on slate (`PLATFORM_ALWAYS_ON_CAPABILITIES`), unless the preset is `minimal`; - `job` and `queue`, moved ahead of the tokens that schedule background work. - `resolveCapabilityArgument(token, { stack, packageRoot, providerModule, env })` answers **what each provider is constructed with**: - `automation` gets the app's root; - `analytics` gets the app's `analyticsCubes` (the top level, then the legacy `cubes`, then each package body's); - `email` and `sms` get the deployment's mail and SMS configuration; - `storage` gets its local root. 2. **`serve` is re-pointed, and its composition does not change.** Its token expansion (`requires` plus the slate) and its per-token argument block are replaced by the two calls. The precedence, the order and the declared/best-effort split are unchanged. Evidence: `serve-package-declared-capabilities` (spawned) and the three capability e2e files pass on this branch (below). - **`os migrate plan`'s declaration boot reads the same token rule.** The boot-preparation parity pin that landed on `main` during this round (#22579, `boot-preparation-parity.test.ts`) holds every step `serve` runs to a shared function the migrate boot runs too; the merge made it red. `composeServedPlatform` (`schema-migration-plugins.ts`) now expands its tokens through `resolveServedCapabilities` instead of its own `[...requires, ...PLATFORM_ALWAYS_ON_CAPABILITIES]` — the same set; a declared `auth` now places `email` after the declared tokens, as `serve` does. The pin lists `resolveServedCapabilities` under `composition`, and `resolveCapabilityArgument` as `SERVE_ONLY` with its reason: that boot constructs each provider for its declarations only, with a measured posture per token, and runs no `start()`. 3. **`bootStack` mounts the always-on slate and builds each provider from the app's configuration.** - The slate is `queue`, `job`, `cache`, `settings`, `email`, `storage`, `sms`, `sharing`, `messaging`, `analytics` and `package-registry`. - The analytics service gets the app's cubes. The email and SMS services get the configuration with `OS_EMAIL_*` / `OS_SMS_*` over it, and storage gets the `OS_STORAGE_LOCAL_ROOT` root. - A caller's `extraPlugins` / `security` / `analytics` instance still wins by identity. Neither that precedence nor the instance rule changes. - The fixed-point hard-dependency search in `required-providers.ts` is removed: with the slate always mounted, it could no longer find anything to add. 4. **Where the configuration readers live — the boundary, measured.** - The `email` and `sms` readers moved from `serve.ts` to `@objectstack/plugin-email` and `@objectstack/service-sms`. Each must refuse exactly what its package's transports cannot build, so it reads that package's transport vocabulary. Both packages depend on `@objectstack/core`, so core cannot import them. - `resolveCapabilityArgument` reads each reader off the provider module the boot already loaded to construct the provider. A module without its reader is refused by name. - The storage reader moved into core. `serve.ts` re-exports all moved names, so `data-migration-plugins.ts` and the existing CLI tests import them unchanged. - What stays with each boot: - `--preset`, a CLI flag; - the host defaults — MCP (`OS_MCP_SERVER_ENABLED`) and pinyin search (`OS_SEARCH_PINYIN_ENABLED`, which `serve` stamps into the process env from the locales). Both are process decisions, not configuration, and `bootStack` passes none; - the failure policy. `serve` makes a declared token fatal and logs and skips a slate one. `bootStack` fails on either and names the remedy, which is the ruling's "a plugin that cannot be mounted fails the boot loudly"; - the production storage warning. ## Clause-② — line 2 is the claim's corrected line, and the changeset carries it byte for byte Line 2 is the claim's `Clause-②:` line as the seat corrected it (`6099249975`), and the changeset carries the same line byte for byte. Both arms are measured: - **Narrowing.** A mail or SMS configuration (or an `OS_EMAIL_*` / `OS_SMS_*` environment) that names a transport that cannot deliver now fails `bootStack`. Before, the provider was absent or built with defaults. - **Widening.** `bootStack` mounts the always-on slate and hands each provider the app's configuration, so the app's analytics cubes now reach the registry. A cube the service refuses is warned and skipped by the service — `AnalyticsService`'s constructor registers each cube inside a `try` (`analytics-service.ts:1490`–`:1498`) — under `bootStack` as under `serve`, and no boot fails on it. Contract review `6103628981` (①.6) corrected the cube point: an earlier revision listed the cube under the narrowing. `scripts/pm/clause2-line.mjs` reads the line as `yes` / arm `narrowing`, with a BREAKING banner and the ADR-0087 marker `not-required (no-migration-prescription)`; `check-adr-0087-registration` is green. ## Pins (one per gap, each with a control, each ablation-verified) `packages/verify/src/harness.served-composition.test.ts` has 7 cases: - **The slate:** an app that requires nothing gets every slate provider. Control: `approvals` and `realtime` are not mounted. - **Cubes:** a top-level cube and a package-body cube reach `analytics.cubeRegistry`. Control: an app with no cube has none of that name. - **Configuration:** `email.persist: false` reaches the provider, so a send leaves 0 `sys_email` rows. The control (no configuration) leaves 1 row. A `provider: 'smtp'` with no host fails the boot, naming `EmailServicePlugin`, the provider and `OS_EMAIL_SMTP_HOST`. Ablations, through `scripts/ablation-replace.mjs`. Verify's tests import `./harness.js` from source and alias `@objectstack/core` to source, so no build sits between mutation and reading. Every leg was restored to the HEAD blob with `git diff HEAD` empty. The legs ran at `95060af7c7` (A1, A2, A3 first run) and `3cb1f2abcc` (A3 re-run). That was before the token rule's parameter became the declared tokens (`03181fc07b`, which changes its input, not what it answers), so the A1 anchor reads as it was then. | Leg | Mutation | Result | |---|---|---| | A1 slate | `resolveServedCapabilities(opts.config)` given `{ preset: 'minimal' }` | 4 failed / 3 passed. The slate case went red (`expected [ 'queue', 'job', 'cache', …(5) ] to deeply equal []`), and so did the email cases (no email service). The cube cases stayed green. | | A2 cubes | the analytics argument replaced by `undefined` | 2 failed / 5 passed, exactly the two cube cases (`expected [] to include 'svc_note_cube'`) | | A3 configuration, first run | the main provider's argument skipped | 3 failed. The control failed too, because a default-built email service has no default sender (`VALIDATION_FAILED: from address required`). So the probe was changed to pass its own sender, and the leg was re-run. | | A3 configuration, re-run | same | 2 failed / 5 passed, exactly the persist case (`expected 1 to be +0`) and the smtp refusal (`promise resolved … instead of rejecting`). The control stayed green. | `packages/core/src/capability-composition.test.ts` has 12 cases, one per step of each rule. `harness.required-providers.test.ts` was moved off slate tokens (`cache` → `realtime`), so it still proves the `requires` reader and not the slate. ## Re-pointed tests (no second copy) - `serve-email-config-parity.contract.test.ts` moved, with its reader, to `@objectstack/plugin-email` as `capability-arg.config-parity.contract.test.ts`. It still scans the reader's own source for `cfgEmail` reads, against `EmailServiceConfigSchema`. That package already declares the comment-mask cross-package input. - `serve-auth-app-name.contract.test.ts`: the email half now drives `resolveCapabilityArgument('email', …)` with `@objectstack/plugin-email` as the provider module. The source half asserts that AuthPlugin's `appName` reads the same three stack keys, and that `serve.ts` calls the rule at exactly one site with `stack: config` and `env: process.env`. - `normalized-call-sites.test.ts`: the `commands/serve.ts :: config.analyticsCubes` row is gone, because the read moved to core. ## Dogfood: the full suite was the reading, and it required three test-only adaptations | File | What changed | |---|---| | `attachments-permission-matrix` (a′) | Its premise is a composition WITHOUT service-storage, which the slate now mounts. The fixture passes a stand-in under `com.objectstack.service.storage` in `extraPlugins`, the caller-wins rule. It keeps the real plugin, its objects and its floor alternate out — the shape `serve --preset minimal` boots. It failed before the change (a `reference_not_found` on `file_id`, 400) and passes after. | | `activity-parent-read-gate` | The list case GETs the parent of every row the boot mirrored, platform objects included (its header). The slate's email service seeds its templates at boot: 24 `sys_activity` rows about `sys_email_template` on this fixture, counted. The case measured 4372 ms with the slate and 2690 ms with the slate ablated in verify's `dist/` (preflight present / absent both green). It timed out once at the 5000 ms default in shard 1. It gets an explicit 30 s bound. | | `audit-log-parent-read-gate` | Same shape over `sys_audit_log`: 5025 ms (timed out) with the slate, 3032 ms ablated. It gets the same bound. | ## Evidence (commands, verbatim counts) Every reading below is on head `03181fc07b` (`git rev-parse --short HEAD`), which merges `origin/main` `762db996ad`, with every package rebuilt from that tree first (`pnpm turbo run build --filter=@objectstack/dogfood^... --filter=@objectstack/cli... --filter=@objectstack/example-crm...`: 63 successful). The A/B timings are the exception: they were taken on `8be3183274` / `a3b4b05071`, with verify's `dist/` mutated and restored. | Command | Result | |---|---| | `pnpm turbo run typecheck --filter=@objectstack/core --filter=@objectstack/plugin-email --filter=@objectstack/service-sms --filter=@objectstack/verify --filter=@objectstack/cli --concurrency=2` | 63 successful, 63 total | | `pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2` | 28 files passed, 223 tests passed | | `pnpm --filter @objectstack/core test` | 90 files / 2319 tests passed | | `pnpm --filter @objectstack/core run test:repo` | 4 / 51 passed | | `pnpm --filter @objectstack/plugin-email test` | 32 / 543 passed | | `pnpm --filter @objectstack/service-sms test` | 5 / 74 passed | | `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2` | 279 files / 4156 tests passed | | the same, over the 15 serve, capability, boot-parity and schema-migration files this diff touches or depends on | 15 / 322 passed | | `… --project integration` over `serve-package-declared-capabilities`, `plan.boot-parity`, `schema-migrate.requires-providers`, `schema-migrate.host-composition` and `schema-migrate` | 5 / 26 passed | | `OS_TEST_TIERS=nightly … --project integration` over `serve-package-registry-always-on.e2e`, `serve-mcp-capability-collision.e2e` and `requires-retired-capability.e2e` | 3 / 11 passed | | `node packages/cli/bin/run.js verify --app examples/app-crm/objectstack.config.ts --rls` and the same for `examples/app-showcase` | both "verify passed — no runtime failures" | | Dogfood, `OS_TEST_SHARD=k/3 pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2` | 1/3: 80 files, 611 tests passed. 2/3: 80 files passed, 563 tests passed / 1 skipped. 3/3: 79 files passed / 1 skipped, 751 tests passed / 8 skipped. | | `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths), every command run with its exit recorded before any pipe | 77 of 77 exit 0 | | `node scripts/pm/dispatch-gates.mjs --ran ran.list` | "77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN" | The roster gates for the directories this diff writes in also ran, each exit 0: `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`, `check-changeset-fixed`, `check:published-readme-exports` and `check:stack-collection-maps`. `check:dual-build-cjs-loads` refused with `PREREQUISITE NOT MET` (exit 3) until the eight packages it reads (`studio`, `client-react`, …) were built. It ran after that and is among the 77. ## Deviations from the claim's file surface, each forced by a measurement - `@objectstack/plugin-email` and `@objectstack/service-sms` (src, their index exports, the moved parity contract): the email and SMS readers depend on each package's transport vocabulary, and `@objectstack/core` cannot import either package. - `packages/cli/src/utils/schema-migration-plugins.ts` and `boot-preparation-parity.test.ts`: the #22579 pin that landed on `main` during this round, which the merge turned red. - Three `packages/qa/dogfood` test files, not `bootShowcase`: the full dogfood run's red. None is one of #22615's files (#22615 touched no dogfood file). - Three CLI test files whose prose or source scan named the moved code. ## Acceptance notes (not filed) - **The remaining divergence from `serve`, measured.** `bootStack` does not mount MCP or pinyin search. These are host-process defaults decided by env, and the pinyin one stamps the process env from the stack's locales. A test process boots many stacks, so a stamp from one boot would outlive it. A suite that needs either passes the provider in `extraPlugins`; the MCP and pinyin dogfood suites already do. Carrier: #22301, item 1. - **`email` / `sms` / `appName` are read but cannot be authored.** The mail and SMS readers read `config.email`, `config.sms` and `config.appName`, and `defineStack` refuses all three keys (`STACK_SCHEMA_INVALID`, "Unrecognized key(s) on this stack definition", measured on this tree). So they reach a provider only from a configuration `defineStack` did not build — which `os build` / `os validate` refuse (`STACK_PROVENANCE_MISSING`) and `os serve` boots. It is pre-existing and unchanged here, since this diff moves the readers and does not change what they read. The new pins add `email` to the configuration object for that reason. It is reported to the seat as a finding. - **A refused `bootStack` keeps its kernel's process-signal listeners.** `ObjectKernel`'s constructor registers SIGINT / SIGTERM / SIGQUIT listeners, and a boot refused before `bootstrap()` never releases them. At worker exit this prints `ERROR Shutdown failed … Kernel not running`, observed after this file's refused-boot case. The behaviour is pre-existing: every refused boot path shares it. It is cosmetic. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e865300 commit 7098aca

25 files changed

Lines changed: 1505 additions & 744 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/core': minor
3+
'@objectstack/verify': minor
4+
'@objectstack/plugin-email': minor
5+
'@objectstack/service-sms': minor
6+
'@objectstack/cli': patch
7+
---
8+
9+
`bootStack` mounts the always-on capability slate `objectstack serve` mounts for every app, and builds each provider from the app's configuration the way `serve` builds it
10+
11+
Clause-②: yes (narrowing: a configuration whose mail or SMS settings, or the OS_EMAIL_* / OS_SMS_* environment, name a transport that cannot deliver, now fails bootStack where it booted; widening: bootStack mounts the always-on slate objectstack serve mounts for every app and hands each provider the app's configuration, so the app's analytics cubes now reach the registry, where it mounted fewer and built them with defaults)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling or stored shape is removed, renamed or re-shaped, and no stored row is read, rewritten, converted or dropped, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is the in-process verification boot of @objectstack/verify: a configuration whose mail or SMS settings (or the OS_EMAIL_* / OS_SMS_* environment) name a transport that cannot deliver now fails the boot where the provider was absent or built with defaults. The other categories are closed on facts: every bumped package publishes (not unpublished); no ADR-0087 id covers these paths and this diff adds none (not registered / already-registered); and the narrowed surface is a runtime boot function, not an interface or a type (not runtime-interface-only / type-surface-only). -->
14+
15+
**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes.
16+
17+
**`@objectstack/verify` — the composition `serve` mounts, built the way `serve` builds it.** For one configuration, `bootStack(config, opts)` already mounted the providers the app's `requires` names and the plugins in its own `plugins` array. It now also:
18+
19+
- **Mounts the always-on slate** `serve` mounts for every app, whether or not `requires` names it: `queue`, `job`, `cache`, `settings`, `email`, `storage`, `sms`, `sharing`, `messaging`, `analytics` and `package-registry`. Before, a slate provider was mounted only where a mounted plugin hard-depended on it, so an app's tests ran without the inbox delivery, the mail service, the file storage and the package registry its users' server has.
20+
- **Builds each provider from the app's configuration**, by the same rule `serve` reads: the analytics service gets the app's `analyticsCubes` (the top level, then each package body's), the email service the app's `email` block with `OS_EMAIL_*` over it, the SMS service the `sms` block with `OS_SMS_*` over it, and the storage service the `OS_STORAGE_LOCAL_ROOT` root (`.objectstack/data/uploads` under the working directory by default). Before, each was built with its own defaults: no cubes, no mail configuration. So the app's cubes now reach the analytics registry; a cube the service refuses (one over an object the API does not serve) is warned and skipped by the service, under `bootStack` as under `serve`, and no boot fails on it.
21+
- A caller's `extraPlugins`, `security` and `analytics` instances still take precedence by identity, and are not handed the configuration. A suite that needs a provider configured its own way (a temporary storage root, a mail transport) passes its instance in `extraPlugins`.
22+
- Not mounted, because they are decisions about a server process rather than about the configuration: `serve`'s MCP endpoint (`OS_MCP_SERVER_ENABLED`) and its pinyin search (`OS_SEARCH_PINYIN_ENABLED`). A suite that exercises either passes the provider in `extraPlugins`.
23+
24+
**What now fails that booted before (the narrowing).** A provider `bootStack` constructs and cannot build fails the boot, naming the capability token and the package — whether the app declared the token or the slate appended it. `serve` logs a slate provider it cannot build and boots on; a test boot does not, so an app's tests never pass on a composition its server does not run. In practice:
25+
26+
- a configuration whose `email` or `sms` settings, or the `OS_EMAIL_*` / `OS_SMS_*` environment of the test process, name a transport that cannot deliver (`provider: 'smtp'` with no host, `resend` / `postmark` with no API key, an unknown provider tag) is refused with the reader's own remedy. Set `OS_EMAIL_PROVIDER=log` / `OS_SMS_PROVIDER=log` in the test environment if it is not meant to send mail or SMS.
27+
28+
**`@objectstack/core`** exports the rule both boots read: `resolveServedCapabilities` (given the tokens a stack declares, which capability tokens a served boot mounts providers for — those, `email` for a declared `auth`, the host's own defaults, the always-on slate unless the preset is `minimal`, and `job` / `queue` ahead of what schedules background work) and `resolveCapabilityArgument` (what each provider is constructed with), with their `ServedCapabilities`, `CapabilityArgumentInput` and `CapabilityArgument` types; and `resolveStorageCapabilityArg`, `resolveStorageLocalRootEnv` and `StorageCapabilityArg`, moved here from the CLI.
29+
30+
**`@objectstack/plugin-email`** exports `resolveEmailCapabilityArg`, `resolveDeploymentAppName` and `EmailCapabilityArg`, and **`@objectstack/service-sms`** exports `resolveSmsCapabilityArg` and `SmsCapabilityArg` — the readers that build each provider from the deployment's configuration, moved from the CLI to sit beside the transport vocabulary they refuse against. `resolveCapabilityArgument` reads each off the provider module it is handed.
31+
32+
**`@objectstack/cli`**: `serve` reads both rules from `@objectstack/core` and composes exactly what it composed before. `os migrate plan` / `apply`'s declaration boot reads the same token rule for the providers it composes (the same set as before; a declared `auth` now places `email` right after the declared tokens, as `serve` does). `os verify` boots through `bootStack`, so it now mounts the always-on slate and builds the providers from the app's configuration. The moved functions stay importable from the `serve` command module.

‎packages/cli/src/commands/serve-audit-registration.contract.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
// ## Why a source scan rather than a boot
4141
//
4242
// Same reason as this directory's `serve-verify-security-parity.contract.test.ts`
43-
// and `serve-email-config-parity.contract.test.ts`: the failure mode is an EDIT
43+
// and `@objectstack/plugin-email`'s `capability-arg.config-parity.contract.test.ts`
44+
// (this directory's `serve-email-config-parity.contract.test.ts` until #22301):
45+
// the failure mode is an EDIT
4446
// to these files, and every one of the three facts above is invisible to a
4547
// behavioural test. `serve.ts`'s audit block is reachable only from a live
4648
// `objectstack serve` boot with `@objectstack/plugin-auth` and

‎packages/cli/src/commands/serve-auth-app-name.contract.test.ts‎

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,23 @@
1414
//
1515
// Two halves, both needed. The resolver half proves the chain is the email
1616
// chain, rung for rung, so the two consumers cannot disagree about the name.
17-
// The construction half scans `serve.ts` (comments masked, as its siblings
18-
// `serve-email-config-parity.contract.test.ts` and
19-
// `serve-verify-security-parity.contract.test.ts` do), because nothing else
20-
// sees the AuthPlugin literal: the plugin is imported through a variable
21-
// specifier, so the type checker cannot, and only a booted CLI could.
17+
// [#22301] The email half is driven through the call `serve` makes for every
18+
// capability provider — `resolveCapabilityArgument` (`@objectstack/core`) with
19+
// `@objectstack/plugin-email` as the provider module — because that call, not
20+
// a line of `serve.ts`, decides which of the stack's keys the email
21+
// capability is built from. The construction half scans `serve.ts` (comments
22+
// masked, as its sibling `serve-verify-security-parity.contract.test.ts`
23+
// does), because nothing else sees the AuthPlugin literal: the plugin is
24+
// imported through a variable specifier, so the type checker cannot, and only
25+
// a booted CLI could.
2226

2327
import { describe, it, expect } from 'vitest';
2428
import { readFileSync } from 'node:fs';
2529
import path from 'node:path';
2630
import { fileURLToPath } from 'node:url';
27-
import { resolveDeploymentAppName, resolveEmailCapabilityArg } from './serve.js';
31+
import { resolveCapabilityArgument } from '@objectstack/core';
32+
import * as pluginEmail from '@objectstack/plugin-email';
33+
import { resolveDeploymentAppName } from './serve.js';
2834
import { maskComments } from '../../../../scripts/js-comment-mask.mjs';
2935

3036
const SERVE_SOURCE = maskComments(
@@ -58,9 +64,14 @@ function callArguments(source: string, callee: string): string[] {
5864
/** Argument text compared as code: whitespace and a trailing comma dropped. */
5965
const normalise = (args: string) => args.replace(/\s+/g, '').replace(/,$/, '');
6066

67+
/** The app name the email capability is built with, by the call `serve`'s capability loop makes. */
6168
const emailAppName = (cfgEmail: Record<string, any>, env: NodeJS.ProcessEnv, configAppName?: string) =>
62-
(resolveEmailCapabilityArg(cfgEmail, env, configAppName).options
63-
.defaultTemplateContext as Record<string, unknown>).appName;
69+
((resolveCapabilityArgument('email', {
70+
stack: { email: cfgEmail, appName: configAppName },
71+
packageRoot: '/app',
72+
providerModule: pluginEmail,
73+
env,
74+
}).argument as { defaultTemplateContext: Record<string, unknown> }).defaultTemplateContext).appName;
6475

6576
describe('resolveDeploymentAppName is the email chain, rung for rung', () => {
6677
const RUNGS: Array<[string, Record<string, any>, NodeJS.ProcessEnv, string | undefined, string]> = [
@@ -80,7 +91,7 @@ describe('resolveDeploymentAppName is the email chain, rung for rung', () => {
8091
});
8192

8293
describe('serve constructs AuthPlugin with the deployment app name', () => {
83-
it('passes appName from resolveDeploymentAppName, with the arguments the email capability gets', () => {
94+
it('passes appName from resolveDeploymentAppName, over the keys the email capability is built from', () => {
8495
const constructions = callArguments(SERVE_SOURCE, 'new AuthPlugin');
8596
// Absence must be loud: a scan that finds no construction proves nothing.
8697
expect(constructions.length, 'serve.ts must construct AuthPlugin exactly once').toBe(1);
@@ -90,8 +101,15 @@ describe('serve constructs AuthPlugin with the deployment app name', () => {
90101
expect(passed.length, 'AuthPlugin must receive appName from resolveDeploymentAppName').toBe(1);
91102
expect(literal).toMatch(/\bappName:\s*resolveDeploymentAppName\(/);
92103

93-
const emailCalls = callArguments(SERVE_SOURCE, 'resolveEmailCapabilityArg');
94-
expect(emailCalls.length, 'serve.ts must resolve the email capability exactly once').toBe(1);
95-
expect(normalise(passed[0])).toBe(normalise(emailCalls[0]));
104+
// [#22301] The email capability is resolved by `resolveCapabilityArgument`,
105+
// from the stack's `email` block (or `{}`), the environment and the stack's
106+
// `appName` — the call the rungs above drive. AuthPlugin must read those
107+
// same three off this boot's config, and the loop must hand the rule this
108+
// boot's config and environment.
109+
expect(normalise(passed[0])).toBe(normalise('(config as any).email ?? {}, process.env, (config as any).appName'));
110+
const dispatched = callArguments(SERVE_SOURCE, 'resolveCapabilityArgument');
111+
expect(dispatched.length, 'serve.ts must resolve the provider arguments at exactly one site').toBe(1);
112+
expect(normalise(dispatched[0])).toContain('stack:config,');
113+
expect(normalise(dispatched[0])).toContain('env:process.env');
96114
});
97115
});

‎packages/cli/src/commands/serve-verify-security-parity.contract.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
// Neither can see THIS file's failure mode, which is the one that actually
2626
// happened: nothing in the repo pins `serve.ts`'s side, so re-open-coding the
2727
// wiring here — or dropping it — would be green everywhere while the paths
28-
// separate again. Hence a source scan, in the shape of this package's
29-
// `serve-email-config-parity.contract.test.ts`: the grep that would have caught
28+
// separate again. Hence a source scan, in the shape of `@objectstack/plugin-email`'s
29+
// `capability-arg.config-parity.contract.test.ts` (this package's
30+
// `serve-email-config-parity.contract.test.ts` until #22301): the grep that would have caught
3031
// it, mechanised, so the second divergence fails a build instead of waiting for
3132
// someone to run it.
3233
//

0 commit comments

Comments
 (0)