@powerhousedao/switchboard 6.2.3-dev.43 → 6.2.3-dev.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts +2 -0
  2. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts.map +1 -0
  3. package/.tsbuild/test/observability-signals.test.d.ts +2 -0
  4. package/.tsbuild/test/observability-signals.test.d.ts.map +1 -0
  5. package/.tsbuild/test/pglite-preflight.test.d.ts +2 -0
  6. package/.tsbuild/test/pglite-preflight.test.d.ts.map +1 -0
  7. package/.tsbuild/test/snapshot-writer.d.ts +18 -0
  8. package/.tsbuild/test/snapshot-writer.d.ts.map +1 -0
  9. package/.tsbuild/test/workflow-reactor-access.test.d.ts +2 -0
  10. package/.tsbuild/test/workflow-reactor-access.test.d.ts.map +1 -0
  11. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts +2 -0
  12. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts.map +1 -0
  13. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  14. package/CHANGELOG.md +43 -0
  15. package/dist/index.mjs +42 -9
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{server-CMAxYG6D.mjs → server-Dvw8_NeM.mjs} +271 -79
  18. package/dist/server-Dvw8_NeM.mjs.map +1 -0
  19. package/dist/server.d.mts +3 -1
  20. package/dist/server.d.mts.map +1 -1
  21. package/dist/server.mjs +3 -3
  22. package/package.json +15 -16
  23. package/test/boot-unsupported-stored-documents.test.ts +5 -5
  24. package/test/durable-node-fs-pg16.test.ts +80 -0
  25. package/test/fatal-shutdown.test.ts +33 -0
  26. package/test/observability-signals.test.ts +131 -0
  27. package/test/pglite-dialect.test.ts +28 -0
  28. package/test/pglite-preflight.test.ts +313 -0
  29. package/test/pglite-version.test.ts +9 -1
  30. package/test/snapshot-writer.ts +94 -0
  31. package/test/worker-pool.test.ts +27 -1
  32. package/test/workflow/resolvers.test.ts +26 -0
  33. package/test/workflow-reactor-access.test.ts +384 -0
  34. package/test/workflow-reactor-enforcement.test.ts +805 -0
  35. package/test/workflow-runtime.test.ts +53 -0
  36. package/tsconfig.json +0 -3
  37. package/dist/server-CMAxYG6D.mjs.map +0 -1
