Skip to content

Commit 369d3b4

Browse files
committed
fixup! LLM-25904 Add e2e test checking actual codex interaction
1 parent 07e7048 commit 369d3b4

3 files changed

Lines changed: 90 additions & 64 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"start": "bun run src/index.ts",
3333
"generate-types": "./node_modules/.bin/codex app-server generate-ts --out src/app-server",
3434
"test": "vitest run",
35-
"test:e2e": "npm run build && RUN_E2E_TESTS=true vitest run src/__tests__/CodexACPAgent/acp-e2e.test.ts",
35+
"test:e2e": "npm run build && RUN_E2E_TESTS=true vitest run src/__tests__/CodexACPAgent/e2e",
3636
"test:watch": "vitest",
3737
"typecheck": "tsc --noEmit",
3838
"codex-test": "tsx .claude/skills/run-codex/scripts/run-codex-test.ts"

src/__tests__/CodexACPAgent/acp-e2e.test.ts renamed to src/__tests__/CodexACPAgent/e2e/acp-e2e-test-utils.ts

Lines changed: 52 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
1-
import {afterEach, describe, expect, it, vi} from 'vitest';
2-
import * as acp from '@agentclientprotocol/sdk';
3-
import {type ChildProcessWithoutNullStreams, spawn} from 'node:child_process';
4-
import {Readable, Writable} from 'node:stream';
1+
import * as acp from "@agentclientprotocol/sdk";
2+
import {type ChildProcessWithoutNullStreams, spawn} from "node:child_process";
53
import fs from "node:fs";
64
import os from "node:os";
75
import path from "node:path";
8-
import {removeDirectoryWithRetry} from "../acp-test-utils";
6+
import {Readable, Writable} from "node:stream";
7+
import {describe, vi} from "vitest";
8+
import {removeDirectoryWithRetry} from "../../acp-test-utils";
99

10-
const RUN_E2E_TESTS = process.env["RUN_E2E_TESTS"] === "true";
11-
const TEST_TIMEOUT_MS = 90_000;
10+
export const RUN_E2E_TESTS = process.env["RUN_E2E_TESTS"] === "true";
11+
const DEFAULT_E2E_SUITE_TIMEOUT_MS = 60_000;
12+
13+
export interface SpawnedAgentFixture {
14+
expectPromptText(promptText: string, assertText: (text: string) => void, timeoutMs?: number): Promise<void>;
15+
dispose(): Promise<void>;
16+
}
17+
18+
export function describeE2E(name: string, factory: () => void, timeoutMs = DEFAULT_E2E_SUITE_TIMEOUT_MS): void {
19+
describe.skipIf(!RUN_E2E_TESTS)(name, {timeout: timeoutMs}, factory);
20+
}
21+
22+
interface TestSkill {
23+
readonly name: string;
24+
readonly description: string;
25+
readonly body: string;
26+
}
27+
28+
interface RuntimePaths {
29+
readonly rootDir: string;
30+
readonly codexHome: string;
31+
readonly workspaceDir: string;
32+
readonly appServerLogsDir: string;
33+
}
1234

1335
class RecordingClient implements acp.Client {
1436
private readonly textBySessionId = new Map<string, string>();
@@ -33,61 +55,15 @@ class RecordingClient implements acp.Client {
3355
}
3456
}
3557

