@electric-ax/agents-server-conformance-tests 0.1.7 → 0.1.9

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.
package/dist/index.cjs CHANGED
@@ -28,10 +28,30 @@ const node_http = __toESM(require("node:http"));
28
28
  const __electric_sql_client = __toESM(require("@electric-sql/client"));
29
29
  const node_child_process = __toESM(require("node:child_process"));
30
30
 
31
+ //#region src/url.ts
32
+ function appendPathToUrl(baseUrl, path) {
33
+ const base = new URL(baseUrl);
34
+ const pathUrl = new URL(path, `http://electric-agents.local`);
35
+ const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
36
+ const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
37
+ const target = new URL(base);
38
+ target.pathname = `${basePath}${suffix}`;
39
+ target.search = ``;
40
+ target.hash = pathUrl.hash;
41
+ base.searchParams.forEach((value, key) => {
42
+ target.searchParams.append(key, value);
43
+ });
44
+ pathUrl.searchParams.forEach((value, key) => {
45
+ target.searchParams.append(key, value);
46
+ });
47
+ return target.toString();
48
+ }
49
+
50
+ //#endregion
31
51
  //#region src/electric-agents-dsl.ts
32
52
  async function fetchShapeRows(baseUrl, table) {
33
53
  const stream = new __electric_sql_client.ShapeStream({
34
- url: `${baseUrl}/_electric/electric/v1/shape`,
54
+ url: appendPathToUrl(baseUrl, `/_electric/electric/v1/shape`),
35
55
  params: { table },
36
56
  subscribe: false
37
57
  });
@@ -54,10 +74,8 @@ function toServerEntityTypeRegistration(registration) {
54
74
  ...registration.metadata_schema && { metadata_schema: registration.metadata_schema },
55
75
  ...registration.serve_endpoint && { serve_endpoint: registration.serve_endpoint }
56
76
  };
57
- const inboxSchemas = registration.inbox_schemas ?? registration.input_schemas;
58
- const stateSchemas = registration.state_schemas ?? registration.output_schemas;
59
- if (inboxSchemas) body.inbox_schemas = inboxSchemas;
60
- if (stateSchemas) body.state_schemas = stateSchemas;
77
+ if (registration.inbox_schemas) body.inbox_schemas = registration.inbox_schemas;
78
+ if (registration.state_schemas) body.state_schemas = registration.state_schemas;
61
79
  return body;
62
80
  }
63
81
  function normalizeWebhookPayload(body) {
@@ -198,7 +216,7 @@ var ServeEndpointReceiver = class {
198
216
  }
199
217
  };