@@ -0,0 +1,313 @@
1
+ import { createDurableNodeFs } from "@powerhousedao/reactor-api/pglite-node";
2
+ import type { ILogger } from "document-model";
3
+ import { promises as fs } from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ afterAll,
8
+ afterEach,
9
+ beforeAll,
10
+ beforeEach,
11
+ describe,
12
+ expect,
13
+ it,
14
+ vi,
15
+ } from "vitest";
16
+ import { runPglitePreflight } from "../src/pglite-preflight.js";
17
+ import { loadNodeFsClass, loadPGliteModule } from "../src/pglite-version.js";
18
+ import { writeSnapshotFromDir } from "./snapshot-writer.js";
19
+
20
+ // PGlite initdb and pg_dump are several times slower on the Windows runner.
21
+ const BOOT_TIMEOUT = 120_000;
22
+ const RM = { recursive: true, force: true, maxRetries: 10 } as const;
23
+ const ROWS = 10;
24
+ const CONVERTED_LINE =
25
+ /^Converted PGlite snapshot at (.+): snapshot (\d+) bytes -> (\d+) bytes on disk, (\d+) entries, (\d+) ms$/;
26
+
27
+ type StubLogger = ILogger & {
28
+ debug: ReturnType<typeof vi.fn>;
29
+ info: ReturnType<typeof vi.fn>;
30
+ warn: ReturnType<typeof vi.fn>;
31
+ };
32
+
33
+ function stubLogger(): StubLogger {
34
+ const logger = {
35
+ verbose: vi.fn(),
36
+ debug: vi.fn(),
37
+ info: vi.fn(),
38
+ warn: vi.fn(),
39
+ error: vi.fn(),
40
+ child: vi.fn(),
41
+ };
42
+ logger.child.mockReturnValue(logger);
43
+ return logger as unknown as StubLogger;
44
+ }
45
+
46
+ async function exists(p: string): Promise<boolean> {
47
+ try {
48
+ await fs.lstat(p);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ async function pgVersion(dir: string): Promise<string> {
56
+ return (await fs.readFile(path.join(dir, "PG_VERSION"), "utf8")).trim();
57
+ }
58
+
59
+ /** Opens `dir` the way the server does and counts the seeded rows. */
60
+ async function countRows(dir: string, major: 16 | 17): Promise<number> {
61
+ const [{ PGlite: Pg }, Base] = await Promise.all([
62
+ loadPGliteModule(major),
63
+ loadNodeFsClass(major),
64
+ ]);
65
+ const pg = new Pg({
66
+ fs: createDurableNodeFs(Base, dir, {
67
+ fsync: false,
68
+ maintenanceIntervalMs: 0,
69
+ }),
70
+ });
71
+ try {
72
+ await pg.waitReady;
73
+ const { rows } = await pg.query<{ n: number }>(
74
+ 'SELECT count(*)::int AS n FROM "Rows"',
75
+ );
76
+ return rows[0].n;
77
+ } finally {
78
+ await pg.close();
79
+ }
80
+ }
81
+
82
+ function convertedLines(logger: StubLogger): string[] {
83
+ return logger.info.mock.calls
84
+ .map((call) => String(call[0]))
85
+ .filter((line) => CONVERTED_LINE.test(line));
86
+ }
87
+
88
+ describe("runPglitePreflight", () => {
89
+ // A pure-snapshot PG16 store: snapshot.bin and nothing else, the layout
90
+ // AtomicNodeFs left for a store it created itself.
91
+ let template: string;
92
+ let root: string;
93
+ let logger: StubLogger;
94
+
95
+ beforeAll(async () => {
96
+ template = await fs.mkdtemp(path.join(os.tmpdir(), "preflight-template-"));
97
+ const loose = path.join(template, "loose");
98
+ const { PGlite: LegacyPGlite } = await loadPGliteModule(16);
99
+ const LegacyNodeFS = await loadNodeFsClass(16);
100
+ const pg = new LegacyPGlite({ fs: new LegacyNodeFS(loose) });
101
+ await pg.waitReady;
102
+ await pg.exec('CREATE TABLE "Rows" (id int PRIMARY KEY, payload text)');
103
+ await pg.exec(
104
+ `INSERT INTO "Rows" SELECT g, repeat('x', 100) FROM generate_series(1, ${ROWS}) g`,
105
+ );
106
+ await pg.close();
107
+ await fs.rm(path.join(loose, "postmaster.pid"), { force: true });
108
+ expect(await pgVersion(loose)).toBe("16");
109
+
110
+ const store = path.join(template, "store");
111
+ await fs.mkdir(store);
112
+ await writeSnapshotFromDir(loose, path.join(store, "snapshot.bin"));
113
+ await fs.rm(loose, RM);
114
+ }, BOOT_TIMEOUT);
115
+
116
+ afterAll(async () => {
117
+ await fs.rm(template, RM);
118
+ }, BOOT_TIMEOUT);
119
+
120
+ beforeEach(async () => {
121
+ root = await fs.mkdtemp(path.join(os.tmpdir(), "preflight-"));
122
+ logger = stubLogger();
123
+ });
124
+
125
+ afterEach(async () => {
126
+ await fs.rm(root, RM);
127
+ }, BOOT_TIMEOUT);
128
+
129
+ async function snapshotDir(name: string): Promise<string> {
130
+ const dir = path.join(root, name);
131
+ await fs.cp(path.join(template, "store"), dir, { recursive: true });
132
+ return dir;
133
+ }
134
+
135
+ async function looseDir(name: string, major = 17): Promise<string> {
136
+ const dir = path.join(root, name);
137
+ await fs.mkdir(path.join(dir, "pg_wal"), { recursive: true });
138
+ await fs.writeFile(path.join(dir, "PG_VERSION"), `${major}\n`);
139
+ return dir;
140
+ }
141
+
142
+ it(
143
+ "converts before detecting the major, so a PG16 snapshot is migrated",
144
+ async () => {
145
+ const dir = await snapshotDir("reactor-storage");
146
+
147
+ const detected = await runPglitePreflight({
148
+ dirs: [dir],
149
+ migratePglite: true,
150
+ inMemory: false,
151
+ logger,
152
+ });
153
+
154
+ expect(detected.get(dir)).toBe(17);
155
+ expect(await pgVersion(dir)).toBe("17");
156
+ expect(await exists(path.join(dir, "snapshot.bin"))).toBe(false);
157
+ expect(await exists(`${dir}.converting`)).toBe(false);
158
+ expect(await exists(`${dir}.old`)).toBe(false);
159
+ expect(convertedLines(logger)).toHaveLength(1);
160
+ expect(logger.info).toHaveBeenCalledWith(
161
+ expect.stringContaining("Migrating"),
162
+ );
163
+ expect(await countRows(dir, 17)).toBe(ROWS);
164
+ },
165
+ BOOT_TIMEOUT,
166
+ );
167
+
168
+ // Pins the order alone: before this change a pure-snapshot dir had no loose
169
+ // PG_VERSION and the migration logged "No PG_VERSION; skipping". The case
170
+ // above also needs the legacy pg_dump to work.
171
+ it(
172
+ "attempts the migration of a PG16 snapshot instead of skipping it",
173
+ async () => {
174
+ const dir = await snapshotDir("reactor-storage");
175
+
176
+ await runPglitePreflight({
177
+ dirs: [dir],
178
+ migratePglite: true,
179
+ inMemory: false,
180
+ logger,
181
+ }).catch(() => undefined);
182
+
183
+ expect(convertedLines(logger)).toHaveLength(1);
184
+ expect(logger.info).not.toHaveBeenCalledWith(
185
+ expect.stringContaining("No PG_VERSION"),
186
+ );
187
+ expect(logger.info).toHaveBeenCalledWith(
188
+ expect.stringContaining(`Migrating ${dir} from PG16 to PG17`),
189
+ );
190
+ },
191
+ BOOT_TIMEOUT,
192
+ );
193
+
194
+ it(
195
+ "boots a converted dir on the second start without converting",
196
+ async () => {
197
+ const dir = await snapshotDir("reactor-storage");
198
+ const first = { dirs: [dir], inMemory: false, logger };
199
+ await runPglitePreflight(first);
200
+ expect(convertedLines(logger)).toHaveLength(1);
201
+ expect(await pgVersion(dir)).toBe("16");
202
+
203
+ const second = stubLogger();
204
+ const detected = await runPglitePreflight({ ...first, logger: second });
205
+
206
+ expect(detected.get(dir)).toBe(16);
207
+ expect(convertedLines(second)).toHaveLength(0);
208
+ expect(await exists(`${dir}.converting`)).toBe(false);
209
+ expect(await exists(`${dir}.old`)).toBe(false);
210
+ expect(await countRows(dir, 16)).toBe(ROWS);
211
+ },
212
+ BOOT_TIMEOUT,
213
+ );
214
+
215
+ it("removes pg_wal/xlogtemp.*", async () => {
216
+ const dir = await looseDir("read-storage");
217
+ const segment = path.join(dir, "pg_wal", "000000010000000000000001");
218
+ await fs.writeFile(segment, "wal");
219
+ await fs.writeFile(path.join(dir, "pg_wal", "xlogtemp.42"), "");
220
+ await fs.writeFile(path.join(dir, "pg_wal", "xlogtemp.7"), "");
221
+
222
+ const detected = await runPglitePreflight({
223
+ dirs: [dir],
224
+ inMemory: false,
225
+ logger,
226
+ });
227
+
228
+ expect(detected.get(dir)).toBe(17);
229
+ expect(await fs.readdir(path.join(dir, "pg_wal"))).toEqual([
230
+ "000000010000000000000001",
231
+ ]);
232
+ expect(await exists(segment)).toBe(true);
233
+ });
234
+
235
+ it("removes postmaster.pid without a warn call", async () => {
236
+ const dir = await looseDir("read-storage");
237
+ const lockfile = path.join(dir, "postmaster.pid");
238
+ await fs.writeFile(lockfile, "42\n");
239
+
240
+ await runPglitePreflight({ dirs: [dir], inMemory: false, logger });
241
+
242
+ expect(await exists(lockfile)).toBe(false);
243
+ expect(logger.warn).not.toHaveBeenCalled();
244
+ expect(logger.debug).toHaveBeenCalledWith(
245
+ expect.stringContaining(lockfile),
246
+ );
247
+ });
248
+
249
+ it(
250
+ "logs one info line per converted dir with sizes and duration",
251
+ async () => {
252
+ const reactor = await snapshotDir("reactor-storage");
253
+ const readModel = await snapshotDir("read-storage");
254
+
255
+ await runPglitePreflight({
256
+ dirs: [reactor, readModel],
257
+ inMemory: false,
258
+ logger,
259
+ });
260
+
261
+ const lines = convertedLines(logger);
262
+ expect(lines).toHaveLength(2);
263
+ const byDir = new Map(
264
+ lines.map((line) => {
265
+ const match = CONVERTED_LINE.exec(line)!;
266
+ return [match[1], match.slice(2).map(Number)] as const;
267
+ }),
268
+ );
269
+ for (const dir of [reactor, readModel]) {
270
+ const [snapshotBytes, convertedBytes, entries] = byDir.get(dir)!;
271
+ expect(snapshotBytes).toBeGreaterThan(0);
272
+ expect(convertedBytes).toBeGreaterThan(0);
273
+ expect(entries).toBeGreaterThan(0);
274
+ }
275
+ },
276
+ BOOT_TIMEOUT,
277
+ );
278
+
279
+ it("touches nothing in memory", async () => {
280
+ const dir = await snapshotDir("reactor-storage");
281
+
282
+ const detected = await runPglitePreflight({
283
+ dirs: [dir],
284
+ inMemory: true,
285
+ logger,
286
+ });
287
+
288
+ expect(detected.size).toBe(0);
289
+ expect(await exists(path.join(dir, "snapshot.bin"))).toBe(true);
290
+ });
291
+
292
+ it("wipes the dirs and their conversion siblings under forcePgVersion", async () => {
293
+ const dir = await snapshotDir("reactor-storage");
294
+ await fs.mkdir(`${dir}.old`);
295
+ await fs.mkdir(`${dir}.converting`);
296
+
297
+ const detected = await runPglitePreflight({
298
+ dirs: [dir],
299
+ forcePgVersion: 17,
300
+ migratePglite: true,
301
+ inMemory: false,
302
+ logger,
303
+ });
304
+
305
+ expect(detected.size).toBe(0);
306
+ expect(await exists(dir)).toBe(false);
307
+ expect(await exists(`${dir}.old`)).toBe(false);
308
+ expect(await exists(`${dir}.converting`)).toBe(false);
309
+ expect(logger.warn).toHaveBeenCalledWith(
310
+ expect.stringContaining("ignoring --migrate-pglite"),
311
+ );
312
+ });
313
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { parseForcePgVersion } from "../src/pglite-version.js";
2
+ import { openForVerify, parseForcePgVersion } from "../src/pglite-version.js";
3
3
 
4
4
  describe("parseForcePgVersion", () => {
5
5
  it("returns null for undefined", () => {
@@ -35,3 +35,11 @@ describe("parseForcePgVersion", () => {
35
35
  expect(() => parseForcePgVersion("16.5")).toThrow(/PH_FORCE_PG_VERSION/);
36
36
  });
37
37
  });
38
+
39
+ describe("openForVerify", () => {
40
+ it("throws on an unsupported major before touching disk", async () => {
41
+ await expect(openForVerify(15, "/nonexistent/pglite-dir")).rejects.toThrow(
42
+ /PG_VERSION=15 is not supported/,
43
+ );
44
+ });
45
+ });
@@ -0,0 +1,94 @@
1
+ // Test-only v1 snapshot.bin writer over a loose on-disk tree.
2
+ import { promises as fs } from "node:fs";
3
+ import type { FileHandle } from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ const MAGIC = new Uint8Array([0x50, 0x47, 0x4c, 0x41]);
7
+ const FORMAT_VERSION = 1;
8
+ const HEADER_SIZE = 12;
9
+ const ENTRY_PREFIX_SIZE = 9;
10
+ const DEFAULT_SKIP = ["snapshot.bin", "snapshot.bin.tmp"];
11
+
12
+ export interface SnapshotEntry {
13
+ type: 0 | 1;
14
+ mode: number;
15
+ relPath: string;
16
+ data?: Buffer;
17
+ }
18
+
19
+ export interface WriteSnapshotOptions {
20
+ /** Root-level names to leave out. Defaults to the snapshot files. */
21
+ skip?: string[];
22
+ }
23
+
24
+ export async function collectDiskEntries(
25
+ dataDir: string,
26
+ options: WriteSnapshotOptions = {},
27
+ ): Promise<SnapshotEntry[]> {
28
+ const skip = new Set(options.skip ?? DEFAULT_SKIP);
29
+ const entries: SnapshotEntry[] = [];
30
+ const walk = async (dir: string, rel: string) => {
31
+ const names = (await fs.readdir(dir)).sort();
32
+ for (const name of names) {
33
+ if (rel === "" && skip.has(name)) continue;
34
+ const full = path.join(dir, name);
35
+ const relPath = rel === "" ? name : `${rel}/${name}`;
36
+ const stat = await fs.lstat(full);
37
+ if (stat.isDirectory()) {
38
+ entries.push({ type: 0, mode: stat.mode & 0o7777, relPath });
39
+ await walk(full, relPath);
40
+ } else if (stat.isFile()) {
41
+ entries.push({
42
+ type: 1,
43
+ mode: stat.mode & 0o7777,
44
+ relPath,
45
+ data: await fs.readFile(full),
46
+ });
47
+ }
48
+ }
49
+ };
50
+ await walk(dataDir, "");
51
+ return entries;
52
+ }
53
+
54
+ export function encodeSnapshot(entries: SnapshotEntry[]): Buffer {
55
+ const encoder = new TextEncoder();
56
+ const parts: Buffer[] = [];
57
+ const header = Buffer.alloc(HEADER_SIZE);
58
+ header.set(MAGIC, 0);
59
+ header.writeUInt32LE(FORMAT_VERSION, 4);
60
+ header.writeUInt32LE(entries.length, 8);
61
+ parts.push(header);
62
+ for (const entry of entries) {
63
+ const pathBytes = encoder.encode(entry.relPath);
64
+ const dataLen = entry.data?.byteLength ?? 0;
65
+ const prefix = Buffer.alloc(ENTRY_PREFIX_SIZE + pathBytes.byteLength + 4);
66
+ prefix.writeUInt8(entry.type, 0);
67
+ prefix.writeUInt32LE(entry.mode, 1);
68
+ prefix.writeUInt32LE(pathBytes.byteLength, 5);
69
+ prefix.set(pathBytes, ENTRY_PREFIX_SIZE);
70
+ prefix.writeUInt32LE(dataLen, ENTRY_PREFIX_SIZE + pathBytes.byteLength);
71
+ parts.push(prefix);
72
+ if (entry.data && dataLen > 0) parts.push(entry.data);
73
+ }
74
+ return Buffer.concat(parts);
75
+ }
76
+
77
+ /** Serializes `dataDir` into `snapshotPath`. Returns the entry count and size. */
78
+ export async function writeSnapshotFromDir(
79
+ dataDir: string,
80
+ snapshotPath: string,
81
+ options: WriteSnapshotOptions = {},
82
+ ): Promise<{ entries: number; bytes: number }> {
83
+ const entries = await collectDiskEntries(dataDir, options);
84
+ const bytes = encodeSnapshot(entries);
85
+ let fh: FileHandle | undefined;
86
+ try {
87
+ fh = await fs.open(snapshotPath, "w");
88
+ await fh.writeFile(bytes);
89
+ await fh.sync();
90
+ } finally {
91
+ await fh?.close();
92
+ }
93
+ return { entries: entries.length, bytes: bytes.byteLength };
94
+ }
@@ -1,9 +1,12 @@
1
1
  import type { ILogger } from "document-model";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
3
5
  import { describe, expect, it, vi } from "vitest";
4
6
  import {
5
7
  autoWorkerCount,
6
8
  buildWorkerDbConfig,
9
+ keepImportableSources,
7
10
  resolveHostPoolSize,
8
11
  resolveWorkerModelSources,
9
12
  resolveWorkerPoolOptions,
@@ -264,3 +267,26 @@ describe("resolveWorkerModelSources", () => {
264
267
  expect(logger.warn).toHaveBeenCalledTimes(1);
265
268
  });
266
269
  });
270
+
271
+ describe("keepImportableSources", () => {
272
+ it("keeps sources that export a model and drops one that throws or has none", async () => {
273
+ const dir = mkdtempSync(path.join(tmpdir(), "ph-model-sources-"));
274
+ const good = path.join(dir, "good.mjs");
275
+ const broken = path.join(dir, "broken.mjs");
276
+ const empty = path.join(dir, "empty.mjs");
277
+ writeFileSync(
278
+ good,
279
+ 'export const Model = { reducer: () => ({}), documentModel: { global: { id: "test/model" } } };',
280
+ );
281
+ writeFileSync(broken, 'throw new Error("stale dist");');
282
+ // What `ph init` builds for a project with no models yet.
283
+ writeFileSync(empty, "export {};");
284
+
285
+ const kept = await keepImportableSources(
286
+ [{ filePath: broken }, { filePath: good }, { filePath: empty }],
287
+ stubLogger(),
288
+ );
289
+
290
+ expect(kept).toEqual([{ filePath: good }]);
291
+ });
292
+ });
@@ -48,6 +48,7 @@ function fakeRuntime() {
48
48
  testStep: vi.fn(() => Promise.resolve({})),
49
49
  stepOutputTree: vi.fn(() => Promise.resolve({})),
50
50
  blockResolutions: vi.fn(() => Promise.resolve([])),
51
+ reactorAccessDenial: vi.fn((): string | undefined => undefined),
51
52
  testTrigger: vi.fn(() => Promise.resolve(null)),
52
53
  cancelTriggerTestFor: vi.fn(() => Promise.resolve(true)),
53
54
  secrets: vi.fn(() =>
@@ -68,6 +69,12 @@ function build(isAdmin: boolean) {
68
69
  const resolvers = getResolvers(
69
70
  runtime as unknown as WorkflowRuntimeService,
70
71
  authorizationService,
72
+ undefined,
73
+ undefined,
74
+ (documentId) =>
75
+ documentId === "wf-hidden"
76
+ ? Promise.reject(new Error("You may not read this workflow"))
77
+ : Promise.resolve(),
71
78
  ) as Record<string, Record<string, Resolver>>;
72
79
  return {
73
80
  runtime,
@@ -167,6 +174,25 @@ describe("the workflow resolvers and the caller", () => {
167
174
  expect(runtime.secrets).toHaveBeenCalledTimes(1);
168
175
  });
169
176
 
177
+ it("serves why a workflow got no reactor access, naming the publisher", async () => {
178
+ const { runtime, queries } = build(false);
179
+ runtime.reactorAccessDenial.mockReturnValueOnce(
180
+ 'The publisher 0xabc cannot read reactor connection "conn-1"',
181
+ );
182
+
183
+ await expect(
184
+ queries.reactorAccessDenial({}, { workflowId: "wf-1" }, CTX),
185
+ ).resolves.toBe(
186
+ 'The publisher 0xabc cannot read reactor connection "conn-1"',
187
+ );
188
+ await expect(
189
+ queries.reactorAccessDenial({}, { workflowId: "wf-1" }, CTX),
190
+ ).resolves.toBeNull();
191
+ await expect(
192
+ queries.reactorAccessDenial({}, { workflowId: "wf-hidden" }, CTX),
193
+ ).rejects.toThrow("You may not read this workflow");
194
+ });
195
+
170
196
  it("serves the piece version a run's steps ran", async () => {
171
197
  const { runtime, queries } = build(true);
172
198
  runtime.run.mockResolvedValueOnce({