agent-ecosystem/test/main/services/discovery/ProjectScanner.sessionIndex.test.ts
2026-05-04 17:21:05 +03:00

727 lines
25 KiB
TypeScript

import * as crypto from 'node:crypto';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { ProjectScanner } from '../../../../src/main/services/discovery/ProjectScanner';
import { SessionMetadataIndex } from '../../../../src/main/services/discovery/SessionMetadataIndex';
import { subprojectRegistry } from '../../../../src/main/services/discovery/SubprojectRegistry';
import type { FileSystemProvider } from '../../../../src/main/services/infrastructure/FileSystemProvider';
function createSessionLine(content: string, timestamp = '2026-01-01T00:00:00.000Z'): string {
return JSON.stringify({
uuid: crypto.randomUUID(),
type: 'user',
message: { role: 'user', content },
timestamp,
});
}
function createNoiseLine(): string {
return JSON.stringify({
uuid: crypto.randomUUID(),
type: 'system',
content: 'noise',
timestamp: '2026-01-01T00:00:00.000Z',
});
}
function createSessionLineWithCwd(content: string, cwd: string): string {
return JSON.stringify({
uuid: crypto.randomUUID(),
type: 'user',
cwd,
message: { role: 'user', content },
timestamp: '2026-01-01T00:00:00.000Z',
});
}
function createSshLikeLocalProvider(): FileSystemProvider {
return {
type: 'ssh',
exists: (filePath: string) => Promise.resolve(fs.existsSync(filePath)),
readFile: async (filePath: string, encoding: BufferEncoding = 'utf8') =>
fs.promises.readFile(filePath, encoding),
stat: async (filePath: string) => {
const stats = await fs.promises.stat(filePath);
return {
size: stats.size,
mtimeMs: stats.mtimeMs,
birthtimeMs: stats.birthtimeMs,
isFile: () => stats.isFile(),
isDirectory: () => stats.isDirectory(),
};
},
readdir: async (dirPath: string) => {
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
return Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(dirPath, entry.name);
const stats = await fs.promises.stat(entryPath);
return {
name: entry.name,
isFile: () => entry.isFile(),
isDirectory: () => entry.isDirectory(),
size: stats.size,
mtimeMs: stats.mtimeMs,
birthtimeMs: stats.birthtimeMs,
};
})
);
},
createReadStream: (filePath: string, opts?: { start?: number; encoding?: BufferEncoding }) =>
fs.createReadStream(filePath, opts),
dispose: () => undefined,
};
}
function createReadStreamFailingLocalProvider(): FileSystemProvider {
return {
...createSshLikeLocalProvider(),
type: 'local',
createReadStream: () => {
throw new Error('session body should not be read when a fresh index entry exists');
},
};
}
function readIndexSessions(
indexDir: string,
projectDir: string
): Record<string, Record<string, unknown>> {
const raw = fs.readFileSync(SessionMetadataIndex.getIndexPath(indexDir, projectDir), 'utf8');
const parsed = JSON.parse(raw) as { sessions?: Record<string, Record<string, unknown>> };
return parsed.sessions ?? {};
}
function sortStrings(values: string[]): string[] {
return [...values].sort((a, b) => a.localeCompare(b));
}
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | 'timeout'> {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<'timeout'>((resolve) => {
timeoutId = setTimeout(() => resolve('timeout'), ms);
}),
]);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
describe('ProjectScanner session metadata index', () => {
const tempDirs: string[] = [];
afterEach(() => {
subprojectRegistry.clear();
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tempDirs.length = 0;
});
function createFixture(): {
projectsDir: string;
indexDir: string;
projectId: string;
projectDir: string;
sessionPath: string;
} {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scanner-index-'));
tempDirs.push(rootDir);
const projectsDir = path.join(rootDir, 'projects');
const indexDir = path.join(rootDir, 'session-index');
const projectId = '-Users-test-indexed-project';
const projectDir = path.join(projectsDir, projectId);
const sessionPath = path.join(projectDir, 'session-1.jsonl');
fs.mkdirSync(projectDir, { recursive: true });
return {
projectsDir,
indexDir,
projectId,
projectDir,
sessionPath,
};
}
it('does not serve stale indexed metadata after a session file changes', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('old indexed title')}\n`, 'utf8');
const firstScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const firstSessions = await firstScanner.listSessions(projectId);
expect(firstSessions).toHaveLength(1);
expect(firstSessions[0].firstMessage).toBe('old indexed title');
await firstScanner.flushSessionMetadataIndexForTesting();
const indexPath = SessionMetadataIndex.getIndexPath(indexDir, projectDir);
expect(fs.existsSync(indexPath)).toBe(true);
fs.writeFileSync(
sessionPath,
`${createSessionLine('new indexed title with different size')}\n`,
'utf8'
);
const future = new Date('2026-01-01T00:01:00.000Z');
fs.utimesSync(sessionPath, future, future);
const secondScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const secondSessions = await secondScanner.listSessions(projectId);
expect(secondSessions).toHaveLength(1);
expect(secondSessions[0].firstMessage).toBe('new indexed title with different size');
});
it('falls back to live parsing when the persisted index is corrupt', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('live title')}\n`, 'utf8');
fs.mkdirSync(indexDir, { recursive: true });
fs.writeFileSync(
SessionMetadataIndex.getIndexPath(indexDir, projectDir),
'{not valid json',
'utf8'
);
const scanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await scanner.listSessions(projectId);
expect(sessions).toHaveLength(1);
expect(sessions[0].firstMessage).toBe('live title');
});
it('ignores malformed indexed metadata and reparses the live session file', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('live metadata title')}\n`, 'utf8');
const stats = fs.statSync(sessionPath);
fs.mkdirSync(indexDir, { recursive: true });
fs.writeFileSync(
SessionMetadataIndex.getIndexPath(indexDir, projectDir),
JSON.stringify({
schemaVersion: 1,
projectStorageDir: projectDir,
updatedAt: Date.now(),
sessions: {
[sessionPath]: {
sessionId: 'session-1',
filePath: sessionPath,
mtimeMs: stats.mtimeMs,
size: stats.size,
birthtimeMs: stats.birthtimeMs,
hasContent: true,
metadata: {
firstUserMessage: { text: 42, timestamp: 'bad' },
messageCount: 'bad',
isOngoing: 'bad',
gitBranch: null,
},
updatedAt: Date.now(),
},
},
}),
'utf8'
);
const scanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await scanner.listSessions(projectId);
await scanner.flushSessionMetadataIndexForTesting();
expect(sessions).toHaveLength(1);
expect(sessions[0].firstMessage).toBe('live metadata title');
expect(
(
readIndexSessions(indexDir, projectDir)[sessionPath].metadata as {
firstUserMessage?: { text?: string };
}
).firstUserMessage?.text
).toBe('live metadata title');
});
it('does not trust content presence from an entry with malformed metadata', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(
sessionPath,
`${createSessionLine('visible despite corrupt index')}\n`,
'utf8'
);
const stats = fs.statSync(sessionPath);
fs.mkdirSync(indexDir, { recursive: true });
fs.writeFileSync(
SessionMetadataIndex.getIndexPath(indexDir, projectDir),
JSON.stringify({
schemaVersion: 1,
projectStorageDir: projectDir,
updatedAt: Date.now(),
sessions: {
[sessionPath]: {
sessionId: 'session-1',
filePath: sessionPath,
mtimeMs: stats.mtimeMs,
size: stats.size,
birthtimeMs: stats.birthtimeMs,
hasContent: false,
metadata: {
firstUserMessage: { text: 42, timestamp: 'bad' },
messageCount: 'bad',
isOngoing: 'bad',
gitBranch: null,
},
updatedAt: Date.now(),
},
},
}),
'utf8'
);
const scanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await scanner.listSessions(projectId);
expect(sessions).toHaveLength(1);
expect(sessions[0].firstMessage).toBe('visible despite corrupt index');
});
it('does not trust content presence when the indexed session id mismatches the file', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('visible despite id mismatch')}\n`, 'utf8');
const stats = fs.statSync(sessionPath);
fs.mkdirSync(indexDir, { recursive: true });
fs.writeFileSync(
SessionMetadataIndex.getIndexPath(indexDir, projectDir),
JSON.stringify({
schemaVersion: 1,
projectStorageDir: projectDir,
updatedAt: Date.now(),
sessions: {
[sessionPath]: {
sessionId: 'different-session',
filePath: sessionPath,
mtimeMs: stats.mtimeMs,
size: stats.size,
birthtimeMs: stats.birthtimeMs,
hasContent: false,
updatedAt: Date.now(),
},
},
}),
'utf8'
);
const scanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await scanner.listSessions(projectId);
expect(sessions).toHaveLength(1);
expect(sessions[0].firstMessage).toBe('visible despite id mismatch');
});
it('does not trust content presence from an entry with invalid numeric signature fields', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(
sessionPath,
`${createSessionLine('visible despite invalid signature')}\n`,
'utf8'
);
const stats = fs.statSync(sessionPath);
fs.mkdirSync(indexDir, { recursive: true });
fs.writeFileSync(
SessionMetadataIndex.getIndexPath(indexDir, projectDir),
JSON.stringify({
schemaVersion: 1,
projectStorageDir: projectDir,
updatedAt: Date.now(),
sessions: {
[sessionPath]: {
sessionId: 'session-1',
filePath: sessionPath,
mtimeMs: stats.mtimeMs,
size: stats.size,
birthtimeMs: -1,
hasContent: false,
updatedAt: Date.now(),
},
},
}),
'utf8'
);
const scanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await scanner.listSessions(projectId);
expect(sessions).toHaveLength(1);
expect(sessions[0].firstMessage).toBe('visible despite invalid signature');
});
it('does not hide a session when a stale no-content index entry exists', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createNoiseLine()}\n`, 'utf8');
const firstScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const noiseSessions = await firstScanner.listSessions(projectId);
expect(noiseSessions).toHaveLength(0);
await firstScanner.flushSessionMetadataIndexForTesting();
expect(readIndexSessions(indexDir, projectDir)[sessionPath].hasContent).toBe(false);
fs.writeFileSync(sessionPath, `${createSessionLine('visible after stale false')}\n`, 'utf8');
const future = new Date('2026-01-01T00:02:00.000Z');
fs.utimesSync(sessionPath, future, future);
const secondScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const liveSessions = await secondScanner.listSessions(projectId);
expect(liveSessions).toHaveLength(1);
expect(liveSessions[0].firstMessage).toBe('visible after stale false');
});
it('ignores indexed metadata containing non-finite numeric fields', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('live finite numeric title')}\n`, 'utf8');
const stats = fs.statSync(sessionPath);
fs.mkdirSync(indexDir, { recursive: true });
fs.writeFileSync(
SessionMetadataIndex.getIndexPath(indexDir, projectDir),
`{
"schemaVersion": 1,
"projectStorageDir": ${JSON.stringify(projectDir)},
"updatedAt": ${Date.now()},
"sessions": {
${JSON.stringify(sessionPath)}: {
"sessionId": "session-1",
"filePath": ${JSON.stringify(sessionPath)},
"mtimeMs": ${stats.mtimeMs},
"size": ${stats.size},
"birthtimeMs": ${stats.birthtimeMs},
"hasContent": true,
"metadata": {
"firstUserMessage": {
"text": "indexed title with corrupt numeric field",
"timestamp": "2026-01-01T00:00:00.000Z"
},
"messageCount": 1,
"isOngoing": false,
"gitBranch": null,
"model": null,
"contextConsumption": 1e999
},
"updatedAt": ${Date.now()}
}
}
}`,
'utf8'
);
const scanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await scanner.listSessions(projectId);
expect(sessions).toHaveLength(1);
expect(sessions[0].firstMessage).toBe('live finite numeric title');
});
it('ignores indexed metadata with corrupt phase breakdown fields', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('live phase breakdown title')}\n`, 'utf8');
const stats = fs.statSync(sessionPath);
fs.mkdirSync(indexDir, { recursive: true });
fs.writeFileSync(
SessionMetadataIndex.getIndexPath(indexDir, projectDir),
`{
"schemaVersion": 1,
"projectStorageDir": ${JSON.stringify(projectDir)},
"updatedAt": ${Date.now()},
"sessions": {
${JSON.stringify(sessionPath)}: {
"sessionId": "session-1",
"filePath": ${JSON.stringify(sessionPath)},
"mtimeMs": ${stats.mtimeMs},
"size": ${stats.size},
"birthtimeMs": ${stats.birthtimeMs},
"hasContent": true,
"metadata": {
"firstUserMessage": {
"text": "indexed title with corrupt phase breakdown",
"timestamp": "2026-01-01T00:00:00.000Z"
},
"messageCount": 1,
"isOngoing": false,
"gitBranch": null,
"model": null,
"contextConsumption": 10,
"compactionCount": 1,
"phaseBreakdown": [
{
"phaseNumber": 1,
"contribution": 10,
"peakTokens": 10,
"postCompaction": 1e999
}
]
},
"updatedAt": ${Date.now()}
}
}
}`,
'utf8'
);
const scanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await scanner.listSessions(projectId);
expect(sessions).toHaveLength(1);
expect(sessions[0].firstMessage).toBe('live phase breakdown title');
});
it('does not retry forever when the index cannot be persisted', async () => {
const { projectsDir, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('best effort cache')}\n`, 'utf8');
const stats = fs.statSync(sessionPath);
const blockedIndexRoot = path.join(path.dirname(projectsDir), 'blocked-index-root');
fs.writeFileSync(blockedIndexRoot, 'not a directory', 'utf8');
const index = new SessionMetadataIndex({
rootDir: blockedIndexRoot,
persistDelayMs: 0,
});
await index.setMetadata(
{
sessionId: 'session-1',
filePath: sessionPath,
mtimeMs: stats.mtimeMs,
size: stats.size,
birthtimeMs: stats.birthtimeMs,
},
{
firstUserMessage: {
text: 'best effort cache',
timestamp: '2026-01-01T00:00:00.000Z',
},
messageCount: 1,
isOngoing: false,
gitBranch: null,
model: null,
}
);
const flushResult = await withTimeout(
index.flushForTesting().then(() => 'resolved'),
500
);
expect(flushResult).toBe('resolved');
expect(fs.existsSync(SessionMetadataIndex.getIndexPath(blockedIndexRoot, projectDir))).toBe(
false
);
});
it('persists content filtering and metadata across scanner instances for paginated listing', async () => {
const { projectsDir, indexDir, projectId, projectDir } = createFixture();
const visiblePath = path.join(projectDir, 'session-visible.jsonl');
const noisePath = path.join(projectDir, 'session-noise.jsonl');
fs.writeFileSync(visiblePath, `${createSessionLine('visible title')}\n`, 'utf8');
fs.writeFileSync(noisePath, `${createNoiseLine()}\n`, 'utf8');
const firstScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const firstPage = await firstScanner.listSessionsPaginated(projectId, null, 10, {
includeTotalCount: true,
prefilterAll: true,
metadataLevel: 'deep',
});
expect(firstPage.sessions.map((session) => session.id)).toEqual(['session-visible']);
expect(firstPage.totalCount).toBe(1);
await firstScanner.flushSessionMetadataIndexForTesting();
const indexedSessions = readIndexSessions(indexDir, projectDir);
expect(sortStrings(Object.keys(indexedSessions))).toEqual(
sortStrings([noisePath, visiblePath])
);
expect(indexedSessions[visiblePath].hasContent).toBe(true);
expect(indexedSessions[noisePath].hasContent).toBe(false);
expect(
(
indexedSessions[visiblePath].metadata as {
firstUserMessage?: { text?: string };
}
).firstUserMessage?.text
).toBe('visible title');
const secondScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const secondPage = await secondScanner.listSessionsPaginated(projectId, null, 10, {
includeTotalCount: true,
prefilterAll: true,
metadataLevel: 'deep',
});
expect(secondPage.sessions.map((session) => session.id)).toEqual(['session-visible']);
expect(secondPage.sessions[0].firstMessage).toBe('visible title');
expect(secondPage.totalCount).toBe(1);
});
it('serves fresh indexed listing data without reopening the session body', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('fresh cached title')}\n`, 'utf8');
const firstScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const firstSessions = await firstScanner.listSessions(projectId);
expect(firstSessions).toHaveLength(1);
await firstScanner.flushSessionMetadataIndexForTesting();
expect(readIndexSessions(indexDir, projectDir)[sessionPath].hasContent).toBe(true);
const secondScanner = new ProjectScanner(
projectsDir,
undefined,
createReadStreamFailingLocalProvider(),
{
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
}
);
const secondSessions = await secondScanner.listSessions(projectId);
expect(secondSessions).toHaveLength(1);
expect(secondSessions[0].firstMessage).toBe('fresh cached title');
});
it('does not persist a local session index for ssh filesystem providers', async () => {
const { projectsDir, indexDir, projectId, projectDir, sessionPath } = createFixture();
fs.writeFileSync(sessionPath, `${createSessionLine('ssh provider title')}\n`, 'utf8');
const scanner = new ProjectScanner(projectsDir, undefined, createSshLikeLocalProvider(), {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await scanner.listSessions(projectId);
await scanner.flushSessionMetadataIndexForTesting();
expect(sessions).toHaveLength(1);
expect(sessions[0].firstMessage).toBe('ssh provider title');
expect(fs.existsSync(SessionMetadataIndex.getIndexPath(indexDir, projectDir))).toBe(false);
});
it('prunes deleted session entries from the persisted index after relisting', async () => {
const { projectsDir, indexDir, projectId, projectDir } = createFixture();
const keepPath = path.join(projectDir, 'session-keep.jsonl');
const deletePath = path.join(projectDir, 'session-delete.jsonl');
fs.writeFileSync(keepPath, `${createSessionLine('keep title')}\n`, 'utf8');
fs.writeFileSync(deletePath, `${createSessionLine('delete title')}\n`, 'utf8');
const firstScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
await firstScanner.listSessions(projectId);
await firstScanner.flushSessionMetadataIndexForTesting();
expect(sortStrings(Object.keys(readIndexSessions(indexDir, projectDir)))).toEqual(
sortStrings([deletePath, keepPath])
);
fs.rmSync(deletePath);
const secondScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const sessions = await secondScanner.listSessions(projectId);
await secondScanner.flushSessionMetadataIndexForTesting();
expect(sessions.map((session) => session.id)).toEqual(['session-keep']);
expect(Object.keys(readIndexSessions(indexDir, projectDir))).toEqual([keepPath]);
});
it('does not prune sibling composite-subproject entries when listing one subproject', async () => {
const { projectsDir, indexDir, projectId, projectDir } = createFixture();
const sessionAPath = path.join(projectDir, 'session-a.jsonl');
const sessionBPath = path.join(projectDir, 'session-b.jsonl');
fs.writeFileSync(
sessionAPath,
`${createSessionLineWithCwd('title a', '/Users/test/indexed-project-a')}\n`,
'utf8'
);
fs.writeFileSync(
sessionBPath,
`${createSessionLineWithCwd('title b', '/Users/test/indexed-project-b')}\n`,
'utf8'
);
const scanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
const projects = await scanner.scan();
const compositeProjects = projects
.filter((project) => project.id.startsWith(`${projectId}::`))
.sort((a, b) => a.path.localeCompare(b.path));
expect(compositeProjects).toHaveLength(2);
await scanner.listSessions(compositeProjects[0].id);
await scanner.listSessions(compositeProjects[1].id);
await scanner.flushSessionMetadataIndexForTesting();
expect(sortStrings(Object.keys(readIndexSessions(indexDir, projectDir)))).toEqual(
sortStrings([sessionAPath, sessionBPath])
);
const freshScanner = new ProjectScanner(projectsDir, undefined, undefined, {
sessionIndexDir: indexDir,
sessionIndexPersistDelayMs: 0,
});
await freshScanner.scan();
await freshScanner.listSessions(compositeProjects[0].id);
await freshScanner.flushSessionMetadataIndexForTesting();
expect(sortStrings(Object.keys(readIndexSessions(indexDir, projectDir)))).toEqual(
sortStrings([sessionAPath, sessionBPath])
);
});
});