Skip to content

Commit 5cdb27e

Browse files
committed
test(dogfood): the layered, by-name and list reads of a shipped flow name agree across a cold boot
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
1 parent d690943 commit 5cdb27e

1 file changed

Lines changed: 308 additions & 0 deletions

File tree

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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 LAYERED read reports the loader's
5+
// body as the effective layer — the body the by-name read and the list answer
6+
// for that name — and a stored row of that name as a shadowed layer, never as
7+
// the effective layer under the package's lock and provenance flags. Over the
8+
// real showcase composition, on a database file.
9+
//
10+
// ## What was broken
11+
//
12+
// Since #20913 the flattened flow view (the list door and the execution view)
13+
// serves a shipped name from the loader's entries alone, and since #20946 the
14+
// by-name read does too. The layered read did not: its effective layer was
15+
// "overlay wins", so `GET /meta/flow/NAME/layers` answered the stored body as
16+
// the effective definition while its lock and provenance flags named the
17+
// package — and its own docblock promises the effective layer is "what
18+
// `getMetaItem` would return". Three read doors, two answers about one name.
19+
//
20+
// ## Why a booted stack, over two boots
21+
//
22+
// The unit pins (`protocol.flow-layered-shipped-name.test.ts`) hold the read's
23+
// branches with doubles. What they cannot answer is the composition: that the
24+
// layered door's real sources — the stored row, the metadata service and the
25+
// registry the boot hydration filled — resolve the effective layer to the
26+
// loader's body once the row is at rest. A stored row is only read back by a
27+
// cold boot, so the stack is booted twice on one file.
28+
//
29+
// ## What each case pins (triage's pins, plus the controls)
30+
//
31+
// - the layered read of a shipped name with a stored row: the effective layer
32+
// is the loader's body, and the stored row is still reported, as a shadowed
33+
// layer of its own scope, under the package's flags;
34+
// - the layered read, the by-name read and the list agree on that body;
35+
// - the deprecated layers flag on the by-name door answers the same;
36+
// - a shipped name with no stored row is unchanged (control);
37+
// - an organization-scoped row stays out of reach (control);
38+
// - a name no managed package ships keeps its stored row as the effective
39+
// layer (control).
40+
//
41+
// Not pinned here: the published-snapshot door. It reads the layered answer but
42+
// picks its layer by itself, so it does not follow the effective layer; what it
43+
// answers for a shipped name is left to the card's decision.
44+
45+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
46+
import showcaseStack from '@objectstack/example-showcase';
47+
import { bootStack, type VerifyStack } from '@objectstack/verify';
48+
import { RecordChangeTriggerPlugin } from '@objectstack/trigger-record-change';
49+
import { ConnectorRestPlugin } from '@objectstack/connector-rest';
50+
import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi';
51+
import { ConnectorMcpPlugin } from '@objectstack/connector-mcp';
52+
import { fileURLToPath } from 'node:url';
53+
import { mkdtempSync, rmSync } from 'node:fs';
54+
import { tmpdir } from 'node:os';
55+
import { join } from 'node:path';
56+
57+
/** Package-relative connector refs resolve against the cwd — see the sibling boots. */
58+
const SHOWCASE_DIR = fileURLToPath(new URL('../../../../examples/app-showcase/', import.meta.url));
59+
60+
/** The package the showcase composition loads its flows from. */
61+
const SHOWCASE_PACKAGE = 'com.example.showcase';
62+
/** A shipped flow given an environment-wide stored row of its name: the subject. */
63+
const SUBJECT = 'showcase_urgent_task_alert';
64+
/** A shipped flow given no stored row at all: the control. */
65+
const CONTROL = 'showcase_task_completed';
66+
/** A shipped flow given an ORGANIZATION-scoped row only. */
67+
const ORG_SUBJECT = 'showcase_task_assigned_notify';
68+
const ORG_ID = 'org_dogfood_21002';
69+
/** A shipped screen flow whose body seeds the customer flow below (no trigger). */
70+
const CUSTOMER_SOURCE = 'showcase_reassign_wizard';
71+
/** A flow name no managed package ships, given an environment-wide stored row. */
72+
const CUSTOMER = 'dogfood_21002_customer_flow';
73+
74+
/** The node id and label a stored body carries, so it can be told from the loader's. */
75+
const STORED_NODE = 'stored_node_21002';
76+
const STORED_LABEL = 'Stored body 21002';
77+
const CUSTOMER_LABEL = 'Customer flow 21002';
78+
79+
const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
80+
81+
interface FlowBody {
82+
name?: string;
83+
label?: string;
84+
nodes?: Array<{ id: string }>;
85+
}
86+
interface Layered {
87+
code?: FlowBody | null;
88+
overlay?: FlowBody | null;
89+
overlayScope?: 'org' | 'env' | null;
90+
effective?: FlowBody | null;
91+
provenance?: string;
92+
packageId?: string;
93+
lock?: string;
94+
}
95+
interface Engine {
96+
getFlow(name: string): Promise<FlowBody | null>;
97+
packagedFlowOwner(name: string): string | undefined;
98+
}
99+
interface Ql {
100+
insert(object: string, data: Record<string, unknown>, options?: unknown): Promise<unknown>;
101+
find(object: string, options?: unknown): Promise<Array<Record<string, unknown>>>;
102+
}
103+
104+
const plugins = () => [
105+
new RecordChangeTriggerPlugin(),
106+
new ConnectorRestPlugin(),
107+
new ConnectorOpenApiPlugin(),
108+
new ConnectorMcpPlugin({ declarativeStdio: ['node'] }),
109+
];
110+
111+
async function boot(databaseFile: string): Promise<VerifyStack> {
112+
return bootStack(showcaseStack, { automation: true, databaseFile, extraPlugins: plugins() });
113+
}
114+
115+
const nodeIds = (flow: FlowBody | null | undefined) => (flow?.nodes ?? []).map((n) => n.id);
116+
117+
/** A copy of a loader's body with every underscore-prefixed key dropped. */
118+
function plainCopy(loader: FlowBody | null): Record<string, unknown> {
119+
const body: Record<string, unknown> = JSON.parse(JSON.stringify(loader));
120+
for (const key of Object.keys(body)) if (key.startsWith('_')) delete body[key];
121+
return body;
122+
}
123+
124+
/** The loader's body, as a distinguishable stored body: a new label, one node renamed. */
125+
function storedBodyFrom(loader: FlowBody | null): Record<string, unknown> {
126+
const body = plainCopy(loader);
127+
body.label = STORED_LABEL;
128+
const nodes = body.nodes as Array<{ id: string }>;
129+
const edges = body.edges as Array<{ source: string; target: string }>;
130+
const from = nodes[1].id;
131+
nodes[1].id = STORED_NODE;
132+
for (const edge of edges) {
133+
if (edge.source === from) edge.source = STORED_NODE;
134+
if (edge.target === from) edge.target = STORED_NODE;
135+
}
136+
return body;
137+
}
138+
139+
describe('the layered flow read agrees with the by-name read and the list for a shipped name across a cold boot (showcase)', () => {
140+
let stack: VerifyStack;
141+
let token: string;
142+
let prevCwd: string;
143+
let dir: string;
144+
let dbFile: string;
145+
const loader: Record<string, FlowBody | null> = {};
146+
147+
const engine = () => stack.kernel.getServiceAsync('automation') as unknown as Promise<Engine>;
148+
const ql = () => stack.kernel.getServiceAsync('objectql') as unknown as Promise<Ql>;
149+
150+
const call = async (path: string) => {
151+
const res = await stack.apiAs(token, 'GET', path);
152+
const json: unknown = await res.json().catch(() => ({}));
153+
return { status: res.status, json };
154+
};
155+
const unwrap = (json: unknown) => {
156+
const body = json as Record<string, unknown>;
157+
return (body?.data ?? body) as Record<string, unknown>;
158+
};
159+
/** `GET /meta/flow/:name/layers` — the three-layer diagnostic. */
160+
const layers = async (name: string, path = `/meta/flow/${name}/layers`) => {
161+
const read = await call(path);
162+
return { status: read.status, doc: unwrap(read.json) as Layered };
163+
};
164+
/** `GET /meta/flow/:name` — the served document, unwrapped from whichever envelope carries it. */
165+
const byName = async (name: string) => {
166+
const read = await call(`/meta/flow/${name}`);
167+
const doc = unwrap(read.json);
168+
return { status: read.status, doc: (doc?.item ?? doc) as FlowBody };
169+
};
170+
/** `GET /meta/flow` — every served entry of one name. */
171+
const listed = async (name: string) => {
172+
const read = await call('/meta/flow');
173+
const data = unwrap(read.json) as Record<string, unknown> | unknown[];
174+
const items = (Array.isArray(data) ? data : (data as { items?: unknown[] })?.items ?? []) as FlowBody[];
175+
return { status: read.status, entries: items.filter((it) => it?.name === name) };
176+
};
177+
178+
beforeAll(async () => {
179+
prevCwd = process.cwd();
180+
process.chdir(SHOWCASE_DIR);
181+
dir = mkdtempSync(join(tmpdir(), 'dogfood-21002-'));
182+
dbFile = join(dir, 'showcase.db');
183+
184+
// First boot: read the loader's bodies, then put rows in the store.
185+
stack = await boot(dbFile);
186+
const first = await engine();
187+
const store = await ql();
188+
for (const name of [SUBJECT, CONTROL, ORG_SUBJECT, CUSTOMER_SOURCE]) loader[name] = await first.getFlow(name);
189+
190+
const now = new Date().toISOString();
191+
const row = (name: string, organizationId: string | null, body: Record<string, unknown>) => ({
192+
type: 'flow',
193+
name,
194+
organization_id: organizationId,
195+
package_id: null,
196+
state: 'active',
197+
version: 1,
198+
checksum: null,
199+
created_at: now,
200+
updated_at: now,
201+
metadata: JSON.stringify(body),
202+
});
203+
const customer = { ...plainCopy(loader[CUSTOMER_SOURCE]), name: CUSTOMER, label: CUSTOMER_LABEL };
204+
for (const data of [
205+
row(SUBJECT, null, storedBodyFrom(loader[SUBJECT])),
206+
row(ORG_SUBJECT, ORG_ID, storedBodyFrom(loader[ORG_SUBJECT])),
207+
row(CUSTOMER, null, customer),
208+
]) {
209+
await store.insert('sys_metadata', data, { context: SYSTEM_CTX });
210+
}
211+
await stack.stop();
212+
213+
// The measured boot: cold, on the same file.
214+
stack = await boot(dbFile);
215+
token = await stack.signIn();
216+
}, 360_000);
217+
218+
afterAll(async () => {
219+
await stack?.stop();
220+
if (prevCwd) process.chdir(prevCwd);
221+
if (dir) rmSync(dir, { recursive: true, force: true });
222+
});
223+
224+
it('the store holds the rows the second boot read, and the loader still ships the subject', async () => {
225+
const rows = await (await ql()).find('sys_metadata', {
226+
where: { type: 'flow', state: 'active' },
227+
context: SYSTEM_CTX,
228+
});
229+
const names = rows.map((r) => `${String(r.name)}@${String(r.organization_id ?? '')}`);
230+
expect(names).toContain(`${SUBJECT}@`);
231+
expect(names).toContain(`${ORG_SUBJECT}@${ORG_ID}`);
232+
expect(names).toContain(`${CUSTOMER}@`);
233+
expect((await engine()).packagedFlowOwner(SUBJECT)).toBe(SHOWCASE_PACKAGE);
234+
expect(loader[SUBJECT]?.label).not.toBe(STORED_LABEL);
235+
});
236+
237+
it('the layered door reports the loader\'s body as the effective layer of a shipped name with a stored row, under the package\'s flags', async () => {
238+
const read = await layers(SUBJECT);
239+
240+
expect(read.status).toBe(200);
241+
expect(read.doc.effective?.label).toBe(loader[SUBJECT]?.label);
242+
expect(nodeIds(read.doc.effective)).toEqual(nodeIds(loader[SUBJECT]));
243+
expect(nodeIds(read.doc.effective)).not.toContain(STORED_NODE);
244+
expect(nodeIds(read.doc.code)).toEqual(nodeIds(loader[SUBJECT]));
245+
expect(read.doc.packageId).toBe(SHOWCASE_PACKAGE);
246+
expect(read.doc.provenance).toBe('package');
247+
});
248+
249+
it('the stored row is still reported, as a shadowed layer of its own scope', async () => {
250+
const read = await layers(SUBJECT);
251+
252+
expect(read.doc.overlay?.label).toBe(STORED_LABEL);
253+
expect(nodeIds(read.doc.overlay)).toContain(STORED_NODE);
254+
expect(read.doc.overlayScope).toBe('env');
255+
});
256+
257+
it('the layered door, the by-name read and the list answer one and the same body', async () => {
258+
const layered = await layers(SUBJECT);
259+
const read = await byName(SUBJECT);
260+
const list = await listed(SUBJECT);
261+
262+
expect(read.status).toBe(200);
263+
expect(list.status).toBe(200);
264+
expect(list.entries).toHaveLength(1);
265+
expect(layered.doc.effective?.label).toBe(read.doc.label);
266+
expect(list.entries[0].label).toBe(read.doc.label);
267+
expect(nodeIds(layered.doc.effective)).toEqual(nodeIds(read.doc));
268+
expect(nodeIds(list.entries[0])).toEqual(nodeIds(read.doc));
269+
});
270+
271+
it('the deprecated layers flag on the by-name door answers the same layers', async () => {
272+
const read = await layers(SUBJECT, `/meta/flow/${SUBJECT}?layers=true`);
273+
274+
expect(read.status).toBe(200);
275+
expect(nodeIds(read.doc.effective)).toEqual(nodeIds(loader[SUBJECT]));
276+
expect(nodeIds(read.doc.overlay)).toContain(STORED_NODE);
277+
});
278+
279+
it('control: a shipped name with no stored row is unchanged', async () => {
280+
const read = await layers(CONTROL);
281+
282+
expect(read.status).toBe(200);
283+
expect(read.doc.overlay).toBeNull();
284+
expect(read.doc.overlayScope).toBeNull();
285+
expect(nodeIds(read.doc.effective)).toEqual(nodeIds(loader[CONTROL]));
286+
});
287+
288+
it('control: an organization-scoped row stays out of the layered door\'s reach', async () => {
289+
const read = await layers(ORG_SUBJECT);
290+
291+
expect(read.status).toBe(200);
292+
expect(read.doc.overlay).toBeNull();
293+
expect(read.doc.overlayScope).toBeNull();
294+
expect(read.doc.effective?.label).toBe(loader[ORG_SUBJECT]?.label);
295+
expect(nodeIds(read.doc.effective)).toEqual(nodeIds(loader[ORG_SUBJECT]));
296+
});
297+
298+
it('control: a flow name no managed package ships keeps its stored row as the effective layer', async () => {
299+
const read = await layers(CUSTOMER);
300+
const byNameRead = await byName(CUSTOMER);
301+
302+
expect(read.status).toBe(200);
303+
expect(read.doc.effective?.label).toBe(CUSTOMER_LABEL);
304+
expect(read.doc.overlay?.label).toBe(CUSTOMER_LABEL);
305+
expect(read.doc.overlayScope).toBe('env');
306+
expect(byNameRead.doc.label).toBe(CUSTOMER_LABEL);
307+
});
308+
});

0 commit comments

Comments
 (0)