@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 +148 -116
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.js +148 -116
- package/package.json +3 -3
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:
|
|
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
|
-
|
|
58
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
381
|
-
|
|
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: `
|
|
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: `
|
|
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: `
|
|
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
|
|
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(
|
|
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.
|
|
834
|
-
if (step.
|
|
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 →
|
|
1199
|
-
*
|
|
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 === `
|
|
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
|
-
|
|
1337
|
-
|
|
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: `
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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: `
|
|
1906
|
-
(0, vitest.expect)(ctx.lastListResult.every((e) => e.status === `
|
|
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(`
|
|
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
|
|
1931
|
-
(0, vitest.expect)(
|
|
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
|
|
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, [`
|
|
1968
|
-
(0, vitest.expect)(entity.status).toBe(`
|
|
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
|
-
|
|
2007
|
+
inbox_schemas: { query: {
|
|
1980
2008
|
type: `object`,
|
|
1981
2009
|
properties: { text: { type: `string` } },
|
|
1982
2010
|
required: [`text`]
|
|
1983
2011
|
} },
|
|
1984
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
2242
|
+
description: `Type with inbox schemas`,
|
|
2215
2243
|
creation_schema: { type: `object` },
|
|
2216
|
-
|
|
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
|
|
2255
|
+
description: `Type with strict inbox schemas`,
|
|
2228
2256
|
creation_schema: { type: `object` },
|
|
2229
|
-
|
|
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
|
|
2268
|
+
description: `Type with defined inbox schemas`,
|
|
2241
2269
|
creation_schema: { type: `object` },
|
|
2242
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2289
|
+
description: `Type with empty inbox schemas`,
|
|
2262
2290
|
creation_schema: { type: `object` },
|
|
2263
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2313
|
+
description: `Type with strict state schemas`,
|
|
2286
2314
|
creation_schema: { type: `object` },
|
|
2287
|
-
|
|
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
|
|
2326
|
+
description: `Type with defined state schemas`,
|
|
2299
2327
|
creation_schema: { type: `object` },
|
|
2300
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2458
|
+
inbox_schemas: { query: {
|
|
2431
2459
|
type: `object`,
|
|
2432
2460
|
properties: { text: { type: `string` } },
|
|
2433
2461
|
required: [`text`]
|
|
2434
2462
|
} }
|
|
2435
|
-
}).amendSchemas(typeName, {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 (
|
|
2603
|
-
const typeName =
|
|
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
|
|
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`, {
|
|
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
|
-
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(`${
|
|
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(
|
|
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}`, [`
|
|
3456
|
-
(0, vitest.expect)(entity.status).toBe(`
|
|
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(
|
|
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}`, [`
|
|
3476
|
-
(0, vitest.expect)(entity.status).toBe(`
|
|
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(
|
|
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(
|
|
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(
|
|
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(`${
|
|
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(
|
|
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(`${
|
|
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(
|
|
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(`${
|
|
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);
|