36-
interface SpawnedAgentFixture {
37-
expectPromptText(promptText: string, assertText: (text: string) => void, timeoutMs?: number): Promise<void>;
38-
dispose(): Promise<void>;
39-
}
40-
41-
interface TestSkill {
42-
readonly name: string;
43-
readonly description: string;
44-
readonly body: string;
45-
}
46-
47-
interface RuntimePaths {
48-
readonly rootDir: string;
49-
readonly codexHome: string;
50-
readonly workspaceDir: string;
51-
readonly appServerLogsDir: string;
52-
}
53-
54-
describe.skipIf(!RUN_E2E_TESTS)('E2E tests', {timeout: TEST_TIMEOUT_MS}, () => {
55-
let fixture: SpawnedAgentFixture | null = null;
56-
57-
afterEach(async () => {
58-
if (fixture) {
59-
await fixture.dispose();
60-
fixture = null;
61-
}
62-
});
63-
64-
it('returns model response', async () => {
65-
fixture = await createAuthenticatedFixture();
66-
await fixture.expectPromptText("Reply with exactly integration-ok and nothing else.", (text) => {
67-
expect(text.toLowerCase()).toContain("integration-ok");
68-
});
69-
});
70-
71-
it('lists a user skill from the wrapped CODEX_HOME', async () => {
72-
fixture = await createFixtureWithSkill({
73-
name: "integration-skill",
74-
description: "Integration skill",
75-
body: "This skill exists only for integration testing.",
76-
});
77-
await fixture.expectPromptText("/skills", (text) => {
78-
expect(text).toContain("Available skills:");
79-
expect(text).toContain("- integration-skill: Integration skill");
80-
});
81-
});
82-
});
83-
84-
async function createFixtureWithSkill(skill: TestSkill): Promise<SpawnedAgentFixture> {
58+
export async function createFixtureWithSkill(skill: TestSkill): Promise<SpawnedAgentFixture> {
8559
const runtimePaths = createTemporaryRuntimePaths();
8660
writeSkill(runtimePaths.codexHome, skill);
8761
return await createAuthenticatedFixture(runtimePaths);
8862
}
8963

90-
async function createAuthenticatedFixture(runtimePaths = createTemporaryRuntimePaths()): Promise<SpawnedAgentFixture> {
64+
export async function createAuthenticatedFixture(
65+
runtimePaths = createTemporaryRuntimePaths()
66+
): Promise<SpawnedAgentFixture> {
9167
const apiKey = requireLiveApiKey();
9268
const agentProcess = spawn("npm", ["run", "--silent", "start"], {
9369
cwd: process.cwd(),
@@ -114,8 +90,14 @@ async function createAuthenticatedFixture(runtimePaths = createTemporaryRuntimeP
11490
version: "1.0.0",
11591
},
11692
});
117-
expect(initializeResponse.protocolVersion).toBe(acp.PROTOCOL_VERSION);
118-
expect(initializeResponse.authMethods?.map((method) => method.id)).toContain("api-key");
93+
94+
if (initializeResponse.protocolVersion !== acp.PROTOCOL_VERSION) {
95+
throw new Error(`Unexpected protocol version: ${initializeResponse.protocolVersion}`);
96+
}
97+
98+
if (!initializeResponse.authMethods?.some((method) => method.id === "api-key")) {
99+
throw new Error("API key authentication is not available.");
100+
}
119101

120102
await connection.authenticate({
121103
methodId: "api-key",
@@ -126,7 +108,10 @@ async function createAuthenticatedFixture(runtimePaths = createTemporaryRuntimeP
126108
},
127109
});
128110

129-
expect(await getAuthenticationStatus(connection)).toEqual({type: "api-key"});
111+
const authenticationStatus = await getAuthenticationStatus(connection);
112+
if (authenticationStatus["type"] !== "api-key") {
113+
throw new Error(`Unexpected authentication status: ${JSON.stringify(authenticationStatus)}`);
114+
}
130115

131116
return {
132117
async expectPromptText(promptText: string, assertText: (text: string) => void, timeoutMs = 30_000): Promise<void> {
@@ -143,13 +128,15 @@ async function createAuthenticatedFixture(runtimePaths = createTemporaryRuntimeP
143128
}],
144129
});
145130

146-
expect(promptResponse.stopReason).toBe("end_turn");
131+
if (promptResponse.stopReason !== "end_turn") {
132+
throw new Error(`Unexpected stop reason: ${promptResponse.stopReason}`);
133+
}
147134

148135
await vi.waitFor(() => {
149136
assertText(client.readText(newSessionResponse.sessionId));
150137
}, {timeout: timeoutMs});
151138
},
152-
async dispose() {
139+
async dispose(): Promise<void> {
153140
if (!agentProcess.stdin.destroyed && !agentProcess.stdin.writableEnded) {
154141
agentProcess.stdin.end();
155142
}
@@ -179,10 +166,12 @@ function createTemporaryRuntimePaths(): RuntimePaths {
179166
const codexHome = path.join(rootDir, "codex-home");
180167
const workspaceDir = path.join(rootDir, "workspace");
181168
const appServerLogsDir = path.join(rootDir, "logs");
169+
182170
fs.mkdirSync(codexHome, {recursive: true});
183171
fs.mkdirSync(workspaceDir, {recursive: true});
184172
fs.mkdirSync(appServerLogsDir, {recursive: true});
185173
fs.writeFileSync(path.join(codexHome, "config.toml"), 'cli_auth_credentials_store = "file"\n', "utf8");
174+
186175
return {
187176
rootDir,
188177
codexHome,
@@ -223,7 +212,7 @@ function printLogDirectory(logDirectory: string): void {
223212
const content = fs.readFileSync(logFilePath, "utf8").trim();
224213
console.log(`[APP_SERVER_LOGS] Logs from ${logFilePath}:`);
225214
console.log(content.length > 0 ? content : "[APP_SERVER_LOGS] Log file is empty");
226-
console.log('------');
215+
console.log("------");
227216
});
228217
}
229218

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {afterEach, expect, it} from "vitest";
2+
import {
3+
createAuthenticatedFixture,
4+
createFixtureWithSkill,
5+
describeE2E,
6+
type SpawnedAgentFixture,
7+
} from "./acp-e2e-test-utils";
8+
9+
describeE2E("E2E tests", () => {
10+
let fixture: SpawnedAgentFixture | null = null;
11+
12+
afterEach(async () => {
13+
if (fixture) {
14+
await fixture.dispose();
15+
fixture = null;
16+
}
17+
});
18+
19+
it('returns model response', async () => {
20+
fixture = await createAuthenticatedFixture();
21+
await fixture.expectPromptText("Reply with exactly integration-ok and nothing else.", (text) => {
22+
expect(text.toLowerCase()).toContain("integration-ok");
23+
});
24+
});
25+
26+
it('lists a user skill from the wrapped CODEX_HOME', async () => {
27+
fixture = await createFixtureWithSkill({
28+
name: "integration-skill",
29+
description: "Integration skill",
30+
body: "This skill exists only for integration testing.",
31+
});
32+
await fixture.expectPromptText("/skills", (text) => {
33+
expect(text).toContain("Available skills:");
34+
expect(text).toContain("- integration-skill: Integration skill");
35+
});
36+
});
37+
});

0 commit comments

Comments
 (0)