Skip to content

Commit ab488d9

Browse files
fix(cli)!: refuse a production os serve that composes no organization object, --server or not (#25155)
Fixes #25133 Clause-②: yes (narrowing) Metadata-protocol: none Executes the maintainer's ruling 6328987873 (letter A) under the claim 6329345390. Dispatched by the epic seat (`epic:#15194`, `session_01Y5583bs2RDd53MH6kM1ca3`). Ships as an 18.x patch. Review tier: `CONTRACT_REVIEW_TIER`, because the diff declares a narrowing. ## What changed `packages/cli/src/commands/serve.ts`: a production `os serve` / `os start` (no `--dev`) that composes no organization object is now refused **whether or not `--server`**, before any plugin starts: - **The `auth` tier is off and no `AuthPlugin` is mounted (A1).** The condition goes from `flags.server && noAuth` to `(flags.server || !isDev) && noAuth`. A development boot keeps the old shape: it is refused only when it serves. - **The tier is on but no secret resolved (`no-secret`).** The condition drops `flags.server`. `no-secret` is only ever the reason on a production boot, because a development boot resolves the fallback secret, so this needs no posture check of its own. - **Both refusal texts** drop the "run with --no-server" remedy and the "drop the REST/dispatcher plugins" remedy. Both were offered as ways to serve without the data API, and both still write ownerless rows. Each text now says that kernel-only does not avoid the refusal. The no-secret text also names the migrate-and-exit job. - **Unchanged:** - the `host-kernel` reason (the cloud runtime's warn-only path); - `os dev` and `--dev`; - programmatic embedding, which never runs this command. - **Ruling item 2:** no "tenant-scoped objects present" qualifier is written. I measured no composition it would spare (see Acceptance notes). ## Ruling item 5: the count 15 test files name `--no-server`. 8 of them carry neither `--dev` nor an auth secret, not 9: - **7 are unit-level** and boot nothing: `serve-datasource-admin-routes`, `serve-http-server-composition`, `serve-kernel-only-hold`, `serve-no-transport-announcements` (unit), `serve-observability-registration`, `serve-unknown-hostname-guard`, `format.server-ready-no-http-server`. Each reads `serve.ts` source or calls a pure function. - **1 spawns, but not in production:** `serve-migrate-exit-schema-parity.integration`. It spawns through `bin/run-dev.js`, which pins `NODE_ENV=development`, so it is a dev boot and is untouched. The one test that really boots production `--no-server` without a secret is `test/serve-ready-degraded-boot.e2e.test.ts` (nightly tier). It carries `OS_AUTH_SECRET` in its healthy leg only, which is why the flag/secret count misses it. Its degraded leg is re-fixtured onto the one production composition that still skips auth with a warning: a host-kernel stub, a plugin registered under `ObjectOSEnvironmentPlugin`'s name. **Missed by that count, found by CI on `5996c531`:** four cli integration files booted production `--no-server --preset minimal`. `minimal` composes no auth (tiers `['core']`), so ruling item 1's no-auth refusal now reaches them: `serve-no-server-banner`, `serve-no-transport-announcements`, `serve-kernel-only-lifetime` and `serve-no-storage-boot-guard` (6 tests). They are re-fixtured in `6531b332`. The banner and transport legs boot today's production kernel-only command (auth composed, a store). The lifetime and no-storage legs, whose shape is no store and no auth, boot the host-kernel stub with tiers `['core','auth']` and still pass `--preset minimal`. **Consequence:** a production `os serve --preset minimal` is now refused with or without `--server`, unless the stack mounts AuthPlugin itself. It was already refused with `--server`. This is ruling item 1 as written (report 6330837413). Non-test callers: no workflow, Dockerfile, example or script in the repo runs `--no-server`. `os start` always hands its `serve` child a secret. ## Pins: `packages/cli/test/serve-no-auth-secret-refusal.integration.test.ts` (10 legs) All legs spawn `bin/run.js` with `NODE_ENV` unset. - **Refused, with the database never opened:** - no-secret with `--server`; - no-secret with `--no-server`; - no-secret `--no-server` with `OS_MIGRATE_AND_EXIT=1`; - no-auth with `--server`; - no-auth with `--no-server` (with a secret). - **Boots, reaching ready and opening the database:** - a configured secret with `--server`; - a configured secret with `--no-server`; - `--dev --no-server` with no secret; - the host kernel with no secret, `--no-server` (asserts the warn-only line); - no-auth with `--dev --no-server`. ## Proof (head `6531b332`; CI green: 42 check runs, 0 failing, Test Core 6/6) - **The changed files, locally:** - the pin file: 10/10; - `serve-ready-degraded-boot.e2e` (`OS_TEST_TIERS=nightly`): 2/2. - **cli unit layer:** `vitest run --project unit` reports 321/321 files and 4746/4746 tests. Lock `VERDICT command-exit 0`. - **cli typecheck:** exit 0. - **cli integration layer:** the rest of it is declared to CI (`Test Core`). - **Ablation 1:** `ablation-replace.mjs` turns the no-secret condition back to `flags.server && …`, the cli is rebuilt, and `ablation-dist-preflight` confirms the marker is in `dist/`. - Result: 2 failed and 8 passed. The red pins are no-secret `--no-server` ("the boot was not refused") and the migrate-and-exit leg (it exited 0). - Restore: blob == HEAD. - **Ablation 2:** the no-auth condition goes back to `flags.server && …`. - Result: 1 failed and 9 passed. The red pin is no-auth `--no-server`. - Restore: blob == HEAD. After a rebuild, `--absent` confirms both markers are gone from all 680 built files and the tree is clean. - **`node scripts/pm/dispatch-gates.mjs --commands`:** 101 families, every exit code captured before any pipe. - Five were non-zero on the first sweep: four prerequisite refusals (exit 3: a shallow clone, and unbuilt packages) and `check:cli-test-child-env` (exit 1, a real finding). - The finding is fixed in `5996c531`: `NODE_ENV: undefined` now sits after each leg's env spread. - All five re-ran at exit 0 after a full build and a fetch of the pinned fixture commit. `--ran` accounts for 101 of 101. - **`check-governed-merges --branch`:** exit 0, NOT governed (0 of 6 paths), +276 / -63. - **R1:** - `pnpm dev:crm -- --fresh` reached ready, health 200; - a production `os serve` of `examples/app-crm` with `OS_AUTH_SECRET` reached ready, health 200; - the same app with `--no-server` and no secret exits 1 with the new text, and its database is never opened. - **R4:** a walled production `os serve` (`OS_TENANCY_POSTURE=group`, `invite-only`, `OS_PLATFORM_OWNER_EMAIL`, a secret) on a scratch app that declares `@objectstack/organizations` reached ready with `Organizations` composed, health 200. ## Docs - `content/docs/deployment/cli.mdx`: the `--no-server` example and the flag description now say a production kernel-only boot is held to the same auth rule. - `content/docs/upgrading.mdx`: the 18.0 auth-secret paragraph now covers no-auth, `--no-server`, migrate-and-exit, and the unaffected boots. - `content/docs/releases/**` is not touched (ruling item 4). The changeset carries the correction for the v18 page's `--no-server` sentence. ## Acceptance notes - **The `tenancy.enabled: false` remedy was left out on purpose.** The dispatch asked for it as an operator remedy, but ruling item 2 writes no qualifier, so declaring it does not lift the no-auth refusal. Offering it in the refusal text would advertise a fix that does not work. The changeset says so explicitly. - **The pending changeset `.changeset/24583-7b3-organizationless-counter-retired.md`** (already shipped in 18.0.0's `dist`, still pending in `.changeset/`) contradicts this change twice: "Unchanged: `--no-server` (kernel only) still boots with the warning" and "or run kernel-only with `--no-server`". It is outside the claimed file surface and was not edited. This PR's changeset states that it supersedes both sentences. The seat may want that file corrected before the next version cut. - **The degraded-boot e2e's healthy and degraded legs** now differ in config (bare vs host-kernel stub) as well as secret. Its header says so. - **The pin file now spawns 10 boots,** about 34 s of wall time locally. It declares no `test-budget:`, because there is no CI reading yet. - **Edits were first made on the session's primary checkout** (the claim's "the cloud session's own checkout"). They moved to a linked worktree once the guard hooks refused; no content was lost. --- _Generated by [Claude Code](https://claude.ai/code/session_019pgvPFjtU47ULgfY7bZc6Q)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 504d8b3 commit ab488d9

11 files changed

Lines changed: 361 additions & 90 deletions

‎.changeset/24583-7b3-organizationless-counter-retired.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ Clause-②: yes (narrowing)
1515

1616
**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D9, stage 8d).
1717

