@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.js CHANGED
@@ -4,10 +4,30 @@ import { createServer } from "node:http";
4
4
  import { Shape, ShapeStream } from "@electric-sql/client";
5
5
  import { execFile } from "node:child_process";
6
6
 
7
+ //#region src/url.ts
8
+ function appendPathToUrl(baseUrl, path) {
9
+ const base = new URL(baseUrl);
10
+ const pathUrl = new URL(path, `http://electric-agents.local`);
11
+ const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
12
+ const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
13
+ const target = new URL(base);
14
+ target.pathname = `${basePath}${suffix}`;
15
+ target.search = ``;
16
+ target.hash = pathUrl.hash;
17
+ base.searchParams.forEach((value, key) => {
18
+ target.searchParams.append(key, value);
19
+ });
20
+ pathUrl.searchParams.forEach((value, key) => {
21
+ target.searchParams.append(key, value);
22
+ });
23
+ return target.toString();
24
+ }
25
+
26
+ //#endregion
7
27
  //#region src/electric-agents-dsl.ts
8
28
  async function fetchShapeRows(baseUrl, table) {
9
29
  const stream = new ShapeStream({
10
- url: `${baseUrl}/_electric/electric/v1/shape`,
30
+ url: appendPathToUrl(baseUrl, `/_electric/electric/v1/shape`),
11
31
  params: { table },
12
32
  subscribe: false
13
33
  });
@@ -30,10 +50,8 @@ function toServerEntityTypeRegistration(registration) {
30
50
  ...registration.metadata_schema && { metadata_schema: registration.metadata_schema },
31
51
  ...registration.serve_endpoint && { serve_endpoint: registration.serve_endpoint }
32
52
  };
33
- const inboxSchemas = registration.inbox_schemas ?? registration.input_schemas;
34
- const stateSchemas = registration.state_schemas ?? registration.output_schemas;
35
- if (inboxSchemas) body.inbox_schemas = inboxSchemas;
36
- if (stateSchemas) body.state_schemas = stateSchemas;
53
+ if (registration.inbox_schemas) body.inbox_schemas = registration.inbox_schemas;
54
+ if (registration.state_schemas) body.state_schemas = registration.state_schemas;
37
55
  return body;
38
56
  }
39
57
  function normalizeWebhookPayload(body) {
@@ -174,7 +192,7 @@ var ServeEndpointReceiver = class {
174
192
  }
175
193
  };
