@ai-devkit/agent-manager 0.19.1 → 0.21.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/CodexAdapter.test.js +507 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +12 -4
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +95 -7
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters/CodexAdapter.test.ts +410 -0
- package/src/adapters/CodexAdapter.ts +115 -8
|
@@ -86,6 +86,10 @@ describe('CodexAdapter', ()=>{
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
describe('detectAgents', ()=>{
|
|
89
|
+
async function useRealSessionMatcher() {
|
|
90
|
+
const actualMatching = await vi.importActual('../../utils/matching.js');
|
|
91
|
+
mockedMatchProcessesToSessions.mockImplementation(actualMatching.matchProcessesToSessions);
|
|
92
|
+
}
|
|
89
93
|
it('should return empty list when no codex process is running', async ()=>{
|
|
90
94
|
mockedListAgentProcesses.mockReturnValue([]);
|
|
91
95
|
const agents = await adapter.detectAgents();
|
|
@@ -194,6 +198,203 @@ describe('CodexAdapter', ()=>{
|
|
|
194
198
|
force: true
|
|
195
199
|
});
|
|
196
200
|
});
|
|
201
|
+
it('should match when session_meta timestamp and file birthtime both align with process start', async ()=>{
|
|
202
|
+
await useRealSessionMatcher();
|
|
203
|
+
const processStart = new Date('2026-03-18T15:00:00.000Z');
|
|
204
|
+
const sessionTimestamp = '2026-03-18T15:00:05.000Z';
|
|
205
|
+
const processes = [
|
|
206
|
+
{
|
|
207
|
+
pid: 101,
|
|
208
|
+
command: 'codex',
|
|
209
|
+
cwd: '/repo-a',
|
|
210
|
+
tty: 'ttys001',
|
|
211
|
+
startTime: processStart
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
215
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
216
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
|
|
217
|
+
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
218
|
+
const dateDir = path.join(sessionsDir, '2026', '03', '18');
|
|
219
|
+
fs.mkdirSync(dateDir, {
|
|
220
|
+
recursive: true
|
|
221
|
+
});
|
|
222
|
+
const sessionFile = path.join(dateDir, 'sess-aligned.jsonl');
|
|
223
|
+
fs.writeFileSync(sessionFile, [
|
|
224
|
+
JSON.stringify({
|
|
225
|
+
type: 'session_meta',
|
|
226
|
+
payload: {
|
|
227
|
+
id: 'sess-aligned',
|
|
228
|
+
timestamp: sessionTimestamp,
|
|
229
|
+
cwd: '/repo-a'
|
|
230
|
+
}
|
|
231
|
+
}),
|
|
232
|
+
JSON.stringify({
|
|
233
|
+
type: 'event',
|
|
234
|
+
timestamp: sessionTimestamp,
|
|
235
|
+
payload: {
|
|
236
|
+
type: 'token_count',
|
|
237
|
+
message: 'Aligned session'
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
].join('\n'));
|
|
241
|
+
adapter.codexSessionsDir = sessionsDir;
|
|
242
|
+
mockedBatchGetSessionFileBirthtimes.mockReturnValue([
|
|
243
|
+
{
|
|
244
|
+
sessionId: 'sess-aligned',
|
|
245
|
+
filePath: sessionFile,
|
|
246
|
+
projectDir: dateDir,
|
|
247
|
+
birthtimeMs: new Date(sessionTimestamp).getTime(),
|
|
248
|
+
resolvedCwd: ''
|
|
249
|
+
}
|
|
250
|
+
]);
|
|
251
|
+
const agents = await adapter.detectAgents();
|
|
252
|
+
expect(agents).toHaveLength(1);
|
|
253
|
+
expect(agents[0]).toMatchObject({
|
|
254
|
+
pid: 101,
|
|
255
|
+
sessionId: 'sess-aligned',
|
|
256
|
+
summary: 'Aligned session'
|
|
257
|
+
});
|
|
258
|
+
fs.rmSync(tmpDir, {
|
|
259
|
+
recursive: true,
|
|
260
|
+
force: true
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
it('should match late-created session files using session_meta timestamp', async ()=>{
|
|
264
|
+
await useRealSessionMatcher();
|
|
265
|
+
const processStart = new Date('2026-03-18T15:00:00.000Z');
|
|
266
|
+
const sessionTimestamp = '2026-03-18T15:00:10.000Z';
|
|
267
|
+
const lateBirthtime = new Date('2026-03-18T15:05:30.000Z').getTime();
|
|
268
|
+
const processes = [
|
|
269
|
+
{
|
|
270
|
+
pid: 102,
|
|
271
|
+
command: 'codex',
|
|
272
|
+
cwd: '/repo-a',
|
|
273
|
+
tty: 'ttys001',
|
|
274
|
+
startTime: processStart
|
|
275
|
+
}
|
|
276
|
+
];
|
|
277
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
278
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
279
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
|
|
280
|
+
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
281
|
+
const dateDir = path.join(sessionsDir, '2026', '03', '18');
|
|
282
|
+
fs.mkdirSync(dateDir, {
|
|
283
|
+
recursive: true
|
|
284
|
+
});
|
|
285
|
+
const sessionFile = path.join(dateDir, 'sess-late.jsonl');
|
|
286
|
+
fs.writeFileSync(sessionFile, [
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
type: 'session_meta',
|
|
289
|
+
payload: {
|
|
290
|
+
id: 'sess-late',
|
|
291
|
+
timestamp: sessionTimestamp,
|
|
292
|
+
cwd: '/repo-a'
|
|
293
|
+
}
|
|
294
|
+
}),
|
|
295
|
+
JSON.stringify({
|
|
296
|
+
type: 'event',
|
|
297
|
+
timestamp: sessionTimestamp,
|
|
298
|
+
payload: {
|
|
299
|
+
type: 'token_count',
|
|
300
|
+
message: 'Late file session'
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
].join('\n'));
|
|
304
|
+
adapter.codexSessionsDir = sessionsDir;
|
|
305
|
+
mockedBatchGetSessionFileBirthtimes.mockReturnValue([
|
|
306
|
+
{
|
|
307
|
+
sessionId: 'sess-late',
|
|
308
|
+
filePath: sessionFile,
|
|
309
|
+
projectDir: dateDir,
|
|
310
|
+
birthtimeMs: lateBirthtime,
|
|
311
|
+
resolvedCwd: ''
|
|
312
|
+
}
|
|
313
|
+
]);
|
|
314
|
+
const agents = await adapter.detectAgents();
|
|
315
|
+
expect(agents).toHaveLength(1);
|
|
316
|
+
expect(agents[0]).toMatchObject({
|
|
317
|
+
pid: 102,
|
|
318
|
+
sessionId: 'sess-late',
|
|
319
|
+
summary: 'Late file session'
|
|
320
|
+
});
|
|
321
|
+
expect(mockedMatchProcessesToSessions.mock.calls[0][1][0].birthtimeMs).toBe(new Date(sessionTimestamp).getTime());
|
|
322
|
+
fs.rmSync(tmpDir, {
|
|
323
|
+
recursive: true,
|
|
324
|
+
force: true
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
it.each([
|
|
328
|
+
[
|
|
329
|
+
'missing',
|
|
330
|
+
undefined
|
|
331
|
+
],
|
|
332
|
+
[
|
|
333
|
+
'invalid',
|
|
334
|
+
'not-a-date'
|
|
335
|
+
]
|
|
336
|
+
])('should fall back to file birthtime when session_meta timestamp is %s', async (_label, metaTimestamp)=>{
|
|
337
|
+
await useRealSessionMatcher();
|
|
338
|
+
const processStart = new Date('2026-03-18T15:00:00.000Z');
|
|
339
|
+
const fileBirthtime = new Date('2026-03-18T15:00:20.000Z').getTime();
|
|
340
|
+
const processes = [
|
|
341
|
+
{
|
|
342
|
+
pid: 103,
|
|
343
|
+
command: 'codex',
|
|
344
|
+
cwd: '/repo-a',
|
|
345
|
+
tty: 'ttys001',
|
|
346
|
+
startTime: processStart
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
350
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
351
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
|
|
352
|
+
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
353
|
+
const dateDir = path.join(sessionsDir, '2026', '03', '18');
|
|
354
|
+
fs.mkdirSync(dateDir, {
|
|
355
|
+
recursive: true
|
|
356
|
+
});
|
|
357
|
+
const payload = {
|
|
358
|
+
id: `sess-${_label}`,
|
|
359
|
+
cwd: '/repo-a'
|
|
360
|
+
};
|
|
361
|
+
if (metaTimestamp !== undefined) {
|
|
362
|
+
payload.timestamp = metaTimestamp;
|
|
363
|
+
}
|
|
364
|
+
const sessionFile = path.join(dateDir, `sess-${_label}.jsonl`);
|
|
365
|
+
fs.writeFileSync(sessionFile, [
|
|
366
|
+
JSON.stringify({
|
|
367
|
+
type: 'session_meta',
|
|
368
|
+
payload
|
|
369
|
+
}),
|
|
370
|
+
JSON.stringify({
|
|
371
|
+
type: 'event',
|
|
372
|
+
timestamp: '2026-03-18T15:00:30.000Z',
|
|
373
|
+
payload: {
|
|
374
|
+
type: 'token_count',
|
|
375
|
+
message: `${_label} timestamp session`
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
].join('\n'));
|
|
379
|
+
adapter.codexSessionsDir = sessionsDir;
|
|
380
|
+
mockedBatchGetSessionFileBirthtimes.mockReturnValue([
|
|
381
|
+
{
|
|
382
|
+
sessionId: `sess-${_label}`,
|
|
383
|
+
filePath: sessionFile,
|
|
384
|
+
projectDir: dateDir,
|
|
385
|
+
birthtimeMs: fileBirthtime,
|
|
386
|
+
resolvedCwd: ''
|
|
387
|
+
}
|
|
388
|
+
]);
|
|
389
|
+
const agents = await adapter.detectAgents();
|
|
390
|
+
expect(agents).toHaveLength(1);
|
|
391
|
+
expect(agents[0].sessionId).toBe(`sess-${_label}`);
|
|
392
|
+
expect(mockedMatchProcessesToSessions.mock.calls[0][1][0].birthtimeMs).toBe(fileBirthtime);
|
|
393
|
+
fs.rmSync(tmpDir, {
|
|
394
|
+
recursive: true,
|
|
395
|
+
force: true
|
|
396
|
+
});
|
|
397
|
+
});
|
|
197
398
|
it('should fall back to process-only for unmatched processes', async ()=>{
|
|
198
399
|
const processes = [
|
|
199
400
|
{
|
|
@@ -434,6 +635,169 @@ describe('CodexAdapter', ()=>{
|
|
|
434
635
|
});
|
|
435
636
|
}
|
|
436
637
|
});
|
|
638
|
+
it('should map a running Codex process from the hook session mapping', async ()=>{
|
|
639
|
+
const originalHome = process.env.HOME;
|
|
640
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-mapping-'));
|
|
641
|
+
process.env.HOME = tmpHome;
|
|
642
|
+
try {
|
|
643
|
+
const mappedAdapter = new CodexAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
|
|
644
|
+
const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
|
|
645
|
+
const dateDir = path.join(sessionsDir, '2026', '06', '26');
|
|
646
|
+
const mappingPath = path.join(tmpHome, '.codex', 'ai-devkit', 'sessions.json');
|
|
647
|
+
const sessionFile = path.join(dateDir, 'rollout-2026-06-26T09-56-12-sess-mapped.jsonl');
|
|
648
|
+
const recentTs = new Date().toISOString();
|
|
649
|
+
fs.mkdirSync(dateDir, {
|
|
650
|
+
recursive: true
|
|
651
|
+
});
|
|
652
|
+
fs.mkdirSync(path.dirname(mappingPath), {
|
|
653
|
+
recursive: true
|
|
654
|
+
});
|
|
655
|
+
fs.writeFileSync(sessionFile, [
|
|
656
|
+
JSON.stringify({
|
|
657
|
+
type: 'session_meta',
|
|
658
|
+
payload: {
|
|
659
|
+
id: 'sess-mapped',
|
|
660
|
+
timestamp: recentTs,
|
|
661
|
+
cwd: '/repo-mapped'
|
|
662
|
+
}
|
|
663
|
+
}),
|
|
664
|
+
JSON.stringify({
|
|
665
|
+
type: 'event',
|
|
666
|
+
timestamp: recentTs,
|
|
667
|
+
payload: {
|
|
668
|
+
type: 'agent_message',
|
|
669
|
+
message: 'mapped conversation'
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
].join('\n'));
|
|
673
|
+
fs.writeFileSync(mappingPath, JSON.stringify({
|
|
674
|
+
5151: sessionFile
|
|
675
|
+
}));
|
|
676
|
+
const processes = [
|
|
677
|
+
{
|
|
678
|
+
pid: 5151,
|
|
679
|
+
command: 'codex',
|
|
680
|
+
cwd: '/repo-mapped',
|
|
681
|
+
tty: 'ttys001',
|
|
682
|
+
startTime: new Date('2026-06-26T09:56:12.000Z')
|
|
683
|
+
}
|
|
684
|
+
];
|
|
685
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
686
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
687
|
+
const agents = await mappedAdapter.detectAgents();
|
|
688
|
+
expect(agents).toHaveLength(1);
|
|
689
|
+
expect(agents[0]).toMatchObject({
|
|
690
|
+
type: 'codex',
|
|
691
|
+
pid: 5151,
|
|
692
|
+
sessionId: 'sess-mapped',
|
|
693
|
+
projectPath: '/repo-mapped',
|
|
694
|
+
summary: 'mapped conversation',
|
|
695
|
+
sessionFilePath: sessionFile
|
|
696
|
+
});
|
|
697
|
+
expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
|
|
698
|
+
expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
|
|
699
|
+
} finally{
|
|
700
|
+
process.env.HOME = originalHome;
|
|
701
|
+
fs.rmSync(tmpHome, {
|
|
702
|
+
recursive: true,
|
|
703
|
+
force: true
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
it('should fall back to legacy matching when mapped session file cannot be parsed', async ()=>{
|
|
708
|
+
const originalHome = process.env.HOME;
|
|
709
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-mapping-fallback-'));
|
|
710
|
+
process.env.HOME = tmpHome;
|
|
711
|
+
try {
|
|
712
|
+
const mappedAdapter = new CodexAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
|
|
713
|
+
const sessionsDir = path.join(tmpHome, '.codex', 'sessions');
|
|
714
|
+
const dateDir = path.join(sessionsDir, '2026', '06', '26');
|
|
715
|
+
const mappingPath = path.join(tmpHome, '.codex', 'ai-devkit', 'sessions.json');
|
|
716
|
+
const badSessionFile = path.join(dateDir, 'bad.jsonl');
|
|
717
|
+
const fallbackSessionFile = path.join(dateDir, 'fallback.jsonl');
|
|
718
|
+
const recentTs = new Date().toISOString();
|
|
719
|
+
fs.mkdirSync(dateDir, {
|
|
720
|
+
recursive: true
|
|
721
|
+
});
|
|
722
|
+
fs.mkdirSync(path.dirname(mappingPath), {
|
|
723
|
+
recursive: true
|
|
724
|
+
});
|
|
725
|
+
fs.writeFileSync(badSessionFile, '{not-json');
|
|
726
|
+
fs.writeFileSync(fallbackSessionFile, [
|
|
727
|
+
JSON.stringify({
|
|
728
|
+
type: 'session_meta',
|
|
729
|
+
payload: {
|
|
730
|
+
id: 'sess-fallback',
|
|
731
|
+
timestamp: recentTs,
|
|
732
|
+
cwd: '/repo-fallback'
|
|
733
|
+
}
|
|
734
|
+
}),
|
|
735
|
+
JSON.stringify({
|
|
736
|
+
type: 'event',
|
|
737
|
+
timestamp: recentTs,
|
|
738
|
+
payload: {
|
|
739
|
+
type: 'token_count',
|
|
740
|
+
message: 'fallback conversation'
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
].join('\n'));
|
|
744
|
+
fs.writeFileSync(mappingPath, JSON.stringify({
|
|
745
|
+
6161: badSessionFile
|
|
746
|
+
}));
|
|
747
|
+
const processes = [
|
|
748
|
+
{
|
|
749
|
+
pid: 6161,
|
|
750
|
+
command: 'codex',
|
|
751
|
+
cwd: '/repo-fallback',
|
|
752
|
+
tty: 'ttys001',
|
|
753
|
+
startTime: new Date('2026-06-26T09:56:12.000Z')
|
|
754
|
+
}
|
|
755
|
+
];
|
|
756
|
+
const fallbackSession = {
|
|
757
|
+
sessionId: 'sess-fallback',
|
|
758
|
+
filePath: fallbackSessionFile,
|
|
759
|
+
projectDir: dateDir,
|
|
760
|
+
birthtimeMs: new Date('2026-06-26T09:56:15.000Z').getTime(),
|
|
761
|
+
resolvedCwd: ''
|
|
762
|
+
};
|
|
763
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
764
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
765
|
+
mockedBatchGetSessionFileBirthtimes.mockReturnValue([
|
|
766
|
+
fallbackSession
|
|
767
|
+
]);
|
|
768
|
+
mockedMatchProcessesToSessions.mockReturnValue([
|
|
769
|
+
{
|
|
770
|
+
process: processes[0],
|
|
771
|
+
session: {
|
|
772
|
+
...fallbackSession,
|
|
773
|
+
resolvedCwd: '/repo-fallback'
|
|
774
|
+
},
|
|
775
|
+
deltaMs: 3000
|
|
776
|
+
}
|
|
777
|
+
]);
|
|
778
|
+
const agents = await mappedAdapter.detectAgents();
|
|
779
|
+
expect(agents).toHaveLength(1);
|
|
780
|
+
expect(agents[0]).toMatchObject({
|
|
781
|
+
pid: 6161,
|
|
782
|
+
sessionId: 'sess-fallback',
|
|
783
|
+
summary: 'fallback conversation',
|
|
784
|
+
sessionFilePath: fallbackSessionFile
|
|
785
|
+
});
|
|
786
|
+
expect(mockedMatchProcessesToSessions).toHaveBeenCalledWith([
|
|
787
|
+
processes[0]
|
|
788
|
+
], expect.arrayContaining([
|
|
789
|
+
expect.objectContaining({
|
|
790
|
+
filePath: fallbackSessionFile
|
|
791
|
+
})
|
|
792
|
+
]));
|
|
793
|
+
} finally{
|
|
794
|
+
process.env.HOME = originalHome;
|
|
795
|
+
fs.rmSync(tmpHome, {
|
|
796
|
+
recursive: true,
|
|
797
|
+
force: true
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
});
|
|
437
801
|
});
|
|
438
802
|
describe('detectAgents — registry cache short-circuit', ()=>{
|
|
439
803
|
let tmpDir;
|
|
@@ -699,6 +1063,149 @@ describe('CodexAdapter', ()=>{
|
|
|
699
1063
|
const { sessions } = discoverSessions(processes);
|
|
700
1064
|
expect(sessions[0].resolvedCwd).toBe('');
|
|
701
1065
|
});
|
|
1066
|
+
it('should use session_meta timestamp as the matching birthtime when valid', ()=>{
|
|
1067
|
+
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
1068
|
+
adapter.codexSessionsDir = sessionsDir;
|
|
1069
|
+
const discoverSessions = adapter.discoverSessions.bind(adapter);
|
|
1070
|
+
const dateDir = path.join(sessionsDir, '2026', '03', '18');
|
|
1071
|
+
fs.mkdirSync(dateDir, {
|
|
1072
|
+
recursive: true
|
|
1073
|
+
});
|
|
1074
|
+
const sessionFile = path.join(dateDir, 'sess-meta-time.jsonl');
|
|
1075
|
+
const metaTimestamp = '2026-03-18T15:00:05.000Z';
|
|
1076
|
+
fs.writeFileSync(sessionFile, JSON.stringify({
|
|
1077
|
+
type: 'session_meta',
|
|
1078
|
+
payload: {
|
|
1079
|
+
id: 'sess-meta-time',
|
|
1080
|
+
timestamp: metaTimestamp,
|
|
1081
|
+
cwd: '/repo-a'
|
|
1082
|
+
}
|
|
1083
|
+
}));
|
|
1084
|
+
mockedBatchGetSessionFileBirthtimes.mockReturnValue([
|
|
1085
|
+
{
|
|
1086
|
+
sessionId: 'sess-meta-time',
|
|
1087
|
+
filePath: sessionFile,
|
|
1088
|
+
projectDir: dateDir,
|
|
1089
|
+
birthtimeMs: new Date('2026-03-18T15:05:30.000Z').getTime(),
|
|
1090
|
+
resolvedCwd: ''
|
|
1091
|
+
}
|
|
1092
|
+
]);
|
|
1093
|
+
const { sessions } = discoverSessions([
|
|
1094
|
+
{
|
|
1095
|
+
pid: 1,
|
|
1096
|
+
command: 'codex',
|
|
1097
|
+
cwd: '/repo-a',
|
|
1098
|
+
tty: '',
|
|
1099
|
+
startTime: new Date('2026-03-18T15:00:00Z')
|
|
1100
|
+
}
|
|
1101
|
+
]);
|
|
1102
|
+
expect(sessions[0].resolvedCwd).toBe('/repo-a');
|
|
1103
|
+
expect(sessions[0].birthtimeMs).toBe(new Date(metaTimestamp).getTime());
|
|
1104
|
+
});
|
|
1105
|
+
it('should tolerate malformed session_meta and unreadable files', ()=>{
|
|
1106
|
+
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
1107
|
+
adapter.codexSessionsDir = sessionsDir;
|
|
1108
|
+
const discoverSessions = adapter.discoverSessions.bind(adapter);
|
|
1109
|
+
const dateDir = path.join(sessionsDir, '2026', '03', '18');
|
|
1110
|
+
fs.mkdirSync(dateDir, {
|
|
1111
|
+
recursive: true
|
|
1112
|
+
});
|
|
1113
|
+
const malformedFile = path.join(dateDir, 'malformed.jsonl');
|
|
1114
|
+
const missingFile = path.join(dateDir, 'missing.jsonl');
|
|
1115
|
+
fs.writeFileSync(malformedFile, '{not valid json');
|
|
1116
|
+
mockedBatchGetSessionFileBirthtimes.mockReturnValue([
|
|
1117
|
+
{
|
|
1118
|
+
sessionId: 'malformed',
|
|
1119
|
+
filePath: malformedFile,
|
|
1120
|
+
projectDir: dateDir,
|
|
1121
|
+
birthtimeMs: 1710800324000,
|
|
1122
|
+
resolvedCwd: ''
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
sessionId: 'missing',
|
|
1126
|
+
filePath: missingFile,
|
|
1127
|
+
projectDir: dateDir,
|
|
1128
|
+
birthtimeMs: 1710800325000,
|
|
1129
|
+
resolvedCwd: ''
|
|
1130
|
+
}
|
|
1131
|
+
]);
|
|
1132
|
+
const result = discoverSessions([
|
|
1133
|
+
{
|
|
1134
|
+
pid: 1,
|
|
1135
|
+
command: 'codex',
|
|
1136
|
+
cwd: '/repo',
|
|
1137
|
+
tty: '',
|
|
1138
|
+
startTime: new Date('2026-03-18T15:00:00Z')
|
|
1139
|
+
}
|
|
1140
|
+
]);
|
|
1141
|
+
expect(result.sessions).toHaveLength(2);
|
|
1142
|
+
expect(result.sessions[0].resolvedCwd).toBe('');
|
|
1143
|
+
expect(result.sessions[1].resolvedCwd).toBe('');
|
|
1144
|
+
expect(result.contentCache.has(malformedFile)).toBe(true);
|
|
1145
|
+
expect(result.contentCache.has(missingFile)).toBe(false);
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
describe('findSessionFileById', ()=>{
|
|
1149
|
+
let tmpDir;
|
|
1150
|
+
let sessionsDir;
|
|
1151
|
+
const sessionId = 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee';
|
|
1152
|
+
beforeEach(()=>{
|
|
1153
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resume-find-'));
|
|
1154
|
+
sessionsDir = path.join(tmpDir, 'sessions');
|
|
1155
|
+
fs.mkdirSync(path.join(sessionsDir, '2026', '03', '18'), {
|
|
1156
|
+
recursive: true
|
|
1157
|
+
});
|
|
1158
|
+
adapter.codexSessionsDir = sessionsDir;
|
|
1159
|
+
});
|
|
1160
|
+
afterEach(()=>{
|
|
1161
|
+
fs.rmSync(tmpDir, {
|
|
1162
|
+
recursive: true,
|
|
1163
|
+
force: true
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
function writeResumeSession(timestamp) {
|
|
1167
|
+
const filePath = path.join(sessionsDir, '2026', '03', '18', `${sessionId}.jsonl`);
|
|
1168
|
+
const payload = {
|
|
1169
|
+
id: sessionId,
|
|
1170
|
+
cwd: '/repo-a'
|
|
1171
|
+
};
|
|
1172
|
+
if (timestamp !== undefined) {
|
|
1173
|
+
payload.timestamp = timestamp;
|
|
1174
|
+
}
|
|
1175
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
1176
|
+
type: 'session_meta',
|
|
1177
|
+
payload
|
|
1178
|
+
}));
|
|
1179
|
+
return filePath;
|
|
1180
|
+
}
|
|
1181
|
+
it('should return session_meta timestamp as birthtimeMs when valid', ()=>{
|
|
1182
|
+
const metaTimestamp = '2026-03-18T15:00:05.000Z';
|
|
1183
|
+
writeResumeSession(metaTimestamp);
|
|
1184
|
+
const findSessionFileById = adapter.findSessionFileById.bind(adapter);
|
|
1185
|
+
const session = findSessionFileById(sessionId);
|
|
1186
|
+
expect(session).toMatchObject({
|
|
1187
|
+
sessionId,
|
|
1188
|
+
resolvedCwd: '/repo-a',
|
|
1189
|
+
birthtimeMs: new Date(metaTimestamp).getTime()
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1192
|
+
it.each([
|
|
1193
|
+
[
|
|
1194
|
+
'missing',
|
|
1195
|
+
undefined
|
|
1196
|
+
],
|
|
1197
|
+
[
|
|
1198
|
+
'invalid',
|
|
1199
|
+
'not-a-date'
|
|
1200
|
+
]
|
|
1201
|
+
])('should fall back to stat birthtimeMs when session_meta timestamp is %s', (_label, metaTimestamp)=>{
|
|
1202
|
+
const sessionFile = writeResumeSession(metaTimestamp);
|
|
1203
|
+
const stat = fs.statSync(sessionFile);
|
|
1204
|
+
const findSessionFileById = adapter.findSessionFileById.bind(adapter);
|
|
1205
|
+
const session = findSessionFileById(sessionId);
|
|
1206
|
+
expect(session).not.toBeNull();
|
|
1207
|
+
expect(session.birthtimeMs).toBe(stat.birthtimeMs);
|
|
1208
|
+
});
|
|
702
1209
|
});
|
|
703
1210
|
describe('helper methods', ()=>{
|
|
704
1211
|
describe('determineStatus', ()=>{
|