Redesign the Storage Monitor as a storage-composition breakdown (snippets · datasets · app)

This commit is contained in:
2026-06-10 01:32:22 +03:00
parent e7de0cbb8a
commit 7599acd25e
12 changed files with 438 additions and 481 deletions
+50 -72
View File
@@ -1,89 +1,67 @@
import { describe, expect, test } from 'vitest';
import {
CRITICAL_THRESHOLD,
WARNING_THRESHOLD,
humanizeBytes,
summarizeStorage,
} from './storage-estimate';
import { humanizeBytes, jsonByteSize, summarizeStorage } from './storage-estimate';
describe('summarizeStorage', () => {
test('computes the fraction and passes usage/quota through', () => {
const s = summarizeStorage({ usage: 250, quota: 1000 });
expect(s.available).toBe(true);
expect(s.usedBytes).toBe(250);
expect(s.quotaBytes).toBe(1000);
expect(s.fraction).toBeCloseTo(0.25);
expect(s.level).toBe('ok');
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('clamps fraction to 1 when usage exceeds quota', () => {
const s = summarizeStorage({ usage: 1500, quota: 1000 });
expect(s.fraction).toBe(1);
expect(s.level).toBe('critical');
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('guards divide-by-zero: quota of 0 is unusable', () => {
const s = summarizeStorage({ usage: 10, quota: 0 });
expect(s.available).toBe(false);
expect(s.fraction).toBe(0);
expect(s.usedBytes).toBe(0);
expect(s.quotaBytes).toBe(0);
expect(s.level).toBe('ok');
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);
});
describe('level boundaries', () => {
test('just below warning (0.79) is ok', () => {
expect(summarizeStorage({ usage: 79, quota: 100 }).level).toBe('ok');
});
test('exactly at warning (0.80) is warning', () => {
const s = summarizeStorage({ usage: 80, quota: 100 });
expect(s.fraction).toBeCloseTo(WARNING_THRESHOLD);
expect(s.level).toBe('warning');
});
test('just below critical (0.94) is warning', () => {
expect(summarizeStorage({ usage: 94, quota: 100 }).level).toBe('warning');
});
test('exactly at critical (0.95) is critical', () => {
const s = summarizeStorage({ usage: 95, quota: 100 });
expect(s.fraction).toBeCloseTo(CRITICAL_THRESHOLD);
expect(s.level).toBe('critical');
});
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);
});
describe('availability', () => {
test('missing usage -> unavailable', () => {
const s = summarizeStorage({ quota: 1000 });
expect(s.available).toBe(false);
expect(s.fraction).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('missing quota -> unavailable', () => {
const s = summarizeStorage({ usage: 1000 });
expect(s.available).toBe(false);
expect(s.fraction).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);
});
});
test('both missing (empty input) -> unavailable', () => {
const s = summarizeStorage({});
expect(s.available).toBe(false);
});
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('non-finite or negative values -> unavailable', () => {
expect(summarizeStorage({ usage: NaN, quota: 1000 }).available).toBe(false);
expect(summarizeStorage({ usage: Infinity, quota: 1000 }).available).toBe(false);
expect(summarizeStorage({ usage: -1, quota: 1000 }).available).toBe(false);
expect(summarizeStorage({ usage: 10, quota: -5 }).available).toBe(false);
});
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('zero usage with a real quota is available and ok', () => {
const s = summarizeStorage({ usage: 0, quota: 1000 });
expect(s.available).toBe(true);
expect(s.fraction).toBe(0);
expect(s.level).toBe('ok');
});
test('an unserializable (circular) value degrades to 0', () => {
const a: Record<string, unknown> = {};
a.self = a;
expect(jsonByteSize(a)).toBe(0);
});
});