agent-relay 4.0.0 → 4.0.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.
- 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 +1980 -1024
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +4 -3
- package/dist/src/cli/commands/core.js.map +1 -1
- 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/start.d.ts.map +1 -1
- package/dist/src/cli/commands/on/start.js +62 -26
- package/dist/src/cli/commands/on/start.js.map +1 -1
- package/dist/src/cli/commands/on/workspace.d.ts.map +1 -1
- package/dist/src/cli/commands/on/workspace.js +4 -0
- package/dist/src/cli/commands/on/workspace.js.map +1 -1
- package/dist/src/cli/commands/on.js +1 -1
- package/dist/src/cli/commands/on.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +15 -8
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/client-factory.d.ts +2 -2
- package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/src/cli/lib/client-factory.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 +10 -10
- 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/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 +12 -2
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +20 -1
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +1 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/relay.d.ts +2 -1
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +1 -1
- package/packages/sdk/dist/relay.js.map +1 -1
- 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/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/broker-path.ts +136 -30
- package/packages/sdk/src/client.ts +37 -3
- package/packages/sdk/src/index.ts +1 -0
- package/packages/sdk/src/relay.ts +6 -2
- 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/verification.ts +184 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- 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 +2 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Import from the module that will be extracted from runner.ts
|
|
4
|
+
import { resolveStepOutputRef, resolveTemplate, TemplateResolver } from '../template-resolver.js';
|
|
5
|
+
|
|
6
|
+
describe('TemplateResolver', () => {
|
|
7
|
+
const resolver = new TemplateResolver();
|
|
8
|
+
|
|
9
|
+
describe('resolveTemplate', () => {
|
|
10
|
+
it('replaces non-step placeholders and preserves deferred step outputs', () => {
|
|
11
|
+
const result = resolveTemplate('Deploy {{env}} after {{steps.plan.output}}', { env: 'prod' });
|
|
12
|
+
expect(result).toBe('Deploy prod after {{steps.plan.output}}');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('throws on unresolved placeholders', () => {
|
|
16
|
+
expect(() => resolveTemplate('Deploy {{missing}}', {})).toThrow('Unresolved variable: {{missing}}');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('resolveStepOutputRef', () => {
|
|
21
|
+
it('resolves a completed step output by reference', () => {
|
|
22
|
+
const stepOutputs = new Map([['plan', 'Build a REST API']]);
|
|
23
|
+
expect(resolveStepOutputRef('steps.plan.output', stepOutputs)).toBe('Build a REST API');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('accepts references wrapped in template braces', () => {
|
|
27
|
+
const stepOutputs = new Map([['code', 'Created 3 files']]);
|
|
28
|
+
expect(resolveStepOutputRef('{{steps.code.output}}', stepOutputs)).toBe('Created 3 files');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('resolveVariables', () => {
|
|
33
|
+
it('replaces simple {{var}} placeholders in agent tasks', () => {
|
|
34
|
+
const config = {
|
|
35
|
+
version: '1',
|
|
36
|
+
name: 'test',
|
|
37
|
+
swarm: { mode: 'coordinate' as const },
|
|
38
|
+
agents: [{ name: 'a1', cli: 'claude', task: 'Deploy {{env}} to {{region}}' }],
|
|
39
|
+
};
|
|
40
|
+
const result = resolver.resolveVariables(config as any, { env: 'staging', region: 'us-east-1' });
|
|
41
|
+
expect(result.agents[0].task).toBe('Deploy staging to us-east-1');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('replaces variables in workflow step tasks and commands', () => {
|
|
45
|
+
const config = {
|
|
46
|
+
version: '1',
|
|
47
|
+
name: 'test',
|
|
48
|
+
swarm: { mode: 'coordinate' as const },
|
|
49
|
+
agents: [],
|
|
50
|
+
workflows: [
|
|
51
|
+
{
|
|
52
|
+
name: 'wf1',
|
|
53
|
+
steps: [
|
|
54
|
+
{ name: 's1', task: 'Build {{project}}', agent: 'a1' },
|
|
55
|
+
{ name: 's2', command: 'deploy --env={{env}}' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
const result = resolver.resolveVariables(config as any, { project: 'relay', env: 'prod' });
|
|
61
|
+
expect(result.workflows![0].steps[0].task).toBe('Build relay');
|
|
62
|
+
expect(result.workflows![0].steps[1].command).toBe('deploy --env=prod');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('replaces variables in step params', () => {
|
|
66
|
+
const config = {
|
|
67
|
+
version: '1',
|
|
68
|
+
name: 'test',
|
|
69
|
+
swarm: { mode: 'coordinate' as const },
|
|
70
|
+
agents: [],
|
|
71
|
+
workflows: [
|
|
72
|
+
{
|
|
73
|
+
name: 'wf1',
|
|
74
|
+
steps: [{ name: 's1', agent: 'a1', params: { url: '{{base_url}}/api', count: 42 } }],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
const result = resolver.resolveVariables(config as any, { base_url: 'https://example.com' });
|
|
79
|
+
expect((result.workflows![0].steps[0].params as any).url).toBe('https://example.com/api');
|
|
80
|
+
// Non-string params are left untouched
|
|
81
|
+
expect((result.workflows![0].steps[0].params as any).count).toBe(42);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('preserves {{steps.X.output}} placeholders for later resolution', () => {
|
|
85
|
+
const config = {
|
|
86
|
+
version: '1',
|
|
87
|
+
name: 'test',
|
|
88
|
+
swarm: { mode: 'coordinate' as const },
|
|
89
|
+
agents: [{ name: 'a1', cli: 'claude', task: 'Use {{steps.plan.output}} for {{env}}' }],
|
|
90
|
+
};
|
|
91
|
+
const result = resolver.resolveVariables(config as any, { env: 'prod' });
|
|
92
|
+
expect(result.agents[0].task).toBe('Use {{steps.plan.output}} for prod');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('throws on unresolved non-step variables', () => {
|
|
96
|
+
const config = {
|
|
97
|
+
version: '1',
|
|
98
|
+
name: 'test',
|
|
99
|
+
swarm: { mode: 'coordinate' as const },
|
|
100
|
+
agents: [{ name: 'a1', cli: 'claude', task: 'Deploy to {{missing_var}}' }],
|
|
101
|
+
};
|
|
102
|
+
expect(() => resolver.resolveVariables(config as any, {})).toThrow('Unresolved variable: {{missing_var}}');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('does not mutate the original config', () => {
|
|
106
|
+
const config = {
|
|
107
|
+
version: '1',
|
|
108
|
+
name: 'test',
|
|
109
|
+
swarm: { mode: 'coordinate' as const },
|
|
110
|
+
agents: [{ name: 'a1', cli: 'claude', task: 'Deploy {{env}}' }],
|
|
111
|
+
};
|
|
112
|
+
resolver.resolveVariables(config as any, { env: 'staging' });
|
|
113
|
+
expect(config.agents[0].task).toBe('Deploy {{env}}');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('resolveDotPath', () => {
|
|
118
|
+
it('resolves nested dot-path variables', () => {
|
|
119
|
+
const config = {
|
|
120
|
+
version: '1',
|
|
121
|
+
name: 'test',
|
|
122
|
+
swarm: { mode: 'coordinate' as const },
|
|
123
|
+
agents: [{ name: 'a1', cli: 'claude', task: 'Region: {{aws.region}}' }],
|
|
124
|
+
};
|
|
125
|
+
const vars = { aws: { region: 'us-west-2' } } as any;
|
|
126
|
+
const result = resolver.resolveVariables(config as any, vars);
|
|
127
|
+
expect(result.agents[0].task).toBe('Region: us-west-2');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('throws for undefined nested paths', () => {
|
|
131
|
+
const config = {
|
|
132
|
+
version: '1',
|
|
133
|
+
name: 'test',
|
|
134
|
+
swarm: { mode: 'coordinate' as const },
|
|
135
|
+
agents: [{ name: 'a1', cli: 'claude', task: '{{a.b.c}}' }],
|
|
136
|
+
};
|
|
137
|
+
expect(() => resolver.resolveVariables(config as any, { a: { b: {} } } as any)).toThrow(
|
|
138
|
+
'Unresolved variable: {{a.b.c}}'
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('interpolateStepTask', () => {
|
|
144
|
+
it('resolves step output references from completed steps', () => {
|
|
145
|
+
const template = 'Review: {{steps.plan.output}} and {{steps.code.output}}';
|
|
146
|
+
const context = {
|
|
147
|
+
steps: {
|
|
148
|
+
plan: { output: 'Build a REST API' },
|
|
149
|
+
code: { output: 'Created 3 files' },
|
|
150
|
+
},
|
|
151
|
+
} as any;
|
|
152
|
+
const result = resolver.interpolateStepTask(template, context);
|
|
153
|
+
expect(result).toBe('Review: Build a REST API and Created 3 files');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('leaves unresolved step references intact', () => {
|
|
157
|
+
const template = 'Use {{steps.future.output}} later';
|
|
158
|
+
const result = resolver.interpolateStepTask(template, { steps: {} } as any);
|
|
159
|
+
expect(result).toBe('Use {{steps.future.output}} later');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
|
|
6
|
+
// The module under test — does not exist yet (red phase).
|
|
7
|
+
import {
|
|
8
|
+
runVerification,
|
|
9
|
+
stripInjectedTaskEcho,
|
|
10
|
+
checkOutputContains,
|
|
11
|
+
checkFileExists,
|
|
12
|
+
type VerificationCheck,
|
|
13
|
+
type VerificationResult,
|
|
14
|
+
type VerificationOptions,
|
|
15
|
+
WorkflowCompletionError,
|
|
16
|
+
} from '../verification.js';
|
|
17
|
+
|
|
18
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const noopSideEffects = {
|
|
21
|
+
recordStepToolSideEffect: vi.fn(),
|
|
22
|
+
getOrCreateStepEvidenceRecord: vi.fn(() => ({
|
|
23
|
+
evidence: { coordinationSignals: [] },
|
|
24
|
+
})),
|
|
25
|
+
log: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function run(
|
|
29
|
+
check: VerificationCheck,
|
|
30
|
+
output: string,
|
|
31
|
+
stepName = 'test-step',
|
|
32
|
+
options?: VerificationOptions
|
|
33
|
+
): VerificationResult {
|
|
34
|
+
return runVerification(check, output, stepName, undefined, options, noopSideEffects);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── tests ─────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
describe('verification logic', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 1. exit_code — pass on exit 0 (implicit success)
|
|
45
|
+
describe('exit_code', () => {
|
|
46
|
+
it('should pass when agent exited successfully (exit 0 implicit)', () => {
|
|
47
|
+
const result = run({ type: 'exit_code', value: '0' }, 'some output');
|
|
48
|
+
expect(result.passed).toBe(true);
|
|
49
|
+
expect(result.completionReason).toBe('completed_verified');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should still pass for non-zero value (exit_code is implicitly satisfied)', () => {
|
|
53
|
+
// per existing logic, exit_code case is a no-op — always passes if we reach it
|
|
54
|
+
const result = run({ type: 'exit_code', value: '1' }, 'output');
|
|
55
|
+
expect(result.passed).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 2. output_contains — case-sensitive substring match
|
|
60
|
+
describe('output_contains', () => {
|
|
61
|
+
it('should pass when output contains the token', () => {
|
|
62
|
+
const result = run(
|
|
63
|
+
{ type: 'output_contains', value: 'BUILD_SUCCESS' },
|
|
64
|
+
'Starting build...\nBUILD_SUCCESS\nDone.'
|
|
65
|
+
);
|
|
66
|
+
expect(result.passed).toBe(true);
|
|
67
|
+
expect(result.completionReason).toBe('completed_verified');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should fail when output does not contain the token', () => {
|
|
71
|
+
expect(() => run({ type: 'output_contains', value: 'BUILD_SUCCESS' }, 'build failed')).toThrow(
|
|
72
|
+
WorkflowCompletionError
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should be case-sensitive', () => {
|
|
77
|
+
expect(() => run({ type: 'output_contains', value: 'BUILD_SUCCESS' }, 'build_success')).toThrow(
|
|
78
|
+
WorkflowCompletionError
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return failure result instead of throwing when allowFailure is set', () => {
|
|
83
|
+
const result = run({ type: 'output_contains', value: 'MISSING' }, 'no match here', 'test-step', {
|
|
84
|
+
allowFailure: true,
|
|
85
|
+
});
|
|
86
|
+
expect(result.passed).toBe(false);
|
|
87
|
+
expect(result.completionReason).toBe('failed_verification');
|
|
88
|
+
expect(result.error).toContain('MISSING');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 3. file_exists — checks file presence at path
|
|
93
|
+
describe('file_exists', () => {
|
|
94
|
+
let tmpDir: string;
|
|
95
|
+
let tmpFile: string;
|
|
96
|
+
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-test-'));
|
|
99
|
+
tmpFile = path.join(tmpDir, 'artifact.txt');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should pass when the file exists', () => {
|
|
107
|
+
fs.writeFileSync(tmpFile, 'content');
|
|
108
|
+
// file_exists resolves relative to cwd; pass absolute path as value
|
|
109
|
+
const result = run({ type: 'file_exists', value: tmpFile }, '');
|
|
110
|
+
expect(result.passed).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should fail when the file does not exist', () => {
|
|
114
|
+
expect(() => run({ type: 'file_exists', value: path.join(tmpDir, 'nope.txt') }, '')).toThrow(
|
|
115
|
+
WorkflowCompletionError
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// 4. custom verification — returns { passed: false } (no-op in runner)
|
|
121
|
+
describe('custom', () => {
|
|
122
|
+
it('should return passed: false (delegated to caller)', () => {
|
|
123
|
+
const result = run({ type: 'custom', value: 'anything' }, 'output');
|
|
124
|
+
expect(result.passed).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 5. Invalid/unknown verification type — falls through gracefully
|
|
129
|
+
describe('unknown type', () => {
|
|
130
|
+
it('should fall through and pass for unknown verification types', () => {
|
|
131
|
+
const result = run({ type: 'nonexistent' as VerificationCheck['type'], value: 'x' }, 'output');
|
|
132
|
+
// falls through the switch with no match, reaches success path
|
|
133
|
+
expect(result.passed).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 6. completionMarkerFound option
|
|
138
|
+
describe('completionMarkerFound option', () => {
|
|
139
|
+
it('should log legacy marker message when completionMarkerFound is false', () => {
|
|
140
|
+
const result = run({ type: 'exit_code', value: '0' }, 'output', 'my-step', {
|
|
141
|
+
completionMarkerFound: false,
|
|
142
|
+
});
|
|
143
|
+
expect(result.passed).toBe(true);
|
|
144
|
+
expect(noopSideEffects.log).toHaveBeenCalledWith(
|
|
145
|
+
expect.stringContaining('without legacy STEP_COMPLETE marker')
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 7. stripInjectedTaskEcho
|
|
151
|
+
describe('stripInjectedTaskEcho', () => {
|
|
152
|
+
it('should return output unchanged when no injectedTaskText', () => {
|
|
153
|
+
expect(stripInjectedTaskEcho('hello world')).toBe('hello world');
|
|
154
|
+
expect(stripInjectedTaskEcho('hello world', undefined)).toBe('hello world');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should strip the injected task text from output', () => {
|
|
158
|
+
const task = 'Please run the build';
|
|
159
|
+
const output = 'Starting...\nPlease run the build\nBUILD_SUCCESS';
|
|
160
|
+
expect(stripInjectedTaskEcho(output, task)).toBe('Starting...\n\nBUILD_SUCCESS');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should handle CRLF normalization', () => {
|
|
164
|
+
const task = 'Run task\r\nwith newlines';
|
|
165
|
+
const output = 'prefix Run task\nwith newlines suffix';
|
|
166
|
+
expect(stripInjectedTaskEcho(output, task)).toBe('prefix suffix');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle LF to CRLF normalization', () => {
|
|
170
|
+
const task = 'Run task\nwith newlines';
|
|
171
|
+
const output = 'prefix Run task\r\nwith newlines suffix';
|
|
172
|
+
expect(stripInjectedTaskEcho(output, task)).toBe('prefix suffix');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should return output unchanged when task text is not found', () => {
|
|
176
|
+
expect(stripInjectedTaskEcho('output text', 'not present')).toBe('output text');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle empty injected task text', () => {
|
|
180
|
+
expect(stripInjectedTaskEcho('output', '')).toBe('output');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// 8. checkOutputContains with injectedTaskText
|
|
185
|
+
describe('checkOutputContains with injectedTaskText', () => {
|
|
186
|
+
it('should not match token that only appears in injected task echo', () => {
|
|
187
|
+
const task = 'Verify BUILD_SUCCESS appears';
|
|
188
|
+
const output = 'Verify BUILD_SUCCESS appears\nDone.';
|
|
189
|
+
expect(checkOutputContains(output, 'BUILD_SUCCESS', task)).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should match token that appears outside injected task echo', () => {
|
|
193
|
+
const task = 'Run the build';
|
|
194
|
+
const output = 'Run the build\nBUILD_SUCCESS';
|
|
195
|
+
expect(checkOutputContains(output, 'BUILD_SUCCESS', task)).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return false for empty token', () => {
|
|
199
|
+
expect(checkOutputContains('anything', '', undefined)).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 9. checkFileExists path traversal protection
|
|
204
|
+
describe('checkFileExists path traversal', () => {
|
|
205
|
+
let tmpDir: string;
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-traversal-'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should reject path traversal with ../', () => {
|
|
216
|
+
expect(checkFileExists('../../etc/passwd', tmpDir)).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should reject relative path with .. that resolves outside cwd', () => {
|
|
220
|
+
expect(checkFileExists('../../../etc/passwd', tmpDir)).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should allow files within cwd', () => {
|
|
224
|
+
const file = path.join(tmpDir, 'ok.txt');
|
|
225
|
+
fs.writeFileSync(file, 'ok');
|
|
226
|
+
expect(checkFileExists('ok.txt', tmpDir)).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -20,10 +20,11 @@ import type {
|
|
|
20
20
|
WorkflowRunRow,
|
|
21
21
|
WorkflowStep,
|
|
22
22
|
} from './types.js';
|
|
23
|
-
import { WorkflowRunner, type WorkflowEventListener, type
|
|
23
|
+
import { WorkflowRunner, type WorkflowEventListener, type RunnerStepExecutor } from './runner.js';
|
|
24
24
|
import { formatDryRunReport } from './dry-run-format.js';
|
|
25
25
|
import { createDefaultEventLogger, type LogLevel } from './default-logger.js';
|
|
26
26
|
import { runInCloud, type CloudRunOptions } from './cloud-runner.js';
|
|
27
|
+
import type { VariableContext } from './template-resolver.js';
|
|
27
28
|
|
|
28
29
|
// ── Option types for the builder API ────────────────────────────────────────
|
|
29
30
|
|
|
@@ -105,7 +106,7 @@ export interface WorkflowRunOptions {
|
|
|
105
106
|
/** Validate and print execution plan without spawning agents. */
|
|
106
107
|
dryRun?: boolean;
|
|
107
108
|
/** External step executor (e.g. Daytona sandbox backend). */
|
|
108
|
-
executor?:
|
|
109
|
+
executor?: RunnerStepExecutor;
|
|
109
110
|
/** Start from a specific step, skipping all predecessors. */
|
|
110
111
|
startFrom?: string;
|
|
111
112
|
/** Previous run ID whose cached outputs are used with startFrom. */
|
|
@@ -196,7 +197,7 @@ export class WorkflowBuilder {
|
|
|
196
197
|
if (!CHANNEL_RE.test(ch)) {
|
|
197
198
|
throw new Error(
|
|
198
199
|
`Invalid channel name "${ch}". Channel names must be lowercase alphanumeric and hyphens, starting with a letter or number. ` +
|
|
199
|
-
|
|
200
|
+
`Fix: use .toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')`
|
|
200
201
|
);
|
|
201
202
|
}
|
|
202
203
|
this._channel = ch;
|
|
@@ -419,9 +420,8 @@ export class WorkflowBuilder {
|
|
|
419
420
|
// Wire up default console logger unless explicitly disabled
|
|
420
421
|
// renderer: "listr" owns the terminal — skip console logger to avoid garbled output
|
|
421
422
|
// renderer: false implies no output at all
|
|
422
|
-
const logLevel =
|
|
423
|
-
? false
|
|
424
|
-
: (options.logLevel ?? 'normal');
|
|
423
|
+
const logLevel =
|
|
424
|
+
options.renderer === 'listr' || options.renderer === false ? false : (options.logLevel ?? 'normal');
|
|
425
425
|
if (logLevel !== false) {
|
|
426
426
|
runner.on(createDefaultEventLogger(logLevel));
|
|
427
427
|
}
|