@ai-devkit/agent-manager 0.29.0 → 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__/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 +25 -28
- 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/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 +17 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -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__/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 +11 -21
- 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/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 +31 -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);
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { DurableAgentRepository } from '../../durable/DurableAgentRepository.js';
|
|
6
|
+
|
|
7
|
+
const SESSION = '22222222-2222-4222-8222-222222222222';
|
|
8
|
+
const OTHER_SESSION = '33333333-3333-4333-8333-333333333333';
|
|
9
|
+
const roots: string[] = [];
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function fixture() {
|
|
16
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-durable-repository-'));
|
|
17
|
+
roots.push(root);
|
|
18
|
+
const cwd = path.join(root, 'project');
|
|
19
|
+
fs.mkdirSync(cwd);
|
|
20
|
+
const processInspector = { getIdentity: (pid: number) => ({ pid, startedAt: 'owner-start' }) };
|
|
21
|
+
return { cwd, repository: new DurableAgentRepository({ dbPath: path.join(root, 'agents.db'), processInspector }) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('Codex durable-agent repository binding', () => {
|
|
25
|
+
it('creates Codex agents unbound and binds during the owned run', async () => {
|
|
26
|
+
const { cwd, repository } = fixture();
|
|
27
|
+
const agent = await repository.create({ name: 'reviewer', cwd, provider: 'codex' });
|
|
28
|
+
expect(agent).toMatchObject({ provider: 'codex', mode: 'durable', providerSessionId: null });
|
|
29
|
+
|
|
30
|
+
const run = await repository.acquireRun(agent.id);
|
|
31
|
+
const bound = await repository.bindProviderSession(agent.id, run.token, SESSION);
|
|
32
|
+
expect(bound.providerSessionId).toBe(SESSION);
|
|
33
|
+
await expect(repository.bindProviderSession(agent.id, run.token, SESSION))
|
|
34
|
+
.resolves.toMatchObject({ providerSessionId: SESSION });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('rejects invalid, stale, replacement, and duplicate bindings', async () => {
|
|
38
|
+
const { cwd, repository } = fixture();
|
|
39
|
+
const first = await repository.create({ name: 'first', cwd, provider: 'codex' });
|
|
40
|
+
const second = await repository.create({ name: 'second', cwd, provider: 'codex' });
|
|
41
|
+
const firstRun = await repository.acquireRun(first.id);
|
|
42
|
+
const secondRun = await repository.acquireRun(second.id);
|
|
43
|
+
|
|
44
|
+
await expect(repository.bindProviderSession(first.id, 'stale', SESSION))
|
|
45
|
+
.rejects.toMatchObject({ code: 'DURABLE_AGENT_REPOSITORY' });
|
|
46
|
+
await expect(repository.bindProviderSession(first.id, firstRun.token, 'invalid'))
|
|
47
|
+
.rejects.toMatchObject({ code: 'DURABLE_AGENT_REPOSITORY' });
|
|
48
|
+
await repository.bindProviderSession(first.id, firstRun.token, SESSION);
|
|
49
|
+
await expect(repository.bindProviderSession(first.id, firstRun.token, OTHER_SESSION))
|
|
50
|
+
.rejects.toMatchObject({ code: 'CODEX_SESSION_MISMATCH' });
|
|
51
|
+
await expect(repository.bindProviderSession(second.id, secondRun.token, SESSION))
|
|
52
|
+
.rejects.toMatchObject({ code: 'CODEX_SESSION_MISMATCH' });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
CodexPrintError,
|
|
7
|
+
CodexPrintAgentService,
|
|
8
|
+
DurableAgentRepository,
|
|
9
|
+
type ProcessInspector,
|
|
10
|
+
} from '../../index.js';
|
|
11
|
+
|
|
12
|
+
const SESSION = '22222222-2222-4222-8222-222222222222';
|
|
13
|
+
const roots: string[] = [];
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('Codex print-agent fake-provider journey', () => {
|
|
20
|
+
it('creates unbound, then binds and explicitly resumes the provider-minted session', async () => {
|
|
21
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-print-integration-'));
|
|
22
|
+
roots.push(root);
|
|
23
|
+
const cwd = path.join(root, 'project');
|
|
24
|
+
fs.mkdirSync(cwd);
|
|
25
|
+
const processInspector: ProcessInspector = {
|
|
26
|
+
getIdentity: (pid) => ({ pid, startedAt: `process-${pid}` }),
|
|
27
|
+
};
|
|
28
|
+
const repository = new DurableAgentRepository({ dbPath: path.join(root, 'agents.db'), processInspector });
|
|
29
|
+
const runner = {
|
|
30
|
+
run: vi.fn().mockImplementation(async (request) => {
|
|
31
|
+
await request.onSpawn({ pid: 42, startedAt: 'process-42' });
|
|
32
|
+
await request.onSession(SESSION);
|
|
33
|
+
return { sessionId: SESSION, result: `answer:${request.prompt}`, messages: [`answer:${request.prompt}`], exitCode: 0 };
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
const service = new CodexPrintAgentService({
|
|
37
|
+
repository, probe: { validate: vi.fn() }, runner,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const created = await service.create({ name: 'reviewer', cwd });
|
|
41
|
+
expect(created).toMatchObject({ provider: 'codex', providerSessionId: null, sessionHealth: 'uninitialized' });
|
|
42
|
+
|
|
43
|
+
const first = await service.send(created.id, 'first secret');
|
|
44
|
+
expect(first).toMatchObject({ result: 'answer:first secret' });
|
|
45
|
+
const bound = await repository.getById(created.id);
|
|
46
|
+
expect(bound?.providerSessionId).toBe(first.sessionId);
|
|
47
|
+
await expect(service.send(created.id, 'follow up')).resolves.toMatchObject({ result: 'answer:follow up' });
|
|
48
|
+
|
|
49
|
+
expect(runner.run.mock.calls[0][0].agent).toMatchObject({ providerSessionId: null });
|
|
50
|
+
expect(runner.run.mock.calls[1][0].agent).toMatchObject({ providerSessionId: SESSION });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('retains a first-run binding when the provider fails after thread start', async () => {
|
|
54
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-print-bind-failure-'));
|
|
55
|
+
roots.push(root);
|
|
56
|
+
const cwd = path.join(root, 'project');
|
|
57
|
+
fs.mkdirSync(cwd);
|
|
58
|
+
const processInspector: ProcessInspector = {
|
|
59
|
+
getIdentity: (pid) => ({ pid, startedAt: `process-${pid}` }),
|
|
60
|
+
};
|
|
61
|
+
const repository = new DurableAgentRepository({ dbPath: path.join(root, 'agents.db'), processInspector });
|
|
62
|
+
const runner = {
|
|
63
|
+
run: vi.fn().mockImplementation(async (request) => {
|
|
64
|
+
await request.onSpawn({ pid: 42, startedAt: 'process-42' });
|
|
65
|
+
await request.onSession(SESSION);
|
|
66
|
+
throw new CodexPrintError('Codex print run failed.', 'CODEX_PROCESS');
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
const service = new CodexPrintAgentService({
|
|
70
|
+
repository, probe: { validate: vi.fn() }, runner,
|
|
71
|
+
});
|
|
72
|
+
const created = await service.create({ name: 'reviewer', cwd });
|
|
73
|
+
|
|
74
|
+
await expect(service.send(created.id, 'secret')).rejects.toMatchObject({ code: 'CODEX_PROCESS' });
|
|
75
|
+
expect((await repository.getById(created.id))).toMatchObject({
|
|
76
|
+
providerSessionId: SESSION, state: 'degraded', sessionHealth: 'unknown',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|