176
194
  async function electricAgentsFetch$1(baseUrl, path, opts = {}) {
177
- return fetch(`${baseUrl}${routeControlPlanePath$1(path)}`, {
195
+ return fetch(appendPathToUrl(baseUrl, routeControlPlanePath$1(path)), {
178
196
  ...opts,
179
197
  headers: {
180
198
  "content-type": `application/json`,
@@ -193,7 +211,7 @@ function isEntityStreamPath$1(pathname) {
193
211
  return segments.length >= 3 && (lastSegment === `main` || lastSegment === `error`);
194
212
  }
195
213
  function subscriptionEndpoint$1(baseUrl, id) {
196
- return `${baseUrl}/__ds/subscriptions/${encodeURIComponent(id)}`;
214
+ return appendPathToUrl(baseUrl, `/__ds/subscriptions/${encodeURIComponent(id)}`);
197
215
  }
198
216
  function subscriptionPattern$1(pattern) {
199
217
  return pattern.replace(/^\/+/, ``);
@@ -353,8 +371,8 @@ var ElectricAgentsScenario = class {
353
371
  this.steps.push({
354
372
  kind: `amendSchemas`,
355
373
  name,
356
- input_schemas: schemas.input_schemas,
357
- output_schemas: schemas.output_schemas
374
+ inbox_schemas: schemas.inbox_schemas,
375
+ state_schemas: schemas.state_schemas
358
376
  });
359
377
  return this;
360
378
  }
@@ -422,7 +440,7 @@ var ElectricAgentsScenario = class {
422
440
  typeName,
423
441
  instanceId,
424
442
  args: opts?.args,
425
- code: `SCHEMA_VALIDATION_ERROR`,
443
+ code: `SCHEMA_VALIDATION_FAILED`,
426
444
  status: 422
427
445
  });
428
446
  return this;
@@ -432,7 +450,7 @@ var ElectricAgentsScenario = class {
432
450
  kind: `expectSendSchemaError`,
433
451
  payload,
434
452
  messageType: opts?.type ?? `default`,
435
- code: `SCHEMA_VALIDATION_ERROR`,
453
+ code: `SCHEMA_VALIDATION_FAILED`,
436
454
  status: 422
437
455
  });
438
456
  return this;
@@ -442,7 +460,7 @@ var ElectricAgentsScenario = class {
442
460
  kind: `expectWriteSchemaError`,
443
461
  payload,
444
462
  eventType: opts?.type ?? `default`,
445
- code: `SCHEMA_VALIDATION_ERROR`,
463
+ code: `SCHEMA_VALIDATION_FAILED`,
446
464
  status: 422
447
465
  });
448
466
  return this;
@@ -671,7 +689,7 @@ async function executeStep(ctx, step) {
671
689
  case `readStream`: {
672
690
  if (!ctx.currentEntityStreams) throw new Error(`No current entity streams`);
673
691
  const streamPath = step.stream === `error` ? ctx.currentEntityStreams.error : ctx.currentEntityStreams.main;
674
- const res = await fetch(`${ctx.baseUrl}${streamPath}?offset=0000000000000000_0000000000000000`);
692
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${streamPath}?offset=0000000000000000_0000000000000000`));
675
693
  if (res.status === 200) {
676
694
  const text = await res.text();
677
695
  const messages = text ? JSON.parse(text) : [];
@@ -812,8 +830,8 @@ async function executeStep(ctx, step) {
812
830
  }
813
831
  case `amendSchemas`: {
814
832
  const body = {};
815
- if (step.input_schemas) body.inbox_schemas = step.input_schemas;
816
- if (step.output_schemas) body.state_schemas = step.output_schemas;
833
+ if (step.inbox_schemas) body.inbox_schemas = step.inbox_schemas;
834
+ if (step.state_schemas) body.state_schemas = step.state_schemas;
817
835
  const res = await electricAgentsFetch$1(ctx.baseUrl, `/_electric/entity-types/${step.name}/schemas`, {
818
836
  method: `PATCH`,
819
837
  body: JSON.stringify(body)
@@ -1315,8 +1333,8 @@ function applyElectricAgentsAction(model, action, targetIdx, opts) {
1315
1333
  entityTypes: [...model.entityTypes, {
1316
1334
  name: opts?.typeName ?? `prop-type-${typeNum}`,
1317
1335
  hasCreationSchema: false,
1318
- hasInputSchemas: false,
1319
- hasOutputSchemas: false
1336
+ hasInboxSchemas: false,
1337
+ hasStateSchemas: false
1320
1338
  }]
1321
1339
  };
1322
1340
  }
@@ -1474,7 +1492,7 @@ function checkStateProtocolInvariants(events) {
1474
1492
  //#endregion
1475
1493
  //#region src/cli-dsl.ts
1476
1494
  function subscriptionEndpoint(baseUrl, id) {
1477
- return `${baseUrl}/__ds/subscriptions/${encodeURIComponent(id)}`;
1495
+ return appendPathToUrl(baseUrl, `/__ds/subscriptions/${encodeURIComponent(id)}`);
1478
1496
  }
1479
1497
  function subscriptionPattern(pattern) {
1480
1498
  return pattern.replace(/^\/+/, ``);
@@ -1623,7 +1641,7 @@ var CliScenario = class {
1623
1641
  try {
1624
1642
  for (const step of this.steps) switch (step.kind) {
1625
1643
  case `setupType`: {
1626
- const res = await fetch(`${this.baseUrl}/_electric/entity-types`, {
1644
+ const res = await fetch(appendPathToUrl(this.baseUrl, `/_electric/entity-types`), {
1627
1645
  method: `POST`,
1628
1646
  headers: { "content-type": `application/json` },
1629
1647
  body: JSON.stringify(step.registration)
@@ -1752,7 +1770,7 @@ function startNoopReceiver() {
1752
1770
  //#endregion
1753
1771
  //#region src/electric-agents-tests.ts
1754
1772
  async function electricAgentsFetch(baseUrl, path, opts = {}) {
1755
- return fetch(`${baseUrl}${routeControlPlanePath(path)}`, {
1773
+ return fetch(appendPathToUrl(baseUrl, routeControlPlanePath(path)), {
1756
1774
  ...opts,
1757
1775
  headers: {
1758
1776
  "content-type": `application/json`,
@@ -1773,7 +1791,7 @@ function isEntityStreamPath(pathname) {
1773
1791
  async function pollEntityStatus(baseUrl, entityUrl, statuses, timeoutMs = 8e3) {
1774
1792
  const deadline = Date.now() + timeoutMs;
1775
1793
  while (Date.now() < deadline) {
1776
- const res = await fetch(`${baseUrl}${routeControlPlanePath(entityUrl)}`);
1794
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(entityUrl)));
1777
1795
  expect(res.status).toBe(200);
1778
1796
  const entity = await res.json();
1779
1797
  if (statuses.includes(String(entity.status))) return entity;
@@ -1788,13 +1806,13 @@ function runElectricAgentsConformanceTests(config) {
1788
1806
  description: `Test entity type for spawn`,
1789
1807
  creation_schema: { type: `object` }
1790
1808
  }).spawn(`spawn-test-agent`, `entity-1`).expectStatus(`running`).custom(async (ctx) => {
1791
- const mainRes = await fetch(`${ctx.baseUrl}${ctx.currentEntityStreams.main}`, { method: `HEAD` });
1809
+ const mainRes = await fetch(appendPathToUrl(ctx.baseUrl, ctx.currentEntityStreams.main), { method: `HEAD` });
1792
1810
  expect(mainRes.status).toBe(200);
1793
- const errorRes = await fetch(`${ctx.baseUrl}${ctx.currentEntityStreams.error}`, { method: `HEAD` });
1811
+ const errorRes = await fetch(appendPathToUrl(ctx.baseUrl, ctx.currentEntityStreams.error), { method: `HEAD` });
1794
1812
  expect(errorRes.status).toBe(200);
1795
1813
  }).run());
1796
1814
  test(`spawn at unregistered type returns UNKNOWN_ENTITY_TYPE`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
1797
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/unregistered/entity-1`)}`, {
1815
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/unregistered/entity-1`)), {
1798
1816
  method: `PUT`,
1799
1817
  headers: { "content-type": `application/json` },
1800
1818
  body: JSON.stringify({})
@@ -1836,7 +1854,7 @@ function runElectricAgentsConformanceTests(config) {
1836
1854
  creation_schema: { type: `object` }
1837
1855
  }).spawn(`webhook-ctx-agent`, `entity-1`).send({ ping: true }).expectWebhook().expectEntityContext({ type: `webhook-ctx-agent` }).respondDone().run());
1838
1856
  test(`send to nonexistent entity returns 404`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
1839
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/nonexistent-type/nonexistent-id/send`)}`, {
1857
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/nonexistent-type/nonexistent-id/send`)), {
1840
1858
  method: `POST`,
1841
1859
  headers: { "content-type": `application/json` },
1842
1860
  body: JSON.stringify({ payload: {} })
@@ -1962,12 +1980,12 @@ function runElectricAgentsConformanceTests(config) {
1962
1980
  type: `object`,
1963
1981
  properties: { name: { type: `string` } }
1964
1982
  },
1965
- input_schemas: { query: {
1983
+ inbox_schemas: { query: {
1966
1984
  type: `object`,
1967
1985
  properties: { text: { type: `string` } },
1968
1986
  required: [`text`]
1969
1987
  } },
1970
- output_schemas: { result: {
1988
+ state_schemas: { result: {
1971
1989
  type: `object`,
1972
1990
  properties: { answer: { type: `string` } }
1973
1991
  } }
@@ -2003,7 +2021,7 @@ function runElectricAgentsConformanceTests(config) {
2003
2021
  type: `object`,
2004
2022
  properties: { x: { type: `number` } }
2005
2023
  },
2006
- input_schemas: { ping: {
2024
+ inbox_schemas: { ping: {
2007
2025
  type: `object`,
2008
2026
  properties: { msg: { type: `string` } }
2009
2027
  } }
@@ -2029,7 +2047,7 @@ function runElectricAgentsConformanceTests(config) {
2029
2047
  description: `First registration`,
2030
2048
  creation_schema: { type: `object` }
2031
2049
  }).custom(async (ctx) => {
2032
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2050
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2033
2051
  method: `POST`,
2034
2052
  headers: { "content-type": `application/json` },
2035
2053
  body: JSON.stringify({
@@ -2046,7 +2064,7 @@ function runElectricAgentsConformanceTests(config) {
2046
2064
  }).run();
2047
2065
  });
2048
2066
  test(`register rejects missing required fields`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
2049
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2067
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2050
2068
  method: `POST`,
2051
2069
  headers: { "content-type": `application/json` },
2052
2070
  body: JSON.stringify({})
@@ -2091,7 +2109,7 @@ function runElectricAgentsConformanceTests(config) {
2091
2109
  }).expectSpawnSchemaError(typeName, `bad-entity-1`, { args: { invalid: true } }).run();
2092
2110
  });
2093
2111
  test(`typed spawn at unregistered type returns UNKNOWN_ENTITY_TYPE (C9)`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
2094
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/nonexistent/should-fail`)}`, {
2112
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/nonexistent/should-fail`)), {
2095
2113
  method: `PUT`,
2096
2114
  headers: { "content-type": `application/json` },
2097
2115
  body: JSON.stringify({})
@@ -2110,7 +2128,7 @@ function runElectricAgentsConformanceTests(config) {
2110
2128
  }).spawn(typeName, `parent-entity`).custom(async (ctx) => {
2111
2129
  parentUrl = ctx.currentEntityUrl;
2112
2130
  }).custom(async (ctx) => {
2113
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/child-entity`)}`, {
2131
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/child-entity`)), {
2114
2132
  method: `PUT`,
2115
2133
  headers: { "content-type": `application/json` },
2116
2134
  body: JSON.stringify({ parent: parentUrl })
@@ -2137,7 +2155,7 @@ function runElectricAgentsConformanceTests(config) {
2137
2155
  description: `Type for orphan spawn test`,
2138
2156
  creation_schema: { type: `object` }
2139
2157
  }).custom(async (ctx) => {
2140
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/orphan-entity`)}`, {
2158
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/orphan-entity`)), {
2141
2159
  method: `PUT`,
2142
2160
  headers: { "content-type": `application/json` },
2143
2161
  body: JSON.stringify({ parent: `/nonexistent/parent` })
@@ -2158,7 +2176,7 @@ function runElectricAgentsConformanceTests(config) {
2158
2176
  color: `blue`,
2159
2177
  count: `42`
2160
2178
  } }).custom(async (ctx) => {
2161
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
2179
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
2162
2180
  const entity = await res.json();
2163
2181
  const tags = entity.tags;
2164
2182
  expect(tags.color).toBe(`blue`);
@@ -2172,7 +2190,7 @@ function runElectricAgentsConformanceTests(config) {
2172
2190
  name: typeName,
2173
2191
  description: `Type with string-only tags`
2174
2192
  }).custom(async (ctx) => {
2175
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/should-fail`)}`, {
2193
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/should-fail`)), {
2176
2194
  method: `PUT`,
2177
2195
  headers: { "content-type": `application/json` },
2178
2196
  body: JSON.stringify({ tags: { wrong: 123 } })
@@ -2186,20 +2204,20 @@ function runElectricAgentsConformanceTests(config) {
2186
2204
  name: typeName,
2187
2205
  description: `Type with default empty tags`
2188
2206
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2189
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
2207
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
2190
2208
  const entity = await res.json();
2191
2209
  expect(entity.tags).toEqual({});
2192
2210
  }).run();
2193
2211
  });
2194
2212
  });
2195
2213
  describe(`Electric Agents Schema Validation Gates`, () => {
2196
- test(`send validates input_schemas (C11)`, () => {
2214
+ test(`send validates inbox_schemas (C11)`, () => {
2197
2215
  const typeName = `send-schema-valid-${Date.now()}`;
2198
2216
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-schema-sub`).registerType({
2199
2217
  name: typeName,
2200
- description: `Type with input schemas`,
2218
+ description: `Type with inbox schemas`,
2201
2219
  creation_schema: { type: `object` },
2202
- input_schemas: { query: {
2220
+ inbox_schemas: { query: {
2203
2221
  type: `object`,
2204
2222
  properties: { text: { type: `string` } },
2205
2223
  required: [`text`]
@@ -2210,9 +2228,9 @@ function runElectricAgentsConformanceTests(config) {
2210
2228
  const typeName = `send-schema-inv-${Date.now()}`;
2211
2229
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-schema-inv-sub`).registerType({
2212
2230
  name: typeName,
2213
- description: `Type with strict input schemas`,
2231
+ description: `Type with strict inbox schemas`,
2214
2232
  creation_schema: { type: `object` },
2215
- input_schemas: { query: {
2233
+ inbox_schemas: { query: {
2216
2234
  type: `object`,
2217
2235
  properties: { text: { type: `string` } },
2218
2236
  required: [`text`]
@@ -2223,30 +2241,30 @@ function runElectricAgentsConformanceTests(config) {
2223
2241
  const typeName = `send-unknown-type-${Date.now()}`;
2224
2242
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-unknown-sub`).registerType({
2225
2243
  name: typeName,
2226
- description: `Type with defined input schemas`,
2244
+ description: `Type with defined inbox schemas`,
2227
2245
  creation_schema: { type: `object` },
2228
- input_schemas: { query: {
2246
+ inbox_schemas: { query: {
2229
2247
  type: `object`,
2230
2248
  properties: { text: { type: `string` } },
2231
2249
  required: [`text`]
2232
2250
  } }
2233
2251
  }).spawn(typeName, `entity-1`).expectSendUnknownType({ text: `hi` }, { type: `unknown_type` }).run();
2234
2252
  });
2235
- test(`send without type when no input_schemas accepts any`, () => {
2253
+ test(`send without type when no inbox_schemas accepts any`, () => {
2236
2254
  const typeName = `send-no-schemas-${Date.now()}`;
2237
2255
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-noschema-sub`).registerType({
2238
2256
  name: typeName,
2239
- description: `Type without input schemas`,
2257
+ description: `Type without inbox schemas`,
2240
2258
  creation_schema: { type: `object` }
2241
2259
  }).spawn(typeName, `entity-1`).send({ anything: `goes` }).expectWebhook().respondDone().run();
2242
2260
  });
2243
- test(`send with empty input_schemas rejects all`, () => {
2261
+ test(`send with empty inbox_schemas rejects all`, () => {
2244
2262
  const typeName = `send-empty-schemas-${Date.now()}`;
2245
2263
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-empty-sub`).registerType({
2246
2264
  name: typeName,
2247
- description: `Type with empty input schemas`,
2265
+ description: `Type with empty inbox schemas`,
2248
2266
  creation_schema: { type: `object` },
2249
- input_schemas: {}
2267
+ inbox_schemas: {}
2250
2268
  }).spawn(typeName, `entity-1`).expectSendUnknownType({ text: `anything` }, { type: `some_type` }).run();
2251
2269
  });
2252
2270
  test.skip(`write appends event to entity stream`, () => {
@@ -2255,7 +2273,7 @@ function runElectricAgentsConformanceTests(config) {
2255
2273
  name: typeName,
2256
2274
  description: `Type for write test`,
2257
2275
  creation_schema: { type: `object` },
2258
- output_schemas: { research_result: {
2276
+ state_schemas: { research_result: {
2259
2277
  type: `object`,
2260
2278
  properties: { findings: {
2261
2279
  type: `array`,
@@ -2264,13 +2282,13 @@ function runElectricAgentsConformanceTests(config) {
2264
2282
  } }
2265
2283
  }).spawn(typeName, `entity-1`).write({ findings: [`test`] }, { type: `research_result` }).readStream().expectStreamContains(`research_result`).run();
2266
2284
  });
2267
- test.skip(`write validates output_schemas (C12)`, () => {
2285
+ test.skip(`write validates state_schemas (C12)`, () => {
2268
2286
  const typeName = `write-schema-inv-${Date.now()}`;
2269
2287
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-schema-sub`).registerType({
2270
2288
  name: typeName,
2271
- description: `Type with strict output schemas`,
2289
+ description: `Type with strict state schemas`,
2272
2290
  creation_schema: { type: `object` },
2273
- output_schemas: { result: {
2291
+ state_schemas: { result: {
2274
2292
  type: `object`,
2275
2293
  properties: { value: { type: `number` } },
2276
2294
  required: [`value`]
@@ -2281,19 +2299,19 @@ function runElectricAgentsConformanceTests(config) {
2281
2299
  const typeName = `write-unknown-type-${Date.now()}`;
2282
2300
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-unknown-sub`).registerType({
2283
2301
  name: typeName,
2284
- description: `Type with defined output schemas`,
2302
+ description: `Type with defined state schemas`,
2285
2303
  creation_schema: { type: `object` },
2286
- output_schemas: { result: {
2304
+ state_schemas: { result: {
2287
2305
  type: `object`,
2288
2306
  properties: { value: { type: `number` } }
2289
2307
  } }
2290
2308
  }).spawn(typeName, `entity-1`).expectWriteUnknownType({ data: `test` }, { type: `unknown_event` }).run();
2291
2309
  });
2292
- test.skip(`write without type when no output_schemas accepts any`, () => {
2310
+ test.skip(`write without type when no state_schemas accepts any`, () => {
2293
2311
  const typeName = `write-no-schemas-${Date.now()}`;
2294
2312
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-noschema-sub`).registerType({
2295
2313
  name: typeName,
2296
- description: `Type without output schemas`,
2314
+ description: `Type without state schemas`,
2297
2315
  creation_schema: { type: `object` }
2298
2316
  }).spawn(typeName, `entity-1`).write({ anything: `goes` }).run();
2299
2317
  });
@@ -2306,7 +2324,7 @@ function runElectricAgentsConformanceTests(config) {
2306
2324
  }).spawn(typeName, `entity-1`).kill().custom(async (ctx) => {
2307
2325
  const writeHeaders = { "content-type": `application/json` };
2308
2326
  if (ctx.currentWriteToken) writeHeaders[`authorization`] = `Bearer ${ctx.currentWriteToken}`;
2309
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
2327
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
2310
2328
  method: `POST`,
2311
2329
  headers: writeHeaders,
2312
2330
  body: JSON.stringify({
@@ -2360,7 +2378,7 @@ function runElectricAgentsConformanceTests(config) {
2360
2378
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2361
2379
  const tagHeaders = { "content-type": `application/json` };
2362
2380
  if (ctx.currentWriteToken) tagHeaders[`authorization`] = `Bearer ${ctx.currentWriteToken}`;
2363
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/owner`)}`, {
2381
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/owner`)), {
2364
2382
  method: `POST`,
2365
2383
  headers: tagHeaders,
2366
2384
  body: JSON.stringify({ value: 123 })
@@ -2380,7 +2398,7 @@ function runElectricAgentsConformanceTests(config) {
2380
2398
  }).custom(async (ctx) => {
2381
2399
  const tagHeaders = {};
2382
2400
  if (ctx.currentWriteToken) tagHeaders.authorization = `Bearer ${ctx.currentWriteToken}`;
2383
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/priority`)}`, {
2401
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/priority`)), {
2384
2402
  method: `DELETE`,
2385
2403
  headers: tagHeaders
2386
2404
  });
@@ -2413,12 +2431,12 @@ function runElectricAgentsConformanceTests(config) {
2413
2431
  name: typeName,
2414
2432
  description: `Type for schema amendment`,
2415
2433
  creation_schema: { type: `object` },
2416
- input_schemas: { query: {
2434
+ inbox_schemas: { query: {
2417
2435
  type: `object`,
2418
2436
  properties: { text: { type: `string` } },
2419
2437
  required: [`text`]
2420
2438
  } }
2421
- }).amendSchemas(typeName, { input_schemas: { command: {
2439
+ }).amendSchemas(typeName, { inbox_schemas: { command: {
2422
2440
  type: `object`,
2423
2441
  properties: { action: { type: `string` } },
2424
2442
  required: [`action`]
@@ -2430,13 +2448,13 @@ function runElectricAgentsConformanceTests(config) {
2430
2448
  name: typeName,
2431
2449
  description: `Type for schema conflict test`,
2432
2450
  creation_schema: { type: `object` },
2433
- input_schemas: { query: {
2451
+ inbox_schemas: { query: {
2434
2452
  type: `object`,
2435
2453
  properties: { text: { type: `string` } },
2436
2454
  required: [`text`]
2437
2455
  } }
2438
2456
  }).custom(async (ctx) => {
2439
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types/${typeName}/schemas`, {
2457
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types/${typeName}/schemas`), {
2440
2458
  method: `PATCH`,
2441
2459
  headers: { "content-type": `application/json` },
2442
2460
  body: JSON.stringify({ inbox_schemas: { query: {
@@ -2453,13 +2471,13 @@ function runElectricAgentsConformanceTests(config) {
2453
2471
  name: typeName,
2454
2472
  description: `Type for revision pinning test`,
2455
2473
  creation_schema: { type: `object` },
2456
- input_schemas: { query: {
2474
+ inbox_schemas: { query: {
2457
2475
  type: `object`,
2458
2476
  properties: { text: { type: `string` } },
2459
2477
  required: [`text`]
2460
2478
  } }
2461
2479
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2462
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types/${typeName}/schemas`, {
2480
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types/${typeName}/schemas`), {
2463
2481
  method: `PATCH`,
2464
2482
  headers: { "content-type": `application/json` },
2465
2483
  body: JSON.stringify({ inbox_schemas: { new_command: {
@@ -2469,7 +2487,7 @@ function runElectricAgentsConformanceTests(config) {
2469
2487
  });
2470
2488
  expect(res.status).toBe(200);
2471
2489
  }).custom(async (ctx) => {
2472
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/send`)}`, {
2490
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/send`)), {
2473
2491
  method: `POST`,
2474
2492
  headers: { "content-type": `application/json` },
2475
2493
  body: JSON.stringify({
@@ -2500,7 +2518,7 @@ function runElectricAgentsConformanceTests(config) {
2500
2518
  };
2501
2519
  await receiver.start(manifest);
2502
2520
  try {
2503
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2521
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2504
2522
  method: `POST`,
2505
2523
  headers: { "content-type": `application/json` },
2506
2524
  body: JSON.stringify({
@@ -3078,7 +3096,7 @@ function runElectricAgentsConformanceTests(config) {
3078
3096
  name: `rev-multi-agent-${id}`,
3079
3097
  description: `Test multi-revision pinning`,
3080
3098
  creation_schema: { type: `object` },
3081
- input_schemas: { greet: {
3099
+ inbox_schemas: { greet: {
3082
3100
  type: `object`,
3083
3101
  properties: { name: { type: `string` } },
3084
3102
  required: [`name`]
@@ -3119,7 +3137,7 @@ function runElectricAgentsConformanceTests(config) {
3119
3137
  }).deleteType(`amend-del-agent-${id}`).custom(async (ctx) => {
3120
3138
  const res = await electricAgentsFetch(ctx.baseUrl, `/_electric/entity-types/amend-del-agent-${id}/schemas`, {
3121
3139
  method: `PATCH`,
3122
- body: JSON.stringify({ input_schemas: { msg: { type: `object` } } })
3140
+ body: JSON.stringify({ inbox_schemas: { msg: { type: `object` } } })
3123
3141
  });
3124
3142
  expect(res.status).toBe(404);
3125
3143
  }).run();
@@ -3195,7 +3213,7 @@ function runElectricAgentsConformanceTests(config) {
3195
3213
  description: `Test write without token`,
3196
3214
  creation_schema: { type: `object` }
3197
3215
  }).spawn(`auth-notoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3198
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
3216
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
3199
3217
  method: `POST`,
3200
3218
  headers: { "content-type": `application/json` },
3201
3219
  body: JSON.stringify({
@@ -3215,7 +3233,7 @@ function runElectricAgentsConformanceTests(config) {
3215
3233
  description: `Test write with wrong token`,
3216
3234
  creation_schema: { type: `object` }
3217
3235
  }).spawn(`auth-wrongtoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3218
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
3236
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
3219
3237
  method: `POST`,
3220
3238
  headers: {
3221
3239
  "content-type": `application/json`,
@@ -3248,7 +3266,7 @@ function runElectricAgentsConformanceTests(config) {
3248
3266
  description: `Test tag update without token`,
3249
3267
  creation_schema: { type: `object` }
3250
3268
  }).spawn(`auth-meta-notoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3251
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/key`)}`, {
3269
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/key`)), {
3252
3270
  method: `POST`,
3253
3271
  headers: { "content-type": `application/json` },
3254
3272
  body: JSON.stringify({ value: `value` })
@@ -3273,7 +3291,7 @@ function runElectricAgentsConformanceTests(config) {
3273
3291
  description: `Test send without auth`,
3274
3292
  creation_schema: { type: `object` }
3275
3293
  }).spawn(`auth-send-noauth-agent-${id}`, `entity-1`).custom(async (ctx) => {
3276
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/send`)}`, {
3294
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/send`)), {
3277
3295
  method: `POST`,
3278
3296
  headers: { "content-type": `application/json` },
3279
3297
  body: JSON.stringify({ payload: `hi` })
@@ -3288,7 +3306,7 @@ function runElectricAgentsConformanceTests(config) {
3288
3306
  description: `Test GET does not leak write_token`,
3289
3307
  creation_schema: { type: `object` }
3290
3308
  }).spawn(`auth-noleak-agent-${id}`, `entity-1`).custom(async (ctx) => {
3291
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
3309
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
3292
3310
  expect(res.status).toBe(200);
3293
3311
  const entity = await res.json();
3294
3312
  expect(entity.write_token).toBeUndefined();
@@ -3358,7 +3376,7 @@ function runCliConformanceTests(config) {
3358
3376
  name: `cli-spawn-type`,
3359
3377
  description: `Type for CLI spawn test`
3360
3378
  }).setupSubscription(`/cli-spawn-type/**`, `cli-spawn-sub`).exec(`spawn`, `/cli-spawn-type/${id}`).expectExitCode(0).expectStdout(/Spawned/).verifyApi(async (baseUrl) => {
3361
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-spawn-type/${id}`)}`);
3379
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-spawn-type/${id}`)));
3362
3380
  expect(res.status).toBe(200);
3363
3381
  const entity = await res.json();
3364
3382
  expect([`running`, `idle`]).toContain(entity.status);
@@ -3401,11 +3419,11 @@ function runCliConformanceTests(config) {
3401
3419
  name: `cli-send-type`,
3402
3420
  description: `Type for CLI send test`
3403
3421
  }).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) => {
3404
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-send-type/${id}`)}`);
3422
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-send-type/${id}`)));
3405
3423
  expect(res.status).toBe(200);
3406
3424
  const entity = await res.json();
3407
3425
  const streams = entity.streams;
3408
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3426
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3409
3427
  const events = await streamRes.json();
3410
3428
  expect(events.length).toBeGreaterThanOrEqual(1);
3411
3429
  const msgEvent = events.find((e) => e.type === `inbox`);
@@ -3425,7 +3443,7 @@ function runCliConformanceTests(config) {
3425
3443
  name: `cli-inspect-etype`,
3426
3444
  description: `Type for CLI inspect test`
3427
3445
  }).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) => {
3428
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-inspect-etype/${id}`)}`);
3446
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-inspect-etype/${id}`)));
3429
3447
  expect(res.status).toBe(200);
3430
3448
  const entity = await res.json();
3431
3449
  expect([`running`, `idle`]).toContain(entity.status);
@@ -3457,7 +3475,7 @@ function runCliConformanceTests(config) {
3457
3475
  name: `cli-lifecycle-type`,
3458
3476
  description: `Type for full lifecycle test`
3459
3477
  }).setupSubscription(`/cli-lifecycle-type/**`, `cli-lifecycle-sub`).exec(`spawn`, `/cli-lifecycle-type/${id}`).expectExitCode(0).expectStdout(/Spawned/).verifyApi(async (baseUrl) => {
3460
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-lifecycle-type/${id}`)}`);
3478
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-lifecycle-type/${id}`)));
3461
3479
  expect(res.status, `entity should exist after spawn`).toBe(200);
3462
3480
  const entity = await res.json();
3463
3481
  expect([`running`, `idle`]).toContain(entity.status);
@@ -3488,7 +3506,7 @@ function runCliConformanceTests(config) {
3488
3506
  }
3489
3507
  function runMockAgentTests(config) {
3490
3508
  async function spawnEntity(baseUrl, typeName, instanceId) {
3491
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/${encodeURIComponent(typeName)}/${encodeURIComponent(instanceId)}`)}`, {
3509
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/${encodeURIComponent(typeName)}/${encodeURIComponent(instanceId)}`)), {
3492
3510
  method: `PUT`,
3493
3511
  headers: { "content-type": `application/json` },
3494
3512
  body: JSON.stringify({})
@@ -3497,7 +3515,7 @@ function runMockAgentTests(config) {
3497
3515
  return await res.json();
3498
3516
  }
3499
3517
  async function sendMessage(baseUrl, entityUrl, text) {
3500
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`${entityUrl}/send`)}`, {
3518
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`${entityUrl}/send`)), {
3501
3519
  method: `POST`,
3502
3520
  headers: { "content-type": `application/json` },
3503
3521
  body: JSON.stringify({ payload: { text } })
@@ -3507,11 +3525,11 @@ function runMockAgentTests(config) {
3507
3525
  async function pollForAgentResponse(baseUrl, entityUrl, timeoutMs = 1e4) {
3508
3526
  const start = Date.now();
3509
3527
  while (Date.now() - start < timeoutMs) {
3510
- const entityRes = await fetch(`${baseUrl}${routeControlPlanePath(entityUrl)}`);
3528
+ const entityRes = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(entityUrl)));
3511
3529
  if (!entityRes.ok) throw new Error(`Entity ${entityUrl} not found`);
3512
3530
  const entity = await entityRes.json();
3513
3531
  const streams = entity.streams;
3514
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3532
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3515
3533
  const events = await streamRes.json();
3516
3534
  const hasRunComplete = events.some((e) => e.type === `run` && e.headers?.operation === `update`);
3517
3535
  if (hasRunComplete) return events;
@@ -3564,11 +3582,11 @@ function runMockAgentCliTests(config) {
3564
3582
  test(`spawn → send → agent responds with State Protocol events`, async () => {
3565
3583
  const id = `cli-mock-${Date.now()}`;
3566
3584
  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) => {
3567
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/chat/${id}`)}`);
3585
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/chat/${id}`)));
3568
3586
  expect(res.status, `entity should exist`).toBe(200);
3569
3587
  const entity = await res.json();
3570
3588
  const streams = entity.streams;
3571
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3589
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3572
3590
  const events = await streamRes.json();
3573
3591
  expect(events.some((e) => e.type === `run`), `stream should contain run events from agent`).toBe(true);
3574
3592
  expect(events.some((e) => e.type === `text`), `stream should contain text events from agent`).toBe(true);
@@ -3578,11 +3596,11 @@ function runMockAgentCliTests(config) {
3578
3596
  test(`inspect shows entity after mock agent processes message`, async () => {
3579
3597
  const id = `cli-inspect-mock-${Date.now()}`;
3580
3598
  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) => {
3581
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/chat/${id}`)}`);
3599
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/chat/${id}`)));
3582
3600
  expect(res.status).toBe(200);
3583
3601
  const entity = await res.json();
3584
3602
  const streams = entity.streams;
3585
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3603
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3586
3604
  const events = await streamRes.json();
3587
3605
  const hasRunComplete = events.some((e) => e.type === `run` && e.headers?.operation === `update`);
3588
3606
  expect(hasRunComplete, `mock agent should have written run completion event`).toBe(true);