Skip to content

Commit 514001a

Browse files
fix(rest,runtime): the published-snapshot doors answer the package's flow for a shipped flow name with a stored row, as the layered read does (#21002) (#21116)
Fixes #21002 Clause-②: yes (widening) The published-snapshot read of a flow name a managed package ships now answers the package's flow when a stored row of that name is at rest. This holds on the REST route and on its runtime-dispatcher twin. This is the second half of #21002, as triage ruled in `5924438659`: option A, scoped by the decision, not by type. The first half, the layered read, landed in PR #21043. ⚠️ This body follows the #20761 family's disclosure discipline. It talks about doors, roles, codes and statuses only. It has no request body, header or field spelling, and no seeding steps. ## What changed **`packages/rest/src/rest-server.ts`**, the `GET /meta/:type/:name/published` handler: - It still reads the layered answer first. When that answer has a stored layer, the door now asks the protocol's `isShippedFlowName` about the answer's own type and name. - When the predicate holds, the layered read has put the loader's body over the stored row. The door then serves the effective layer, which is the loader's body. - In every other case it serves the stored layer, exactly as before. A protocol that brings no such predicate also keeps today's answer. **`packages/runtime/src/domains/meta.ts`**, the dispatcher twin of that route: - The same change, in the same place. - `MetaDomainProtocol` gains the predicate as an optional member. It is `Pick`ed from `ObjectStackProtocolImplementation`, not restated, so a rename at the producer is a compile error here. This is the same move `domains/automation.ts` makes for `packagedBaseRefusal`. **`packages/metadata-protocol/src/protocol.ts`**, the declared cross-lane surface the claim allows: - `isShippedFlowName` changes from `private` to public. Its body is unchanged. - A docblock paragraph names the doors that ask it. - `getMetaItemLayered`'s effective-layer decision (PR #21043) is not touched. **`.changeset/21002-published-door-shipped-flow.md`**: `@objectstack/metadata-protocol` `minor`, `@objectstack/rest` `patch`, `@objectstack/runtime` `patch`. ## How the doors learn the decision There is one decision point, the predicate PR #21043 already calls. - No per-type list, no new response key, no fourth precedence path, and no second copy of the rule. - Each door asks the protocol's own predicate about the type and name the layered answer reports. The layered answer's type is the canonical singular, so the predicate gets exactly the arguments `getMetaItemLayered` used. - `object` is never named. Its effective layer differs from its stored layer by folding and governance, not by this decision, so it is served byte-identically. The predicate was private to the protocol class. A door in another package could not reach it except by copying the rule (the flow-only scoping plus `packagedArtifactOwner`), which the ruling forbids. So making it public is the minimal reachability change. ## Clause ② reads `yes (widening)`, not the claim's `no` The claim's own reading said the dev re-reads the line against the real diff. The real diff adds one public member to a class `@objectstack/metadata-protocol` exports, so its published declaration grows. - The rule `check-changeset-no-major.mjs` quotes says a purely additive widening of a published package's public surface takes at least `minor`. - The two in-family precedents read it the same way. PR #20817 made `packagedBaseRefusal` public, and PR #20853 made `tenantAuthoredWriteRefusal` public. Both were declared `Clause-②: yes (widening)` with `@objectstack/metadata-protocol` at `minor`. - No wire shape moves. No request key, response key or accept set changes. `rest` and `runtime` stay `patch`: `MetaDomainProtocol` is not exported from the runtime package entry. If the seat rules this `no`, the revert is two lines: this body's second line, and the changeset's `minor` back to `patch` with its own Clause line. ## Reproduction Showcase composition on a database file, cold boot, signed-in admin. The stored rows were written on a first boot and read on the second. The base is `63d1a7c378`. | case | door | before | after | |:---|:---|:---|:---| | shipped flow name, stored row | REST | `200`, the stored body | `200`, the loader's body | | shipped flow name, stored row | dispatcher twin | `200`, the stored body | `200`, the loader's body | | flow name no package ships, stored row | REST and twin | `200`, the stored body | unchanged, same bytes | | `object`, published stored layer | REST and twin | `200`, the stored layer | unchanged, same bytes | "Same bytes" means the SHA-256 of the served document is equal before and after, on both doors. The dispatcher figures come from a throwaway probe that drove `HttpDispatcher` in-process over the booted kernel. The probe was deleted. ## Pins - **REST unit**, `packages/rest/src/meta-published-overlay.test.ts`: 5 new cases. They use the real protocol and the file's own engine double, with a registry that ships one flow from a package. - A shipped name with a stored row answers the loader's body, equal to the layered effective layer. - The plural type spelling reaches the same decision. - Controls: a flow name no package ships keeps its stored row. An `object`'s published stored row is served as stored, and its effective layer is shown to differ. A protocol without the predicate keeps the stored row. - **Runtime unit**, `packages/runtime/src/domains/meta-published-runtime-publish.test.ts`: the same 5 cases, through the real `HttpDispatcher`. - **Dogfood**, the new file `packages/qa/dogfood/test/flow-shipped-name-published-door.dogfood.test.ts`: 5 cases, showcase, cold boot. - A store check, plus the layered read putting the loader's body over the stored row. - The REST door answers the loader's body. - The REST door's body equals the layered effective layer. - Controls: a flow name no package ships keeps its stored body. An `object`'s published stored layer is served unchanged, and its effective layer is shown to differ. - No existing `flow-shipped-name-*.dogfood.test.ts` file is edited. Neither unit file gains an engine double, so `scripts/engine-double-contract.pinned.json` is untouched. **Why the dispatcher twin is not in the dogfood file.** The verify harness mounts no dispatcher `/meta` catch-all. That route is reached only on hosts that mount `@objectstack/hono`'s catch-all, so in this composition the twin is not served at all. Driving `HttpDispatcher` in-process from the dogfood package means importing runtime source. `check:test-source-alias` then refuses four new dist-resolved imports for the dogfood package (`metadata-protocol`, `observability`, `rest`, `service-datasource`). Its remedy is to alias them to source in the dogfood vitest config, which is outside this claim's file surface and would change every isolated dogfood test's resolution. So the twin is pinned at the unit level, with the real protocol and the real dispatcher. ## Ablation The fix was committed first. Both mutations went through `scripts/ablation-replace.mjs`, replacing the predicate clause with a constant false. Each leg is shown below. - **REST door** (`rest-server.ts`), at head `292cc60f45`: - The anchor went from 1 to 0 hits, and the blob from `a97cfde7c227` to `418bc95a799c`. - Unit (source-resolved): 2 failed (shipped name, plural spelling), 12 passed. - Rebuild of `@objectstack/rest`. Then `ablation-dist-preflight --absent` found the predicate call absent from all 6 built files. - Dogfood (dist-resolved): 2 failed (the REST door, and its equality with the effective layer), 3 passed (the store check and both controls). - Restore: blob equals HEAD `a97cfde7c227` and `git diff HEAD` is empty. After the rebuild, the preflight found the call present in 2 built files, and the tree was clean. - **Dispatcher twin** (`meta.ts`), at head `292cc60f45`: - The anchor went from 1 to 0 hits, and the blob from `0c4ddceecbb8` to `1e10bb639fc9`. - Unit (source-resolved): 2 failed (shipped name, plural spelling), 8 passed. - Restore: blob equals HEAD and `git diff HEAD` is empty. ## Verification All at head `292cc60f45` unless a line names another. - **Unit pins:** `meta-published-overlay.test.ts` 14 passed (9 existing, 5 new). `meta-published-runtime-publish.test.ts` 10 passed (5 existing, 5 new). Both at `92f242cee4`, and neither file has changed since. - **Whole packages:** - `@objectstack/rest`: 259 files, 5035 passed, 143 skipped, at `b973faeca2`. - `@objectstack/runtime` (`--project local`): 297 files, 4254 passed, 5 skipped, at `b973faeca2`. - The only later change in either package is the reworded docblock and one dropped fixture key in its unit file. Both files were re-run green after it. - `@objectstack/metadata-protocol`: 196 files passed and 3 skipped; 2930 tests passed and 19 skipped, at `92f242cee4`. - **Dogfood:** the new file, 5 passed. - **Typecheck:** `metadata-protocol`, `rest`, `runtime` (including `check:test-typecheck`) and `dogfood` all exit 0. `tsc --listFiles` counts each new or edited test file once in its own program. - **Gates:** `dispatch-gates --repo objectstack-ai/objectstack --commands` derived 68 families. All 68 were run, each exit code recorded before any pipe, and every one is 0. `--ran` reports 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. - The first pass, at `92f242cee4`, had two non-zero exits. - `check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT MET: 8 packages outside the diff had no dist. They were built (41 of 41 turbo cache hits). - `check:test-source-alias` exited 1 on the dogfood dispatcher leg, which was then removed. The reason is under Pins. - **Lint**, a proven narrowing: - Population, from eslint's own config: 6 of the 7 touched paths are linted. The changeset is ignored, with no matching configuration. - Count, from `--format json` with the `pnpm lint` flags: 6 results, 0 errors, 0 warnings. - Invariance: there is no type-aware linting. Every `parserOptions` block is `ecmaVersion` and `sourceType` only. The config reads only two baseline JSON files, and this diff touches neither. So no untouched file's verdict can move. - **Not measured locally, declared to CI:** the Test Core shards, the full Dogfood Regression Gate, Temporal Conformance, Build Core, the type-check lanes, and the runtime `repo` test project. ## Acceptance notes - **Unchanged background fact**, per the review `5924388874`: in the showcase composition, a shipped flow with no stored row answers `501` `NOT_IMPLEMENTED` on the published door. That kernel has no code/package store. This PR does not change that path. - **Not merged with `origin/main`:** 9 commits landed since the base. None of them touches the 7 files here. PR CI tests the merge ref. - **Read, not edited:** `getMetaItemLayered` and the predicate's body. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ba03198 commit 514001a

