Files
sirmalloc--ccstatusline/src/utils/__tests__/block-cache.test.ts
T
2026-07-13 12:49:29 +08:00

292 lines
11 KiB
TypeScript

import * as fs from 'fs';
import { createHash } from 'node:crypto';
import os from 'node:os';
import path from 'node:path';
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi
} from 'vitest';
import {
getBlockCachePath,
readBlockCache,
writeBlockCache
} from '../jsonl';
function getExpectedCachePath(homeDir: string, configDir: string): string {
const normalizedConfigDir = path.resolve(configDir);
const configHash = createHash('sha256')
.update(normalizedConfigDir)
.digest('hex')
.slice(0, 16);
return path.join(homeDir, '.cache', 'ccstatusline', `block-cache-${configHash}.json`);
}
describe('Block Cache Functions', () => {
let tempDir: string;
let originalClaudeConfigDir: string | undefined;
beforeEach(() => {
// Create a temp directory for test isolation
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-test-'));
// Mock os.homedir to use temp directory
vi.spyOn(os, 'homedir').mockReturnValue(tempDir);
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = path.join(tempDir, '.claude-default');
});
afterEach(() => {
vi.restoreAllMocks();
if (originalClaudeConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR;
} else {
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
}
// Clean up temp directory
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('getBlockCachePath', () => {
it('should return the correct cache path', () => {
const cachePath = getBlockCachePath();
expect(cachePath).toBe(getExpectedCachePath(tempDir, path.join(tempDir, '.claude-default')));
});
it('should return different cache paths for different config directories', () => {
const profileA = path.join(tempDir, '.claude-profile-a');
const profileB = path.join(tempDir, '.claude-profile-b');
const pathA = getBlockCachePath(profileA);
const pathB = getBlockCachePath(profileB);
expect(pathA).toBe(getExpectedCachePath(tempDir, profileA));
expect(pathB).toBe(getExpectedCachePath(tempDir, profileB));
expect(pathA).not.toBe(pathB);
});
});
describe('readBlockCache', () => {
it('should return null when cache file does not exist', () => {
const result = readBlockCache();
expect(result).toBeNull();
});
it('should return cached date when cache file is valid', () => {
const testDate = new Date('2025-01-26T14:00:00.000Z');
const cachePath = getBlockCachePath();
const cacheDir = path.dirname(cachePath);
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(cachePath, JSON.stringify({ startTime: testDate.toISOString() }));
const result = readBlockCache();
expect(result).toEqual(testDate);
});
it('should return cached date when configDir matches expected value', () => {
const testDate = new Date('2025-01-26T14:00:00.000Z');
const configDir = path.join(tempDir, '.claude-profile-a');
const cachePath = getBlockCachePath(configDir);
const cacheDir = path.dirname(cachePath);
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(
cachePath,
JSON.stringify({ startTime: testDate.toISOString(), configDir })
);
const result = readBlockCache(configDir);
expect(result).toEqual(testDate);
});
it('should return null when cache configDir does not match expected value', () => {
const testDate = new Date('2025-01-26T14:00:00.000Z');
const expectedConfigDir = path.join(tempDir, '.claude-profile-b');
const cachePath = getBlockCachePath(expectedConfigDir);
const cacheDir = path.dirname(cachePath);
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(
cachePath,
JSON.stringify({
startTime: testDate.toISOString(),
configDir: path.join(tempDir, '.claude-profile-a')
})
);
const result = readBlockCache(expectedConfigDir);
expect(result).toBeNull();
});
it('should return null when expected configDir is provided but cache has legacy schema', () => {
const testDate = new Date('2025-01-26T14:00:00.000Z');
const expectedConfigDir = path.join(tempDir, '.claude-profile-a');
const cachePath = getBlockCachePath(expectedConfigDir);
const cacheDir = path.dirname(cachePath);
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(cachePath, JSON.stringify({ startTime: testDate.toISOString() }));
const result = readBlockCache(expectedConfigDir);
expect(result).toBeNull();
});
it('should return null when cache file has invalid JSON', () => {
const cachePath = getBlockCachePath();
const cacheDir = path.dirname(cachePath);
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(cachePath, 'not valid json');
const result = readBlockCache();
expect(result).toBeNull();
});
it('should return null when cache file has missing startTime', () => {
const cachePath = getBlockCachePath();
const cacheDir = path.dirname(cachePath);
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(cachePath, JSON.stringify({}));
const result = readBlockCache();
expect(result).toBeNull();
});
it('should return null when startTime is not a string', () => {
const cachePath = getBlockCachePath();
const cacheDir = path.dirname(cachePath);
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(cachePath, JSON.stringify({ startTime: 12345 }));
const result = readBlockCache();
expect(result).toBeNull();
});
it('should return null when startTime is invalid date string', () => {
const cachePath = getBlockCachePath();
const cacheDir = path.dirname(cachePath);
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(cachePath, JSON.stringify({ startTime: 'not a date' }));
const result = readBlockCache();
expect(result).toBeNull();
});
});
describe('writeBlockCache', () => {
it('should create directory and write cache file', () => {
const testDate = new Date('2025-01-26T14:00:00.000Z');
const configDir = path.join(tempDir, '.claude-profile-a');
writeBlockCache(testDate, configDir);
const cachePath = getBlockCachePath(configDir);
expect(fs.existsSync(cachePath)).toBe(true);
const content = fs.readFileSync(cachePath, 'utf-8');
const parsed = JSON.parse(content) as { startTime: string; configDir: string };
expect(parsed.startTime).toBe(testDate.toISOString());
expect(parsed.configDir).toBe(path.resolve(configDir));
});
it('should overwrite existing cache file', () => {
const firstDate = new Date('2025-01-26T14:00:00.000Z');
const secondDate = new Date('2025-01-26T16:00:00.000Z');
const configDir = path.join(tempDir, '.claude-profile-a');
writeBlockCache(firstDate, configDir);
writeBlockCache(secondDate, configDir);
const cachePath = getBlockCachePath(configDir);
const content = fs.readFileSync(cachePath, 'utf-8');
const parsed = JSON.parse(content) as { startTime: string; configDir: string };
expect(parsed.startTime).toBe(secondDate.toISOString());
expect(parsed.configDir).toBe(path.resolve(configDir));
});
});
});
describe('getCachedBlockMetrics integration', () => {
let tempDir: string;
let originalClaudeConfigDir: string | undefined;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-test-'));
vi.spyOn(os, 'homedir').mockReturnValue(tempDir);
// Mock CLAUDE_CONFIG_DIR to point to a non-existent directory
// This ensures getBlockMetrics returns null when cache is expired
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = path.join(tempDir, '.claude-nonexistent');
});
afterEach(() => {
vi.restoreAllMocks();
// Restore original CLAUDE_CONFIG_DIR
if (originalClaudeConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR;
} else {
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
}
fs.rmSync(tempDir, { recursive: true, force: true });
});
it('should return cached result when cache is within block duration', async () => {
const { getCachedBlockMetrics, writeBlockCache, getBlockCachePath } = await import('../jsonl');
// Set up a cache with a start time 2 hours ago (within 5-hour block)
const testStartTime = new Date();
testStartTime.setHours(testStartTime.getHours() - 2);
writeBlockCache(testStartTime);
// Verify cache was written
expect(fs.existsSync(getBlockCachePath())).toBe(true);
const result = getCachedBlockMetrics();
expect(result).not.toBeNull();
expect(result?.startTime.getTime()).toBe(testStartTime.getTime());
});
it('should recalculate when cache is expired (beyond block duration)', async () => {
const { getCachedBlockMetrics, writeBlockCache } = await import('../jsonl');
// Set up a cache with a start time 6 hours ago (beyond 5-hour block)
const expiredStartTime = new Date();
expiredStartTime.setHours(expiredStartTime.getHours() - 6);
writeBlockCache(expiredStartTime);
const result = getCachedBlockMetrics();
// Should return null because cache is expired and no real JSONL files exist
expect(result).toBeNull();
});
it('should recalculate when no cache exists', async () => {
const { getCachedBlockMetrics } = await import('../jsonl');
const result = getCachedBlockMetrics();
// Should return null because no cache and no real JSONL files exist
expect(result).toBeNull();
});
it('should recalculate when cache belongs to a different config directory', async () => {
const { getCachedBlockMetrics, writeBlockCache } = await import('../jsonl');
const profileA = path.join(tempDir, '.claude-profile-a');
const profileB = path.join(tempDir, '.claude-profile-b');
const testStartTime = new Date();
testStartTime.setHours(testStartTime.getHours() - 2);
process.env.CLAUDE_CONFIG_DIR = profileA;
writeBlockCache(testStartTime);
process.env.CLAUDE_CONFIG_DIR = profileB;
const result = getCachedBlockMetrics();
// Should return null because cache is fresh but scoped to a different profile
expect(result).toBeNull();
});
});