import { describe, expect, test } from 'vitest'; import { humanizeBytes, jsonByteSize, summarizeStorage } from './storage-estimate'; describe('summarizeStorage (composition)', () => { test('with origin usage: app = usage − snippets − datasets, total = usage', () => { const c = summarizeStorage({ usageBytes: 1000, snippetBytes: 100, datasetBytes: 300 }); expect(c.originMeasured).toBe(true); expect(c.totalBytes).toBe(1000); expect(c.segments).toEqual([ { key: 'snippets', label: 'Snippets', bytes: 100 }, { key: 'datasets', label: 'Datasets', bytes: 300 }, { key: 'app', label: 'App', bytes: 600 }, ]); }); test('without origin usage: snippets + datasets only, total is their sum', () => { const c = summarizeStorage({ snippetBytes: 100, datasetBytes: 300 }); expect(c.originMeasured).toBe(false); expect(c.totalBytes).toBe(400); expect(c.segments.map((s) => s.key)).toEqual(['snippets', 'datasets']); }); test('usage smaller than measured data falls back to snippets+datasets (estimate lag)', () => { const c = summarizeStorage({ usageBytes: 50, snippetBytes: 100, datasetBytes: 300 }); expect(c.originMeasured).toBe(false); expect(c.totalBytes).toBe(400); expect(c.segments).toHaveLength(2); }); test('usage exactly equal to measured data yields a zero-byte app segment', () => { const c = summarizeStorage({ usageBytes: 400, snippetBytes: 100, datasetBytes: 300 }); expect(c.originMeasured).toBe(true); expect(c.segments.find((s) => s.key === 'app')?.bytes).toBe(0); }); test('invalid / negative / non-finite inputs degrade to 0 bytes', () => { const c = summarizeStorage({ usageBytes: NaN, snippetBytes: -5, datasetBytes: Infinity }); expect(c.segments[0].bytes).toBe(0); // snippets expect(c.segments[1].bytes).toBe(0); // datasets expect(c.originMeasured).toBe(false); // usage NaN → unusable expect(c.totalBytes).toBe(0); }); test('empty workspace: every segment is zero, total zero', () => { const c = summarizeStorage({ snippetBytes: 0, datasetBytes: 0 }); expect(c.totalBytes).toBe(0); expect(c.segments.every((s) => s.bytes === 0)).toBe(true); }); }); describe('jsonByteSize', () => { test('measures the UTF-8 byte length of the JSON serialization', () => { expect(jsonByteSize({ a: 1 })).toBe(new TextEncoder().encode('{"a":1}').length); }); test('counts multi-byte characters by their UTF-8 size', () => { // "é" is 2 UTF-8 bytes; JSON.stringify("é") => the 4-byte string «"é"». expect(jsonByteSize('é')).toBe(4); }); test('an unserializable (circular) value degrades to 0', () => { const a: Record = {}; a.self = a; expect(jsonByteSize(a)).toBe(0); }); }); describe('humanizeBytes', () => { test('zero renders as whole bytes', () => { expect(humanizeBytes(0)).toBe('0 B'); }); test('small values stay in whole bytes', () => { expect(humanizeBytes(1)).toBe('1 B'); expect(humanizeBytes(512)).toBe('512 B'); expect(humanizeBytes(1023)).toBe('1023 B'); }); test('KB boundary (1024) and within KB', () => { expect(humanizeBytes(1024)).toBe('1.0 KB'); expect(humanizeBytes(1536)).toBe('1.5 KB'); }); test('MB boundary and within MB', () => { expect(humanizeBytes(1024 * 1024)).toBe('1.0 MB'); expect(humanizeBytes(5 * 1024 * 1024)).toBe('5.0 MB'); expect(humanizeBytes(2.5 * 1024 * 1024)).toBe('2.5 MB'); }); test('GB boundary', () => { expect(humanizeBytes(1024 * 1024 * 1024)).toBe('1.0 GB'); }); test('caps at the largest unit (TB)', () => { expect(humanizeBytes(1024 ** 4)).toBe('1.0 TB'); expect(humanizeBytes(1024 ** 5)).toBe('1024.0 TB'); }); test('negative and non-finite inputs degrade to 0 B', () => { expect(humanizeBytes(-1)).toBe('0 B'); expect(humanizeBytes(NaN)).toBe('0 B'); expect(humanizeBytes(Infinity)).toBe('0 B'); }); });