@electric-ax/agents-server-conformance-tests 0.1.2

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.
@@ -0,0 +1,540 @@
1
+ import { StreamFn } from "@mariozechner/pi-agent-core";
2
+
3
+ //#region src/electric-agents-tests.d.ts
4
+ /**
5
+ * Conformance tests for the Electric Agents entity runtime.
6
+ *
7
+ * Uses the electricAgents() DSL for declarative scenario-based testing.
8
+ * Each test first registers an entity type before spawning entities.
9
+ *
10
+ * Spec reference: docs/electric-agents-spec.md
11
+ */
12
+ /**
13
+ * Conformance tests for the Electric Agents entity runtime.
14
+ *
15
+ * Uses the electricAgents() DSL for declarative scenario-based testing.
16
+ * Each test first registers an entity type before spawning entities.
17
+ *
18
+ * Spec reference: docs/electric-agents-spec.md
19
+ */
20
+ interface ElectricAgentsTestOptions {
21
+ baseUrl: string;
22
+ }
23
+ declare function runElectricAgentsConformanceTests(config: ElectricAgentsTestOptions): void;
24
+ interface CliTestOptions {
25
+ baseUrl: string;
26
+ cliBin: string;
27
+ }
28
+ declare function runCliConformanceTests(config: CliTestOptions): void;
29
+ interface MockAgentTestOptions {
30
+ baseUrl: string;
31
+ }
32
+ declare function runMockAgentTests(config: MockAgentTestOptions): void;
33
+ interface MockAgentCliTestOptions {
34
+ baseUrl: string;
35
+ cliBin: string;
36
+ }
37
+ declare function runMockAgentCliTests(config: MockAgentCliTestOptions): void; //#endregion
38
+ //#region src/electric-agents-dsl.d.ts
39
+ /**
40
+ * Electric Agents testing DSL — fluent builder, history recorder, and invariant checkers.
41
+ *
42
+ * Usage:
43
+ * await electricAgents(baseUrl)
44
+ * .subscription('/agents/**', 'agent-handler')
45
+ * .spawn('/agents/task-1')
46
+ * .send({ text: 'hello' })
47
+ * .expectWebhook()
48
+ * .expectEntityContext()
49
+ * .respondDone()
50
+ * .expectStatus('running')
51
+ * .kill()
52
+ * .expectStatus('stopped')
53
+ * .run()
54
+ */
55
+ type HistoryEvent = {
56
+ type: `subscription_created`;
57
+ pattern: string;
58
+ subscriptionId: string;
59
+ webhookUrl: string;
60
+ } | {
61
+ type: `entity_spawned`;
62
+ entityUrl: string;
63
+ entityType?: string;
64
+ status: string;
65
+ streams: {
66
+ main: string;
67
+ error: string;
68
+ };
69
+ parent?: string;
70
+ } | {
71
+ type: `message_sent`;
72
+ entityUrl: string;
73
+ payload: unknown;
74
+ from?: string;
75
+ } | {
76
+ type: `webhook_received`;
77
+ consumer_id: string;
78
+ epoch: number;
79
+ wake_id: string;
80
+ entity?: WebhookEntityContext;
81
+ trigger_event?: string;
82
+ } | {
83
+ type: `webhook_responded`;
84
+ status: number;
85
+ body: unknown;
86
+ } | {
87
+ type: `entity_status_checked`;
88
+ entityUrl: string;
89
+ status: string;
90
+ } | {
91
+ type: `entity_killed`;
92
+ entityUrl: string;
93
+ } | {
94
+ type: `entity_list_fetched`;
95
+ count: number;
96
+ filter?: {
97
+ type?: string;
98
+ status?: string;
99
+ };
100
+ } | {
101
+ type: `stream_read`;
102
+ path: string;
103
+ messageCount: number;
104
+ } | {
105
+ type: `send_rejected`;
106
+ entityUrl: string;
107
+ status: number;
108
+ code: string;
109
+ } | {
110
+ type: `spawn_rejected`;
111
+ url: string;
112
+ status: number;
113
+ code: string;
114
+ } | {
115
+ type: `entity_type_registered`;
116
+ name: string;
117
+ revision: number;
118
+ } | {
119
+ type: `entity_type_inspected`;
120
+ name: string;
121
+ revision: number;
122
+ } | {
123
+ type: `entity_type_deleted`;
124
+ name: string;
125
+ } | {
126
+ type: `entity_type_schemas_amended`;
127
+ name: string;
128
+ revision: number;
129
+ } | {
130
+ type: `entity_write`;
131
+ entityUrl: string;
132
+ eventType?: string;
133
+ payload: unknown;
134
+ } | {
135
+ type: `tags_updated`;
136
+ entityUrl: string;
137
+ tags: Record<string, string>;
138
+ } | {
139
+ type: `tags_checked`;
140
+ entityUrl: string;
141
+ tags: Record<string, string>;
142
+ } | {
143
+ type: `spawn_schema_rejected`;
144
+ typeName: string;
145
+ instanceId: string;
146
+ status: number;
147
+ code: string;
148
+ } | {
149
+ type: `send_schema_rejected`;
150
+ entityUrl: string;
151
+ messageType: string;
152
+ status: number;
153
+ code: string;
154
+ } | {
155
+ type: `write_schema_rejected`;
156
+ entityUrl: string;
157
+ eventType: string;
158
+ status: number;
159
+ code: string;
160
+ } | {
161
+ type: `send_unknown_type_rejected`;
162
+ entityUrl: string;
163
+ messageType: string;
164
+ status: number;
165
+ code: string;
166
+ } | {
167
+ type: `write_unknown_type_rejected`;
168
+ entityUrl: string;
169
+ eventType: string;
170
+ status: number;
171
+ code: string;
172
+ } | {
173
+ type: `entity_persisted_verified`;
174
+ entityUrl: string;
175
+ } | {
176
+ type: `state_protocol_write`;
177
+ entityUrl: string;
178
+ eventType: string;
179
+ key: string;
180
+ };
181
+ interface WebhookEntityContext {
182
+ type?: string;
183
+ status: string;
184
+ url: string;
185
+ streams: {
186
+ main: string;
187
+ error: string;
188
+ };
189
+ tags?: Record<string, string>;
190
+ }
191
+ interface EntityTypeRegistration {
192
+ name: string;
193
+ description: string;
194
+ creation_schema?: Record<string, unknown>;
195
+ input_schemas?: Record<string, Record<string, unknown>>;
196
+ output_schemas?: Record<string, Record<string, unknown>>;
197
+ inbox_schemas?: Record<string, Record<string, unknown>>;
198
+ state_schemas?: Record<string, Record<string, unknown>>;
199
+ metadata_schema?: Record<string, unknown>;
200
+ serve_endpoint?: string;
201
+ }
202
+ interface ElectricAgentsStreamValue {
203
+ from?: string;
204
+ payload?: unknown;
205
+ [key: string]: unknown;
206
+ }
207
+ interface ElectricAgentsStreamEvent {
208
+ type: string;
209
+ key?: string;
210
+ value?: ElectricAgentsStreamValue;
211
+ headers?: Record<string, unknown>;
212
+ [key: string]: unknown;
213
+ }
214
+ interface WebhookNotification {
215
+ body: string;
216
+ parsed: {
217
+ consumer_id: string;
218
+ epoch: number;
219
+ wake_id: string;
220
+ primary_stream: string;
221
+ streams: Array<{
222
+ path: string;
223
+ offset: string;
224
+ }>;
225
+ triggered_by: Array<string>;
226
+ callback: string;
227
+ token: string;
228
+ entity?: WebhookEntityContext;
229
+ trigger_event?: string;
230
+ };
231
+ resolve: (response: {
232
+ status: number;
233
+ body: string;
234
+ }) => void;
235
+ }
236
+ declare class WebhookReceiver {
237
+ private server;
238
+ private _url;
239
+ private notifications;
240
+ private waitResolvers;
241
+ private consumedCount;
242
+ start(): Promise<string>;
243
+ stop(): Promise<void>;
244
+ get url(): string;
245
+ private handleRequest;
246
+ waitForNotification(timeoutMs?: number): Promise<WebhookNotification>;
247
+ expectNoNotification(timeoutMs?: number): Promise<void>;
248
+ get received(): Array<WebhookNotification>;
249
+ }
250
+ declare class ServeEndpointReceiver {
251
+ private server;
252
+ private _url;
253
+ private manifest;
254
+ start(manifest: EntityTypeRegistration): Promise<string>;
255
+ stop(): Promise<void>;
256
+ get url(): string;
257
+ private handleRequest;
258
+ }
259
+ interface ExpectWebhookOpts {
260
+ timeoutMs?: number;
261
+ }
262
+ interface EntityContextChecks {
263
+ url?: string;
264
+ type?: string;
265
+ status?: string;
266
+ }
267
+ interface RunContext {
268
+ baseUrl: string;
269
+ receiver: WebhookReceiver;
270
+ history: Array<HistoryEvent>;
271
+ subscriptions: Array<{
272
+ pattern: string;
273
+ id: string;
274
+ }>;
275
+ currentEntityUrl: string | null;
276
+ currentEntityStreams: {
277
+ main: string;
278
+ error: string;
279
+ } | null;
280
+ currentWriteToken: string | null;
281
+ notification: WebhookNotification | null;
282
+ lastListResult: Array<{
283
+ url: string;
284
+ type: string;
285
+ status: string;
286
+ [key: string]: unknown;
287
+ }> | null;
288
+ lastListTotal: number | null;
289
+ lastStreamMessages: Array<ElectricAgentsStreamEvent> | null;
290
+ currentEntityType: string | null;
291
+ lastTypeResult: Record<string, unknown> | null;
292
+ lastTypeListResult: Array<{
293
+ name: string;
294
+ description: string | null;
295
+ [key: string]: unknown;
296
+ }> | null;
297
+ serveReceiver: ServeEndpointReceiver | null;
298
+ }
299
+ declare class ElectricAgentsScenario {
300
+ private baseUrl;
301
+ private steps;
302
+ private _skipInvariants;
303
+ constructor(baseUrl: string);
304
+ subscription(pattern: string, id: string): this;
305
+ spawn(typeName: string, instanceId: string, opts?: {
306
+ args?: Record<string, unknown>;
307
+ tags?: Record<string, string>;
308
+ parent?: string;
309
+ initialMessage?: unknown;
310
+ }): this;
311
+ send(payload: unknown, opts: {
312
+ from: string;
313
+ type?: string;
314
+ }): this;
315
+ sendTo(url: string, payload: unknown, opts: {
316
+ from: string;
317
+ }): this;
318
+ kill(): this;
319
+ killUrl(url: string): this;
320
+ expectWebhook(opts?: ExpectWebhookOpts): this;
321
+ respondDone(): this;
322
+ expectEntityContext(checks?: EntityContextChecks): this;
323
+ expectStatus(status: string): this;
324
+ expectStreamContains(messageType: string): this;
325
+ readStream(stream?: `main` | `error`): this;
326
+ list(filter?: {
327
+ type?: string;
328
+ status?: string;
329
+ parent?: string;
330
+ limit?: number;
331
+ offset?: number;
332
+ }): this;
333
+ expectListCount(opts: {
334
+ min?: number;
335
+ max?: number;
336
+ exact?: number;
337
+ }): this;
338
+ expectListTotal(total: number): this;
339
+ registerType(registration: EntityTypeRegistration): this;
340
+ expectTypeExists(name: string): this;
341
+ inspectType(name: string): this;
342
+ deleteType(name: string): this;
343
+ expectTypeNotExists(name: string): this;
344
+ amendSchemas(name: string, schemas: {
345
+ input_schemas?: Record<string, Record<string, unknown>>;
346
+ output_schemas?: Record<string, Record<string, unknown>>;
347
+ }): this;
348
+ listTypes(): this;
349
+ registerTypeViaServe(registration: EntityTypeRegistration): this;
350
+ write(payload: unknown, opts?: {
351
+ type?: string;
352
+ }): this;
353
+ writeStateProtocol(event: {
354
+ type: string;
355
+ key: string;
356
+ value: Record<string, unknown>;
357
+ headers: {
358
+ operation: `insert` | `update`;
359
+ [k: string]: unknown;
360
+ };
361
+ }): this;
362
+ expectStreamEvent(type: string, key: string, operation: `insert` | `update`, valueCheck?: (value: Record<string, unknown>) => void): this;
363
+ expectStreamEventCount(type: string, count: number): this;
364
+ setTags(tags: Record<string, string>): this;
365
+ expectTags(expected: Record<string, string>): this;
366
+ expectSpawnSchemaError(typeName: string, instanceId: string, opts?: {
367
+ args?: Record<string, unknown>;
368
+ }): this;
369
+ expectSendSchemaError(payload: unknown, opts: {
370
+ from: string;
371
+ type?: string;
372
+ }): this;
373
+ expectWriteSchemaError(payload: unknown, opts?: {
374
+ type?: string;
375
+ }): this;
376
+ expectSendUnknownType(payload: unknown, opts: {
377
+ from: string;
378
+ type: string;
379
+ }): this;
380
+ expectWriteUnknownType(payload: unknown, opts: {
381
+ type: string;
382
+ }): this;
383
+ expectEntityPersisted(): this;
384
+ expectSpawnError(url: string, code: string, status: number): this;
385
+ expectSendError(code: string, status: number): this;
386
+ custom(fn: (ctx: RunContext) => Promise<void>): this;
387
+ wait(ms: number): this;
388
+ skipInvariants(): this;
389
+ run(): Promise<Array<HistoryEvent>>;
390
+ }
391
+ declare function checkInvariants(history: Array<HistoryEvent>): void;
392
+ /**
393
+ * Actions available in the Electric Agents entity lifecycle.
394
+ * Used by enabledElectricAgentsActions() as the ENABLED predicate.
395
+ */
396
+ type ElectricAgentsAction = `register_type` | `delete_type` | `spawn` | `send` | `kill` | `check_status` | `list`;
397
+ /**
398
+ * Model of a single entity type's state.
399
+ */
400
+ interface EntityTypeModel {
401
+ name: string;
402
+ hasCreationSchema: boolean;
403
+ hasInputSchemas: boolean;
404
+ hasOutputSchemas: boolean;
405
+ }
406
+ /**
407
+ * Model of a single entity's state — the "abstract" version
408
+ * that tracks what should be true, independent of the server.
409
+ */
410
+ interface EntityModel {
411
+ url: string;
412
+ typeName: string;
413
+ status: `running` | `stopped`;
414
+ messageCount: number;
415
+ }
416
+ /**
417
+ * World model tracking all entities and entity types in a scenario.
418
+ * This is the Init/Next state that the property test evolves.
419
+ */
420
+ interface ElectricAgentsWorldModel {
421
+ entityTypes: Array<EntityTypeModel>;
422
+ entities: Array<EntityModel>;
423
+ nextEntityNum: number;
424
+ }
425
+ /**
426
+ * ENABLED predicate — determines which actions can fire from the current state.
427
+ *
428
+ * - register_type: always (up to a cap of 3 types)
429
+ * - delete_type: when entity types exist and no running entities use them
430
+ * - spawn: when at least one entity type is registered (up to a cap)
431
+ * - send: when at least one entity is running
432
+ * - kill: when at least one entity is running
433
+ * - check_status: when at least one entity exists
434
+ * - list: always
435
+ */
436
+ declare function enabledElectricAgentsActions(model: ElectricAgentsWorldModel): Array<ElectricAgentsAction>;
437
+ /**
438
+ * Next relation — pure state transition for the model.
439
+ * The real server execution happens separately in the property test.
440
+ */
441
+ declare function applyElectricAgentsAction(model: ElectricAgentsWorldModel, action: ElectricAgentsAction, targetIdx?: number): ElectricAgentsWorldModel;
442
+ declare function electricAgents(baseUrl: string): ElectricAgentsScenario;
443
+ declare function checkStateProtocolInvariants(events: Array<Record<string, unknown>>): void; //#endregion
444
+ //#region src/cli-dsl.d.ts
445
+ /**
446
+ * CLI Testing DSL — fluent builder for testing the electric-agents CLI binary.
447
+ *
448
+ * Executes CLI commands as subprocesses against a running test server,
449
+ * asserting on stdout, stderr, and exit codes.
450
+ *
451
+ * Usage:
452
+ * await cliTest(baseUrl, cliBin)
453
+ * .exec('types')
454
+ * .expectStdout(/No entity types/)
455
+ * .exec('spawn', '/my-type/instance-1')
456
+ * .expectStdout(/Spawned/)
457
+ * .exec('ps')
458
+ * .expectStdout(/my-type/)
459
+ * .exec('kill', '/my-type/instance-1')
460
+ * .expectStdout(/Killed/)
461
+ * .run()
462
+ */
463
+ interface ExecResult {
464
+ stdout: string;
465
+ stderr: string;
466
+ exitCode: number;
467
+ }
468
+ interface CliHistory {
469
+ command: Array<string>;
470
+ stdout: string;
471
+ stderr: string;
472
+ exitCode: number;
473
+ }
474
+ declare class CliScenario {
475
+ private baseUrl;
476
+ private cliBin;
477
+ private steps;
478
+ constructor(baseUrl: string, cliBin: string);
479
+ /**
480
+ * Register an entity type via HTTP (setup, not a CLI test).
481
+ */
482
+ setupType(registration: Record<string, unknown>): this;
483
+ /**
484
+ * Create a webhook subscription via HTTP (setup, not a CLI test).
485
+ */
486
+ setupSubscription(pattern: string, id: string): this;
487
+ /**
488
+ * Verify internal server state via direct API call.
489
+ * Provides an extra level of guarantee beyond CLI output.
490
+ */
491
+ verifyApi(fn: (baseUrl: string) => void | Promise<void>): this;
492
+ /**
493
+ * Execute a CLI command. Args are passed directly to the binary.
494
+ */
495
+ exec(...args: Array<string>): this;
496
+ /**
497
+ * Assert stdout of the last exec matches a regex.
498
+ */
499
+ expectStdout(pattern: RegExp): this;
500
+ /**
501
+ * Assert stdout of the last exec does NOT match a regex.
502
+ */
503
+ expectStdoutNot(pattern: RegExp): this;
504
+ /**
505
+ * Assert stdout contains exact text.
506
+ */
507
+ expectStdoutContains(text: string): this;
508
+ /**
509
+ * Assert stderr of the last exec matches a regex.
510
+ */
511
+ expectStderr(pattern: RegExp): this;
512
+ /**
513
+ * Assert exit code of the last exec.
514
+ */
515
+ expectExitCode(code: number): this;
516
+ /**
517
+ * Parse stdout as JSON and run a check function.
518
+ */
519
+ expectJson(check: (data: unknown) => void): this;
520
+ /**
521
+ * Custom assertion on the last exec result.
522
+ */
523
+ custom(fn: (last: ExecResult) => void | Promise<void>): this;
524
+ /**
525
+ * Wait for a given number of milliseconds.
526
+ */
527
+ wait(ms: number): this;
528
+ /**
529
+ * Run all steps sequentially, returning the history.
530
+ */
531
+ run(): Promise<Array<CliHistory>>;
532
+ }
533
+ declare function cliTest(baseUrl: string, cliBin: string): CliScenario;
534
+
535
+ //#endregion
536
+ //#region src/mock-stream.d.ts
537
+ declare function createMockStreamFn(text: string): StreamFn;
538
+
539
+ //#endregion
540
+ export { CliHistory, CliScenario, CliTestOptions, ElectricAgentsAction, ElectricAgentsScenario, ElectricAgentsTestOptions, ElectricAgentsWorldModel, EntityModel, HistoryEvent, MockAgentCliTestOptions, MockAgentTestOptions, RunContext, ServeEndpointReceiver, applyElectricAgentsAction, checkInvariants, checkStateProtocolInvariants, cliTest, createMockStreamFn, electricAgents, enabledElectricAgentsActions, runCliConformanceTests, runElectricAgentsConformanceTests, runMockAgentCliTests, runMockAgentTests };