Skip to content

Commit 641885e

Browse files
test: add basic tests to check app-server communication
1 parent ab1cd10 commit 641885e

11 files changed

Lines changed: 306 additions & 7 deletions

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"package:darwin-arm64": "cd dist/bin && zip codex-acp-arm64-darwin.zip codex-acp-arm64-darwin",
1818
"package:win-x64": "cd dist/bin && zip codex-acp-x64-windows.zip codex-acp-x64-windows.exe",
1919
"start": "bun run src/index.ts",
20-
"generate-types": "codex app-server generate-ts --out src/app-server",
20+
"generate-types": "./node_modules/.bin/codex app-server generate-ts --out src/app-server",
2121
"test": "vitest run",
2222
"test:watch": "vitest",
2323
"typecheck": "tsc --noEmit"
@@ -28,6 +28,7 @@
2828
"type": "module",
2929
"devDependencies": {
3030
"@types/node": "^24.10.1",
31+
"@openai/codex": "^0.65.0",
3132
"tsx": "^4.20.6",
3233
"typescript": "^5.9.3",
3334
"vitest": "^4.0.10"

src/CodexAcpClient.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,14 @@ export class CodexAcpClient {
5858
return result.success;
5959
}
6060

61+
async logout(): Promise<void> {
62+
await this.codexClient.accountLogout();
63+
await this.codexClient.awaitAccountUpdated();
64+
}
65+
6166
async authRequired(): Promise<Boolean> {
6267
const response = await this.codexClient.accountRead({refreshToken: false})
63-
return response.requiresOpenaiAuth || !response.account;
68+
return response.requiresOpenaiAuth && !response.account;
6469
}
6570

6671
/**

src/CodexAppServerClient.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import type {
77
ServerNotification
88
} from "./app-server";
99
import type {
10-
AccountLoginCompletedNotification,
10+
AccountLoginCompletedNotification, AccountUpdatedNotification,
1111
GetAccountParams,
12-
GetAccountResponse, LoginAccountParams, LoginAccountResponse,
12+
GetAccountResponse, LoginAccountParams, LoginAccountResponse, LogoutAccountResponse,
1313
ThreadStartParams,
1414
ThreadStartResponse,
1515
TurnCompletedNotification,
@@ -26,6 +26,11 @@ export class CodexAppServerClient {
2626

2727
constructor(connection: MessageConnection) {
2828
this.connection = connection;
29+
this.onServerNotification((notification) => {
30+
for (const callback of this.transportEventHandlers) {
31+
callback({ eventType: "notification", ...notification});
32+
}
33+
});
2934
}
3035

3136
async initialize(params: InitializeParams): Promise<InitializeResponse> {
@@ -44,6 +49,10 @@ export class CodexAppServerClient {
4449
return await this.sendRequest({ method: "account/login/start", params: params });
4550
}
4651

52+
async accountLogout(): Promise<LogoutAccountResponse> {
53+
return await this.sendRequest({ method: "account/logout", params: undefined });
54+
}
55+
4756
async awaitLoginCompleted(): Promise<AccountLoginCompletedNotification> {
4857
return await new Promise((resolve) => {
4958
this.connection.onNotification("account/login/completed", (event: AccountLoginCompletedNotification) => {
@@ -52,6 +61,14 @@ export class CodexAppServerClient {
5261
});
5362
}
5463

64+
async awaitAccountUpdated(): Promise<AccountUpdatedNotification> {
65+
return await new Promise((resolve) => {
66+
this.connection.onNotification("account/updated", (event: AccountUpdatedNotification) => {
67+
resolve(event);
68+
});
69+
});
70+
}
71+
5572
async accountRead(params: GetAccountParams): Promise<GetAccountResponse> {
5673
return await this.sendRequest({ method: "account/read", params: params });
5774
}
@@ -87,11 +104,31 @@ export class CodexAppServerClient {
87104
return (params as { msg?: EventMsg })?.msg ?? null;
88105
}
89106

107+
private transportEventHandlers: Array<(event: ClientTransportEvent) => void> = [];
108+
onClientTransportEvent(callback: (event: ClientTransportEvent) => void){
109+
this.transportEventHandlers.push(callback);
110+
}
111+
90112
private async sendRequest<R>(request: CodexRequest): Promise<R> {
91-
return await this.connection.sendRequest(request.method, request.params);
113+
for (const callback of this.transportEventHandlers) {
114+
callback({ eventType: "request", ...request});
115+
}
116+
let result: any;
117+
if (request.params) {
118+
result = await this.connection.sendRequest<R>(request.method, request.params)
119+
}
120+
else {
121+
await this.connection.sendRequest<R>(request.method);
122+
}
123+
for (const callback of this.transportEventHandlers) {
124+
callback({ eventType: "response", ...result});
125+
}
126+
return result;
92127
}
93128
}
94129

130+
export type ClientTransportEvent = { eventType: "request" } & CodexRequest | { eventType: "response" } & unknown | { eventType: "notification" } & ServerNotification;
131+
95132
type CodexRequest = DistributiveOmit<ClientRequest, "id">
96133

97134
type DistributiveOmit<T, K extends keyof any> = T extends any
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {describe, expect, it, vi, beforeEach} from 'vitest';
2+
import type {CodexAuthRequest} from "../../CodexAuthMethod";
3+
import {createTestFixture, type TestFixture} from "../acp-test-utils";
4+
5+
describe('ACP server test', () => {
6+
7+
let fixture: TestFixture;
8+
beforeEach(() => {
9+
fixture = createTestFixture();
10+
vi.clearAllMocks();
11+
});
12+
13+
const ignoredFields = ["thread", "cwd", "id", "createdAt", "path", "threadId", "userAgent"];
14+
15+
it('should start conversation', async () => {
16+
const codexAcpAgent = fixture.getCodexAcpAgent();
17+
await codexAcpAgent.initialize({protocolVersion: 1});
18+
19+
fixture.getCodexAcpClient().authRequired = vi.fn().mockResolvedValue(false);
20+
const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
21+
codexAcpAgent.prompt({ sessionId: newSessionResponse.sessionId, prompt: [{type: "text", text: "Hi!"}] });
22+
23+
const transportDump = fixture.getTransportDump(ignoredFields);
24+
await expect(transportDump).toMatchFileSnapshot("data/start-conversation.json");
25+
});
26+
27+
it('should throw error without authentication', async () => {
28+
const codexAcpAgent = fixture.getCodexAcpAgent();
29+
30+
await codexAcpAgent.initialize({protocolVersion: 1});
31+
await fixture.getCodexAcpClient().logout();
32+
fixture.clearTransportDump();
33+
34+
await expect(
35+
codexAcpAgent.newSession({cwd: "", mcpServers: []})
36+
).rejects.toThrow("Authentication required");
37+
38+
const transportDump = fixture.getTransportDump(ignoredFields);
39+
await expect(transportDump).toMatchFileSnapshot("data/auth-failed.json");
40+
});
41+
42+
it('should authenticate with key', async () => {
43+
const codexAcpAgent = fixture.getCodexAcpAgent();
44+
45+
await codexAcpAgent.initialize({protocolVersion: 1});
46+
await fixture.getCodexAcpClient().logout();
47+
fixture.clearTransportDump();
48+
49+
const authRequest: CodexAuthRequest = { methodId: "api-key", _meta: {apiKey: "TOKEN"} }
50+
await codexAcpAgent.authenticate(authRequest);
51+
const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
52+
expect(newSessionResponse.sessionId).toBeDefined()
53+
54+
const transportDump = fixture.getTransportDump(ignoredFields);
55+
await expect(transportDump).toMatchFileSnapshot("data/auth-with-key.json");
56+
});
57+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"eventType": "request",
3+
"method": "account/read",
4+
"params": {
5+
"refreshToken": false
6+
}
7+
}
8+
{
9+
"eventType": "response",
10+
"account": null,
11+
"requiresOpenaiAuth": true
12+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"eventType": "request",
3+
"method": "account/login/start",
4+
"params": {
5+
"type": "apiKey",
6+
"apiKey": "TOKEN"
7+
}
8+
}
9+
{
10+
"eventType": "response",
11+
"type": "apiKey"
12+
}
13+
{
14+
"eventType": "request",
15+
"method": "account/read",
16+
"params": {
17+
"refreshToken": false
18+
}
19+
}
20+
{
21+
"eventType": "response",
22+
"account": {
23+
"type": "apiKey"
24+
},
25+
"requiresOpenaiAuth": true
26+
}
27+
{
28+
"eventType": "request",
29+
"method": "thread/start",
30+
"params": {
31+
"config": null,
32+
"modelProvider": null,
33+
"model": null,
34+
"cwd": "cwd",
35+
"approvalPolicy": "never",
36+
"sandbox": null,
37+
"baseInstructions": null,
38+
"developerInstructions": null
39+
}
40+
}
41+
{
42+
"eventType": "response",
43+
"thread": "thread",
44+
"model": "gpt-5.1-codex-max",
45+
"modelProvider": "openai",
46+
"cwd": "cwd",
47+
"approvalPolicy": "never",
48+
"sandbox": {
49+
"type": "workspaceWrite",
50+
"writableRoots": [],
51+
"networkAccess": false,
52+
"excludeTmpdirEnvVar": false,
53+
"excludeSlashTmp": false
54+
},
55+
"reasoningEffort": "medium"
56+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"eventType": "request",
3+
"method": "initialize",
4+
"params": {
5+
"clientInfo": {
6+
"name": "codex-acp",
7+
"version": "0.0.5",
8+
"title": "Codex ACP"
9+
}
10+
}
11+
}
12+
{
13+
"eventType": "response",
14+
"userAgent": "userAgent"
15+
}
16+
{
17+
"eventType": "request",
18+
"method": "thread/start",
19+
"params": {
20+
"config": null,
21+
"modelProvider": null,
22+
"model": null,
23+
"cwd": "cwd",
24+
"approvalPolicy": "never",
25+
"sandbox": null,
26+
"baseInstructions": null,
27+
"developerInstructions": null
28+
}
29+
}
30+
{
31+
"eventType": "response",
32+
"thread": "thread",
33+
"model": "gpt-5.1-codex-max",
34+
"modelProvider": "openai",
35+
"cwd": "cwd",
36+
"approvalPolicy": "never",
37+
"sandbox": {
38+
"type": "workspaceWrite",
39+
"writableRoots": [],
40+
"networkAccess": false,
41+
"excludeTmpdirEnvVar": false,
42+
"excludeSlashTmp": false
43+
},
44+
"reasoningEffort": "medium"
45+
}
46+
{
47+
"eventType": "request",
48+
"method": "turn/start",
49+
"params": {
50+
"threadId": "threadId",
51+
"input": [
52+
{
53+
"type": "text",
54+
"text": "Hi!"
55+
}
56+
],
57+
"approvalPolicy": null,
58+
"sandboxPolicy": null,
59+
"summary": null,
60+
"cwd": "cwd",
61+
"effort": null,
62+
"model": null
63+
}
64+
}

src/__tests__/CodexACPAgent/initialize.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ describe('CodexACPAgent - initialize', () => {
2929
protocolVersion: acp.PROTOCOL_VERSION
3030
};
3131
const result = await agent.initialize(params);
32-
3332
expect(result).toEqual({
3433
protocolVersion: acp.PROTOCOL_VERSION,
3534
agentCapabilities: {

0 commit comments

Comments
 (0)