@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.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;
@@ -678,7 +696,13 @@ async function executeStep(ctx, step) {
678
696
  let entityUrl = ctx.currentEntityUrl;
679
697
  if (step.kind === `killUrl`) entityUrl = step.url;
680
698
  if (!entityUrl) throw new Error(`No current entity — did you spawn first?`);
681
- const res = await electricAgentsFetch$1(ctx.baseUrl, entityUrl, { method: `DELETE` });
699
+ const res = await electricAgentsFetch$1(ctx.baseUrl, `${entityUrl}/signal`, {
700
+ method: `POST`,
701
+ body: JSON.stringify({
702
+ signal: `SIGKILL`,
703
+ reason: `Killed by conformance test`
704
+ })
705
+ });
682
706
  (0, vitest.expect)(res.status).toBe(200);
683
707
  ctx.history.push({
684
708
  type: `entity_killed`,
@@ -689,7 +713,7 @@ async function executeStep(ctx, step) {
689
713
  case `readStream`: {
690
714
  if (!ctx.currentEntityStreams) throw new Error(`No current entity streams`);
691
715
  const streamPath = step.stream === `error` ? ctx.currentEntityStreams.error : ctx.currentEntityStreams.main;
692
- const res = await fetch(`${ctx.baseUrl}${streamPath}?offset=0000000000000000_0000000000000000`);
716
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${streamPath}?offset=0000000000000000_0000000000000000`));
693
717
  if (res.status === 200) {
694
718
  const text = await res.text();
695
719
  const messages = text ? JSON.parse(text) : [];
@@ -830,8 +854,8 @@ async function executeStep(ctx, step) {
830
854
  }
831
855
  case `amendSchemas`: {
832
856
  const body = {};
833
- if (step.input_schemas) body.inbox_schemas = step.input_schemas;
834
- 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;
835
859
  const res = await electricAgentsFetch$1(ctx.baseUrl, `/_electric/entity-types/${step.name}/schemas`, {
836
860
  method: `PATCH`,
837
861
  body: JSON.stringify(body)
@@ -1195,8 +1219,8 @@ function checkStreamPathsMatchEntityUrl(history) {
1195
1219
  /**
1196
1220
  * Spec S4 — Safety: entity status transitions must be valid.
1197
1221
  * spawning → running is valid (at spawn time)
1198
- * running/idle → stopped is valid (at kill time)
1199
- * stopped → running is NOT valid
1222
+ * running/idle → killed is valid (at kill time)
1223
+ * killed → running is NOT valid
1200
1224
  * Soundness: Sound | Completeness: Incomplete (only checks observed status reads)
1201
1225
  */
1202
1226
  function checkStatusTransitionsValid(history) {
@@ -1205,7 +1229,7 @@ function checkStatusTransitionsValid(history) {
1205
1229
  if (event.type === `entity_spawned`) lastStatus.set(event.entityUrl, event.status);
1206
1230
  if (event.type === `entity_status_checked`) {
1207
1231
  const prev = lastStatus.get(event.entityUrl);
1208
- if (prev === `stopped`) (0, vitest.expect)(event.status, `Safety: entity ${event.entityUrl} transitioned from stopped to ${event.status}`).toBe(`stopped`);
1232
+ if (prev === `killed`) (0, vitest.expect)(event.status, `Safety: entity ${event.entityUrl} transitioned from killed to ${event.status}`).toBe(`killed`);
1209
1233
  lastStatus.set(event.entityUrl, event.status);
1210
1234
  }
1211
1235
  }
@@ -1324,17 +1348,17 @@ function enabledElectricAgentsActions(model) {
1324
1348
  * Next relation — pure state transition for the model.
1325
1349
  * The real server execution happens separately in the property test.
1326
1350
  */
1327
- function applyElectricAgentsAction(model, action, targetIdx) {
1351
+ function applyElectricAgentsAction(model, action, targetIdx, opts) {
1328
1352
  switch (action) {
1329
1353
  case `register_type`: {
1330
1354
  const typeNum = model.entityTypes.length;
1331
1355
  return {
1332
1356
  ...model,
1333
1357
  entityTypes: [...model.entityTypes, {
1334
- name: `prop-type-${typeNum}`,
1358
+ name: opts?.typeName ?? `prop-type-${typeNum}`,
1335
1359
  hasCreationSchema: false,
1336
- hasInputSchemas: false,
1337
- hasOutputSchemas: false
1360
+ hasInboxSchemas: false,
1361
+ hasStateSchemas: false
1338
1362
  }]
1339
1363
  };
1340
1364
  }
@@ -1381,7 +1405,7 @@ function applyElectricAgentsAction(model, action, targetIdx) {
1381
1405
  const e = entities[targetIdx];
1382
1406
  entities[targetIdx] = {
1383
1407
  ...e,
1384
- status: `stopped`
1408
+ status: `killed`
1385
1409
  };
1386
1410
  return {
1387
1411
  ...model,
@@ -1492,7 +1516,7 @@ function checkStateProtocolInvariants(events) {
1492
1516
  //#endregion
1493
1517
  //#region src/cli-dsl.ts
1494
1518
  function subscriptionEndpoint(baseUrl, id) {
1495
- return `${baseUrl}/__ds/subscriptions/${encodeURIComponent(id)}`;
1519
+ return appendPathToUrl(baseUrl, `/__ds/subscriptions/${encodeURIComponent(id)}`);
1496
1520
  }
1497
1521
  function subscriptionPattern(pattern) {
1498
1522
  return pattern.replace(/^\/+/, ``);
@@ -1641,7 +1665,7 @@ var CliScenario = class {
1641
1665
  try {
1642
1666
  for (const step of this.steps) switch (step.kind) {
1643
1667
  case `setupType`: {
1644
- const res = await fetch(`${this.baseUrl}/_electric/entity-types`, {
1668
+ const res = await fetch(appendPathToUrl(this.baseUrl, `/_electric/entity-types`), {
1645
1669
  method: `POST`,
1646
1670
  headers: { "content-type": `application/json` },
1647
1671
  body: JSON.stringify(step.registration)
@@ -1770,7 +1794,7 @@ function startNoopReceiver() {
1770
1794
  //#endregion
1771
1795
  //#region src/electric-agents-tests.ts
1772
1796
  async function electricAgentsFetch(baseUrl, path, opts = {}) {
1773
- return fetch(`${baseUrl}${routeControlPlanePath(path)}`, {
1797
+ return fetch(appendPathToUrl(baseUrl, routeControlPlanePath(path)), {
1774
1798
  ...opts,
1775
1799
  headers: {
1776
1800
  "content-type": `application/json`,
@@ -1791,7 +1815,7 @@ function isEntityStreamPath(pathname) {
1791
1815
  async function pollEntityStatus(baseUrl, entityUrl, statuses, timeoutMs = 8e3) {
1792
1816
  const deadline = Date.now() + timeoutMs;
1793
1817
  while (Date.now() < deadline) {
1794
- const res = await fetch(`${baseUrl}${routeControlPlanePath(entityUrl)}`);
1818
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(entityUrl)));
1795
1819
  (0, vitest.expect)(res.status).toBe(200);
1796
1820
  const entity = await res.json();
1797
1821
  if (statuses.includes(String(entity.status))) return entity;
@@ -1806,13 +1830,13 @@ function runElectricAgentsConformanceTests(config) {
1806
1830
  description: `Test entity type for spawn`,
1807
1831
  creation_schema: { type: `object` }
1808
1832
  }).spawn(`spawn-test-agent`, `entity-1`).expectStatus(`running`).custom(async (ctx) => {
1809
- const mainRes = await fetch(`${ctx.baseUrl}${ctx.currentEntityStreams.main}`, { method: `HEAD` });
1833
+ const mainRes = await fetch(appendPathToUrl(ctx.baseUrl, ctx.currentEntityStreams.main), { method: `HEAD` });
1810
1834
  (0, vitest.expect)(mainRes.status).toBe(200);
1811
- const errorRes = await fetch(`${ctx.baseUrl}${ctx.currentEntityStreams.error}`, { method: `HEAD` });
1835
+ const errorRes = await fetch(appendPathToUrl(ctx.baseUrl, ctx.currentEntityStreams.error), { method: `HEAD` });
1812
1836
  (0, vitest.expect)(errorRes.status).toBe(200);
1813
1837
  }).run());
1814
1838
  (0, vitest.test)(`spawn at unregistered type returns UNKNOWN_ENTITY_TYPE`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
1815
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/unregistered/entity-1`)}`, {
1839
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/unregistered/entity-1`)), {
1816
1840
  method: `PUT`,
1817
1841
  headers: { "content-type": `application/json` },
1818
1842
  body: JSON.stringify({})
@@ -1854,7 +1878,7 @@ function runElectricAgentsConformanceTests(config) {
1854
1878
  creation_schema: { type: `object` }
1855
1879
  }).spawn(`webhook-ctx-agent`, `entity-1`).send({ ping: true }).expectWebhook().expectEntityContext({ type: `webhook-ctx-agent` }).respondDone().run());
