@theia/ai-codex 1.67.0-next.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,672 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
18
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
19
+ let disableJSDOM = enableJSDOM();
20
+ FrontendApplicationConfigProvider.set({});
21
+
22
+ import { expect } from 'chai';
23
+ import * as sinon from 'sinon';
24
+ import { Container } from '@theia/core/shared/inversify';
25
+ import { TokenUsageService } from '@theia/ai-core';
26
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
27
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
28
+ import { URI } from '@theia/core/lib/common/uri';
29
+ import { ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
30
+ import { ChatAgentLocation, MarkdownChatResponseContentImpl, ThinkingChatResponseContentImpl, ErrorChatResponseContentImpl, MutableChatRequestModel } from '@theia/ai-chat';
31
+ import { CodexFrontendService } from './codex-frontend-service';
32
+ import { CodexChatAgent, CODEX_CHAT_AGENT_ID, CODEX_TOOL_CALLS_KEY, CODEX_INPUT_TOKENS_KEY, CODEX_OUTPUT_TOKENS_KEY } from './codex-chat-agent';
33
+
34
+ import type {
35
+ CommandExecutionItem,
36
+ FileChangeItem,
37
+ McpToolCallItem,
38
+ WebSearchItem,
39
+ TodoListItem,
40
+ AgentMessageItem,
41
+ ItemCompletedEvent,
42
+ TurnCompletedEvent,
43
+ ThreadEvent
44
+ } from '@openai/codex-sdk';
45
+
46
+ disableJSDOM();
47
+
48
+ /**
49
+ * Helper interface to access protected methods for testing purposes.
50
+ * This avoids using 'as any' casts when testing protected methods.
51
+ */
52
+ interface CodexChatAgentTestAccess {
53
+ getToolCalls(request: MutableChatRequestModel): Map<string, unknown>;
54
+ isToolInvocation(item: unknown): boolean;
55
+ extractToolArguments(item: CommandExecutionItem | FileChangeItem | McpToolCallItem | WebSearchItem | TodoListItem): string;
56
+ extractSandboxMode(modeId?: string): 'read-only' | 'workspace-write' | 'danger-full-access';
57
+ updateTokens(request: MutableChatRequestModel, inputTokens: number, outputTokens: number): void;
58
+ getSessionTotalTokens(request: MutableChatRequestModel): { inputTokens: number; outputTokens: number };
59
+ }
60
+
61
+ describe('CodexChatAgent', () => {
62
+ let container: Container;
63
+ let mockRequest: MutableChatRequestModel;
64
+
65
+ before(async () => {
66
+ disableJSDOM = enableJSDOM();
67
+ });
68
+
69
+ beforeEach(() => {
70
+ container = new Container();
71
+
72
+ const mockCodexService = {
73
+ send: sinon.stub()
74
+ } as unknown as CodexFrontendService;
75
+
76
+ const mockTokenUsageService = {
77
+ recordTokenUsage: sinon.stub().resolves(),
78
+ getTokenUsages: sinon.stub().resolves([]),
79
+ setClient: sinon.stub()
80
+ };
81
+
82
+ const mockFileService = {
83
+ exists: sinon.stub().resolves(true),
84
+ read: sinon.stub().resolves({ value: { toString: () => 'content' } })
85
+ } as unknown as FileService;
86
+
87
+ const mockWorkspaceService = {
88
+ roots: Promise.resolve([{ resource: new URI('file:///test') }])
89
+ } as unknown as WorkspaceService;
90
+
91
+ const mockFileChangeFactory = sinon.stub();
92
+
93
+ container.bind(CodexFrontendService).toConstantValue(mockCodexService);
94
+ container.bind(TokenUsageService).toConstantValue(mockTokenUsageService);
95
+ container.bind(FileService).toConstantValue(mockFileService);
96
+ container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
97
+ container.bind(ChangeSetFileElementFactory).toConstantValue(mockFileChangeFactory);
98
+ container.bind(CodexChatAgent).toSelf();
99
+
100
+ const addContentStub = sinon.stub();
101
+ const responseContentChangedStub = sinon.stub();
102
+ const completeStub = sinon.stub();
103
+ const errorStub = sinon.stub();
104
+ const getRequestsStub = sinon.stub().returns([]);
105
+ const setSuggestionsStub = sinon.stub();
106
+ const addDataStub = sinon.stub();
107
+ const getDataByKeyStub = sinon.stub();
108
+
109
+ mockRequest = {
110
+ id: 'test-request-id',
111
+ request: { text: 'test prompt' },
112
+ session: {
113
+ id: 'test-session-id',
114
+ getRequests: getRequestsStub,
115
+ setSuggestions: setSuggestionsStub
116
+ },
117
+ response: {
118
+ response: {
119
+ addContent: addContentStub,
120
+ responseContentChanged: responseContentChangedStub
121
+ },
122
+ complete: completeStub,
123
+ error: errorStub,
124
+ cancellationToken: { isCancellationRequested: false }
125
+ },
126
+ addData: addDataStub,
127
+ getDataByKey: getDataByKeyStub
128
+ } as unknown as MutableChatRequestModel;
129
+ });
130
+
131
+ afterEach(() => {
132
+ sinon.restore();
133
+ });
134
+
135
+ after(() => {
136
+ disableJSDOM();
137
+ });
138
+
139
+ function createAgentMessageEvent(text: string, id: string = 'msg-1'): ItemCompletedEvent {
140
+ return {
141
+ type: 'item.completed',
142
+ item: {
143
+ type: 'agent_message',
144
+ id,
145
+ text
146
+ } as AgentMessageItem
147
+ };
148
+ }
149
+
150
+ function createReasoningEvent(text: string, id: string = 'reason-1'): ItemCompletedEvent {
151
+ return {
152
+ type: 'item.completed',
153
+ item: {
154
+ type: 'reasoning',
155
+ id,
156
+ text
157
+ }
158
+ };
159
+ }
160
+
161
+ function createCommandExecutionCompletedEvent(command: string, exitCode: number, output: string, id: string = 'cmd-1'): ItemCompletedEvent {
162
+ return {
163
+ type: 'item.completed',
164
+ item: {
165
+ type: 'command_execution',
166
+ id,
167
+ command,
168
+ status: 'completed' as const,
169
+ exit_code: exitCode,
170
+ aggregated_output: output
171
+ } as unknown as CommandExecutionItem
172
+ };
173
+ }
174
+
175
+ function createTurnCompletedEvent(inputTokens: number, outputTokens: number): TurnCompletedEvent {
176
+ return {
177
+ type: 'turn.completed',
178
+ usage: {
179
+ input_tokens: inputTokens,
180
+ output_tokens: outputTokens,
181
+ cached_input_tokens: 0
182
+ }
183
+ };
184
+ }
185
+
186
+ async function* createMockStream(events: ThreadEvent[]): AsyncIterable<ThreadEvent> {
187
+ for (const event of events) {
188
+ yield event;
189
+ }
190
+ }
191
+
192
+ describe('agent metadata', () => {
193
+ it('should have correct id', () => {
194
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
195
+ expect(agent.id).to.equal(CODEX_CHAT_AGENT_ID);
196
+ });
197
+
198
+ it('should have correct name', () => {
199
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
200
+ expect(agent.name).to.equal('Codex');
201
+ });
202
+
203
+ it('should have correct description', () => {
204
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
205
+ expect(agent.description).to.include('OpenAI');
206
+ expect(agent.description).to.include('Codex');
207
+ });
208
+
209
+ it('should support all chat locations', () => {
210
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
211
+ expect(agent.locations).to.deep.equal(ChatAgentLocation.ALL);
212
+ });
213
+
214
+ it('should have three modes', () => {
215
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
216
+ expect(agent.modes).to.have.lengthOf(3);
217
+ expect(agent.modes![0].id).to.equal('workspace-write');
218
+ expect(agent.modes![1].id).to.equal('read-only');
219
+ expect(agent.modes![2].id).to.equal('danger-full-access');
220
+ });
221
+ });
222
+
223
+ describe('invoke() integration tests', () => {
224
+ it('should process agent_message events through invoke', async () => {
225
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
226
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
227
+
228
+ const events = [
229
+ createAgentMessageEvent('Hello, I can help you with that.'),
230
+ createTurnCompletedEvent(50, 25)
231
+ ];
232
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
233
+
234
+ await agent.invoke(mockRequest);
235
+
236
+ const addContentStub = (mockRequest.response.response.addContent as sinon.SinonStub);
237
+ const completeStub = (mockRequest.response.complete as sinon.SinonStub);
238
+ expect(addContentStub.calledOnce).to.be.true;
239
+ const addedContent = addContentStub.firstCall.args[0];
240
+ expect(addedContent).to.be.instanceOf(MarkdownChatResponseContentImpl);
241
+ expect(addedContent.content.value).to.equal('Hello, I can help you with that.');
242
+ expect(completeStub.calledOnce).to.be.true;
243
+ });
244
+
245
+ it('should process reasoning events through invoke', async () => {
246
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
247
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
248
+
249
+ const events = [
250
+ createReasoningEvent('Let me think about the best approach...'),
251
+ createTurnCompletedEvent(30, 15)
252
+ ];
253
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
254
+
255
+ await agent.invoke(mockRequest);
256
+
257
+ const addContentStub = (mockRequest.response.response.addContent as sinon.SinonStub);
258
+ const completeStub = (mockRequest.response.complete as sinon.SinonStub);
259
+ expect(addContentStub.calledOnce).to.be.true;
260
+ const addedContent = addContentStub.firstCall.args[0];
261
+ expect(addedContent).to.be.instanceOf(ThinkingChatResponseContentImpl);
262
+ expect(addedContent.content).to.equal('Let me think about the best approach...');
263
+ expect(completeStub.calledOnce).to.be.true;
264
+ });
265
+
266
+ it('should process command_execution tool calls through invoke', async () => {
267
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
268
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
269
+
270
+ (mockRequest.getDataByKey as sinon.SinonStub).withArgs(CODEX_TOOL_CALLS_KEY).returns(undefined);
271
+
272
+ const events = [
273
+ {
274
+ type: 'item.started' as const,
275
+ item: {
276
+ type: 'command_execution' as const,
277
+ id: 'cmd-1',
278
+ command: 'npm test',
279
+ status: 'running' as const,
280
+ aggregated_output: ''
281
+ } as unknown as CommandExecutionItem
282
+ },
283
+ createCommandExecutionCompletedEvent('npm test', 0, 'All tests passed'),
284
+ createTurnCompletedEvent(100, 50)
285
+ ];
286
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
287
+
288
+ await agent.invoke(mockRequest);
289
+
290
+ // Should add tool call twice: once on start (pending), once on completion
291
+ const addContentStub = (mockRequest.response.response.addContent as sinon.SinonStub);
292
+ const completeStub = (mockRequest.response.complete as sinon.SinonStub);
293
+ expect(addContentStub.callCount).to.equal(2);
294
+ expect(completeStub.calledOnce).to.be.true;
295
+ });
296
+
297
+ it('should process turn.completed events through invoke', async () => {
298
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
299
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
300
+
301
+ const events = [
302
+ createAgentMessageEvent('Done!'),
303
+ createTurnCompletedEvent(150, 75)
304
+ ];
305
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
306
+ (mockRequest.session.getRequests as sinon.SinonStub).returns([mockRequest]);
307
+ (mockRequest.getDataByKey as sinon.SinonStub).withArgs(CODEX_INPUT_TOKENS_KEY).returns(0);
308
+ (mockRequest.getDataByKey as sinon.SinonStub).withArgs(CODEX_OUTPUT_TOKENS_KEY).returns(0);
309
+
310
+ await agent.invoke(mockRequest);
311
+
312
+ const addDataStub = (mockRequest.addData as sinon.SinonStub);
313
+ const completeStub = (mockRequest.response.complete as sinon.SinonStub);
314
+ expect(addDataStub.calledWith(CODEX_INPUT_TOKENS_KEY, 150)).to.be.true;
315
+ expect(addDataStub.calledWith(CODEX_OUTPUT_TOKENS_KEY, 75)).to.be.true;
316
+ expect(completeStub.calledOnce).to.be.true;
317
+ });
318
+
319
+ it('should handle errors from codexService.send', async () => {
320
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
321
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
322
+
323
+ const testError = new Error('Network failure');
324
+ (mockCodexService.send as sinon.SinonStub).rejects(testError);
325
+
326
+ await agent.invoke(mockRequest);
327
+
328
+ const addContentStub = (mockRequest.response.response.addContent as sinon.SinonStub);
329
+ const errorStub = (mockRequest.response.error as sinon.SinonStub);
330
+ expect(addContentStub.calledOnce).to.be.true;
331
+ const addedContent = addContentStub.firstCall.args[0];
332
+ expect(addedContent).to.be.instanceOf(ErrorChatResponseContentImpl);
333
+ expect(errorStub.calledWith(testError)).to.be.true;
334
+ });
335
+
336
+ it('should call response.complete() on successful completion', async () => {
337
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
338
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
339
+
340
+ const events = [
341
+ createAgentMessageEvent('Success'),
342
+ createTurnCompletedEvent(10, 5)
343
+ ];
344
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
345
+
346
+ await agent.invoke(mockRequest);
347
+
348
+ const completeStub = (mockRequest.response.complete as sinon.SinonStub);
349
+ const errorStub = (mockRequest.response.error as sinon.SinonStub);
350
+ expect(completeStub.calledOnce).to.be.true;
351
+ expect(errorStub.called).to.be.false;
352
+ });
353
+
354
+ it('should call response.error() on failure', async () => {
355
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
356
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
357
+
358
+ const error = new Error('Test error');
359
+ (mockCodexService.send as sinon.SinonStub).rejects(error);
360
+
361
+ await agent.invoke(mockRequest);
362
+
363
+ const errorStub = (mockRequest.response.error as sinon.SinonStub);
364
+ expect(errorStub.calledWith(error)).to.be.true;
365
+ });
366
+
367
+ it('should extract prompt and session ID correctly', async () => {
368
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
369
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
370
+
371
+ const customRequest = {
372
+ ...mockRequest,
373
+ request: { text: '@Codex write a test' },
374
+ session: {
375
+ id: 'session-123',
376
+ getRequests: mockRequest.session.getRequests,
377
+ setSuggestions: mockRequest.session.setSuggestions
378
+ }
379
+ } as unknown as MutableChatRequestModel;
380
+ const events = [createTurnCompletedEvent(10, 5)];
381
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
382
+
383
+ await agent.invoke(customRequest);
384
+
385
+ expect((mockCodexService.send as sinon.SinonStub).calledOnce).to.be.true;
386
+ const callArgs = (mockCodexService.send as sinon.SinonStub).firstCall.args[0];
387
+ expect(callArgs.prompt).to.equal('write a test');
388
+ expect(callArgs.sessionId).to.equal('session-123');
389
+ });
390
+
391
+ it('should pass sandboxMode from modeId to codexService.send', async () => {
392
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
393
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
394
+
395
+ const customRequest = {
396
+ ...mockRequest,
397
+ request: { text: 'test prompt', modeId: 'read-only' },
398
+ session: mockRequest.session
399
+ } as unknown as MutableChatRequestModel;
400
+ const events = [createTurnCompletedEvent(10, 5)];
401
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
402
+
403
+ await agent.invoke(customRequest);
404
+
405
+ expect((mockCodexService.send as sinon.SinonStub).calledOnce).to.be.true;
406
+ const callArgs = (mockCodexService.send as sinon.SinonStub).firstCall.args[0];
407
+ expect(callArgs.sandboxMode).to.equal('read-only');
408
+ });
409
+
410
+ it('should default sandboxMode to workspace-write when modeId is undefined', async () => {
411
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
412
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
413
+
414
+ const customRequest = {
415
+ ...mockRequest,
416
+ request: { text: 'test prompt' },
417
+ session: mockRequest.session
418
+ } as unknown as MutableChatRequestModel;
419
+ const events = [createTurnCompletedEvent(10, 5)];
420
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
421
+
422
+ await agent.invoke(customRequest);
423
+
424
+ expect((mockCodexService.send as sinon.SinonStub).calledOnce).to.be.true;
425
+ const callArgs = (mockCodexService.send as sinon.SinonStub).firstCall.args[0];
426
+ expect(callArgs.sandboxMode).to.equal('workspace-write');
427
+ });
428
+
429
+ it('should default sandboxMode to workspace-write when modeId is invalid', async () => {
430
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
431
+ const mockCodexService = container.get<CodexFrontendService>(CodexFrontendService);
432
+
433
+ const customRequest = {
434
+ ...mockRequest,
435
+ request: { text: 'test prompt', modeId: 'invalid-mode' },
436
+ session: mockRequest.session
437
+ } as unknown as MutableChatRequestModel;
438
+ const events = [createTurnCompletedEvent(10, 5)];
439
+ (mockCodexService.send as sinon.SinonStub).resolves(createMockStream(events));
440
+
441
+ await agent.invoke(customRequest);
442
+
443
+ expect((mockCodexService.send as sinon.SinonStub).calledOnce).to.be.true;
444
+ const callArgs = (mockCodexService.send as sinon.SinonStub).firstCall.args[0];
445
+ expect(callArgs.sandboxMode).to.equal('workspace-write');
446
+ });
447
+ });
448
+
449
+ describe('protected methods', () => {
450
+ it('getToolCalls should create new map if not exists', () => {
451
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
452
+ (mockRequest.getDataByKey as sinon.SinonStub).withArgs(CODEX_TOOL_CALLS_KEY).returns(undefined);
453
+
454
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
455
+ const result = testAccess.getToolCalls(mockRequest);
456
+
457
+ expect(result).to.be.instanceOf(Map);
458
+ expect(result.size).to.equal(0);
459
+ expect((mockRequest.addData as sinon.SinonStub).calledWith(CODEX_TOOL_CALLS_KEY, result)).to.be.true;
460
+ });
461
+
462
+ it('getToolCalls should return existing map', () => {
463
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
464
+ const existingMap = new Map<string, unknown>();
465
+ existingMap.set('test-id', {});
466
+ (mockRequest.getDataByKey as sinon.SinonStub).withArgs(CODEX_TOOL_CALLS_KEY).returns(existingMap);
467
+
468
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
469
+ const result = testAccess.getToolCalls(mockRequest);
470
+
471
+ expect(result).to.equal(existingMap);
472
+ expect(result.size).to.equal(1);
473
+ expect((mockRequest.addData as sinon.SinonStub).called).to.be.false;
474
+ });
475
+
476
+ it('isToolInvocation should return true for command_execution item', () => {
477
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
478
+ const item = {
479
+ type: 'command_execution',
480
+ id: 'cmd-1',
481
+ command: 'npm test',
482
+ status: 'running',
483
+ aggregated_output: ''
484
+ } as unknown as CommandExecutionItem;
485
+
486
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
487
+ expect(testAccess.isToolInvocation(item)).to.be.true;
488
+ });
489
+
490
+ it('isToolInvocation should return true for file_change item', () => {
491
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
492
+ const item = {
493
+ type: 'file_change',
494
+ id: 'file-1',
495
+ changes: [],
496
+ status: 'running'
497
+ } as unknown as FileChangeItem;
498
+
499
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
500
+ expect(testAccess.isToolInvocation(item)).to.be.true;
501
+ });
502
+
503
+ it('isToolInvocation should return true for mcp_tool_call item', () => {
504
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
505
+ const item = {
506
+ type: 'mcp_tool_call',
507
+ id: 'mcp-1',
508
+ server: 'test-server',
509
+ tool: 'test-tool',
510
+ status: 'running'
511
+ } as unknown as McpToolCallItem;
512
+
513
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
514
+ expect(testAccess.isToolInvocation(item)).to.be.true;
515
+ });
516
+
517
+ it('isToolInvocation should return true for web_search item', () => {
518
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
519
+ const item: WebSearchItem = {
520
+ type: 'web_search',
521
+ id: 'search-1',
522
+ query: 'test query'
523
+ };
524
+
525
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
526
+ expect(testAccess.isToolInvocation(item)).to.be.true;
527
+ });
528
+
529
+ it('isToolInvocation should return true for todo_list item', () => {
530
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
531
+ const item: TodoListItem = {
532
+ type: 'todo_list',
533
+ id: 'todo-1',
534
+ items: []
535
+ };
536
+
537
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
538
+ expect(testAccess.isToolInvocation(item)).to.be.true;
539
+ });
540
+
541
+ it('isToolInvocation should return false for agent_message item', () => {
542
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
543
+ const item: AgentMessageItem = {
544
+ type: 'agent_message',
545
+ id: 'msg-1',
546
+ text: 'Hello'
547
+ };
548
+
549
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
550
+ expect(testAccess.isToolInvocation(item)).to.be.false;
551
+ });
552
+
553
+ it('extractToolArguments should extract command_execution arguments', () => {
554
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
555
+ const item: CommandExecutionItem = {
556
+ type: 'command_execution',
557
+ id: 'cmd-1',
558
+ command: 'npm test',
559
+ status: 'completed',
560
+ exit_code: 0,
561
+ aggregated_output: 'test output'
562
+ };
563
+
564
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
565
+ const result = testAccess.extractToolArguments(item);
566
+ const parsed = JSON.parse(result);
567
+
568
+ expect(parsed.command).to.equal('npm test');
569
+ expect(parsed.status).to.equal('completed');
570
+ expect(parsed.exit_code).to.equal(0);
571
+ });
572
+
573
+ it('extractToolArguments should extract file_change arguments', () => {
574
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
575
+ const item: FileChangeItem = {
576
+ type: 'file_change',
577
+ id: 'file-1',
578
+ changes: [{ path: '/test/file.ts', kind: 'add' }],
579
+ status: 'completed'
580
+ };
581
+
582
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
583
+ const result = testAccess.extractToolArguments(item);
584
+ const parsed = JSON.parse(result);
585
+
586
+ expect(parsed.changes).to.deep.equal([{ path: '/test/file.ts', kind: 'add' }]);
587
+ expect(parsed.status).to.equal('completed');
588
+ });
589
+
590
+ it('updateTokens should store token data and update session suggestion', () => {
591
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
592
+ (mockRequest.session.getRequests as sinon.SinonStub).returns([mockRequest]);
593
+ (mockRequest.getDataByKey as sinon.SinonStub).withArgs(CODEX_INPUT_TOKENS_KEY).returns(100);
594
+ (mockRequest.getDataByKey as sinon.SinonStub).withArgs(CODEX_OUTPUT_TOKENS_KEY).returns(50);
595
+
596
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
597
+ testAccess.updateTokens(mockRequest, 100, 50);
598
+
599
+ expect((mockRequest.addData as sinon.SinonStub).calledWith(CODEX_INPUT_TOKENS_KEY, 100)).to.be.true;
600
+ expect((mockRequest.addData as sinon.SinonStub).calledWith(CODEX_OUTPUT_TOKENS_KEY, 50)).to.be.true;
601
+ expect((mockRequest.session.setSuggestions as sinon.SinonStub).called).to.be.true;
602
+ });
603
+
604
+ it('getSessionTotalTokens should sum tokens across multiple requests', () => {
605
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
606
+ const request1 = {
607
+ getDataByKey: sinon.stub()
608
+ };
609
+ (request1.getDataByKey as sinon.SinonStub).withArgs(CODEX_INPUT_TOKENS_KEY).returns(100);
610
+ (request1.getDataByKey as sinon.SinonStub).withArgs(CODEX_OUTPUT_TOKENS_KEY).returns(50);
611
+
612
+ const request2 = {
613
+ getDataByKey: sinon.stub()
614
+ };
615
+ (request2.getDataByKey as sinon.SinonStub).withArgs(CODEX_INPUT_TOKENS_KEY).returns(200);
616
+ (request2.getDataByKey as sinon.SinonStub).withArgs(CODEX_OUTPUT_TOKENS_KEY).returns(75);
617
+
618
+ (mockRequest.session.getRequests as sinon.SinonStub).returns([request1, request2]);
619
+
620
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
621
+ const result = testAccess.getSessionTotalTokens(mockRequest);
622
+
623
+ expect(result.inputTokens).to.equal(300);
624
+ expect(result.outputTokens).to.equal(125);
625
+ });
626
+
627
+ it('getSessionTotalTokens should handle requests with no token data', () => {
628
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
629
+ const request1 = {
630
+ getDataByKey: sinon.stub().returns(undefined)
631
+ };
632
+
633
+ (mockRequest.session.getRequests as sinon.SinonStub).returns([request1]);
634
+
635
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
636
+ const result = testAccess.getSessionTotalTokens(mockRequest);
637
+
638
+ expect(result.inputTokens).to.equal(0);
639
+ expect(result.outputTokens).to.equal(0);
640
+ });
641
+
642
+ it('extractSandboxMode should return read-only for read-only modeId', () => {
643
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
644
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
645
+ expect(testAccess.extractSandboxMode('read-only')).to.equal('read-only');
646
+ });
647
+
648
+ it('extractSandboxMode should return workspace-write for workspace-write modeId', () => {
649
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
650
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
651
+ expect(testAccess.extractSandboxMode('workspace-write')).to.equal('workspace-write');
652
+ });
653
+
654
+ it('extractSandboxMode should return danger-full-access for danger-full-access modeId', () => {
655
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
656
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
657
+ expect(testAccess.extractSandboxMode('danger-full-access')).to.equal('danger-full-access');
658
+ });
659
+
660
+ it('extractSandboxMode should default to workspace-write for undefined modeId', () => {
661
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
662
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
663
+ expect(testAccess.extractSandboxMode(undefined)).to.equal('workspace-write');
664
+ });
665
+
666
+ it('extractSandboxMode should default to workspace-write for invalid modeId', () => {
667
+ const agent = container.get<CodexChatAgent>(CodexChatAgent);
668
+ const testAccess = agent as unknown as CodexChatAgentTestAccess;
669
+ expect(testAccess.extractSandboxMode('invalid-mode')).to.equal('workspace-write');
670
+ });
671
+ });
672
+ });