7 files changed

Lines changed: 598 additions & 3 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/rest': patch
4+
'@objectstack/runtime': patch
5+
---
6+
7+
fix(rest,runtime): the published-snapshot read of a flow name a managed package ships answers the package's flow, as the layered read does (#21002)
8+
9+
Clause-②: yes (widening)
10+
11+
`flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. Since the previous half of #21002, the layered read, `GET /api/v1/meta/flow/:name/layers`, reports the package's flow as the effective layer for a name a managed package ships, and a stored flow of that name as a separate layer that does not take effect. The published-snapshot read, `GET /api/v1/meta/:type/:name/published`, and its runtime-dispatcher twin read that same layered answer, but served its stored layer whenever one was present. So for such a name they still answered `200` with the stored flow, not the package's.
12+
13+
Both published-snapshot doors now serve the layered read's effective layer when that read put the package's flow over a stored flow, which is the package's flow. They ask the metadata protocol's own check for that decision rather than repeating it. In every other case they answer exactly as before: a flow name no managed package ships, and every other metadata type, `object` included, still answer the stored layer when one is present, and an item with no stored layer still falls through to the code/package snapshot. The stored flow is not deleted, rewritten or refused.
14+
15+
**The widening.** `@objectstack/metadata-protocol` makes one existing method public: `ObjectStackProtocolImplementation.isShippedFlowName(type, name)`. It answers whether `name` is a flow name a managed package ships. It was private to the class, so a door in another package could not ask it any other way. Its answer is unchanged, and the layered read, the by-name read and the flow list keep calling it.

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14630,8 +14630,17 @@ export class ObjectStackProtocolImplementation implements
1463014630
* tenant-authored row it is, and the automation boot pull reports it as a
1463114631
* shadowed contender. What becomes of such rows (keep, refuse, migrate) is
1463214632
* not decided by this method.
14633+
*
14634+
* [#21002] PUBLIC so the published-snapshot doors can ASK it, never
14635+
* re-derive it. {@link getMetaItemLayered} decides its effective layer with
14636+
* this predicate, and `GET /meta/:type/:name/published` (the REST route
14637+
* and its dispatcher twin) reads that layered answer: when a stored row is
14638+
* present and this predicate holds for the answer's `type` and `name`, the
14639+
* effective layer — the loader's body — is what the door serves, and in
14640+
* every other case the door serves the stored row as before. One decision
14641+
* point for the three reads; the doors hold no copy of the rule.
1463314642
*/
14634-
private isShippedFlowName(type: string, name: unknown): boolean {
14643+
isShippedFlowName(type: string, name: unknown): boolean {
1463514644
if ((PLURAL_TO_SINGULAR[type] ?? type) !== 'flow') return false;
1463614645
if (typeof name !== 'string' || name === '') return false;
1463714646
return this.packagedArtifactOwner({ type: 'flow', name }) !== undefined;
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// [#21002, #20761 ruling rule 1, ADR-0126 §2, ADR-0131 D6] For a flow name the
4+
// loader ships from a managed package, the published-snapshot door answers the
5+
// loader's body once a stored row of that name is at rest — the body the layered
6+
// read reports as effective. Over the real showcase composition, on a database
7+
// file, across a cold boot.
8+
//
9+
// ## What was broken
10+
//
11+
// Since #21002's first half the layered read decides its effective layer for a
12+
// shipped flow name with `isShippedFlowName`: the loader's body, with the stored
13+
// row reported beside it as a shadowed layer. The published doors read that same
14+
// layered answer but served its stored layer whenever one was present, so they
15+
// still answered the stored body for a sealed name — ADR-0126 §2's "never an
16+
// overlay read path", left open on one door and its twin.
17+
//
18+
// ## The ruling these cases pin (triage, scoped by the decision, not by type)
19+
//
20+
// When the predicate decided the effective layer — the loader's body over a
21+
// stored row — the doors serve that effective layer. In every other case they
22+
// serve exactly what they served before. So:
23+
//
24+
// - a shipped flow name with a stored row: the door answers the loader's
25+
// body, the same body the layered read reports as effective;
26+
// - a flow name no managed package ships, with a stored row: the door still
27+
// answers the stored body (control);
28+
// - an `object` with a published stored row, whose effective layer differs
29+
// from its stored layer by folding and governance (not by the predicate):
30+
// the door still answers the stored layer, unchanged (control).
31+
//
32+
// ## Why only the REST door is booted here
33+
//
34+
// This composition serves `/meta` through the REST route alone; the
35+
// dispatcher's `/meta` domain is reached only on hosts that mount the
36+
// dispatcher's catch-all, which this harness does not. Its twin of this door is
37+
// pinned with the real protocol and the real `HttpDispatcher` in
38+
// `packages/runtime/src/domains/meta-published-runtime-publish.test.ts`.
39+
40+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
41+
import showcaseStack from '@objectstack/example-showcase';
42+
import { bootStack, type VerifyStack } from '@objectstack/verify';
43+
import { RecordChangeTriggerPlugin } from '@objectstack/trigger-record-change';
44+
import { ConnectorRestPlugin } from '@objectstack/connector-rest';
45+
import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi';
46+
import { ConnectorMcpPlugin } from '@objectstack/connector-mcp';
47+
import { fileURLToPath } from 'node:url';
48+
import { mkdtempSync, rmSync } from 'node:fs';
49+
import { tmpdir } from 'node:os';
50+
import { join } from 'node:path';
51+
52+
/** Package-relative connector refs resolve against the cwd — see the sibling boots. */
53+
const SHOWCASE_DIR = fileURLToPath(new URL('../../../../examples/app-showcase/', import.meta.url));
54+
55+
/** The package the showcase composition loads its flows from. */
56+
const SHOWCASE_PACKAGE = 'com.example.showcase';
57+
/** A shipped flow given an environment-wide stored row of its name: the subject. */
58+
const SUBJECT = 'showcase_urgent_task_alert';
59+
/** A shipped screen flow whose body seeds the customer flow below (no trigger). */
60+
const CUSTOMER_SOURCE = 'showcase_reassign_wizard';
61+
/** A flow name no managed package ships, given an environment-wide stored row. */
62+
const CUSTOMER = 'dogfood_21002_pub_customer_flow';
63+
/** A writable base and an object published into it at runtime: the `object` control. */
64+
const OBJECT_BASE = 'app.dogfood_21002_pub';
65+
const OBJECT_NAME = 'dogfood_21002_pub_widget';
66+
67+
/** The node id and label a stored body carries, so it can be told from the loader's. */
68+
const STORED_NODE = 'stored_node_21002_pub';
69+
const STORED_LABEL = 'Stored body 21002 pub';
70+
const CUSTOMER_LABEL = 'Customer flow 21002 pub';
71+
const OBJECT_LABEL = 'Widget 21002 pub';
72+
73+
const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
74+
75+
interface FlowBody {
76+
name?: string;
77+
label?: string;
78+
nodes?: Array<{ id: string }>;
79+
}
80+
interface Layered {
81+
type?: string;
82+
name?: string;
83+
code?: Record<string, unknown> | null;
84+
overlay?: Record<string, unknown> | null;
85+
overlayScope?: 'org' | 'env' | null;
86+
effective?: Record<string, unknown> | null;
87+
}
88+
interface Engine {
89+
getFlow(name: string): Promise<FlowBody | null>;
90+
packagedFlowOwner(name: string): string | undefined;
91+
}
92+
interface Ql {
93+
insert(object: string, data: Record<string, unknown>, options?: unknown): Promise<unknown>;
94+
find(object: string, options?: unknown): Promise<Array<Record<string, unknown>>>;
95+
}
96+
interface Protocol {
97+
saveMetaItem(request: Record<string, unknown>): Promise<unknown>;
98+
}
99+
100+
const plugins = () => [
101+
new RecordChangeTriggerPlugin(),
102+
new ConnectorRestPlugin(),
103+
new ConnectorOpenApiPlugin(),
104+
new ConnectorMcpPlugin({ declarativeStdio: ['node'] }),
105+
];
106+
107+
async function boot(databaseFile: string): Promise<VerifyStack> {
108+
return bootStack(showcaseStack, { automation: true, databaseFile, extraPlugins: plugins() });
109+
}
110+
111+
const nodeIds = (flow: unknown) => ((flow as FlowBody | null | undefined)?.nodes ?? []).map((n) => n.id);
112+
const labelOf = (doc: unknown) => (doc as { label?: unknown } | null | undefined)?.label;
113+
114+
/** A copy of a loader's body with every underscore-prefixed key dropped. */
115+
function plainCopy(loader: FlowBody | null): Record<string, unknown> {
116+
const body: Record<string, unknown> = JSON.parse(JSON.stringify(loader));
117+
for (const key of Object.keys(body)) if (key.startsWith('_')) delete body[key];
118+
return body;
119+
}
120+
121+
/** The loader's body, as a distinguishable stored body: a new label, one node renamed. */
122+
function storedBodyFrom(loader: FlowBody | null): Record<string, unknown> {
123+
const body = plainCopy(loader);
124+
body.label = STORED_LABEL;
125+
const nodes = body.nodes as Array<{ id: string }>;
126+
const edges = body.edges as Array<{ source: string; target: string }>;
127+
const from = nodes[1].id;
128+
nodes[1].id = STORED_NODE;
129+
for (const edge of edges) {
130+
if (edge.source === from) edge.source = STORED_NODE;
131+
if (edge.target === from) edge.target = STORED_NODE;
132+
}
133+
return body;
134+
}
135+
136+
describe('the published-snapshot door answers the loader\'s body for a shipped flow name with a stored row, across a cold boot (showcase)', () => {
137+
let stack: VerifyStack;
138+
let token: string;
139+
let prevCwd: string;
140+
let dir: string;
141+
let dbFile: string;
142+
const loader: Record<string, FlowBody | null> = {};
143+
144+
const engine = () => stack.kernel.getServiceAsync('automation') as unknown as Promise<Engine>;
145+
const ql = () => stack.kernel.getServiceAsync('objectql') as unknown as Promise<Ql>;
146+
147+
/** `GET /meta/:type/:name/published` on the REST route — the served document itself. */
148+
const restPublished = async (type: string, name: string) => {
149+
const res = await stack.apiAs(token, 'GET', `/meta/${type}/${name}/published`);
150+
const body: unknown = await res.json().catch(() => ({}));
151+
return { status: res.status, doc: body as Record<string, unknown> };
152+
};
153+
/** `GET /meta/:type/:name/layers` — the three-layer answer the door reads. */
154+
const layers = async (type: string, name: string) => {
155+
const res = await stack.apiAs(token, 'GET', `/meta/${type}/${name}/layers`);
156+
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
157+
return { status: res.status, doc: (body?.data ?? body) as Layered };
158+
};
159+
160+
beforeAll(async () => {
161+
prevCwd = process.cwd();
162+
process.chdir(SHOWCASE_DIR);
163+
dir = mkdtempSync(join(tmpdir(), 'dogfood-21002-pub-'));
164+
dbFile = join(dir, 'showcase.db');
165+
166+
// First boot: read the loader's bodies, put the flow rows in the store,
167+
// and publish the control object through the runtime authoring door.
168+
stack = await boot(dbFile);
169+
const first = await engine();
170+
const store = await ql();
171+
for (const name of [SUBJECT, CUSTOMER_SOURCE]) loader[name] = await first.getFlow(name);
172+
173+
const now = new Date().toISOString();
174+
const row = (name: string, body: Record<string, unknown>) => ({
175+
type: 'flow',
176+
name,
177+
organization_id: null,
178+
package_id: null,
179+
state: 'active',
180+
version: 1,
181+
checksum: null,
182+
created_at: now,
183+
updated_at: now,
184+
metadata: JSON.stringify(body),
185+
});
186+
const customer = { ...plainCopy(loader[CUSTOMER_SOURCE]), name: CUSTOMER, label: CUSTOMER_LABEL };
187+
for (const data of [row(SUBJECT, storedBodyFrom(loader[SUBJECT])), row(CUSTOMER, customer)]) {
188+
await store.insert('sys_metadata', data, { context: SYSTEM_CTX });
189+
}
190+
191+
const protocol = await stack.kernel.getServiceAsync<Protocol>('protocol');
192+
const objectBody = {
193+
name: OBJECT_NAME,
194+
label: OBJECT_LABEL,
195+
sharingModel: 'private',
196+
fields: { title: { type: 'text', label: 'Title' } },
197+
};
198+
await protocol.saveMetaItem({ type: 'object', name: OBJECT_NAME, item: objectBody, packageId: OBJECT_BASE, mode: 'draft' });
199+
await protocol.saveMetaItem({ type: 'object', name: OBJECT_NAME, item: objectBody, packageId: OBJECT_BASE, mode: 'publish' });
200+
await stack.stop();
201+
202+
// The measured boot: cold, on the same file.
203+
stack = await boot(dbFile);
204+
token = await stack.signIn();
205+
}, 360_000);
206+
207+
afterAll(async () => {
208+
await stack?.stop();
209+
if (prevCwd) process.chdir(prevCwd);
210+
if (dir) rmSync(dir, { recursive: true, force: true });
211+
});
212+
213+
it('the store holds the rows the second boot read, and the layered read puts the loader\'s body over the stored row', async () => {
214+
const rows = await (await ql()).find('sys_metadata', {
215+
where: { state: 'active' },
216+
context: SYSTEM_CTX,
217+
});
218+
const keys = rows.map((r) => `${String(r.type)}/${String(r.name)}@${String(r.organization_id ?? '')}`);
219+
expect(keys).toContain(`flow/${SUBJECT}@`);
220+
expect(keys).toContain(`flow/${CUSTOMER}@`);
221+
expect(keys).toContain(`object/${OBJECT_NAME}@`);
222+
expect((await engine()).packagedFlowOwner(SUBJECT)).toBe(SHOWCASE_PACKAGE);
223+
expect(labelOf(loader[SUBJECT])).not.toBe(STORED_LABEL);
224+
225+
// The decision the doors follow: a stored layer is present, and the
226+
// effective layer is the loader's body, not that stored layer.
227+
const read = await layers('flow', SUBJECT);
228+
expect(read.status).toBe(200);
229+
expect(nodeIds(read.doc.overlay)).toContain(STORED_NODE);
230+
expect(nodeIds(read.doc.effective)).toEqual(nodeIds(loader[SUBJECT]));
231+
});
232+
233+
it('the REST published door answers the loader\'s body for a shipped flow name with a stored row', async () => {
234+
const read = await restPublished('flow', SUBJECT);
235+
236+
expect(read.status).toBe(200);
237+
expect(labelOf(read.doc)).toBe(labelOf(loader[SUBJECT]));
238+
expect(nodeIds(read.doc)).toEqual(nodeIds(loader[SUBJECT]));
239+
expect(nodeIds(read.doc)).not.toContain(STORED_NODE);
240+
});
241+
242+
it('the published door serves the body the layered read reports as effective', async () => {
243+
const layered = await layers('flow', SUBJECT);
244+
const rest = await restPublished('flow', SUBJECT);
245+
246+
expect(rest.doc).toEqual(layered.doc.effective);
247+
});
248+
249+
it('control: a flow name no managed package ships keeps its stored body on the published door', async () => {
250+
const layered = await layers('flow', CUSTOMER);
251+
const rest = await restPublished('flow', CUSTOMER);
252+
253+
expect(layered.doc.overlayScope).toBe('env');
254+
expect(rest.status).toBe(200);
255+
expect(labelOf(rest.doc)).toBe(CUSTOMER_LABEL);
256+
expect(rest.doc).toEqual(layered.doc.overlay);
257+
});
258+
259+
it('control: an object\'s published stored layer is served unchanged, not its effective layer', async () => {
260+
const layered = await layers('object', OBJECT_NAME);
261+
const rest = await restPublished('object', OBJECT_NAME);
262+
263+
expect(layered.status).toBe(200);
264+
// The control discriminates: this object's effective layer is NOT its
265+
// stored layer, so a door that served the effective layer would fail here.
266+
expect(layered.doc.overlay).not.toBeNull();
267+
expect(layered.doc.effective).not.toEqual(layered.doc.overlay);
268+
269+
expect(rest.status).toBe(200);
270+
expect(labelOf(rest.doc)).toBe(OBJECT_LABEL);
271+
expect(rest.doc).toEqual(layered.doc.overlay);
272+
});
273+
});

0 commit comments

Comments
 (0)