agent-relay 3.2.22 → 4.0.1
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 +5 -5
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +6564 -2100
- package/dist/src/cli/bootstrap.d.ts.map +1 -1
- package/dist/src/cli/bootstrap.js +2 -0
- package/dist/src/cli/bootstrap.js.map +1 -1
- package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
- package/dist/src/cli/commands/agent-management.js +14 -4
- package/dist/src/cli/commands/agent-management.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts +2 -6
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +31 -12
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/messaging.d.ts.map +1 -1
- package/dist/src/cli/commands/messaging.js +10 -3
- package/dist/src/cli/commands/messaging.js.map +1 -1
- package/dist/src/cli/commands/monitoring.d.ts +2 -2
- package/dist/src/cli/commands/monitoring.d.ts.map +1 -1
- package/dist/src/cli/commands/monitoring.js +15 -6
- package/dist/src/cli/commands/monitoring.js.map +1 -1
- package/dist/src/cli/commands/on/dotfiles.d.ts +35 -0
- package/dist/src/cli/commands/on/dotfiles.d.ts.map +1 -0
- package/dist/src/cli/commands/on/dotfiles.js +157 -0
- package/dist/src/cli/commands/on/dotfiles.js.map +1 -0
- package/dist/src/cli/commands/on/prereqs.d.ts +15 -0
- package/dist/src/cli/commands/on/prereqs.d.ts.map +1 -0
- package/dist/src/cli/commands/on/prereqs.js +103 -0
- package/dist/src/cli/commands/on/prereqs.js.map +1 -0
- package/dist/src/cli/commands/on/provision.d.ts +22 -0
- package/dist/src/cli/commands/on/provision.d.ts.map +1 -0
- package/dist/src/cli/commands/on/provision.js +157 -0
- package/dist/src/cli/commands/on/provision.js.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts +2 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.js +208 -0
- package/dist/src/cli/commands/on/relayfile-binary.js.map +1 -0
- package/dist/src/cli/commands/on/scan.d.ts +8 -0
- package/dist/src/cli/commands/on/scan.d.ts.map +1 -0
- package/dist/src/cli/commands/on/scan.js +59 -0
- package/dist/src/cli/commands/on/scan.js.map +1 -0
- package/dist/src/cli/commands/on/services.d.ts +17 -0
- package/dist/src/cli/commands/on/services.d.ts.map +1 -0
- package/dist/src/cli/commands/on/services.js +328 -0
- package/dist/src/cli/commands/on/services.js.map +1 -0
- package/dist/src/cli/commands/on/start.d.ts +61 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -0
- package/dist/src/cli/commands/on/start.js +1107 -0
- package/dist/src/cli/commands/on/start.js.map +1 -0
- package/dist/src/cli/commands/on/stop.d.ts +4 -0
- package/dist/src/cli/commands/on/stop.d.ts.map +1 -0
- package/dist/src/cli/commands/on/stop.js +11 -0
- package/dist/src/cli/commands/on/stop.js.map +1 -0
- package/dist/src/cli/commands/on/token.d.ts +8 -0
- package/dist/src/cli/commands/on/token.d.ts.map +1 -0
- package/dist/src/cli/commands/on/token.js +26 -0
- package/dist/src/cli/commands/on/token.js.map +1 -0
- package/dist/src/cli/commands/on/workspace.d.ts +4 -0
- package/dist/src/cli/commands/on/workspace.d.ts.map +1 -0
- package/dist/src/cli/commands/on/workspace.js +245 -0
- package/dist/src/cli/commands/on/workspace.js.map +1 -0
- package/dist/src/cli/commands/on.d.ts +10 -0
- package/dist/src/cli/commands/on.d.ts.map +1 -0
- package/dist/src/cli/commands/on.js +52 -0
- package/dist/src/cli/commands/on.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +10 -21
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/lib/bridge.js +1 -1
- package/dist/src/cli/lib/bridge.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts +14 -4
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +82 -120
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/client-factory.d.ts +4 -4
- package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/src/cli/lib/client-factory.js +14 -11
- package/dist/src/cli/lib/client-factory.js.map +1 -1
- package/dist/src/cli/lib/core-maintenance.d.ts.map +1 -1
- package/dist/src/cli/lib/core-maintenance.js +11 -22
- package/dist/src/cli/lib/core-maintenance.js.map +1 -1
- package/dist/src/cost/pricing.d.ts +18 -0
- package/dist/src/cost/pricing.d.ts.map +1 -0
- package/dist/src/cost/pricing.js +111 -0
- package/dist/src/cost/pricing.js.map +1 -0
- package/dist/src/cost/tracker.d.ts +13 -0
- package/dist/src/cost/tracker.d.ts.map +1 -0
- package/dist/src/cost/tracker.js +152 -0
- package/dist/src/cost/tracker.js.map +1 -0
- package/dist/src/cost/types.d.ts +23 -0
- package/dist/src/cost/types.d.ts.map +1 -0
- package/dist/src/cost/types.js +2 -0
- package/dist/src/cost/types.js.map +1 -0
- package/package.json +15 -12
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/package.json +1 -1
- package/packages/cloud/package.json +3 -3
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +10 -3
- package/packages/sdk/dist/broker-path.d.ts +3 -2
- package/packages/sdk/dist/broker-path.d.ts.map +1 -1
- package/packages/sdk/dist/broker-path.js +119 -32
- package/packages/sdk/dist/broker-path.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +119 -197
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +354 -823
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/examples/example.js +2 -5
- package/packages/sdk/dist/examples/example.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +3 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +3 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/relay-adapter.d.ts +9 -26
- package/packages/sdk/dist/relay-adapter.d.ts.map +1 -1
- package/packages/sdk/dist/relay-adapter.js +75 -47
- package/packages/sdk/dist/relay-adapter.js.map +1 -1
- package/packages/sdk/dist/relay.d.ts +26 -6
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +213 -43
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/transport.d.ts +58 -0
- package/packages/sdk/dist/transport.d.ts.map +1 -0
- package/packages/sdk/dist/transport.js +184 -0
- package/packages/sdk/dist/transport.js.map +1 -0
- package/packages/sdk/dist/types.d.ts +69 -0
- package/packages/sdk/dist/types.d.ts.map +1 -0
- package/packages/sdk/dist/types.js +5 -0
- package/packages/sdk/dist/types.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js +117 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +4 -3
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -1
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js +378 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js +145 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js +170 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +3 -2
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +1 -3
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/channel-messenger.d.ts +28 -0
- package/packages/sdk/dist/workflows/channel-messenger.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/channel-messenger.js +255 -0
- package/packages/sdk/dist/workflows/channel-messenger.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +7 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +7 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/process-spawner.d.ts +35 -0
- package/packages/sdk/dist/workflows/process-spawner.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/process-spawner.js +141 -0
- package/packages/sdk/dist/workflows/process-spawner.js.map +1 -0
- package/packages/sdk/dist/workflows/run.d.ts +2 -1
- package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/run.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +6 -6
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +443 -719
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/step-executor.d.ts +95 -0
- package/packages/sdk/dist/workflows/step-executor.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/step-executor.js +393 -0
- package/packages/sdk/dist/workflows/step-executor.js.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts +33 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.js +144 -0
- package/packages/sdk/dist/workflows/template-resolver.js.map +1 -0
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +17 -2
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/dist/workflows/verification.d.ts +33 -0
- package/packages/sdk/dist/workflows/verification.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/verification.js +122 -0
- package/packages/sdk/dist/workflows/verification.js.map +1 -0
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/unit.test.ts +100 -1
- package/packages/sdk/src/broker-path.ts +136 -30
- package/packages/sdk/src/client.ts +453 -1069
- package/packages/sdk/src/examples/example.ts +2 -5
- package/packages/sdk/src/index.ts +9 -1
- package/packages/sdk/src/relay-adapter.ts +75 -55
- package/packages/sdk/src/relay.ts +262 -55
- package/packages/sdk/src/transport.ts +216 -0
- package/packages/sdk/src/types.ts +75 -0
- package/packages/sdk/src/workflows/__tests__/channel-messenger.test.ts +137 -0
- package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +4 -3
- package/packages/sdk/src/workflows/__tests__/step-executor.test.ts +444 -0
- package/packages/sdk/src/workflows/__tests__/template-resolver.test.ts +162 -0
- package/packages/sdk/src/workflows/__tests__/verification.test.ts +229 -0
- package/packages/sdk/src/workflows/builder.ts +6 -6
- package/packages/sdk/src/workflows/channel-messenger.ts +314 -0
- package/packages/sdk/src/workflows/index.ts +12 -0
- package/packages/sdk/src/workflows/process-spawner.ts +201 -0
- package/packages/sdk/src/workflows/run.ts +2 -1
- package/packages/sdk/src/workflows/runner.ts +636 -951
- package/packages/sdk/src/workflows/step-executor.ts +579 -0
- package/packages/sdk/src/workflows/template-resolver.ts +180 -0
- package/packages/sdk/src/workflows/validator.ts +20 -2
- package/packages/sdk/src/workflows/verification.ts +184 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +0 -8
- package/packages/sdk-py/src/agent_relay/client.py +329 -522
- package/packages/sdk-py/src/agent_relay/protocol.py +2 -96
- package/packages/sdk-py/src/agent_relay/relay.py +1 -4
- package/packages/sdk-py/tests/test_wait_for_api_url.py +92 -0
- package/packages/sdk-py/uv.lock +5388 -0
- package/packages/telemetry/dist/client.d.ts.map +1 -1
- package/packages/telemetry/dist/client.js +1 -1
- package/packages/telemetry/dist/client.js.map +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/telemetry/src/client.ts +3 -10
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/scripts/postinstall.js +121 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared input/output types for the broker SDK.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentRuntime, HeadlessProvider, MessageInjectionMode, RestartPolicy } from './protocol.js';
|
|
6
|
+
|
|
7
|
+
export interface SpawnPtyInput {
|
|
8
|
+
name: string;
|
|
9
|
+
cli: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
channels?: string[];
|
|
12
|
+
task?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
team?: string;
|
|
16
|
+
shadowOf?: string;
|
|
17
|
+
shadowMode?: string;
|
|
18
|
+
idleThresholdSecs?: number;
|
|
19
|
+
restartPolicy?: RestartPolicy;
|
|
20
|
+
continueFrom?: string;
|
|
21
|
+
skipRelayPrompt?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SpawnHeadlessInput {
|
|
25
|
+
name: string;
|
|
26
|
+
provider: HeadlessProvider;
|
|
27
|
+
args?: string[];
|
|
28
|
+
channels?: string[];
|
|
29
|
+
task?: string;
|
|
30
|
+
skipRelayPrompt?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AgentTransport = 'pty' | 'headless';
|
|
34
|
+
|
|
35
|
+
export interface SpawnProviderInput {
|
|
36
|
+
name: string;
|
|
37
|
+
provider: string;
|
|
38
|
+
transport?: AgentTransport;
|
|
39
|
+
args?: string[];
|
|
40
|
+
channels?: string[];
|
|
41
|
+
task?: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
cwd?: string;
|
|
44
|
+
team?: string;
|
|
45
|
+
shadowOf?: string;
|
|
46
|
+
shadowMode?: string;
|
|
47
|
+
idleThresholdSecs?: number;
|
|
48
|
+
restartPolicy?: RestartPolicy;
|
|
49
|
+
continueFrom?: string;
|
|
50
|
+
skipRelayPrompt?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SendMessageInput {
|
|
54
|
+
to: string;
|
|
55
|
+
text: string;
|
|
56
|
+
from?: string;
|
|
57
|
+
threadId?: string;
|
|
58
|
+
workspaceId?: string;
|
|
59
|
+
workspaceAlias?: string;
|
|
60
|
+
priority?: number;
|
|
61
|
+
data?: Record<string, unknown>;
|
|
62
|
+
mode?: MessageInjectionMode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ListAgent {
|
|
66
|
+
name: string;
|
|
67
|
+
runtime: AgentRuntime;
|
|
68
|
+
provider?: HeadlessProvider;
|
|
69
|
+
cli?: string;
|
|
70
|
+
model?: string;
|
|
71
|
+
team?: string;
|
|
72
|
+
channels: string[];
|
|
73
|
+
parent?: string;
|
|
74
|
+
pid?: number;
|
|
75
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Import from the module that will be extracted from runner.ts
|
|
4
|
+
import {
|
|
5
|
+
ChannelMessenger,
|
|
6
|
+
formatError,
|
|
7
|
+
formatStepOutput,
|
|
8
|
+
sendToChannel,
|
|
9
|
+
truncateMessage,
|
|
10
|
+
} from '../channel-messenger.js';
|
|
11
|
+
|
|
12
|
+
describe('channel messenger helpers', () => {
|
|
13
|
+
it('sendToChannel forwards messages to the relay client', async () => {
|
|
14
|
+
const relay = { send: vi.fn().mockResolvedValue(undefined) };
|
|
15
|
+
await sendToChannel(relay, 'workflow-room', 'hello');
|
|
16
|
+
expect(relay.send).toHaveBeenCalledWith('workflow-room', 'hello');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('truncateMessage keeps the most recent tail within the limit', () => {
|
|
20
|
+
expect(truncateMessage('abcdefghij', 4)).toBe('ghij');
|
|
21
|
+
expect(truncateMessage('abc', 10)).toBe('abc');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('formatStepOutput returns a completion note when scrubbed output is empty', () => {
|
|
25
|
+
expect(formatStepOutput('plan', '▗▖\n')).toBe('**[plan]** Step completed — output written to disk');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('formatStepOutput scrubs noise and formats a fenced block', () => {
|
|
29
|
+
const output = 'Thinking…\nuseful line\n';
|
|
30
|
+
expect(formatStepOutput('plan', output)).toBe('**[plan] Output:**\n```\nuseful line\n```');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('formatError normalizes unknown errors', () => {
|
|
34
|
+
expect(formatError('build', new Error('Boom'))).toBe('**[build]** Failed: Boom');
|
|
35
|
+
expect(formatError('build', 'bad input')).toBe('**[build]** Failed: bad input');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('ChannelMessenger', () => {
|
|
40
|
+
describe('buildNonInteractiveAwareness', () => {
|
|
41
|
+
it('returns undefined when no non-interactive agents exist', () => {
|
|
42
|
+
const messenger = new ChannelMessenger();
|
|
43
|
+
const agents = new Map([['worker', { name: 'worker', cli: 'claude', interactive: true }]]);
|
|
44
|
+
const result = messenger.buildNonInteractiveAwareness(agents as any, new Map());
|
|
45
|
+
expect(result).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('lists non-interactive agents with step references', () => {
|
|
49
|
+
const messenger = new ChannelMessenger();
|
|
50
|
+
const agents = new Map([
|
|
51
|
+
['bg-worker', { name: 'bg-worker', cli: 'claude', interactive: false }],
|
|
52
|
+
]);
|
|
53
|
+
const stepStates = new Map([
|
|
54
|
+
['analyze', { row: { agentName: 'bg-worker', status: 'running' } }],
|
|
55
|
+
]);
|
|
56
|
+
const result = messenger.buildNonInteractiveAwareness(agents as any, stepStates as any);
|
|
57
|
+
expect(result).toContain('bg-worker');
|
|
58
|
+
expect(result).toContain('{{steps.analyze.output}}');
|
|
59
|
+
expect(result).toContain('cannot receive messages');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('buildDelegationGuidance', () => {
|
|
64
|
+
it('includes timeout note when timeout is provided', () => {
|
|
65
|
+
const messenger = new ChannelMessenger();
|
|
66
|
+
const result = messenger.buildDelegationGuidance('claude', 300_000);
|
|
67
|
+
expect(result).toContain('5 minutes');
|
|
68
|
+
expect(result).toContain('AUTONOMOUS DELEGATION');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('includes sub-agent option only for claude CLI', () => {
|
|
72
|
+
const messenger = new ChannelMessenger();
|
|
73
|
+
const claudeResult = messenger.buildDelegationGuidance('claude');
|
|
74
|
+
const codexResult = messenger.buildDelegationGuidance('codex');
|
|
75
|
+
expect(claudeResult).toContain('Task tool');
|
|
76
|
+
expect(codexResult).not.toContain('Task tool');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('omits timeout note when no timeout given', () => {
|
|
80
|
+
const messenger = new ChannelMessenger();
|
|
81
|
+
const result = messenger.buildDelegationGuidance('claude');
|
|
82
|
+
expect(result).not.toContain('minutes before this step');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('buildRelayRegistrationNote', () => {
|
|
87
|
+
it('returns empty string for claude CLI', () => {
|
|
88
|
+
const messenger = new ChannelMessenger();
|
|
89
|
+
expect(messenger.buildRelayRegistrationNote('claude', 'worker-1')).toBe('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns registration instructions for non-claude CLIs', () => {
|
|
93
|
+
const messenger = new ChannelMessenger();
|
|
94
|
+
const result = messenger.buildRelayRegistrationNote('codex', 'helper-1');
|
|
95
|
+
expect(result).toContain('register(name="helper-1")');
|
|
96
|
+
expect(result).toContain('RELAY SETUP');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('postCompletionReport', () => {
|
|
101
|
+
it('formats a completion report with step results', () => {
|
|
102
|
+
const postSpy = vi.fn();
|
|
103
|
+
const messenger = new ChannelMessenger({ postFn: postSpy });
|
|
104
|
+
const outcomes = [
|
|
105
|
+
{ name: 'plan', agent: 'lead', status: 'completed', attempts: 1, verificationPassed: true },
|
|
106
|
+
{ name: 'code', agent: 'worker', status: 'completed', attempts: 2 },
|
|
107
|
+
{ name: 'optional', agent: 'worker', status: 'skipped', attempts: 0 },
|
|
108
|
+
];
|
|
109
|
+
messenger.postCompletionReport('my-workflow', outcomes as any, 'All done', 0.95);
|
|
110
|
+
expect(postSpy).toHaveBeenCalledTimes(1);
|
|
111
|
+
const text = postSpy.mock.calls[0][0];
|
|
112
|
+
expect(text).toContain('my-workflow');
|
|
113
|
+
expect(text).toContain('Complete');
|
|
114
|
+
expect(text).toContain('95%');
|
|
115
|
+
expect(text).toContain('verified');
|
|
116
|
+
expect(text).toContain('2 attempts');
|
|
117
|
+
expect(text).toContain('skipped');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('postFailureReport', () => {
|
|
122
|
+
it('formats a failure report with error details', () => {
|
|
123
|
+
const postSpy = vi.fn();
|
|
124
|
+
const messenger = new ChannelMessenger({ postFn: postSpy });
|
|
125
|
+
const outcomes = [
|
|
126
|
+
{ name: 'plan', agent: 'lead', status: 'completed', attempts: 1 },
|
|
127
|
+
{ name: 'code', agent: 'worker', status: 'failed', attempts: 3, error: 'Timeout exceeded' },
|
|
128
|
+
];
|
|
129
|
+
messenger.postFailureReport('my-workflow', outcomes as any, 'Step failed');
|
|
130
|
+
expect(postSpy).toHaveBeenCalledTimes(1);
|
|
131
|
+
const text = postSpy.mock.calls[0][0];
|
|
132
|
+
expect(text).toContain('Failed');
|
|
133
|
+
expect(text).toContain('1/2 steps passed');
|
|
134
|
+
expect(text).toContain('Timeout exceeded');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -140,7 +140,7 @@ describe('formatRunSummaryTable', () => {
|
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
describe('WorkflowRunner logRunSummary', () => {
|
|
143
|
-
it('
|
|
143
|
+
it('uses the table summary format even when no reports exist', () => {
|
|
144
144
|
const runner = new WorkflowRunner({ cwd: '/tmp/workflow-runner' });
|
|
145
145
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
146
146
|
|
|
@@ -152,8 +152,9 @@ describe('WorkflowRunner logRunSummary', () => {
|
|
|
152
152
|
|
|
153
153
|
const combined = logSpy.mock.calls.flat().join('\n');
|
|
154
154
|
expect(combined).toContain('Workflow "sample-workflow"');
|
|
155
|
-
expect(combined).toContain('
|
|
156
|
-
expect(combined).
|
|
155
|
+
expect(combined).toContain('Step Status');
|
|
156
|
+
expect(combined).toContain('lint');
|
|
157
|
+
expect(combined).toContain('pass');
|
|
157
158
|
|
|
158
159
|
logSpy.mockRestore();
|
|
159
160
|
});
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { StepExecutor, type StepExecutorDeps, type StepResult } from '../step-executor.js';
|
|
4
|
+
import type { ProcessSpawner } from '../process-spawner.js';
|
|
5
|
+
import { createProcessSpawner } from '../process-spawner.js';
|
|
6
|
+
import type {
|
|
7
|
+
WorkflowStep,
|
|
8
|
+
AgentDefinition,
|
|
9
|
+
WorkflowStepStatus,
|
|
10
|
+
} from '../types.js';
|
|
11
|
+
|
|
12
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function makeStep(overrides: Partial<WorkflowStep> = {}): WorkflowStep {
|
|
15
|
+
return {
|
|
16
|
+
name: 'step-1',
|
|
17
|
+
type: 'deterministic',
|
|
18
|
+
command: 'echo hello',
|
|
19
|
+
...overrides,
|
|
20
|
+
} as WorkflowStep;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeAgent(overrides: Partial<AgentDefinition> = {}): AgentDefinition {
|
|
24
|
+
return {
|
|
25
|
+
name: 'worker-1',
|
|
26
|
+
cli: 'claude',
|
|
27
|
+
role: 'specialist',
|
|
28
|
+
...overrides,
|
|
29
|
+
} as AgentDefinition;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mockSpawner(overrides: Partial<ProcessSpawner> = {}): ProcessSpawner {
|
|
33
|
+
return {
|
|
34
|
+
spawnShell: vi.fn(async () => ({ output: 'hello\n', exitCode: 0 })),
|
|
35
|
+
spawnAgent: vi.fn(async () => ({ output: 'done', exitCode: 0 })),
|
|
36
|
+
spawnInteractive: vi.fn(async () => ({ output: 'completed', exitCode: 0 })),
|
|
37
|
+
buildCommand: vi.fn(() => ({ bin: 'claude', args: ['--task', 'x'] })),
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeDeps(overrides: Partial<StepExecutorDeps> = {}): StepExecutorDeps {
|
|
43
|
+
return {
|
|
44
|
+
cwd: '/tmp/test-project',
|
|
45
|
+
runId: 'run-001',
|
|
46
|
+
postToChannel: vi.fn(),
|
|
47
|
+
persistStepRow: vi.fn(),
|
|
48
|
+
persistStepOutput: vi.fn(),
|
|
49
|
+
resolveTemplate: vi.fn((s: string) => s),
|
|
50
|
+
getStepOutput: vi.fn(() => ''),
|
|
51
|
+
checkAborted: vi.fn(),
|
|
52
|
+
waitIfPaused: vi.fn(async () => {}),
|
|
53
|
+
log: vi.fn(),
|
|
54
|
+
processSpawner: mockSpawner(),
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createExecutor(overrides: Partial<StepExecutorDeps> = {}): StepExecutor {
|
|
60
|
+
return new StepExecutor(makeDeps(overrides));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 1. Deterministic step execution ──────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe('StepExecutor — deterministic steps', () => {
|
|
66
|
+
it('runs a shell command and captures stdout', async () => {
|
|
67
|
+
const executor = createExecutor();
|
|
68
|
+
const step = makeStep({ command: 'echo hello' });
|
|
69
|
+
const result = await executor.executeOne(step, new Map());
|
|
70
|
+
expect(result.status).toBe('completed');
|
|
71
|
+
expect(result.output).toContain('hello');
|
|
72
|
+
expect(result.exitCode).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('marks step failed on non-zero exit code', async () => {
|
|
76
|
+
const executor = createExecutor({
|
|
77
|
+
processSpawner: mockSpawner({
|
|
78
|
+
spawnShell: vi.fn(async () => ({ output: 'err', exitCode: 1 })),
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
const step = makeStep({ command: 'false' });
|
|
82
|
+
const result = await executor.executeOne(step, new Map());
|
|
83
|
+
expect(result.status).toBe('failed');
|
|
84
|
+
expect(result.exitCode).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('succeeds with non-zero exit when failOnError is false', async () => {
|
|
88
|
+
const executor = createExecutor({
|
|
89
|
+
processSpawner: mockSpawner({
|
|
90
|
+
spawnShell: vi.fn(async () => ({ output: 'warn', exitCode: 1 })),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
const step = makeStep({ command: 'maybe-fail', failOnError: false });
|
|
94
|
+
const result = await executor.executeOne(step, new Map());
|
|
95
|
+
expect(result.status).toBe('completed');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── 2. Non-interactive agent step ────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe('StepExecutor — non-interactive agent steps', () => {
|
|
102
|
+
it('spawns a codex worker and captures output', async () => {
|
|
103
|
+
const spawner = mockSpawner();
|
|
104
|
+
const executor = createExecutor({ processSpawner: spawner });
|
|
105
|
+
const agent = makeAgent({ cli: 'codex', name: 'codex-worker', interactive: false });
|
|
106
|
+
const step = makeStep({ name: 'codex-step', type: 'agent', agent: 'codex-worker', task: 'Fix the bug', command: undefined });
|
|
107
|
+
const agentMap = new Map([['codex-worker', agent]]);
|
|
108
|
+
|
|
109
|
+
const result = await executor.executeOne(step, agentMap);
|
|
110
|
+
expect(spawner.spawnAgent).toHaveBeenCalledWith(
|
|
111
|
+
agent, 'Fix the bug', expect.objectContaining({ cwd: '/tmp/test-project' })
|
|
112
|
+
);
|
|
113
|
+
expect(result.status).toBe('completed');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('fails when agent is not found in agentMap', async () => {
|
|
117
|
+
const executor = createExecutor();
|
|
118
|
+
const step = makeStep({ name: 'orphan', type: 'agent', agent: 'missing-agent', task: 'Do stuff', command: undefined });
|
|
119
|
+
const result = await executor.executeOne(step, new Map());
|
|
120
|
+
expect(result.status).toBe('failed');
|
|
121
|
+
expect(result.error).toContain('not found');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── 3. Interactive agent step ────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
describe('StepExecutor — interactive agent steps', () => {
|
|
128
|
+
it('spawns a claude lead via spawnInteractive', async () => {
|
|
129
|
+
const spawner = mockSpawner();
|
|
130
|
+
const executor = createExecutor({ processSpawner: spawner });
|
|
131
|
+
const agent = makeAgent({ cli: 'claude', name: 'lead-agent' });
|
|
132
|
+
const step = makeStep({ name: 'lead-step', type: 'agent', agent: 'lead-agent', task: 'Coordinate work', command: undefined });
|
|
133
|
+
const agentMap = new Map([['lead-agent', agent]]);
|
|
134
|
+
|
|
135
|
+
const result = await executor.executeOne(step, agentMap);
|
|
136
|
+
expect(spawner.spawnInteractive).toHaveBeenCalled();
|
|
137
|
+
expect(result.status).toBe('completed');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── 4. Step timeout handling ─────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
describe('StepExecutor — timeout handling', () => {
|
|
144
|
+
it('passes timeoutMs through to process spawner', async () => {
|
|
145
|
+
const spawner = mockSpawner();
|
|
146
|
+
const executor = createExecutor({ processSpawner: spawner });
|
|
147
|
+
const step = makeStep({ command: 'sleep 60', timeoutMs: 5000 });
|
|
148
|
+
|
|
149
|
+
await executor.executeOne(step, new Map());
|
|
150
|
+
expect(spawner.spawnShell).toHaveBeenCalledWith(
|
|
151
|
+
'sleep 60',
|
|
152
|
+
expect.objectContaining({ timeoutMs: 5000 })
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('fails step when spawn rejects due to timeout', async () => {
|
|
157
|
+
const executor = createExecutor({
|
|
158
|
+
processSpawner: mockSpawner({
|
|
159
|
+
spawnShell: vi.fn(async () => { throw new Error('Process timed out'); }),
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
const step = makeStep({ command: 'sleep 60', timeoutMs: 100 });
|
|
163
|
+
const result = await executor.executeOne(step, new Map());
|
|
164
|
+
expect(result.status).toBe('failed');
|
|
165
|
+
expect(result.error).toContain('timed out');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── 5. Step dependency resolution (dependsOn) ────────────────────────────────
|
|
170
|
+
|
|
171
|
+
describe('StepExecutor — dependency resolution', () => {
|
|
172
|
+
it('returns only steps whose deps are all completed', () => {
|
|
173
|
+
const executor = createExecutor();
|
|
174
|
+
const steps = [
|
|
175
|
+
makeStep({ name: 'a' }),
|
|
176
|
+
makeStep({ name: 'b', dependsOn: ['a'] }),
|
|
177
|
+
makeStep({ name: 'c', dependsOn: ['a', 'b'] }),
|
|
178
|
+
];
|
|
179
|
+
const statuses = new Map<string, WorkflowStepStatus>([
|
|
180
|
+
['a', 'completed'],
|
|
181
|
+
['b', 'pending'],
|
|
182
|
+
['c', 'pending'],
|
|
183
|
+
]);
|
|
184
|
+
const ready = executor.findReady(steps, statuses);
|
|
185
|
+
expect(ready.map((s) => s.name)).toEqual(['b']);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('treats skipped deps as satisfied', () => {
|
|
189
|
+
const executor = createExecutor();
|
|
190
|
+
const steps = [
|
|
191
|
+
makeStep({ name: 'a' }),
|
|
192
|
+
makeStep({ name: 'b', dependsOn: ['a'] }),
|
|
193
|
+
];
|
|
194
|
+
const statuses = new Map<string, WorkflowStepStatus>([
|
|
195
|
+
['a', 'skipped'],
|
|
196
|
+
['b', 'pending'],
|
|
197
|
+
]);
|
|
198
|
+
const ready = executor.findReady(steps, statuses);
|
|
199
|
+
expect(ready.map((s) => s.name)).toEqual(['b']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('returns steps with no deps when all are pending', () => {
|
|
203
|
+
const executor = createExecutor();
|
|
204
|
+
const steps = [
|
|
205
|
+
makeStep({ name: 'a' }),
|
|
206
|
+
makeStep({ name: 'b', dependsOn: ['a'] }),
|
|
207
|
+
];
|
|
208
|
+
const statuses = new Map<string, WorkflowStepStatus>([
|
|
209
|
+
['a', 'pending'],
|
|
210
|
+
['b', 'pending'],
|
|
211
|
+
]);
|
|
212
|
+
const ready = executor.findReady(steps, statuses);
|
|
213
|
+
expect(ready.map((s) => s.name)).toEqual(['a']);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('returns nothing when all deps are failed', () => {
|
|
217
|
+
const executor = createExecutor();
|
|
218
|
+
const steps = [
|
|
219
|
+
makeStep({ name: 'a' }),
|
|
220
|
+
makeStep({ name: 'b', dependsOn: ['a'] }),
|
|
221
|
+
];
|
|
222
|
+
const statuses = new Map<string, WorkflowStepStatus>([
|
|
223
|
+
['a', 'failed'],
|
|
224
|
+
['b', 'pending'],
|
|
225
|
+
]);
|
|
226
|
+
const ready = executor.findReady(steps, statuses);
|
|
227
|
+
expect(ready.map((s) => s.name)).toEqual([]);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── 6. Step output capture and storage ───────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
describe('StepExecutor — output capture', () => {
|
|
234
|
+
it('persists step output after successful completion', async () => {
|
|
235
|
+
const deps = makeDeps();
|
|
236
|
+
const executor = new StepExecutor(deps);
|
|
237
|
+
const step = makeStep({ command: 'echo result-data' });
|
|
238
|
+
|
|
239
|
+
await executor.executeOne(step, new Map());
|
|
240
|
+
expect(deps.persistStepOutput).toHaveBeenCalledWith(
|
|
241
|
+
'run-001', 'step-1', expect.stringContaining('hello')
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('persists step row status on completion', async () => {
|
|
246
|
+
const deps = makeDeps();
|
|
247
|
+
const executor = new StepExecutor(deps);
|
|
248
|
+
const step = makeStep({ command: 'echo ok' });
|
|
249
|
+
|
|
250
|
+
await executor.executeOne(step, new Map());
|
|
251
|
+
expect(deps.persistStepRow).toHaveBeenCalledWith(
|
|
252
|
+
expect.any(String),
|
|
253
|
+
expect.objectContaining({ status: 'completed' })
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('captures output on failure', async () => {
|
|
258
|
+
const deps = makeDeps({
|
|
259
|
+
processSpawner: mockSpawner({
|
|
260
|
+
spawnShell: vi.fn(async () => ({ output: 'error: not found', exitCode: 1 })),
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
const executor = new StepExecutor(deps);
|
|
264
|
+
const step = makeStep({ command: 'bad-command' });
|
|
265
|
+
const result = await executor.executeOne(step, new Map());
|
|
266
|
+
expect(result.output).toContain('error: not found');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('suppresses output when captureOutput is false', async () => {
|
|
270
|
+
const executor = createExecutor();
|
|
271
|
+
const step = makeStep({ command: 'echo secret', captureOutput: false });
|
|
272
|
+
const result = await executor.executeOne(step, new Map());
|
|
273
|
+
expect(result.output).toContain('Command completed');
|
|
274
|
+
expect(result.output).not.toContain('hello');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ── 7. Step retry on failure ─────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
describe('StepExecutor — retry logic', () => {
|
|
281
|
+
// Note: monitorStep retries on thrown errors (spawn failures), not on non-zero exit codes.
|
|
282
|
+
// Non-zero exit codes are handled by toCompletionResult and produce immediate failure.
|
|
283
|
+
|
|
284
|
+
it('retries when spawn throws an error', async () => {
|
|
285
|
+
let attempt = 0;
|
|
286
|
+
const executor = createExecutor({
|
|
287
|
+
processSpawner: mockSpawner({
|
|
288
|
+
spawnShell: vi.fn(async () => {
|
|
289
|
+
attempt++;
|
|
290
|
+
if (attempt < 3) throw new Error('connection refused');
|
|
291
|
+
return { output: 'ok', exitCode: 0 };
|
|
292
|
+
}),
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
const step = makeStep({ command: 'flaky', retries: 3 });
|
|
296
|
+
const result = await executor.executeOne(step, new Map());
|
|
297
|
+
expect(result.status).toBe('completed');
|
|
298
|
+
expect(result.retries).toBe(2);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('fails after exhausting retries on thrown errors', async () => {
|
|
302
|
+
const executor = createExecutor({
|
|
303
|
+
processSpawner: mockSpawner({
|
|
304
|
+
spawnShell: vi.fn(async () => { throw new Error('always fails'); }),
|
|
305
|
+
}),
|
|
306
|
+
});
|
|
307
|
+
const step = makeStep({ command: 'always-fail', retries: 2 });
|
|
308
|
+
const result = await executor.executeOne(step, new Map());
|
|
309
|
+
expect(result.status).toBe('failed');
|
|
310
|
+
expect(result.retries).toBe(2);
|
|
311
|
+
expect(result.error).toContain('always fails');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('does not retry on non-zero exit code (immediate failure)', async () => {
|
|
315
|
+
const spawnShell = vi.fn(async () => ({ output: 'fail', exitCode: 1 }));
|
|
316
|
+
const executor = createExecutor({
|
|
317
|
+
processSpawner: mockSpawner({ spawnShell }),
|
|
318
|
+
});
|
|
319
|
+
const step = makeStep({ command: 'bad', retries: 3 });
|
|
320
|
+
const result = await executor.executeOne(step, new Map());
|
|
321
|
+
expect(result.status).toBe('failed');
|
|
322
|
+
// Called only once — no retries for clean non-zero exits
|
|
323
|
+
expect(spawnShell).toHaveBeenCalledTimes(1);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('calls onStepRetried callback on each retry', async () => {
|
|
327
|
+
const onStepRetried = vi.fn();
|
|
328
|
+
let attempt = 0;
|
|
329
|
+
const executor = createExecutor({
|
|
330
|
+
onStepRetried,
|
|
331
|
+
processSpawner: mockSpawner({
|
|
332
|
+
spawnShell: vi.fn(async () => {
|
|
333
|
+
attempt++;
|
|
334
|
+
if (attempt < 2) throw new Error('transient');
|
|
335
|
+
return { output: 'ok', exitCode: 0 };
|
|
336
|
+
}),
|
|
337
|
+
}),
|
|
338
|
+
});
|
|
339
|
+
const step = makeStep({ command: 'flaky', retries: 2 });
|
|
340
|
+
await executor.executeOne(step, new Map());
|
|
341
|
+
expect(onStepRetried).toHaveBeenCalledTimes(1);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// ── 8. Process spawner — command building ────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
describe('ProcessSpawner — buildCommand', () => {
|
|
348
|
+
it('builds claude CLI command', () => {
|
|
349
|
+
const spawner = createProcessSpawner({ cwd: '/tmp' });
|
|
350
|
+
const agent = makeAgent({ cli: 'claude', name: 'claude-worker' });
|
|
351
|
+
const cmd = spawner.buildCommand(agent, 'Do the task');
|
|
352
|
+
expect(cmd.bin).toBe('claude');
|
|
353
|
+
expect(cmd.args).toContain('Do the task');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('builds codex CLI command', () => {
|
|
357
|
+
const spawner = createProcessSpawner({ cwd: '/tmp' });
|
|
358
|
+
const agent = makeAgent({ cli: 'codex', name: 'codex-worker' });
|
|
359
|
+
const cmd = spawner.buildCommand(agent, 'Fix bug');
|
|
360
|
+
expect(cmd.bin).toBe('codex');
|
|
361
|
+
expect(cmd.args).toContain('Fix bug');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('builds aider CLI command', () => {
|
|
365
|
+
const spawner = createProcessSpawner({ cwd: '/tmp' });
|
|
366
|
+
const agent = makeAgent({ cli: 'aider', name: 'aider-worker' });
|
|
367
|
+
const cmd = spawner.buildCommand(agent, 'Refactor');
|
|
368
|
+
expect(cmd.bin).toBe('aider');
|
|
369
|
+
expect(cmd.args).toContain('Refactor');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('builds gemini CLI command', () => {
|
|
373
|
+
const spawner = createProcessSpawner({ cwd: '/tmp' });
|
|
374
|
+
const agent = makeAgent({ cli: 'gemini', name: 'gemini-worker' });
|
|
375
|
+
const cmd = spawner.buildCommand(agent, 'Analyze');
|
|
376
|
+
expect(cmd.bin).toBe('gemini');
|
|
377
|
+
expect(cmd.args).toContain('Analyze');
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ── 9. executeAll — DAG orchestration ────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
describe('StepExecutor — executeAll', () => {
|
|
384
|
+
it('executes steps in dependency order', async () => {
|
|
385
|
+
const order: string[] = [];
|
|
386
|
+
const executor = createExecutor({
|
|
387
|
+
processSpawner: mockSpawner({
|
|
388
|
+
spawnShell: vi.fn(async () => {
|
|
389
|
+
return { output: 'ok', exitCode: 0 };
|
|
390
|
+
}),
|
|
391
|
+
}),
|
|
392
|
+
onStepStarted: vi.fn((step) => { order.push(step.name); }),
|
|
393
|
+
});
|
|
394
|
+
const steps = [
|
|
395
|
+
makeStep({ name: 'a', command: 'echo a' }),
|
|
396
|
+
makeStep({ name: 'b', command: 'echo b', dependsOn: ['a'] }),
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
const results = await executor.executeAll(steps, new Map());
|
|
400
|
+
expect(results.size).toBe(2);
|
|
401
|
+
expect(order).toEqual(['a', 'b']);
|
|
402
|
+
expect(results.get('a')?.status).toBe('completed');
|
|
403
|
+
expect(results.get('b')?.status).toBe('completed');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('skips downstream steps on fail-fast', async () => {
|
|
407
|
+
const executor = createExecutor({
|
|
408
|
+
processSpawner: mockSpawner({
|
|
409
|
+
spawnShell: vi.fn(async () => ({ output: 'err', exitCode: 1 })),
|
|
410
|
+
}),
|
|
411
|
+
markDownstreamSkipped: vi.fn(),
|
|
412
|
+
});
|
|
413
|
+
const steps = [
|
|
414
|
+
makeStep({ name: 'a', command: 'fail' }),
|
|
415
|
+
makeStep({ name: 'b', command: 'echo b', dependsOn: ['a'] }),
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
await expect(
|
|
419
|
+
executor.executeAll(steps, new Map(), { strategy: 'fail-fast' })
|
|
420
|
+
).rejects.toThrow('Step "a" failed');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('continues past failures with continue strategy', async () => {
|
|
424
|
+
let callCount = 0;
|
|
425
|
+
const executor = createExecutor({
|
|
426
|
+
processSpawner: mockSpawner({
|
|
427
|
+
spawnShell: vi.fn(async () => {
|
|
428
|
+
callCount++;
|
|
429
|
+
if (callCount === 1) return { output: 'err', exitCode: 1 };
|
|
430
|
+
return { output: 'ok', exitCode: 0 };
|
|
431
|
+
}),
|
|
432
|
+
}),
|
|
433
|
+
markDownstreamSkipped: vi.fn(),
|
|
434
|
+
});
|
|
435
|
+
const steps = [
|
|
436
|
+
makeStep({ name: 'a', command: 'fail' }),
|
|
437
|
+
makeStep({ name: 'c', command: 'echo c' }), // no dependency on a
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
const results = await executor.executeAll(steps, new Map(), { strategy: 'continue' });
|
|
441
|
+
expect(results.get('a')?.status).toBe('failed');
|
|
442
|
+
expect(results.get('c')?.status).toBe('completed');
|
|
443
|
+
});
|
|
444
|
+
});
|