@powerhousedao/switchboard 6.2.3-dev.5 → 6.2.3-dev.51

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 (143) hide show
  1. package/.tsbuild/test/attachments/byte-grants.test.d.ts +2 -0
  2. package/.tsbuild/test/attachments/byte-grants.test.d.ts.map +1 -0
  3. package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts +2 -0
  4. package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts.map +1 -0
  5. package/.tsbuild/test/attachments/reserve-metadata-parity.test.d.ts +2 -0
  6. package/.tsbuild/test/attachments/reserve-metadata-parity.test.d.ts.map +1 -0
  7. package/.tsbuild/test/attachments/url-signer.test.d.ts +2 -0
  8. package/.tsbuild/test/attachments/url-signer.test.d.ts.map +1 -0
  9. package/.tsbuild/test/boot-create-signature-policy.test.d.ts +2 -0
  10. package/.tsbuild/test/boot-create-signature-policy.test.d.ts.map +1 -0
  11. package/.tsbuild/test/boot-mcp-enabled.test.d.ts +2 -0
  12. package/.tsbuild/test/boot-mcp-enabled.test.d.ts.map +1 -0
  13. package/.tsbuild/test/boot-privacy.test.d.ts +2 -0
  14. package/.tsbuild/test/boot-privacy.test.d.ts.map +1 -0
  15. package/.tsbuild/test/boot-unsupported-stored-documents.test.d.ts +2 -0
  16. package/.tsbuild/test/boot-unsupported-stored-documents.test.d.ts.map +1 -0
  17. package/.tsbuild/test/boot-without-renown.test.d.ts +2 -0
  18. package/.tsbuild/test/boot-without-renown.test.d.ts.map +1 -0
  19. package/.tsbuild/test/boot-without-require-signatures.test.d.ts +2 -0
  20. package/.tsbuild/test/boot-without-require-signatures.test.d.ts.map +1 -0
  21. package/.tsbuild/test/boot-worker-trust-policy.test.d.ts +2 -0
  22. package/.tsbuild/test/boot-worker-trust-policy.test.d.ts.map +1 -0
  23. package/.tsbuild/test/boot-workflow-singleton-conflict.test.d.ts +2 -0
  24. package/.tsbuild/test/boot-workflow-singleton-conflict.test.d.ts.map +1 -0
  25. package/.tsbuild/test/boot-workflow-singleton-retry.test.d.ts +2 -0
  26. package/.tsbuild/test/boot-workflow-singleton-retry.test.d.ts.map +1 -0
  27. package/.tsbuild/test/builder-defaults.test.d.ts +2 -0
  28. package/.tsbuild/test/builder-defaults.test.d.ts.map +1 -0
  29. package/.tsbuild/test/create-signature-policy.test.d.ts +2 -0
  30. package/.tsbuild/test/create-signature-policy.test.d.ts.map +1 -0
  31. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts +2 -0
  32. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts.map +1 -0
  33. package/.tsbuild/test/fatal-shutdown.test.d.ts +2 -0
  34. package/.tsbuild/test/fatal-shutdown.test.d.ts.map +1 -0
  35. package/.tsbuild/test/mcp-flag.test.d.ts +2 -0
  36. package/.tsbuild/test/mcp-flag.test.d.ts.map +1 -0
  37. package/.tsbuild/test/observability-signals.test.d.ts +2 -0
  38. package/.tsbuild/test/observability-signals.test.d.ts.map +1 -0
  39. package/.tsbuild/test/pglite-preflight.test.d.ts +2 -0
  40. package/.tsbuild/test/pglite-preflight.test.d.ts.map +1 -0
  41. package/.tsbuild/test/renown-trust-policy.test.d.ts +2 -0
  42. package/.tsbuild/test/renown-trust-policy.test.d.ts.map +1 -0
  43. package/.tsbuild/test/snapshot-writer.d.ts +18 -0
  44. package/.tsbuild/test/snapshot-writer.d.ts.map +1 -0
  45. package/.tsbuild/test/unsupported-stored-documents.test.d.ts +2 -0
  46. package/.tsbuild/test/unsupported-stored-documents.test.d.ts.map +1 -0
  47. package/.tsbuild/test/workflow/harness.d.ts +56 -0
  48. package/.tsbuild/test/workflow/harness.d.ts.map +1 -0
  49. package/.tsbuild/test/workflow/oauth.test.d.ts +2 -0
  50. package/.tsbuild/test/workflow/oauth.test.d.ts.map +1 -0
  51. package/.tsbuild/test/workflow/resolvers.test.d.ts +2 -0
  52. package/.tsbuild/test/workflow/resolvers.test.d.ts.map +1 -0
  53. package/.tsbuild/test/workflow/webhook-dedupe.test.d.ts +2 -0
  54. package/.tsbuild/test/workflow/webhook-dedupe.test.d.ts.map +1 -0
  55. package/.tsbuild/test/workflow/webhook-signed.test.d.ts +2 -0
  56. package/.tsbuild/test/workflow/webhook-signed.test.d.ts.map +1 -0
  57. package/.tsbuild/test/workflow/webhook-trigger-test.test.d.ts +2 -0
  58. package/.tsbuild/test/workflow/webhook-trigger-test.test.d.ts.map +1 -0
  59. package/.tsbuild/test/workflow/webhook-unsigned.test.d.ts +2 -0
  60. package/.tsbuild/test/workflow/webhook-unsigned.test.d.ts.map +1 -0
  61. package/.tsbuild/test/workflow-reactor-access.test.d.ts +2 -0
  62. package/.tsbuild/test/workflow-reactor-access.test.d.ts.map +1 -0
  63. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts +2 -0
  64. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts.map +1 -0
  65. package/.tsbuild/test/workflow-read-gate.test.d.ts +2 -0
  66. package/.tsbuild/test/workflow-read-gate.test.d.ts.map +1 -0
  67. package/.tsbuild/test/workflow-runtime.test.d.ts +2 -0
  68. package/.tsbuild/test/workflow-runtime.test.d.ts.map +1 -0
  69. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  70. package/.tsbuild/tsdown.config.d.ts.map +1 -1
  71. package/.tsbuild/vitest.config.d.ts.map +1 -1
  72. package/Auth.md +94 -0
  73. package/CHANGELOG.md +541 -0
  74. package/README.md +35 -0
  75. package/dist/index.d.mts +2 -1
  76. package/dist/index.mjs +48 -49
  77. package/dist/index.mjs.map +1 -1
  78. package/dist/install-packages.d.mts +2 -1
  79. package/dist/install-packages.mjs +2 -2
  80. package/dist/install-packages.mjs.map +1 -1
  81. package/dist/migrate.d.mts +2 -1
  82. package/dist/migrate.mjs +4 -3
  83. package/dist/migrate.mjs.map +1 -1
  84. package/dist/server-J9fpd42v.mjs +3549 -0
  85. package/dist/server-J9fpd42v.mjs.map +1 -0
  86. package/dist/server.d.mts +155 -30
  87. package/dist/server.d.mts.map +1 -1
  88. package/dist/server.mjs +4 -5
  89. package/dist/{utils-Baw7rThP.mjs → utils-uaBlzyjS.mjs} +15 -9
  90. package/dist/utils-uaBlzyjS.mjs.map +1 -0
  91. package/dist/utils.d.mts +4 -5
  92. package/dist/utils.d.mts.map +1 -1
  93. package/dist/utils.mjs +3 -3
  94. package/package.json +22 -18
  95. package/test/attachment-reference-read-model.test.ts +6 -1
  96. package/test/attachments/auth.test.ts +85 -0
  97. package/test/attachments/byte-grants.test.ts +290 -0
  98. package/test/attachments/byte-routes-read-gate.integration.test.ts +314 -0
  99. package/test/attachments/download-target.test.ts +201 -32
  100. package/test/attachments/index.test.ts +34 -3
  101. package/test/attachments/reserve-metadata-parity.test.ts +96 -0
  102. package/test/attachments/routes-integration.test.ts +60 -4
  103. package/test/attachments/routes.test.ts +166 -23
  104. package/test/attachments/service-config.test.ts +13 -3
  105. package/test/attachments/url-signer.test.ts +105 -0
  106. package/test/boot-create-signature-policy.test.ts +108 -0
  107. package/test/boot-mcp-enabled.test.ts +121 -0
  108. package/test/boot-privacy.test.ts +243 -0
  109. package/test/boot-unsupported-stored-documents.test.ts +213 -0
  110. package/test/boot-without-renown.test.ts +81 -0
  111. package/test/boot-without-require-signatures.test.ts +74 -0
  112. package/test/boot-worker-trust-policy.test.ts +90 -0
  113. package/test/boot-workflow-singleton-conflict.test.ts +107 -0
  114. package/test/boot-workflow-singleton-retry.test.ts +133 -0
  115. package/test/builder-defaults.test.ts +80 -0
  116. package/test/create-signature-policy.test.ts +69 -0
  117. package/test/durable-node-fs-pg16.test.ts +80 -0
  118. package/test/fatal-shutdown.test.ts +165 -0
  119. package/test/mcp-flag.test.ts +56 -0
  120. package/test/observability-signals.test.ts +131 -0
  121. package/test/pglite-dialect.test.ts +217 -2
  122. package/test/pglite-preflight.test.ts +313 -0
  123. package/test/pglite-version.test.ts +9 -1
  124. package/test/renown-trust-policy.test.ts +315 -0
  125. package/test/snapshot-writer.ts +94 -0
  126. package/test/unsupported-stored-documents.test.ts +72 -0
  127. package/test/worker-pool.test.ts +27 -1
  128. package/test/workflow/harness.ts +289 -0
  129. package/test/workflow/oauth.test.ts +214 -0
  130. package/test/workflow/resolvers.test.ts +357 -0
  131. package/test/workflow/webhook-dedupe.test.ts +295 -0
  132. package/test/workflow/webhook-signed.test.ts +509 -0
  133. package/test/workflow/webhook-trigger-test.test.ts +82 -0
  134. package/test/workflow/webhook-unsigned.test.ts +345 -0
  135. package/test/workflow-reactor-access.test.ts +384 -0
  136. package/test/workflow-reactor-enforcement.test.ts +808 -0
  137. package/test/workflow-read-gate.test.ts +213 -0
  138. package/test/workflow-runtime.test.ts +1067 -0
  139. package/tsconfig.json +9 -3
  140. package/tsdown.config.ts +3 -1
  141. package/dist/server-CiKkVE6g.mjs +0 -1555
  142. package/dist/server-CiKkVE6g.mjs.map +0 -1
  143. package/dist/utils-Baw7rThP.mjs.map +0 -1
