import assert from 'node:assert/strict'
import test from 'node:test'
import {
clampRange,
closestIndex,
colorForLanguage,
decoratePoints,
filterPointsByRange,
languageStats,
} from './chart-model.ts'
function point(
sha: string,
at: string,
buckets: Array<{ language: string; kind: string; additions: number; deletions: number }>,
) {
const additions = buckets.reduce((sum, bucket) => sum + bucket.additions, 0)
const deletions = buckets.reduce((sum, bucket) => sum + bucket.deletions, 0)
return {
sha,
at,
message: sha,
additions,
deletions,
net: additions - deletions,
cumulative: 0,
buckets,
}
}
test('cumulative carries untouched languages forward instead of dropping to 0', () => {
const decorated = decoratePoints(
[
point('a', '2026-01-01T00:00:00Z', [
{ language: 'TypeScript', kind: 'source', additions: 1000, deletions: 0 },
]),
point('b', '2026-01-02T00:00:00Z', [
{ language: 'Markdown', kind: 'docs', additions: 5, deletions: 0 },
]),
point('c', '2026-01-03T00:00:00Z', [
{ language: 'TypeScript', kind: 'source', additions: 0, deletions: 200 },
]),
],
['TypeScript', 'Markdown'],
['source', 'tests', 'docs'],
)
assert.equal(decorated[0]?.cumulative, 1000)
assert.equal(decorated[1]?.cumulative, 1005)
assert.equal(decorated[1]?.byLanguage.get('TypeScript')?.cumulative, 1000)
assert.equal(decorated[2]?.cumulative, 805)
assert.ok((decorated[1]?.cumulative ?? 0) > 0)
})
test('date window keeps pre-window cumulative', () => {
const decorated = decoratePoints(
[
point('a', '2026-08-01T00:00:00Z', [
{ language: 'TypeScript', kind: 'source', additions: 500000, deletions: 0 },
]),
point('b', '2026-08-28T00:00:00Z', [
{ language: 'JSON', kind: 'source', additions: 20, deletions: 0 },
]),
],
['TypeScript', 'JSON'],
['source', 'tests', 'docs'],
)
const visible = filterPointsByRange(decorated, '2026-08-27T00:00:00Z', '2026-08-29T00:00:00Z')
assert.equal(visible.length, 1)
assert.equal(visible[0]?.cumulative, 500020)
})
test('language chips sort by current-view lines', () => {
const decorated = decoratePoints(
[
point('a', '2026-01-01T00:00:00Z', [
{ language: 'YAML', kind: 'source', additions: 10, deletions: 0 },
{ language: 'TypeScript', kind: 'source', additions: 900, deletions: 0 },
{ language: 'CSS', kind: 'source', additions: 50, deletions: 0 },
]),
],
['YAML', 'TypeScript', 'CSS'],
['source', 'tests', 'docs'],
)
assert.deepEqual(
languageStats(decorated, ['YAML', 'TypeScript', 'CSS']).map((item) => item.language),
['TypeScript', 'CSS', 'YAML'],
)
assert.equal(languageStats(decorated, ['YAML', 'TypeScript', 'CSS'])[0]?.lines, 900)
})
test('language colors stay stable when sort order changes', () => {
const languages = ['CSS', 'TypeScript', 'YAML']
assert.equal(
colorForLanguage('TypeScript', languages),
colorForLanguage('TypeScript', ['YAML', 'TypeScript', 'CSS']),
)
assert.notEqual(colorForLanguage('TypeScript', languages), colorForLanguage('CSS', languages))
})
test('range helpers snap dates to commit indices', () => {
const points = [
{ at: '2026-08-01T00:00:00Z' },
{ at: '2026-08-15T00:00:00Z' },
{ at: '2026-08-29T00:00:00Z' },
]
assert.equal(closestIndex(points, '2026-08-14T12:00:00Z'), 1)
assert.deepEqual(clampRange(2, 0, 2), { from: 0, to: 2 })
})