18-
- **`os serve`: a production server boot whose auth did not compose for a missing secret is refused.** With the `auth` tier on and no `OS_AUTH_SECRET` (outside `--dev`), `serve` used to skip AuthPlugin with a warning and serve anyway. That boot had no security model: anonymous object data is always denied, so its data API was unusable. It also had no organization object, so its inline seed wrote rows with `organization_id` NULL and drew their autonumbers from the `__global__` counter. Measured on `examples/app-showcase` before this change: health 200, two NULL-organization rows and a `__global__` counter row on a fresh install. The boot now stops before the kernel starts, so nothing is written, and the message names both fixes. Unchanged: `--no-server` (kernel only) still boots with the warning, a development boot still uses its fallback secret, and the cloud host kernel (`ObjectOSEnvironmentPlugin`, where auth is each project's) still only warns. `os start` never reaches the refusal: it always supplies a secret to the `serve` child it spawns.
18+
- **`os serve`: a production server boot whose auth did not compose for a missing secret is refused.** With the `auth` tier on and no `OS_AUTH_SECRET` (outside `--dev`), `serve` used to skip AuthPlugin with a warning and serve anyway. That boot had no security model: anonymous object data is always denied, so its data API was unusable. It also had no organization object, so its inline seed wrote rows with `organization_id` NULL and drew their autonumbers from the `__global__` counter. Measured on `examples/app-showcase` before this change: health 200, two NULL-organization rows and a `__global__` counter row on a fresh install. The boot now stops before the kernel starts, so nothing is written, and the message names both fixes. A kernel-only boot (`--no-server`) is refused the same way: it runs the same seed, so it would write the same ownerless rows. Unchanged: a development boot still uses its fallback secret, and the cloud host kernel (`ObjectOSEnvironmentPlugin`, where auth is each project's) still only warns. `os start` never reaches the refusal: it always supplies a secret to the `serve` child it spawns.
1919
- **`@objectstack/driver-sql` (and `@objectstack/driver-turso`'s remote face, which calls the same rule): the counter refuses an organization-less row on a tenant table.** Wherever the driver registered the organization object (`sys_organization`), a `create` / `bulkCreate` / `upsert` on an object with a tenant column, whose row carries no organization and whose options carry no `tenantId`, is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (status 500), the code the engine already answers for the same write. It is refused before any counter row is read or written, and nothing is written. New exports: `OrganizationlessCounterRefusal`, `ORGANIZATIONLESS_COUNTER_REFUSAL_CODE`, `ORGANIZATIONLESS_COUNTER_REFUSAL_STATUS`, `renderOrganizationlessCounterRefusal`. Unchanged: `__global__` stays the counter key of an object with no tenant column, and of a composition that registers no organization object on the driver (a lean `@objectstack/objectql/core` embedding), where no organization can exist. Counter rows a 17.x install filed under `__global__` for a tenant table stay in `_objectstack_sequences`; the counter no longer reads them, and `os migrate duplicates` still reports them.
2020
- **`@objectstack/objectql`: `GLOBAL_TENANT` is no longer exported**, and `SystemWriteOrganizationRequiredError`'s message no longer describes the `__global__` counter as where the write would land. The code, status and fields are unchanged.
2121
- **Texts.** The lint explanation of `unique/legacy-organization-composite`, the `UniqueScopeSchema` TSDoc and the error-code ledger now say that a NULL-organization row is residue an earlier release wrote, not something a new write produces.
2222

2323
**The remedy.**
24-
- A production boot refused for the missing secret: set `OS_AUTH_SECRET` (a long random value, identical across restarts and nodes), or run kernel-only with `--no-server`.
24+
- A production boot refused for the missing secret: set `OS_AUTH_SECRET` (a long random value, identical across restarts and nodes), or compose auth by other means (mount `AuthPlugin` in `plugins`). Running kernel-only with `--no-server` does not avoid the refusal.
2525
- A direct driver write refused by the counter: carry the organization — `driver.create(object, { ...row, organization_id: orgId })` or `driver.create(object, row, { tenantId: orgId })` — or write through the engine, which derives it under `single`.
2626

2727
The retired export:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli)!: a production `os serve` that composes no organization object is refused whether or not `--server`, so `--no-server` no longer writes ownerless rows
6+
7+
Clause-②: yes (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) A boot-time refusal in `os serve`: no key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. No export, type, flag or error code is removed or renamed; `--no-server` keeps its meaning (no HTTP server plugin) and only stops being a way round the auth refusal. The remedy is a deployment one, stated below. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this boot (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
11+
12+
**BREAKING** boot-refusal narrowing (ADR-0131 D1, "NULL is not a state", and D9). It changes which production boots start, not what a metadata author can write, so it ships as `patch` with this banner.
13+
14+
- FROM: a production `os serve --no-server` (no `--dev`; `os start` has no `--no-server` flag) booted the kernel when the stack's `auth` tier was on and no auth secret resolved, or when the stack mounted no auth at all (the `auth` tier off, `--preset minimal` included, with no `AuthPlugin` of its own), warning `⚠ AuthPlugin skipped — set OS_AUTH_SECRET …`. Neither boot composes an organization object, so every row it wrote, its inline seed first, landed with no organization. Measured on an 18.0.0-born store: a deleted seed row was written back with `organization_id` NULL. On a fresh store the seed failed instead, every write refused by the NOT NULL tenant column. Both refusal texts named "run with --no-server" as a remedy.
15+
- TO: both boots are refused whether or not `--server`, before any plugin starts. That includes a production `os serve --preset minimal`: the minimal preset composes no `auth` tier, so unless the stack mounts `AuthPlugin` itself it is refused, kernel-only or not (with `--server` it already was), so nothing is written and the database is never opened. That covers a `--no-server` migrate-and-exit job (`OS_MIGRATE_AND_EXIT=1`), which runs the same kernel bootstrap. Neither refusal text offers `--no-server` any more: each says a kernel-only boot does not avoid it.
16+
17+
**Unchanged:** `os dev` and `--dev` (a development boot resolves its fallback secret, and a no-auth development boot is still refused only when it serves); an embedded kernel (`createStandaloneStack`, `@objectstack/objectql/core`), which never runs this command; and the cloud runtime's host kernel (`ObjectOSEnvironmentPlugin`, where auth is each project's), which keeps its warn-only path. `os start` always hands its `serve` child an auth secret, so it reaches the no-auth case only.
18+
19+
**The remedy.**
20+
- Refused for the missing secret: set `OS_AUTH_SECRET` (a long random value, identical across restarts and nodes, e.g. `OS_AUTH_SECRET=$(openssl rand -hex 32)`).
21+
- Refused because the stack mounts no auth: enable auth (add the `auth` tier, or mount `AuthPlugin` in `plugins`) and set `OS_AUTH_SECRET`. Declaring the stack's own objects `tenancy: { enabled: false }` does not lift this refusal: the platform's own system objects are tenant-scoped, so a production stack with no organization object would still write ownerless rows.
22+
- A cloud migrate-and-exit job (`OS_MIGRATE_AND_EXIT=1`) must carry `OS_AUTH_SECRET` in production; without it the job is now refused, naming the variable.
23+
24+
**Release-notes correction.** The 18.0 release page says a production `os serve` with no auth secret can "serve without the data API (`--no-server`)". That remedy is withdrawn: `--no-server` is refused the same way. The pending changeset `24583-7b3-organizationless-counter-retired` is corrected in this change to match.

‎content/docs/deployment/cli.mdx‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -354,20 +354,28 @@ os serve # Default: port 3000
354354
os serve -p 4000 # Custom port
355355
os serve --dev # Development mode (pretty logs, devPlugins)
356356
os serve --dev --ui # Dev mode with Console UI
357-
os serve --no-server # Skip HTTP server (kernel only)
358-
os serve --preset minimal # Skip auto-loaded auth/i18n/ui plugins
357+
os serve --no-server # Skip HTTP server (kernel only; production still needs OS_AUTH_SECRET)
358+
os serve --preset minimal # Skip auto-loaded auth/i18n/ui plugins (production: refused unless the stack mounts AuthPlugin)
359359
```
360360
361361
**Options:**
362362
- `-p, --port <port>` — Server port (default: env `OS_PORT` or `3000`)
363363
- `--dev` — Development mode (loads devPlugins, pretty logging)
364364
- `--ui / --no-ui` — Toggle Console UI at `/_console/` (default `on`)
365-
- `--server / --no-server` — Toggle HTTP server plugin
365+
- `--server / --no-server` — Toggle HTTP server plugin. `--no-server` serves no
366+
data API but still boots the kernel and writes its seed, so a production boot
367+
(no `--dev`) is held to the same auth rule as a server boot: a stack that
368+
enables auth needs `OS_AUTH_SECRET`, and a stack that mounts no auth is
369+
refused. Both are refused before any plugin starts, and so is a
370+
`--no-server` migrate-and-exit job (`OS_MIGRATE_AND_EXIT=1`) without the
371+
secret.
366372
- `--prebuilt` — Skip esbuild / `bundle-require` and load the config as
367373
native ESM (use this in production builds where the config is already
368374
pre-compiled)
369375
- `--preset minimal | default | full` — Override the auto-registration
370-
tier (see below)
376+
tier (see below). `minimal` composes no `auth` tier, so a production boot
377+
(no `--dev`) with it is refused, with or without `--server`, unless the
378+
stack mounts `AuthPlugin` in its own `plugins`.
371379
372380
**Probes.** A served process exposes `GET /api/v1/health` (liveness — process
373381
only, and deliberately blind to configuration and credentials, so a
@@ -386,14 +394,18 @@ only gate the **automatic** registration of optional plugins.
386394
387395
| Preset | Tiers | Auto-loaded optional plugins |
388396
|:---|:---|:---|
389-
| `minimal` | `core` | none |
397+
| `minimal` | `core` | none — in production, refused unless the stack mounts `AuthPlugin` itself (see below) |
390398
| `default` *(default)* | `core`, `i18n`, `ui`, `ai`, `auth` | i18n service, Console UI, AI service, Auth + Security + Audit |
391399
| `full` | `core`, `i18n`, `ui`, `ai`, `auth` | currently an alias of `default` — same tiers, no additional plugins |
392400
393-
The `auth` tier requires `OS_AUTH_SECRET` to be set; otherwise `AuthPlugin`
394-
is skipped with a yellow warning and the `/api/v1/auth/*` endpoints will
395-
return 404. (In `--dev` mode the CLI falls back to an insecure local secret so
396-
login works out of the box.) To take full control, set `tiers` on the stack config:
401+
The `auth` tier requires `OS_AUTH_SECRET` in production: without it the boot
402+
is refused before any plugin starts, with or without `--server`, because a boot
403+
with no `AuthPlugin` has no organization and every row it wrote would have no
404+
owner. A production boot whose tiers leave `auth` out (`--preset minimal`
405+
included) is refused the same way unless the stack mounts `AuthPlugin` itself.
406+
In `--dev` mode the CLI falls back to an insecure local secret, so login works
407+
out of the box. (The cloud runtime's host kernel, where auth is each project's,
408+
only warns.) To take full control, set `tiers` on the stack config:
397409
398410
```typescript
399411
import { defineStack } from '@objectstack/spec';

‎content/docs/upgrading.mdx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,20 @@ one it is refused: `✖ FATAL: tenancy posture 'group' is walled, but this deplo
410410
**A production `os serve` deployment needs an auth secret.** A production
411411
`os serve` whose stack enables auth and which resolves no `OS_AUTH_SECRET` is
412412
refused before any plugin starts, so set the secret before you start such a
413-
deployment on 18.0. `os start` is not affected: it generates an auth secret on
414-
its first run and persists it to `<home>/auth-secret`.
413+
deployment on 18.0. A production boot whose stack mounts no auth at all (the
414+
`auth` tier off and no `AuthPlugin` of its own) is refused the same way: enable
415+
auth and set the secret. Either boot has no organization, so every row it wrote
416+
would have no owner.
417+
418+
`--no-server` does not avoid either refusal: a kernel-only boot runs the same
419+
seed and would write the same ownerless rows. Earlier 18 releases let a
420+
`--no-server` boot through, and their refusal text offered `--no-server` as a
421+
remedy; it is not one. The same holds for a migrate-and-exit job
422+
(`OS_MIGRATE_AND_EXIT=1`): give it `OS_AUTH_SECRET` too. A development boot
423+
(`os dev`, `--dev`) and an embedded kernel are not affected, and neither is the
424+
cloud runtime's host kernel, whose auth is each project's. `os start` is not
425+
affected: it generates an auth secret on its first run and persists it to
426+
`<home>/auth-secret`.
415427

416428
What the plan lists, how a blocked row is resolved, and every flag are in the
417429
command's reference:

‎packages/cli/src/commands/serve-kernel-only-lifetime.integration.test.ts‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2-
// test-budget: 40 — spawns the real producer once per single-cause fixture: three boots of the built CLI (production --no-server --preset minimal, --dev --no-server, a default composed boot), each differing only in the input under test, held 3 s past its banner and then stopped by SIGTERM; 27.0 s, the slower of two local runs on a shared container, x 1.25, no CI reading yet
2+
// test-budget: 40 — spawns the real producer once per single-cause fixture: three boots of the built CLI (production --no-server on the host kernel, --dev --no-server, a default composed boot), each differing only in the input under test, held 3 s past its banner and then stopped by SIGTERM; 27.0 s, the slower of two local runs on a shared container, x 1.25, no CI reading yet
33

44
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
55
import { spawn, type ChildProcess } from 'node:child_process';
@@ -30,9 +30,18 @@ import { fileURLToPath } from 'node:url';
3030
* Each boot is read for its banner and `objectstack:seed-settled`, then left
3131
* alone for {@link HOLD_WINDOW_MS}, then sent SIGTERM:
3232
*
33-
* 1. production `--no-server --preset minimal` (the card's command): still
34-
* alive after the window, and SIGTERM ends it with the kernel's code, 0,
35-
* the kernel's own handler exiting the process (no signal death);
33+
* 1. production `--no-server` with no store and no auth composed (the card's
34+
* shape): still alive after the window, and SIGTERM ends it with the
35+
* kernel's code, 0, the kernel's own handler exiting the process (no
36+
* signal death). [ADR-0131 D9] The card's own `--preset minimal` composes
37+
* no auth, and a production boot with no auth is now refused whether or
38+
* not `--server`; the one production composition that still boots with
39+
* no auth is the cloud runtime's host kernel (auth is each project's
40+
* there), so this leg boots that — {@link HOST_KERNEL_PLUGIN} in the
41+
* config, `tiers: ['core', 'auth']` so the `auth` tier is on and skipped
42+
* by the host-kernel path, still `--preset minimal`, no secret. It keeps
43+
* what made the defect: no storage driver, so no pool holding the event
44+
* loop;
3645
* 2. CONTROL: `--dev --no-server` behaves as before: the same;
3746
* 3. CONTROL: a default composed production boot sends
3847
* `[objectstack:listening, objectstack:seed-settled]`, stays up, and stops
@@ -83,6 +92,8 @@ const OBJECT = `{ name: 'fx_task', label: 'Task', pluralLabel: 'Tasks', fields:
8392
const MANIFEST = `{ id: 'com.example.fx23744', namespace: 'fx', version: '1.0.0', type: 'app', name: 'Fx' }`;
8493
/** An instantiated plugin, which is what makes this a HOST config. */
8594
const HOST_PLUGIN = `{ name: 'com.example.fx23744.host-plugin', version: '1.0.0', init: async () => {}, start: async () => {} }`;
95+
/** The cloud runtime's host kernel, as `isHostKernelComposition` recognises it: auth is skipped with a warning. */
96+
const HOST_KERNEL_PLUGIN = `{ name: 'com.objectstack.runtime.objectos-environment', version: '0.0.0', init: async () => {}, start: async () => {} }`;
8697

8798
interface IpcMessage { type?: string }
8899
interface Exit { code: number | null; signal: NodeJS.Signals | null }
@@ -99,6 +110,8 @@ interface Lifetime {
99110
const children: ChildProcess[] = [];
100111
let root: string;
101112
let plainDir: string;
113+
/** The same, on the cloud runtime's host kernel — see shape 1. */
114+
let hostKernelDir: string;
102115

103116
/** The root of an installed dependency of this package, found through `node_modules`. */
104117
function packageRoot(specifier: string): string {
@@ -249,6 +262,15 @@ beforeAll(() => {
249262
+ `export default defineStack({ manifest: ${MANIFEST}, objects: [${OBJECT}], plugins: [${HOST_PLUGIN}] });\n`,
250263
);
251264
symlinkSync(packageRoot('@objectstack/spec'), join(plainDir, 'node_modules', '@objectstack', 'spec'), 'dir');
265+
hostKernelDir = join(root, 'host-kernel');
266+
mkdirSync(join(hostKernelDir, 'node_modules', '@objectstack'), { recursive: true });
267+
writeFileSync(join(hostKernelDir, 'package.json'), JSON.stringify({ name: 'fx-23744-host-kernel', private: true, type: 'module' }));
268+
writeFileSync(
269+
join(hostKernelDir, 'objectstack.config.ts'),
270+
"import { defineStack } from '@objectstack/spec';\n"
271+
+ `export default defineStack({ manifest: ${MANIFEST}, tiers: ['core', 'auth'], objects: [${OBJECT}], plugins: [${HOST_PLUGIN}, ${HOST_KERNEL_PLUGIN}] });\n`,
272+
);
273+
symlinkSync(packageRoot('@objectstack/spec'), join(hostKernelDir, 'node_modules', '@objectstack', 'spec'), 'dir');
252274
});
253275

254276
afterAll(() => {
@@ -260,9 +282,9 @@ afterAll(() => {
260282

261283
describe('os serve — a boot with no HTTP transport stays up until it is stopped (#23744)', () => {
262284
it(
263-
'production --no-server --preset minimal (the card\'s command): alive after its banner, and SIGTERM ends it with 0',
285+
'production --no-server with no store and no auth (the card\'s shape, on the host kernel): alive after its banner, and SIGTERM ends it with 0',
264286
async () => {
265-
expectHeldThenStopped(await bootHoldStop(plainDir, {}, ['--no-server', '--preset', 'minimal']));
287+
expectHeldThenStopped(await bootHoldStop(hostKernelDir, {}, ['--no-server', '--preset', 'minimal']));
266288
},
267289
240_000,
268290
);

0 commit comments

Comments
 (0)