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