Files
bowenliang123 026ffca599 feat(context): fold the range off the step-start daily ledger
Context Insights now reads contextActivity for every range-scoped figure: a step's settlement, tool run, and wall time book on the local day its step/start fired, so a step crossing midnight stays on its start day. The KPI band, Token Stats, and session selection share the daily ledger; turns, tool calls, and step wall time join each day, and contextActivity's stateVersion moves to 5 so cached rows cold-refold. The Activity heatmap drops the Steps metric for daily token volume; Timing Stats stays session-lifetime and is labeled as such.

Refs #172
2026-10-20 08:42:51 +08:00

184 lines
9.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { gridOf, makeHeatmap, todayKey } from '../../../src/client/components/heatmap'
import { click, makeKit, mount, query, queryAll, text } from '../helpers/kit'
const kit = makeKit()
const Heatmap = makeHeatmap(kit)
// 2026-09-16 was a Wednesday of the week starting Sunday 2026-09-13.
const TODAY = '2026-09-16'
describe('gridOf', () => {
test('lays out weeks of Sunday-first columns ending at today’s week', () => {
const grid = gridOf(TODAY, 2)
assert.ok(grid !== null)
assert.equal(grid.length, 2)
assert.equal(grid[0][0].key, '2026-09-06', 'the first column opens one week earlier')
assert.equal(grid[1][0].key, '2026-09-13', 'the last column is today’s Sunday')
assert.equal(grid[1][3].key, TODAY)
assert.equal(grid[1][3].future, false)
assert.equal(grid[1][4].future, true, 'days after today render as future placeholders')
assert.equal(grid[1][6].key, '2026-09-19')
})
test('degraded inputs read null (the card then shows its empty note)', () => {
assert.equal(gridOf('', 2), null)
assert.equal(gridOf('garbage', 2), null)
assert.equal(gridOf(TODAY, 0), null, 'no columns to draw')
assert.equal(gridOf(TODAY, 3000), null, 'a window reaching before the epoch has no first Sunday')
// 9999-12-31 was a Friday of the week starting Sunday 9999-12-26: the
// week's tail days leave the four-digit year, so no grid can hold it.
assert.equal(gridOf('9999-12-31', 2), null, 'the final week of the representable range overflows')
})
})
describe('Heatmap', () => {
test('an empty ledger renders the empty note, no grid', async () => {
const m = await mount(h(Heatmap, { days: {}, today: TODAY }))
assert.ok(text(m.container).includes('No activity yet'))
assert.equal(queryAll(m.container, '.lc-heat-cell').length, 0)
await m.unmount()
})
test('a corrupt today key degrades to the empty note', async () => {
const m = await mount(h(Heatmap, { days: { '2026-09-16': { tokens: 5, requests: 1, sessions: 1, cost: null } }, today: 'junk' }))
assert.ok(text(m.container).includes('No activity yet'))
await m.unmount()
})
test('cells draw with depth levels; data days are buttons with labels', async () => {
const days = {
'2026-09-16': { tokens: 100, requests: 4, sessions: 2, cost: null },
'2026-09-15': { tokens: 50, requests: 2, sessions: 1, cost: null },
'2026-09-14': { tokens: 25, requests: 1, sessions: 1, cost: null },
'2026-09-10': { tokens: 30, requests: 0, sessions: 1, cost: null },
'2026-09-09': { tokens: 75, requests: 3, sessions: 3, cost: null },
'2026-09-08': { tokens: 1, requests: 1, sessions: 1, cost: null },
'2026-09-07': { tokens: 0, requests: 2, sessions: 1, cost: null },
}
const m = await mount(h(Heatmap, { days, today: TODAY, weeks: 2 }))
const buttons = queryAll<HTMLButtonElement>(m.container, 'button.lc-heat-cell')
assert.equal(buttons.length, 7)
const byKey = new Map(buttons.map(b => [b.getAttribute('aria-label')?.split('\n')[0], b]))
assert.ok(byKey.get('2026-09-16')?.className.includes('lc-heat-4'), 'the maximum day is deepest')
assert.ok(byKey.get('2026-09-15')?.className.includes('lc-heat-2'))
assert.ok(byKey.get('2026-09-14')?.className.includes('lc-heat-1'))
assert.ok(byKey.get('2026-09-10')?.className.includes('lc-heat-2'), '30 of the 100-token peak draws level 2')
assert.ok(byKey.get('2026-09-09')?.className.includes('lc-heat-3'))
assert.ok(byKey.get('2026-09-08')?.className.includes('lc-heat-1'), 'a crumb is never invisible')
// A zero-token day stays as an activity button (its requests still count),
// but the token metric depths it at level 0.
assert.ok(byKey.get('2026-09-07')?.className.includes('lc-heat-0'))
assert.equal(byKey.get('2026-09-16')?.getAttribute('aria-label'), '2026-09-16\n2 active sessions\n100 tokens')
assert.equal(byKey.get('2026-09-10')?.getAttribute('aria-label'), '2026-09-10\n1 active sessions\n30 tokens')
// Inert cells: data-less days and future days draw as plain spans.
const spans = queryAll(m.container, 'span.lc-heat-cell')
assert.ok(spans.length > 0)
assert.ok(spans.some(s => s.className.includes('lc-heat-future')), 'future placeholders present')
// Weekday labels sit on the rows they name (Sunday-first: row 1 = Mon): Mon/Wed/Fri carry their names, the rest blank.
const wds = queryAll(m.container, '.lc-heat-wd')
assert.deepEqual(wds.map(w => w.textContent), ['', 'Mon', '', 'Wed', '', 'Fri', ''])
await m.unmount()
})
test('month labels mark the columns where a month begins', async () => {
const days = { '2026-09-16': { tokens: 10, requests: 1, sessions: 1, cost: null } }
// The default 8-week window runs Sunday 2026-07-26 → 2026-09-13: July
// began outside it, August begins in the Jul 26 column (Sat Aug 1) and September in the Aug 30 column (Tue Sep 1).
const m = await mount(h(Heatmap, { days, today: TODAY }))
assert.deepEqual(queryAll(m.container, '.lc-heat-mon').map(s => s.textContent), ['Aug', 'Sep'])
await m.unmount()
// A mid-month window never crosses a month's first day, yet the opening
// column still names its own month; later columns stay unlabeled.
const m2 = await mount(h(Heatmap, { days, today: TODAY, weeks: 2 }))
assert.deepEqual(queryAll(m2.container, '.lc-heat-mon').map(s => s.textContent), ['Sep'])
await m2.unmount()
// A later column that opens on the 1st carries the label itself (its
// whole week stays in the new month; 2026-02-01 was a Sunday).
const m3 = await mount(h(Heatmap, { days: { '2026-02-03': { tokens: 1, requests: 1, sessions: 1, cost: null } }, today: '2026-02-03', weeks: 2 }))
assert.deepEqual(queryAll(m3.container, '.lc-heat-mon').map(s => s.textContent), ['Jan', 'Feb'])
await m3.unmount()
})
test('a Less→More key lays out the five depth steps under the grid', async () => {
const m = await mount(h(Heatmap, { days: { '2026-09-16': { tokens: 10, requests: 1, sessions: 1, cost: null } }, today: TODAY, weeks: 2 }))
const legend = query(m.container, '.lc-heat-legend')
assert.deepEqual(queryAll(legend, '.lc-heat-cell').map(s => s.className), [
'lc-heat-cell lc-heat-0',
'lc-heat-cell lc-heat-1',
'lc-heat-cell lc-heat-2',
'lc-heat-cell lc-heat-3',
'lc-heat-cell lc-heat-4',
])
assert.deepEqual(queryAll(legend, '.lc-heat-key').map(s => s.textContent), ['Less', 'More'])
await m.unmount()
})
test('the sessions metric depths on each day’s distinct sessions, independently of tokens', async () => {
const days = {
'2026-09-16': { tokens: 100, requests: 4, sessions: 2, cost: null },
'2026-09-09': { tokens: 75, requests: 1, sessions: 3, cost: null },
}
const m = await mount(h(Heatmap, { days, today: TODAY, weeks: 2, metric: 'sessions' }))
const byKey = new Map(queryAll<HTMLButtonElement>(m.container, 'button.lc-heat-cell').map(b => [b.getAttribute('aria-label')?.split('\n')[0], b]))
assert.ok(byKey.get('2026-09-16')?.className.includes('lc-heat-3'), 'two of the three-session peak draws level 3')
assert.ok(byKey.get('2026-09-09')?.className.includes('lc-heat-4'), 'the sessions peak depths level 4 that its smaller token share never would')
await m.unmount()
})
test('cells carry the page hover tip in data attributes (date, sessions, tokens; an empty day the bare date)', async () => {
const m = await mount(h(Heatmap, {
days: { '2026-09-16': { tokens: 10, requests: 1, sessions: 3, cost: null } },
today: TODAY,
weeks: 2,
}))
assert.equal(
query<HTMLButtonElement>(m.container, 'button.lc-heat-cell').getAttribute('data-lc-tip'),
'2026-09-16\n3 active sessions\n10 tokens',
)
assert.match(query(m.container, 'span.lc-heat-0').getAttribute('data-lc-tip') ?? '', /^\d{4}-\d{2}-\d{2}$/)
await m.unmount()
})
test('clicking a data day pins it; clicking again releases; the ring follows the selection', async () => {
const picked: (string | null)[] = []
const record = (day: string | null): void => { picked.push(day) }
const m = await mount(h(Heatmap, {
days: { '2026-09-16': { tokens: 10, requests: 1, sessions: 1, cost: null } },
today: TODAY,
weeks: 2,
selected: null,
onSelect: record,
}))
const cell = query<HTMLButtonElement>(m.container, 'button.lc-heat-cell')
await click(cell)
assert.deepEqual(picked, ['2026-09-16'])
await m.update(h(Heatmap, {
days: { '2026-09-16': { tokens: 10, requests: 1, sessions: 1, cost: null } },
today: TODAY,
weeks: 2,
selected: '2026-09-16',
onSelect: record,
}))
assert.ok(query(m.container, 'button.lc-heat-cell').className.includes('lc-heat-on'))
assert.equal(query(m.container, 'button.lc-heat-cell').getAttribute('aria-pressed'), 'true')
await click(query<HTMLButtonElement>(m.container, 'button.lc-heat-cell'))
assert.deepEqual(picked, ['2026-09-16', null], 'the second click releases the pin')
await m.unmount()
})
test('without an onSelect relay the cells stay inert', async () => {
const m = await mount(h(Heatmap, { days: { '2026-09-16': { tokens: 10, requests: 1, sessions: 1, cost: null } }, today: TODAY, weeks: 2 }))
const cell = query<HTMLButtonElement>(m.container, 'button.lc-heat-cell')
await click(cell)
assert.ok(!cell.className.includes('lc-heat-on'))
await m.unmount()
})
test('todayKey keys the render day (the panel’s runtime call)', () => {
assert.match(todayKey(), /^\d{4}-\d{2}-\d{2}$/)
})
})