@@ -0,0 +1,384 @@
1
+ // The Switchboard's workflow runtime and its subgraph, composed over a real
2
+ // reactor with grants, with auth enforcement on and off (ADR 0005 §5-§9).
3
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ ReactorBuilder,
8
+ ReactorClientBuilder,
9
+ type IReactorClient,
10
+ type InProcessReactorClientModule,
11
+ } from "@powerhousedao/reactor";
12
+ import {
13
+ AuthorizationPolicy,
14
+ getDbClient,
15
+ type Context,
16
+ type IAuthorizationService,
17
+ } from "@powerhousedao/reactor-api";
18
+ import {
19
+ initializeAuth,
20
+ withSignaturePolicy,
21
+ type DocumentModelModule,
22
+ type Grant,
23
+ type ISigner,
24
+ } from "@powerhousedao/shared/document-model";
25
+ import {
26
+ createRelationalDb,
27
+ type IRelationalDb,
28
+ } from "@powerhousedao/shared/processors";
29
+ import {
30
+ Connection,
31
+ REACTOR_CONNECTOR_ID,
32
+ actions as connectionActions,
33
+ type ConnectionDocument,
34
+ } from "@powerhousedao/workflow/document-models/connection";
35
+ import {
36
+ Workflow,
37
+ actions as workflowActions,
38
+ } from "@powerhousedao/workflow/document-models/workflow";
39
+ import {
40
+ MemoryKeyStorage,
41
+ RenownCryptoBuilder,
42
+ RenownCryptoSigner,
43
+ } from "@renown/sdk/node";
44
+ import type { ILogger } from "document-model";
45
+ import { documentModelDocumentModelModule } from "document-model";
46
+ import type { Kysely } from "kysely";
47
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
48
+ import {
49
+ composeWorkflowRuntime,
50
+ type ComposedWorkflowRuntime,
51
+ } from "../src/workflow-runtime.mjs";
52
+
53
+ const PIECE = "@powerhousedao/piece-switchboard-docs";
54
+ const PIECE_VERSION = "1.0.0";
55
+ const PUBLISHER = "0xpublisher";
56
+ const OTHER = "0xother";
57
+ const HOST = "0xhost";
58
+
59
+ const SOURCE = `
60
+ const action = (name, requireReactor) => ({
61
+ name,
62
+ displayName: name,
63
+ description: name,
64
+ requireAuth: false,
65
+ requireReactor,
66
+ props: {},
67
+ run: async () => undefined,
68
+ });
69
+ module.exports = {
70
+ app: {
71
+ displayName: "Docs",
72
+ description: "Works with documents",
73
+ logoUrl: "https://example.com/docs.png",
74
+ categories: [],
75
+ actions: {
76
+ create_doc: {
77
+ ...action("create_doc", "write"),
78
+ run: async (ctx) => {
79
+ const doc = await ctx.reactor.createEmpty("powerhouse/document-model");
80
+ return { id: doc.header.id };
81
+ },
82
+ },
83
+ pick_doc: {
84
+ ...action("pick_doc", "read"),
85
+ props: {
86
+ document: {
87
+ displayName: "Document",
88
+ type: "DROPDOWN",
89
+ required: false,
90
+ refreshers: [],
91
+ options: async (_values, ctx) => {
92
+ const page = await ctx.reactor.find({
93
+ type: "powerhouse/document-model",
94
+ });
95
+ return {
96
+ options: page.results.map((d) => ({
97
+ label: d.header.id,
98
+ value: d.header.id,
99
+ })),
100
+ };
101
+ },
102
+ },
103
+ },
104
+ },
105
+ },
106
+ triggers: {},
107
+ },
108
+ };
109
+ `;
110
+
111
+ const logger = {
112
+ verbose: vi.fn(),
113
+ debug: vi.fn(),
114
+ info: vi.fn(),
115
+ warn: vi.fn(),
116
+ error: vi.fn(),
117
+ child: () => logger,
118
+ } as unknown as ILogger;
119
+
120
+ const ctxOf = (address?: string) =>
121
+ ({
122
+ headers: {},
123
+ db: {},
124
+ user: address ? { address, appKey: undefined } : undefined,
125
+ }) as unknown as Context;
126
+
127
+ const grant = (
128
+ id: string,
129
+ principal: Grant["principal"],
130
+ capability: Grant["capability"],
131
+ ): Grant => ({ id, description: id, effect: "allow", principal, capability });
132
+
133
+ async function signerFor(address: string): Promise<ISigner> {
134
+ const crypto = await new RenownCryptoBuilder()
135
+ .withKeyPairStorage(new MemoryKeyStorage())
136
+ .build();
137
+ return new RenownCryptoSigner(crypto, "test", {
138
+ address,
139
+ networkId: "eip155",
140
+ chainId: 1,
141
+ });
142
+ }
143
+
144
+ const OPEN: IAuthorizationService = {
145
+ config: {
146
+ admins: [],
147
+ defaultProtection: false,
148
+ policy: AuthorizationPolicy.OPEN,
149
+ },
150
+ isSupremeAdmin: () => true,
151
+ canCreate: () => true,
152
+ canRead: () => Promise.resolve(true),
153
+ canWrite: () => Promise.resolve(true),
154
+ canManage: () => Promise.resolve(true),
155
+ canMutate: () => Promise.resolve(true),
156
+ };
157
+
158
+ type Resolver = (parent: unknown, args: unknown, ctx: Context) => unknown;
159
+
160
+ describe.each([
161
+ { enforcement: false, label: "auth enforcement off" },
162
+ { enforcement: true, label: "auth enforcement on" },
163
+ ])("the Switchboard workflow runtime, $label", ({ enforcement }) => {
164
+ let root = "";
165
+ let module: InProcessReactorClientModule;
166
+ let hostSigner: ISigner;
167
+ let publisher: IReactorClient;
168
+ let composed: ComposedWorkflowRuntime;
169
+ let queries: Record<string, Resolver>;
170
+ let mutations: Record<string, Resolver>;
171
+
172
+ async function createDocument(
173
+ client: IReactorClient,
174
+ model: DocumentModelModule,
175
+ id: string,
176
+ grants?: Grant[],
177
+ ) {
178
+ await client.create(
179
+ withSignaturePolicy(model.utils.createDocument(), "legacy", { id }),
180
+ );
181
+ if (grants) {
182
+ await client.execute(id, "main", [
183
+ initializeAuth({ version: 1, grants }),
184
+ ]);
185
+ }
186
+ }
187
+
188
+ beforeAll(async () => {
189
+ root = await mkdtemp(join(tmpdir(), "switchboard-reactor-access-"));
190
+ const bundle = join(root, "dist", "node", "pieces", "docs");
191
+ await mkdir(bundle, { recursive: true });
192
+ await writeFile(
193
+ join(bundle, "package.json"),
194
+ JSON.stringify({ name: PIECE, version: PIECE_VERSION, main: "index.js" }),
195
+ );
196
+ await writeFile(join(bundle, "index.js"), SOURCE);
197
+
198
+ hostSigner = await signerFor(HOST);
199
+ module = await new ReactorClientBuilder()
200
+ .withSigner(hostSigner)
201
+ .withCreateSignaturePolicy("legacy")
202
+ .withReactorBuilder(
203
+ new ReactorBuilder()
204
+ .withDocumentModelSources([
205
+ Workflow as unknown as DocumentModelModule,
206
+ Connection as unknown as DocumentModelModule,
207
+ documentModelDocumentModelModule as unknown as DocumentModelModule,
208
+ ])
209
+ .withExecutorConfig({
210
+ featureFlags: enforcement
211
+ ? { documentDecisions: true, authEnforcement: true }
212
+ : {},
213
+ })
214
+ // Test keys hold no Renown credential.
215
+ .withTrustPolicy({ authorizeSigner: () => Promise.resolve(true) }),
216
+ )
217
+ .buildModule();
218
+ publisher = await new ReactorClientBuilder()
219
+ .withReactor(
220
+ module.reactor,
221
+ module.eventBus,
222
+ module.documentIndexer,
223
+ module.documentView,
224
+ )
225
+ .withSigner(await signerFor(PUBLISHER))
226
+ .build();
227
+
228
+ // Only the publisher reads or writes the first; the host may write both.
229
+ const publisherOnly = grant(
230
+ "publisher",
231
+ { address: PUBLISHER },
232
+ { can: "execute" },
233
+ );
234
+ await createDocument(
235
+ publisher,
236
+ documentModelDocumentModelModule as never,
237
+ "doc-publisher",
238
+ [publisherOnly],
239
+ );
240
+ await createDocument(
241
+ publisher,
242
+ documentModelDocumentModelModule as never,
243
+ "doc-shared",
244
+ [publisherOnly, grant("host", { address: HOST }, { can: "execute" })],
245
+ );
246
+ await createDocument(publisher, Connection as never, "conn-open");
247
+ await publisher.execute("conn-open", "main", [
248
+ connectionActions.setConnector({
249
+ connectorId: REACTOR_CONNECTOR_ID,
250
+ authType: "REACTOR",
251
+ }),
252
+ connectionActions.setConfig({ config: { endpoint: "local" } }),
253
+ ]);
254
+ await createDocument(publisher, Workflow as never, "wf-create");
255
+ await publisher.execute("wf-create", "main", [
256
+ workflowActions.addStep({
257
+ id: "create",
258
+ key: "create",
259
+ name: "create",
260
+ pieceName: PIECE,
261
+ pieceVersion: PIECE_VERSION,
262
+ actionName: "create_doc",
263
+ config: {},
264
+ reactorConnectionId: "conn-open",
265
+ }),
266
+ ]);
267
+ const authorizationService = OPEN;
268
+ const { db } = getDbClient();
269
+ const relationalDb = createRelationalDb(
270
+ db as unknown as Kysely<unknown>,
271
+ ) as IRelationalDb;
272
+ const pieces = new Map([
273
+ [root, [{ name: PIECE, version: PIECE_VERSION, bundleDir: bundle }]],
274
+ ]);
275
+ composed = await composeWorkflowRuntime({
276
+ reactorClient: module.client,
277
+ clientModule: module,
278
+ relationalDb,
279
+ attachments: {} as never,
280
+ authorizationService,
281
+ pieces: { getPieces: () => pieces, onPiecesChange: () => undefined },
282
+ logger,
283
+ });
284
+ const subgraph = new composed.subgraph({
285
+ reactorClient: module.client,
286
+ authorizationService,
287
+ relationalDb,
288
+ } as never);
289
+ const resolvers = subgraph.resolvers as Record<
290
+ string,
291
+ Record<string, Resolver>
292
+ >;
293
+ queries = resolvers.WorkflowRuntimeQueries;
294
+ mutations = resolvers.WorkflowRuntimeMutations;
295
+ });
296
+
297
+ afterAll(async () => {
298
+ await composed.stop();
299
+ module.reactor.kill();
300
+ await rm(root, { recursive: true, force: true });
301
+ });
302
+
303
+ it("tells any caller whether auth is enforced", () => {
304
+ expect(queries.authEnforcement({}, {}, ctxOf())).toBe(enforcement);
305
+ expect(queries.authEnforcement({}, {}, ctxOf(OTHER))).toBe(enforcement);
306
+ });
307
+
308
+ it("names the host's identity and the grant form to signed-in callers only", () => {
309
+ expect(queries.reactorIdentity({}, {}, ctxOf())).toBeNull();
310
+ expect(queries.authConditions({}, {}, ctxOf())).toBeNull();
311
+
312
+ expect(queries.reactorIdentity({}, {}, ctxOf(OTHER))).toEqual({
313
+ address: HOST,
314
+ key: hostSigner.app?.key,
315
+ });
316
+ expect(queries.authConditions({}, {}, ctxOf(OTHER))).toBe(false);
317
+ });
318
+
319
+ it("resolves options reading as the caller", async () => {
320
+ const options = async (caller: string, reactorConnectionId?: string) =>
321
+ (
322
+ (await queries.blockOptions(
323
+ {},
324
+ {
325
+ block: {
326
+ pieceName: PIECE,
327
+ pieceVersion: PIECE_VERSION,
328
+ kind: "action",
329
+ name: "pick_doc",
330
+ },
331
+ propName: "document",
332
+ ...(reactorConnectionId ? { reactorConnectionId } : {}),
333
+ },
334
+ ctxOf(caller),
335
+ )) as { options: { value: string }[] }
336
+ ).options.map((option) => option.value);
337
+
338
+ expect(await options(PUBLISHER, "conn-open")).toEqual(
339
+ expect.arrayContaining(["doc-publisher", "doc-shared"]),
340
+ );
341
+ // Both grant only named principals; the read gate serves OTHER neither.
342
+ const asOther = await options(OTHER, "conn-open");
343
+ expect(asOther).not.toContain("doc-publisher");
344
+ expect(asOther).not.toContain("doc-shared");
345
+ });
346
+
347
+ it("grants the host on a document a run creates, so it can write it later", async () => {
348
+ const result = (await mutations.testStep(
349
+ {},
350
+ { workflowId: "wf-create", stepId: "create" },
351
+ ctxOf(PUBLISHER),
352
+ )) as { status: string; error?: string | null; output: { id: string } };
353
+ expect(result.error ?? undefined).toBeUndefined();
354
+ expect(result.status).toBe("SUCCEEDED");
355
+ const { id } = result.output;
356
+
357
+ const created = await module.client.get(id);
358
+ expect(created.state.auth.grants.map((g) => g.principal)).toEqual(
359
+ expect.arrayContaining([{ address: PUBLISHER }, { address: HOST }]),
360
+ );
361
+ // The host signs this write; under enforcement it lands only with its grant.
362
+ const renamed = await module.client.execute(id, "main", [
363
+ documentModelDocumentModelModule.actions.setModelName({ name: "Host" }),
364
+ ]);
365
+ expect(
366
+ (renamed.state as unknown as { global: { name: string } }).global.name,
367
+ ).toBe("Host");
368
+ });
369
+
370
+ it("checks a REACTOR connection's config instead of calling it unconfigured", async () => {
371
+ const result = (await mutations.checkConnection(
372
+ {},
373
+ { connectionId: "conn-open" },
374
+ ctxOf(PUBLISHER),
375
+ )) as { ok: boolean; detail: string | null };
376
+
377
+ expect(result).toMatchObject({
378
+ ok: true,
379
+ detail: "Local reactor, write access",
380
+ });
381
+ const connection = await module.client.get<ConnectionDocument>("conn-open");
382
+ expect(connection.state.global.status).toBe("OK");
383
+ });
384
+ });