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