Skip to content

Commit d3a3c23

Browse files
authored
feat: add e2e tests for dev server lifecycle (#734)
1 parent 7f4c1bb commit d3a3c23

3 files changed

Lines changed: 122 additions & 2 deletions

File tree

e2e-tests/dev-lifecycle.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { waitForServerReady } from '../src/cli/operations/dev/utils.js';
2+
import { cleanSpawnEnv, parseJsonOutput, spawnAndCollect } from '../src/test-utils/index.js';
3+
import { baseCanRun as canRun, runAgentCoreCLI } from './e2e-helper.js';
4+
import { type ChildProcess, spawn } from 'node:child_process';
5+
import { randomUUID } from 'node:crypto';
6+
import { mkdir, rm } from 'node:fs/promises';
7+
import { tmpdir } from 'node:os';
8+
import { join } from 'node:path';
9+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
10+
import { z } from 'zod';
11+
12+
const DEV_SERVER_PORT = 18080;
13+
const DEV_SERVER_PORT_STR = String(DEV_SERVER_PORT);
14+
15+
describe.sequential('e2e: dev server lifecycle', () => {
16+
let testDir: string;
17+
let projectPath: string;
18+
let serverProcess: ChildProcess | null = null;
19+
20+
beforeAll(async () => {
21+
if (!canRun) return;
22+
23+
testDir = join(tmpdir(), `agentcore-e2e-dev-${randomUUID()}`);
24+
await mkdir(testDir, { recursive: true });
25+
26+
const result = await runAgentCoreCLI(
27+
[
28+
'create',
29+
'--name',
30+
'DevTest',
31+
'--language',
32+
'Python',
33+
'--framework',
34+
'Strands',
35+
'--model-provider',
36+
'Bedrock',
37+
'--memory',
38+
'none',
39+
'--json',
40+
],
41+
testDir
42+
);
43+
expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
44+
projectPath = z.object({ projectPath: z.string() }).parse(parseJsonOutput(result.stdout)).projectPath;
45+
}, 120000);
46+
47+
afterAll(async () => {
48+
if (serverProcess?.pid) {
49+
try {
50+
process.kill(-serverProcess.pid, 'SIGTERM');
51+
} catch {
52+
// Process group already exited
53+
}
54+
await new Promise<void>(resolve => {
55+
serverProcess?.on('exit', () => resolve());
56+
setTimeout(resolve, 5000);
57+
});
58+
serverProcess = null;
59+
}
60+
if (testDir) {
61+
await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
62+
}
63+
}, 30000);
64+
65+
it.skipIf(!canRun)(
66+
'dev --logs starts the server and accepts connections',
67+
async () => {
68+
serverProcess = spawn('agentcore', ['dev', '--logs', '--port', DEV_SERVER_PORT_STR], {
69+
cwd: projectPath,
70+
stdio: 'pipe',
71+
detached: true,
72+
env: cleanSpawnEnv(),
73+
});
74+
75+
const ready = await waitForServerReady(DEV_SERVER_PORT, 90000);
76+
expect(ready, 'Dev server should accept connections').toBe(true);
77+
},
78+
120000
79+
);
80+
81+
it.skipIf(!canRun)(
82+
'dev invokes the running server and returns a response',
83+
async () => {
84+
const result = await spawnAndCollect(
85+
'agentcore',
86+
['dev', 'What is 2 plus 2? Reply with just the number.', '--port', DEV_SERVER_PORT_STR],
87+
projectPath
88+
);
89+
90+
expect(result.exitCode, `Invoke failed (exit ${result.exitCode}): ${result.stderr}`).toBe(0);
91+
expect(result.stdout.length, 'Should produce a response').toBeGreaterThan(0);
92+
},
93+
60000
94+
);
95+
96+
it.skipIf(!canRun)(
97+
'dev --stream returns a response',
98+
async () => {
99+
const result = await spawnAndCollect(
100+
'agentcore',
101+
['dev', 'Say hello', '--stream', '--port', DEV_SERVER_PORT_STR],
102+
projectPath
103+
);
104+
105+
expect(result.exitCode, `Stream invoke failed (exit ${result.exitCode}): ${result.stderr}`).toBe(0);
106+
expect(result.stdout.length, 'Should produce output').toBeGreaterThan(0);
107+
},
108+
60000
109+
);
110+
});

src/test-utils/cli-runner.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ export interface RunResult {
1313
exitCode: number;
1414
}
1515

16+
/**
17+
* Build a clean env for spawned CLI processes.
18+
* Strips INIT_CWD which npm/npx sets to the runner's directory — without this,
19+
* the CLI resolves the working directory from INIT_CWD instead of the spawn's cwd.
20+
* @see https://docs.npmjs.com/cli/v10/commands/npm-run-script
21+
*/
22+
export function cleanSpawnEnv(extraEnv: Record<string, string> = {}): NodeJS.ProcessEnv {
23+
return { ...process.env, INIT_CWD: undefined, ...extraEnv };
24+
}
25+
1626
/**
1727
* Spawn a command, collect output, and strip ANSI codes.
1828
*/
@@ -25,7 +35,7 @@ export function spawnAndCollect(
2535
return new Promise(resolve => {
2636
const proc = spawn(command, args, {
2737
cwd,
28-
env: { ...process.env, INIT_CWD: undefined, ...extraEnv },
38+
env: cleanSpawnEnv(extraEnv),
2939
});
3040

3141
let stdout = '';

src/test-utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Import these helpers instead of duplicating code in each test file.
44
*/
55

6-
export { runCLI, spawnAndCollect, type RunResult } from './cli-runner.js';
6+
export { runCLI, spawnAndCollect, cleanSpawnEnv, type RunResult } from './cli-runner.js';
77
export { exists } from './fs-helpers.js';
88
export { hasCommand, hasAwsCredentials, prereqs } from './prereqs.js';
99
export { createTestProject, type TestProject, type CreateTestProjectOptions } from './project-factory.js';

0 commit comments

Comments
 (0)