agent-relay 2.3.4 → 2.3.6
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/README.md +1 -1
- package/dist/src/cli/index.js +124 -7
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +23 -26
- package/packages/acp-bridge/package.json +2 -2
- package/packages/bridge/package.json +7 -7
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +2 -2
- package/packages/daemon/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +5 -5
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/dist/index.d.ts +1 -29
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +1 -38
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/package.json +4 -25
- package/packages/sdk/src/index.ts +1 -69
- package/packages/sdk-py/README.md +56 -0
- package/packages/sdk-py/pyproject.toml +23 -0
- package/packages/sdk-py/src/agent_relay/__init__.py +27 -0
- package/packages/sdk-py/src/agent_relay/builder.py +367 -0
- package/packages/sdk-py/src/agent_relay/types.py +92 -0
- package/packages/sdk-py/tests/__init__.py +0 -0
- package/packages/sdk-py/tests/test_builder.py +101 -0
- package/packages/sdk-ts/dist/index.d.ts +1 -0
- package/packages/sdk-ts/dist/index.d.ts.map +1 -1
- package/packages/sdk-ts/dist/index.js +1 -0
- package/packages/sdk-ts/dist/index.js.map +1 -1
- package/packages/sdk-ts/dist/workflows/barrier.d.ts +72 -0
- package/packages/sdk-ts/dist/workflows/barrier.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/barrier.js +162 -0
- package/packages/sdk-ts/dist/workflows/barrier.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/builder.d.ts +101 -0
- package/packages/sdk-ts/dist/workflows/builder.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/builder.js +179 -0
- package/packages/sdk-ts/dist/workflows/builder.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/cli.d.ts +10 -0
- package/packages/sdk-ts/dist/workflows/cli.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/cli.js +82 -0
- package/packages/sdk-ts/dist/workflows/cli.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/coordinator.d.ts +68 -0
- package/packages/sdk-ts/dist/workflows/coordinator.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/coordinator.js +353 -0
- package/packages/sdk-ts/dist/workflows/coordinator.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/index.d.ts +10 -0
- package/packages/sdk-ts/dist/workflows/index.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/index.js +10 -0
- package/packages/sdk-ts/dist/workflows/index.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/memory-db.d.ts +17 -0
- package/packages/sdk-ts/dist/workflows/memory-db.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/memory-db.js +33 -0
- package/packages/sdk-ts/dist/workflows/memory-db.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/run.d.ts +31 -0
- package/packages/sdk-ts/dist/workflows/run.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/run.js +24 -0
- package/packages/sdk-ts/dist/workflows/run.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/runner.d.ts +119 -0
- package/packages/sdk-ts/dist/workflows/runner.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/runner.js +650 -0
- package/packages/sdk-ts/dist/workflows/runner.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/state.d.ts +77 -0
- package/packages/sdk-ts/dist/workflows/state.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/state.js +140 -0
- package/packages/sdk-ts/dist/workflows/state.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/templates.d.ts +47 -0
- package/packages/sdk-ts/dist/workflows/templates.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/templates.js +395 -0
- package/packages/sdk-ts/dist/workflows/templates.js.map +1 -0
- package/packages/sdk-ts/dist/workflows/types.d.ts +126 -0
- package/packages/sdk-ts/dist/workflows/types.d.ts.map +1 -0
- package/packages/sdk-ts/dist/workflows/types.js +8 -0
- package/packages/sdk-ts/dist/workflows/types.js.map +1 -0
- package/packages/sdk-ts/package.json +8 -2
- package/packages/sdk-ts/src/__tests__/error-scenarios.test.ts +682 -0
- package/packages/sdk-ts/src/__tests__/swarm-coordinator.test.ts +416 -0
- package/packages/sdk-ts/src/__tests__/workflow-runner.test.ts +333 -0
- package/packages/sdk-ts/src/index.ts +1 -0
- package/packages/sdk-ts/src/workflows/README.md +450 -0
- package/packages/sdk-ts/src/workflows/barrier.ts +254 -0
- package/packages/sdk-ts/src/workflows/builder.ts +241 -0
- package/packages/sdk-ts/src/workflows/builtin-templates/bug-fix.yaml +75 -0
- package/packages/sdk-ts/src/workflows/builtin-templates/code-review.yaml +82 -0
- package/packages/sdk-ts/src/workflows/builtin-templates/documentation.yaml +70 -0
- package/packages/sdk-ts/src/workflows/builtin-templates/feature-dev.yaml +76 -0
- package/packages/sdk-ts/src/workflows/builtin-templates/refactor.yaml +82 -0
- package/packages/sdk-ts/src/workflows/builtin-templates/security-audit.yaml +84 -0
- package/packages/sdk-ts/src/workflows/cli.ts +93 -0
- package/packages/sdk-ts/src/workflows/coordinator.ts +520 -0
- package/packages/sdk-ts/src/workflows/index.ts +9 -0
- package/packages/sdk-ts/src/workflows/memory-db.ts +39 -0
- package/packages/sdk-ts/src/workflows/run.ts +47 -0
- package/packages/sdk-ts/src/workflows/runner.ts +873 -0
- package/packages/sdk-ts/src/workflows/schema.json +321 -0
- package/packages/sdk-ts/src/workflows/state.ts +279 -0
- package/packages/sdk-ts/src/workflows/templates.ts +544 -0
- package/packages/sdk-ts/src/workflows/types.ts +178 -0
- package/packages/sdk-ts/tsconfig.json +6 -1
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +3 -3
- package/packages/wrapper/package.json +5 -6
- package/packages/api-types/.trajectories/active/traj_xbsvuzogscey.json +0 -15
- package/packages/api-types/.trajectories/index.json +0 -12
- package/packages/api-types/dist/index.d.ts +0 -21
- package/packages/api-types/dist/index.d.ts.map +0 -1
- package/packages/api-types/dist/index.js +0 -22
- package/packages/api-types/dist/index.js.map +0 -1
- package/packages/api-types/dist/schemas/agent.d.ts +0 -259
- package/packages/api-types/dist/schemas/agent.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/agent.js +0 -102
- package/packages/api-types/dist/schemas/agent.js.map +0 -1
- package/packages/api-types/dist/schemas/api.d.ts +0 -290
- package/packages/api-types/dist/schemas/api.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/api.js +0 -162
- package/packages/api-types/dist/schemas/api.js.map +0 -1
- package/packages/api-types/dist/schemas/decision.d.ts +0 -230
- package/packages/api-types/dist/schemas/decision.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/decision.js +0 -104
- package/packages/api-types/dist/schemas/decision.js.map +0 -1
- package/packages/api-types/dist/schemas/fleet.d.ts +0 -615
- package/packages/api-types/dist/schemas/fleet.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/fleet.js +0 -71
- package/packages/api-types/dist/schemas/fleet.js.map +0 -1
- package/packages/api-types/dist/schemas/history.d.ts +0 -180
- package/packages/api-types/dist/schemas/history.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/history.js +0 -72
- package/packages/api-types/dist/schemas/history.js.map +0 -1
- package/packages/api-types/dist/schemas/index.d.ts +0 -14
- package/packages/api-types/dist/schemas/index.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/index.js +0 -22
- package/packages/api-types/dist/schemas/index.js.map +0 -1
- package/packages/api-types/dist/schemas/message.d.ts +0 -456
- package/packages/api-types/dist/schemas/message.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/message.js +0 -88
- package/packages/api-types/dist/schemas/message.js.map +0 -1
- package/packages/api-types/dist/schemas/session.d.ts +0 -60
- package/packages/api-types/dist/schemas/session.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/session.js +0 -36
- package/packages/api-types/dist/schemas/session.js.map +0 -1
- package/packages/api-types/dist/schemas/task.d.ts +0 -111
- package/packages/api-types/dist/schemas/task.d.ts.map +0 -1
- package/packages/api-types/dist/schemas/task.js +0 -64
- package/packages/api-types/dist/schemas/task.js.map +0 -1
- package/packages/api-types/package.json +0 -61
- package/packages/api-types/scripts/generate-openapi.ts +0 -106
- package/packages/api-types/src/index.ts +0 -22
- package/packages/api-types/src/schemas/agent.test.ts +0 -164
- package/packages/api-types/src/schemas/agent.ts +0 -110
- package/packages/api-types/src/schemas/api.test.ts +0 -372
- package/packages/api-types/src/schemas/api.ts +0 -194
- package/packages/api-types/src/schemas/decision.test.ts +0 -324
- package/packages/api-types/src/schemas/decision.ts +0 -136
- package/packages/api-types/src/schemas/fleet.test.ts +0 -212
- package/packages/api-types/src/schemas/fleet.ts +0 -83
- package/packages/api-types/src/schemas/history.test.ts +0 -242
- package/packages/api-types/src/schemas/history.ts +0 -84
- package/packages/api-types/src/schemas/index.ts +0 -148
- package/packages/api-types/src/schemas/message.test.ts +0 -192
- package/packages/api-types/src/schemas/message.ts +0 -98
- package/packages/api-types/src/schemas/session.test.ts +0 -104
- package/packages/api-types/src/schemas/session.ts +0 -40
- package/packages/api-types/src/schemas/task.test.ts +0 -192
- package/packages/api-types/src/schemas/task.ts +0 -78
- package/packages/api-types/tsconfig.json +0 -19
- package/packages/api-types/vitest.config.ts +0 -9
- package/packages/benchmark/README.md +0 -200
- package/packages/benchmark/datasets/coding-tasks.yaml +0 -127
- package/packages/benchmark/datasets/coordination-tasks.yaml +0 -122
- package/packages/benchmark/datasets/quick-test.yaml +0 -20
- package/packages/benchmark/dist/benchmark.d.ts +0 -47
- package/packages/benchmark/dist/benchmark.d.ts.map +0 -1
- package/packages/benchmark/dist/benchmark.js +0 -224
- package/packages/benchmark/dist/benchmark.js.map +0 -1
- package/packages/benchmark/dist/cli.d.ts +0 -8
- package/packages/benchmark/dist/cli.d.ts.map +0 -1
- package/packages/benchmark/dist/cli.js +0 -185
- package/packages/benchmark/dist/cli.js.map +0 -1
- package/packages/benchmark/dist/harbor.d.ts +0 -53
- package/packages/benchmark/dist/harbor.d.ts.map +0 -1
- package/packages/benchmark/dist/harbor.js +0 -127
- package/packages/benchmark/dist/harbor.js.map +0 -1
- package/packages/benchmark/dist/index.d.ts +0 -48
- package/packages/benchmark/dist/index.d.ts.map +0 -1
- package/packages/benchmark/dist/index.js +0 -50
- package/packages/benchmark/dist/index.js.map +0 -1
- package/packages/benchmark/dist/runners/base.d.ts +0 -63
- package/packages/benchmark/dist/runners/base.d.ts.map +0 -1
- package/packages/benchmark/dist/runners/base.js +0 -156
- package/packages/benchmark/dist/runners/base.js.map +0 -1
- package/packages/benchmark/dist/runners/index.d.ts +0 -10
- package/packages/benchmark/dist/runners/index.d.ts.map +0 -1
- package/packages/benchmark/dist/runners/index.js +0 -10
- package/packages/benchmark/dist/runners/index.js.map +0 -1
- package/packages/benchmark/dist/runners/single.d.ts +0 -19
- package/packages/benchmark/dist/runners/single.d.ts.map +0 -1
- package/packages/benchmark/dist/runners/single.js +0 -111
- package/packages/benchmark/dist/runners/single.js.map +0 -1
- package/packages/benchmark/dist/runners/subagent.d.ts +0 -32
- package/packages/benchmark/dist/runners/subagent.d.ts.map +0 -1
- package/packages/benchmark/dist/runners/subagent.js +0 -212
- package/packages/benchmark/dist/runners/subagent.js.map +0 -1
- package/packages/benchmark/dist/runners/swarm.d.ts +0 -36
- package/packages/benchmark/dist/runners/swarm.d.ts.map +0 -1
- package/packages/benchmark/dist/runners/swarm.js +0 -273
- package/packages/benchmark/dist/runners/swarm.js.map +0 -1
- package/packages/benchmark/dist/types.d.ts +0 -178
- package/packages/benchmark/dist/types.d.ts.map +0 -1
- package/packages/benchmark/dist/types.js +0 -16
- package/packages/benchmark/dist/types.js.map +0 -1
- package/packages/benchmark/package.json +0 -80
- package/packages/benchmark/src/benchmark.ts +0 -298
- package/packages/benchmark/src/cli.ts +0 -240
- package/packages/benchmark/src/harbor.ts +0 -170
- package/packages/benchmark/src/index.ts +0 -73
- package/packages/benchmark/src/runners/base.ts +0 -205
- package/packages/benchmark/src/runners/index.ts +0 -10
- package/packages/benchmark/src/runners/single.ts +0 -121
- package/packages/benchmark/src/runners/subagent.ts +0 -240
- package/packages/benchmark/src/runners/swarm.ts +0 -326
- package/packages/benchmark/src/types.ts +0 -205
- package/packages/benchmark/tsconfig.json +0 -20
- package/packages/cli-tester/README.md +0 -277
- package/packages/cli-tester/dist/index.d.ts +0 -21
- package/packages/cli-tester/dist/index.d.ts.map +0 -1
- package/packages/cli-tester/dist/index.js +0 -21
- package/packages/cli-tester/dist/index.js.map +0 -1
- package/packages/cli-tester/dist/utils/credential-check.d.ts +0 -56
- package/packages/cli-tester/dist/utils/credential-check.d.ts.map +0 -1
- package/packages/cli-tester/dist/utils/credential-check.js +0 -230
- package/packages/cli-tester/dist/utils/credential-check.js.map +0 -1
- package/packages/cli-tester/dist/utils/socket-client.d.ts +0 -76
- package/packages/cli-tester/dist/utils/socket-client.d.ts.map +0 -1
- package/packages/cli-tester/dist/utils/socket-client.js +0 -153
- package/packages/cli-tester/dist/utils/socket-client.js.map +0 -1
- package/packages/cli-tester/docker/Dockerfile +0 -61
- package/packages/cli-tester/docker/docker-compose.yml +0 -71
- package/packages/cli-tester/docker/entrypoint.sh +0 -58
- package/packages/cli-tester/package.json +0 -32
- package/packages/cli-tester/scripts/clear-auth.sh +0 -101
- package/packages/cli-tester/scripts/inject-message.sh +0 -42
- package/packages/cli-tester/scripts/start.sh +0 -71
- package/packages/cli-tester/scripts/test-cli.sh +0 -56
- package/packages/cli-tester/scripts/test-full-spawn.sh +0 -238
- package/packages/cli-tester/scripts/test-registration.sh +0 -182
- package/packages/cli-tester/scripts/test-setup-flow.sh +0 -202
- package/packages/cli-tester/scripts/test-spawn.sh +0 -140
- package/packages/cli-tester/scripts/test-with-daemon.sh +0 -247
- package/packages/cli-tester/scripts/verify-auth.sh +0 -112
- package/packages/cli-tester/src/index.ts +0 -40
- package/packages/cli-tester/src/utils/credential-check.ts +0 -284
- package/packages/cli-tester/src/utils/socket-client.ts +0 -211
- package/packages/cli-tester/tests/credential-check.test.ts +0 -56
- package/packages/cli-tester/tsconfig.json +0 -11
- package/packages/sdk/dist/browser-client.d.ts +0 -212
- package/packages/sdk/dist/browser-client.d.ts.map +0 -1
- package/packages/sdk/dist/browser-client.js +0 -750
- package/packages/sdk/dist/browser-client.js.map +0 -1
- package/packages/sdk/dist/browser-framing.d.ts +0 -46
- package/packages/sdk/dist/browser-framing.d.ts.map +0 -1
- package/packages/sdk/dist/browser-framing.js +0 -122
- package/packages/sdk/dist/browser-framing.js.map +0 -1
- package/packages/sdk/dist/standalone.d.ts +0 -89
- package/packages/sdk/dist/standalone.d.ts.map +0 -1
- package/packages/sdk/dist/standalone.js +0 -131
- package/packages/sdk/dist/standalone.js.map +0 -1
- package/packages/sdk/dist/transports/index.d.ts +0 -92
- package/packages/sdk/dist/transports/index.d.ts.map +0 -1
- package/packages/sdk/dist/transports/index.js +0 -129
- package/packages/sdk/dist/transports/index.js.map +0 -1
- package/packages/sdk/dist/transports/socket-transport.d.ts +0 -30
- package/packages/sdk/dist/transports/socket-transport.d.ts.map +0 -1
- package/packages/sdk/dist/transports/socket-transport.js +0 -94
- package/packages/sdk/dist/transports/socket-transport.js.map +0 -1
- package/packages/sdk/dist/transports/types.d.ts +0 -69
- package/packages/sdk/dist/transports/types.d.ts.map +0 -1
- package/packages/sdk/dist/transports/types.js +0 -10
- package/packages/sdk/dist/transports/types.js.map +0 -1
- package/packages/sdk/dist/transports/websocket-transport.d.ts +0 -55
- package/packages/sdk/dist/transports/websocket-transport.d.ts.map +0 -1
- package/packages/sdk/dist/transports/websocket-transport.js +0 -180
- package/packages/sdk/dist/transports/websocket-transport.js.map +0 -1
- package/packages/sdk/src/browser-client.ts +0 -985
- package/packages/sdk/src/browser-framing.test.ts +0 -115
- package/packages/sdk/src/browser-framing.ts +0 -150
- package/packages/sdk/src/standalone.ts +0 -183
- package/packages/sdk/src/transports/index.ts +0 -197
- package/packages/sdk/src/transports/socket-transport.ts +0 -115
- package/packages/sdk/src/transports/types.ts +0 -77
- package/packages/sdk/src/transports/websocket-transport.ts +0 -245
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error scenario tests across all swarm workflow services.
|
|
3
|
+
*
|
|
4
|
+
* Tests failure modes, edge cases, and error propagation in
|
|
5
|
+
* StateStore, BarrierManager, SwarmCoordinator, and WorkflowRunner.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { StateStore } from '../workflows/state.js';
|
|
10
|
+
import { BarrierManager } from '../workflows/barrier.js';
|
|
11
|
+
import { SwarmCoordinator } from '../workflows/coordinator.js';
|
|
12
|
+
import type { DbClient } from '../workflows/coordinator.js';
|
|
13
|
+
import type { BarrierRow } from '../workflows/barrier.js';
|
|
14
|
+
import type { StateEntry } from '../workflows/state.js';
|
|
15
|
+
|
|
16
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function makeDb(): DbClient {
|
|
19
|
+
return {
|
|
20
|
+
query: vi.fn().mockResolvedValue({ rows: [] }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── StateStore error scenarios ───────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe('StateStore error scenarios', () => {
|
|
27
|
+
let db: DbClient;
|
|
28
|
+
let store: StateStore;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
db = makeDb();
|
|
32
|
+
store = new StateStore(db);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('consensus gating', () => {
|
|
36
|
+
it('should reject writes when consensus gate returns false', async () => {
|
|
37
|
+
store.setConsensusGate(async () => false);
|
|
38
|
+
|
|
39
|
+
await expect(
|
|
40
|
+
store.set('run_1', 'key', 'value', 'agent-1'),
|
|
41
|
+
).rejects.toThrow('rejected by consensus gate');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should emit state:gated event on rejection', async () => {
|
|
45
|
+
const spy = vi.fn();
|
|
46
|
+
store.on('state:gated', spy);
|
|
47
|
+
store.setConsensusGate(async () => false);
|
|
48
|
+
|
|
49
|
+
await store.set('run_1', 'key', 'value', 'agent-1').catch(() => {});
|
|
50
|
+
|
|
51
|
+
expect(spy).toHaveBeenCalledWith('run_1', 'key', 'agent-1');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should allow writes when consensus gate returns true', async () => {
|
|
55
|
+
const entry: StateEntry = {
|
|
56
|
+
id: 'st_1',
|
|
57
|
+
runId: 'run_1',
|
|
58
|
+
namespace: 'default',
|
|
59
|
+
key: 'key',
|
|
60
|
+
value: 'value',
|
|
61
|
+
expiresAt: null,
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
updatedAt: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [entry] });
|
|
66
|
+
store.setConsensusGate(async () => true);
|
|
67
|
+
|
|
68
|
+
const result = await store.set('run_1', 'key', 'value', 'agent-1');
|
|
69
|
+
expect(result).toEqual(entry);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should clear consensus gate', async () => {
|
|
73
|
+
store.setConsensusGate(async () => false);
|
|
74
|
+
store.clearConsensusGate();
|
|
75
|
+
|
|
76
|
+
const entry: StateEntry = {
|
|
77
|
+
id: 'st_1',
|
|
78
|
+
runId: 'run_1',
|
|
79
|
+
namespace: 'default',
|
|
80
|
+
key: 'key',
|
|
81
|
+
value: 'value',
|
|
82
|
+
expiresAt: null,
|
|
83
|
+
createdAt: new Date().toISOString(),
|
|
84
|
+
updatedAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [entry] });
|
|
87
|
+
|
|
88
|
+
await expect(store.set('run_1', 'key', 'value', 'agent-1')).resolves.toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('DB failures', () => {
|
|
93
|
+
it('should propagate DB errors on set', async () => {
|
|
94
|
+
vi.mocked(db.query).mockRejectedValueOnce(new Error('connection lost'));
|
|
95
|
+
await expect(store.set('run_1', 'key', 'v', 'agent')).rejects.toThrow('connection lost');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should propagate DB errors on get', async () => {
|
|
99
|
+
vi.mocked(db.query).mockRejectedValueOnce(new Error('timeout'));
|
|
100
|
+
await expect(store.get('run_1', 'key')).rejects.toThrow('timeout');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should propagate DB errors on delete', async () => {
|
|
104
|
+
vi.mocked(db.query).mockRejectedValueOnce(new Error('disk full'));
|
|
105
|
+
await expect(store.delete('run_1', 'key')).rejects.toThrow('disk full');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('namespace isolation', () => {
|
|
110
|
+
it('should use custom namespace when provided', async () => {
|
|
111
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
112
|
+
await store.get('run_1', 'key', { namespace: 'custom' });
|
|
113
|
+
expect(db.query).toHaveBeenCalledWith(
|
|
114
|
+
expect.any(String),
|
|
115
|
+
['run_1', 'custom', 'key'],
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should use default namespace when not provided', async () => {
|
|
120
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
121
|
+
await store.get('run_1', 'key');
|
|
122
|
+
expect(db.query).toHaveBeenCalledWith(
|
|
123
|
+
expect.any(String),
|
|
124
|
+
['run_1', 'default', 'key'],
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('TTL', () => {
|
|
130
|
+
it('should set expiresAt when ttlMs provided', async () => {
|
|
131
|
+
const entry: StateEntry = {
|
|
132
|
+
id: 'st_1',
|
|
133
|
+
runId: 'run_1',
|
|
134
|
+
namespace: 'default',
|
|
135
|
+
key: 'key',
|
|
136
|
+
value: 'v',
|
|
137
|
+
expiresAt: new Date(Date.now() + 5000).toISOString(),
|
|
138
|
+
createdAt: new Date().toISOString(),
|
|
139
|
+
updatedAt: new Date().toISOString(),
|
|
140
|
+
};
|
|
141
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [entry] });
|
|
142
|
+
|
|
143
|
+
const result = await store.set('run_1', 'key', 'v', 'agent', { ttlMs: 5000 });
|
|
144
|
+
expect(result.expiresAt).not.toBeNull();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('event emission', () => {
|
|
149
|
+
it('should emit state:set on successful write', async () => {
|
|
150
|
+
const entry: StateEntry = {
|
|
151
|
+
id: 'st_1',
|
|
152
|
+
runId: 'run_1',
|
|
153
|
+
namespace: 'default',
|
|
154
|
+
key: 'key',
|
|
155
|
+
value: 'v',
|
|
156
|
+
expiresAt: null,
|
|
157
|
+
createdAt: new Date().toISOString(),
|
|
158
|
+
updatedAt: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [entry] });
|
|
161
|
+
|
|
162
|
+
const spy = vi.fn();
|
|
163
|
+
store.on('state:set', spy);
|
|
164
|
+
|
|
165
|
+
await store.set('run_1', 'key', 'v', 'agent');
|
|
166
|
+
expect(spy).toHaveBeenCalledWith(entry);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should emit state:deleted on successful delete', async () => {
|
|
170
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [{ id: 'st_1' }] });
|
|
171
|
+
|
|
172
|
+
const spy = vi.fn();
|
|
173
|
+
store.on('state:deleted', spy);
|
|
174
|
+
|
|
175
|
+
await store.delete('run_1', 'key');
|
|
176
|
+
expect(spy).toHaveBeenCalledWith('run_1', 'key', 'default');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should not emit state:deleted when key not found', async () => {
|
|
180
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
181
|
+
|
|
182
|
+
const spy = vi.fn();
|
|
183
|
+
store.on('state:deleted', spy);
|
|
184
|
+
|
|
185
|
+
await store.delete('run_1', 'key');
|
|
186
|
+
expect(spy).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('snapshot', () => {
|
|
191
|
+
it('should return empty object for no entries', async () => {
|
|
192
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
193
|
+
const snapshot = await store.snapshot('run_1');
|
|
194
|
+
expect(snapshot).toEqual({});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should build key-value map from entries', async () => {
|
|
198
|
+
const entries: StateEntry[] = [
|
|
199
|
+
{ id: '1', runId: 'run_1', namespace: 'default', key: 'a', value: 1, expiresAt: null, createdAt: '', updatedAt: '' },
|
|
200
|
+
{ id: '2', runId: 'run_1', namespace: 'default', key: 'b', value: 'hello', expiresAt: null, createdAt: '', updatedAt: '' },
|
|
201
|
+
];
|
|
202
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: entries });
|
|
203
|
+
|
|
204
|
+
const snapshot = await store.snapshot('run_1');
|
|
205
|
+
expect(snapshot).toEqual({ a: 1, b: 'hello' });
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── BarrierManager error scenarios ───────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
describe('BarrierManager error scenarios', () => {
|
|
213
|
+
let db: DbClient;
|
|
214
|
+
let manager: BarrierManager;
|
|
215
|
+
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
db = makeDb();
|
|
218
|
+
manager = new BarrierManager(db);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
afterEach(() => {
|
|
222
|
+
manager.cleanup();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('barrier creation', () => {
|
|
226
|
+
it('should create barrier and emit barrier:created', async () => {
|
|
227
|
+
const barrier: BarrierRow = {
|
|
228
|
+
id: 'bar_1',
|
|
229
|
+
runId: 'run_1',
|
|
230
|
+
barrierName: 'test-barrier',
|
|
231
|
+
waitFor: ['agent-a', 'agent-b'],
|
|
232
|
+
resolved: [],
|
|
233
|
+
isSatisfied: false,
|
|
234
|
+
timeoutMs: null,
|
|
235
|
+
createdAt: new Date().toISOString(),
|
|
236
|
+
updatedAt: new Date().toISOString(),
|
|
237
|
+
};
|
|
238
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [barrier] });
|
|
239
|
+
|
|
240
|
+
const spy = vi.fn();
|
|
241
|
+
manager.on('barrier:created', spy);
|
|
242
|
+
|
|
243
|
+
const result = await manager.createBarrier('run_1', {
|
|
244
|
+
name: 'test-barrier',
|
|
245
|
+
waitFor: ['agent-a', 'agent-b'],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(result.barrierName).toBe('test-barrier');
|
|
249
|
+
expect(spy).toHaveBeenCalledWith(barrier);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should create multiple barriers in batch', async () => {
|
|
253
|
+
const barrier: BarrierRow = {
|
|
254
|
+
id: 'bar_1',
|
|
255
|
+
runId: 'run_1',
|
|
256
|
+
barrierName: 'b1',
|
|
257
|
+
waitFor: ['a'],
|
|
258
|
+
resolved: [],
|
|
259
|
+
isSatisfied: false,
|
|
260
|
+
timeoutMs: null,
|
|
261
|
+
createdAt: new Date().toISOString(),
|
|
262
|
+
updatedAt: new Date().toISOString(),
|
|
263
|
+
};
|
|
264
|
+
vi.mocked(db.query).mockResolvedValue({ rows: [barrier] });
|
|
265
|
+
|
|
266
|
+
const results = await manager.createBarriers('run_1', [
|
|
267
|
+
{ name: 'b1', waitFor: ['a'] },
|
|
268
|
+
{ name: 'b2', waitFor: ['b'] },
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
expect(results).toHaveLength(2);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('barrier resolution', () => {
|
|
276
|
+
it('should resolve barrier and check satisfaction (all mode)', async () => {
|
|
277
|
+
const barrier: BarrierRow = {
|
|
278
|
+
id: 'bar_1',
|
|
279
|
+
runId: 'run_1',
|
|
280
|
+
barrierName: 'b1',
|
|
281
|
+
waitFor: ['agent-a', 'agent-b'],
|
|
282
|
+
resolved: ['agent-a'],
|
|
283
|
+
isSatisfied: false,
|
|
284
|
+
timeoutMs: null,
|
|
285
|
+
createdAt: new Date().toISOString(),
|
|
286
|
+
updatedAt: new Date().toISOString(),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// First, create the barrier to set the mode
|
|
290
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [barrier] });
|
|
291
|
+
await manager.createBarrier('run_1', {
|
|
292
|
+
name: 'b1',
|
|
293
|
+
waitFor: ['agent-a', 'agent-b'],
|
|
294
|
+
mode: 'all',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Now resolve with partial (not satisfied yet)
|
|
298
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [barrier] });
|
|
299
|
+
const result = await manager.resolve('run_1', 'b1', 'agent-a');
|
|
300
|
+
expect(result.satisfied).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should satisfy barrier in any mode with single resolution', async () => {
|
|
304
|
+
const barrier: BarrierRow = {
|
|
305
|
+
id: 'bar_1',
|
|
306
|
+
runId: 'run_1',
|
|
307
|
+
barrierName: 'b1',
|
|
308
|
+
waitFor: ['agent-a', 'agent-b'],
|
|
309
|
+
resolved: ['agent-a'],
|
|
310
|
+
isSatisfied: false,
|
|
311
|
+
timeoutMs: null,
|
|
312
|
+
createdAt: new Date().toISOString(),
|
|
313
|
+
updatedAt: new Date().toISOString(),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Create barrier in "any" mode
|
|
317
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [barrier] });
|
|
318
|
+
await manager.createBarrier('run_1', {
|
|
319
|
+
name: 'b1',
|
|
320
|
+
waitFor: ['agent-a', 'agent-b'],
|
|
321
|
+
mode: 'any',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Resolve — should satisfy immediately since mode is "any"
|
|
325
|
+
vi.mocked(db.query)
|
|
326
|
+
.mockResolvedValueOnce({ rows: [barrier] }) // resolve UPDATE
|
|
327
|
+
.mockResolvedValueOnce({ rows: [{ ...barrier, isSatisfied: true }] }); // markSatisfied UPDATE
|
|
328
|
+
|
|
329
|
+
const satisfiedSpy = vi.fn();
|
|
330
|
+
manager.on('barrier:satisfied', satisfiedSpy);
|
|
331
|
+
|
|
332
|
+
const result = await manager.resolve('run_1', 'b1', 'agent-a');
|
|
333
|
+
expect(result.satisfied).toBe(true);
|
|
334
|
+
expect(satisfiedSpy).toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should throw when barrier not found during resolve', async () => {
|
|
338
|
+
// resolve UPDATE returns empty
|
|
339
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
340
|
+
// getBarrier also returns empty
|
|
341
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
342
|
+
|
|
343
|
+
await expect(
|
|
344
|
+
manager.resolve('run_1', 'nonexistent', 'agent-a'),
|
|
345
|
+
).rejects.toThrow('not found');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should return existing state when barrier already satisfied', async () => {
|
|
349
|
+
const barrier: BarrierRow = {
|
|
350
|
+
id: 'bar_1',
|
|
351
|
+
runId: 'run_1',
|
|
352
|
+
barrierName: 'b1',
|
|
353
|
+
waitFor: ['a'],
|
|
354
|
+
resolved: ['a'],
|
|
355
|
+
isSatisfied: true,
|
|
356
|
+
timeoutMs: null,
|
|
357
|
+
createdAt: '',
|
|
358
|
+
updatedAt: '',
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// resolve UPDATE returns empty (already satisfied, WHERE is_satisfied=FALSE doesn't match)
|
|
362
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
363
|
+
// getBarrier returns the already-satisfied barrier
|
|
364
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [barrier] });
|
|
365
|
+
|
|
366
|
+
const result = await manager.resolve('run_1', 'b1', 'a');
|
|
367
|
+
expect(result.satisfied).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('barrier timeout', () => {
|
|
372
|
+
it('should schedule timeout and emit barrier:timeout', async () => {
|
|
373
|
+
vi.useFakeTimers();
|
|
374
|
+
|
|
375
|
+
const barrier: BarrierRow = {
|
|
376
|
+
id: 'bar_1',
|
|
377
|
+
runId: 'run_1',
|
|
378
|
+
barrierName: 'b1',
|
|
379
|
+
waitFor: ['a'],
|
|
380
|
+
resolved: [],
|
|
381
|
+
isSatisfied: false,
|
|
382
|
+
timeoutMs: 1000,
|
|
383
|
+
createdAt: '',
|
|
384
|
+
updatedAt: '',
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
vi.mocked(db.query).mockResolvedValue({ rows: [barrier] });
|
|
388
|
+
|
|
389
|
+
const timeoutSpy = vi.fn();
|
|
390
|
+
manager.on('barrier:timeout', timeoutSpy);
|
|
391
|
+
|
|
392
|
+
await manager.createBarrier('run_1', {
|
|
393
|
+
name: 'b1',
|
|
394
|
+
waitFor: ['a'],
|
|
395
|
+
timeoutMs: 1000,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
await vi.advanceTimersByTimeAsync(1100);
|
|
399
|
+
|
|
400
|
+
expect(timeoutSpy).toHaveBeenCalledWith(barrier);
|
|
401
|
+
|
|
402
|
+
vi.useRealTimers();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('cleanup', () => {
|
|
407
|
+
it('should clear all timeout timers', async () => {
|
|
408
|
+
const barrier: BarrierRow = {
|
|
409
|
+
id: 'bar_1',
|
|
410
|
+
runId: 'run_1',
|
|
411
|
+
barrierName: 'b1',
|
|
412
|
+
waitFor: ['a'],
|
|
413
|
+
resolved: [],
|
|
414
|
+
isSatisfied: false,
|
|
415
|
+
timeoutMs: 60000,
|
|
416
|
+
createdAt: '',
|
|
417
|
+
updatedAt: '',
|
|
418
|
+
};
|
|
419
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [barrier] });
|
|
420
|
+
|
|
421
|
+
await manager.createBarrier('run_1', {
|
|
422
|
+
name: 'b1',
|
|
423
|
+
waitFor: ['a'],
|
|
424
|
+
timeoutMs: 60000,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(() => manager.cleanup()).not.toThrow();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('queries', () => {
|
|
432
|
+
it('getBarrier should return null for missing barrier', async () => {
|
|
433
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
434
|
+
const result = await manager.getBarrier('run_1', 'nonexistent');
|
|
435
|
+
expect(result).toBeNull();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('isSatisfied should return false when barrier does not exist', async () => {
|
|
439
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
440
|
+
const result = await manager.isSatisfied('run_1', 'missing');
|
|
441
|
+
expect(result).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('getUnsatisfiedBarriers should query with is_satisfied = FALSE', async () => {
|
|
445
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
446
|
+
await manager.getUnsatisfiedBarriers('run_1');
|
|
447
|
+
expect(db.query).toHaveBeenCalledWith(
|
|
448
|
+
expect.stringContaining('is_satisfied = FALSE'),
|
|
449
|
+
['run_1'],
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// ── SwarmCoordinator error scenarios ─────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
describe('SwarmCoordinator error scenarios', () => {
|
|
458
|
+
let db: DbClient;
|
|
459
|
+
let coordinator: SwarmCoordinator;
|
|
460
|
+
|
|
461
|
+
beforeEach(() => {
|
|
462
|
+
db = makeDb();
|
|
463
|
+
coordinator = new SwarmCoordinator(db);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('run lifecycle errors', () => {
|
|
467
|
+
it('should throw when starting a non-pending run', async () => {
|
|
468
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
469
|
+
await expect(coordinator.startRun('run_1')).rejects.toThrow('not found or not in pending');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should throw when completing a non-existent run', async () => {
|
|
473
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
474
|
+
await expect(coordinator.completeRun('bad')).rejects.toThrow('not found');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should throw when failing a non-existent run', async () => {
|
|
478
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
479
|
+
await expect(coordinator.failRun('bad', 'error')).rejects.toThrow('not found');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should throw when cancelling a non-existent run', async () => {
|
|
483
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
484
|
+
await expect(coordinator.cancelRun('bad')).rejects.toThrow('not found');
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
describe('step lifecycle errors', () => {
|
|
489
|
+
it('should throw when starting a non-pending step', async () => {
|
|
490
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
491
|
+
await expect(coordinator.startStep('step_bad')).rejects.toThrow('not in pending state');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should throw when completing a non-running step', async () => {
|
|
495
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
496
|
+
await expect(coordinator.completeStep('step_bad')).rejects.toThrow('not in running state');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should throw when failing a non-running step', async () => {
|
|
500
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
501
|
+
await expect(coordinator.failStep('step_bad', 'err')).rejects.toThrow('not in running state');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should throw when skipping a non-existent step', async () => {
|
|
505
|
+
vi.mocked(db.query).mockResolvedValueOnce({ rows: [] });
|
|
506
|
+
await expect(coordinator.skipStep('step_bad')).rejects.toThrow('not found');
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('DB propagation', () => {
|
|
511
|
+
it('should propagate DB errors from createRun', async () => {
|
|
512
|
+
vi.mocked(db.query).mockRejectedValueOnce(new Error('connection refused'));
|
|
513
|
+
await expect(coordinator.createRun('ws-1', {
|
|
514
|
+
version: '1',
|
|
515
|
+
name: 'test',
|
|
516
|
+
swarm: { pattern: 'fan-out' },
|
|
517
|
+
agents: [{ name: 'a', cli: 'claude' }],
|
|
518
|
+
})).rejects.toThrow('connection refused');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should propagate DB errors from getSteps', async () => {
|
|
522
|
+
vi.mocked(db.query).mockRejectedValueOnce(new Error('query timeout'));
|
|
523
|
+
await expect(coordinator.getSteps('run_1')).rejects.toThrow('query timeout');
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// ── WorkflowRunner error scenarios ───────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
describe('WorkflowRunner error scenarios', () => {
|
|
531
|
+
// Mock AgentRelay for runner tests
|
|
532
|
+
const mockAgent = {
|
|
533
|
+
name: 'test-agent',
|
|
534
|
+
waitForExit: vi.fn().mockResolvedValue(0),
|
|
535
|
+
release: vi.fn(),
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
vi.mock('@agent-relay/sdk-ts/relay', () => ({
|
|
539
|
+
AgentRelay: vi.fn().mockImplementation(() => ({
|
|
540
|
+
spawnPty: vi.fn().mockResolvedValue(mockAgent),
|
|
541
|
+
human: vi.fn().mockReturnValue({ sendMessage: vi.fn() }),
|
|
542
|
+
shutdown: vi.fn(),
|
|
543
|
+
})),
|
|
544
|
+
}));
|
|
545
|
+
|
|
546
|
+
let WorkflowRunner: any;
|
|
547
|
+
let db: any;
|
|
548
|
+
let runner: any;
|
|
549
|
+
|
|
550
|
+
beforeEach(async () => {
|
|
551
|
+
const mod = await import('../workflows/runner.js');
|
|
552
|
+
WorkflowRunner = mod.WorkflowRunner;
|
|
553
|
+
|
|
554
|
+
const runs = new Map();
|
|
555
|
+
const steps = new Map();
|
|
556
|
+
|
|
557
|
+
db = {
|
|
558
|
+
insertRun: vi.fn(async (run: any) => runs.set(run.id, { ...run })),
|
|
559
|
+
updateRun: vi.fn(async (id: string, patch: any) => {
|
|
560
|
+
const existing = runs.get(id);
|
|
561
|
+
if (existing) runs.set(id, { ...existing, ...patch });
|
|
562
|
+
}),
|
|
563
|
+
getRun: vi.fn(async (id: string) => runs.get(id) ?? null),
|
|
564
|
+
insertStep: vi.fn(async (step: any) => steps.set(step.id, { ...step })),
|
|
565
|
+
updateStep: vi.fn(async (id: string, patch: any) => {
|
|
566
|
+
const existing = steps.get(id);
|
|
567
|
+
if (existing) steps.set(id, { ...existing, ...patch });
|
|
568
|
+
}),
|
|
569
|
+
getStepsByRunId: vi.fn(async (runId: string) => {
|
|
570
|
+
return [...steps.values()].filter((s: any) => s.runId === runId);
|
|
571
|
+
}),
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
runner = new WorkflowRunner({ db, workspaceId: 'ws-test' });
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('validation errors', () => {
|
|
578
|
+
it('should reject non-object config', () => {
|
|
579
|
+
expect(() => runner.validateConfig('string')).toThrow('non-null object');
|
|
580
|
+
expect(() => runner.validateConfig(42)).toThrow('non-null object');
|
|
581
|
+
expect(() => runner.validateConfig(undefined)).toThrow('non-null object');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should reject config without swarm', () => {
|
|
585
|
+
expect(() =>
|
|
586
|
+
runner.validateConfig({ version: '1', name: 'x', agents: [{ name: 'a', cli: 'claude' }] }),
|
|
587
|
+
).toThrow('missing required field "swarm"');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('should reject config with null swarm', () => {
|
|
591
|
+
expect(() =>
|
|
592
|
+
runner.validateConfig({
|
|
593
|
+
version: '1',
|
|
594
|
+
name: 'x',
|
|
595
|
+
swarm: null,
|
|
596
|
+
agents: [{ name: 'a', cli: 'claude' }],
|
|
597
|
+
}),
|
|
598
|
+
).toThrow('missing required field "swarm"');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('should reject workflows with non-object steps', () => {
|
|
602
|
+
expect(() =>
|
|
603
|
+
runner.validateConfig({
|
|
604
|
+
version: '1',
|
|
605
|
+
name: 'x',
|
|
606
|
+
swarm: { pattern: 'dag' },
|
|
607
|
+
agents: [{ name: 'a', cli: 'claude' }],
|
|
608
|
+
workflows: [{ name: 'wf', steps: ['not-an-object'] }],
|
|
609
|
+
}),
|
|
610
|
+
).toThrow('each step must be an object');
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('should reject step missing required fields', () => {
|
|
614
|
+
expect(() =>
|
|
615
|
+
runner.validateConfig({
|
|
616
|
+
version: '1',
|
|
617
|
+
name: 'x',
|
|
618
|
+
swarm: { pattern: 'dag' },
|
|
619
|
+
agents: [{ name: 'a', cli: 'claude' }],
|
|
620
|
+
workflows: [{ name: 'wf', steps: [{ name: 's1', agent: 'a' }] }],
|
|
621
|
+
}),
|
|
622
|
+
).toThrow('each step must have "name", "agent", and "task"');
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe('variable resolution errors', () => {
|
|
627
|
+
it('should throw on unresolved variable in agent task', () => {
|
|
628
|
+
const config = {
|
|
629
|
+
version: '1',
|
|
630
|
+
name: 'test',
|
|
631
|
+
swarm: { pattern: 'dag' as const },
|
|
632
|
+
agents: [{ name: 'a', cli: 'claude' as const, task: 'Fix {{bug}}' }],
|
|
633
|
+
};
|
|
634
|
+
expect(() => runner.resolveVariables(config, {})).toThrow('Unresolved variable: {{bug}}');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should throw on unresolved variable in workflow step task', () => {
|
|
638
|
+
const config = {
|
|
639
|
+
version: '1',
|
|
640
|
+
name: 'test',
|
|
641
|
+
swarm: { pattern: 'dag' as const },
|
|
642
|
+
agents: [{ name: 'a', cli: 'claude' as const }],
|
|
643
|
+
workflows: [{
|
|
644
|
+
name: 'wf',
|
|
645
|
+
steps: [{ name: 's1', agent: 'a', task: 'Deploy to {{env}}' }],
|
|
646
|
+
}],
|
|
647
|
+
};
|
|
648
|
+
expect(() => runner.resolveVariables(config, {})).toThrow('Unresolved variable: {{env}}');
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
describe('execution errors', () => {
|
|
653
|
+
it('should fail run when workflow not found by name', async () => {
|
|
654
|
+
const config = {
|
|
655
|
+
version: '1',
|
|
656
|
+
name: 'test',
|
|
657
|
+
swarm: { pattern: 'dag' as const },
|
|
658
|
+
agents: [{ name: 'a', cli: 'claude' as const }],
|
|
659
|
+
workflows: [{ name: 'wf1', steps: [{ name: 's1', agent: 'a', task: 'x' }] }],
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
await expect(runner.execute(config, 'nonexistent')).rejects.toThrow('not found');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should fail run when config has no workflows', async () => {
|
|
666
|
+
const config = {
|
|
667
|
+
version: '1',
|
|
668
|
+
name: 'test',
|
|
669
|
+
swarm: { pattern: 'dag' as const },
|
|
670
|
+
agents: [{ name: 'a', cli: 'claude' as const }],
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
await expect(runner.execute(config)).rejects.toThrow('No workflows defined');
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
describe('resume errors', () => {
|
|
678
|
+
it('should throw when resuming non-existent run', async () => {
|
|
679
|
+
await expect(runner.resume('bad_id')).rejects.toThrow('not found');
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
});
|