Skip to content

Commit 6496714

Browse files
committed
test(cli): pin that every always-on slate token has a serve mount; boot-level proof for package-registry
The vocabulary pin turns the prose that declined to assert the package-registry absence into the assertion it was waiting for: every PLATFORM_ALWAYS_ON_CAPABILITIES token keys a CAPABILITY_PROVIDERS entry or a CAPABILITY_TO_TIER tier. The identity table gains the new row, and a nightly e2e boots serve twice on one database to show an API-created package survives a restart, plus a marketplace declarer mounting one PackageServicePlugin. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
1 parent b324830 commit 6496714

4 files changed

Lines changed: 379 additions & 10 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `objectstack serve` mounts the always-on `package-registry` capability, so a package created through the API survives a restart on a stock boot (#19387)
6+
7+
Clause-②: no
8+
9+
`package-registry` has been on the always-on slate (`PLATFORM_ALWAYS_ON_CAPABILITIES`) since the `marketplace` / `package-registry` split, and `serve` appended it to every app's `requires`. But `Serve.CAPABILITY_PROVIDERS` did not key it, and the resolver's no-provider branch says nothing about a token the app did not declare itself. So an app that did not declare `requires: ['marketplace']` got no `package` service. `POST /api/v1/packages` answered `201`, printed `no 'package' service — '…' registered in-memory only (will not survive a restart)`, and `GET /api/v1/packages/:id` answered `404` after a restart.
10+
11+
- **`package-registry` now mounts `PackageServicePlugin`** from `@objectstack/service-package`, the provider the spec's `PLATFORM_CAPABILITY_PROVIDERS` row declares for it. A stock boot creates `sys_packages` and replays it at start, so installs and manifest edits made through the API persist.
12+
- **Apps that declare `marketplace` boot as before, with one `PackageServicePlugin`.** `marketplace` resolves to the same provider. The capability resolver now remembers the providers it has mounted itself, so the always-on token does not mount a second copy. Without that change, a declarer's boot would print `Plugin superseded: 'package-service'`.
13+
- **A stock database gains one table, `sys_packages`.** `PackageServicePlugin` creates it with raw DDL, as it already did for `marketplace` declarers. On the in-memory driver (`memory://`), which has no raw SQL, the boot now logs that the DDL was not run and that package hydration was skipped. Packages there last only as long as the process, as before.
14+
- `--preset minimal` still opts out of the whole slate. `protocol.installPackage` keeps its in-memory-only branch as the documented degraded path for hosts that mount no provider.

‎packages/cli/test/serve-capability-identity.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ const EXPECTED_PROVIDER_NAME: Record<string, string> = {
105105
realtime: 'com.objectstack.service.realtime',
106106
mcp: 'com.objectstack.mcp',
107107
marketplace: 'package-service',
108+
// Same provider as `marketplace` (#19387). The resolver's own-mount dedup
109+
// relies on this: each row's identities contain the name the ONE constructed
110+
// PackageServicePlugin registers, which the drift block below re-derives.
111+
'package-registry': 'package-service',
108112
email: 'com.objectstack.service.email',
109113
sms: 'com.objectstack.service.sms',
110114
sharing: 'com.objectstack.service.sharing',

‎packages/cli/test/serve-capability-vocabulary.test.ts‎

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,8 @@ describe('serve capability registries vs spec vocabulary (#3265)', () => {
4949
* `Serve.ALWAYS_ON_CAPABILITIES` is a re-export of the spec slate, so this is
5050
* a SURFACE pin rather than a second copy of the spec-side one: it asserts
5151
* the split survives the hop the CLI takes, and that hop is what decides
52-
* which tokens land in an app's `requires`.
53-
*
54-
* ⚠️ Measured on `serve`'s resolver at c17ff70f3f and deliberately NOT
55-
* asserted: `Serve.CAPABILITY_PROVIDERS` keys `marketplace` and does not yet
56-
* key `package-registry`, so appending this token mounts nothing under
57-
* `objectstack serve` until the runtime half of the same ruling lands
58-
* (#17676 items 2/3/5, the engine lane). Pinning that ABSENCE here would
59-
* turn the engine lane's own fix red for doing the ruled thing, so the gap
60-
* is recorded in words and the pin states only what must hold either side of
61-
* it.
52+
* which tokens land in an app's `requires`. Whether an appended token then
53+
* MOUNTS anything is the next pin's question.
6254
*/
6355
it("appends the package-registry persistence to every app, never the catalogue half (#17676 A')", () => {
6456
expect(Serve.ALWAYS_ON_CAPABILITIES).toContain('package-registry');
@@ -70,6 +62,41 @@ describe('serve capability registries vs spec vocabulary (#3265)', () => {
7062
expect(PLATFORM_CAPABILITY_TOKENS).toContain('package-registry');
7163
expect(PLATFORM_CAPABILITY_TOKENS).toContain('marketplace');
7264
});
65+
66+
/**
67+
* #19387 — the absence direction, which this file used to record in words
68+
* only.
69+
*
70+
* `serve` force-appends every slate token to an app's `requires`, and then
71+
* mounts a token through exactly one of two paths: a CAPABILITY_PROVIDERS
72+
* entry (the `requires` resolver) or a CAPABILITY_TO_TIER entry (the
73+
* dedicated tier blocks — the named list whose own docblock says why those
74+
* tokens carry no provider entry). A slate token on NEITHER path passes both
75+
* conjuncts of the resolver's no-provider branch — it was not declared by
76+
* the app, and it is inside the vocabulary — so it mounts nothing and says
77+
* nothing. That is how `package-registry` sat on the slate inert after the
78+
* #17676 A' carve-out landed its spec half.
79+
*
80+
* ⛔ So a slate entry with no mount goes red HERE, on the pull request that
81+
* adds it, rather than being noticed as a missing service after release.
82+
* The spec slate and this package ship in one fixed release group, so a
83+
* green pin covers every published pairing; the runtime branch is not given
84+
* a warning for a case this makes unreachable.
85+
*/
86+
it('every always-on slate token has a serve mount — a provider entry or a tier (#19387)', () => {
87+
const providerTokens = new Set(Object.keys(Serve.CAPABILITY_PROVIDERS));
88+
const tierTokens = new Set(Object.keys(Serve.CAPABILITY_TO_TIER));
89+
// Non-vacuity: an empty slate would pass the filter below over nothing.
90+
expect(Serve.ALWAYS_ON_CAPABILITIES.length).toBeGreaterThan(0);
91+
const unmounted = Serve.ALWAYS_ON_CAPABILITIES.filter(
92+
(token) => !providerTokens.has(token) && !tierTokens.has(token),
93+
);
94+
expect(
95+
unmounted,
96+
'always-on tokens that `serve` force-appends to every app and then mounts NOTHING for — ' +
97+
'key each one in Serve.CAPABILITY_PROVIDERS at the provider PLATFORM_CAPABILITY_PROVIDERS declares',
98+
).toEqual([]);
99+
});
73100
});
74101

75102
// framework#3366 — the installable-provider registry must classify EVERY

0 commit comments

Comments
 (0)