@electric-ax/agents-server-conformance-tests 0.1.6 → 0.1.8

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;
@@ -654,7 +672,13 @@ async function executeStep(ctx, step) {
654
672
  let entityUrl = ctx.currentEntityUrl;
655
673
  if (step.kind === `killUrl`) entityUrl = step.url;
656
674
  if (!entityUrl) throw new Error(`No current entity — did you spawn first?`);
657
- const res = await electricAgentsFetch$1(ctx.baseUrl, entityUrl, { method: `DELETE` });
675
+ const res = await electricAgentsFetch$1(ctx.baseUrl, `${entityUrl}/signal`, {
676
+ method: `POST`,
677
+ body: JSON.stringify({
678
+ signal: `SIGKILL`,
679
+ reason: `Killed by conformance test`
680
+ })
681
+ });
658
682
  expect(res.status).toBe(200);
659
683
  ctx.history.push({
660
684
  type: `entity_killed`,
@@ -665,7 +689,7 @@ async function executeStep(ctx, step) {
665
689
  case `readStream`: {
666
690
  if (!ctx.currentEntityStreams) throw new Error(`No current entity streams`);
667
691
  const streamPath = step.stream === `error` ? ctx.currentEntityStreams.error : ctx.currentEntityStreams.main;
668
- const res = await fetch(`${ctx.baseUrl}${streamPath}?offset=0000000000000000_0000000000000000`);
692
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${streamPath}?offset=0000000000000000_0000000000000000`));
669
693
  if (res.status === 200) {
670
694
  const text = await res.text();
671
695
  const messages = text ? JSON.parse(text) : [];
@@ -806,8 +830,8 @@ async function executeStep(ctx, step) {
806
830
  }
807
831
  case `amendSchemas`: {
808
832
  const body = {};
809
- if (step.input_schemas) body.inbox_schemas = step.input_schemas;
810
- 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;
811
835
  const res = await electricAgentsFetch$1(ctx.baseUrl, `/_electric/entity-types/${step.name}/schemas`, {
812
836
  method: `PATCH`,
813
837
  body: JSON.stringify(body)
@@ -1171,8 +1195,8 @@ function checkStreamPathsMatchEntityUrl(history) {
1171
1195
  /**
1172
1196
  * Spec S4 — Safety: entity status transitions must be valid.
1173
1197
  * spawning → running is valid (at spawn time)
1174
- * running/idle → stopped is valid (at kill time)
1175
- * stopped → running is NOT valid
1198
+ * running/idle → killed is valid (at kill time)
1199
+ * killed → running is NOT valid
1176
1200
  * Soundness: Sound | Completeness: Incomplete (only checks observed status reads)
1177
1201
  */
1178
1202
  function checkStatusTransitionsValid(history) {
@@ -1181,7 +1205,7 @@ function checkStatusTransitionsValid(history) {
1181
1205
  if (event.type === `entity_spawned`) lastStatus.set(event.entityUrl, event.status);
1182
1206
  if (event.type === `entity_status_checked`) {
1183
1207
  const prev = lastStatus.get(event.entityUrl);
1184
- if (prev === `stopped`) expect(event.status, `Safety: entity ${event.entityUrl} transitioned from stopped to ${event.status}`).toBe(`stopped`);
1208
+ if (prev === `killed`) expect(event.status, `Safety: entity ${event.entityUrl} transitioned from killed to ${event.status}`).toBe(`killed`);
1185
1209
  lastStatus.set(event.entityUrl, event.status);
1186
1210
  }
1187
1211
  }
@@ -1300,17 +1324,17 @@ function enabledElectricAgentsActions(model) {
1300
1324
  * Next relation — pure state transition for the model.
1301
1325
  * The real server execution happens separately in the property test.
1302
1326
  */
1303
- function applyElectricAgentsAction(model, action, targetIdx) {
1327
+ function applyElectricAgentsAction(model, action, targetIdx, opts) {
1304
1328
  switch (action) {
1305
1329
  case `register_type`: {
1306
1330
  const typeNum = model.entityTypes.length;
1307
1331
  return {
1308
1332
  ...model,
1309
1333
  entityTypes: [...model.entityTypes, {
1310
- name: `prop-type-${typeNum}`,
1334
+ name: opts?.typeName ?? `prop-type-${typeNum}`,
1311
1335
  hasCreationSchema: false,
1312
- hasInputSchemas: false,
1313
- hasOutputSchemas: false
1336
+ hasInboxSchemas: false,
1337
+ hasStateSchemas: false
1314
1338
  }]
1315
1339
  };
1316
1340
  }
@@ -1357,7 +1381,7 @@ function applyElectricAgentsAction(model, action, targetIdx) {
1357
1381
  const e = entities[targetIdx];
1358
1382
  entities[targetIdx] = {
1359
1383
  ...e,
1360
- status: `stopped`
1384
+ status: `killed`
1361
1385
  };
1362
1386
  return {
1363
1387
  ...model,
@@ -1468,7 +1492,7 @@ function checkStateProtocolInvariants(events) {
1468
1492
  //#endregion
1469
1493
  //#region src/cli-dsl.ts
1470
1494
  function subscriptionEndpoint(baseUrl, id) {
1471
- return `${baseUrl}/__ds/subscriptions/${encodeURIComponent(id)}`;
1495
+ return appendPathToUrl(baseUrl, `/__ds/subscriptions/${encodeURIComponent(id)}`);
1472
1496
  }
1473
1497
  function subscriptionPattern(pattern) {
1474
1498
  return pattern.replace(/^\/+/, ``);
@@ -1617,7 +1641,7 @@ var CliScenario = class {
1617
1641
  try {
1618
1642
  for (const step of this.steps) switch (step.kind) {
1619
1643
  case `setupType`: {
1620
- const res = await fetch(`${this.baseUrl}/_electric/entity-types`, {
1644
+ const res = await fetch(appendPathToUrl(this.baseUrl, `/_electric/entity-types`), {
1621
1645
  method: `POST`,
1622
1646
  headers: { "content-type": `application/json` },
1623
1647
  body: JSON.stringify(step.registration)
@@ -1746,7 +1770,7 @@ function startNoopReceiver() {
1746
1770
  //#endregion
1747
1771
  //#region src/electric-agents-tests.ts
1748
1772
  async function electricAgentsFetch(baseUrl, path, opts = {}) {
1749
- return fetch(`${baseUrl}${routeControlPlanePath(path)}`, {
1773
+ return fetch(appendPathToUrl(baseUrl, routeControlPlanePath(path)), {
1750
1774
  ...opts,
1751
1775
  headers: {
1752
1776
  "content-type": `application/json`,
@@ -1767,7 +1791,7 @@ function isEntityStreamPath(pathname) {
1767
1791
  async function pollEntityStatus(baseUrl, entityUrl, statuses, timeoutMs = 8e3) {
1768
1792
  const deadline = Date.now() + timeoutMs;
1769
1793
  while (Date.now() < deadline) {
1770
- const res = await fetch(`${baseUrl}${routeControlPlanePath(entityUrl)}`);
1794
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(entityUrl)));
1771
1795
  expect(res.status).toBe(200);
1772
1796
  const entity = await res.json();
1773
1797
  if (statuses.includes(String(entity.status))) return entity;
@@ -1782,13 +1806,13 @@ function runElectricAgentsConformanceTests(config) {
1782
1806
  description: `Test entity type for spawn`,
1783
1807
  creation_schema: { type: `object` }
1784
1808
  }).spawn(`spawn-test-agent`, `entity-1`).expectStatus(`running`).custom(async (ctx) => {
1785
- const mainRes = await fetch(`${ctx.baseUrl}${ctx.currentEntityStreams.main}`, { method: `HEAD` });
1809
+ const mainRes = await fetch(appendPathToUrl(ctx.baseUrl, ctx.currentEntityStreams.main), { method: `HEAD` });
1786
1810
  expect(mainRes.status).toBe(200);
1787
- const errorRes = await fetch(`${ctx.baseUrl}${ctx.currentEntityStreams.error}`, { method: `HEAD` });
1811
+ const errorRes = await fetch(appendPathToUrl(ctx.baseUrl, ctx.currentEntityStreams.error), { method: `HEAD` });
1788
1812
  expect(errorRes.status).toBe(200);
1789
1813
  }).run());
1790
1814
  test(`spawn at unregistered type returns UNKNOWN_ENTITY_TYPE`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
1791
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/unregistered/entity-1`)}`, {
1815
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/unregistered/entity-1`)), {
1792
1816
  method: `PUT`,
1793
1817
  headers: { "content-type": `application/json` },
1794
1818
  body: JSON.stringify({})
@@ -1830,7 +1854,7 @@ function runElectricAgentsConformanceTests(config) {
1830
1854
  creation_schema: { type: `object` }
1831
1855
  }).spawn(`webhook-ctx-agent`, `entity-1`).send({ ping: true }).expectWebhook().expectEntityContext({ type: `webhook-ctx-agent` }).respondDone().run());
