From a9b0d5c919e2d73f2edc04aa6099492ad94513f4 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:55:20 +0100 Subject: [PATCH 1/6] refactor: rename FIVE_HOURS_MS to SESSION_DURATION_MS Replace hardcoded FIVE_HOURS_MS constant with SESSION_DURATION_MS to better reflect Claude Code official billing session terminology. This aligns with Anthropic documented 5-hour session billing cycles and makes the code more semantically clear about what the time period represents. The constant value remains unchanged (5 * 60 * 60 * 1000 = 18000000ms) but the name now accurately describes it as a session duration rather than just five hours. --- src/five-hour-blocks.internal.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/five-hour-blocks.internal.ts b/src/five-hour-blocks.internal.ts index 9c0f4ea2..2eb695ef 100644 --- a/src/five-hour-blocks.internal.ts +++ b/src/five-hour-blocks.internal.ts @@ -1,4 +1,4 @@ -const FIVE_HOURS_MS = 5 * 60 * 60 * 1000; +const SESSION_DURATION_MS = 5 * 60 * 60 * 1000; const DEFAULT_RECENT_DAYS = 3; export type LoadedUsageEntry = { @@ -74,13 +74,13 @@ export function identifyFiveHourBlocks(entries: LoadedUsageEntry[]): FiveHourBlo const lastEntryTime = lastEntry.timestamp; const timeSinceLastEntry = entryTime.getTime() - lastEntryTime.getTime(); - if (timeSinceBlockStart > FIVE_HOURS_MS || timeSinceLastEntry > FIVE_HOURS_MS) { + if (timeSinceBlockStart > SESSION_DURATION_MS || timeSinceLastEntry > SESSION_DURATION_MS) { // Close current block const block = createBlock(currentBlockStart, currentBlockEntries, now); blocks.push(block); // Add gap block if there's a significant gap - if (timeSinceLastEntry > FIVE_HOURS_MS) { + if (timeSinceLastEntry > SESSION_DURATION_MS) { const gapBlock = createGapBlock(lastEntryTime, entryTime); if (gapBlock != null) { blocks.push(gapBlock); @@ -108,10 +108,10 @@ export function identifyFiveHourBlocks(entries: LoadedUsageEntry[]): FiveHourBlo } function createBlock(startTime: Date, entries: LoadedUsageEntry[], now: Date): FiveHourBlock { - const endTime = new Date(startTime.getTime() + FIVE_HOURS_MS); + const endTime = new Date(startTime.getTime() + SESSION_DURATION_MS); const lastEntry = entries[entries.length - 1]; const actualEndTime = lastEntry != null ? lastEntry.timestamp : startTime; - const isActive = now.getTime() - actualEndTime.getTime() < FIVE_HOURS_MS && now < endTime; + const isActive = now.getTime() - actualEndTime.getTime() < SESSION_DURATION_MS && now < endTime; // Aggregate token counts const tokenCounts: TokenCounts = { @@ -149,11 +149,11 @@ function createBlock(startTime: Date, entries: LoadedUsageEntry[], now: Date): F function createGapBlock(lastActivityTime: Date, nextActivityTime: Date): FiveHourBlock | null { // Only create gap blocks for gaps longer than 5 hours const gapDuration = nextActivityTime.getTime() - lastActivityTime.getTime(); - if (gapDuration <= FIVE_HOURS_MS) { + if (gapDuration <= SESSION_DURATION_MS) { return null; } - const gapStart = new Date(lastActivityTime.getTime() + FIVE_HOURS_MS); + const gapStart = new Date(lastActivityTime.getTime() + SESSION_DURATION_MS); const gapEnd = nextActivityTime; return { From 4c90d3d2d72ad20e96f35f3dee00b864202c59e4 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:57:32 +0100 Subject: [PATCH 2/6] refactor: rename five-hour-blocks files to session-blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename five-hour-blocks.internal.ts to session-blocks.internal.ts and corresponding test file to better reflect Claude Code session terminology. Update all import statements across the codebase to reference the new file names. Files renamed: - five-hour-blocks.internal.ts → session-blocks.internal.ts - five-hour-blocks.internal.test.ts → session-blocks.internal.test.ts Updated imports in: - data-loader.ts - commands/blocks.ts - session-blocks.internal.test.ts --- src/commands/blocks.ts | 4 ++-- src/data-loader.ts | 10 +++++----- ...nternal.test.ts => session-blocks.internal.test.ts} | 2 +- ...r-blocks.internal.ts => session-blocks.internal.ts} | 0 4 files changed, 8 insertions(+), 8 deletions(-) rename src/{five-hour-blocks.internal.test.ts => session-blocks.internal.test.ts} (99%) rename src/{five-hour-blocks.internal.ts => session-blocks.internal.ts} (100%) diff --git a/src/commands/blocks.ts b/src/commands/blocks.ts index d423080f..f3b86922 100644 --- a/src/commands/blocks.ts +++ b/src/commands/blocks.ts @@ -2,13 +2,13 @@ import process from 'node:process'; import { define } from 'gunshi'; import pc from 'picocolors'; import { getDefaultClaudePath, loadFiveHourBlockData } from '../data-loader.ts'; +import { log, logger } from '../logger.ts'; import { calculateBurnRate, filterRecentBlocks, type FiveHourBlock, projectBlockUsage, -} from '../five-hour-blocks.internal.ts'; -import { log, logger } from '../logger.ts'; +} from '../session-blocks.internal.ts'; import { sharedCommandConfig } from '../shared-args.internal.ts'; import { formatCurrency, formatModelsDisplay, formatNumber } from '../utils.internal.ts'; import { ResponsiveTable } from '../utils.table.ts'; diff --git a/src/data-loader.ts b/src/data-loader.ts index e15b4970..3a8c81ba 100644 --- a/src/data-loader.ts +++ b/src/data-loader.ts @@ -9,15 +9,15 @@ import { sort } from 'fast-sort'; import { isDirectorySync } from 'path-type'; import { glob } from 'tinyglobby'; import * as v from 'valibot'; -import { - type FiveHourBlock, - identifyFiveHourBlocks, - type LoadedUsageEntry, -} from './five-hour-blocks.internal.ts'; import { logger } from './logger.ts'; import { PricingFetcher, } from './pricing-fetcher.ts'; +import { + type FiveHourBlock, + identifyFiveHourBlocks, + type LoadedUsageEntry, +} from './session-blocks.internal.ts'; const DEFAULT_CLAUDE_CODE_PATH = path.join(homedir(), '.claude'); diff --git a/src/five-hour-blocks.internal.test.ts b/src/session-blocks.internal.test.ts similarity index 99% rename from src/five-hour-blocks.internal.test.ts rename to src/session-blocks.internal.test.ts index a26d39cf..4251284c 100644 --- a/src/five-hour-blocks.internal.test.ts +++ b/src/session-blocks.internal.test.ts @@ -6,7 +6,7 @@ import { identifyFiveHourBlocks, type LoadedUsageEntry, projectBlockUsage, -} from './five-hour-blocks.internal.ts'; +} from './session-blocks.internal.ts'; const FIVE_HOURS_MS = 5 * 60 * 60 * 1000; diff --git a/src/five-hour-blocks.internal.ts b/src/session-blocks.internal.ts similarity index 100% rename from src/five-hour-blocks.internal.ts rename to src/session-blocks.internal.ts From 9b187a779677e2b5158d7074f70f93678e9d2b1c Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:00:22 +0100 Subject: [PATCH 3/6] refactor: rename FiveHourBlock type to SessionBlock Update type definition from FiveHourBlock to SessionBlock throughout the session-blocks.internal.ts file to better reflect Claude Code session terminology. This change aligns the type name with Anthropic official billing session concept rather than just describing the duration. All function signatures and variable declarations within the file have been updated to use the new SessionBlock type. --- src/session-blocks.internal.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/session-blocks.internal.ts b/src/session-blocks.internal.ts index 2eb695ef..39f5959d 100644 --- a/src/session-blocks.internal.ts +++ b/src/session-blocks.internal.ts @@ -21,7 +21,7 @@ export type TokenCounts = { cacheReadInputTokens: number; }; -export type FiveHourBlock = { +export type SessionBlock = { id: string; // ISO string of block start time startTime: Date; endTime: Date; // startTime + 5 hours (for normal blocks) or gap end time (for gap blocks) @@ -45,12 +45,12 @@ export type ProjectedUsage = { remainingMinutes: number; }; -export function identifyFiveHourBlocks(entries: LoadedUsageEntry[]): FiveHourBlock[] { +export function identifySessionBlocks(entries: LoadedUsageEntry[]): SessionBlock[] { if (entries.length === 0) { return []; } - const blocks: FiveHourBlock[] = []; + const blocks: SessionBlock[] = []; const sortedEntries = [...entries].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); let currentBlockStart: Date | null = null; @@ -107,7 +107,7 @@ export function identifyFiveHourBlocks(entries: LoadedUsageEntry[]): FiveHourBlo return blocks; } -function createBlock(startTime: Date, entries: LoadedUsageEntry[], now: Date): FiveHourBlock { +function createBlock(startTime: Date, entries: LoadedUsageEntry[], now: Date): SessionBlock { const endTime = new Date(startTime.getTime() + SESSION_DURATION_MS); const lastEntry = entries[entries.length - 1]; const actualEndTime = lastEntry != null ? lastEntry.timestamp : startTime; @@ -146,7 +146,7 @@ function createBlock(startTime: Date, entries: LoadedUsageEntry[], now: Date): F }; } -function createGapBlock(lastActivityTime: Date, nextActivityTime: Date): FiveHourBlock | null { +function createGapBlock(lastActivityTime: Date, nextActivityTime: Date): SessionBlock | null { // Only create gap blocks for gaps longer than 5 hours const gapDuration = nextActivityTime.getTime() - lastActivityTime.getTime(); if (gapDuration <= SESSION_DURATION_MS) { @@ -174,7 +174,7 @@ function createGapBlock(lastActivityTime: Date, nextActivityTime: Date): FiveHou }; } -export function calculateBurnRate(block: FiveHourBlock): BurnRate | null { +export function calculateBurnRate(block: SessionBlock): BurnRate | null { if (block.entries.length === 0 || (block.isGap ?? false)) { return null; } @@ -203,7 +203,7 @@ export function calculateBurnRate(block: FiveHourBlock): BurnRate | null { }; } -export function projectBlockUsage(block: FiveHourBlock): ProjectedUsage | null { +export function projectBlockUsage(block: SessionBlock): ProjectedUsage | null { if (!block.isActive || (block.isGap ?? false)) { return null; } @@ -231,7 +231,7 @@ export function projectBlockUsage(block: FiveHourBlock): ProjectedUsage | null { }; } -export function filterRecentBlocks(blocks: FiveHourBlock[], days: number = DEFAULT_RECENT_DAYS): FiveHourBlock[] { +export function filterRecentBlocks(blocks: SessionBlock[], days: number = DEFAULT_RECENT_DAYS): SessionBlock[] { const now = new Date(); const cutoffTime = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); From 957ad365e437e2bf8a43ff3c4e4caeacb052a125 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:01:40 +0100 Subject: [PATCH 4/6] refactor: update function names and type references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update all imports and function calls to use new SessionBlock terminology: - loadFiveHourBlockData → loadSessionBlockData - FiveHourBlock → SessionBlock - identifyFiveHourBlocks → identifySessionBlocks Updated files: - data-loader.ts: function name and return type - commands/blocks.ts: import, function calls, and type annotations This completes the transition from FiveHour terminology to Session terminology in the core data handling logic. --- src/commands/blocks.ts | 14 +++++++------- src/data-loader.ts | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/commands/blocks.ts b/src/commands/blocks.ts index f3b86922..069d1450 100644 --- a/src/commands/blocks.ts +++ b/src/commands/blocks.ts @@ -1,13 +1,13 @@ import process from 'node:process'; import { define } from 'gunshi'; import pc from 'picocolors'; -import { getDefaultClaudePath, loadFiveHourBlockData } from '../data-loader.ts'; +import { getDefaultClaudePath, loadSessionBlockData } from '../data-loader.ts'; import { log, logger } from '../logger.ts'; import { calculateBurnRate, filterRecentBlocks, - type FiveHourBlock, projectBlockUsage, + type SessionBlock, } from '../session-blocks.internal.ts'; import { sharedCommandConfig } from '../shared-args.internal.ts'; import { formatCurrency, formatModelsDisplay, formatNumber } from '../utils.internal.ts'; @@ -19,7 +19,7 @@ const WARNING_THRESHOLD = 0.8; const COMPACT_WIDTH_THRESHOLD = 120; const DEFAULT_TERMINAL_WIDTH = 120; -function formatBlockTime(block: FiveHourBlock, compact = false): string { +function formatBlockTime(block: SessionBlock, compact = false): string { const start = compact ? block.startTime.toLocaleString(undefined, { month: '2-digit', @@ -118,7 +118,7 @@ export const blocksCommand = define({ logger.level = 0; } - let blocks = await loadFiveHourBlockData({ + let blocks = await loadSessionBlockData({ since: ctx.values.since, until: ctx.values.until, claudePath: getDefaultClaudePath(), @@ -158,7 +158,7 @@ export const blocksCommand = define({ } if (ctx.values.active) { - blocks = blocks.filter((block: FiveHourBlock) => block.isActive); + blocks = blocks.filter((block: SessionBlock) => block.isActive); if (blocks.length === 0) { if (ctx.values.json) { log(JSON.stringify({ blocks: [], message: 'No active block' })); @@ -173,7 +173,7 @@ export const blocksCommand = define({ if (ctx.values.json) { // JSON output const jsonOutput = { - blocks: blocks.map((block: FiveHourBlock) => { + blocks: blocks.map((block: SessionBlock) => { const burnRate = block.isActive ? calculateBurnRate(block) : null; const projection = block.isActive ? projectBlockUsage(block) : null; @@ -218,7 +218,7 @@ export const blocksCommand = define({ // Table output if (ctx.values.active && blocks.length === 1) { // Detailed active block view - const block = blocks[0] as FiveHourBlock; + const block = blocks[0] as SessionBlock; if (block == null) { logger.warn('No active block found.'); process.exit(0); diff --git a/src/data-loader.ts b/src/data-loader.ts index 3a8c81ba..6b17d2c3 100644 --- a/src/data-loader.ts +++ b/src/data-loader.ts @@ -14,9 +14,9 @@ import { PricingFetcher, } from './pricing-fetcher.ts'; import { - type FiveHourBlock, - identifyFiveHourBlocks, + identifySessionBlocks, type LoadedUsageEntry, + type SessionBlock, } from './session-blocks.internal.ts'; const DEFAULT_CLAUDE_CODE_PATH = path.join(homedir(), '.claude'); @@ -820,9 +820,9 @@ export async function loadMonthlyUsageData( return sortByDate(monthlyArray, item => `${item.month}-01`, options?.order); } -export async function loadFiveHourBlockData( +export async function loadSessionBlockData( options?: LoadOptions, -): Promise { +): Promise { const claudePath = options?.claudePath ?? getDefaultClaudePath(); const claudeDir = path.join(claudePath, 'projects'); const files = await glob(['**/*.jsonl'], { @@ -899,8 +899,8 @@ export async function loadFiveHourBlockData( } } - // Identify 5-hour blocks - const blocks = identifyFiveHourBlocks(allEntries); + // Identify session blocks + const blocks = identifySessionBlocks(allEntries); // Filter by date range if specified const filtered = (options?.since != null && options.since !== '') || (options?.until != null && options.until !== '') From 459630fe7a6e8adc8809c65925603d4d2c91b953 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:02:30 +0100 Subject: [PATCH 5/6] refactor: update user-facing text to session terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update command descriptions and user messages to use "session" terminology instead of "5-hour" references: - Command descriptions: "5-hour billing blocks" → "session billing blocks" - Error messages: "No active 5-hour block found" → "No active session block found" - UI titles: "5-Hour Block Status" → "Session Block Status" - Report headers: "5-Hour Blocks" → "Session Blocks" Updated in commands/blocks.ts and mcp.ts to provide consistent user-facing terminology that aligns with Claude Code official session concept. --- src/commands/blocks.ts | 8 ++++---- src/mcp.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/blocks.ts b/src/commands/blocks.ts index 069d1450..b55c4514 100644 --- a/src/commands/blocks.ts +++ b/src/commands/blocks.ts @@ -91,7 +91,7 @@ function parseTokenLimit(value: string | undefined, maxFromAll: number): number export const blocksCommand = define({ name: 'blocks', - description: 'Show usage report grouped by 5-hour billing blocks', + description: 'Show usage report grouped by session billing blocks', args: { ...sharedCommandConfig.args, active: { @@ -164,7 +164,7 @@ export const blocksCommand = define({ log(JSON.stringify({ blocks: [], message: 'No active block' })); } else { - logger.info('No active 5-hour block found.'); + logger.info('No active session block found.'); } process.exit(0); } @@ -226,7 +226,7 @@ export const blocksCommand = define({ const burnRate = calculateBurnRate(block); const projection = projectBlockUsage(block); - logger.box('Current 5-Hour Block Status'); + logger.box('Current Session Block Status'); const now = new Date(); const elapsed = Math.round( @@ -279,7 +279,7 @@ export const blocksCommand = define({ } else { // Table view for multiple blocks - logger.box('Claude Code Token Usage Report - 5-Hour Blocks'); + logger.box('Claude Code Token Usage Report - Session Blocks'); // Calculate token limit if "max" is specified const actualTokenLimit = parseTokenLimit(ctx.values.tokenLimit, maxTokensFromAll); diff --git a/src/mcp.ts b/src/mcp.ts index 8c98531c..49766fd7 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -7,8 +7,8 @@ import { name, version } from '../package.json'; import { getDefaultClaudePath, loadDailyUsageData, - loadFiveHourBlockData, loadMonthlyUsageData, + loadSessionBlockData, loadSessionData, } from './data-loader.ts'; import { CostModes, dateSchema } from './types.internal.ts'; @@ -88,10 +88,10 @@ export function createMcpServer({ server.addTool({ name: 'blocks', - description: 'Show usage report grouped by 5-hour billing blocks', + description: 'Show usage report grouped by session billing blocks', parameters: parametersSchema, execute: async (args) => { - const blocksData = await loadFiveHourBlockData({ ...args, claudePath }); + const blocksData = await loadSessionBlockData({ ...args, claudePath }); return JSON.stringify(blocksData); }, }); From 2d67512ee25958f0d104a7f61e77a4310e0aaa22 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:04:57 +0100 Subject: [PATCH 6/6] fix: update test files to use session terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update all test files to use new session terminology: - FIVE_HOURS_MS → SESSION_DURATION_MS - FiveHourBlock → SessionBlock - loadFiveHourBlockData → loadSessionBlockData - identifyFiveHourBlocks → identifySessionBlocks This fixes compilation errors after refactoring to session-based naming convention. All test logic remains the same, only terminology has been updated to match the new API. --- src/data-loader.test.ts | 24 +++++----- src/session-blocks.internal.test.ts | 72 ++++++++++++++--------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/data-loader.test.ts b/src/data-loader.test.ts index 18ddf236..c0cacfa3 100644 --- a/src/data-loader.test.ts +++ b/src/data-loader.test.ts @@ -9,8 +9,8 @@ import { formatDateCompact, getDefaultClaudePath, loadDailyUsageData, - loadFiveHourBlockData, loadMonthlyUsageData, + loadSessionBlockData, loadSessionData, type UsageData, } from './data-loader.ts'; @@ -1940,10 +1940,10 @@ describe('calculateCostForEntry', () => { }); }); -describe('loadFiveHourBlockData', () => { +describe('loadSessionBlockData', () => { test('returns empty array when no files found', async () => { await using fixture = await createFixture({ projects: {} }); - const result = await loadFiveHourBlockData({ claudePath: fixture.path }); + const result = await loadSessionBlockData({ claudePath: fixture.path }); expect(result).toEqual([]); }); @@ -2005,7 +2005,7 @@ describe('loadFiveHourBlockData', () => { }, }); - const result = await loadFiveHourBlockData({ claudePath: fixture.path }); + const result = await loadSessionBlockData({ claudePath: fixture.path }); expect(result.length).toBeGreaterThan(0); // Should have blocks expect(result[0]?.entries).toHaveLength(1); // First block has one entry // Total entries across all blocks should be 3 @@ -2040,7 +2040,7 @@ describe('loadFiveHourBlockData', () => { }); // Test display mode - const displayResult = await loadFiveHourBlockData({ + const displayResult = await loadSessionBlockData({ claudePath: fixture.path, mode: 'display', }); @@ -2048,7 +2048,7 @@ describe('loadFiveHourBlockData', () => { expect(displayResult[0]?.costUSD).toBe(0.01); // Test calculate mode - const calculateResult = await loadFiveHourBlockData({ + const calculateResult = await loadSessionBlockData({ claudePath: fixture.path, mode: 'calculate', }); @@ -2106,7 +2106,7 @@ describe('loadFiveHourBlockData', () => { }); // Test filtering with since parameter - const sinceResult = await loadFiveHourBlockData({ + const sinceResult = await loadSessionBlockData({ claudePath: fixture.path, since: '20240102', }); @@ -2114,7 +2114,7 @@ describe('loadFiveHourBlockData', () => { expect(sinceResult.every(block => block.startTime >= date2)).toBe(true); // Test filtering with until parameter - const untilResult = await loadFiveHourBlockData({ + const untilResult = await loadSessionBlockData({ claudePath: fixture.path, until: '20240102', }); @@ -2164,14 +2164,14 @@ describe('loadFiveHourBlockData', () => { }); // Test ascending order - const ascResult = await loadFiveHourBlockData({ + const ascResult = await loadSessionBlockData({ claudePath: fixture.path, order: 'asc', }); expect(ascResult[0]?.startTime).toEqual(date1); // Test descending order - const descResult = await loadFiveHourBlockData({ + const descResult = await loadSessionBlockData({ claudePath: fixture.path, order: 'desc', }); @@ -2215,7 +2215,7 @@ describe('loadFiveHourBlockData', () => { }, }); - const result = await loadFiveHourBlockData({ claudePath: fixture.path }); + const result = await loadSessionBlockData({ claudePath: fixture.path }); expect(result).toHaveLength(1); expect(result[0]?.entries).toHaveLength(1); // Only one entry after deduplication }); @@ -2247,7 +2247,7 @@ describe('loadFiveHourBlockData', () => { }, }); - const result = await loadFiveHourBlockData({ claudePath: fixture.path }); + const result = await loadSessionBlockData({ claudePath: fixture.path }); expect(result).toHaveLength(1); expect(result[0]?.entries).toHaveLength(1); }); diff --git a/src/session-blocks.internal.test.ts b/src/session-blocks.internal.test.ts index 4251284c..1a5f28a8 100644 --- a/src/session-blocks.internal.test.ts +++ b/src/session-blocks.internal.test.ts @@ -2,13 +2,13 @@ import { describe, expect, test } from 'bun:test'; import { calculateBurnRate, filterRecentBlocks, - type FiveHourBlock, - identifyFiveHourBlocks, + identifySessionBlocks, type LoadedUsageEntry, projectBlockUsage, + type SessionBlock, } from './session-blocks.internal.ts'; -const FIVE_HOURS_MS = 5 * 60 * 60 * 1000; +const SESSION_DURATION_MS = 5 * 60 * 60 * 1000; function createMockEntry( timestamp: Date, @@ -30,9 +30,9 @@ function createMockEntry( }; } -describe('identifyFiveHourBlocks', () => { +describe('identifySessionBlocks', () => { test('returns empty array for empty entries', () => { - const result = identifyFiveHourBlocks([]); + const result = identifySessionBlocks([]); expect(result).toEqual([]); }); @@ -44,7 +44,7 @@ describe('identifyFiveHourBlocks', () => { createMockEntry(new Date(baseTime.getTime() + 2 * 60 * 60 * 1000)), // 2 hours later ]; - const blocks = identifyFiveHourBlocks(entries); + const blocks = identifySessionBlocks(entries); expect(blocks).toHaveLength(1); expect(blocks[0]?.startTime).toEqual(baseTime); expect(blocks[0]?.entries).toHaveLength(3); @@ -60,7 +60,7 @@ describe('identifyFiveHourBlocks', () => { createMockEntry(new Date(baseTime.getTime() + 6 * 60 * 60 * 1000)), // 6 hours later ]; - const blocks = identifyFiveHourBlocks(entries); + const blocks = identifySessionBlocks(entries); expect(blocks).toHaveLength(3); // first block, gap block, second block expect(blocks[0]?.entries).toHaveLength(1); expect(blocks[1]?.isGap).toBe(true); // gap block @@ -75,7 +75,7 @@ describe('identifyFiveHourBlocks', () => { createMockEntry(new Date(baseTime.getTime() + 8 * 60 * 60 * 1000)), // 8 hours later ]; - const blocks = identifyFiveHourBlocks(entries); + const blocks = identifySessionBlocks(entries); expect(blocks).toHaveLength(3); // first block, gap block, second block expect(blocks[0]?.entries).toHaveLength(2); expect(blocks[1]?.isGap).toBe(true); @@ -91,7 +91,7 @@ describe('identifyFiveHourBlocks', () => { createMockEntry(new Date(baseTime.getTime() + 1 * 60 * 60 * 1000)), // 1 hour later ]; - const blocks = identifyFiveHourBlocks(entries); + const blocks = identifySessionBlocks(entries); expect(blocks).toHaveLength(1); expect(blocks[0]?.entries[0]?.timestamp).toEqual(baseTime); expect(blocks[0]?.entries[1]?.timestamp).toEqual(new Date(baseTime.getTime() + 1 * 60 * 60 * 1000)); @@ -105,7 +105,7 @@ describe('identifyFiveHourBlocks', () => { createMockEntry(new Date(baseTime.getTime() + 60 * 60 * 1000), 2000, 1000, 'claude-opus-4-20250514'), ]; - const blocks = identifyFiveHourBlocks(entries); + const blocks = identifySessionBlocks(entries); expect(blocks).toHaveLength(1); expect(blocks[0]?.models).toEqual(['claude-sonnet-4-20250514', 'claude-opus-4-20250514']); }); @@ -117,7 +117,7 @@ describe('identifyFiveHourBlocks', () => { { ...createMockEntry(new Date(baseTime.getTime() + 60 * 60 * 1000)), costUSD: null }, ]; - const blocks = identifyFiveHourBlocks(entries); + const blocks = identifySessionBlocks(entries); expect(blocks).toHaveLength(1); expect(blocks[0]?.costUSD).toBe(0.01); // Only the first entry's cost }); @@ -126,7 +126,7 @@ describe('identifyFiveHourBlocks', () => { const baseTime = new Date('2024-01-01T10:00:00Z'); const entries: LoadedUsageEntry[] = [createMockEntry(baseTime)]; - const blocks = identifyFiveHourBlocks(entries); + const blocks = identifySessionBlocks(entries); expect(blocks[0]?.id).toBe(baseTime.toISOString()); }); @@ -134,8 +134,8 @@ describe('identifyFiveHourBlocks', () => { const baseTime = new Date('2024-01-01T10:00:00Z'); const entries: LoadedUsageEntry[] = [createMockEntry(baseTime)]; - const blocks = identifyFiveHourBlocks(entries); - expect(blocks[0]?.endTime).toEqual(new Date(baseTime.getTime() + FIVE_HOURS_MS)); + const blocks = identifySessionBlocks(entries); + expect(blocks[0]?.endTime).toEqual(new Date(baseTime.getTime() + SESSION_DURATION_MS)); }); test('handles cache tokens correctly', () => { @@ -152,7 +152,7 @@ describe('identifyFiveHourBlocks', () => { model: 'claude-sonnet-4-20250514', }; - const blocks = identifyFiveHourBlocks([entry]); + const blocks = identifySessionBlocks([entry]); expect(blocks[0]?.tokenCounts.cacheCreationInputTokens).toBe(100); expect(blocks[0]?.tokenCounts.cacheReadInputTokens).toBe(200); }); @@ -160,7 +160,7 @@ describe('identifyFiveHourBlocks', () => { describe('calculateBurnRate', () => { test('returns null for empty entries', () => { - const block: FiveHourBlock = { + const block: SessionBlock = { id: '2024-01-01T10:00:00.000Z', startTime: new Date('2024-01-01T10:00:00Z'), endTime: new Date('2024-01-01T15:00:00Z'), @@ -181,7 +181,7 @@ describe('calculateBurnRate', () => { }); test('returns null for gap blocks', () => { - const block: FiveHourBlock = { + const block: SessionBlock = { id: 'gap-2024-01-01T10:00:00.000Z', startTime: new Date('2024-01-01T10:00:00Z'), endTime: new Date('2024-01-01T15:00:00Z'), @@ -204,10 +204,10 @@ describe('calculateBurnRate', () => { test('returns null when duration is zero or negative', () => { const baseTime = new Date('2024-01-01T10:00:00Z'); - const block: FiveHourBlock = { + const block: SessionBlock = { id: baseTime.toISOString(), startTime: baseTime, - endTime: new Date(baseTime.getTime() + FIVE_HOURS_MS), + endTime: new Date(baseTime.getTime() + SESSION_DURATION_MS), isActive: true, entries: [ createMockEntry(baseTime), @@ -230,10 +230,10 @@ describe('calculateBurnRate', () => { test('calculates burn rate correctly', () => { const baseTime = new Date('2024-01-01T10:00:00Z'); const laterTime = new Date(baseTime.getTime() + 60 * 1000); // 1 minute later - const block: FiveHourBlock = { + const block: SessionBlock = { id: baseTime.toISOString(), startTime: baseTime, - endTime: new Date(baseTime.getTime() + FIVE_HOURS_MS), + endTime: new Date(baseTime.getTime() + SESSION_DURATION_MS), isActive: true, entries: [ createMockEntry(baseTime, 1000, 500, 'claude-sonnet-4-20250514', 0.01), @@ -258,7 +258,7 @@ describe('calculateBurnRate', () => { describe('projectBlockUsage', () => { test('returns null for inactive blocks', () => { - const block: FiveHourBlock = { + const block: SessionBlock = { id: '2024-01-01T10:00:00.000Z', startTime: new Date('2024-01-01T10:00:00Z'), endTime: new Date('2024-01-01T15:00:00Z'), @@ -279,7 +279,7 @@ describe('projectBlockUsage', () => { }); test('returns null for gap blocks', () => { - const block: FiveHourBlock = { + const block: SessionBlock = { id: 'gap-2024-01-01T10:00:00.000Z', startTime: new Date('2024-01-01T10:00:00Z'), endTime: new Date('2024-01-01T15:00:00Z'), @@ -301,7 +301,7 @@ describe('projectBlockUsage', () => { }); test('returns null when burn rate cannot be calculated', () => { - const block: FiveHourBlock = { + const block: SessionBlock = { id: '2024-01-01T10:00:00.000Z', startTime: new Date('2024-01-01T10:00:00Z'), endTime: new Date('2024-01-01T15:00:00Z'), @@ -324,10 +324,10 @@ describe('projectBlockUsage', () => { test('projects usage correctly for active block', () => { const now = new Date(); const startTime = new Date(now.getTime() - 60 * 60 * 1000); // 1 hour ago - const endTime = new Date(startTime.getTime() + FIVE_HOURS_MS); + const endTime = new Date(startTime.getTime() + SESSION_DURATION_MS); const pastTime = new Date(startTime.getTime() + 30 * 60 * 1000); // 30 minutes after start - const block: FiveHourBlock = { + const block: SessionBlock = { id: startTime.toISOString(), startTime, endTime, @@ -360,11 +360,11 @@ describe('filterRecentBlocks', () => { const recentTime = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000); // 2 days ago const oldTime = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000); // 5 days ago - const blocks: FiveHourBlock[] = [ + const blocks: SessionBlock[] = [ { id: recentTime.toISOString(), startTime: recentTime, - endTime: new Date(recentTime.getTime() + FIVE_HOURS_MS), + endTime: new Date(recentTime.getTime() + SESSION_DURATION_MS), isActive: false, entries: [], tokenCounts: { @@ -379,7 +379,7 @@ describe('filterRecentBlocks', () => { { id: oldTime.toISOString(), startTime: oldTime, - endTime: new Date(oldTime.getTime() + FIVE_HOURS_MS), + endTime: new Date(oldTime.getTime() + SESSION_DURATION_MS), isActive: false, entries: [], tokenCounts: { @@ -402,11 +402,11 @@ describe('filterRecentBlocks', () => { const now = new Date(); const oldTime = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); // 10 days ago - const blocks: FiveHourBlock[] = [ + const blocks: SessionBlock[] = [ { id: oldTime.toISOString(), startTime: oldTime, - endTime: new Date(oldTime.getTime() + FIVE_HOURS_MS), + endTime: new Date(oldTime.getTime() + SESSION_DURATION_MS), isActive: true, // Active block entries: [], tokenCounts: { @@ -430,11 +430,11 @@ describe('filterRecentBlocks', () => { const withinCustomRange = new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000); // 4 days ago const outsideCustomRange = new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000); // 8 days ago - const blocks: FiveHourBlock[] = [ + const blocks: SessionBlock[] = [ { id: withinCustomRange.toISOString(), startTime: withinCustomRange, - endTime: new Date(withinCustomRange.getTime() + FIVE_HOURS_MS), + endTime: new Date(withinCustomRange.getTime() + SESSION_DURATION_MS), isActive: false, entries: [], tokenCounts: { @@ -449,7 +449,7 @@ describe('filterRecentBlocks', () => { { id: outsideCustomRange.toISOString(), startTime: outsideCustomRange, - endTime: new Date(outsideCustomRange.getTime() + FIVE_HOURS_MS), + endTime: new Date(outsideCustomRange.getTime() + SESSION_DURATION_MS), isActive: false, entries: [], tokenCounts: { @@ -472,11 +472,11 @@ describe('filterRecentBlocks', () => { const now = new Date(); const oldTime = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); // 10 days ago - const blocks: FiveHourBlock[] = [ + const blocks: SessionBlock[] = [ { id: oldTime.toISOString(), startTime: oldTime, - endTime: new Date(oldTime.getTime() + FIVE_HOURS_MS), + endTime: new Date(oldTime.getTime() + SESSION_DURATION_MS), isActive: false, entries: [], tokenCounts: {