agent-ecosystem/test/main/utils/jsonl.test.ts
matt b4e8eedbef refactor(jsonl.test): improve cleanup logic in analyzeSessionFileMetadata test
- Wrapped the cleanup of the temporary directory in a try-finally block to ensure it executes even if an error occurs during the test.
- Added retry logic to the directory removal process to handle potential ENOTEMPTY errors on Windows, enhancing test reliability.

This commit enhances the robustness of the test by ensuring proper cleanup and error handling.
2026-02-13 23:24:44 +09:00

186 lines
5.7 KiB
TypeScript

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { describe, expect, it } from 'vitest';
import { analyzeSessionFileMetadata, calculateMetrics } from '../../../src/main/utils/jsonl';
import type { ParsedMessage } from '../../../src/main/types';
// Helper to create a minimal ParsedMessage
function createMessage(overrides: Partial<ParsedMessage> = {}): ParsedMessage {
return {
uuid: 'test-uuid',
parentUuid: null,
type: 'assistant',
timestamp: new Date('2024-01-01T10:00:00Z'),
content: '',
isSidechain: false,
isMeta: false,
isCompactSummary: false,
toolCalls: [],
toolResults: [],
...overrides,
};
}
describe('jsonl', () => {
describe('calculateMetrics', () => {
it('should return empty metrics for empty messages array', () => {
const result = calculateMetrics([]);
expect(result.durationMs).toBe(0);
expect(result.totalTokens).toBe(0);
expect(result.inputTokens).toBe(0);
expect(result.outputTokens).toBe(0);
expect(result.messageCount).toBe(0);
});
it('should calculate total tokens from usage', () => {
const messages = [
createMessage({
usage: {
input_tokens: 100,
output_tokens: 50,
},
}),
];
const result = calculateMetrics(messages);
expect(result.inputTokens).toBe(100);
expect(result.outputTokens).toBe(50);
expect(result.totalTokens).toBe(150);
});
it('should sum tokens across multiple messages', () => {
const messages = [
createMessage({
usage: { input_tokens: 100, output_tokens: 50 },
}),
createMessage({
usage: { input_tokens: 200, output_tokens: 100 },
}),
];
const result = calculateMetrics(messages);
expect(result.inputTokens).toBe(300);
expect(result.outputTokens).toBe(150);
expect(result.totalTokens).toBe(450);
});
it('should handle cache tokens', () => {
const messages = [
createMessage({
usage: {
input_tokens: 100,
output_tokens: 50,
cache_read_input_tokens: 25,
cache_creation_input_tokens: 10,
},
}),
];
const result = calculateMetrics(messages);
expect(result.cacheReadTokens).toBe(25);
expect(result.cacheCreationTokens).toBe(10);
expect(result.totalTokens).toBe(185); // 100 + 50 + 25 + 10
});
it('should calculate duration from timestamps', () => {
const messages = [
createMessage({ timestamp: new Date('2024-01-01T10:00:00Z') }),
createMessage({ timestamp: new Date('2024-01-01T10:01:00Z') }),
createMessage({ timestamp: new Date('2024-01-01T10:02:00Z') }),
];
const result = calculateMetrics(messages);
expect(result.durationMs).toBe(120000); // 2 minutes in ms
});
it('should count messages', () => {
const messages = [createMessage(), createMessage(), createMessage()];
const result = calculateMetrics(messages);
expect(result.messageCount).toBe(3);
});
it('should handle messages without usage', () => {
const messages = [
createMessage({ type: 'user', content: 'Hello' }),
createMessage({ type: 'system' }),
];
const result = calculateMetrics(messages);
expect(result.totalTokens).toBe(0);
expect(result.messageCount).toBe(2);
});
it('should handle single message duration', () => {
const messages = [createMessage({ timestamp: new Date('2024-01-01T10:00:00Z') })];
const result = calculateMetrics(messages);
expect(result.durationMs).toBe(0); // min === max
});
it('should handle undefined token values', () => {
const messages = [
createMessage({
usage: {
input_tokens: undefined as unknown as number,
output_tokens: 50,
},
}),
];
const result = calculateMetrics(messages);
expect(result.inputTokens).toBe(0);
expect(result.outputTokens).toBe(50);
});
});
describe('analyzeSessionFileMetadata', () => {
it('should extract first message, count, ongoing state, and git branch in one pass', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonl-meta-'));
try {
const filePath = path.join(tempDir, 'session.jsonl');
const lines = [
JSON.stringify({
type: 'user',
uuid: 'u1',
timestamp: '2026-01-01T00:00:00.000Z',
gitBranch: 'feature/test',
message: { role: 'user', content: 'hello world' },
isMeta: false,
}),
JSON.stringify({
type: 'assistant',
uuid: 'a1',
timestamp: '2026-01-01T00:00:01.000Z',
message: {
role: 'assistant',
content: [{ type: 'thinking', thinking: 'thinking...' }],
},
}),
];
fs.writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf8');
const result = await analyzeSessionFileMetadata(filePath);
expect(result.firstUserMessage?.text).toBe('hello world');
expect(result.firstUserMessage?.timestamp).toBe('2026-01-01T00:00:00.000Z');
expect(result.messageCount).toBe(1);
expect(result.isOngoing).toBe(true);
expect(result.gitBranch).toBe('feature/test');
} finally {
try {
fs.rmSync(tempDir, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 200,
});
} catch {
// Best-effort cleanup; ignore ENOTEMPTY on Windows when dir is in use
}
}
});
});
});