1856
1880
  (0, vitest.test)(`send to nonexistent entity returns 404`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
1857
- 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`)), {
1858
1882
  method: `POST`,
1859
1883
  headers: { "content-type": `application/json` },
1860
1884
  body: JSON.stringify({ payload: {} })
@@ -1902,8 +1926,8 @@ function runElectricAgentsConformanceTests(config) {
1902
1926
  name: `status-filter-agent`,
1903
1927
  description: `Test entity type for status filter`,
1904
1928
  creation_schema: { type: `object` }
1905
- }).spawn(`status-filter-agent`, `entity-1`).spawn(`status-filter-agent`, `entity-2`).kill().list({ status: `stopped` }).custom(async (ctx) => {
1906
- (0, vitest.expect)(ctx.lastListResult.every((e) => e.status === `stopped`)).toBe(true);
1929
+ }).spawn(`status-filter-agent`, `entity-1`).spawn(`status-filter-agent`, `entity-2`).kill().list({ status: `killed` }).custom(async (ctx) => {
1930
+ (0, vitest.expect)(ctx.lastListResult.every((e) => e.status === `killed`)).toBe(true);
1907
1931
  }).list({ status: `running` }).custom(async (ctx) => {
1908
1932
  (0, vitest.expect)(ctx.lastListResult.every((e) => [`running`, `idle`].includes(e.status))).toBe(true);
1909
1933
  }).run();
@@ -1918,7 +1942,7 @@ function runElectricAgentsConformanceTests(config) {
1918
1942
  name: `kill-test-agent`,
1919
1943
  description: `Test entity type for kill`,
1920
1944
  creation_schema: { type: `object` }
1921
- }).spawn(`kill-test-agent`, `entity-1`).kill().expectStatus(`stopped`).expectSendError(`NOT_RUNNING`, 409).run());
1945
+ }).spawn(`kill-test-agent`, `entity-1`).kill().expectStatus(`killed`).expectSendError(`NOT_RUNNING`, 409).run());
1922
1946
  (0, vitest.test)(`stream data persists after kill`, () => electricAgents(config.baseUrl).subscription(`/kill-persist-agent/**`, `kill-persist-sub`).registerType({
1923
1947
  name: `kill-persist-agent`,
1924
1948
  description: `Test entity type for kill persistence`,
@@ -1927,8 +1951,9 @@ function runElectricAgentsConformanceTests(config) {
1927
1951
  const msgs = ctx.lastStreamMessages;
1928
1952
  const msgReceived = msgs.find((m) => m.type === `inbox`);
1929
1953
  (0, vitest.expect)(msgReceived.value?.payload).toEqual({ before: `kill` });
1930
- const stopped = msgs.find((m) => m.type === `entity_stopped`);
1931
- (0, vitest.expect)(stopped).toBeDefined();
1954
+ const signal = msgs.find((m) => m.type === `signal`);
1955
+ (0, vitest.expect)(signal).toBeDefined();
1956
+ (0, vitest.expect)(signal.value?.signal).toBe(`SIGKILL`);
1932
1957
  }).run());
1933
1958
  vitest.test.skip(`multiple entities under same subscription`, () => {
1934
1959
  let firstEntityUrl = null;
@@ -1942,7 +1967,10 @@ function runElectricAgentsConformanceTests(config) {
1942
1967
  }).spawn(`multi-test-worker`, `entity-2`).custom(async (ctx) => {
1943
1968
  secondEntityUrl = ctx.currentEntityUrl;
1944
1969
  }).custom(async (ctx) => {
1945
- const res = await electricAgentsFetch(ctx.baseUrl, firstEntityUrl, { method: `DELETE` });
1970
+ const res = await electricAgentsFetch(ctx.baseUrl, `${firstEntityUrl}/signal`, {
1971
+ method: `POST`,
1972
+ body: JSON.stringify({ signal: `SIGKILL` })
1973
+ });
1946
1974
  (0, vitest.expect)(res.status).toBe(200);
1947
1975
  ctx.history.push({
1948
1976
  type: `entity_killed`,
@@ -1964,8 +1992,8 @@ function runElectricAgentsConformanceTests(config) {
1964
1992
  description: `Test entity type for E2E lifecycle`,
1965
1993
  creation_schema: { type: `object` }
1966
1994
  }).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) => {
1967
- const entity = await pollEntityStatus(ctx.baseUrl, ctx.currentEntityUrl, [`stopped`]);
1968
- (0, vitest.expect)(entity.status).toBe(`stopped`);
1995
+ const entity = await pollEntityStatus(ctx.baseUrl, ctx.currentEntityUrl, [`killed`]);
1996
+ (0, vitest.expect)(entity.status).toBe(`killed`);
1969
1997
  }).expectSendError(`NOT_RUNNING`, 409).run());
1970
1998
  });
1971
1999
  (0, vitest.describe)(`Electric Agents Entity Type Registration`, () => {
@@ -1976,12 +2004,12 @@ function runElectricAgentsConformanceTests(config) {
1976
2004
  type: `object`,
1977
2005
  properties: { name: { type: `string` } }
1978
2006
  },
1979
- input_schemas: { query: {
2007
+ inbox_schemas: { query: {
1980
2008
  type: `object`,
1981
2009
  properties: { text: { type: `string` } },
1982
2010
  required: [`text`]
1983
2011
  } },
1984
- output_schemas: { result: {
2012
+ state_schemas: { result: {
1985
2013
  type: `object`,
1986
2014
  properties: { answer: { type: `string` } }
1987
2015
  } }
@@ -2017,7 +2045,7 @@ function runElectricAgentsConformanceTests(config) {
2017
2045
  type: `object`,
2018
2046
  properties: { x: { type: `number` } }
2019
2047
  },
2020
- input_schemas: { ping: {
2048
+ inbox_schemas: { ping: {
2021
2049
  type: `object`,
2022
2050
  properties: { msg: { type: `string` } }
2023
2051
  } }
@@ -2043,7 +2071,7 @@ function runElectricAgentsConformanceTests(config) {
2043
2071
  description: `First registration`,
2044
2072
  creation_schema: { type: `object` }
2045
2073
  }).custom(async (ctx) => {
2046
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2074
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2047
2075
  method: `POST`,
2048
2076
  headers: { "content-type": `application/json` },
2049
2077
  body: JSON.stringify({
@@ -2060,7 +2088,7 @@ function runElectricAgentsConformanceTests(config) {
2060
2088
  }).run();
2061
2089
  });
2062
2090
  (0, vitest.test)(`register rejects missing required fields`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
2063
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2091
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2064
2092
  method: `POST`,
2065
2093
  headers: { "content-type": `application/json` },
2066
2094
  body: JSON.stringify({})
@@ -2105,7 +2133,7 @@ function runElectricAgentsConformanceTests(config) {
2105
2133
  }).expectSpawnSchemaError(typeName, `bad-entity-1`, { args: { invalid: true } }).run();
2106
2134
  });
2107
2135
  (0, vitest.test)(`typed spawn at unregistered type returns UNKNOWN_ENTITY_TYPE (C9)`, () => electricAgents(config.baseUrl).custom(async (ctx) => {
2108
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/nonexistent/should-fail`)}`, {
2136
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/nonexistent/should-fail`)), {
2109
2137
  method: `PUT`,
2110
2138
  headers: { "content-type": `application/json` },
2111
2139
  body: JSON.stringify({})
@@ -2124,7 +2152,7 @@ function runElectricAgentsConformanceTests(config) {
2124
2152
  }).spawn(typeName, `parent-entity`).custom(async (ctx) => {
2125
2153
  parentUrl = ctx.currentEntityUrl;
2126
2154
  }).custom(async (ctx) => {
2127
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/child-entity`)}`, {
2155
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/child-entity`)), {
2128
2156
  method: `PUT`,
2129
2157
  headers: { "content-type": `application/json` },
2130
2158
  body: JSON.stringify({ parent: parentUrl })
@@ -2151,7 +2179,7 @@ function runElectricAgentsConformanceTests(config) {
2151
2179
  description: `Type for orphan spawn test`,
2152
2180
  creation_schema: { type: `object` }
2153
2181
  }).custom(async (ctx) => {
2154
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/orphan-entity`)}`, {
2182
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/orphan-entity`)), {
2155
2183
  method: `PUT`,
2156
2184
  headers: { "content-type": `application/json` },
2157
2185
  body: JSON.stringify({ parent: `/nonexistent/parent` })
@@ -2172,7 +2200,7 @@ function runElectricAgentsConformanceTests(config) {
2172
2200
  color: `blue`,
2173
2201
  count: `42`
2174
2202
  } }).custom(async (ctx) => {
2175
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
2203
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
2176
2204
  const entity = await res.json();
2177
2205
  const tags = entity.tags;
2178
2206
  (0, vitest.expect)(tags.color).toBe(`blue`);
@@ -2186,7 +2214,7 @@ function runElectricAgentsConformanceTests(config) {
2186
2214
  name: typeName,
2187
2215
  description: `Type with string-only tags`
2188
2216
  }).custom(async (ctx) => {
2189
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`/${typeName}/should-fail`)}`, {
2217
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`/${typeName}/should-fail`)), {
2190
2218
  method: `PUT`,
2191
2219
  headers: { "content-type": `application/json` },
2192
2220
  body: JSON.stringify({ tags: { wrong: 123 } })
@@ -2200,20 +2228,20 @@ function runElectricAgentsConformanceTests(config) {
2200
2228
  name: typeName,
2201
2229
  description: `Type with default empty tags`
2202
2230
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2203
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
2231
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
2204
2232
  const entity = await res.json();
2205
2233
  (0, vitest.expect)(entity.tags).toEqual({});
2206
2234
  }).run();
2207
2235
  });
2208
2236
  });
2209
2237
  (0, vitest.describe)(`Electric Agents Schema Validation Gates`, () => {
2210
- (0, vitest.test)(`send validates input_schemas (C11)`, () => {
2238
+ (0, vitest.test)(`send validates inbox_schemas (C11)`, () => {
2211
2239
  const typeName = `send-schema-valid-${Date.now()}`;
2212
2240
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-schema-sub`).registerType({
2213
2241
  name: typeName,
2214
- description: `Type with input schemas`,
2242
+ description: `Type with inbox schemas`,
2215
2243
  creation_schema: { type: `object` },
2216
- input_schemas: { query: {
2244
+ inbox_schemas: { query: {
2217
2245
  type: `object`,
2218
2246
  properties: { text: { type: `string` } },
2219
2247
  required: [`text`]
@@ -2224,9 +2252,9 @@ function runElectricAgentsConformanceTests(config) {
2224
2252
  const typeName = `send-schema-inv-${Date.now()}`;
2225
2253
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-schema-inv-sub`).registerType({
2226
2254
  name: typeName,
2227
- description: `Type with strict input schemas`,
2255
+ description: `Type with strict inbox schemas`,
2228
2256
  creation_schema: { type: `object` },
2229
- input_schemas: { query: {
2257
+ inbox_schemas: { query: {
2230
2258
  type: `object`,
2231
2259
  properties: { text: { type: `string` } },
2232
2260
  required: [`text`]
@@ -2237,30 +2265,30 @@ function runElectricAgentsConformanceTests(config) {
2237
2265
  const typeName = `send-unknown-type-${Date.now()}`;
2238
2266
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-unknown-sub`).registerType({
2239
2267
  name: typeName,
2240
- description: `Type with defined input schemas`,
2268
+ description: `Type with defined inbox schemas`,
2241
2269
  creation_schema: { type: `object` },
2242
- input_schemas: { query: {
2270
+ inbox_schemas: { query: {
2243
2271
  type: `object`,
2244
2272
  properties: { text: { type: `string` } },
2245
2273
  required: [`text`]
2246
2274
  } }
2247
2275
  }).spawn(typeName, `entity-1`).expectSendUnknownType({ text: `hi` }, { type: `unknown_type` }).run();
2248
2276
  });
2249
- (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`, () => {
2250
2278
  const typeName = `send-no-schemas-${Date.now()}`;
2251
2279
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-noschema-sub`).registerType({
2252
2280
  name: typeName,
2253
- description: `Type without input schemas`,
2281
+ description: `Type without inbox schemas`,
2254
2282
  creation_schema: { type: `object` }
2255
2283
  }).spawn(typeName, `entity-1`).send({ anything: `goes` }).expectWebhook().respondDone().run();
2256
2284
  });
2257
- (0, vitest.test)(`send with empty input_schemas rejects all`, () => {
2285
+ (0, vitest.test)(`send with empty inbox_schemas rejects all`, () => {
2258
2286
  const typeName = `send-empty-schemas-${Date.now()}`;
2259
2287
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `send-empty-sub`).registerType({
2260
2288
  name: typeName,
2261
- description: `Type with empty input schemas`,
2289
+ description: `Type with empty inbox schemas`,
2262
2290
  creation_schema: { type: `object` },
2263
- input_schemas: {}
2291
+ inbox_schemas: {}
2264
2292
  }).spawn(typeName, `entity-1`).expectSendUnknownType({ text: `anything` }, { type: `some_type` }).run();
2265
2293
  });
2266
2294
  vitest.test.skip(`write appends event to entity stream`, () => {
@@ -2269,7 +2297,7 @@ function runElectricAgentsConformanceTests(config) {
2269
2297
  name: typeName,
2270
2298
  description: `Type for write test`,
2271
2299
  creation_schema: { type: `object` },
2272
- output_schemas: { research_result: {
2300
+ state_schemas: { research_result: {
2273
2301
  type: `object`,
2274
2302
  properties: { findings: {
2275
2303
  type: `array`,
@@ -2278,13 +2306,13 @@ function runElectricAgentsConformanceTests(config) {
2278
2306
  } }
2279
2307
  }).spawn(typeName, `entity-1`).write({ findings: [`test`] }, { type: `research_result` }).readStream().expectStreamContains(`research_result`).run();
2280
2308
  });
2281
- vitest.test.skip(`write validates output_schemas (C12)`, () => {
2309
+ vitest.test.skip(`write validates state_schemas (C12)`, () => {
2282
2310
  const typeName = `write-schema-inv-${Date.now()}`;
2283
2311
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-schema-sub`).registerType({
2284
2312
  name: typeName,
2285
- description: `Type with strict output schemas`,
2313
+ description: `Type with strict state schemas`,
2286
2314
  creation_schema: { type: `object` },
2287
- output_schemas: { result: {
2315
+ state_schemas: { result: {
2288
2316
  type: `object`,
2289
2317
  properties: { value: { type: `number` } },
2290
2318
  required: [`value`]
@@ -2295,19 +2323,19 @@ function runElectricAgentsConformanceTests(config) {
2295
2323
  const typeName = `write-unknown-type-${Date.now()}`;
2296
2324
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-unknown-sub`).registerType({
2297
2325
  name: typeName,
2298
- description: `Type with defined output schemas`,
2326
+ description: `Type with defined state schemas`,
2299
2327
  creation_schema: { type: `object` },
2300
- output_schemas: { result: {
2328
+ state_schemas: { result: {
2301
2329
  type: `object`,
2302
2330
  properties: { value: { type: `number` } }
2303
2331
  } }
2304
2332
  }).spawn(typeName, `entity-1`).expectWriteUnknownType({ data: `test` }, { type: `unknown_event` }).run();
2305
2333
  });
2306
- 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`, () => {
2307
2335
  const typeName = `write-no-schemas-${Date.now()}`;
2308
2336
  return electricAgents(config.baseUrl).subscription(`/${typeName}/**`, `write-noschema-sub`).registerType({
2309
2337
  name: typeName,
2310
- description: `Type without output schemas`,
2338
+ description: `Type without state schemas`,
2311
2339
  creation_schema: { type: `object` }
2312
2340
  }).spawn(typeName, `entity-1`).write({ anything: `goes` }).run();
2313
2341
  });
@@ -2320,7 +2348,7 @@ function runElectricAgentsConformanceTests(config) {
2320
2348
  }).spawn(typeName, `entity-1`).kill().custom(async (ctx) => {
2321
2349
  const writeHeaders = { "content-type": `application/json` };
2322
2350
  if (ctx.currentWriteToken) writeHeaders[`authorization`] = `Bearer ${ctx.currentWriteToken}`;
2323
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
2351
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
2324
2352
  method: `POST`,
2325
2353
  headers: writeHeaders,
2326
2354
  body: JSON.stringify({
@@ -2374,7 +2402,7 @@ function runElectricAgentsConformanceTests(config) {
2374
2402
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2375
2403
  const tagHeaders = { "content-type": `application/json` };
2376
2404
  if (ctx.currentWriteToken) tagHeaders[`authorization`] = `Bearer ${ctx.currentWriteToken}`;
2377
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/owner`)}`, {
2405
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/owner`)), {
2378
2406
  method: `POST`,
2379
2407
  headers: tagHeaders,
2380
2408
  body: JSON.stringify({ value: 123 })
@@ -2394,7 +2422,7 @@ function runElectricAgentsConformanceTests(config) {
2394
2422
  }).custom(async (ctx) => {
2395
2423
  const tagHeaders = {};
2396
2424
  if (ctx.currentWriteToken) tagHeaders.authorization = `Bearer ${ctx.currentWriteToken}`;
2397
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/priority`)}`, {
2425
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/priority`)), {
2398
2426
  method: `DELETE`,
2399
2427
  headers: tagHeaders
2400
2428
  });
@@ -2427,12 +2455,12 @@ function runElectricAgentsConformanceTests(config) {
2427
2455
  name: typeName,
2428
2456
  description: `Type for schema amendment`,
2429
2457
  creation_schema: { type: `object` },
2430
- input_schemas: { query: {
2458
+ inbox_schemas: { query: {
2431
2459
  type: `object`,
2432
2460
  properties: { text: { type: `string` } },
2433
2461
  required: [`text`]
2434
2462
  } }
2435
- }).amendSchemas(typeName, { input_schemas: { command: {
2463
+ }).amendSchemas(typeName, { inbox_schemas: { command: {
2436
2464
  type: `object`,
2437
2465
  properties: { action: { type: `string` } },
2438
2466
  required: [`action`]
@@ -2444,13 +2472,13 @@ function runElectricAgentsConformanceTests(config) {
2444
2472
  name: typeName,
2445
2473
  description: `Type for schema conflict test`,
2446
2474
  creation_schema: { type: `object` },
2447
- input_schemas: { query: {
2475
+ inbox_schemas: { query: {
2448
2476
  type: `object`,
2449
2477
  properties: { text: { type: `string` } },
2450
2478
  required: [`text`]
2451
2479
  } }
2452
2480
  }).custom(async (ctx) => {
2453
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types/${typeName}/schemas`, {
2481
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types/${typeName}/schemas`), {
2454
2482
  method: `PATCH`,
2455
2483
  headers: { "content-type": `application/json` },
2456
2484
  body: JSON.stringify({ inbox_schemas: { query: {
@@ -2467,13 +2495,13 @@ function runElectricAgentsConformanceTests(config) {
2467
2495
  name: typeName,
2468
2496
  description: `Type for revision pinning test`,
2469
2497
  creation_schema: { type: `object` },
2470
- input_schemas: { query: {
2498
+ inbox_schemas: { query: {
2471
2499
  type: `object`,
2472
2500
  properties: { text: { type: `string` } },
2473
2501
  required: [`text`]
2474
2502
  } }
2475
2503
  }).spawn(typeName, `entity-1`).custom(async (ctx) => {
2476
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types/${typeName}/schemas`, {
2504
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types/${typeName}/schemas`), {
2477
2505
  method: `PATCH`,
2478
2506
  headers: { "content-type": `application/json` },
2479
2507
  body: JSON.stringify({ inbox_schemas: { new_command: {
@@ -2483,7 +2511,7 @@ function runElectricAgentsConformanceTests(config) {
2483
2511
  });
2484
2512
  (0, vitest.expect)(res.status).toBe(200);
2485
2513
  }).custom(async (ctx) => {
2486
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/send`)}`, {
2514
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/send`)), {
2487
2515
  method: `POST`,
2488
2516
  headers: { "content-type": `application/json` },
2489
2517
  body: JSON.stringify({
@@ -2514,7 +2542,7 @@ function runElectricAgentsConformanceTests(config) {
2514
2542
  };
2515
2543
  await receiver.start(manifest);
2516
2544
  try {
2517
- const res = await fetch(`${ctx.baseUrl}/_electric/entity-types`, {
2545
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `/_electric/entity-types`), {
2518
2546
  method: `POST`,
2519
2547
  headers: { "content-type": `application/json` },
2520
2548
  body: JSON.stringify({
@@ -2563,7 +2591,6 @@ function runElectricAgentsConformanceTests(config) {
2563
2591
  const runId = `prop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2564
2592
  const baseUrl = config.baseUrl;
2565
2593
  const entityUrls = [];
2566
- const registeredTypeNames = [];
2567
2594
  const scenario = electricAgents(baseUrl);
2568
2595
  let model = {
2569
2596
  entityTypes: [],
@@ -2571,21 +2598,21 @@ function runElectricAgentsConformanceTests(config) {
2571
2598
  nextEntityNum: 0
2572
2599
  };
2573
2600
  let entityCounter = 0;
2601
+ let typeCounter = 0;
2574
2602
  for (const action of actions) {
2575
2603
  const valid = enabledElectricAgentsActions(model);
2576
2604
  if (!valid.includes(action)) continue;
2577
2605
  switch (action) {
2578
2606
  case `register_type`: {
2579
- const typeNum = model.entityTypes.length;
2607
+ const typeNum = typeCounter++;
2580
2608
  const typeName = `prop-type-${runId}-${typeNum}`;
2581
- registeredTypeNames.push(typeName);
2582
2609
  scenario.subscription(`/${typeName}/**`, `prop-sub-${typeName}`);
2583
2610
  scenario.registerType({
2584
2611
  name: typeName,
2585
2612
  description: `Property-based test type ${typeNum}`,
2586
2613
  creation_schema: { type: `object` }
2587
2614
  });
2588
- model = applyElectricAgentsAction(model, `register_type`);
2615
+ model = applyElectricAgentsAction(model, `register_type`, void 0, { typeName });
2589
2616
  break;
2590
2617
  }
2591
2618
  case `delete_type`: {
@@ -2593,14 +2620,13 @@ function runElectricAgentsConformanceTests(config) {
2593
2620
  const deletableIndices = model.entityTypes.map((t, i) => !runningModelTypeNames.has(t.name) ? i : -1).filter((i) => i >= 0);
2594
2621
  if (deletableIndices.length === 0) break;
2595
2622
  const deleteIdx = deletableIndices[0];
2596
- scenario.deleteType(registeredTypeNames[deleteIdx]);
2597
- registeredTypeNames.splice(deleteIdx, 1);
2623
+ scenario.deleteType(model.entityTypes[deleteIdx].name);
2598
2624
  model = applyElectricAgentsAction(model, `delete_type`);
2599
2625
  break;
2600
2626
  }
2601
2627
  case `spawn`: {
2602
- if (registeredTypeNames.length === 0) break;
2603
- const typeName = registeredTypeNames[0];
2628
+ if (model.entityTypes.length === 0) break;
2629
+ const typeName = model.entityTypes[0].name;
2604
2630
  const instanceId = `entity-${entityCounter++}`;
2605
2631
  scenario.spawn(typeName, instanceId);
2606
2632
  scenario.custom(async (ctx) => {
@@ -2645,7 +2671,10 @@ function runElectricAgentsConformanceTests(config) {
2645
2671
  scenario.custom(async (ctx) => {
2646
2672
  const url = entityUrls[targetIdx];
2647
2673
  if (!url) return;
2648
- const res = await electricAgentsFetch(ctx.baseUrl, url, { method: `DELETE` });
2674
+ const res = await electricAgentsFetch(ctx.baseUrl, `${url}/signal`, {
2675
+ method: `POST`,
2676
+ body: JSON.stringify({ signal: `SIGKILL` })
2677
+ });
2649
2678
  (0, vitest.expect)(res.status).toBe(200);
2650
2679
  ctx.history.push({
2651
2680
  type: `entity_killed`,
@@ -3019,7 +3048,10 @@ function runElectricAgentsConformanceTests(config) {
3019
3048
  description: `test`
3020
3049
  })
3021
3050
  });
3022
- const res = await electricAgentsFetch(config.baseUrl, `/${typeName}/nonexistent-id-12345`, { method: `DELETE` });
3051
+ const res = await electricAgentsFetch(config.baseUrl, `/${typeName}/nonexistent-id-12345/signal`, {
3052
+ method: `POST`,
3053
+ body: JSON.stringify({ signal: `SIGKILL` })
3054
+ });
3023
3055
  (0, vitest.expect)(res.status).toBe(404);
3024
3056
  const body = await res.json();
3025
3057
  (0, vitest.expect)(body.error.code).toBe(`NOT_FOUND`);
@@ -3088,7 +3120,7 @@ function runElectricAgentsConformanceTests(config) {
3088
3120
  name: `rev-multi-agent-${id}`,
3089
3121
  description: `Test multi-revision pinning`,
3090
3122
  creation_schema: { type: `object` },
3091
- input_schemas: { greet: {
3123
+ inbox_schemas: { greet: {
3092
3124
  type: `object`,
3093
3125
  properties: { name: { type: `string` } },
3094
3126
  required: [`name`]
@@ -3129,7 +3161,7 @@ function runElectricAgentsConformanceTests(config) {
3129
3161
  }).deleteType(`amend-del-agent-${id}`).custom(async (ctx) => {
3130
3162
  const res = await electricAgentsFetch(ctx.baseUrl, `/_electric/entity-types/amend-del-agent-${id}/schemas`, {
3131
3163
  method: `PATCH`,
3132
- body: JSON.stringify({ input_schemas: { msg: { type: `object` } } })
3164
+ body: JSON.stringify({ inbox_schemas: { msg: { type: `object` } } })
3133
3165
  });
3134
3166
  (0, vitest.expect)(res.status).toBe(404);
3135
3167
  }).run();
@@ -3205,7 +3237,7 @@ function runElectricAgentsConformanceTests(config) {
3205
3237
  description: `Test write without token`,
3206
3238
  creation_schema: { type: `object` }
3207
3239
  }).spawn(`auth-notoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3208
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
3240
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
3209
3241
  method: `POST`,
3210
3242
  headers: { "content-type": `application/json` },
3211
3243
  body: JSON.stringify({
@@ -3225,7 +3257,7 @@ function runElectricAgentsConformanceTests(config) {
3225
3257
  description: `Test write with wrong token`,
3226
3258
  creation_schema: { type: `object` }
3227
3259
  }).spawn(`auth-wrongtoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3228
- const res = await fetch(`${ctx.baseUrl}${ctx.currentEntityUrl}/main`, {
3260
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, `${ctx.currentEntityUrl}/main`), {
3229
3261
  method: `POST`,
3230
3262
  headers: {
3231
3263
  "content-type": `application/json`,
@@ -3258,7 +3290,7 @@ function runElectricAgentsConformanceTests(config) {
3258
3290
  description: `Test tag update without token`,
3259
3291
  creation_schema: { type: `object` }
3260
3292
  }).spawn(`auth-meta-notoken-agent-${id}`, `entity-1`).custom(async (ctx) => {
3261
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/tags/key`)}`, {
3293
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/tags/key`)), {
3262
3294
  method: `POST`,
3263
3295
  headers: { "content-type": `application/json` },
3264
3296
  body: JSON.stringify({ value: `value` })
@@ -3283,7 +3315,7 @@ function runElectricAgentsConformanceTests(config) {
3283
3315
  description: `Test send without auth`,
3284
3316
  creation_schema: { type: `object` }
3285
3317
  }).spawn(`auth-send-noauth-agent-${id}`, `entity-1`).custom(async (ctx) => {
3286
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(`${ctx.currentEntityUrl}/send`)}`, {
3318
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(`${ctx.currentEntityUrl}/send`)), {
3287
3319
  method: `POST`,
3288
3320
  headers: { "content-type": `application/json` },
3289
3321
  body: JSON.stringify({ payload: `hi` })
@@ -3298,7 +3330,7 @@ function runElectricAgentsConformanceTests(config) {
3298
3330
  description: `Test GET does not leak write_token`,
3299
3331
  creation_schema: { type: `object` }
3300
3332
  }).spawn(`auth-noleak-agent-${id}`, `entity-1`).custom(async (ctx) => {
3301
- const res = await fetch(`${ctx.baseUrl}${routeControlPlanePath(ctx.currentEntityUrl)}`);
3333
+ const res = await fetch(appendPathToUrl(ctx.baseUrl, routeControlPlanePath(ctx.currentEntityUrl)));
3302
3334
  (0, vitest.expect)(res.status).toBe(200);
3303
3335
  const entity = await res.json();
3304
3336
  (0, vitest.expect)(entity.write_token).toBeUndefined();
@@ -3368,7 +3400,7 @@ function runCliConformanceTests(config) {
3368
3400
  name: `cli-spawn-type`,
3369
3401
  description: `Type for CLI spawn test`
3370
3402
  }).setupSubscription(`/cli-spawn-type/**`, `cli-spawn-sub`).exec(`spawn`, `/cli-spawn-type/${id}`).expectExitCode(0).expectStdout(/Spawned/).verifyApi(async (baseUrl) => {
3371
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-spawn-type/${id}`)}`);
3403
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-spawn-type/${id}`)));
3372
3404
  (0, vitest.expect)(res.status).toBe(200);
3373
3405
  const entity = await res.json();
3374
3406
  (0, vitest.expect)([`running`, `idle`]).toContain(entity.status);
@@ -3411,11 +3443,11 @@ function runCliConformanceTests(config) {
3411
3443
  name: `cli-send-type`,
3412
3444
  description: `Type for CLI send test`
3413
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) => {
3414
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-send-type/${id}`)}`);
3446
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-send-type/${id}`)));
3415
3447
  (0, vitest.expect)(res.status).toBe(200);
3416
3448
  const entity = await res.json();
3417
3449
  const streams = entity.streams;
3418
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3450
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3419
3451
  const events = await streamRes.json();
3420
3452
  (0, vitest.expect)(events.length).toBeGreaterThanOrEqual(1);
3421
3453
  const msgEvent = events.find((e) => e.type === `inbox`);
@@ -3435,7 +3467,7 @@ function runCliConformanceTests(config) {
3435
3467
  name: `cli-inspect-etype`,
3436
3468
  description: `Type for CLI inspect test`
3437
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) => {
3438
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-inspect-etype/${id}`)}`);
3470
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-inspect-etype/${id}`)));
3439
3471
  (0, vitest.expect)(res.status).toBe(200);
3440
3472
  const entity = await res.json();
3441
3473
  (0, vitest.expect)([`running`, `idle`]).toContain(entity.status);
@@ -3452,8 +3484,8 @@ function runCliConformanceTests(config) {
3452
3484
  name: `cli-kill-type`,
3453
3485
  description: `Type for CLI kill test`
3454
3486
  }).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) => {
3455
- const entity = await pollEntityStatus(baseUrl, `/cli-kill-type/${id}`, [`stopped`]);
3456
- (0, vitest.expect)(entity.status).toBe(`stopped`);
3487
+ const entity = await pollEntityStatus(baseUrl, `/cli-kill-type/${id}`, [`killed`]);
3488
+ (0, vitest.expect)(entity.status).toBe(`killed`);
3457
3489
  }).run();
3458
3490
  }, 15e3);
3459
3491
  (0, vitest.test)(`kill nonexistent entity fails`, async () => {
@@ -3467,13 +3499,13 @@ function runCliConformanceTests(config) {
3467
3499
  name: `cli-lifecycle-type`,
3468
3500
  description: `Type for full lifecycle test`
3469
3501
  }).setupSubscription(`/cli-lifecycle-type/**`, `cli-lifecycle-sub`).exec(`spawn`, `/cli-lifecycle-type/${id}`).expectExitCode(0).expectStdout(/Spawned/).verifyApi(async (baseUrl) => {
3470
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/cli-lifecycle-type/${id}`)}`);
3502
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/cli-lifecycle-type/${id}`)));
3471
3503
  (0, vitest.expect)(res.status, `entity should exist after spawn`).toBe(200);
3472
3504
  const entity = await res.json();
3473
3505
  (0, vitest.expect)([`running`, `idle`]).toContain(entity.status);
3474
3506
  }).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) => {
3475
- const entity = await pollEntityStatus(baseUrl, `/cli-lifecycle-type/${id}`, [`stopped`]);
3476
- (0, vitest.expect)(entity.status).toBe(`stopped`);
3507
+ const entity = await pollEntityStatus(baseUrl, `/cli-lifecycle-type/${id}`, [`killed`]);
3508
+ (0, vitest.expect)(entity.status).toBe(`killed`);
3477
3509
  }).exec(`ps`, `--status`, `running`).expectExitCode(0).expectStdoutNot(new RegExp(id)).run();
3478
3510
  }, 15e3);
3479
3511
  (0, vitest.test)(`send to stopped entity fails`, async () => {
@@ -3498,7 +3530,7 @@ function runCliConformanceTests(config) {
3498
3530
  }
3499
3531
  function runMockAgentTests(config) {
3500
3532
  async function spawnEntity(baseUrl, typeName, instanceId) {
3501
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/${encodeURIComponent(typeName)}/${encodeURIComponent(instanceId)}`)}`, {
3533
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/${encodeURIComponent(typeName)}/${encodeURIComponent(instanceId)}`)), {
3502
3534
  method: `PUT`,
3503
3535
  headers: { "content-type": `application/json` },
3504
3536
  body: JSON.stringify({})
@@ -3507,7 +3539,7 @@ function runMockAgentTests(config) {
3507
3539
  return await res.json();
3508
3540
  }
3509
3541
  async function sendMessage(baseUrl, entityUrl, text) {
3510
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`${entityUrl}/send`)}`, {
3542
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`${entityUrl}/send`)), {
3511
3543
  method: `POST`,
3512
3544
  headers: { "content-type": `application/json` },
3513
3545
  body: JSON.stringify({ payload: { text } })
@@ -3517,11 +3549,11 @@ function runMockAgentTests(config) {
3517
3549
  async function pollForAgentResponse(baseUrl, entityUrl, timeoutMs = 1e4) {
3518
3550
  const start = Date.now();
3519
3551
  while (Date.now() - start < timeoutMs) {
3520
- const entityRes = await fetch(`${baseUrl}${routeControlPlanePath(entityUrl)}`);
3552
+ const entityRes = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(entityUrl)));
3521
3553
  if (!entityRes.ok) throw new Error(`Entity ${entityUrl} not found`);
3522
3554
  const entity = await entityRes.json();
3523
3555
  const streams = entity.streams;
3524
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3556
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3525
3557
  const events = await streamRes.json();
3526
3558
  const hasRunComplete = events.some((e) => e.type === `run` && e.headers?.operation === `update`);
3527
3559
  if (hasRunComplete) return events;
@@ -3574,11 +3606,11 @@ function runMockAgentCliTests(config) {
3574
3606
  (0, vitest.test)(`spawn → send → agent responds with State Protocol events`, async () => {
3575
3607
  const id = `cli-mock-${Date.now()}`;
3576
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) => {
3577
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/chat/${id}`)}`);
3609
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/chat/${id}`)));
3578
3610
  (0, vitest.expect)(res.status, `entity should exist`).toBe(200);
3579
3611
  const entity = await res.json();
3580
3612
  const streams = entity.streams;
3581
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3613
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3582
3614
  const events = await streamRes.json();
3583
3615
  (0, vitest.expect)(events.some((e) => e.type === `run`), `stream should contain run events from agent`).toBe(true);
3584
3616
  (0, vitest.expect)(events.some((e) => e.type === `text`), `stream should contain text events from agent`).toBe(true);
@@ -3588,11 +3620,11 @@ function runMockAgentCliTests(config) {
3588
3620
  (0, vitest.test)(`inspect shows entity after mock agent processes message`, async () => {
3589
3621
  const id = `cli-inspect-mock-${Date.now()}`;
3590
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) => {
3591
- const res = await fetch(`${baseUrl}${routeControlPlanePath(`/chat/${id}`)}`);
3623
+ const res = await fetch(appendPathToUrl(baseUrl, routeControlPlanePath(`/chat/${id}`)));
3592
3624
  (0, vitest.expect)(res.status).toBe(200);
3593
3625
  const entity = await res.json();
3594
3626
  const streams = entity.streams;
3595
- const streamRes = await fetch(`${baseUrl}${streams.main}?offset=0000000000000000_0000000000000000`);
3627
+ const streamRes = await fetch(appendPathToUrl(baseUrl, `${streams.main}?offset=0000000000000000_0000000000000000`));
3596
3628
  const events = await streamRes.json();
3597
3629
  const hasRunComplete = events.some((e) => e.type === `run` && e.headers?.operation === `update`);
3598
3630
  (0, vitest.expect)(hasRunComplete, `mock agent should have written run completion event`).toBe(true);