1832
1856
  test(`send to nonexistent entity returns 404`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
1833
- 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`)), {
1834
1858
  method: `POST`,
1835
1859
  headers: { "content-type": `application/json` },
1836
1860
  body: JSON.stringify({ payload: {} })
@@ -1878,8 +1902,8 @@ function runElectricAgentsConformanceTests(config) {
1878
1902
  name: `status-filter-agent`,
1879
1903
  description: `Test entity type for status filter`,
1880
1904
  creation_schema: { type: `object` }
1881
- }).spawn(`status-filter-agent`, `entity-1`).spawn(`status-filter-agent`, `entity-2`).kill().list({ status: `stopped` }).custom(async (ctx) => {
1882
- expect(ctx.lastListResult.every((e) => e.status === `stopped`)).toBe(true);
1905
+ }).spawn(`status-filter-agent`, `entity-1`).spawn(`status-filter-agent`, `entity-2`).kill().list({ status: `killed` }).custom(async (ctx) => {
1906
+ expect(ctx.lastListResult.every((e) => e.status === `killed`)).toBe(true);
1883
1907
  }).list({ status: `running` }).custom(async (ctx) => {
1884
1908
  expect(ctx.lastListResult.every((e) => [`running`, `idle`].includes(e.status))).toBe(true);
1885
1909
  }).run();
@@ -1894,7 +1918,7 @@ function runElectricAgentsConformanceTests(config) {
1894
1918
  name: `kill-test-agent`,
1895
1919
  description: `Test entity type for kill`,
1896
1920
  creation_schema: { type: `object` }
1897
- }).spawn(`kill-test-agent`, `entity-1`).kill().expectStatus(`stopped`).expectSendError(`NOT_RUNNING`, 409).run());
1921
+ }).spawn(`kill-test-agent`, `entity-1`).kill().expectStatus(`killed`).expectSendError(`NOT_RUNNING`, 409).run());
1898
1922
  test(`stream data persists after kill`, () => electricAgents(config.baseUrl).subscription(`/kill-persist-agent/**`, `kill-persist-sub`).registerType({
1899
1923
  name: `kill-persist-agent`,
1900
1924
  description: `Test entity type for kill persistence`,
@@ -1903,8 +1927,9 @@ function runElectricAgentsConformanceTests(config) {
1903
1927
  const msgs = ctx.lastStreamMessages;
1904
1928
  const msgReceived = msgs.find((m) => m.type === `inbox`);
1905
1929
  expect(msgReceived.value?.payload).toEqual({ before: `kill` });
1906
- const stopped = msgs.find((m) => m.type === `entity_stopped`);
1907
- expect(stopped).toBeDefined();
1930
+ const signal = msgs.find((m) => m.type === `signal`);
1931
+ expect(signal).toBeDefined();
1932
+ expect(signal.value?.signal).toBe(`SIGKILL`);
1908
1933
  }).run());
1909
1934
  test.skip(`multiple entities under same subscription`, () => {
1910
1935
  let firstEntityUrl = null;
@@ -1918,7 +1943,10 @@ function runElectricAgentsConformanceTests(config) {
1918
1943
  }).spawn(`multi-test-worker`, `entity-2`).custom(async (ctx) => {
1919
1944
  secondEntityUrl = ctx.currentEntityUrl;
1920
1945
  }).custom(async (ctx) => {
1921
- const res = await electricAgentsFetch(ctx.baseUrl, firstEntityUrl, { method: `DELETE` });
1946
+ const res = await electricAgentsFetch(ctx.baseUrl, `${firstEntityUrl}/signal`, {
1947
+ method: `POST`,
1948
+ body: JSON.stringify({ signal: `SIGKILL` })
1949
+ });
1922
1950
  expect(res.status).toBe(200);
1923
1951
  ctx.history.push({
1924
1952
  type: `entity_killed`,
@@ -1940,8 +1968,8 @@ function runElectricAgentsConformanceTests(config) {
1940
1968
  description: `Test entity type for E2E lifecycle`,
1941
1969
  creation_schema: { type: `object` }
1942
1970
  }).spawn(`e2e-test-agent`, `entity-1`).expectStatus(`running`).send({ task: `do-something` }).expectWebhook().expectEntityContext({ type: `e2e-test-agent` }).respondDone().readStream().expectStreamContains(`inbox`).kill().custom(async (ctx) => {
1943
- const entity = await pollEntityStatus(ctx.baseUrl, ctx.currentEntityUrl, [`stopped`]);
1944
- expect(entity.status).toBe(`stopped`);
1971
+ const entity = await pollEntityStatus(ctx.baseUrl, ctx.currentEntityUrl, [`killed`]);
1972
+ expect(entity.status).toBe(`killed`);
1945
1973
  }).expectSendError(`NOT_RUNNING`, 409).run());
1946
1974
  });
1947
1975
  describe(`Electric Agents Entity Type Registration`, () => {
@@ -1952,12 +1980,12 @@ function runElectricAgentsConformanceTests(config) {
1952
1980
  type: `object`,
1953
1981
  properties: { name: { type: `string` } }
1954
1982
  },
1955
- input_schemas: { query: {
1983
+ inbox_schemas: { query: {
1956
1984
  type: `object`,
1957
1985
  properties: { text: { type: `string` } },
1958
1986
  required: [`text`]
1959
1987
  } },
1960
- output_schemas: { result: {
1988
+ state_schemas: { result: {
1961
1989
  type: `object`,
1962
1990
  properties: { answer: { type: `string` } }
1963
1991
  } }
@@ -1993,7 +2021,7 @@ function runElectricAgentsConformanceTests(config) {
1993
2021
  type: `object`,
1994
2022
  properties: { x: { type: `number` } }
1995
2023
  },
1996
- input_schemas: { ping: {
2024
+ inbox_schemas: { ping: {
1997
2025
  type: `object`,
1998
2026
  properties: { msg: { type: `string` } }
1999
2027
  } }
@@ -2019,7 +2047,7 @@ function runElectricAgentsConformanceTests(config) {
2019
2047
  description: `First registration`,
2020
2048
  creation_schema: { type: `object` }
2021
2049
  }).custom(async (ctx) => {
2022
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2050
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2023
2051
  method: `POST`,
2024
2052
  headers: { "content-type": `application/json` },
2025
2053
  body: JSON.stringify({
@@ -2036,7 +2064,7 @@ function runElectricAgentsConformanceTests(config) {
2036
2064
  }).run();
2037
2065
  });
2038
2066
  test(`register rejects missing required fields`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
2039
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2067
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2040
2068
  method: `POST`,
2041
2069
  headers: { "content-type": `application/json` },
2042
2070
  body: JSON.stringify({})
@@ -2081,7 +2109,7 @@ function runElectricAgentsConformanceTests(config) {
2081
2109
  }).expectSpawnSchemaError(typeName, `bad-entity-1`, { args: { invalid: true } }).run();
2082
2110
  });
2083
2111
  test(`typed spawn at unregistered type returns UNKNOWN_ENTITY_TYPE (C9)`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
2084
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/nonexistent/should-fail`)}`, {
2112
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/nonexistent/should-fail`)), {
2085
2113
  method: `PUT`,
2086
2114
  headers: { "content-type": `application/json` },
2087
2115
  body: JSON.stringify({})
@@ -2100,7 +2128,7 @@ function runElectricAgentsConformanceTests(config) {
2100
2128
  }).spawn(typeName, `parent-entity`).custom(async (ctx) => {
2101
2129
  parentUrl = ctx.currentEntityUrl;
2102
2130
  }).custom(async (ctx) => {
2103
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/child-entity`)}`, {
2131
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/child-entity`)), {
2104
2132
  method: `PUT`,
2105
2133
  headers: { "content-type": `application/json` },
2106
2134
  body: JSON.stringify({ parent: parentUrl })
@@ -2127,7 +2155,7 @@ function runElectricAgentsConformanceTests(config) {
2127
2155
  description: `Type for orphan spawn test`,
2128
2156
  creation_schema: { type: `object` }
2129
2157
  }).custom(async (ctx) => {
2130
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/orphan-entity`)}`, {
2158
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/orphan-entity`)), {
2131
2159
  method: `PUT`,
2132
2160
  headers: { "content-type": `application/json` },
2133
2161
  body: JSON.stringify({ parent: `/nonexistent/parent` })
@@ -2148,7 +2176,7 @@ function runElectricAgentsConformanceTests(config) {
2148
2176
  color: `blue`,
2149
2177
  count: `42`
2150
2178
  } }).custom(async (ctx) => {
2151
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
2179
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
2152
2180
  const entity = await res.json();
2153
2181
  const tags = entity.tags;
2154
2182
  expect(tags.color).toBe(`blue`);
@@ -2162,7 +2190,7 @@ function runElectricAgentsConformanceTests(config) {
2162
2190
  name: typeName,
2163
2191
  description: `Type with string-only tags`
2164
2192
  }).custom(async (ctx) => {
2165
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/should-fail`)}`, {
2193
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/should-fail`)), {
2166
2194
  method: `PUT`,
2167
2195
  headers: { "content-type": `application/json` },
2168
2196
  body: JSON.stringify({ tags: { wrong: 123 } })
@@ -2176,20 +2204,20 @@ function runElectricAgentsConformanceTests(config) {
2176
2204
  name: typeName,
2177
2205
  description: `Type with default empty tags`
2178
2206
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2179
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
2207
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
2180
2208
  const entity = await res.json();
2181
2209
  expect(entity.tags).toEqual({});
2182
2210
  }).run();
2183
2211
  });
2184
2212
  });
2185
2213
  describe(`Electric Agents Schema Validation Gates`, () => {
2186
- test(`send validates input_schemas (C11)`, () => {
2214
+ test(`send validates inbox_schemas (C11)`, () => {
2187
2215
  const typeName = `send-schema-valid-${Date.now()}`;
2188
2216
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-schema-sub`).registerType({
2189
2217
  name: typeName,
2190
- description: `Type with input schemas`,
2218
+ description: `Type with inbox schemas`,
2191
2219
  creation_schema: { type: `object` },
2192
- input_schemas: { query: {
2220
+ inbox_schemas: { query: {
2193
2221
  type: `object`,
2194
2222
  properties: { text: { type: `string` } },
2195
2223
  required: [`text`]
@@ -2200,9 +2228,9 @@ function runElectricAgentsConformanceTests(config) {
2200
2228
  const typeName = `send-schema-inv-${Date.now()}`;
2201
2229
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-schema-inv-sub`).registerType({
2202
2230
  name: typeName,
2203
- description: `Type with strict input schemas`,
2231
+ description: `Type with strict inbox schemas`,
2204
2232
  creation_schema: { type: `object` },
2205
- input_schemas: { query: {
2233
+ inbox_schemas: { query: {
2206
2234
  type: `object`,
2207
2235
  properties: { text: { type: `string` } },
2208
2236
  required: [`text`]
@@ -2213,30 +2241,30 @@ function runElectricAgentsConformanceTests(config) {
2213
2241
  const typeName = `send-unknown-type-${Date.now()}`;
2214
2242
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-unknown-sub`).registerType({
2215
2243
  name: typeName,
2216
- description: `Type with defined input schemas`,
2244
+ description: `Type with defined inbox schemas`,
2217
2245
  creation_schema: { type: `object` },
2218
- input_schemas: { query: {
2246
+ inbox_schemas: { query: {
2219
2247
  type: `object`,
2220
2248
  properties: { text: { type: `string` } },
2221
2249
  required: [`text`]
2222
2250
  } }
2223
2251
  }).spawn(typeName, `entity-1`).expectSendUnknownType({ text: `hi` }, { type: `unknown_type` }).run();
2224
2252
  });
2225
- test(`send without type when no input_schemas accepts any`, () => {
2253
+ test(`send without type when no inbox_schemas accepts any`, () => {
2226
2254
  const typeName = `send-no-schemas-${Date.now()}`;
2227
2255
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-noschema-sub`).registerType({
2228
2256
  name: typeName,
2229
- description: `Type without input schemas`,
2257
+ description: `Type without inbox schemas`,
2230
2258
  creation_schema: { type: `object` }
2231
2259
  }).spawn(typeName, `entity-1`).send({ anything: `goes` }).expectWebhook().respondDone().run();
2232
2260
  });
2233
- test(`send with empty input_schemas rejects all`, () => {
2261
+ test(`send with empty inbox_schemas rejects all`, () => {
2234
2262
  const typeName = `send-empty-schemas-${Date.now()}`;
2235
2263
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-empty-sub`).registerType({
2236
2264
  name: typeName,
2237
- description: `Type with empty input schemas`,
2265
+ description: `Type with empty inbox schemas`,
2238
2266
  creation_schema: { type: `object` },
2239
- input_schemas: {}
2267
+ inbox_schemas: {}
2240
2268
  }).spawn(typeName, `entity-1`).expectSendUnknownType({ text: `anything` }, { type: `some_type` }).run();
2241
2269
  });
2242
2270
  test.skip(`write appends event to entity stream`, () => {
@@ -2245,7 +2273,7 @@ function runElectricAgentsConformanceTests(config) {
2245
2273
  name: typeName,
2246
2274
  description: `Type for write test`,
2247
2275
  creation_schema: { type: `object` },
2248
- output_schemas: { research_result: {
2276
+ state_schemas: { research_result: {
2249
2277
  type: `object`,
2250
2278
  properties: { findings: {
2251
2279
  type: `array`,
@@ -2254,13 +2282,13 @@ function runElectricAgentsConformanceTests(config) {
2254
2282
  } }
2255
2283
  }).spawn(typeName, `entity-1`).write({ findings: [`test`] }, { type: `research_result` }).readStream().expectStreamContains(`research_result`).run();
2256
2284
  });
2257
- test.skip(`write validates output_schemas (C12)`, () => {
2285
+ test.skip(`write validates state_schemas (C12)`, () => {
2258
2286
  const typeName = `write-schema-inv-${Date.now()}`;
2259
2287
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-schema-sub`).registerType({
2260
2288
  name: typeName,
2261
- description: `Type with strict output schemas`,
2289
+ description: `Type with strict state schemas`,
2262
2290
  creation_schema: { type: `object` },
2263
- output_schemas: { result: {
2291
+ state_schemas: { result: {
2264
2292
  type: `object`,
2265
2293
  properties: { value: { type: `number` } },
2266
2294
  required: [`value`]
@@ -2271,19 +2299,19 @@ function runElectricAgentsConformanceTests(config) {
2271
2299
  const typeName = `write-unknown-type-${Date.now()}`;
2272
2300
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-unknown-sub`).registerType({
2273
2301
  name: typeName,
2274
- description: `Type with defined output schemas`,
2302
+ description: `Type with defined state schemas`,
2275
2303
  creation_schema: { type: `object` },
2276
- output_schemas: { result: {
2304
+ state_schemas: { result: {
2277
2305
  type: `object`,
2278
2306
  properties: { value: { type: `number` } }
2279
2307
  } }
2280
2308
  }).spawn(typeName, `entity-1`).expectWriteUnknownType({ data: `test` }, { type: `unknown_event` }).run();
2281
2309
  });
2282
- test.skip(`write without type when no output_schemas accepts any`, () => {
2310
+ test.skip(`write without type when no state_schemas accepts any`, () => {
2283
2311
  const typeName = `write-no-schemas-${Date.now()}`;
2284
2312
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-noschema-sub`).registerType({
2285
2313
  name: typeName,
2286
- description: `Type without output schemas`,
2314
+ description: `Type without state schemas`,
2287
2315
  creation_schema: { type: `object` }
2288
2316
  }).spawn(typeName, `entity-1`).write({ anything: `goes` }).run();
2289
2317
  });
@@ -2296,7 +2324,7 @@ function runElectricAgentsConformanceTests(config) {
2296
2324
  }).spawn(typeName, `entity-1`).kill().custom(async (ctx) => {
2297
2325
  const writeHeaders = { "content-type": `application/json` };
2298
2326
  if (ctx.currentWriteToken) writeHeaders[`authorization`] = `Bearer ${ctx.currentWriteToken}`;
2299
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
2327
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
2300
2328
  method: `POST`,
2301
2329
  headers: writeHeaders,
2302
2330
  body: JSON.stringify({
@@ -2350,7 +2378,7 @@ function runElectricAgentsConformanceTests(config) {
2350
2378
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2351
2379
  const tagHeaders = { "content-type": `application/json` };
2352
2380
  if (ctx.currentWriteToken) tagHeaders[`authorization`] = `Bearer ${ctx.currentWriteToken}`;
2353
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/owner`)}`, {
2381
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/owner`)), {
2354
2382
  method: `POST`,
2355
2383
  headers: tagHeaders,
2356
2384
  body: JSON.stringify({ value: 123 })
@@ -2370,7 +2398,7 @@ function runElectricAgentsConformanceTests(config) {
2370
2398
  }).custom(async (ctx) => {
2371
2399
  const tagHeaders = {};
2372
2400
  if (ctx.currentWriteToken) tagHeaders.authorization = `Bearer ${ctx.currentWriteToken}`;
2373
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/priority`)}`, {
2401
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/priority`)), {
2374
2402
  method: `DELETE`,
2375
2403
  headers: tagHeaders
2376
2404
  });
@@ -2403,12 +2431,12 @@ function runElectricAgentsConformanceTests(config) {
2403
2431
  name: typeName,
2404
2432
  description: `Type for schema amendment`,
2405
2433
  creation_schema: { type: `object` },
2406
- input_schemas: { query: {
2434
+ inbox_schemas: { query: {
2407
2435
  type: `object`,
2408
2436
  properties: { text: { type: `string` } },
2409
2437
  required: [`text`]
2410
2438
  } }
2411
- }).amendSchemas(typeName, { input_schemas: { command: {
2439
+ }).amendSchemas(typeName, { inbox_schemas: { command: {
2412
2440
  type: `object`,
2413
2441
  properties: { action: { type: `string` } },
2414
2442
  required: [`action`]
@@ -2420,13 +2448,13 @@ function runElectricAgentsConformanceTests(config) {
2420
2448
  name: typeName,
2421
2449
  description: `Type for schema conflict test`,
2422
2450
  creation_schema: { type: `object` },
2423
- input_schemas: { query: {
2451
+ inbox_schemas: { query: {
2424
2452
  type: `object`,
2425
2453
  properties: { text: { type: `string` } },
2426
2454
  required: [`text`]
2427
2455
  } }
2428
2456
  }).custom(async (ctx) => {
2429
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types/${typeName}/schemas`, {
2457
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types/${typeName}/schemas`), {
2430
2458
  method: `PATCH`,
2431
2459
  headers: { "content-type": `application/json` },
2432
2460
  body: JSON.stringify({ inbox_schemas: { query: {
@@ -2443,13 +2471,13 @@ function runElectricAgentsConformanceTests(config) {
2443
2471
  name: typeName,
2444
2472
  description: `Type for revision pinning test`,
2445
2473
  creation_schema: { type: `object` },
2446
- input_schemas: { query: {
2474
+ inbox_schemas: { query: {
2447
2475
  type: `object`,
2448
2476
  properties: { text: { type: `string` } },
2449
2477
  required: [`text`]
2450
2478
  } }
2451
2479
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2452
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types/${typeName}/schemas`, {
2480
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types/${typeName}/schemas`), {
2453
2481
  method: `PATCH`,
2454
2482
  headers: { "content-type": `application/json` },
2455
2483
  body: JSON.stringify({ inbox_schemas: { new_command: {
@@ -2459,7 +2487,7 @@ function runElectricAgentsConformanceTests(config) {
2459
2487
  });
2460
2488
  expect(res.status).toBe(200);
2461
2489
  }).custom(async (ctx) => {
2462
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/send`)}`, {
2490
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/send`)), {
2463
2491
  method: `POST`,
2464
2492
  headers: { "content-type": `application/json` },
2465
2493
  body: JSON.stringify({
@@ -2490,7 +2518,7 @@ function runElectricAgentsConformanceTests(config) {
2490
2518
  };
2491
2519
  await receiver.start(manifest);
2492
2520
  try {
2493
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2521
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2494
2522
  method: `POST`,
2495
2523
  headers: { "content-type": `application/json` },
2496
2524
  body: JSON.stringify({
@@ -2539,7 +2567,6 @@ function runElectricAgentsConformanceTests(config) {
2539
2567
  const runId = `prop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2540
2568
  const baseUrl = config.baseUrl;
2541
2569
  const entityUrls = [];
2542
- const registeredTypeNames = [];
2543
2570
  const scenario = electricAgents(baseUrl);
2544
2571
  let model = {
2545
2572
  entityTypes: [],
@@ -2547,21 +2574,21 @@ function runElectricAgentsConformanceTests(config) {
2547
2574
  nextEntityNum: 0
2548
2575
  };
2549
2576
  let entityCounter = 0;
2577
+ let typeCounter = 0;
2550
2578
  for (const action of actions) {
2551
2579
  const valid = enabledElectricAgentsActions(model);
2552
2580
  if (!valid.includes(action)) continue;
2553
2581
  switch (action) {
2554
2582
  case `register_type`: {
2555
- const typeNum = model.entityTypes.length;
2583
+ const typeNum = typeCounter++;
2556
2584
  const typeName = `prop-type-${runId}-${typeNum}`;
2557
- registeredTypeNames.push(typeName);
2558
2585
  scenario.subscription(`/${typeName}/**`, `prop-sub-${typeName}`);
2559
2586
  scenario.registerType({
2560
2587
  name: typeName,
2561
2588
  description: `Property-based test type ${typeNum}`,
2562
2589
  creation_schema: { type: `object` }
2563
2590
  });
2564
- model = applyElectricAgentsAction(model, `register_type`);
2591
+ model = applyElectricAgentsAction(model, `register_type`, void 0, { typeName });
2565
2592
  break;
2566
2593
  }
2567
2594
  case `delete_type`: {
@@ -2569,14 +2596,13 @@ function runElectricAgentsConformanceTests(config) {
2569
2596
  const deletableIndices = model.entityTypes.map((t, i) => !runningModelTypeNames.has(t.name) ? i : -1).filter((i) => i >= 0);
2570
2597
  if (deletableIndices.length === 0) break;
2571
2598
  const deleteIdx = deletableIndices[0];
2572
- scenario.deleteType(registeredTypeNames[deleteIdx]);
2573
- registeredTypeNames.splice(deleteIdx, 1);
2599
+ scenario.deleteType(model.entityTypes[deleteIdx].name);
2574
2600
  model = applyElectricAgentsAction(model, `delete_type`);
2575
2601
  break;
2576
2602
  }
2577
2603
  case `spawn`: {
2578
- if (registeredTypeNames.length === 0) break;
2579
- const typeName = registeredTypeNames[0];
2604
+ if (model.entityTypes.length === 0) break;
2605
+ const typeName = model.entityTypes[0].name;
2580
2606
  const instanceId = `entity-${entityCounter++}`;
2581
2607
  scenario.spawn(typeName, instanceId);
2582
2608
  scenario.custom(async (ctx) => {
@@ -2621,7 +2647,10 @@ function runElectricAgentsConformanceTests(config) {
2621
2647
  scenario.custom(async (ctx) => {
2622
2648
  const url = entityUrls[targetIdx];
2623
2649
  if (!url) return;
2624
- const res = await electricAgentsFetch(ctx.baseUrl, url, { method: `DELETE` });
2650
+ const res = await electricAgentsFetch(ctx.baseUrl, `${url}/signal`, {
2651
+ method: `POST`,
2652
+ body: JSON.stringify({ signal: `SIGKILL` })
2653
+ });
2625
2654
  expect(res.status).toBe(200);
2626
2655
  ctx.history.push({
2627
2656
  type: `entity_killed`,
@@ -2995,7 +3024,10 @@ function runElectricAgentsConformanceTests(config) {
2995
3024
  description: `test`
2996
3025
  })
2997
3026
  });
2998
- const res = await electricAgentsFetch(config.baseUrl, `/${typeName}/nonexistent-id-12345`, { method: `DELETE` });
3027
+ const res = await electricAgentsFetch(config.baseUrl, `/${typeName}/nonexistent-id-12345/signal`, {
3028
+ method: `POST`,
3029
+ body: JSON.stringify({ signal: `SIGKILL` })
3030
+ });
2999
3031
  expect(res.status).toBe(404);
3000
3032
  const body = await res.json();
3001
3033
  expect(body.error.code).toBe(`NOT_FOUND`);
@@ -3064,7 +3096,7 @@ function runElectricAgentsConformanceTests(config) {
3064
3096
  name: `rev-multi-agent-${id}`,
3065
3097
  description: `Test multi-revision pinning`,
3066
3098
  creation_schema: { type: `object` },
3067
- input_schemas: { greet: {
3099
+ inbox_schemas: { greet: {
3068
3100
  type: `object`,
3069
3101
  properties: { name: { type: `string` } },
3070
3102
  required: [`name`]
@@ -3105,7 +3137,7 @@ function runElectricAgentsConformanceTests(config) {
3105
3137
  }).deleteType(`amend-del-agent-${id}`).custom(async (ctx) => {
3106
3138
  const res = await electricAgentsFetch(ctx.baseUrl, `/_electric/entity-types/amend-del-agent-${id}/schemas`, {
3107
3139
  method: `PATCH`,
3108
- body: JSON.stringify({ input_schemas: { msg: { type: `object` } } })
3140
+ body: JSON.stringify({ inbox_schemas: { msg: { type: `object` } } })
3109
3141
  });
3110
3142
  expect(res.status).toBe(404);
3111
3143
  }).run();
@@ -3181,7 +3213,7 @@ function runElectricAgentsConformanceTests(config) {
3181
3213
  description: `Test write without token`,
3182
3214
  creation_schema: { type: `object` }
3183
3215
  }).spawn(`auth-notoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3184
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
3216
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
3185
3217
  method: `POST`,
3186
3218
  headers: { "content-type": `application/json` },
3187
3219
  body: JSON.stringify({
@@ -3201,7 +3233,7 @@ function runElectricAgentsConformanceTests(config) {
3201
3233
  description: `Test write with wrong token`,
3202
3234
  creation_schema: { type: `object` }
3203
3235
  }).spawn(`auth-wrongtoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3204
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
3236
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
3205
3237
  method: `POST`,
3206
3238
  headers: {
3207
3239
  "content-type": `application/json`,
@@ -3234,7 +3266,7 @@ function runElectricAgentsConformanceTests(config) {
3234
3266
  description: `Test tag update without token`,
3235
3267
  creation_schema: { type: `object` }
3236
3268
  }).spawn(`auth-meta-notoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3237
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/key`)}`, {
3269
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/key`)), {
3238
3270
  method: `POST`,
3239
3271
  headers: { "content-type": `application/json` },
3240
3272
  body: JSON.stringify({ value: `value` })
@@ -3259,7 +3291,7 @@ function runElectricAgentsConformanceTests(config) {
3259
3291
  description: `Test send without auth`,
3260
3292
  creation_schema: { type: `object` }
3261
3293
  }).spawn(`auth-send-noauth-agent-${id}`, `entity-1`).custom(async (ctx) => {
3262
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/send`)}`, {
3294
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/send`)), {
3263
3295
  method: `POST`,
3264
3296
  headers: { "content-type": `application/json` },
3265
3297
  body: JSON.stringify({ payload: `hi` })
@@ -3274,7 +3306,7 @@ function runElectricAgentsConformanceTests(config) {
3274
3306
  description: `Test GET does not leak write_token`,
3275
3307
  creation_schema: { type: `object` }
3276
3308
  }).spawn(`auth-noleak-agent-${id}`, `entity-1`).custom(async (ctx) => {
3277
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
3309
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
3278
3310
  expect(res.status).toBe(200);
3279
3311
  const entity = await res.json();
3280
3312
  expect(entity.write_token).toBeUndefined();
@@ -3344,7 +3376,7 @@ function runCliConformanceTests(config) {
3344
3376
  name: `cli-spawn-type`,
3345
3377
  description: `Type for CLI spawn test`
3346
3378
  }).setupSubscription(`/cli-spawn-type/**`, `cli-spawn-sub`).exec(`spawn`, `/cli-spawn-type/${id}`).expectExitCode(0).expectStdout(/Spawned/).verifyApi(async (baseUrl) => {
3347
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-spawn-type/${id}`)}`);
3379
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-spawn-type/${id}`)));
3348
3380
  expect(res.status).toBe(200);
3349
3381
  const entity = await res.json();
3350
3382
  expect([`running`, `idle`]).toContain(entity.status);
@@ -3387,11 +3419,11 @@ function runCliConformanceTests(config) {
3387
3419
  name: `cli-send-type`,
3388
3420
  description: `Type for CLI send test`
3389
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) => {
3390
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-send-type/${id}`)}`);
3422
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-send-type/${id}`)));
3391
3423
  expect(res.status).toBe(200);
3392
3424
  const entity = await res.json();
3393
3425
  const streams = entity.streams;
3394
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3426
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3395
3427
  const events = await streamRes.json();
3396
3428
  expect(events.length).toBeGreaterThanOrEqual(1);
3397
3429
  const msgEvent = events.find((e) => e.type === `inbox`);
@@ -3411,7 +3443,7 @@ function runCliConformanceTests(config) {
3411
3443
  name: `cli-inspect-etype`,
3412
3444
  description: `Type for CLI inspect test`
3413
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) => {
3414
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-inspect-etype/${id}`)}`);
3446
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-inspect-etype/${id}`)));
3415
3447
  expect(res.status).toBe(200);
3416
3448
  const entity = await res.json();
3417
3449
  expect([`running`, `idle`]).toContain(entity.status);
@@ -3428,8 +3460,8 @@ function runCliConformanceTests(config) {
3428
3460
  name: `cli-kill-type`,
3429
3461
  description: `Type for CLI kill test`
3430
3462
  }).setupSubscription(`/cli-kill-type/**`, `cli-kill-sub`).exec(`spawn`, `/cli-kill-type/${id}`).expectExitCode(0).exec(`kill`, `/cli-kill-type/${id}`).expectExitCode(0).expectStdout(/Killed/).verifyApi(async (baseUrl) => {
3431
- const entity = await pollEntityStatus(baseUrl, `/cli-kill-type/${id}`, [`stopped`]);
3432
- expect(entity.status).toBe(`stopped`);
3463
+ const entity = await pollEntityStatus(baseUrl, `/cli-kill-type/${id}`, [`killed`]);
3464
+ expect(entity.status).toBe(`killed`);
3433
3465
  }).run();
3434
3466
  }, 15e3);
3435
3467
  test(`kill nonexistent entity fails`, async () => {
@@ -3443,13 +3475,13 @@ function runCliConformanceTests(config) {
3443
3475
  name: `cli-lifecycle-type`,
3444
3476
  description: `Type for full lifecycle test`
3445
3477
  }).setupSubscription(`/cli-lifecycle-type/**`, `cli-lifecycle-sub`).exec(`spawn`, `/cli-lifecycle-type/${id}`).expectExitCode(0).expectStdout(/Spawned/).verifyApi(async (baseUrl) => {
3446
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-lifecycle-type/${id}`)}`);
3478
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-lifecycle-type/${id}`)));
3447
3479
  expect(res.status, `entity should exist after spawn`).toBe(200);
3448
3480
  const entity = await res.json();
3449
3481
  expect([`running`, `idle`]).toContain(entity.status);
3450
3482
  }).exec(`send`, `/cli-lifecycle-type/${id}`, `test message`).expectExitCode(0).expectStdout(/Message sent/).exec(`inspect`, `/cli-lifecycle-type/${id}`).expectExitCode(0).expectStdout(/running|idle/).exec(`kill`, `/cli-lifecycle-type/${id}`).expectExitCode(0).expectStdout(/Killed/).verifyApi(async (baseUrl) => {
3451
- const entity = await pollEntityStatus(baseUrl, `/cli-lifecycle-type/${id}`, [`stopped`]);
3452
- expect(entity.status).toBe(`stopped`);
3483
+ const entity = await pollEntityStatus(baseUrl, `/cli-lifecycle-type/${id}`, [`killed`]);
3484
+ expect(entity.status).toBe(`killed`);
3453
3485
  }).exec(`ps`, `--status`, `running`).expectExitCode(0).expectStdoutNot(new RegExp(id)).run();
3454
3486
  }, 15e3);
3455
3487
  test(`send to stopped entity fails`, async () => {
@@ -3474,7 +3506,7 @@ function runCliConformanceTests(config) {
3474
3506
  }
3475
3507
  function runMockAgentTests(config) {
3476
3508
  async function spawnEntity(baseUrl, typeName, instanceId) {
3477
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/${encodeURIComponent(typeName)}/${encodeURIComponent(instanceId)}`)}`, {
3509
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/${encodeURIComponent(typeName)}/${encodeURIComponent(instanceId)}`)), {
3478
3510
  method: `PUT`,
3479
3511
  headers: { "content-type": `application/json` },
3480
3512
  body: JSON.stringify({})
@@ -3483,7 +3515,7 @@ function runMockAgentTests(config) {
3483
3515
  return await res.json();
3484
3516
  }
3485
3517
  async function sendMessage(baseUrl, entityUrl, text) {
3486
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`${entityUrl}/send`)}`, {
3518
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`${entityUrl}/send`)), {
3487
3519
  method: `POST`,
3488
3520
  headers: { "content-type": `application/json` },
3489
3521
  body: JSON.stringify({ payload: { text } })
@@ -3493,11 +3525,11 @@ function runMockAgentTests(config) {
3493
3525
  async function pollForAgentResponse(baseUrl, entityUrl, timeoutMs = 1e4) {
3494
3526
  const start = Date.now();
3495
3527
  while (Date.now() - start < timeoutMs) {
3496
- const entityRes = await fetch(`${baseUrl}${routeControlPlanePath(entityUrl)}`);
3528
+ const entityRes = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(entityUrl)));
3497
3529
  if (!entityRes.ok) throw new Error(`Entity ${entityUrl} not found`);
3498
3530
  const entity = await entityRes.json();
3499
3531
  const streams = entity.streams;
3500
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3532
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3501
3533
  const events = await streamRes.json();
3502
3534
  const hasRunComplete = events.some((e) => e.type === `run` && e.headers?.operation === `update`);
3503
3535
  if (hasRunComplete) return events;
@@ -3550,11 +3582,11 @@ function runMockAgentCliTests(config) {
3550
3582
  test(`spawn → send → agent responds with State Protocol events`, async () => {
3551
3583
  const id = `cli-mock-${Date.now()}`;
3552
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) => {
3553
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/chat/${id}`)}`);
3585
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/chat/${id}`)));
3554
3586
  expect(res.status, `entity should exist`).toBe(200);
3555
3587
  const entity = await res.json();
3556
3588
  const streams = entity.streams;
3557
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3589
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3558
3590
  const events = await streamRes.json();
3559
3591
  expect(events.some((e) => e.type === `run`), `stream should contain run events from agent`).toBe(true);
3560
3592
  expect(events.some((e) => e.type === `text`), `stream should contain text events from agent`).toBe(true);
@@ -3564,11 +3596,11 @@ function runMockAgentCliTests(config) {
3564
3596
  test(`inspect shows entity after mock agent processes message`, async () => {
3565
3597
  const id = `cli-inspect-mock-${Date.now()}`;
3566
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) => {
3567
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/chat/${id}`)}`);
3599
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/chat/${id}`)));
3568
3600
  expect(res.status).toBe(200);
3569
3601
  const entity = await res.json();
3570
3602
  const streams = entity.streams;
3571
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3603
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3572
3604
  const events = await streamRes.json();
3573
3605
  const hasRunComplete = events.some((e) => e.type === `run` && e.headers?.operation === `update`);
3574
3606
  expect(hasRunComplete, `mock agent should have written run completion event`).toBe(true);