Skip to content

Commit 59fddad

Browse files
authored
refactor: extract CLI workflow into cli-workflow.ts and add unit tests (#21)
Signed-off-by: Jiaxiao (mossaka) Zhou <duibao55328@gmail.com>
1 parent 87c8431 commit 59fddad

3 files changed

Lines changed: 198 additions & 26 deletions

File tree

src/cli-workflow.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { runMainWorkflow, WorkflowDependencies } from './cli-workflow';
2+
import { WrapperConfig } from './types';
3+
4+
const baseConfig: WrapperConfig = {
5+
allowedDomains: ['github.com'],
6+
copilotCommand: 'echo "hello"',
7+
logLevel: 'info',
8+
keepContainers: false,
9+
workDir: '/tmp/awf-test',
10+
imageRegistry: 'registry',
11+
imageTag: 'latest',
12+
buildLocal: false,
13+
};
14+
15+
const createLogger = () => ({
16+
info: jest.fn(),
17+
success: jest.fn(),
18+
warn: jest.fn(),
19+
});
20+
21+
describe('runMainWorkflow', () => {
22+
it('executes workflow steps in order and logs success for zero exit code', async () => {
23+
const callOrder: string[] = [];
24+
const dependencies: WorkflowDependencies = {
25+
ensureFirewallNetwork: jest.fn().mockImplementation(async () => {
26+
callOrder.push('ensureFirewallNetwork');
27+
return { squidIp: '172.30.0.10' };
28+
}),
29+
setupHostIptables: jest.fn().mockImplementation(async () => {
30+
callOrder.push('setupHostIptables');
31+
}),
32+
writeConfigs: jest.fn().mockImplementation(async () => {
33+
callOrder.push('writeConfigs');
34+
}),
35+
startContainers: jest.fn().mockImplementation(async () => {
36+
callOrder.push('startContainers');
37+
}),
38+
runCopilotCommand: jest.fn().mockImplementation(async () => {
39+
callOrder.push('runCopilotCommand');
40+
return { exitCode: 0 };
41+
}),
42+
};
43+
const performCleanup = jest.fn().mockImplementation(async () => {
44+
callOrder.push('performCleanup');
45+
});
46+
const logger = createLogger();
47+
48+
const exitCode = await runMainWorkflow(baseConfig, dependencies, {
49+
logger,
50+
performCleanup,
51+
});
52+
53+
expect(callOrder).toEqual([
54+
'ensureFirewallNetwork',
55+
'setupHostIptables',
56+
'writeConfigs',
57+
'startContainers',
58+
'runCopilotCommand',
59+
'performCleanup',
60+
]);
61+
expect(exitCode).toBe(0);
62+
expect(logger.success).toHaveBeenCalledWith('Command completed successfully');
63+
expect(logger.warn).not.toHaveBeenCalled();
64+
});
65+
66+
it('logs warning with exit code when command fails', async () => {
67+
const callOrder: string[] = [];
68+
const dependencies: WorkflowDependencies = {
69+
ensureFirewallNetwork: jest.fn().mockImplementation(async () => {
70+
callOrder.push('ensureFirewallNetwork');
71+
return { squidIp: '172.30.0.10' };
72+
}),
73+
setupHostIptables: jest.fn().mockImplementation(async () => {
74+
callOrder.push('setupHostIptables');
75+
}),
76+
writeConfigs: jest.fn().mockImplementation(async () => {
77+
callOrder.push('writeConfigs');
78+
}),
79+
startContainers: jest.fn().mockImplementation(async () => {
80+
callOrder.push('startContainers');
81+
}),
82+
runCopilotCommand: jest.fn().mockImplementation(async () => {
83+
callOrder.push('runCopilotCommand');
84+
return { exitCode: 42 };
85+
}),
86+
};
87+
const performCleanup = jest.fn().mockImplementation(async () => {
88+
callOrder.push('performCleanup');
89+
});
90+
const logger = createLogger();
91+
92+
const exitCode = await runMainWorkflow(baseConfig, dependencies, {
93+
logger,
94+
performCleanup,
95+
});
96+
97+
expect(exitCode).toBe(42);
98+
expect(callOrder).toEqual([
99+
'ensureFirewallNetwork',
100+
'setupHostIptables',
101+
'writeConfigs',
102+
'startContainers',
103+
'runCopilotCommand',
104+
'performCleanup',
105+
]);
106+
expect(logger.warn).toHaveBeenCalledWith('Command completed with exit code: 42');
107+
expect(logger.success).not.toHaveBeenCalled();
108+
});
109+
});

src/cli-workflow.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { WrapperConfig } from './types';
2+
3+
export interface WorkflowDependencies {
4+
ensureFirewallNetwork: () => Promise<{ squidIp: string }>;
5+
setupHostIptables: (squidIp: string, port: number) => Promise<void>;
6+
writeConfigs: (config: WrapperConfig) => Promise<void>;
7+
startContainers: (workDir: string, allowedDomains: string[]) => Promise<void>;
8+
runCopilotCommand: (
9+
workDir: string,
10+
allowedDomains: string[]
11+
) => Promise<{ exitCode: number }>;
12+
}
13+
14+
export interface WorkflowCallbacks {
15+
onHostIptablesSetup?: () => void;
16+
onContainersStarted?: () => void;
17+
}
18+
19+
export interface WorkflowLogger {
20+
info: (message: string, ...args: unknown[]) => void;
21+
success: (message: string, ...args: unknown[]) => void;
22+
warn: (message: string, ...args: unknown[]) => void;
23+
}
24+
25+
export interface WorkflowOptions extends WorkflowCallbacks {
26+
logger: WorkflowLogger;
27+
performCleanup: () => Promise<void>;
28+
}
29+
30+
/**
31+
* Executes the primary workflow for the CLI. This function is intentionally pure so
32+
* it can be unit tested with mocked dependencies.
33+
*/
34+
export async function runMainWorkflow(
35+
config: WrapperConfig,
36+
dependencies: WorkflowDependencies,
37+
options: WorkflowOptions
38+
): Promise<number> {
39+
const { logger, performCleanup, onHostIptablesSetup, onContainersStarted } = options;
40+
41+
// Step 0: Setup host-level network and iptables
42+
logger.info('Setting up host-level firewall network and iptables rules...');
43+
const networkConfig = await dependencies.ensureFirewallNetwork();
44+
await dependencies.setupHostIptables(networkConfig.squidIp, 3128);
45+
onHostIptablesSetup?.();
46+
47+
// Step 1: Write configuration files
48+
logger.info('Generating configuration files...');
49+
await dependencies.writeConfigs(config);
50+
51+
// Step 2: Start containers
52+
await dependencies.startContainers(config.workDir, config.allowedDomains);
53+
onContainersStarted?.();
54+
55+
// Step 3: Wait for copilot to complete
56+
const result = await dependencies.runCopilotCommand(config.workDir, config.allowedDomains);
57+
58+
// Step 4: Cleanup (logs will be preserved automatically if they exist)
59+
await performCleanup();
60+
61+
if (result.exitCode === 0) {
62+
logger.success('Command completed successfully');
63+
} else {
64+
logger.warn(`Command completed with exit code: ${result.exitCode}`);
65+
}
66+
67+
return result.exitCode;
68+
}

src/cli.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
ensureFirewallNetwork,
1818
setupHostIptables,
1919
cleanupHostIptables,
20-
cleanupFirewallNetwork,
2120
} from './host-iptables';
21+
import { runMainWorkflow } from './cli-workflow';
2222
import { redactSecrets } from './redact-secrets';
2323

