Skip to content

Commit 33ee9aa

Browse files
fix(runtime): the base export serves the base's own document while a .custom override of it is live (#29687)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8HidkhJzbWECJnYCJR3RqC2)_ Fixes #29649 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`GET /packages/BASE/export` 在 `.custom` 覆盖已发布时,改为输出基础包自己的那份文档(原样),不再把该项丢掉。 - 为什么改:真实 showcase 启动下,发布一个仪表盘/报表覆盖后,基础包导出少了这一项(#29597 的过滤器引入);#29640 合入后仪表盘/报表已好,但页面、应用、数据集、文档、书籍的覆盖仍会让该项从导出中消失。 - 风险与回滚:只动导出读取一个函数,只在某项声明覆盖本包时生效;回滚即还原 `packages.ts` 这一处。 - 先看:`packages/runtime/src/domains/packages.ts`(`ownDocumentsOf`)、新 pin `packages-export-base-showcase-boot.integration.test.ts`。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-export-base-showcase-boot.integration.test.ts`,预期 8/8 通过。 - 需要你做的:无,席位落地。 ## What changes - `assemblePackageManifest` (`packages/runtime/src/domains/packages.ts`): the per-package layer's items now go through `ownDocumentsOf`. An item that declares it overrides the exported package (the pair's override) is still never exported under the base, as #29563 ruled, but it is now **replaced, in its place, by the base's own code artifact** of the name (`registry.getArtifactItem(type, name, BASE)`, the prefer-local composite slot `registerApp` filled) when the layer lists no own item beside it. Before, it was only dropped, which left the item out when the layer held the override alone. - New pin `packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts`: the #29563 harness (real `HttpDispatcher`, protocol, `ObjectQL` on SQLite) plus the real `MetadataManager` holding the base's code documents as `MetadataPlugin`'s artifact door stores them, which is the shape every `defineStack` host boots with. Per type (view, dashboard, report, page, app, dataset, doc, book) it asserts the base's whole export is byte-identical before and after the published override, and after a restart over the same database; the item is present and equal; no item carries `overrides`; the pair's row is published; and for view, dashboard and report the effective read and the pair's export serve the override. - Patch changeset for `@objectstack/runtime`. ## Why ADR-0146 D8 item 3: `GET /packages/hotcrm/export` "carries the base unchanged". The per-package layer merges the registry's items over the metadata service's, one per `(name, package)` slot. A published `.custom` override registered in the registry's bare slot wearing the base's envelope therefore takes the base's slot, and the base's own item is not listed at all. The #29563 filter then left nothing. The #29563 pin stayed green because its boot has no metadata service, so the layer listed both documents. #29600 (PR #29640, merged as 7dae3b3 while this card ran) takes the dashboard, report and email_template copy out of the bare slot, which closes this card's dashboard and report rows on its own. It deliberately leaves page, app, dataset, doc and book in the bare slot (#29613), and for those five the base export still loses the item. This fix holds the D8 item 3 guarantee for every type at the export door, whatever the registry holds. ## Tests Measured on a real showcase boot (`pnpm --filter @objectstack/example-showcase dev -- --fresh`), with the card's reproduction (ensure-customization, draft PUT with `overrides`, `publish-drafts`), counting items in `GET /api/v1/packages/com.example.showcase/export`: | tree | dashboard | report | view | page | app | dataset | doc | book | whole base export | |:--|:--|:--|:--|:--|:--|:--|:--|:--|:--| | main b391feb | 3 → 2 absent | 4 → 3 absent | 36 → 36 equal | not measured | not measured | not measured | not measured | not measured | differs | | main 7dae3b3 (#29640, no fix) | 3 → 3 equal | 4 → 4 equal | 36 → 36 equal | 29 → 28 absent | 1 → 0 absent | 4 → 3 absent | 8 → 7 absent | 1 → 0 absent | differs | | this branch (fix on b391feb) | 3 → 3 equal | 4 → 4 equal | 36 → 36 equal | — | — | — | — | — | identical, also after a restart (persistent OS_HOME) | | this branch (fix + 7dae3b3) | 3 → 3 equal | 4 → 4 equal | 36 → 36 equal | 29 → 29 equal | 1 → 1 equal | 4 → 4 equal | 8 → 8 equal | 1 → 1 equal | identical | Pin, red first: on b391feb (pin alone) dashboard and report red ("expected [] to deeply equal" the shipped item), view green; on 7dae3b3 (pin alone) page, app, dataset, doc and book red the same way, the other three green; with the fix 8/8 green. The export-read neighbours stay green: `packages-export-base-unchanged-per-type.integration.test.ts`, `packages-export-override-declaration.test.ts`, `packages-export-per-package-layer.test.ts` (4 files, 12/12 before the pin grew to 8 types). ## Acceptance notes - On the same real boot, a published page, app, dataset, doc or book override is not served by the effective read at all: `GET /api/v1/meta/page/showcase_start_here` (with or without `?package=com.example.showcase`) answers the shipped label, and the pair's export lists none of them. #29613 measured `getMetaItem` serving these overrides from the bare slot on a harness with no metadata service; on a real boot the metadata service's copy wins. This is #29613's row-served path, reported there, not touched here. - Studio's `studio-authoring.package-customize-restore-display` acceptance[4] re-run belongs to the QA seat; the card's own reproduction passes on a real boot (table above). ## Verification <details><summary>Local gates</summary> Head 51c4d1e (origin/main 7dae3b3 merged in). `node scripts/pm/dispatch-gates.mjs --commands` derived 59 families; each run with its exit code captured before any pipe, then `--ran`: - 58 exit 0. - NOT MEASURED (exit 3, prerequisite): `node scripts/check-plugin-teardown-shape.mjs --self-test`, its positive control is pinned to a commit this shallow clone cannot reach. - `--ran` reconciliation: "59 derived famil(ies) accounted for, 58 run, 1 NOT-MEASURED". - An earlier run on e373cc1 caught `check:query-options-erasure` (exit 1, the pin's `as any` driver query grew the test surface 236 → 237); fixed in 51c4d1e, now exit 0 at 236. Outside the derivation, also run: `pnpm lint` exit 0 (e373cc1; the later commit only drops an `as any` in the pin); `pnpm --filter @objectstack/runtime typecheck` exit 0 (51c4d1e); `pnpm --filter @objectstack/runtime test` exit 0 on e373cc1, Test Files 530 passed (530), Tests 8520 passed, 19 skipped; the new pin alone 8/8 on 51c4d1e. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbac7S1AsbGi5a46PrB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2ea6042 commit 33ee9aa

3 files changed

Lines changed: 384 additions & 1 deletion

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
Clause-②: no
6+
Metadata-protocol: none
7+
8+
`GET /packages/<base>/export` carries the base's own document again while its `.custom` pair's override of an item is live (measured for dashboard, report, page, app, dataset, doc and book) (ADR-0146 D8 item 3). On a boot that also holds the base's code documents in the metadata service, as every `defineStack` host does, the per-package layer listed only the override copy under the base's name, and the export's override filter (#29563) left the item out of the base's export. The export now serves the base's code artifact in that item's place, so the base's export is byte-identical with or without a published override, and still carries no `overrides` block.
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#29649, ADR-0146 D8 item 3] "`GET /packages/hotcrm/export` carries the base
5+
* unchanged" while its `.custom` pair's override of an item is live — on a base
6+
* booted as the showcase boots, where the base's code documents are held twice:
7+
* by the registry (`ObjectQL.registerApp`) AND by the metadata service
8+
* (`MetadataPlugin`'s artifact door, which stamps each body with the base's
9+
* envelope before it stores it).
10+
*
11+
* That second holder is what the #29563 pin
12+
* (`packages-export-base-unchanged-per-type.integration.test.ts`) does not
13+
* register, and why it stayed green while a real showcase boot lost the item:
14+
* the per-package layer merges the registry's items over the metadata
15+
* service's, one per `(name, package)` slot, and the published override is
16+
* registered in the registry's bare slot wearing the base's envelope (#29600).
17+
* So the base's slot of the name holds the override alone, and the export's
18+
* "never an override of THIS package" filter (#29563) left nothing in it.
19+
* Measured on a real showcase boot at b391feb1: dashboards 3 → 2, reports
20+
* 4 → 3, views 36 → 36. #29640 (7dae3b3c) took the dashboard and report copy
21+
* out of the bare slot; on a real showcase boot at 7dae3b3c a published page,
22+
* app, dataset, doc or book override still dropped the item (pages 29 → 28,
23+
* apps 1 → 0, datasets 4 → 3, docs 8 → 7, books 1 → 0).
24+
*
25+
* Per type (view, dashboard, report, and the five types #29640 left in the
26+
* registry's bare slot: page, app, dataset, doc, book), over the real door
27+
* (`HttpDispatcher.handlePackages`), the real protocol, the real `ObjectQL`
28+
* engine on a SQLite file and the real `MetadataManager`:
29+
*
30+
* - the base's export after the override is published is byte-identical to
31+
* the base's export before it, so the item is present and equal, and no
32+
* item in it carries an `overrides` block;
33+
* - the same holds on a restart over the same database;
34+
* - the pair's export carries the overriding item with its `overrides` block
35+
* (view, dashboard, report; see `pairServed`);
36+
* - CONTROL: the pair's override row is published, and for view, dashboard
37+
* and report the effective read of the base's name serves it, so the base
38+
* export's answer is the export's own.
39+
*/
40+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
41+
import { mkdtempSync, rmSync } from 'node:fs';
42+
import { tmpdir } from 'node:os';
43+
import { join } from 'node:path';
44+
import { ObjectQL } from '@objectstack/objectql';
45+
import { SqlDriver } from '@objectstack/driver-sql';
46+
import { MetadataManager } from '@objectstack/metadata';
47+
import { ObjectStackProtocolImplementation, resetEnvWritableMetadataTypes } from '@objectstack/metadata-protocol';
48+
import {
49+
SysMetadataAuditObject,
50+
SysMetadataCommitObject,
51+
SysMetadataHistoryObject,
52+
SysMetadataObject,
53+
} from '@objectstack/metadata-core';
54+
import { applyProtection } from '@objectstack/spec/shared';
55+
import { expandViewContainer } from '@objectstack/spec/ui';
56+
import { HttpDispatcher } from '../http-dispatcher.js';
57+
58+
const BASE = 'com.example.showcase';
59+
const BASE_VERSION = '0.1.0';
60+
const PAIR = 'com.example.showcase.custom';
61+
const PLATFORM_PKG = 'com.objectstack.platform';
62+
const DECLARATION = { package: BASE, basedOn: BASE_VERSION } as const;
63+
64+
const CALLER: any = {
65+
request: {},
66+
environmentId: 'platform',
67+
executionContext: { userId: 'u_admin', isSystem: false, systemPermissions: ['manage_metadata', 'studio.access'] },
68+
};
69+
70+
const OBJECT = 'showcase_task';
71+
const VIEW = `${OBJECT}.in_progress`;
72+
const DASHBOARD = 'showcase_ops_dashboard';
73+
const REPORT = 'showcase_hours_by_status';
74+
const OVERRIDE_LABEL = 'Edited label';
75+
76+
const COLUMNS = [{ field: 'title' }, { field: 'status' }];
77+
const viewContainer = () => ({
78+
object: OBJECT,
79+
list: { label: 'All Tasks', type: 'grid', data: { provider: 'object', object: OBJECT }, columns: COLUMNS },
80+
listViews: {
81+
in_progress: { label: 'In Progress', type: 'grid', data: { provider: 'object', object: OBJECT }, columns: COLUMNS },
82+
},
83+
});
84+
const dashboards = () => [
85+
{ name: 'showcase_chart_gallery', label: 'Chart Gallery', columns: 12, widgets: [] },
86+
{ name: DASHBOARD, label: 'Delivery Operations', columns: 12, widgets: [] },
87+
{ name: 'showcase_revenue_pulse', label: 'Revenue Pulse', columns: 12, widgets: [] },
88+
];
89+
const report = (name: string, label: string) => ({
90+
name,
91+
label,
92+
type: 'summary',
93+
dataset: 'showcase_task_metrics',
94+
rows: ['status'],
95+
values: ['task_count'],
96+
});
97+
const reports = () => [
98+
report(REPORT, 'Hours by Status'),
99+
report('showcase_task_overview', 'Task Overview'),
100+
];
101+
102+
/**
103+
* The base's collections the metadata service holds besides its views, as the
104+
* artifact door files them: plural collection → metadata type. Every one is a
105+
* type a `.custom` pair can publish an override of.
106+
*/
107+
const SERVICE_COLLECTIONS = {
108+
apps: 'app',
109+
pages: 'page',
110+
datasets: 'dataset',
111+
dashboards: 'dashboard',
112+
reports: 'report',
113+
docs: 'doc',
114+
books: 'book',
115+
} as const;
116+
117+
/**
118+
* `pairServed`: whether the pair's export and the effective read serve the
119+
* override. For the five types #29640 left in the bare slot neither does on
120+
* this boot shape (the pair's per-package layer lists none of them, and the
121+
* effective read serves the base's document; #29613), so only the base's half
122+
* is pinned for them.
123+
*/
124+
const TYPES = [
125+
{ type: 'view', plural: 'viewItems', name: VIEW, pairServed: true },
126+
{ type: 'dashboard', plural: 'dashboards', name: DASHBOARD, pairServed: true },
127+
{ type: 'report', plural: 'reports', name: REPORT, pairServed: true },
128+
{ type: 'page', plural: 'pages', name: 'showcase_start_here', pairServed: false },
129+
{ type: 'app', plural: 'apps', name: 'showcase_app', pairServed: false },
130+
{ type: 'dataset', plural: 'datasets', name: 'showcase_task_metrics', pairServed: false },
131+
{ type: 'doc', plural: 'docs', name: 'showcase_index', pairServed: false },
132+
{ type: 'book', plural: 'books', name: 'showcase_manual', pairServed: false },
133+
] as const;
134+
135+
/** The base's body, as the showcase's `defineStack` carries it: one fresh copy per reader. */
136+
function baseBody(): Record<string, any> {
137+
return {
138+
id: BASE,
139+
name: 'showcase',
140+
version: BASE_VERSION,
141+
namespace: 'showcase',
142+
objects: [{
143+
name: OBJECT,
144+
label: 'Task',
145+
sharingModel: 'private',
146+
fields: {
147+
title: { name: 'title', label: 'Title', type: 'text' },
148+
status: { name: 'status', label: 'Status', type: 'text' },
149+
},
150+
}],
151+
datasets: [{
152+
name: 'showcase_task_metrics',
153+
label: 'Task Metrics',
154+
object: OBJECT,
155+
dimensions: [{ name: 'status', label: 'Status', field: 'status', type: 'string' }],
156+
measures: [{ name: 'task_count', label: 'Tasks', aggregate: 'count' }],
157+
}],
158+
apps: [{ name: 'showcase_app', label: 'Showcase', navigation: [] }],
159+
pages: [
160+
{ name: 'showcase_capability_map', label: 'Capability Map', type: 'app', regions: [] },
161+
{ name: 'showcase_start_here', label: 'Start Here', type: 'app', regions: [] },
162+
],
163+
views: [viewContainer()],
164+
dashboards: dashboards(),
165+
reports: reports(),
166+
docs: [
167+
{ name: 'showcase_docs_guide', label: 'Docs Guide', content: '# Docs guide' },
168+
{ name: 'showcase_index', label: 'Showcase', content: '# Showcase' },
169+
],
170+
books: [{ name: 'showcase_manual', label: 'Showcase Manual', groups: [] }],
171+
};
172+
}
173+
174+
/**
175+
* The metadata service's copy of the base's documents, stored as
176+
* `MetadataPlugin`'s artifact door stores them: each body stamped with the
177+
* base's envelope (`applyProtection`), a view container under its object and
178+
* each view it expands to under its own name.
179+
*/
180+
async function metadataServiceOf(): Promise<MetadataManager> {
181+
const manager = new MetadataManager({ formats: ['json'], loaders: [] });
182+
const body = baseBody();
183+
const stamp = <T extends Record<string, any>>(item: T): T =>
184+
applyProtection(item as any, { packageId: BASE, packageVersion: BASE_VERSION }) as T;
185+
for (const container of body.views) {
186+
await manager.register('view', OBJECT, stamp(container), { notify: false });
187+
for (const view of expandViewContainer(OBJECT, container)) {
188+
await manager.register('view', view.name, stamp(view as any), { notify: false });
189+
}
190+
}
191+
for (const [plural, type] of Object.entries(SERVICE_COLLECTIONS)) {
192+
for (const item of body[plural]) await manager.register(type, item.name, stamp(item), { notify: false });
193+
}
194+
return manager;
195+
}
196+
197+
let cleanup: Array<() => void | Promise<void>> = [];
198+
afterEach(async () => {
199+
for (const c of cleanup.reverse()) await c();
200+
cleanup = [];
201+
});
202+
203+
const envSnapshot = { OS_HOME: process.env.OS_HOME, OS_METADATA_WRITABLE: process.env.OS_METADATA_WRITABLE };
204+
let home: string;
205+
beforeAll(() => {
206+
home = mkdtempSync(join(tmpdir(), 'os-29649-home-'));
207+
process.env.OS_HOME = home;
208+
});
209+
afterAll(() => {
210+
for (const [k, v] of Object.entries(envSnapshot)) {
211+
if (v === undefined) delete process.env[k];
212+
else process.env[k] = v;
213+
}
214+
rmSync(home, { recursive: true, force: true });
215+
});
216+
beforeEach(() => {
217+
delete process.env.OS_METADATA_WRITABLE;
218+
ObjectStackProtocolImplementation.resetEnvWritableCache();
219+
resetEnvWritableMetadataTypes();
220+
vi.spyOn(console, 'warn').mockImplementation(() => {});
221+
});
222+
afterEach(() => { vi.restoreAllMocks(); });
223+
224+
/** One boot over the SQLite file in `dir`; the pair is installed on the first boot only. */
225+
async function boot(dir: string, { installPair }: { installPair: boolean }) {
226+
const driver = new SqlDriver({
227+
client: 'better-sqlite3',
228+
connection: { filename: join(dir, 'data.sqlite') },
229+
useNullAsDefault: true,
230+
});
231+
const objects = [SysMetadataObject, SysMetadataHistoryObject, SysMetadataAuditObject, SysMetadataCommitObject] as any[];
232+
await driver.initObjects(objects);
233+
234+
const engine = new ObjectQL();
235+
engine.registerDriver(driver as any, true);
236+
await engine.init();
237+
for (const o of objects) engine.registry.registerObject(o, PLATFORM_PKG);
238+
cleanup.push(() => engine.destroy());
239+
engine.registerApp(baseBody());
240+
241+
const services = new Map<string, unknown>([['objectql', engine], ['metadata', await metadataServiceOf()]]);
242+
const protocol: any = new ObjectStackProtocolImplementation(engine as any, () => services as any);
243+
services.set('protocol', protocol);
244+
const get = (name: string) => services.get(name) ?? null;
245+
const dispatcher = new HttpDispatcher({ context: { getService: get }, getService: get, getServiceAsync: async (n: string) => get(n) } as any);
246+
247+
if (installPair) {
248+
await protocol.installPackage({
249+
manifest: {
250+
id: PAIR, name: 'showcase-custom', version: '0.1.0', type: 'app',
251+
namespace: 'showcase_custom', dependencies: { [BASE]: '^0' },
252+
},
253+
});
254+
}
255+
256+
return {
257+
protocol,
258+
/** The pair's active (published) rows of `(type, name)`, read off the store. */
259+
async publishedRows(type: string, name: string): Promise<unknown[]> {
260+
return await driver.find('sys_metadata', { where: { type, name, package_id: PAIR, state: 'active' } });
261+
},
262+
async exported(id: string): Promise<Record<string, any>> {
263+
const res = await dispatcher.handlePackages(`/${encodeURIComponent(id)}/export`, 'GET', undefined, {}, CALLER);
264+
expect(res.response?.status, JSON.stringify(res.response?.body)).toBe(200);
265+
const body: any = JSON.parse(JSON.stringify(res.response?.body ?? null));
266+
return body?.data ?? body;
267+
},
268+
async override(type: string, name: string, item: Record<string, unknown>) {
269+
const saved = await protocol.saveMetaItem({ type, name, item, mode: 'draft', packageId: PAIR });
270+
expect(saved?.success, `the ${type} draft save`).toBe(true);
271+
await protocol.publishMetaItem({ type, name, packageId: PAIR });
272+
},
273+
};
274+
}
275+
276+
const itemsOf = (manifest: Record<string, any>, plural: string, name: string): any[] =>
277+
(manifest[plural] ?? []).filter((item: any) => item?.name === name);
278+
279+
/** Every exported item, across the collections, that carries an `overrides` block. */
280+
const overridingItems = (manifest: Record<string, any>): string[] => Object.entries(manifest)
281+
.filter(([, value]) => Array.isArray(value))
282+
.flatMap(([plural, value]) => (value as any[])
283+
.filter((item) => item && typeof item === 'object' && 'overrides' in item)
284+
.map((item) => `${plural}/${item.name}`));
285+
286+
describe('[#29649] GET /packages/<base>/export carries the base unchanged on a showcase-shaped boot', () => {
287+
for (const { type, plural, name, pairServed } of TYPES) {
288+
it(`${type}: the base's export is byte-identical with the override published, and after a restart`, async () => {
289+
const dir = mkdtempSync(join(tmpdir(), 'os-29649-db-'));
290+
cleanup.push(() => rmSync(dir, { recursive: true, force: true }));
291+
292+
const door = await boot(dir, { installPair: true });
293+
const before = await door.exported(BASE);
294+
const shipped = itemsOf(before, plural, name);
295+
expect(shipped, `the base's ${type} before the override`).toHaveLength(1);
296+
297+
await door.override(type, name, { ...shipped[0], label: OVERRIDE_LABEL, overrides: { ...DECLARATION } });
298+
299+
// CONTROL: the override is published, and, where the protocol serves
300+
// it, the effective read of the base's name serves it.
301+
expect(await door.publishedRows(type, name), `the pair's published ${type} row`).toHaveLength(1);
302+
if (pairServed) {
303+
const effective: any = await door.protocol.getMetaItem({ type, name, packageId: BASE });
304+
expect(effective?.item?.label, `the effective ${type} read`).toBe(OVERRIDE_LABEL);
305+
}
306+
307+
const after = await door.exported(BASE);
308+
expect(itemsOf(after, plural, name), `the base's ${type} with the override live`).toEqual(shipped);
309+
expect(overridingItems(after), `the base's export carries no override`).toEqual([]);
310+
expect(JSON.stringify(after), `the base's whole export with the ${type} override live`).toBe(JSON.stringify(before));
311+
312+
if (pairServed) {
313+
const pair = itemsOf(await door.exported(PAIR), plural, name);
314+
expect(pair.map((item) => item.label), `the pair's ${type} export`).toEqual([OVERRIDE_LABEL]);
315+
expect(pair[0].overrides).toEqual(DECLARATION);
316+
}
317+
318+
// A restart over the same database: the published row is hydrated from storage.
319+
const restarted = await boot(dir, { installPair: false });
320+
expect(JSON.stringify(await restarted.exported(BASE)), `the base's whole export after a restart`).toBe(JSON.stringify(before));
321+
});
322+
}
323+
});

0 commit comments

Comments
 (0)