Skip to content

Commit d16b9fb

Browse files
fix(metadata-protocol, objectql, core): platform store reads and writes carry the explicit system opt-in (#21938)
Fixes #21911 Clause-②: no - Each producer takes the explicit system opt-in that exists today. No gate before the hand-off fires on any of them, so nothing accepted or refused changes today. This is a slice of #21908: the engine-lane producers of the principal-less hand-off. #21908 stays open, and it builds the deny itself once every producer has a route. ## What changed Every engine call in the card's functions now passes `context: { isSystem: true }`. Inside a `SysMetadataRepository` transaction it passes `{ ...ctx, isSystem: true }`, so the transaction handle still rides along. This is the opt-in that already exists. There is no new API, no export change, and no change to what any door authorizes. | Row | Package | Function (engine calls moved) | |:--|:--|:--| | 1 | metadata-protocol | `findServedOverlayRow` (1 `findOne`) | | 2 | metadata-protocol | `overlayLockLayerAt` (1 `find`, in its store reader) | | 3 | metadata-protocol | `readActiveOverlayRows` / `queryByOrg` (2 `find`), `readFlattenedMetaItems` (2 `find`, draft preview) | | 4 | metadata-protocol | `foldStoredCollection` (1 `find`). These are the only reads `assertRuntimeAuthoringRules` issues. | | 5 | metadata-protocol | `SysMetadataRepository`: `get` 1, `put` 6, `delete` 3, `promoteDraft` 1, `restoreVersion` 2, `listDrafts` 1, `nextItemVersion` 1, `nextEventSeq` 1 | | 6 | metadata-protocol | `recordMetadataAudit` (insert), `persistPackageCommitRow` (insert), `publishPackageDrafts`, `resolveOverlayPackageBinding`, `storedFlowBindingAgrees`, `deletePackage`, `duplicatePackage` (1 read each), `reassignOrphanedMetadata` (`find` + `update`) | | 9 | objectql | `ObjectQLPlugin.readAuthoredActionRows` (3 `find`), `readAuthoredHookRows` (2 `find`) | | 10 | core | `readAuthoredTranslationLayer` (2 `find`) | H1 holds: every row sits where the card says on `cab63967`. Every engine call in each named function was enumerated with the TypeScript AST, not only the first one: 32 in metadata-protocol, 5 in objectql and 2 in core. Each now carries the opt-in. ## The six gates: none fires on these calls (Zone 1) A system context skips the six gates the middleware still runs before the hand-off's `next()`. Each one, on the `sys_metadata` family (`sys_metadata`, `_history`, `_audit`, `_commit`): - **package-managed**: acts only on `sys_permission_set`. - **system-row**: acts only on `sys_position` and `sys_capability`. - **curated-capability**: acts only on `sys_capability`. - **audience-anchor**: acts only on `sys_position_permission_set`. - **engine-owned**: the bucket matches (the family is `engine-owned` / `append-only`), but `isUserContextWrite` needs a `userId`. A context with no principal passes it by construction, exactly as a system one does. - **delegated-administration**: acts only on the RBAC link tables and `sys_member`. **Measured.** A local, uncommitted instrument sat at plugin-security's engine middleware. It wrapped each of the six gates, so a throw was recorded per gate and per operation. It also recorded the outcome after the hand-off. After the change it dry-ran the six gates for every moved `isSystem` call, with the flag cleared. The run covered the dogfood suite and a booted showcase dev composition. - Before: 0 gate throws on any of 35,248 (dogfood) + 388 (boot) principal-less operations, from any producer. 0 downstream failures on the card's functions. - After: 0 gates would fire on any moved call. The instrument was reverted, and `security-plugin.ts` equals its HEAD blob (`5b4ab280`). plugin-security's `dist/` was rebuilt clean, and `ablation-dist-preflight --absent` passed. ## Before and after, per function (H2) Principal-less, non-system operations credited to each function. A function is credited when it is the first frame past the engine, its closures and the repository transaction wrapper. | Function | dogfood before → after | boot before → after | |:--|--:|--:| | `findServedOverlayRow` | 13,614 → 0 | 80 → 0 | | `overlayLockLayerAt` | 13,736 → 0 | 80 → 0 | | `queryByOrg` (`readActiveOverlayRows`) | 2,037 → 0 | 16 → 0 | | `readFlattenedMetaItems` draft preview | 0 → 0 (no run reached it; unit-pinned) | 0 → 0 | | `foldStoredCollection` | 761 → 0 | 0 → 0 | | `SysMetadataRepository.get` / `put` / `delete` | 169 / 296 / 41 → 0 | 0 | | `promoteDraft` / `restoreVersion` / `listDrafts` | 6 / 2 / 1 → 0 | 0 | | `nextItemVersion` / `nextEventSeq` | 105 / 105 → 0 | 0 | | `recordMetadataAudit` / `persistPackageCommitRow` | 110 / 1 → 0 | 0 | | `publishPackageDrafts` / `resolveOverlayPackageBinding` / `storedFlowBindingAgrees` | 1 / 1 / 6 → 0 | 0 | | `deletePackage` / `duplicatePackage` / `reassignOrphanedMetadata` | 1 / 1 / 2 → 0 | 0 | | `readAuthoredActionRows` / `readAuthoredHookRows` | 810 / 496 → 0 | 3 / 2 → 0 | | `readAuthoredTranslationLayer` | 496 → 0 | 2 → 0 | | **all principal-less operations, any producer** | **35,248 → 2,476** | **388 → 204** | After the change, the same calls arrive as `isSystem` operations in matching numbers. For example: `findServedOverlayRow` 13,618, `queryByOrg` 2,037, `put` 296, `recordMetadataAudit` 110. The dogfood suite was green on both sides with identical counts: 205 files passed + 1 skipped, 1,590 tests passed + 9 skipped. The boot answered the same statuses on both sides: admin data reads 200, anonymous reads 401. The 2,476 / 204 operations that remain come from the other slices' producers (settings, messaging, storage, auth, webhooks, datasource). ## Tests - **Unit pins, one per package (H4):** - `metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts`: the engine double records the context of every call. It drives draft save → publish → active save → rollback → delete, the overlay and list reads (each private reader directly, too), and reassign / duplicate / uninstall. Each step asserts that it reached the store and that every call carried `isSystem: true`. Inside the transaction the context is exactly `{ transaction, isSystem: true }`. - objectql: `plugin-authored-actions.test.ts` and `plugin-authored-hooks.test.ts` each gain one case. - core: `authored-translation-sync.test.ts` gains one case. - **Ablations, each committed first and restored through `scripts/ablation-replace.mjs` (blob equals HEAD, `git diff HEAD` empty).** Each pin resolves its subject from `src`, so no `dist` leg was needed. The expected direction was red, and red is what was observed: 1. `findServedOverlayRow` opt-in dropped: 2 of 3 metadata-protocol cases red. 2. `put`'s history-insert opt-in reverted to `{ context: ctx }`: 2 of 3 red. 3. `readAuthoredHookRows`' first read set to `isSystem: false`: 1 of 11 red. 4. `readAuthoredTranslationLayer`'s first read set to `isSystem: false`: 1 of 6 red. - The first attempt at 3 and 4 used a replacement that was a prefix of its anchor. The tool refused it as a no-op and nothing ran. They were re-run with the `false` spelling. - **Package suites (H4 falsified test-side; see the acceptance notes)**, at `89ced04af3`. The merge to `9fd7113eaa` brought only two docs pages and one `rest` test: - metadata-protocol: 217 files passed + 3 skipped, 27,921 tests passed. - objectql (`local` + `repo`): 376 files, 7,476 tests passed. - core: 80 files, 2,226 tests passed. - `typecheck` for all three exit 0, including objectql's and core's `check:test-typecheck`. - **Instrumented runs:** the dogfood suite (7 chunks, 206 files) and a booted showcase dev composition, before and after, all under `os-verify-lock`. The numbers are in the table above. - **Gates:** `dispatch-gates --commands` at `9fd7113eaa` derives 76 families. All 76 were run there and exited 0, and `--ran` reconciles 76 derived, 76 run, 0 NOT-MEASURED. - `check:engine-split-ratio` first refused on the shallow clone. It was deepened (`--shallow-since=2026-06-30`) and re-run. - `check:dual-build-cjs-loads` first needed dists of unbuilt packages and a `plugin-audit` declaration. These were built, and the gate re-ran green. - `check:objectql-double-limit` flagged the new double as limit-blind. The double now applies the caller's bound. - **Lint, a proven narrowing (CI runs the full `pnpm lint`):** 1. The population comes from `eslint.config.mjs`: `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` plus the `packages/**` object. 2. `eslint --no-inline-config --format json` over the 13 changed `.ts` files: 13 files, 0 errors, 0 warnings. 3. The config never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move any untouched file's verdict. ## Acceptance notes - **Same family, not moved here (static, not in the card's list):** `SysMetadataRepository.getByHash`, `list`, `history` and `replayFromHistory` still reach the engine with no context. No measured run reached them (0 records in either probe). They belong to #21908's closure census. - **One engine check besides the six gates also stands down under `isSystem`:** the referential-integrity check on a caller-supplied lookup. On these writes the only lookup it judged was `sys_metadata.organization_id`, which the repository fills from the door-derived organization. The probe recorded 0 refusals from it (0 downstream failures on the card's functions). The other `isSystem` reads on the census page touch none of these four objects, or only change a log line (the tenant-audit warning, the reference-cleanup actor label). - **H4 was falsified, and the fix is test-side only:** objectql's protocol suites held seven exact-argument expectations, the reassign rebind and the `listDrafts` WHERE. Each now includes the opt-in. Two revert/rollback conflict pins (`protocol-commit-history`, `protocol-writepath-object-ownership`) found `put`'s in-transaction read by a bare `context` key, and every repository read now carries one. Their engine now hands its transaction callback a handle, as `ObjectQL.transaction` does, and the pin discriminates on that handle. metadata-protocol's and core's own suites passed unchanged. - **`scripts/engine-double-contract.pinned.json`** gains three rows (`--write`, a grow-only coverage ledger) for the new pin's double. That double is copied from the pinned one in `protocol-publish-drafts-org-scope.test.ts`. - **Probe artifact:** after the change, the `isSystem` count for `overlayLockLayerAt` reads 10. This is not because its reads vanished. The function is not `async`, so it does not appear in the async stack the probe filtered system records by. Its principal-less count (the claim) is 0 on both runs. - **H5:** #21864's head (`d8657b5c`, re-tested after every merge of `main`) test-merges cleanly onto this branch at `9fd7113eaa`. Its hunks are in `anonymousFormIntakeOrgScopeRefusal`, `saveMetaItem` and `promoteDraftForPublish`, and none of them is a named function here. All 16 protocol calls keep the opt-in in the merged tree. - **H6:** the cross-lane declaration is on #6367 (`6003826474`). - **H3:** the `isSystem` census page needs no edit. Object-literal producers are not elevation reads, and the census gate is green with its counts unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0728cbf commit d16b9fb

15 files changed

Lines changed: 493 additions & 36 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
"@objectstack/objectql": patch
4+
"@objectstack/core": patch
5+
---
6+
7+
The platform's own `sys_metadata` reads and writes now carry the explicit system opt-in (`isSystem: true`) instead of reaching the data engine with no principal at all
8+
9+
Clause-②: no
10+
11+
- **What moved.** Each engine call in these functions now passes `context: { isSystem: true }`. Inside a repository transaction it passes `{ ...ctx, isSystem: true }`, so the transaction handle still rides along.
12+
- `@objectstack/metadata-protocol`: the overlay reads (`findServedOverlayRow`, `overlayLockLayerAt`), the list read (`readActiveOverlayRows`, and `readFlattenedMetaItems`' draft preview), the authoring gate's stored-collection fold (`foldStoredCollection`), the audit and commit trail writes (`recordMetadataAudit`, `persistPackageCommitRow`), and the package verbs' store calls (`publishPackageDrafts`, `resolveOverlayPackageBinding`, `storedFlowBindingAgrees`, `deletePackage`, `duplicatePackage`, `reassignOrphanedMetadata`).
13+
- `SysMetadataRepository`: `get`, `put`, `delete`, `promoteDraft`, `restoreVersion`, `listDrafts` and the two lineage counters.
14+
- `@objectstack/objectql`: `ObjectQLPlugin`'s authored action and hook reads, at boot and on resync.
15+
- `@objectstack/core`: the authored-translation read (`readAuthoredTranslationLayer`).
16+
- **Why.** plugin-security passes an engine operation whose context has no user, no position, no permission set and no `isSystem` straight on to the next handler (ADR-0096's principal-less hand-off). These calls worked only because of that pass-through. They are platform plumbing: any door in front of them has already authorized the caller, and the protocol scopes its own rows by organization. So they now say so with the opt-in that already exists.
17+
- **No gate verdict moves.** A system context skips the six gates the middleware still runs before that pass-through: package-managed, system-row, curated-capability, audience-anchor, engine-owned and delegated-administration. Four of them only act on other objects. The engine-owned gate never fires on a context with no user. The delegated-administration gate only acts on the RBAC link tables. So none of the six applies to the `sys_metadata` family. An instrumented run of the dogfood suite and a booted dev composition recorded no gate firing on any of these calls before the change. After the change it recorded no principal-less call from these functions.
18+
- **One engine check also stands down under `isSystem`.** That is the referential-integrity check on a caller-supplied lookup. On these writes the only lookup it judged was `sys_metadata.organization_id`, which the repository fills from the door-derived organization. The instrumented runs recorded no refusal from it on any of these calls.
19+
- ⛔ No new API, no export change, and no change to what any door authorizes.

‎packages/core/src/fallbacks/authored-translation-sync.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,22 @@ describe('authored-translation sync replays the conversion chain over stored row
118118
expect(warn).toHaveBeenCalledTimes(1);
119119
});
120120
});
121+
122+
// [#21911, ADR-0096] The boot / resync read is platform plumbing with no caller
123+
// behind it: it carries the explicit system opt-in rather than reaching the
124+
// engine as a principal-less context. No singular rows, so the legacy plural
125+
// fallback issues too.
126+
describe('authored-translation read carries the explicit system opt-in (#21911)', () => {
127+
it('passes isSystem on the singular read and on the plural fallback', async () => {
128+
const engine = engineOf([]);
129+
130+
const layer = await readAuthoredTranslationLayer(engine);
131+
132+
expect(layer).toEqual({});
133+
expect(engine.find.mock.calls.map(([obj, q]) => [obj, q?.where?.type])).toEqual([
134+
['sys_metadata', 'translation'],
135+
['sys_metadata', 'translations'],
136+
]);
137+
for (const [, q] of engine.find.mock.calls) expect(q?.context).toEqual({ isSystem: true });
138+
});
139+
});