2424
/**
@@ -200,32 +200,27 @@ program
200200
});
201201

202202
try {
203-
// Step 0: Setup host-level network and iptables
204-
logger.info('Setting up host-level firewall network and iptables rules...');
205-
const networkConfig = await ensureFirewallNetwork();
206-
await setupHostIptables(networkConfig.squidIp, 3128);
207-
hostIptablesSetup = true;
208-
209-
// Step 1: Write configuration files
210-
logger.info('Generating configuration files...');
211-
await writeConfigs(config);
212-
213-
// Step 2: Start containers
214-
await startContainers(config.workDir, config.allowedDomains);
215-
containersStarted = true;
216-
217-
// Step 3: Wait for copilot to complete
218-
const result = await runCopilotCommand(config.workDir, config.allowedDomains);
219-
exitCode = result.exitCode;
220-
221-
// Step 4: Cleanup (logs will be preserved automatically if they exist)
222-
await performCleanup();
203+
exitCode = await runMainWorkflow(
204+
config,
205+
{
206+
ensureFirewallNetwork,
207+
setupHostIptables,
208+
writeConfigs,
209+
startContainers,
210+
runCopilotCommand,
211+
},
212+
{
213+
logger,
214+
performCleanup,
215+
onHostIptablesSetup: () => {
216+
hostIptablesSetup = true;
217+
},
218+
onContainersStarted: () => {
219+
containersStarted = true;
220+
},
221+
}
222+
);
223223

224-
if (exitCode === 0) {
225-
logger.success(`Command completed successfully`);
226-
} else {
227-
logger.warn(`Command completed with exit code: ${exitCode}`);
228-
}
229224
process.exit(exitCode);
230225
} catch (error) {
231226
logger.error('Fatal error:', error);

0 commit comments

Comments
 (0)