agent-ecosystem/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts
777genius 816ff210b7 fix(recent-projects): keep codex partial warning only for empty results
Large Codex histories routinely hit the scan budget while still returning
useful project candidates, so the detailed "partial" warning was firing on
healthy degraded runs. Only warn when a degraded scan yields zero candidates;
otherwise the run is logged at info level with the degraded flag.
2026-05-31 17:55:01 +03:00

889 lines
28 KiB
TypeScript

import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { CodexSessionFileRecentProjectsSourceAdapter } from '@features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { LoggerPort } from '@features/recent-projects/core/application/ports/LoggerPort';
import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver';
function createLogger(): LoggerPort & {
info: ReturnType<typeof vi.fn>;
warn: ReturnType<typeof vi.fn>;
error: ReturnType<typeof vi.fn>;
} {
return {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
}
function getSessionFileCachePath(appDataPath: string): string {
return path.join(appDataPath, 'recent-projects', 'codex-session-files-index.json');
}
async function writeRollout(
filePath: string,
payload: {
cwd: string;
source?: string;
timestamp?: string;
branch?: string;
metadataPadding?: string;
},
mtime: Date
): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(
filePath,
`${JSON.stringify({
timestamp: payload.timestamp ?? mtime.toISOString(),
type: 'session_meta',
payload: {
id: path.basename(filePath, '.jsonl'),
timestamp: payload.timestamp ?? mtime.toISOString(),
cwd: payload.cwd,
source: payload.source ?? 'cli',
git: payload.branch ? { branch: payload.branch } : undefined,
...(payload.metadataPadding
? { base_instructions: { text: payload.metadataPadding } }
: {}),
},
})}\n${'x'.repeat(1024)}`,
'utf8'
);
await fs.utimes(filePath, mtime, mtime);
}
function deferred<T>(): {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (error: unknown) => void;
} {
let resolve!: (value: T) => void;
let reject!: (error: unknown) => void;
const promise = new Promise<T>((promiseResolve, promiseReject) => {
resolve = promiseResolve;
reject = promiseReject;
});
return { promise, resolve, reject };
}
describe('CodexSessionFileRecentProjectsSourceAdapter', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codex-session-files-'));
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
it('loads recent interactive Codex projects from session files', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue({
id: 'repo:alpha',
name: 'alpha',
}),
} as unknown as RecentProjectIdentityResolver;
const updatedAt = new Date('2026-04-14T12:00:00.000Z');
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'),
{
cwd: '/Users/test/projects/alpha',
branch: 'main',
},
updatedAt
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath: path.join(tempDir, 'app-data'),
});
await expect(adapter.list()).resolves.toEqual({
candidates: [
expect.objectContaining({
identity: 'repo:alpha',
displayName: 'alpha',
primaryPath: '/Users/test/projects/alpha',
lastActivityAt: updatedAt.getTime(),
providerIds: ['codex'],
sourceKind: 'codex',
openTarget: {
type: 'synthetic-path',
path: '/Users/test/projects/alpha',
},
branchName: 'main',
}),
],
degraded: false,
});
expect(identityResolver.resolve).toHaveBeenCalledWith('/Users/test/projects/alpha');
});
it('marks a Codex session project as deleted when its cwd is gone', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const fsProvider = {
exists: vi.fn().mockResolvedValue(false),
};
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-deleted.jsonl'),
{
cwd: '/Users/test/projects/deleted',
},
new Date('2026-04-14T12:00:00.000Z')
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1', fsProvider }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath: path.join(tempDir, 'app-data'),
});
const result = await adapter.list();
expect(result.candidates[0]).toEqual(
expect.objectContaining({
primaryPath: '/Users/test/projects/deleted',
filesystemState: 'deleted',
})
);
expect(fsProvider.exists).toHaveBeenCalledWith('/Users/test/projects/deleted');
});
it('loads Codex projects from large session metadata lines without parsing the full line', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const updatedAt = new Date('2026-04-14T12:00:00.000Z');
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-large.jsonl'),
{
cwd: '/Users/test/projects/large',
metadataPadding: 'x'.repeat(160_000),
},
updatedAt
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath: path.join(tempDir, 'app-data'),
});
const result = await adapter.list();
expect(result.candidates).toEqual([
expect.objectContaining({
primaryPath: '/Users/test/projects/large',
sourceKind: 'codex',
}),
]);
});
it('deduplicates sessions by cwd and keeps the newest activity', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '13', 'rollout-alpha-old.jsonl'),
{
cwd: '/Users/test/projects/alpha',
branch: 'old',
},
new Date('2026-04-13T12:00:00.000Z')
);
await writeRollout(
path.join(codexHome, 'archived_sessions', 'rollout-alpha-new.jsonl'),
{
cwd: '/Users/test/projects/alpha',
branch: 'new',
},
new Date('2026-04-14T12:00:00.000Z')
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath: path.join(tempDir, 'app-data'),
});
const result = await adapter.list();
expect(result.candidates).toHaveLength(1);
expect(result.candidates[0]).toEqual(
expect.objectContaining({
primaryPath: '/Users/test/projects/alpha',
lastActivityAt: Date.parse('2026-04-14T12:00:00.000Z'),
branchName: 'new',
})
);
expect(identityResolver.resolve).toHaveBeenCalledTimes(1);
});
it('keeps scanning past duplicate recent sessions to find more projects', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const baseTime = Date.parse('2026-04-14T12:00:00.000Z');
await Promise.all(
Array.from({ length: 130 }).map((_, index) =>
writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', `rollout-alpha-${index}.jsonl`),
{
cwd: '/Users/test/projects/alpha',
branch: 'main',
},
new Date(baseTime - index * 1000)
)
)
);
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-beta.jsonl'),
{
cwd: '/Users/test/projects/beta',
branch: 'main',
},
new Date(baseTime - 140_000)
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath: path.join(tempDir, 'app-data'),
});
const result = await adapter.list();
expect(result.candidates.map((candidate) => candidate.primaryPath)).toEqual([
'/Users/test/projects/alpha',
'/Users/test/projects/beta',
]);
});
it('reuses cached unchanged session metadata without reopening jsonl files', async () => {
const codexHome = path.join(tempDir, '.codex');
const appDataPath = path.join(tempDir, 'app-data');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const updatedAt = new Date('2026-04-14T12:00:00.000Z');
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'),
{
cwd: '/Users/test/projects/alpha',
branch: 'main',
},
updatedAt
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
await expect(adapter.list()).resolves.toEqual({
candidates: [
expect.objectContaining({
primaryPath: '/Users/test/projects/alpha',
branchName: 'main',
}),
],
degraded: false,
});
const openSpy = vi.spyOn(fs, 'open');
const cachedAdapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
await expect(cachedAdapter.list()).resolves.toEqual({
candidates: [
expect.objectContaining({
primaryPath: '/Users/test/projects/alpha',
branchName: 'main',
}),
],
degraded: false,
});
expect(openSpy).not.toHaveBeenCalled();
});
it('coalesces concurrent Codex session-file source reads', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const resolveResult = deferred<null>();
const identityResolver = {
resolve: vi.fn().mockReturnValue(resolveResult.promise),
} as unknown as RecentProjectIdentityResolver;
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'),
{
cwd: '/Users/test/projects/alpha',
branch: 'main',
},
new Date('2026-04-14T12:00:00.000Z')
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath: path.join(tempDir, 'app-data'),
});
const first = adapter.list();
await vi.waitFor(() => expect(identityResolver.resolve).toHaveBeenCalledTimes(1));
const second = adapter.list();
resolveResult.resolve(null);
await expect(Promise.all([first, second])).resolves.toEqual([
expect.objectContaining({ degraded: false }),
expect.objectContaining({ degraded: false }),
]);
expect(identityResolver.resolve).toHaveBeenCalledTimes(1);
});
it('does not reuse an in-flight local Codex session-file read for another active context', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const resolveResult = deferred<null>();
const identityResolver = {
resolve: vi.fn().mockReturnValue(resolveResult.promise),
} as unknown as RecentProjectIdentityResolver;
let activeContext: unknown = { type: 'local', id: 'local-1' };
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'),
{
cwd: '/Users/test/projects/alpha',
branch: 'main',
},
new Date('2026-04-14T12:00:00.000Z')
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => activeContext as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath: path.join(tempDir, 'app-data'),
});
const first = adapter.list();
await vi.waitFor(() => expect(identityResolver.resolve).toHaveBeenCalledTimes(1));
activeContext = { type: 'ssh', id: 'ssh-1' };
await expect(adapter.list()).resolves.toEqual({
candidates: [],
degraded: false,
});
resolveResult.resolve(null);
await expect(first).resolves.toEqual(expect.objectContaining({ degraded: false }));
expect(identityResolver.resolve).toHaveBeenCalledTimes(1);
});
it('invalidates cached session metadata when the jsonl fingerprint changes', async () => {
const codexHome = path.join(tempDir, '.codex');
const appDataPath = path.join(tempDir, 'app-data');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const sessionPath = path.join(
codexHome,
'sessions',
'2026',
'04',
'14',
'rollout-active.jsonl'
);
await writeRollout(
sessionPath,
{
cwd: '/Users/test/projects/alpha',
},
new Date('2026-04-14T12:00:00.000Z')
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
await expect(adapter.list()).resolves.toEqual({
candidates: [
expect.objectContaining({
primaryPath: '/Users/test/projects/alpha',
}),
],
degraded: false,
});
await writeRollout(
sessionPath,
{
cwd: '/Users/test/projects/beta',
},
new Date('2026-04-14T12:01:00.000Z')
);
const refreshedAdapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
await expect(refreshedAdapter.list()).resolves.toEqual({
candidates: [
expect.objectContaining({
primaryPath: '/Users/test/projects/beta',
}),
],
degraded: false,
});
});
it('does not let a slow jsonl read hold the whole source past its timeout budget', async () => {
const codexHome = path.join(tempDir, '.codex');
const appDataPath = path.join(tempDir, 'app-data');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const baseTime = Date.parse('2026-04-14T12:00:00.000Z');
const slowSessionPath = path.join(
codexHome,
'sessions',
'2026',
'04',
'14',
'rollout-slow.jsonl'
);
await writeRollout(
slowSessionPath,
{
cwd: '/Users/test/projects/slow',
},
new Date(baseTime)
);
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-fast.jsonl'),
{
cwd: '/Users/test/projects/fast',
},
new Date(baseTime - 1000)
);
const originalOpen = fs.open.bind(fs);
vi.spyOn(fs, 'open').mockImplementation(async (...args) => {
if (String(args[0]) === slowSessionPath) {
await new Promise((resolve) => setTimeout(resolve, 2500));
}
return originalOpen(...args);
});
const startedAt = Date.now();
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
const result = await adapter.list();
expect(Date.now() - startedAt).toBeLessThan(1600);
expect(result.degraded).toBe(true);
expect(result.candidates.map((candidate) => candidate.primaryPath)).toEqual([
'/Users/test/projects/fast',
]);
expect(logger.info).toHaveBeenCalledWith(
'codex session-file recent-projects source loaded',
expect.objectContaining({
degraded: true,
files: 2,
timedOutReads: 1,
})
);
});
it('keeps a partial warning when no recent project candidates are available', async () => {
const codexHome = path.join(tempDir, '.codex');
const appDataPath = path.join(tempDir, 'app-data');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const slowSessionPath = path.join(
codexHome,
'sessions',
'2026',
'04',
'14',
'rollout-slow.jsonl'
);
await writeRollout(
slowSessionPath,
{
cwd: '/Users/test/projects/slow',
},
new Date('2026-04-14T12:00:00.000Z')
);
const originalOpen = fs.open.bind(fs);
vi.spyOn(fs, 'open').mockImplementation(async (...args) => {
if (String(args[0]) === slowSessionPath) {
await new Promise((resolve) => setTimeout(resolve, 2500));
}
return originalOpen(...args);
});
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
const result = await adapter.list();
expect(result).toEqual({
candidates: [],
degraded: true,
});
expect(logger.warn).toHaveBeenCalledWith(
'codex session-file recent-projects source partial',
expect.objectContaining({
candidates: 0,
files: 1,
timedOutReads: 1,
})
);
});
it('ignores a corrupt session-file cache and rebuilds from session files', async () => {
const codexHome = path.join(tempDir, '.codex');
const appDataPath = path.join(tempDir, 'app-data');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'),
{
cwd: '/Users/test/projects/alpha',
},
new Date('2026-04-14T12:00:00.000Z')
);
const cachePath = getSessionFileCachePath(appDataPath);
await fs.mkdir(path.dirname(cachePath), { recursive: true });
await fs.writeFile(cachePath, '{not-json', 'utf8');
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
await expect(adapter.list()).resolves.toEqual({
candidates: [
expect.objectContaining({
primaryPath: '/Users/test/projects/alpha',
}),
],
degraded: false,
});
});
it('skips an oversized legacy session-file cache before reading it', async () => {
const codexHome = path.join(tempDir, '.codex');
const appDataPath = path.join(tempDir, 'app-data');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'),
{
cwd: '/Users/test/projects/alpha',
},
new Date('2026-04-14T12:00:00.000Z')
);
const cachePath = getSessionFileCachePath(appDataPath);
await fs.mkdir(path.dirname(cachePath), { recursive: true });
await fs.writeFile(cachePath, 'x', 'utf8');
await fs.truncate(cachePath, 4 * 1024 * 1024 + 1);
const readFileSpy = vi.spyOn(fs, 'readFile');
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
const result = await adapter.list();
expect(readFileSpy).not.toHaveBeenCalledWith(cachePath, 'utf8');
expect(result).toEqual({
candidates: [
expect.objectContaining({
primaryPath: '/Users/test/projects/alpha',
}),
],
degraded: false,
});
expect(logger.warn).toHaveBeenCalledWith(
'codex session-file recent-projects cache skipped - too large',
expect.objectContaining({
bytes: 4 * 1024 * 1024 + 1,
maxBytes: 4 * 1024 * 1024,
})
);
await expect(fs.stat(cachePath)).resolves.toEqual(
expect.objectContaining({
size: expect.any(Number),
})
);
expect((await fs.stat(cachePath)).size).toBeLessThan(4 * 1024 * 1024);
});
it('returns a degraded partial result under the uncached read cap and completes on the next cached pass', async () => {
const codexHome = path.join(tempDir, '.codex');
const appDataPath = path.join(tempDir, 'app-data');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const baseTime = Date.parse('2026-04-14T12:00:00.000Z');
await Promise.all(
Array.from({ length: 170 }).map((_, index) =>
writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', `rollout-alpha-${index}.jsonl`),
{
cwd: '/Users/test/projects/alpha',
branch: 'main',
},
new Date(baseTime - index * 1000)
)
)
);
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-beta.jsonl'),
{
cwd: '/Users/test/projects/beta',
branch: 'main',
},
new Date(baseTime - 200_000)
);
const firstAdapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
const firstResult = await firstAdapter.list();
expect(firstResult.degraded).toBe(true);
expect(firstResult.candidates.map((candidate) => candidate.primaryPath)).toEqual([
'/Users/test/projects/alpha',
]);
expect(logger.info).toHaveBeenCalledWith(
'codex session-file recent-projects source loaded',
expect.objectContaining({
degraded: true,
files: 171,
uncachedReads: 160,
skippedUncached: 11,
})
);
const secondAdapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
const secondResult = await secondAdapter.list();
expect(secondResult.degraded).toBe(false);
expect(secondResult.candidates.map((candidate) => candidate.primaryPath)).toEqual([
'/Users/test/projects/alpha',
'/Users/test/projects/beta',
]);
});
it('bounds discovered Codex session files before reading metadata', async () => {
const codexHome = path.join(tempDir, '.codex');
const appDataPath = path.join(tempDir, 'app-data');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn().mockResolvedValue(null),
} as unknown as RecentProjectIdentityResolver;
const baseTime = Date.parse('2026-04-14T12:00:00.000Z');
await Promise.all(
Array.from({ length: 505 }).map((_, index) =>
writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', `rollout-alpha-${index}.jsonl`),
{
cwd: '/Users/test/projects/alpha',
branch: 'main',
},
new Date(baseTime - index * 1000)
)
)
);
const readdirSpy = vi.spyOn(fs, 'readdir');
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath,
});
const result = await adapter.list();
expect(result.degraded).toBe(true);
expect(result.candidates.map((candidate) => candidate.primaryPath)).toEqual([
'/Users/test/projects/alpha',
]);
expect(logger.info).toHaveBeenCalledWith(
'codex session-file recent-projects source loaded',
expect.objectContaining({
degraded: true,
files: 500,
visitedFiles: 505,
droppedOlderFiles: 5,
uncachedReads: 160,
skippedUncached: 340,
})
);
expect(readdirSpy).not.toHaveBeenCalled();
});
it('skips non-interactive and ephemeral sessions', async () => {
const codexHome = path.join(tempDir, '.codex');
const logger = createLogger();
const identityResolver = {
resolve: vi.fn(),
} as unknown as RecentProjectIdentityResolver;
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-background.jsonl'),
{
cwd: '/Users/test/projects/background',
source: 'background',
},
new Date('2026-04-14T12:00:00.000Z')
);
await writeRollout(
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-temp.jsonl'),
{
cwd: '/private/var/folders/x/T/codex-agent-teams-appstyle-123',
source: 'cli',
},
new Date('2026-04-14T12:01:00.000Z')
);
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome,
appDataPath: path.join(tempDir, 'app-data'),
});
await expect(adapter.list()).resolves.toEqual({
candidates: [],
degraded: false,
});
expect(identityResolver.resolve).not.toHaveBeenCalled();
});
it('returns an empty healthy result when Codex session folders are absent', async () => {
const logger = createLogger();
const identityResolver = {
resolve: vi.fn(),
} as unknown as RecentProjectIdentityResolver;
const adapter = new CodexSessionFileRecentProjectsSourceAdapter({
getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never,
getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never,
identityResolver,
logger,
codexHome: path.join(tempDir, 'missing-codex-home'),
appDataPath: path.join(tempDir, 'app-data'),
});
await expect(adapter.list()).resolves.toEqual({
candidates: [],
degraded: false,
});
});
});