2026-08-09 19:30:25 +03:00
|
|
|
import { describe, expect, test } from 'bun:test';
|
|
|
|
|
import { computeContextUsage, DEFAULT_CONTEXT_LIMIT } from './contextUsage';
|
|
|
|
|
|
|
|
|
|
const assistant = (tokens: Record<string, unknown>, id = 'msg') => ({ id, role: 'assistant', tokens });
|
|
|
|
|
|
|
|
|
|
describe('computeContextUsage', () => {
|
|
|
|
|
test('sums every token bucket of the newest reporting assistant message', () => {
|
|
|
|
|
const usage = computeContextUsage(
|
|
|
|
|
[assistant({ input: 100, output: 20, reasoning: 5, cache: { read: 800, write: 75 } })],
|
|
|
|
|
2000,
|
|
|
|
|
);
|
|
|
|
|
expect(usage?.totalTokens).toBe(1000);
|
|
|
|
|
expect(usage?.percent).toBe(50);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('reports the latest turn rather than a sum across turns', () => {
|
2026-08-14 23:06:56 +02:00
|
|
|
// A turn's tokens describe that turn's window, so adding turns up would
|
|
|
|
|
// report several times the real fill.
|
2026-08-09 19:30:25 +03:00
|
|
|
const usage = computeContextUsage(
|
|
|
|
|
[
|
|
|
|
|
assistant({ input: 400, output: 0, reasoning: 0 }, 'old'),
|
|
|
|
|
assistant({ input: 900, output: 0, reasoning: 0 }, 'new'),
|
|
|
|
|
],
|
|
|
|
|
1000,
|
|
|
|
|
);
|
|
|
|
|
expect(usage?.totalTokens).toBe(900);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('skips user messages and assistant turns that reported nothing', () => {
|
|
|
|
|
const usage = computeContextUsage(
|
|
|
|
|
[
|
|
|
|
|
assistant({ input: 300, output: 0, reasoning: 0 }, 'real'),
|
|
|
|
|
assistant({ input: 0, output: 0, reasoning: 0 }, 'zeroed'),
|
|
|
|
|
{ id: 'user', role: 'user' },
|
|
|
|
|
],
|
|
|
|
|
1000,
|
|
|
|
|
);
|
|
|
|
|
expect(usage?.totalTokens).toBe(300);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('leaves the percentage unrounded', () => {
|
|
|
|
|
// Rounding here is what made the panel print "34.0%" against the header's
|
|
|
|
|
// "33.6%".
|
|
|
|
|
const usage = computeContextUsage([assistant({ input: 336, output: 0, reasoning: 0 })], 1000);
|
|
|
|
|
expect(usage?.percent.toFixed(1)).toBe('33.6');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('falls back to the default limit when the model exposes none', () => {
|
|
|
|
|
const usage = computeContextUsage([assistant({ input: 20_000, output: 0, reasoning: 0 })], 0);
|
|
|
|
|
expect(usage?.limit).toBe(DEFAULT_CONTEXT_LIMIT);
|
|
|
|
|
expect(usage?.percent).toBe(10);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('returns null when no message carries usable tokens', () => {
|
|
|
|
|
expect(computeContextUsage([], 1000)).toBeNull();
|
|
|
|
|
expect(computeContextUsage([{ id: 'u', role: 'user' }], 1000)).toBeNull();
|
|
|
|
|
expect(computeContextUsage([assistant({ input: 0, output: 0, reasoning: 0 })], 1000)).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('tolerates partial token payloads', () => {
|
|
|
|
|
const usage = computeContextUsage([assistant({ input: 10 })], 100);
|
|
|
|
|
expect(usage?.totalTokens).toBe(10);
|
|
|
|
|
});
|
2026-08-14 23:06:56 +02:00
|
|
|
|
|
|
|
|
test('prefers the server-reported total over summing round-trip fields', () => {
|
|
|
|
|
// Real payload from opencode 1.18.18: ~14 tool-call round-trips accumulated
|
|
|
|
|
// cache.read to 3.29M while the 1M window really held 232,872. Summing
|
|
|
|
|
// rendered 330.6%; the reported total renders the real 23.3%.
|
|
|
|
|
const usage = computeContextUsage(
|
|
|
|
|
[assistant({ total: 232_872, input: 0, output: 14_523, reasoning: 0, cache: { read: 3_291_956, write: 0 } })],
|
|
|
|
|
1_000_000,
|
|
|
|
|
);
|
|
|
|
|
expect(usage?.totalTokens).toBe(232_872);
|
|
|
|
|
expect(usage?.percent.toFixed(4)).toBe('23.2872');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('selects a message whose only signal is the reported total', () => {
|
|
|
|
|
const usage = computeContextUsage(
|
|
|
|
|
[assistant({ total: 5_000, input: 0, output: 0, reasoning: 0 })],
|
|
|
|
|
100_000,
|
|
|
|
|
);
|
|
|
|
|
expect(usage?.totalTokens).toBe(5_000);
|
|
|
|
|
});
|
2026-08-09 19:30:25 +03:00
|
|
|
});
|