200
218
  async function electricAgentsFetch$1(baseUrl, path, opts = {}) {
201
- return fetch(`${baseUrl}${routeControlPlanePath$1(path)}`, {
219
+ return fetch(appendPathToUrl(baseUrl, routeControlPlanePath$1(path)), {
202
220
  ...opts,
203
221
  headers: {
204
222
  "content-type": `application/json`,
@@ -217,7 +235,7 @@ function isEntityStreamPath$1(pathname) {
217
235
  return segments.length >= 3 && (lastSegment === `main` || lastSegment === `error`);
218
236
  }
219
237
  function subscriptionEndpoint$1(baseUrl, id) {
220
- return `${baseUrl}/__ds/subscriptions/${encodeURIComponent(id)}`;
238
+ return appendPathToUrl(baseUrl, `/__ds/subscriptions/${encodeURIComponent(id)}`);
221
239
  }
222
240
  function subscriptionPattern$1(pattern) {
223
241
  return pattern.replace(/^\/+/, ``);
@@ -377,8 +395,8 @@ var ElectricAgentsScenario = class {
377
395
  this.steps.push({
378
396
  kind: `amendSchemas`,
379
397
  name,
380
- input_schemas: schemas.input_schemas,
381
- output_schemas: schemas.output_schemas
398
+ inbox_schemas: schemas.inbox_schemas,
399
+ state_schemas: schemas.state_schemas
382
400
  });
383
401
  return this;
384
402
  }
@@ -446,7 +464,7 @@ var ElectricAgentsScenario = class {
446
464
  typeName,
447
465
  instanceId,
448
466
  args: opts?.args,
449
- code: `SCHEMA_VALIDATION_ERROR`,
467
+ code: `SCHEMA_VALIDATION_FAILED`,
450
468
  status: 422
451
469
  });
452
470
  return this;
@@ -456,7 +474,7 @@ var ElectricAgentsScenario = class {
456
474
  kind: `expectSendSchemaError`,
457
475
  payload,
458
476
  messageType: opts?.type ?? `default`,
459
- code: `SCHEMA_VALIDATION_ERROR`,
477
+ code: `SCHEMA_VALIDATION_FAILED`,
460
478
  status: 422
461
479
  });
462
480
  return this;
@@ -466,7 +484,7 @@ var ElectricAgentsScenario = class {
466
484
  kind: `expectWriteSchemaError`,
467
485
  payload,
468
486
  eventType: opts?.type ?? `default`,
469
- code: `SCHEMA_VALIDATION_ERROR`,
487
+ code: `SCHEMA_VALIDATION_FAILED`,
470
488
  status: 422
471
489
  });
472
490
  return this;
@@ -695,7 +713,7 @@ async function executeStep(ctx, step) {
695
713
  case `readStream`: {
696
714
  if (!ctx.currentEntityStreams) throw new Error(`No current entity streams`);
697
715
  const streamPath = step.stream === `error` ? ctx.currentEntityStreams.error : ctx.currentEntityStreams.main;
698
- const res = await fetch(`${ctx.baseUrl}${streamPath}?offset=0000000000000000_0000000000000000`);
716
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${streamPath}?offset=0000000000000000_0000000000000000`));
699
717
  if (res.status === 200) {
700
718
  const text = await res.text();
701
719
  const messages = text ? JSON.parse(text) : [];
@@ -836,8 +854,8 @@ async function executeStep(ctx, step) {
836
854
  }
837
855
  case `amendSchemas`: {
838
856
  const body = {};
839
- if (step.input_schemas) body.inbox_schemas = step.input_schemas;
840
- if (step.output_schemas) body.state_schemas = step.output_schemas;
857
+ if (step.inbox_schemas) body.inbox_schemas = step.inbox_schemas;
858
+ if (step.state_schemas) body.state_schemas = step.state_schemas;
841
859
  const res = await electricAgentsFetch$1(ctx.baseUrl, `/_electric/entity-types/${step.name}/schemas`, {
842
860
  method: `PATCH`,
843
861
  body: JSON.stringify(body)
@@ -1339,8 +1357,8 @@ function applyElectricAgentsAction(model, action, targetIdx, opts) {
1339
1357
  entityTypes: [...model.entityTypes, {
1340
1358
  name: opts?.typeName ?? `prop-type-${typeNum}`,
1341
1359
  hasCreationSchema: false,
1342
- hasInputSchemas: false,
1343
- hasOutputSchemas: false
1360
+ hasInboxSchemas: false,
1361
+ hasStateSchemas: false
1344
1362
  }]
1345
1363
  };
1346
1364
  }
@@ -1498,7 +1516,7 @@ function checkStateProtocolInvariants(events) {
1498
1516
  //#endregion
1499
1517
  //#region src/cli-dsl.ts
1500
1518
  function subscriptionEndpoint(baseUrl, id) {
1501
- return `${baseUrl}/__ds/subscriptions/${encodeURIComponent(id)}`;
1519
+ return appendPathToUrl(baseUrl, `/__ds/subscriptions/${encodeURIComponent(id)}`);
1502
1520
  }
1503
1521
  function subscriptionPattern(pattern) {
1504
1522
  return pattern.replace(/^\/+/, ``);
@@ -1647,7 +1665,7 @@ var CliScenario = class {
1647
1665
  try {
1648
1666
  for (const step of this.steps) switch (step.kind) {
1649
1667
  case `setupType`: {
1650
- const res = await fetch(`${this.baseUrl}/_electric/entity-types`, {
1668
+ const res = await fetch(appendPathToUrl(this.baseUrl, `/_electric/entity-types`), {
1651
1669
  method: `POST`,
1652
1670
  headers: { "content-type": `application/json` },
1653
1671
  body: JSON.stringify(step.registration)
@@ -1776,7 +1794,7 @@ function startNoopReceiver() {
1776
1794
  //#endregion
1777
1795
  //#region src/electric-agents-tests.ts
1778
1796
  async function electricAgentsFetch(baseUrl, path, opts = {}) {
1779
- return fetch(`${baseUrl}${routeControlPlanePath(path)}`, {
1797
+ return fetch(appendPathToUrl(baseUrl, routeControlPlanePath(path)), {
1780
1798
  ...opts,
1781
1799
  headers: {
1782
1800
  "content-type": `application/json`,
@@ -1797,7 +1815,7 @@ function isEntityStreamPath(pathname) {
1797
1815
  async function pollEntityStatus(baseUrl, entityUrl, statuses, timeoutMs = 8e3) {
1798
1816
  const deadline = Date.now() + timeoutMs;
1799
1817
  while (Date.now() < deadline) {
1800
- const res = await fetch(`${baseUrl}${routeControlPlanePath(entityUrl)}`);
1818
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(entityUrl)));
1801
1819
  (0, vitest.expect)(res.status).toBe(200);
1802
1820
  const entity = await res.json();
1803
1821
  if (statuses.includes(String(entity.status))) return entity;
@@ -1812,13 +1830,13 @@ function runElectricAgentsConformanceTests(config) {
1812
1830
  description: `Test entity type for spawn`,
1813
1831
  creation_schema: { type: `object` }
1814
1832
  }).spawn(`spawn-test-agent`, `entity-1`).expectStatus(`running`).custom(async (ctx) => {
1815
- const mainRes = await fetch(`${ctx.baseUrl}${ctx.currentEntityStreams.main}`, { method: `HEAD` });
1833
+ const mainRes = await fetch(appendPathToUrl(ctx.baseUrl, ctx.currentEntityStreams.main), { method: `HEAD` });
1816
1834
  (0, vitest.expect)(mainRes.status).toBe(200);
1817
- const errorRes = await fetch(`${ctx.baseUrl}${ctx.currentEntityStreams.error}`, { method: `HEAD` });
1835
+ const errorRes = await fetch(appendPathToUrl(ctx.baseUrl, ctx.currentEntityStreams.error), { method: `HEAD` });
1818
1836
  (0, vitest.expect)(errorRes.status).toBe(200);
1819
1837
  }).run());
1820
1838
  (0, vitest.test)(`spawn at unregistered type returns UNKNOWN_ENTITY_TYPE`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
1821
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/unregistered/entity-1`)}`, {
1839
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/unregistered/entity-1`)), {
1822
1840
  method: `PUT`,
1823
1841
  headers: { "content-type": `application/json` },
1824
1842
  body: JSON.stringify({})
@@ -1860,7 +1878,7 @@ function runElectricAgentsConformanceTests(config) {
1860
1878
  creation_schema: { type: `object` }
1861
1879
  }).spawn(`webhook-ctx-agent`, `entity-1`).send({ ping: true }).expectWebhook().expectEntityContext({ type: `webhook-ctx-agent` }).respondDone().run());
1862
1880
  (0, vitest.test)(`send to nonexistent entity returns 404`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
1863
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/nonexistent-type/nonexistent-id/send`)}`, {
1881
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/nonexistent-type/nonexistent-id/send`)), {
1864
1882
  method: `POST`,
1865
1883
  headers: { "content-type": `application/json` },
1866
1884
  body: JSON.stringify({ payload: {} })
@@ -1986,12 +2004,12 @@ function runElectricAgentsConformanceTests(config) {
1986
2004
  type: `object`,
1987
2005
  properties: { name: { type: `string` } }
1988
2006
  },
1989
- input_schemas: { query: {
2007
+ inbox_schemas: { query: {
1990
2008
  type: `object`,
1991
2009
  properties: { text: { type: `string` } },
1992
2010
  required: [`text`]
1993
2011
  } },
1994
- output_schemas: { result: {
2012
+ state_schemas: { result: {
1995
2013
  type: `object`,
1996
2014
  properties: { answer: { type: `string` } }
1997
2015
  } }
@@ -2027,7 +2045,7 @@ function runElectricAgentsConformanceTests(config) {
2027
2045
  type: `object`,
2028
2046
  properties: { x: { type: `number` } }
2029
2047
  },
2030
- input_schemas: { ping: {
2048
+ inbox_schemas: { ping: {
2031
2049
  type: `object`,
2032
2050
  properties: { msg: { type: `string` } }
2033
2051
  } }
@@ -2053,7 +2071,7 @@ function runElectricAgentsConformanceTests(config) {
2053
2071
  description: `First registration`,
2054
2072
  creation_schema: { type: `object` }
2055
2073
  }).custom(async (ctx) => {
2056
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2074
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2057
2075
  method: `POST`,
2058
2076
  headers: { "content-type": `application/json` },
2059
2077
  body: JSON.stringify({
@@ -2070,7 +2088,7 @@ function runElectricAgentsConformanceTests(config) {
2070
2088
  }).run();
2071
2089
  });
2072
2090
  (0, vitest.test)(`register rejects missing required fields`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
2073
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2091
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2074
2092
  method: `POST`,
2075
2093
  headers: { "content-type": `application/json` },
2076
2094
  body: JSON.stringify({})
@@ -2115,7 +2133,7 @@ function runElectricAgentsConformanceTests(config) {
2115
2133
  }).expectSpawnSchemaError(typeName, `bad-entity-1`, { args: { invalid: true } }).run();
2116
2134
  });
2117
2135
  (0, vitest.test)(`typed spawn at unregistered type returns UNKNOWN_ENTITY_TYPE (C9)`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
2118
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/nonexistent/should-fail`)}`, {
2136
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/nonexistent/should-fail`)), {
2119
2137
  method: `PUT`,
2120
2138
  headers: { "content-type": `application/json` },
2121
2139
  body: JSON.stringify({})
@@ -2134,7 +2152,7 @@ function runElectricAgentsConformanceTests(config) {
2134
2152
  }).spawn(typeName, `parent-entity`).custom(async (ctx) => {
2135
2153
  parentUrl = ctx.currentEntityUrl;
2136
2154
  }).custom(async (ctx) => {
2137
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/child-entity`)}`, {
2155
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/child-entity`)), {
2138
2156
  method: `PUT`,
2139
2157
  headers: { "content-type": `application/json` },
2140
2158
  body: JSON.stringify({ parent: parentUrl })
@@ -2161,7 +2179,7 @@ function runElectricAgentsConformanceTests(config) {
2161
2179
  description: `Type for orphan spawn test`,
2162
2180
  creation_schema: { type: `object` }
2163
2181
  }).custom(async (ctx) => {
2164
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/orphan-entity`)}`, {
2182
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/orphan-entity`)), {
2165
2183
  method: `PUT`,
2166
2184
  headers: { "content-type": `application/json` },
2167
2185
  body: JSON.stringify({ parent: `/nonexistent/parent` })
@@ -2182,7 +2200,7 @@ function runElectricAgentsConformanceTests(config) {
2182
2200
  color: `blue`,
2183
2201
  count: `42`
2184
2202
  } }).custom(async (ctx) => {
2185
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
2203
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
2186
2204
  const entity = await res.json();
2187
2205
  const tags = entity.tags;
2188
2206
  (0, vitest.expect)(tags.color).toBe(`blue`);
@@ -2196,7 +2214,7 @@ function runElectricAgentsConformanceTests(config) {
2196
2214
  name: typeName,
2197
2215
  description: `Type with string-only tags`
2198
2216
  }).custom(async (ctx) => {
2199
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/should-fail`)}`, {
2217
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/should-fail`)), {
2200
2218
  method: `PUT`,
2201
2219
  headers: { "content-type": `application/json` },
2202
2220
  body: JSON.stringify({ tags: { wrong: 123 } })
@@ -2210,20 +2228,20 @@ function runElectricAgentsConformanceTests(config) {
2210
2228
  name: typeName,
2211
2229
  description: `Type with default empty tags`
2212
2230
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2213
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
2231
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
2214
2232
  const entity = await res.json();
2215
2233
  (0, vitest.expect)(entity.tags).toEqual({});
2216
2234
  }).run();
2217
2235
  });
2218
2236
  });
2219
2237
  (0, vitest.describe)(`Electric Agents Schema Validation Gates`, () => {
2220
- (0, vitest.test)(`send validates input_schemas (C11)`, () => {
2238
+ (0, vitest.test)(`send validates inbox_schemas (C11)`, () => {
2221
2239
  const typeName = `send-schema-valid-${Date.now()}`;
2222
2240
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-schema-sub`).registerType({
2223
2241
  name: typeName,
2224
- description: `Type with input schemas`,
2242
+ description: `Type with inbox schemas`,
2225
2243
  creation_schema: { type: `object` },
2226
- input_schemas: { query: {
2244
+ inbox_schemas: { query: {
2227
2245
  type: `object`,
2228
2246
  properties: { text: { type: `string` } },
2229
2247
  required: [`text`]
@@ -2234,9 +2252,9 @@ function runElectricAgentsConformanceTests(config) {
2234
2252
  const typeName = `send-schema-inv-${Date.now()}`;
2235
2253
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-schema-inv-sub`).registerType({
2236
2254
  name: typeName,
2237
- description: `Type with strict input schemas`,
2255
+ description: `Type with strict inbox schemas`,
2238
2256
  creation_schema: { type: `object` },
2239
- input_schemas: { query: {
2257
+ inbox_schemas: { query: {
2240
2258
  type: `object`,
2241
2259
  properties: { text: { type: `string` } },
2242
2260
  required: [`text`]
@@ -2247,30 +2265,30 @@ function runElectricAgentsConformanceTests(config) {
2247
2265
  const typeName = `send-unknown-type-${Date.now()}`;
2248
2266
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-unknown-sub`).registerType({
2249
2267
  name: typeName,
2250
- description: `Type with defined input schemas`,
2268
+ description: `Type with defined inbox schemas`,
2251
2269
  creation_schema: { type: `object` },
2252
- input_schemas: { query: {
2270
+ inbox_schemas: { query: {
2253
2271
  type: `object`,
2254
2272
  properties: { text: { type: `string` } },
2255
2273
  required: [`text`]
2256
2274
  } }
2257
2275
  }).spawn(typeName, `entity-1`).expectSendUnknownType({ text: `hi` }, { type: `unknown_type` }).run();
2258
2276
  });
2259
- (0, vitest.test)(`send without type when no input_schemas accepts any`, () => {
2277
+ (0, vitest.test)(`send without type when no inbox_schemas accepts any`, () => {
2260
2278
  const typeName = `send-no-schemas-${Date.now()}`;
2261
2279
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-noschema-sub`).registerType({
2262
2280
  name: typeName,
2263
- description: `Type without input schemas`,
2281
+ description: `Type without inbox schemas`,
2264
2282
  creation_schema: { type: `object` }
2265
2283
  }).spawn(typeName, `entity-1`).send({ anything: `goes` }).expectWebhook().respondDone().run();
2266
2284
  });
2267
- (0, vitest.test)(`send with empty input_schemas rejects all`, () => {
2285
+ (0, vitest.test)(`send with empty inbox_schemas rejects all`, () => {
2268
2286
  const typeName = `send-empty-schemas-${Date.now()}`;
2269
2287
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-empty-sub`).registerType({
2270
2288
  name: typeName,
2271
- description: `Type with empty input schemas`,
2289
+ description: `Type with empty inbox schemas`,
2272
2290
  creation_schema: { type: `object` },
2273
- input_schemas: {}
2291
+ inbox_schemas: {}
2274
2292
  }).spawn(typeName, `entity-1`).expectSendUnknownType({ text: `anything` }, { type: `some_type` }).run();
2275
2293
  });
2276
2294
  vitest.test.skip(`write appends event to entity stream`, () => {
@@ -2279,7 +2297,7 @@ function runElectricAgentsConformanceTests(config) {
2279
2297
  name: typeName,
2280
2298
  description: `Type for write test`,
2281
2299
  creation_schema: { type: `object` },
2282
- output_schemas: { research_result: {
2300
+ state_schemas: { research_result: {
2283
2301
  type: `object`,
2284
2302
  properties: { findings: {
2285
2303
  type: `array`,
@@ -2288,13 +2306,13 @@ function runElectricAgentsConformanceTests(config) {
2288
2306
  } }
2289
2307
  }).spawn(typeName, `entity-1`).write({ findings: [`test`] }, { type: `research_result` }).readStream().expectStreamContains(`research_result`).run();
2290
2308
  });
2291
- vitest.test.skip(`write validates output_schemas (C12)`, () => {
2309
+ vitest.test.skip(`write validates state_schemas (C12)`, () => {
2292
2310
  const typeName = `write-schema-inv-${Date.now()}`;
2293
2311
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-schema-sub`).registerType({
2294
2312
  name: typeName,
2295
- description: `Type with strict output schemas`,
2313
+ description: `Type with strict state schemas`,
2296
2314
  creation_schema: { type: `object` },
2297
- output_schemas: { result: {
2315
+ state_schemas: { result: {
2298
2316
  type: `object`,
2299
2317
  properties: { value: { type: `number` } },
2300
2318
  required: [`value`]
@@ -2305,19 +2323,19 @@ function runElectricAgentsConformanceTests(config) {
2305
2323
  const typeName = `write-unknown-type-${Date.now()}`;
2306
2324
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-unknown-sub`).registerType({
2307
2325
  name: typeName,
2308
- description: `Type with defined output schemas`,
2326
+ description: `Type with defined state schemas`,
2309
2327
  creation_schema: { type: `object` },
2310
- output_schemas: { result: {
2328
+ state_schemas: { result: {
2311
2329
  type: `object`,
2312
2330
  properties: { value: { type: `number` } }
2313
2331
  } }
2314
2332
  }).spawn(typeName, `entity-1`).expectWriteUnknownType({ data: `test` }, { type: `unknown_event` }).run();
2315
2333
  });
2316
- vitest.test.skip(`write without type when no output_schemas accepts any`, () => {
2334
+ vitest.test.skip(`write without type when no state_schemas accepts any`, () => {
2317
2335
  const typeName = `write-no-schemas-${Date.now()}`;
2318
2336
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-noschema-sub`).registerType({
2319
2337
  name: typeName,
2320
- description: `Type without output schemas`,
2338
+ description: `Type without state schemas`,
2321
2339
  creation_schema: { type: `object` }
2322
2340
  }).spawn(typeName, `entity-1`).write({ anything: `goes` }).run();
2323
2341
  });
@@ -2330,7 +2348,7 @@ function runElectricAgentsConformanceTests(config) {
2330
2348
  }).spawn(typeName, `entity-1`).kill().custom(async (ctx) => {
2331
2349
  const writeHeaders = { "content-type": `application/json` };
2332
2350
  if (ctx.currentWriteToken) writeHeaders[`authorization`] = `Bearer ${ctx.currentWriteToken}`;
2333
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
2351
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
2334
2352
  method: `POST`,
2335
2353
  headers: writeHeaders,
2336
2354
  body: JSON.stringify({
@@ -2384,7 +2402,7 @@ function runElectricAgentsConformanceTests(config) {
2384
2402
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2385
2403
  const tagHeaders = { "content-type": `application/json` };
2386
2404
  if (ctx.currentWriteToken) tagHeaders[`authorization`] = `Bearer ${ctx.currentWriteToken}`;
2387
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/owner`)}`, {
2405
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/owner`)), {
2388
2406
  method: `POST`,
2389
2407
  headers: tagHeaders,
2390
2408
  body: JSON.stringify({ value: 123 })
@@ -2404,7 +2422,7 @@ function runElectricAgentsConformanceTests(config) {
2404
2422
  }).custom(async (ctx) => {
2405
2423
  const tagHeaders = {};
2406
2424
  if (ctx.currentWriteToken) tagHeaders.authorization = `Bearer ${ctx.currentWriteToken}`;
2407
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/priority`)}`, {
2425
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/priority`)), {
2408
2426
  method: `DELETE`,
2409
2427
  headers: tagHeaders
2410
2428
  });
@@ -2437,12 +2455,12 @@ function runElectricAgentsConformanceTests(config) {
2437
2455
  name: typeName,
2438
2456
  description: `Type for schema amendment`,
2439
2457
  creation_schema: { type: `object` },
2440
- input_schemas: { query: {
2458
+ inbox_schemas: { query: {
2441
2459
  type: `object`,
2442
2460
  properties: { text: { type: `string` } },
2443
2461
  required: [`text`]
2444
2462
  } }
2445
- }).amendSchemas(typeName, { input_schemas: { command: {
2463
+ }).amendSchemas(typeName, { inbox_schemas: { command: {
2446
2464
  type: `object`,
2447
2465
  properties: { action: { type: `string` } },
2448
2466
  required: [`action`]
@@ -2454,13 +2472,13 @@ function runElectricAgentsConformanceTests(config) {
2454
2472
  name: typeName,
2455
2473
  description: `Type for schema conflict test`,
2456
2474
  creation_schema: { type: `object` },
2457
- input_schemas: { query: {
2475
+ inbox_schemas: { query: {
2458
2476
  type: `object`,
2459
2477
  properties: { text: { type: `string` } },
2460
2478
  required: [`text`]
2461
2479
  } }
2462
2480
  }).custom(async (ctx) => {
2463
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types/${typeName}/schemas`, {
2481
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types/${typeName}/schemas`), {
2464
2482
  method: `PATCH`,
2465
2483
  headers: { "content-type": `application/json` },
2466
2484
  body: JSON.stringify({ inbox_schemas: { query: {
@@ -2477,13 +2495,13 @@ function runElectricAgentsConformanceTests(config) {
2477
2495
  name: typeName,
2478
2496
  description: `Type for revision pinning test`,
2479
2497
  creation_schema: { type: `object` },
2480
- input_schemas: { query: {
2498
+ inbox_schemas: { query: {
2481
2499
  type: `object`,
2482
2500
  properties: { text: { type: `string` } },
2483
2501
  required: [`text`]
2484
2502
  } }
2485
2503
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2486
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types/${typeName}/schemas`, {
2504
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types/${typeName}/schemas`), {
2487
2505
  method: `PATCH`,
2488
2506
  headers: { "content-type": `application/json` },
2489
2507
  body: JSON.stringify({ inbox_schemas: { new_command: {
@@ -2493,7 +2511,7 @@ function runElectricAgentsConformanceTests(config) {
2493
2511
  });
2494
2512
  (0, vitest.expect)(res.status).toBe(200);
2495
2513
  }).custom(async (ctx) => {
2496
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/send`)}`, {
2514
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/send`)), {
2497
2515
  method: `POST`,
2498
2516
  headers: { "content-type": `application/json` },
2499
2517
  body: JSON.stringify({
@@ -2524,7 +2542,7 @@ function runElectricAgentsConformanceTests(config) {
2524
2542
  };
2525
2543
  await receiver.start(manifest);
2526
2544
  try {
2527
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2545
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2528
2546
  method: `POST`,
2529
2547
  headers: { "content-type": `application/json` },
2530
2548
  body: JSON.stringify({
@@ -3102,7 +3120,7 @@ function runElectricAgentsConformanceTests(config) {
3102
3120
  name: `rev-multi-agent-${id}`,
3103
3121
  description: `Test multi-revision pinning`,
3104
3122
  creation_schema: { type: `object` },
3105
- input_schemas: { greet: {
3123
+ inbox_schemas: { greet: {
3106
3124
  type: `object`,
3107
3125
  properties: { name: { type: `string` } },
3108
3126
  required: [`name`]
@@ -3143,7 +3161,7 @@ function runElectricAgentsConformanceTests(config) {
3143
3161
  }).deleteType(`amend-del-agent-${id}`).custom(async (ctx) => {
3144
3162
  const res = await electricAgentsFetch(ctx.baseUrl, `/_electric/entity-types/amend-del-agent-${id}/schemas`, {
3145
3163
  method: `PATCH`,
3146
- body: JSON.stringify({ input_schemas: { msg: { type: `object` } } })
3164
+ body: JSON.stringify({ inbox_schemas: { msg: { type: `object` } } })
3147
3165
  });
3148
3166
  (0, vitest.expect)(res.status).toBe(404);
3149
3167
  }).run();
@@ -3219,7 +3237,7 @@ function runElectricAgentsConformanceTests(config) {
3219
3237
  description: `Test write without token`,
3220
3238
  creation_schema: { type: `object` }
3221
3239
  }).spawn(`auth-notoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3222
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
3240
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
3223
3241
  method: `POST`,
3224
3242
  headers: { "content-type": `application/json` },
3225
3243
  body: JSON.stringify({
@@ -3239,7 +3257,7 @@ function runElectricAgentsConformanceTests(config) {
3239
3257
  description: `Test write with wrong token`,
3240
3258
  creation_schema: { type: `object` }
3241
3259
  }).spawn(`auth-wrongtoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3242
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
3260
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
3243
3261
  method: `POST`,
3244
3262
  headers: {
3245
3263
  "content-type": `application/json`,
@@ -3272,7 +3290,7 @@ function runElectricAgentsConformanceTests(config) {
3272
3290
  description: `Test tag update without token`,
3273
3291
  creation_schema: { type: `object` }
3274
3292
  }).spawn(`auth-meta-notoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3275
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/key`)}`, {
3293
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/key`)), {
3276
3294
  method: `POST`,
3277
3295
  headers: { "content-type": `application/json` },
3278
3296
  body: JSON.stringify({ value: `value` })
@@ -3297,7 +3315,7 @@ function runElectricAgentsConformanceTests(config) {
3297
3315
  description: `Test send without auth`,
3298
3316
  creation_schema: { type: `object` }
3299
3317
  }).spawn(`auth-send-noauth-agent-${id}`, `entity-1`).custom(async (ctx) => {
3300
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/send`)}`, {
3318
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/send`)), {
3301
3319
  method: `POST`,
3302
3320
  headers: { "content-type": `application/json` },
3303
3321
  body: JSON.stringify({ payload: `hi` })
@@ -3312,7 +3330,7 @@ function runElectricAgentsConformanceTests(config) {
3312
3330
  description: `Test GET does not leak write_token`,
3313
3331
  creation_schema: { type: `object` }
3314
3332
  }).spawn(`auth-noleak-agent-${id}`, `entity-1`).custom(async (ctx) => {
3315
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
3333
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
3316
3334
  (0, vitest.expect)(res.status).toBe(200);
3317
3335
  const entity = await res.json();
3318
3336
  (0, vitest.expect)(entity.write_token).toBeUndefined();
@@ -3382,7 +3400,7 @@ function runCliConformanceTests(config) {
3382
3400
  name: `cli-spawn-type`,
3383
3401
  description: `Type for CLI spawn test`
3384
3402
  }).setupSubscription(`/cli-spawn-type/**`, `cli-spawn-sub`).exec(`spawn`, `/cli-spawn-type/${id}`).expectExitCode(0).expectStdout(/Spawned/).verifyApi(async (baseUrl) => {
3385
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-spawn-type/${id}`)}`);
3403
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-spawn-type/${id}`)));
3386
3404
  (0, vitest.expect)(res.status).toBe(200);
3387
3405
  const entity = await res.json();
3388
3406
  (0, vitest.expect)([`running`, `idle`]).toContain(entity.status);
@@ -3425,11 +3443,11 @@ function runCliConformanceTests(config) {
3425
3443
  name: `cli-send-type`,
3426
3444
  description: `Type for CLI send test`
3427
3445
  }).setupSubscription(`/cli-send-type/**`, `cli-send-sub`).exec(`spawn`, `/cli-send-type/${id}`).expectExitCode(0).exec(`send`, `/cli-send-type/${id}`, `hello world`).expectExitCode(0).expectStdout(/Message sent/).verifyApi(async (baseUrl) => {
3428
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-send-type/${id}`)}`);
3446
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-send-type/${id}`)));
3429
3447
  (0, vitest.expect)(res.status).toBe(200);
3430
3448
  const entity = await res.json();
3431
3449
  const streams = entity.streams;
3432
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3450
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3433
3451
  const events = await streamRes.json();
3434
3452
  (0, vitest.expect)(events.length).toBeGreaterThanOrEqual(1);
3435
3453
  const msgEvent = events.find((e) => e.type === `inbox`);
@@ -3449,7 +3467,7 @@ function runCliConformanceTests(config) {
3449
3467
  name: `cli-inspect-etype`,
3450
3468
  description: `Type for CLI inspect test`
3451
3469
  }).setupSubscription(`/cli-inspect-etype/**`, `cli-inspect-sub`).exec(`spawn`, `/cli-inspect-etype/${id}`).expectExitCode(0).exec(`inspect`, `/cli-inspect-etype/${id}`).expectExitCode(0).expectStdout(/running|idle/).verifyApi(async (baseUrl) => {
3452
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-inspect-etype/${id}`)}`);
3470
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-inspect-etype/${id}`)));
3453
3471
  (0, vitest.expect)(res.status).toBe(200);
3454
3472
  const entity = await res.json();
3455
3473
  (0, vitest.expect)([`running`, `idle`]).toContain(entity.status);
@@ -3481,7 +3499,7 @@ function runCliConformanceTests(config) {
3481
3499
  name: `cli-lifecycle-type`,
3482
3500
  description: `Type for full lifecycle test`
3483
3501
  }).setupSubscription(`/cli-lifecycle-type/**`, `cli-lifecycle-sub`).exec(`spawn`, `/cli-lifecycle-type/${id}`).expectExitCode(0).expectStdout(/Spawned/).verifyApi(async (baseUrl) => {
3484
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-lifecycle-type/${id}`)}`);
3502
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-lifecycle-type/${id}`)));
3485
3503
  (0, vitest.expect)(res.status, `entity should exist after spawn`).toBe(200);
3486
3504
  const entity = await res.json();
3487
3505
  (0, vitest.expect)([`running`, `idle`]).toContain(entity.status);
@@ -3512,7 +3530,7 @@ function runCliConformanceTests(config) {
3512
3530
  }
3513
3531
  function runMockAgentTests(config) {
3514
3532
  async function spawnEntity(baseUrl, typeName, instanceId) {
3515
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/${encodeURIComponent(typeName)}/${encodeURIComponent(instanceId)}`)}`, {
3533
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/${encodeURIComponent(typeName)}/${encodeURIComponent(instanceId)}`)), {
3516
3534
  method: `PUT`,
3517
3535
  headers: { "content-type": `application/json` },
3518
3536
  body: JSON.stringify({})
@@ -3521,7 +3539,7 @@ function runMockAgentTests(config) {
3521
3539
  return await res.json();
3522
3540
  }
3523
3541
  async function sendMessage(baseUrl, entityUrl, text) {
3524
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`${entityUrl}/send`)}`, {
3542
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`${entityUrl}/send`)), {
3525
3543
  method: `POST`,
3526
3544
  headers: { "content-type": `application/json` },
3527
3545
  body: JSON.stringify({ payload: { text } })
@@ -3531,11 +3549,11 @@ function runMockAgentTests(config) {
3531
3549
  async function pollForAgentResponse(baseUrl, entityUrl, timeoutMs = 1e4) {
3532
3550
  const start = Date.now();
3533
3551
  while (Date.now() - start < timeoutMs) {
3534
- const entityRes = await fetch(`${baseUrl}${routeControlPlanePath(entityUrl)}`);
3552
+ const entityRes = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(entityUrl)));
3535
3553
  if (!entityRes.ok) throw new Error(`Entity ${entityUrl} not found`);
3536
3554
  const entity = await entityRes.json();
3537
3555
  const streams = entity.streams;
3538
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3556
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3539
3557
  const events = await streamRes.json();
3540
3558
  const hasRunComplete = events.some((e) => e.type === `run` && e.headers?.operation === `update`);
3541
3559
  if (hasRunComplete) return events;
@@ -3588,11 +3606,11 @@ function runMockAgentCliTests(config) {
3588
3606
  (0, vitest.test)(`spawn → send → agent responds with State Protocol events`, async () => {
3589
3607
  const id = `cli-mock-${Date.now()}`;
3590
3608
  await cliTest(config.baseUrl, config.cliBin).exec(`spawn`, `/chat/${id}`).expectExitCode(0).expectStdout(/Spawned/).exec(`send`, `/chat/${id}`, `hello from CLI`).expectExitCode(0).expectStdout(/Message sent/).wait(3e3).verifyApi(async (baseUrl) => {
3591
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/chat/${id}`)}`);
3609
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/chat/${id}`)));
3592
3610
  (0, vitest.expect)(res.status, `entity should exist`).toBe(200);
3593
3611
  const entity = await res.json();
3594
3612
  const streams = entity.streams;
3595
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3613
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3596
3614
  const events = await streamRes.json();
3597
3615
  (0, vitest.expect)(events.some((e) => e.type === `run`), `stream should contain run events from agent`).toBe(true);
3598
3616
  (0, vitest.expect)(events.some((e) => e.type === `text`), `stream should contain text events from agent`).toBe(true);
@@ -3602,11 +3620,11 @@ function runMockAgentCliTests(config) {
3602
3620
  (0, vitest.test)(`inspect shows entity after mock agent processes message`, async () => {
3603
3621
  const id = `cli-inspect-mock-${Date.now()}`;
3604
3622
  await cliTest(config.baseUrl, config.cliBin).exec(`spawn`, `/chat/${id}`).expectExitCode(0).exec(`send`, `/chat/${id}`, `test message`).expectExitCode(0).wait(3e3).exec(`inspect`, `/chat/${id}`).expectExitCode(0).expectStdout(/running|idle/).verifyApi(async (baseUrl) => {
3605
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/chat/${id}`)}`);
3623
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/chat/${id}`)));
3606
3624
  (0, vitest.expect)(res.status).toBe(200);
3607
3625
  const entity = await res.json();
3608
3626
  const streams = entity.streams;
3609
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3627
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3610
3628
  const events = await streamRes.json();
3611
3629
  const hasRunComplete = events.some((e) => e.type === `run` && e.headers?.operation === `update`);
3612
3630
  (0, vitest.expect)(hasRunComplete, `mock agent should have written run completion event`).toBe(true);