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" ;
53import fs from "node:fs" ;
64import os from "node:os" ;
75import 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
1335class 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
0 commit comments