@ai-devkit/agent-manager 0.28.1 → 0.30.0
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.
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +1 -342
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/capacity/codex.test.js +343 -0
- package/dist/__tests__/capacity/codex.test.js.map +1 -0
- package/dist/__tests__/capacity/index.test.js +50 -0
- package/dist/__tests__/capacity/index.test.js.map +1 -0
- package/dist/__tests__/database/DurableAgentsDatabase.test.js +31 -3
- package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -1
- package/dist/__tests__/durable/CodexCliProbe.test.js +129 -0
- package/dist/__tests__/durable/CodexCliProbe.test.js.map +1 -0
- package/dist/__tests__/durable/CodexDurableAgentRepository.test.js +84 -0
- package/dist/__tests__/durable/CodexDurableAgentRepository.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintAgent.integration.test.js +126 -0
- package/dist/__tests__/durable/CodexPrintAgent.integration.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintAgentService.test.js +271 -0
- package/dist/__tests__/durable/CodexPrintAgentService.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintRunner.test.js +393 -0
- package/dist/__tests__/durable/CodexPrintRunner.test.js.map +1 -0
- package/dist/__tests__/print/ClaudeCliProbe.test.js +3 -7
- package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +33 -27
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgentService.test.js +65 -0
- package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintRunner.test.js +4 -10
- package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
- package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js +79 -0
- package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js.map +1 -0
- package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js +436 -0
- package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js +76 -0
- package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js +43 -0
- package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js +202 -0
- package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js +119 -0
- package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js +386 -0
- package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js +34 -0
- package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js.map +1 -0
- package/dist/__tests__/utils/ClaudeSessionParser.test.js +2 -2
- package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/capacity/codex.d.ts +36 -0
- package/dist/capacity/codex.d.ts.map +1 -0
- package/dist/capacity/codex.js +317 -0
- package/dist/capacity/codex.js.map +1 -0
- package/dist/capacity/index.d.ts +11 -0
- package/dist/capacity/index.d.ts.map +1 -0
- package/dist/capacity/index.js +49 -0
- package/dist/capacity/index.js.map +1 -0
- package/dist/capacity/types.d.ts +17 -0
- package/dist/capacity/types.d.ts.map +1 -0
- package/dist/capacity/types.js +3 -0
- package/dist/capacity/types.js.map +1 -0
- package/dist/database/migrations/004_codex_durable_sessions.sql +1 -0
- package/dist/durable/DurableAgent.d.ts +23 -3
- package/dist/durable/DurableAgent.d.ts.map +1 -1
- package/dist/durable/DurableAgent.js +12 -0
- package/dist/durable/DurableAgent.js.map +1 -1
- package/dist/durable/DurableAgentRepository.d.ts +6 -7
- package/dist/durable/DurableAgentRepository.d.ts.map +1 -1
- package/dist/durable/DurableAgentRepository.js +73 -42
- package/dist/durable/DurableAgentRepository.js.map +1 -1
- package/dist/durable/process.d.ts +8 -0
- package/dist/durable/process.d.ts.map +1 -0
- package/dist/durable/process.js +40 -0
- package/dist/durable/process.js.map +1 -0
- package/dist/durable/run.d.ts +32 -0
- package/dist/durable/run.d.ts.map +1 -0
- package/dist/durable/run.js +46 -0
- package/dist/durable/run.js.map +1 -0
- package/dist/durable/utils.d.ts +19 -0
- package/dist/durable/utils.d.ts.map +1 -0
- package/dist/durable/utils.js +52 -0
- package/dist/durable/utils.js.map +1 -0
- package/dist/index.d.ts +19 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -8
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/ClaudeAgentMapper.d.ts +21 -0
- package/dist/providers/claude/ClaudeAgentMapper.d.ts.map +1 -0
- package/dist/providers/claude/ClaudeAgentMapper.js +42 -0
- package/dist/providers/claude/ClaudeAgentMapper.js.map +1 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts +29 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -0
- package/dist/providers/claude/ClaudeCodeAdapter.js +129 -0
- package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -0
- package/dist/{adapters/ClaudeCodeAdapter.d.ts → providers/claude/ClaudeSessionLocator.d.ts} +68 -52
- package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -0
- package/dist/{adapters/ClaudeCodeAdapter.js → providers/claude/ClaudeSessionLocator.js} +71 -189
- package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -0
- package/dist/{utils → providers/claude}/ClaudeSessionParser.d.ts +2 -2
- package/dist/providers/claude/ClaudeSessionParser.d.ts.map +1 -0
- package/dist/{utils → providers/claude}/ClaudeSessionParser.js +1 -1
- package/dist/providers/claude/ClaudeSessionParser.js.map +1 -0
- package/dist/providers/claude/durable/ClaudeCliProbe.d.ts.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.js +4 -9
- package/dist/providers/claude/durable/ClaudeCliProbe.js.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudePrintAgentService.d.ts +4 -9
- package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudePrintAgentService.js +45 -0
- package/dist/providers/claude/durable/ClaudePrintAgentService.js.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudePrintRunner.d.ts +3 -3
- package/dist/providers/claude/durable/ClaudePrintRunner.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudePrintRunner.js +75 -0
- package/dist/providers/claude/durable/ClaudePrintRunner.js.map +1 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.d.ts +17 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.js +69 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.js.map +1 -0
- package/dist/providers/codex/durable/CodexCliProbe.d.ts +20 -0
- package/dist/providers/codex/durable/CodexCliProbe.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexCliProbe.js +61 -0
- package/dist/providers/codex/durable/CodexCliProbe.js.map +1 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.d.ts +40 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.js +50 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.js.map +1 -0
- package/dist/providers/codex/durable/CodexPrintRunner.d.ts +34 -0
- package/dist/providers/codex/durable/CodexPrintRunner.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexPrintRunner.js +78 -0
- package/dist/providers/codex/durable/CodexPrintRunner.js.map +1 -0
- package/dist/providers/codex/durable/CodexStreamParser.d.ts +21 -0
- package/dist/providers/codex/durable/CodexStreamParser.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexStreamParser.js +86 -0
- package/dist/providers/codex/durable/CodexStreamParser.js.map +1 -0
- package/dist/providers/pi/durable/PiCliProbe.d.ts +20 -0
- package/dist/providers/pi/durable/PiCliProbe.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiCliProbe.js +51 -0
- package/dist/providers/pi/durable/PiCliProbe.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintAgentService.d.ts +39 -0
- package/dist/providers/pi/durable/PiPrintAgentService.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintAgentService.js +47 -0
- package/dist/providers/pi/durable/PiPrintAgentService.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintProtocol.d.ts +4 -0
- package/dist/providers/pi/durable/PiPrintProtocol.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintProtocol.js +44 -0
- package/dist/providers/pi/durable/PiPrintProtocol.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintRunner.d.ts +32 -0
- package/dist/providers/pi/durable/PiPrintRunner.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintRunner.js +61 -0
- package/dist/providers/pi/durable/PiPrintRunner.js.map +1 -0
- package/dist/providers/pi/durable/PiStreamParser.d.ts +21 -0
- package/dist/providers/pi/durable/PiStreamParser.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiStreamParser.js +71 -0
- package/dist/providers/pi/durable/PiStreamParser.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1 -307
- package/src/__tests__/capacity/codex.test.ts +184 -0
- package/src/__tests__/capacity/index.test.ts +38 -0
- package/src/__tests__/database/DurableAgentsDatabase.test.ts +28 -3
- package/src/__tests__/durable/CodexCliProbe.test.ts +63 -0
- package/src/__tests__/durable/CodexDurableAgentRepository.test.ts +54 -0
- package/src/__tests__/durable/CodexPrintAgent.integration.test.ts +79 -0
- package/src/__tests__/durable/CodexPrintAgentService.test.ts +139 -0
- package/src/__tests__/durable/CodexPrintRunner.test.ts +189 -0
- package/src/__tests__/fixtures/fake-codex.cjs +45 -0
- package/src/__tests__/print/ClaudeCliProbe.test.ts +3 -7
- package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +19 -22
- package/src/__tests__/print/ClaudePrintAgentService.test.ts +33 -1
- package/src/__tests__/print/ClaudePrintRunner.test.ts +4 -10
- package/src/__tests__/providers/claude/ClaudeAgentMapper.test.ts +84 -0
- package/src/__tests__/providers/claude/ClaudeSessionLocator.test.ts +411 -0
- package/src/__tests__/providers/pi/durable/PiCliProbe.test.ts +38 -0
- package/src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts +32 -0
- package/src/__tests__/providers/pi/durable/PiPrintAgentService.test.ts +69 -0
- package/src/__tests__/providers/pi/durable/PiPrintProtocol.test.ts +33 -0
- package/src/__tests__/providers/pi/durable/PiPrintRunner.test.ts +107 -0
- package/src/__tests__/providers/pi/durable/PiStreamParser.test.ts +24 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +2 -2
- package/src/adapters/index.ts +1 -1
- package/src/capacity/codex.ts +320 -0
- package/src/capacity/index.ts +57 -0
- package/src/capacity/types.ts +18 -0
- package/src/database/migrations/004_codex_durable_sessions.sql +1 -0
- package/src/durable/DurableAgent.ts +51 -3
- package/src/durable/DurableAgentRepository.ts +60 -31
- package/src/durable/process.ts +28 -0
- package/src/durable/run.ts +74 -0
- package/src/durable/utils.ts +61 -0
- package/src/index.ts +37 -15
- package/src/providers/claude/ClaudeAgentMapper.ts +62 -0
- package/src/providers/claude/ClaudeCodeAdapter.ts +160 -0
- package/src/{adapters/ClaudeCodeAdapter.ts → providers/claude/ClaudeSessionLocator.ts} +97 -239
- package/src/{utils → providers/claude}/ClaudeSessionParser.ts +2 -2
- package/src/{durable → providers/claude/durable}/ClaudeCliProbe.ts +4 -10
- package/src/providers/claude/durable/ClaudePrintAgentService.ts +66 -0
- package/src/providers/claude/durable/ClaudePrintRunner.ts +92 -0
- package/src/providers/claude/durable/ClaudeStreamParser.ts +79 -0
- package/src/providers/codex/durable/CodexCliProbe.ts +60 -0
- package/src/providers/codex/durable/CodexPrintAgentService.ts +65 -0
- package/src/providers/codex/durable/CodexPrintRunner.ts +94 -0
- package/src/providers/codex/durable/CodexStreamParser.ts +104 -0
- package/src/providers/pi/durable/PiCliProbe.ts +48 -0
- package/src/providers/pi/durable/PiPrintAgentService.ts +69 -0
- package/src/providers/pi/durable/PiPrintProtocol.ts +39 -0
- package/src/providers/pi/durable/PiPrintRunner.ts +88 -0
- package/src/providers/pi/durable/PiStreamParser.ts +84 -0
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +0 -1
- package/dist/adapters/ClaudeCodeAdapter.js.map +0 -1
- package/dist/durable/ClaudeCliProbe.d.ts.map +0 -1
- package/dist/durable/ClaudeCliProbe.js.map +0 -1
- package/dist/durable/ClaudePrintAgentService.d.ts.map +0 -1
- package/dist/durable/ClaudePrintAgentService.js +0 -66
- package/dist/durable/ClaudePrintAgentService.js.map +0 -1
- package/dist/durable/ClaudePrintRunner.d.ts.map +0 -1
- package/dist/durable/ClaudePrintRunner.js +0 -128
- package/dist/durable/ClaudePrintRunner.js.map +0 -1
- package/dist/utils/ClaudeSessionParser.d.ts.map +0 -1
- package/dist/utils/ClaudeSessionParser.js.map +0 -1
- package/src/durable/ClaudePrintAgentService.ts +0 -94
- package/src/durable/ClaudePrintRunner.ts +0 -139
- /package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.d.ts +0 -0
|
@@ -6,7 +6,7 @@ import type { MockedFunction } from 'vitest';
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
|
|
9
|
-
import { ClaudeCodeAdapter } from '../../
|
|
9
|
+
import { ClaudeCodeAdapter } from '../../providers/claude/ClaudeCodeAdapter.js';
|
|
10
10
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
11
11
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
12
12
|
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
@@ -764,115 +764,6 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
764
764
|
});
|
|
765
765
|
});
|
|
766
766
|
|
|
767
|
-
describe('discoverSessions', () => {
|
|
768
|
-
let tmpDir: string;
|
|
769
|
-
|
|
770
|
-
beforeEach(() => {
|
|
771
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-test-'));
|
|
772
|
-
(adapter as any).projectsDir = path.join(tmpDir, 'projects');
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
afterEach(() => {
|
|
776
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
it('should return empty when projects dir does not exist', () => {
|
|
780
|
-
(adapter as any).projectsDir = path.join(tmpDir, 'nonexistent');
|
|
781
|
-
const discoverSessions = (adapter as any).discoverSessions.bind(adapter);
|
|
782
|
-
|
|
783
|
-
const result = discoverSessions([
|
|
784
|
-
{ pid: 1, command: 'claude', cwd: '/test', tty: '' },
|
|
785
|
-
]);
|
|
786
|
-
expect(result).toEqual([]);
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
it('should scan only directories matching process CWDs', () => {
|
|
790
|
-
const projectsDir = path.join(tmpDir, 'projects');
|
|
791
|
-
(adapter as any).projectsDir = projectsDir;
|
|
792
|
-
const discoverSessions = (adapter as any).discoverSessions.bind(adapter);
|
|
793
|
-
|
|
794
|
-
// /my/project → -my-project (encoded dir)
|
|
795
|
-
const encodedDir = path.join(projectsDir, '-my-project');
|
|
796
|
-
fs.mkdirSync(encodedDir, { recursive: true });
|
|
797
|
-
|
|
798
|
-
// Also create another dir that should NOT be scanned
|
|
799
|
-
const otherDir = path.join(projectsDir, '-other-project');
|
|
800
|
-
fs.mkdirSync(otherDir, { recursive: true });
|
|
801
|
-
|
|
802
|
-
const mockFiles: SessionFile[] = [
|
|
803
|
-
{
|
|
804
|
-
sessionId: 's1',
|
|
805
|
-
filePath: path.join(encodedDir, 's1.jsonl'),
|
|
806
|
-
projectDir: encodedDir,
|
|
807
|
-
birthtimeMs: 1710800324000,
|
|
808
|
-
resolvedCwd: '',
|
|
809
|
-
},
|
|
810
|
-
];
|
|
811
|
-
mockedBatchGetSessionFileBirthtimes.mockReturnValue(mockFiles);
|
|
812
|
-
|
|
813
|
-
const processes = [
|
|
814
|
-
{ pid: 1, command: 'claude', cwd: '/my/project', tty: '' },
|
|
815
|
-
];
|
|
816
|
-
|
|
817
|
-
const result = discoverSessions(processes);
|
|
818
|
-
expect(result).toHaveLength(1);
|
|
819
|
-
expect(result[0].resolvedCwd).toBe('/my/project');
|
|
820
|
-
// batchGetSessionFileBirthtimes called once with all dirs
|
|
821
|
-
expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledTimes(1);
|
|
822
|
-
expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledWith([encodedDir]);
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
it('should return empty when encoded dir does not exist', () => {
|
|
826
|
-
const projectsDir = path.join(tmpDir, 'projects');
|
|
827
|
-
fs.mkdirSync(projectsDir, { recursive: true });
|
|
828
|
-
(adapter as any).projectsDir = projectsDir;
|
|
829
|
-
const discoverSessions = (adapter as any).discoverSessions.bind(adapter);
|
|
830
|
-
|
|
831
|
-
// Process CWD /test encodes to -test, but that dir doesn't exist
|
|
832
|
-
const result = discoverSessions([
|
|
833
|
-
{ pid: 1, command: 'claude', cwd: '/test', tty: '' },
|
|
834
|
-
]);
|
|
835
|
-
expect(result).toEqual([]);
|
|
836
|
-
expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
it('should deduplicate when multiple processes share same CWD', () => {
|
|
840
|
-
const projectsDir = path.join(tmpDir, 'projects');
|
|
841
|
-
(adapter as any).projectsDir = projectsDir;
|
|
842
|
-
const discoverSessions = (adapter as any).discoverSessions.bind(adapter);
|
|
843
|
-
|
|
844
|
-
const encodedDir = path.join(projectsDir, '-my-project');
|
|
845
|
-
fs.mkdirSync(encodedDir, { recursive: true });
|
|
846
|
-
|
|
847
|
-
mockedBatchGetSessionFileBirthtimes.mockReturnValue([
|
|
848
|
-
{ sessionId: 's1', filePath: path.join(encodedDir, 's1.jsonl'), projectDir: encodedDir, birthtimeMs: 1710800324000, resolvedCwd: '' },
|
|
849
|
-
]);
|
|
850
|
-
|
|
851
|
-
const processes = [
|
|
852
|
-
{ pid: 1, command: 'claude', cwd: '/my/project', tty: '' },
|
|
853
|
-
{ pid: 2, command: 'claude', cwd: '/my/project', tty: '' },
|
|
854
|
-
];
|
|
855
|
-
|
|
856
|
-
const result = discoverSessions(processes);
|
|
857
|
-
// Should only call batch once with deduplicated dir
|
|
858
|
-
expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledTimes(1);
|
|
859
|
-
expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledWith([encodedDir]);
|
|
860
|
-
expect(result).toHaveLength(1);
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
it('should skip processes with empty cwd', () => {
|
|
864
|
-
const projectsDir = path.join(tmpDir, 'projects');
|
|
865
|
-
fs.mkdirSync(projectsDir, { recursive: true });
|
|
866
|
-
(adapter as any).projectsDir = projectsDir;
|
|
867
|
-
const discoverSessions = (adapter as any).discoverSessions.bind(adapter);
|
|
868
|
-
|
|
869
|
-
const result = discoverSessions([
|
|
870
|
-
{ pid: 1, command: 'claude', cwd: '', tty: '' },
|
|
871
|
-
]);
|
|
872
|
-
expect(result).toEqual([]);
|
|
873
|
-
});
|
|
874
|
-
});
|
|
875
|
-
|
|
876
767
|
describe('helper methods', () => {
|
|
877
768
|
describe('determineStatus', () => {
|
|
878
769
|
it('should return "unknown" for sessions with no last entry type', () => {
|
|
@@ -1092,203 +983,6 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
1092
983
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1093
984
|
});
|
|
1094
985
|
|
|
1095
|
-
describe('tryPidFileMatching', () => {
|
|
1096
|
-
let sessionsDir: string;
|
|
1097
|
-
let projectsDir: string;
|
|
1098
|
-
|
|
1099
|
-
beforeEach(() => {
|
|
1100
|
-
sessionsDir = path.join(tmpDir, 'sessions');
|
|
1101
|
-
projectsDir = path.join(tmpDir, 'projects');
|
|
1102
|
-
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
1103
|
-
(adapter as any).sessionsDir = sessionsDir;
|
|
1104
|
-
(adapter as any).projectsDir = projectsDir;
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
const makeProc = (pid: number, cwd = '/project/test', startTime?: Date): ProcessInfo => ({
|
|
1108
|
-
pid, command: 'claude', cwd, tty: 'ttys001', startTime,
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
const writePidFile = (pid: number, sessionId: string, cwd: string, startedAt: number) => {
|
|
1112
|
-
fs.writeFileSync(
|
|
1113
|
-
path.join(sessionsDir, `${pid}.json`),
|
|
1114
|
-
JSON.stringify({ pid, sessionId, cwd, startedAt, kind: 'interactive', entrypoint: 'cli' }),
|
|
1115
|
-
);
|
|
1116
|
-
};
|
|
1117
|
-
|
|
1118
|
-
const writeJsonl = (cwd: string, sessionId: string) => {
|
|
1119
|
-
const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
1120
|
-
const projDir = path.join(projectsDir, encoded);
|
|
1121
|
-
fs.mkdirSync(projDir, { recursive: true });
|
|
1122
|
-
const filePath = path.join(projDir, `${sessionId}.jsonl`);
|
|
1123
|
-
fs.writeFileSync(filePath, JSON.stringify({ type: 'assistant', timestamp: new Date().toISOString() }));
|
|
1124
|
-
return filePath;
|
|
1125
|
-
};
|
|
1126
|
-
|
|
1127
|
-
it('should return direct match when PID file and JSONL both exist within time tolerance', () => {
|
|
1128
|
-
const startTime = new Date();
|
|
1129
|
-
const proc = makeProc(1001, '/project/test', startTime);
|
|
1130
|
-
writePidFile(1001, 'session-abc', '/project/test', startTime.getTime());
|
|
1131
|
-
writeJsonl('/project/test', 'session-abc');
|
|
1132
|
-
|
|
1133
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1134
|
-
const { direct, fallback } = tryMatch([proc]);
|
|
1135
|
-
|
|
1136
|
-
expect(direct).toHaveLength(1);
|
|
1137
|
-
expect(fallback).toHaveLength(0);
|
|
1138
|
-
expect(direct[0].sessionFile.sessionId).toBe('session-abc');
|
|
1139
|
-
expect(direct[0].sessionFile.resolvedCwd).toBe('/project/test');
|
|
1140
|
-
expect(direct[0].process.pid).toBe(1001);
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
it('should fall back when PID file exists but JSONL is missing', () => {
|
|
1144
|
-
const startTime = new Date();
|
|
1145
|
-
const proc = makeProc(1002, '/project/test', startTime);
|
|
1146
|
-
writePidFile(1002, 'nonexistent-session', '/project/test', startTime.getTime());
|
|
1147
|
-
// No JSONL file written
|
|
1148
|
-
|
|
1149
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1150
|
-
const { direct, fallback } = tryMatch([proc]);
|
|
1151
|
-
|
|
1152
|
-
expect(direct).toHaveLength(0);
|
|
1153
|
-
expect(fallback).toHaveLength(1);
|
|
1154
|
-
expect(fallback[0].pid).toBe(1002);
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
it('should fall back when startedAt is stale (>60s from proc.startTime)', () => {
|
|
1158
|
-
const startTime = new Date();
|
|
1159
|
-
const staleTime = startTime.getTime() - 90_000; // 90 seconds earlier
|
|
1160
|
-
const proc = makeProc(1003, '/project/test', startTime);
|
|
1161
|
-
writePidFile(1003, 'stale-session', '/project/test', staleTime);
|
|
1162
|
-
writeJsonl('/project/test', 'stale-session');
|
|
1163
|
-
|
|
1164
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1165
|
-
const { direct, fallback } = tryMatch([proc]);
|
|
1166
|
-
|
|
1167
|
-
expect(direct).toHaveLength(0);
|
|
1168
|
-
expect(fallback).toHaveLength(1);
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
it('should accept PID file when startedAt is within 60s tolerance', () => {
|
|
1172
|
-
const startTime = new Date();
|
|
1173
|
-
const closeTime = startTime.getTime() - 30_000; // 30 seconds earlier — within tolerance
|
|
1174
|
-
const proc = makeProc(1004, '/project/test', startTime);
|
|
1175
|
-
writePidFile(1004, 'close-session', '/project/test', closeTime);
|
|
1176
|
-
writeJsonl('/project/test', 'close-session');
|
|
1177
|
-
|
|
1178
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1179
|
-
const { direct, fallback } = tryMatch([proc]);
|
|
1180
|
-
|
|
1181
|
-
expect(direct).toHaveLength(1);
|
|
1182
|
-
expect(fallback).toHaveLength(0);
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
it('should fall back when PID file is absent', () => {
|
|
1186
|
-
const proc = makeProc(1005, '/project/test', new Date());
|
|
1187
|
-
// No PID file written
|
|
1188
|
-
|
|
1189
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1190
|
-
const { direct, fallback } = tryMatch([proc]);
|
|
1191
|
-
|
|
1192
|
-
expect(direct).toHaveLength(0);
|
|
1193
|
-
expect(fallback).toHaveLength(1);
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
it('should fall back when PID file contains malformed JSON', () => {
|
|
1197
|
-
const proc = makeProc(1006, '/project/test', new Date());
|
|
1198
|
-
fs.writeFileSync(path.join(sessionsDir, '1006.json'), 'not valid json {{{');
|
|
1199
|
-
|
|
1200
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1201
|
-
expect(() => {
|
|
1202
|
-
const { direct, fallback } = tryMatch([proc]);
|
|
1203
|
-
expect(direct).toHaveLength(0);
|
|
1204
|
-
expect(fallback).toHaveLength(1);
|
|
1205
|
-
}).not.toThrow();
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
|
-
it('should fall back for all processes when sessions dir does not exist', () => {
|
|
1209
|
-
(adapter as any).sessionsDir = path.join(tmpDir, 'nonexistent-sessions');
|
|
1210
|
-
const processes = [makeProc(2001, '/a', new Date()), makeProc(2002, '/b', new Date())];
|
|
1211
|
-
|
|
1212
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1213
|
-
const { direct, fallback } = tryMatch(processes);
|
|
1214
|
-
|
|
1215
|
-
expect(direct).toHaveLength(0);
|
|
1216
|
-
expect(fallback).toHaveLength(2);
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
it('should correctly split mixed processes (some with PID files, some without)', () => {
|
|
1220
|
-
const startTime = new Date();
|
|
1221
|
-
const proc1 = makeProc(3001, '/project/one', startTime);
|
|
1222
|
-
const proc2 = makeProc(3002, '/project/two', startTime);
|
|
1223
|
-
const proc3 = makeProc(3003, '/project/three', startTime);
|
|
1224
|
-
|
|
1225
|
-
writePidFile(3001, 'session-one', '/project/one', startTime.getTime());
|
|
1226
|
-
writeJsonl('/project/one', 'session-one');
|
|
1227
|
-
writePidFile(3003, 'session-three', '/project/three', startTime.getTime());
|
|
1228
|
-
writeJsonl('/project/three', 'session-three');
|
|
1229
|
-
// proc2 has no PID file
|
|
1230
|
-
|
|
1231
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1232
|
-
const { direct, fallback } = tryMatch([proc1, proc2, proc3]);
|
|
1233
|
-
|
|
1234
|
-
expect(direct).toHaveLength(2);
|
|
1235
|
-
expect(fallback).toHaveLength(1);
|
|
1236
|
-
expect(direct.map((d: any) => d.process.pid).sort()).toEqual([3001, 3003]);
|
|
1237
|
-
expect(fallback[0].pid).toBe(3002);
|
|
1238
|
-
});
|
|
1239
|
-
|
|
1240
|
-
it('should skip stale-file check when proc.startTime is undefined', () => {
|
|
1241
|
-
const proc = makeProc(4001, '/project/test', undefined); // no startTime
|
|
1242
|
-
writePidFile(4001, 'no-time-session', '/project/test', Date.now() - 999_999);
|
|
1243
|
-
writeJsonl('/project/test', 'no-time-session');
|
|
1244
|
-
|
|
1245
|
-
const tryMatch = (adapter as any).tryPidFileMatching.bind(adapter);
|
|
1246
|
-
const { direct, fallback } = tryMatch([proc]);
|
|
1247
|
-
|
|
1248
|
-
// startTime undefined → stale check skipped → direct match
|
|
1249
|
-
expect(direct).toHaveLength(1);
|
|
1250
|
-
expect(fallback).toHaveLength(0);
|
|
1251
|
-
});
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
describe('getProjectDir', () => {
|
|
1255
|
-
const encode = (cwd: string) => (adapter as any).getProjectDir(cwd) as string;
|
|
1256
|
-
|
|
1257
|
-
it('should replace path separators with hyphens', () => {
|
|
1258
|
-
const expected = path.join((adapter as any).projectsDir, '-Users-foo-bar');
|
|
1259
|
-
expect(encode('/Users/foo/bar')).toBe(expected);
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
it('should encode underscores as hyphens (matches Claude Code CLI)', () => {
|
|
1263
|
-
const expected = path.join((adapter as any).projectsDir, '-Users-foo-my-project');
|
|
1264
|
-
expect(encode('/Users/foo/my_project')).toBe(expected);
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
it('should encode dots as hyphens', () => {
|
|
1268
|
-
const expected = path.join((adapter as any).projectsDir, '-Users-foo--worktrees-x');
|
|
1269
|
-
expect(encode('/Users/foo/.worktrees/x')).toBe(expected);
|
|
1270
|
-
});
|
|
1271
|
-
|
|
1272
|
-
it('should collide paths that differ only in non-alphanumeric chars', () => {
|
|
1273
|
-
// The encoding is intentionally lossy — callers must
|
|
1274
|
-
// disambiguate via session JSONL contents, not dir name.
|
|
1275
|
-
expect(encode('/a/b_c')).toBe(encode('/a/b-c'));
|
|
1276
|
-
expect(encode('/a/b_c')).toBe(encode('/a/b.c'));
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
it('should resolve to a real session dir when cwd contains underscores', () => {
|
|
1280
|
-
const cwd = '/Users/foo/my_project';
|
|
1281
|
-
const projectsDir = (adapter as any).projectsDir as string;
|
|
1282
|
-
const expectedDir = path.join(projectsDir, '-Users-foo-my-project');
|
|
1283
|
-
fs.mkdirSync(expectedDir, { recursive: true });
|
|
1284
|
-
const sessionFile = path.join(expectedDir, 'session-underscore.jsonl');
|
|
1285
|
-
fs.writeFileSync(sessionFile, '');
|
|
1286
|
-
|
|
1287
|
-
expect(encode(cwd)).toBe(expectedDir);
|
|
1288
|
-
expect(fs.existsSync(path.join(encode(cwd), 'session-underscore.jsonl'))).toBe(true);
|
|
1289
|
-
});
|
|
1290
|
-
});
|
|
1291
|
-
|
|
1292
986
|
describe('readSession', () => {
|
|
1293
987
|
it('should parse session file with timestamps, cwd, and entry type', () => {
|
|
1294
988
|
const readSession = (adapter as any).parser.readSession.bind((adapter as any).parser);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
CODEX_APP_SERVER_ARGS,
|
|
4
|
+
parseUsage,
|
|
5
|
+
probeCodexCapacity,
|
|
6
|
+
resolveCodexAuthPath,
|
|
7
|
+
toRateWindow
|
|
8
|
+
} from '../../capacity/codex.js';
|
|
9
|
+
|
|
10
|
+
const checkedAt = '2026-08-20T10:00:00.000Z';
|
|
11
|
+
const context = { installed: true, checkedAt };
|
|
12
|
+
|
|
13
|
+
function apiUsage(overrides: Record<string, unknown> = {}) {
|
|
14
|
+
return {
|
|
15
|
+
rate_limit: {
|
|
16
|
+
primary_window: { used_percent: 20, limit_window_seconds: 18_000, reset_at: 1_787_220_000 },
|
|
17
|
+
secondary_window: { used_percent: 60, limit_window_seconds: 604_800, reset_at: 1_787_824_800 },
|
|
18
|
+
...overrides
|
|
19
|
+
},
|
|
20
|
+
credits: { balance: 12.5 },
|
|
21
|
+
additional_rate_limits: [{
|
|
22
|
+
limit_name: 'reviews',
|
|
23
|
+
rate_limit: {
|
|
24
|
+
primary_window: { used_percent: 10, limit_window_seconds: 3_600, reset_at: 1_787_220_000 }
|
|
25
|
+
}
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('Codex auth resolution', () => {
|
|
31
|
+
it('uses CODEX_HOME before HOME', () => {
|
|
32
|
+
expect(resolveCodexAuthPath({ CODEX_HOME: '/custom/codex', HOME: '/users/test' })).toBe('/custom/codex/auth.json');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('falls back to ~/.codex/auth.json', () => {
|
|
36
|
+
expect(resolveCodexAuthPath({ HOME: '/users/test' })).toBe('/users/test/.codex/auth.json');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('Codex API usage mapping', () => {
|
|
41
|
+
it('converts an API window without treating missing data as zero', () => {
|
|
42
|
+
expect(toRateWindow({ used_percent: 25, limit_window_seconds: 18_000, reset_at: 1_787_220_000 }, 'session', 'Session')).toEqual({
|
|
43
|
+
id: 'session', label: 'Session', durationMinutes: 300, usedPercent: 25,
|
|
44
|
+
resetsAt: '2026-08-20T10:00:00.000Z'
|
|
45
|
+
});
|
|
46
|
+
expect(toRateWindow({}, 'session', 'Session')).toMatchObject({ usedPercent: null });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('maps session, weekly, credits, and extra limits', () => {
|
|
50
|
+
const snapshot = parseUsage(apiUsage(), 'pat');
|
|
51
|
+
expect(snapshot).toMatchObject({ source: 'pat', creditsRemaining: 12.5 });
|
|
52
|
+
expect(snapshot.windows).toEqual([
|
|
53
|
+
expect.objectContaining({ id: 'session', durationMinutes: 300 }),
|
|
54
|
+
expect.objectContaining({ id: 'weekly', durationMinutes: 10080 }),
|
|
55
|
+
expect.objectContaining({ id: 'reviews:primary', durationMinutes: 60 })
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('represents missing limits as unavailable rather than zero', () => {
|
|
60
|
+
const snapshot = parseUsage({ credits: {} }, 'oauth');
|
|
61
|
+
expect(snapshot.windows).toEqual([]);
|
|
62
|
+
expect(snapshot.creditsRemaining).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('tiered Codex probing', () => {
|
|
67
|
+
it('selects PAT, calls whoami then usage, and never invokes the CLI', async () => {
|
|
68
|
+
const fetch = vi.fn()
|
|
69
|
+
.mockResolvedValueOnce(new Response(JSON.stringify({ chatgpt_account_id: 'acct-1' }), { status: 200 }))
|
|
70
|
+
.mockResolvedValueOnce(new Response(JSON.stringify(apiUsage()), { status: 200 }));
|
|
71
|
+
const rpc = vi.fn();
|
|
72
|
+
const result = await probeCodexCapacity({
|
|
73
|
+
...context, readFile: async () => JSON.stringify({
|
|
74
|
+
personal_access_token: 'pat-secret',
|
|
75
|
+
tokens: { access_token: 'ignored-oauth', account_id: 'ignored-account' }
|
|
76
|
+
}), fetch, rpc
|
|
77
|
+
});
|
|
78
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
79
|
+
expect(fetch.mock.calls[0][0]).toBe('https://auth.openai.com/api/accounts/v1/user-auth-credential/whoami');
|
|
80
|
+
expect(fetch.mock.calls[1][0]).toBe('https://chatgpt.com/backend-api/wham/usage');
|
|
81
|
+
expect(fetch.mock.calls[1][1].headers).toMatchObject({ Authorization: 'Bearer pat-secret', 'ChatGPT-Account-Id': 'acct-1' });
|
|
82
|
+
expect(rpc).not.toHaveBeenCalled();
|
|
83
|
+
expect(result).toMatchObject({ provider: 'codex', available: 'yes', creditsRemaining: 12.5, authenticated: true });
|
|
84
|
+
expect(result.windows.map(window => window.id)).toEqual(['session', 'weekly', 'reviews:primary']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('selects a fresh OAuth token without calling whoami', async () => {
|
|
88
|
+
const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify(apiUsage()), { status: 200 }));
|
|
89
|
+
const result = await probeCodexCapacity({
|
|
90
|
+
...context,
|
|
91
|
+
readFile: async () => JSON.stringify({ tokens: { access_token: 'oauth-secret', account_id: 'acct-2', expires_at: 1_800_000_000 } }),
|
|
92
|
+
fetch,
|
|
93
|
+
now: () => new Date('2026-08-20T10:00:00.000Z')
|
|
94
|
+
});
|
|
95
|
+
expect(fetch).toHaveBeenCalledOnce();
|
|
96
|
+
expect(fetch.mock.calls[0][1].headers).toMatchObject({ Authorization: 'Bearer oauth-secret', 'ChatGPT-Account-Id': 'acct-2' });
|
|
97
|
+
expect(result.available).toBe('yes');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it.each([
|
|
101
|
+
['missing auth file', async () => { throw Object.assign(new Error('missing'), { code: 'ENOENT' }); }],
|
|
102
|
+
['stale OAuth token', async () => JSON.stringify({ tokens: { access_token: 'stale-secret', account_id: 'acct', expires_at: 1 } })],
|
|
103
|
+
['OAuth 401', async () => JSON.stringify({ tokens: { access_token: 'oauth-secret', account_id: 'acct', expires_at: 1_800_000_000 } })]
|
|
104
|
+
])('falls back to the CLI for %s', async (name, readFile) => {
|
|
105
|
+
const fetch = vi.fn().mockResolvedValue(new Response('', { status: name === 'OAuth 401' ? 401 : 200 }));
|
|
106
|
+
const rpc = vi.fn(async () => ({
|
|
107
|
+
rateLimits: { rateLimits: { primary: { usedPercent: 5, windowDurationMins: 300, resetsAt: null } } },
|
|
108
|
+
account: { account: { type: 'chatgpt' } }
|
|
109
|
+
}));
|
|
110
|
+
const result = await probeCodexCapacity({ ...context, readFile, fetch, rpc, now: () => new Date(checkedAt) });
|
|
111
|
+
expect(rpc).toHaveBeenCalledOnce();
|
|
112
|
+
expect(result.windows).toHaveLength(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('falls back to CLI if PAT requests fail', async () => {
|
|
116
|
+
const rpc = vi.fn(async () => ({ rateLimits: {}, account: { account: null } }));
|
|
117
|
+
const result = await probeCodexCapacity({
|
|
118
|
+
...context,
|
|
119
|
+
readFile: async () => JSON.stringify({ personal_access_token: 'pat-secret' }),
|
|
120
|
+
fetch: vi.fn().mockRejectedValue(new Error('network failure pat-secret')),
|
|
121
|
+
rpc
|
|
122
|
+
});
|
|
123
|
+
expect(rpc).toHaveBeenCalledOnce();
|
|
124
|
+
expect(result.available).toBe('unknown');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('tries fresh OAuth after a PAT request fails', async () => {
|
|
128
|
+
const fetch = vi.fn()
|
|
129
|
+
.mockRejectedValueOnce(new Error('PAT failed'))
|
|
130
|
+
.mockResolvedValueOnce(new Response(JSON.stringify(apiUsage()), { status: 200 }));
|
|
131
|
+
const rpc = vi.fn();
|
|
132
|
+
const result = await probeCodexCapacity({
|
|
133
|
+
...context,
|
|
134
|
+
readFile: async () => JSON.stringify({
|
|
135
|
+
personal_access_token: 'pat-secret',
|
|
136
|
+
tokens: { access_token: 'oauth-secret', account_id: 'acct', expires_at: 1_800_000_000 }
|
|
137
|
+
}),
|
|
138
|
+
fetch,
|
|
139
|
+
rpc,
|
|
140
|
+
now: () => new Date(checkedAt)
|
|
141
|
+
});
|
|
142
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
143
|
+
expect(result.available).toBe('yes');
|
|
144
|
+
expect(rpc).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('uses hardened read-only app-server arguments and both account methods', async () => {
|
|
148
|
+
const rpc = vi.fn(async () => ({
|
|
149
|
+
rateLimits: { rateLimits: { primary: { usedPercent: 5, windowDurationMins: 300, resetsAt: null } } },
|
|
150
|
+
account: { account: { type: 'chatgpt' } }
|
|
151
|
+
}));
|
|
152
|
+
await probeCodexCapacity({ ...context, readFile: async () => '{}', rpc });
|
|
153
|
+
const messages = rpc.mock.calls[0][0];
|
|
154
|
+
expect(messages.map(message => message.method)).toEqual([
|
|
155
|
+
'initialize', 'initialized', 'account/rateLimits/read', 'account/read'
|
|
156
|
+
]);
|
|
157
|
+
expect(JSON.stringify(messages)).not.toMatch(/prompt|turn\/start/);
|
|
158
|
+
expect(CODEX_APP_SERVER_ARGS).toEqual(['-s', 'read-only', '-a', 'untrusted', 'app-server']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('uses account/read to distinguish logged-out CLI state', async () => {
|
|
162
|
+
const result = await probeCodexCapacity({
|
|
163
|
+
...context,
|
|
164
|
+
readFile: async () => '{}',
|
|
165
|
+
rpc: async () => ({ rateLimits: {}, account: { account: null } })
|
|
166
|
+
});
|
|
167
|
+
expect(result).toMatchObject({ authenticated: false, available: 'unknown' });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('never exposes tokens or raw auth content through failures', async () => {
|
|
171
|
+
const secrets = ['pat-secret-value', 'oauth-secret-value', 'refresh-secret-value'];
|
|
172
|
+
const result = await probeCodexCapacity({
|
|
173
|
+
...context,
|
|
174
|
+
readFile: async () => JSON.stringify({
|
|
175
|
+
personal_access_token: secrets[0],
|
|
176
|
+
tokens: { access_token: secrets[1], refresh_token: secrets[2] }
|
|
177
|
+
}),
|
|
178
|
+
fetch: vi.fn().mockRejectedValue(new Error(secrets.join(' '))),
|
|
179
|
+
rpc: async () => { throw new Error(secrets.join(' ')); }
|
|
180
|
+
});
|
|
181
|
+
const output = JSON.stringify(result);
|
|
182
|
+
for (const secret of secrets) expect(output).not.toContain(secret);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getCodexCapacityReport } from '../../capacity/index.js';
|
|
3
|
+
|
|
4
|
+
const checkedAt = '2026-08-09T10:00:00.000Z';
|
|
5
|
+
|
|
6
|
+
describe('getCodexCapacityReport', () => {
|
|
7
|
+
it('checks Codex installation before probing', async () => {
|
|
8
|
+
const probe = vi.fn(async context => ({
|
|
9
|
+
provider: 'codex', generatedAt: context.checkedAt,
|
|
10
|
+
authenticated: true, available: 'yes' as const, windows: [], creditsRemaining: null
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const report = await getCodexCapacityReport({
|
|
14
|
+
now: () => new Date(checkedAt),
|
|
15
|
+
path: '/usr/bin:/opt/bin',
|
|
16
|
+
access: async target => {
|
|
17
|
+
if (target !== '/opt/bin/codex') throw new Error('missing');
|
|
18
|
+
},
|
|
19
|
+
probe
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(probe).toHaveBeenCalledWith({ installed: true, checkedAt });
|
|
23
|
+
expect(report).toMatchObject({ provider: 'codex', generatedAt: checkedAt, available: 'yes' });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('redacts unexpected probe failures into a stable unknown result', async () => {
|
|
27
|
+
const report = await getCodexCapacityReport({
|
|
28
|
+
now: () => new Date(checkedAt),
|
|
29
|
+
path: '',
|
|
30
|
+
probe: async () => { throw new Error('private provider response'); }
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(report).toMatchObject({
|
|
34
|
+
provider: 'codex', available: 'unknown', authenticated: null, windows: []
|
|
35
|
+
});
|
|
36
|
+
expect(JSON.stringify(report)).not.toContain('private provider response');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -19,9 +19,9 @@ function dbPath(): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
describe('durable agents schema', () => {
|
|
22
|
-
it('migrates to version
|
|
22
|
+
it('migrates to version 4 with durable constraints and indexes', () => {
|
|
23
23
|
const connection = new DatabaseConnection({ dbPath: dbPath() });
|
|
24
|
-
expect(getSchemaVersion(connection)).toBe(
|
|
24
|
+
expect(getSchemaVersion(connection)).toBe(4);
|
|
25
25
|
const table = connection.queryOne<{ sql: string }>(
|
|
26
26
|
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'durable_agents'",
|
|
27
27
|
);
|
|
@@ -35,6 +35,31 @@ describe('durable agents schema', () => {
|
|
|
35
35
|
connection.close();
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
it('upgrades a version 3 database to nullable, unique provider sessions', () => {
|
|
39
|
+
const file = dbPath();
|
|
40
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
41
|
+
const raw = new Database(file);
|
|
42
|
+
raw.exec(`
|
|
43
|
+
CREATE TABLE durable_agents (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
provider_session_id TEXT NOT NULL UNIQUE
|
|
46
|
+
);
|
|
47
|
+
INSERT INTO durable_agents (id, provider_session_id) VALUES ('existing', 'existing-session');
|
|
48
|
+
PRAGMA user_version = 3;
|
|
49
|
+
`);
|
|
50
|
+
raw.close();
|
|
51
|
+
|
|
52
|
+
const connection = new DatabaseConnection({ dbPath: file });
|
|
53
|
+
expect(getSchemaVersion(connection)).toBe(4);
|
|
54
|
+
expect(() => connection.execute(
|
|
55
|
+
'INSERT INTO durable_agents (id, provider_session_id) VALUES (?, NULL)', ['codex'],
|
|
56
|
+
)).not.toThrow();
|
|
57
|
+
expect(() => connection.execute(
|
|
58
|
+
'INSERT INTO durable_agents (id, provider_session_id) VALUES (?, ?)', ['duplicate', 'existing-session'],
|
|
59
|
+
)).toThrow(/UNIQUE/i);
|
|
60
|
+
connection.close();
|
|
61
|
+
});
|
|
62
|
+
|
|
38
63
|
it('enforces case-insensitive names and active-run consistency but permits new providers', () => {
|
|
39
64
|
const connection = new DatabaseConnection({ dbPath: dbPath() });
|
|
40
65
|
const insert = (name: string, provider: string, state = 'ready') => connection.execute(`
|
|
@@ -57,7 +82,7 @@ describe('readonly DatabaseConnection', () => {
|
|
|
57
82
|
writable.close();
|
|
58
83
|
const before = fs.statSync(file).mtimeMs;
|
|
59
84
|
const readonly = new DatabaseConnection({ dbPath: file, readonly: true });
|
|
60
|
-
expect(readonly.queryOne<{ user_version: number }>('PRAGMA user_version')?.user_version).toBe(
|
|
85
|
+
expect(readonly.queryOne<{ user_version: number }>('PRAGMA user_version')?.user_version).toBe(4);
|
|
61
86
|
readonly.close();
|
|
62
87
|
expect(fs.statSync(file).mtimeMs).toBe(before);
|
|
63
88
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('CodexCliProbe', () => {
|
|
4
|
+
it('validates version, exec JSON/stdin, and resume capabilities without a model call', async () => {
|
|
5
|
+
const api = await import('../../index.js') as Record<string, unknown>;
|
|
6
|
+
expect(api).toHaveProperty('CodexCliProbe');
|
|
7
|
+
const exec = vi.fn()
|
|
8
|
+
.mockResolvedValueOnce({ stdout: 'codex-cli 0.147.0', stderr: '' })
|
|
9
|
+
.mockResolvedValueOnce({ stdout: 'Usage: codex exec [PROMPT]\n--json\n- read from stdin\nresume', stderr: '' })
|
|
10
|
+
.mockResolvedValueOnce({ stdout: 'Usage: codex exec resume [SESSION_ID] [PROMPT]\n--json\n- stdin', stderr: '' });
|
|
11
|
+
const Probe = api.CodexCliProbe as new (options: unknown) => any;
|
|
12
|
+
|
|
13
|
+
await expect(new Probe({ executable: 'fake-codex', exec }).validate()).resolves.toEqual({
|
|
14
|
+
executable: 'fake-codex', version: 'codex-cli 0.147.0',
|
|
15
|
+
});
|
|
16
|
+
expect(exec.mock.calls).toEqual([
|
|
17
|
+
['fake-codex', ['--version']],
|
|
18
|
+
['fake-codex', ['exec', '--help']],
|
|
19
|
+
['fake-codex', ['exec', 'resume', '--help']],
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects unsupported and unavailable CLIs with bounded sanitized errors', async () => {
|
|
24
|
+
const api = await import('../../index.js') as Record<string, unknown>;
|
|
25
|
+
const Probe = api.CodexCliProbe as new (options: unknown) => any;
|
|
26
|
+
const unsupported = new Probe({ exec: vi.fn()
|
|
27
|
+
.mockResolvedValueOnce({ stdout: 'version', stderr: '' })
|
|
28
|
+
.mockResolvedValueOnce({ stdout: 'exec', stderr: '' })
|
|
29
|
+
.mockResolvedValueOnce({ stdout: 'resume', stderr: '' }) });
|
|
30
|
+
await expect(unsupported.validate()).rejects.toMatchObject({ code: 'CODEX_CLI_UNSUPPORTED' });
|
|
31
|
+
const missingCommands = new Probe({ exec: vi.fn()
|
|
32
|
+
.mockResolvedValueOnce({ stdout: 'version', stderr: '' })
|
|
33
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
|
34
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' }) });
|
|
35
|
+
await expect(missingCommands.validate()).rejects.toMatchObject({ code: 'CODEX_CLI_UNSUPPORTED' });
|
|
36
|
+
|
|
37
|
+
const unavailable = new Probe({ exec: vi.fn().mockRejectedValue(new Error(`bad\0${'x'.repeat(1000)}`)) });
|
|
38
|
+
const error = await unavailable.validate().catch((value: Error & { code: string }) => value);
|
|
39
|
+
expect(error.code).toBe('CODEX_CLI_UNAVAILABLE');
|
|
40
|
+
expect(error.message).not.toContain('\0');
|
|
41
|
+
expect(error.message.length).toBeLessThan(600);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('reports an empty version response as unknown', async () => {
|
|
45
|
+
const api = await import('../../index.js') as Record<string, unknown>;
|
|
46
|
+
const Probe = api.CodexCliProbe as new (options: unknown) => any;
|
|
47
|
+
const exec = vi.fn()
|
|
48
|
+
.mockResolvedValueOnce({ stdout: ' \n', stderr: '' })
|
|
49
|
+
.mockResolvedValueOnce({ stdout: 'exec --json -', stderr: '' })
|
|
50
|
+
.mockResolvedValueOnce({ stdout: 'resume --json -', stderr: '' });
|
|
51
|
+
await expect(new Probe({ exec }).validate()).resolves.toMatchObject({ version: 'unknown' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('requires a standalone stdin dash rather than accepting flag hyphens', async () => {
|
|
55
|
+
const api = await import('../../index.js') as Record<string, unknown>;
|
|
56
|
+
const Probe = api.CodexCliProbe as new (options: unknown) => any;
|
|
57
|
+
const exec = vi.fn()
|
|
58
|
+
.mockResolvedValueOnce({ stdout: 'version', stderr: '' })
|
|
59
|
+
.mockResolvedValueOnce({ stdout: 'exec --json', stderr: '' })
|
|
60
|
+
.mockResolvedValueOnce({ stdout: 'resume --json', stderr: '' });
|
|
61
|
+
await expect(new Probe({ exec }).validate()).rejects.toMatchObject({ code: 'CODEX_CLI_UNSUPPORTED' });
|
|
62
|
+
});
|
|
63
|
+
});
|