‎packages/core/src/fallbacks/authored-translation-sync.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,18 @@ export async function readAuthoredTranslationLayer(
119119
): Promise<Record<string, Record<string, unknown>> | null> {
120120
let rows: any[];
121121
try {
122+
// [#21911, ADR-0096] The explicit system opt-in: a boot / resync read of
123+
// the platform store with no caller behind it, never a principal-less
124+
// engine context.
122125
rows = (await engine.find('sys_metadata', {
123126
where: { type: 'translation', state: 'active' },
127+
context: { isSystem: true },
124128
})) ?? [];
125129
if (rows.length === 0) {
126130
// Legacy plural rows — mirrors the protocol's singular/plural fallback.
127131
rows = (await engine.find('sys_metadata', {
128132
where: { type: 'translations', state: 'active' },
133+
context: { isSystem: true },
129134
})) ?? [];
130135
}
131136
} catch (err: any) {
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#21911, ADR-0096] The metadata protocol's platform-store calls carry the
5+
* EXPLICIT system opt-in, never a principal-less context.
6+
*
7+
* plugin-security hands an engine operation whose context carries no user, no
8+
* position, no permission set and no `isSystem` straight to `next()` (the
9+
* ADR-0096 E1 hand-off). The protocol's own reads and writes of the
10+
* `sys_metadata` family reached the engine exactly that way — a `{ where }`
11+
* with no `context`, or a repository transaction context with only its
12+
* handle — so they worked only because the hand-off let them through. They
13+
* now pass `context: { isSystem: true }` (inside a repository transaction,
14+
* `{ ...ctx, isSystem: true }`, so the handle rides along), the opt-in that
15+
* already exists; nothing about what any door authorizes moves.
16+
*
17+
* The observation channel is the engine double's own record of the context
18+
* each call arrived with. Every step asserts it recorded at least one call
19+
* (so a step that stops reaching the store reds instead of passing over
20+
* nothing) and that every recorded call carried `isSystem: true`.
21+
*
22+
* The double is the pinned shape of `protocol-publish-drafts-org-scope.test.ts`
23+
* (its write verbs and `findOne` open with the producer's own dispatch
24+
* predicates), plus that recorder and a transaction that hands its callback a
25+
* real handle object, as `ObjectQL.transaction` does.
26+
*/
27+
28+
import { describe, expect, it } from 'vitest';
29+
import { assertEngineDeleteDispatch, assertEngineUpdateDispatch, assertEngineFindOnePredicate } from '@objectstack/metadata-core';
30+
import { ObjectStackProtocolImplementation } from './protocol.js';
31+
32+
interface Row {
33+
id: string;
34+
type: string;
35+
name: string;
36+
organization_id: string | null;
37+
package_id: string | null;
38+
state: string;
39+
metadata: string;
40+
[k: string]: unknown;
41+
}
42+
43+
interface HistoryRow {
44+
id: string;
45+
organization_id: string | null;
46+
type: string;
47+
name: string;
48+
version: number;
49+
[k: string]: unknown;
50+
}
51+
52+
interface EngineCall {
53+
verb: string;
54+
table: string;
55+
context: Record<string, unknown> | undefined;
56+
}
57+
58+
const TRX = { trx: 'handle' };
59+
60+
function keyOf(w: Record<string, unknown>) {
61+
return `${w.type}|${w.name}|${w.organization_id ?? '__env__'}|${w.state ?? 'active'}|${w.package_id ?? '__nopkg__'}`;
62+
}
63+
64+
function matchesMetadataWhere(r: Row, where: Record<string, unknown>): boolean {
65+
for (const [k, v] of Object.entries(where)) {
66+
if (k === '$or') {
67+
const clauses = v as Array<Record<string, unknown>>;
68+
if (!clauses.some((c) => matchesMetadataWhere(r, c))) return false;
69+
continue;
70+
}
71+
if (k.startsWith('$')) throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
72+
if (v === undefined) continue;
73+
if (r[k] !== v) return false;
74+
}
75+
return true;
76+
}
77+
78+
function makeStubEngine() {
79+
const rows = new Map<string, Row>();
80+
const historyRows: HistoryRow[] = [];
81+
const calls: EngineCall[] = [];
82+
let nextId = 0;
83+
84+
const record = (verb: string, table: string, context: unknown) => {
85+
calls.push({ verb, table, context: context as Record<string, unknown> | undefined });
86+
};
87+
88+
const findRow = (w: Record<string, unknown>): { key: string; row: Row } | null => {
89+
if (w.id !== undefined) {
90+
for (const [k, r] of rows) if (r.id === w.id) return { key: k, row: r };
91+
return null;
92+
}
93+
for (const [k, r] of rows) if (matchesMetadataWhere(r, w)) return { key: k, row: r };
94+
return null;
95+
};
96+
97+
const matchesHistory = (h: HistoryRow, w: Record<string, unknown>): boolean => {
98+
for (const k of ['organization_id', 'type', 'name', 'version', 'checksum'] as const) {
99+
if (w[k] !== undefined && h[k] !== w[k]) return false;
100+
}
101+
return true;
102+
};
103+
104+
const engine: any = {
105+
async findOne(table: string, opts: { where: Record<string, unknown>; context?: unknown }) {
106+
assertEngineFindOnePredicate(table, opts);
107+
record('findOne', table, opts.context);
108+
if (table === 'sys_metadata_history') {
109+
return historyRows.find((h) => matchesHistory(h, opts.where)) ?? null;
110+
}
111+
return findRow(opts.where)?.row ?? null;
112+
},
113+
async find(table: string, opts: { where: Record<string, unknown>; limit?: number; context?: unknown }) {
114+
record('find', table, opts?.context);
115+
const hits: unknown[] = table === 'sys_metadata_history'
116+
? historyRows.filter((h) => matchesHistory(h, opts.where ?? {}))
117+
: table === 'sys_metadata'
118+
? Array.from(rows.values()).filter((r) => matchesMetadataWhere(r, opts.where ?? {}))
119+
: [];
120+
// The caller's bound, applied after the filter, by presence.
121+
return typeof opts?.limit === 'number' ? hits.slice(0, opts.limit) : hits;
122+
},
123+
async insert(table: string, data: Record<string, unknown>, options?: { context?: unknown }) {
124+
record('insert', table, options?.context);
125+
nextId += 1;
126+
if (table === 'sys_metadata_history') {
127+
historyRows.push({ id: `h_${nextId}`, ...(data as any) });
128+
return { id: `h_${nextId}` };
129+
}
130+
if (table !== 'sys_metadata') return { id: `${table}_${nextId}` };
131+
const row = { id: `r_${nextId}`, ...(data as any) } as Row;
132+
rows.set(keyOf(data), row);
133+
return { id: row.id };
134+
},
135+
async update(table: string, data: Record<string, unknown>, opts: { where: Record<string, unknown>; context?: unknown }) {
136+
assertEngineUpdateDispatch(data, opts);
137+
record('update', table, opts.context);
138+
const found = findRow(opts.where);
139+
if (!found) return { id: null };
140+
const merged = { ...found.row, ...(data as any) };
141+
rows.delete(found.key);
142+
rows.set(keyOf(merged), merged);
143+
return { id: found.row.id };
144+
},
145+
async delete(table: string, opts: { where: Record<string, unknown>; context?: unknown }) {
146+
assertEngineDeleteDispatch(opts);
147+
record('delete', table, opts.context);
148+
const found = findRow(opts.where);
149+
if (!found) return { deleted: 0 };
150+
rows.delete(found.key);
151+
return { deleted: 1 };
152+
},
153+
async transaction<T>(cb: (ctx: any, info: { owned: boolean }) => Promise<T>): Promise<T> {
154+
return cb({ transaction: TRX }, { owned: true });
155+
},
156+
registry: {
157+
registerItem: () => undefined,
158+
registerObject: () => undefined,
159+
unregisterItem: () => undefined,
160+
listItems: () => [],
161+
getItem: () => undefined,
162+
getObject: () => undefined,
163+
getPackage: () => undefined,
164+
getArtifactItem: () => undefined,
165+
isPackageDisabled: () => false,
166+
applyNavContributions: (app: unknown) => app,
167+
},
168+
};
169+
return { engine, rows, historyRows, calls };
170+
}
171+
172+
const viewBody = (name: string) => ({
173+
name,
174+
label: 'Project Tasks',
175+
object: 'proj_task',
176+
viewKind: 'list',
177+
columns: [{ field: 'title', label: 'Title' }],
178+
});
179+
180+
/**
181+
* Runs `step`, then asserts it reached the store and that every engine call
182+
* it issued carried the explicit system opt-in.
183+
*/
184+
async function expectSystemOptIn(
185+
calls: EngineCall[],
186+
label: string,
187+
step: () => Promise<unknown>,
188+
): Promise<EngineCall[]> {
189+
const from = calls.length;
190+
await step();
191+
const issued = calls.slice(from);
192+
expect(issued.length, `${label}: reached the store`).toBeGreaterThan(0);
193+
const principalLess = issued.filter((c) => c.context?.isSystem !== true);
194+
expect(principalLess, `${label}: every engine call carries isSystem: true`).toEqual([]);
195+
return issued;
196+
}
197+
198+
describe('platform-store calls carry the explicit system opt-in (#21911)', () => {
199+
it('the repository write path: draft save, publish, active save, rollback, delete', async () => {
200+
const { engine, calls } = makeStubEngine();
201+
const protocol = new ObjectStackProtocolImplementation(engine);
202+
203+
// SysMetadataRepository.put (create, draft) + the two lineage counters.
204+
const draftCalls = await expectSystemOptIn(calls, 'draft save', () => protocol.saveMetaItem({
205+
type: 'view', name: 'proj_task_grid', item: viewBody('proj_task_grid'),
206+
packageId: 'app.pin', mode: 'draft',
207+
}));
208+
// Inside the repository transaction the handle rides along with the opt-in.
209+
const inTxn = draftCalls.filter((c) => c.context?.transaction !== undefined);
210+
expect(inTxn.length, 'the put transaction reached the store').toBeGreaterThan(0);
211+
for (const c of inTxn) expect(c.context).toEqual({ transaction: TRX, isSystem: true });
212+
213+
// publishPackageDrafts: listDrafts, its pre-publish active-row read,
214+
// promoteDraft (+ put), recordMetadataAudit and persistPackageCommitRow.
215+
const publishCalls = await expectSystemOptIn(calls, 'publish', async () => {
216+
const res = await protocol.publishPackageDrafts({ packageId: 'app.pin' });
217+
expect(res).toMatchObject({ success: true, publishedCount: 1, failedCount: 0 });
218+
});
219+
expect(publishCalls.some((c) => c.table === 'sys_metadata_audit' && c.verb === 'insert')).toBe(true);
220+
expect(publishCalls.some((c) => c.table === 'sys_metadata_commit' && c.verb === 'insert')).toBe(true);
221+
222+
// put (update) + the runtime authoring gate's stored-collection fold.
223+
await expectSystemOptIn(calls, 'active save', () => protocol.saveMetaItem({
224+
type: 'view', name: 'proj_task_grid',
225+
item: { ...viewBody('proj_task_grid'), label: 'Project Tasks v2' },
226+
packageId: 'app.pin', mode: 'publish', force: true,
227+
}));
228+
229+
// restoreVersion (history read + active read, then put).
230+
await expectSystemOptIn(calls, 'rollback', async () => {
231+
await protocol.rollbackMetaItem({ type: 'view', name: 'proj_task_grid', toVersion: 1 } as any);
232+
});
233+
234+
// SysMetadataRepository.delete (findOne, delete, tombstone insert).
235+
const deleteCalls = await expectSystemOptIn(calls, 'delete', () => protocol.deleteMetaItem({
236+
type: 'view', name: 'proj_task_grid', force: true,
237+
} as any));
238+
expect(deleteCalls.some((c) => c.verb === 'delete' && c.table === 'sys_metadata')).toBe(true);
239+
});
240+
241+
it('the overlay reads: served row, lock layer, list read, draft preview', async () => {
242+
const { engine, calls } = makeStubEngine();
243+
const protocol = new ObjectStackProtocolImplementation(engine) as any;
244+
245+
await protocol.saveMetaItem({
246+
type: 'view', name: 'proj_task_grid', item: viewBody('proj_task_grid'),
247+
packageId: 'app.pin', mode: 'draft',
248+
});
249+
250+
// findServedOverlayRow + overlayLockLayerAt (the by-name read).
251+
await expectSystemOptIn(calls, 'by-name read', () => protocol.getMetaItem({ type: 'view', name: 'proj_task_grid' }));
252+
// readFlattenedMetaItems: readActiveOverlayRows' queryByOrg + the draft preview.
253+
await expectSystemOptIn(calls, 'list read with draft preview', () => protocol.getMetaItems({
254+
type: 'view', previewDrafts: true,
255+
}));
256+
// Each private reader on its own, so no caller can mask one of them.
257+
await expectSystemOptIn(calls, 'findServedOverlayRow', () => protocol.findServedOverlayRow({
258+
type: 'view', name: 'proj_task_grid', orgId: 'org_a', state: 'draft', packageId: 'app.pin',
259+
}));
260+
await expectSystemOptIn(calls, 'overlayLockLayerAt', () => protocol.overlayLockLayerAt(
261+
{ type: 'view', name: 'proj_task_grid', organizationId: 'org_a' }, { otherSpelling: true },
262+
));
263+
await expectSystemOptIn(calls, 'readActiveOverlayRows', () => protocol.readActiveOverlayRows({ type: 'view' }, 'org_a'));
264+
await expectSystemOptIn(calls, 'foldStoredCollection', () => protocol.foldStoredCollection([], 'object', 'objects', 'org_a'));
265+
await expectSystemOptIn(calls, 'resolveOverlayPackageBinding', () => protocol.resolveOverlayPackageBinding('view', 'proj_task_grid', 'org_a'));
266+
await expectSystemOptIn(calls, 'storedFlowBindingAgrees', () => protocol.storedFlowBindingAgrees('flow_a', 'app.pin'));
267+
await expectSystemOptIn(calls, 'recordMetadataAudit', () => protocol.recordMetadataAudit({
268+
type: 'view', name: 'proj_task_grid', operation: 'update', outcome: 'success', code: 'OK',
269+
}));
270+
await expectSystemOptIn(calls, 'persistPackageCommitRow', () => protocol.persistPackageCommitRow({ id: 'c_1' }));
271+
});
272+
273+
it('the package verbs: reassign orphans, duplicate, uninstall', async () => {
274+
const { engine, calls } = makeStubEngine();
275+
const protocol = new ObjectStackProtocolImplementation(engine);
276+
277+
await protocol.saveMetaItem({
278+
type: 'view', name: 'orphan_grid', item: viewBody('orphan_grid'), mode: 'publish',
279+
});
280+
281+
const reassignCalls = await expectSystemOptIn(calls, 'reassignOrphanedMetadata', async () => {
282+
const res = await protocol.reassignOrphanedMetadata({ targetPackageId: 'app.pin' });
283+
expect(res.reassignedCount).toBe(1);
284+
});
285+
expect(reassignCalls.map((c) => c.verb)).toEqual(['find', 'update']);
286+
287+
await expectSystemOptIn(calls, 'duplicatePackage', async () => {
288+
await protocol.duplicatePackage({
289+
sourcePackageId: 'app.pin', targetPackageId: 'com.example.pincopy', targetNamespace: 'pincopy',
290+
} as any).catch(() => undefined);
291+
});
292+
await expectSystemOptIn(calls, 'deletePackage', async () => {
293+
await protocol.deletePackage({ packageId: 'com.example.pincopy', allTenants: true } as any).catch(() => undefined);
294+
});
295+
});
296+
});

0 commit comments

Comments
 (0)