Skip to content

Commit 645fab2

Browse files
committed
feat: Add unit tests for aegis_workflow container orchestration behavior
1 parent 5e9aef6 commit 645fab2

File tree

2 files changed

+376
-0
lines changed

2 files changed

+376
-0
lines changed

src/activities/index.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import type { ExecuteContainerRunResponse } from '../types.js';
3+
4+
const executeContainerRunMock = vi.fn();
5+
6+
vi.mock('../grpc/client.js', () => ({
7+
aegisRuntimeClient: {
8+
executeContainerRun: executeContainerRunMock,
9+
executeAgent: vi.fn(),
10+
executeSystemCommand: vi.fn(),
11+
validateWithJudges: vi.fn(),
12+
storeTrajectoryPattern: vi.fn(),
13+
},
14+
}));
15+
16+
vi.mock('../logger.js', () => ({
17+
logger: {
18+
info: vi.fn(),
19+
error: vi.fn(),
20+
warn: vi.fn(),
21+
debug: vi.fn(),
22+
},
23+
}));
24+
25+
import { executeParallelContainerRunActivity } from './index.js';
26+
27+
function ok(exit_code: number, name = 'step'): ExecuteContainerRunResponse & { name?: string } {
28+
return {
29+
exit_code,
30+
stdout: `${name}-stdout`,
31+
stderr: `${name}-stderr`,
32+
duration_ms: 10,
33+
attempts: 1,
34+
};
35+
}
36+
37+
describe('executeParallelContainerRunActivity', () => {
38+
beforeEach(() => {
39+
executeContainerRunMock.mockReset();
40+
});
41+
42+
it('returns failure result (no throw) for all_succeed when any step fails', async () => {
43+
executeContainerRunMock
44+
.mockResolvedValueOnce(ok(0, 'unit'))
45+
.mockResolvedValueOnce(ok(2, 'lint'));
46+
47+
const result = await executeParallelContainerRunActivity({
48+
execution_id: 'exec-1',
49+
state_name: 'TEST',
50+
completion: 'all_succeed',
51+
steps: [
52+
{ name: 'unit', image: 'alpine', command: ['true'] },
53+
{ name: 'lint', image: 'alpine', command: ['false'] },
54+
],
55+
});
56+
57+
expect(result.overall_success).toBe(false);
58+
expect(result.completion).toBe('all_succeed');
59+
expect(result.succeeded).toBe(1);
60+
expect(result.failed).toBe(1);
61+
expect(result.results).toHaveLength(2);
62+
});
63+
64+
it('returns failure result (no throw) for any_succeed when all steps fail', async () => {
65+
executeContainerRunMock.mockResolvedValue(ok(3));
66+
67+
const result = await executeParallelContainerRunActivity({
68+
execution_id: 'exec-2',
69+
state_name: 'TEST',
70+
completion: 'any_succeed',
71+
steps: [
72+
{ name: 'unit', image: 'alpine', command: ['false'] },
73+
{ name: 'lint', image: 'alpine', command: ['false'] },
74+
],
75+
});
76+
77+
expect(result.overall_success).toBe(false);
78+
expect(result.completion).toBe('any_succeed');
79+
expect(result.succeeded).toBe(0);
80+
expect(result.failed).toBe(2);
81+
});
82+
83+
it('returns success for best_effort even when all steps fail', async () => {
84+
executeContainerRunMock.mockResolvedValue(ok(1));
85+
86+
const result = await executeParallelContainerRunActivity({
87+
execution_id: 'exec-3',
88+
state_name: 'TEST',
89+
completion: 'best_effort',
90+
steps: [
91+
{ name: 'unit', image: 'alpine', command: ['false'] },
92+
{ name: 'lint', image: 'alpine', command: ['false'] },
93+
],
94+
});
95+
96+
expect(result.overall_success).toBe(true);
97+
expect(result.succeeded).toBe(0);
98+
expect(result.failed).toBe(2);
99+
});
100+
101+
it('converts rejected step call into non-zero step result and preserves aggregation', async () => {
102+
executeContainerRunMock
103+
.mockResolvedValueOnce(ok(0, 'unit'))
104+
.mockRejectedValueOnce(new Error('grpc unavailable'));
105+
106+
const result = await executeParallelContainerRunActivity({
107+
execution_id: 'exec-4',
108+
state_name: 'TEST',
109+
completion: 'all_succeed',
110+
steps: [
111+
{ name: 'unit', image: 'alpine', command: ['true'] },
112+
{ name: 'lint', image: 'alpine', command: ['false'] },
113+
],
114+
});
115+
116+
expect(result.overall_success).toBe(false);
117+
const lint = result.results.find((r) => r.name === 'lint');
118+
expect(lint).toBeDefined();
119+
expect(lint?.exit_code).toBe(1);
120+
expect(lint?.stderr).toContain('grpc unavailable');
121+
});
122+
});
123+
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import type { TemporalWorkflowDefinition } from '../types.js';
3+
4+
const activityMocks = {
5+
executeAgentActivity: vi.fn(),
6+
executeSystemCommandActivity: vi.fn(),
7+
validateOutputActivity: vi.fn(),
8+
executeParallelAgentsActivity: vi.fn(),
9+
storeTrajectoryPatternActivity: vi.fn(),
10+
fetchWorkflowDefinition: vi.fn(),
11+
publishEventActivity: vi.fn(),
12+
executeContainerRunActivity: vi.fn(),
13+
executeParallelContainerRunActivity: vi.fn(),
14+
};
15+
16+
vi.mock('../activities/index.js', () => activityMocks);
17+
18+
vi.mock('@temporalio/workflow', () => ({
19+
proxyActivities: () => activityMocks,
20+
setHandler: vi.fn(),
21+
defineSignal: vi.fn(() => Symbol('humanInput')),
22+
condition: vi.fn(async (predicate: () => boolean) => predicate()),
23+
workflowInfo: vi.fn(() => ({ workflowId: 'exec-123' })),
24+
}));
25+
26+
import { aegis_workflow } from './aegis-workflow.js';
27+
28+
function baseDefinition(states: TemporalWorkflowDefinition['states'], initial = 'BUILD'): TemporalWorkflowDefinition {
29+
return {
30+
workflow_id: 'wf-1',
31+
name: 'ci-workflow',
32+
version: '1.0.0',
33+
initial_state: initial,
34+
context: {},
35+
states,
36+
};
37+
}
38+
39+
describe('aegis_workflow container orchestration behavior', () => {
40+
beforeEach(() => {
41+
for (const fn of Object.values(activityMocks)) {
42+
fn.mockReset();
43+
}
44+
activityMocks.publishEventActivity.mockResolvedValue(undefined);
45+
activityMocks.executeSystemCommandActivity.mockResolvedValue({
46+
status: 'success',
47+
exit_code: 0,
48+
stdout: 'ok',
49+
stderr: '',
50+
});
51+
});
52+
53+
it('stores ContainerRun output shape with nested output object for blackboard templates', async () => {
54+
activityMocks.fetchWorkflowDefinition.mockResolvedValue(
55+
baseDefinition({
56+
BUILD: {
57+
kind: 'ContainerRun',
58+
container_run_name: 'build',
59+
container_run_image: 'rust:1.75',
60+
container_run_command: ['cargo', 'build'],
61+
transitions: [],
62+
},
63+
})
64+
);
65+
activityMocks.executeContainerRunActivity.mockResolvedValue({
66+
exit_code: 0,
67+
stdout: 'build-ok',
68+
stderr: '',
69+
duration_ms: 120,
70+
attempts: 1,
71+
});
72+
73+
const result = await aegis_workflow({ workflow_name: 'ci-workflow', input: {} });
74+
const build = result.blackboard?.BUILD;
75+
76+
expect(result.status).toBe('completed');
77+
expect(build?.status).toBe('success');
78+
expect(build?.output?.exit_code).toBe(0);
79+
expect(build?.output?.stdout).toBe('build-ok');
80+
expect(build?.exit_code).toBe(0);
81+
});
82+
83+
it('routes on_success using container exit code 0', async () => {
84+
activityMocks.fetchWorkflowDefinition.mockResolvedValue(
85+
baseDefinition(
86+
{
87+
BUILD: {
88+
kind: 'ContainerRun',
89+
container_run_name: 'build',
90+
container_run_image: 'rust:1.75',
91+
container_run_command: ['cargo', 'build'],
92+
transitions: [
93+
{ condition: 'on_success', target: 'PASS' },
94+
{ condition: 'on_failure', target: 'FAIL' },
95+
],
96+
},
97+
PASS: { kind: 'System', command: 'echo pass', transitions: [] },
98+
FAIL: { kind: 'System', command: 'echo fail', transitions: [] },
99+
},
100+
'BUILD'
101+
)
102+
);
103+
activityMocks.executeContainerRunActivity.mockResolvedValue({
104+
exit_code: 0,
105+
stdout: 'ok',
106+
stderr: '',
107+
duration_ms: 10,
108+
attempts: 1,
109+
});
110+
111+
const result = await aegis_workflow({ workflow_name: 'ci-workflow', input: {} });
112+
expect(result.final_state).toBe('PASS');
113+
});
114+
115+
it('routes on_failure using non-zero container exit code', async () => {
116+
activityMocks.fetchWorkflowDefinition.mockResolvedValue(
117+
baseDefinition(
118+
{
119+
BUILD: {
120+
kind: 'ContainerRun',
121+
container_run_name: 'build',
122+
container_run_image: 'rust:1.75',
123+
container_run_command: ['cargo', 'build'],
124+
transitions: [
125+
{ condition: 'on_success', target: 'PASS' },
126+
{ condition: 'on_failure', target: 'FAIL' },
127+
],
128+
},
129+
PASS: { kind: 'System', command: 'echo pass', transitions: [] },
130+
FAIL: { kind: 'System', command: 'echo fail', transitions: [] },
131+
},
132+
'BUILD'
133+
)
134+
);
135+
activityMocks.executeContainerRunActivity.mockResolvedValue({
136+
exit_code: 2,
137+
stdout: '',
138+
stderr: 'compile failed',
139+
duration_ms: 10,
140+
attempts: 1,
141+
});
142+
143+
const result = await aegis_workflow({ workflow_name: 'ci-workflow', input: {} });
144+
expect(result.final_state).toBe('FAIL');
145+
});
146+
147+
it('supports exit_code_non_zero transition for ContainerRun outputs', async () => {
148+
activityMocks.fetchWorkflowDefinition.mockResolvedValue(
149+
baseDefinition(
150+
{
151+
BUILD: {
152+
kind: 'ContainerRun',
153+
container_run_name: 'build',
154+
container_run_image: 'rust:1.75',
155+
container_run_command: ['cargo', 'build'],
156+
transitions: [
157+
{ condition: 'exit_code_non_zero', target: 'FAIL' },
158+
{ condition: 'always', target: 'PASS' },
159+
],
160+
},
161+
PASS: { kind: 'System', command: 'echo pass', transitions: [] },
162+
FAIL: { kind: 'System', command: 'echo fail', transitions: [] },
163+
},
164+
'BUILD'
165+
)
166+
);
167+
activityMocks.executeContainerRunActivity.mockResolvedValue({
168+
exit_code: 9,
169+
stdout: '',
170+
stderr: 'failed',
171+
duration_ms: 10,
172+
attempts: 1,
173+
});
174+
175+
const result = await aegis_workflow({ workflow_name: 'ci-workflow', input: {} });
176+
expect(result.final_state).toBe('FAIL');
177+
});
178+
179+
it('returns ParallelContainerRun blackboard output keyed by step name', async () => {
180+
activityMocks.fetchWorkflowDefinition.mockResolvedValue(
181+
baseDefinition(
182+
{
183+
TEST: {
184+
kind: 'ParallelContainerRun',
185+
parallel_container_steps: [
186+
{ name: 'unit-tests', image: 'rust:1.75', command: ['cargo', 'test'] },
187+
{ name: 'lint', image: 'rust:1.75', command: ['cargo', 'clippy'] },
188+
],
189+
parallel_container_completion: 'all_succeed',
190+
transitions: [],
191+
},
192+
},
193+
'TEST'
194+
)
195+
);
196+
activityMocks.executeParallelContainerRunActivity.mockResolvedValue({
197+
overall_success: true,
198+
completion: 'all_succeed',
199+
succeeded: 2,
200+
failed: 0,
201+
results: [
202+
{ name: 'unit-tests', exit_code: 0, stdout: 'ok', stderr: '', duration_ms: 12 },
203+
{ name: 'lint', exit_code: 0, stdout: 'ok', stderr: '', duration_ms: 8 },
204+
],
205+
});
206+
207+
const result = await aegis_workflow({ workflow_name: 'ci-workflow', input: {} });
208+
const testOutput = result.blackboard?.TEST?.output;
209+
210+
expect(result.status).toBe('completed');
211+
expect(testOutput?.['unit-tests']?.stdout).toBe('ok');
212+
expect(testOutput?.['lint']?.exit_code).toBe(0);
213+
});
214+
215+
it('routes on_failure for ParallelContainerRun when aggregation fails', async () => {
216+
activityMocks.fetchWorkflowDefinition.mockResolvedValue(
217+
baseDefinition(
218+
{
219+
TEST: {
220+
kind: 'ParallelContainerRun',
221+
parallel_container_steps: [
222+
{ name: 'unit', image: 'rust:1.75', command: ['cargo', 'test'] },
223+
{ name: 'lint', image: 'rust:1.75', command: ['cargo', 'clippy'] },
224+
],
225+
parallel_container_completion: 'all_succeed',
226+
transitions: [
227+
{ condition: 'on_success', target: 'PASS' },
228+
{ condition: 'on_failure', target: 'FAIL' },
229+
],
230+
},
231+
PASS: { kind: 'System', command: 'echo pass', transitions: [] },
232+
FAIL: { kind: 'System', command: 'echo fail', transitions: [] },
233+
},
234+
'TEST'
235+
)
236+
);
237+
activityMocks.executeParallelContainerRunActivity.mockResolvedValue({
238+
overall_success: false,
239+
completion: 'all_succeed',
240+
succeeded: 1,
241+
failed: 1,
242+
results: [
243+
{ name: 'unit', exit_code: 0, stdout: 'ok', stderr: '', duration_ms: 12 },
244+
{ name: 'lint', exit_code: 2, stdout: '', stderr: 'lint fail', duration_ms: 8 },
245+
],
246+
});
247+
248+
const result = await aegis_workflow({ workflow_name: 'ci-workflow', input: {} });
249+
expect(result.final_state).toBe('FAIL');
250+
expect(result.blackboard?.TEST?.status).toBe('failed');
251+
});
252+
});
253+

0 commit comments

Comments
 (0)