import { test } from 'node:test'; import assert from 'node:assert/strict'; import { Store } from '../src/store.js'; import { MS_PER_DAY } from '../src/pure.js'; import { createLocalStorage } from './localstorage-shim.js'; const DAY = MS_PER_DAY; const NOW = 1_700_000_000_000; function freshStore() { return new Store({ storage: createLocalStorage() }); } test('Store: starts empty', () => { const s = freshStore(); assert.equal(s.getTarget('strength'), null); }); test('Store: setTarget validates via parseTarget', () => { const s = freshStore(); assert.equal(s.setTarget('strength', '25M'), true); assert.equal(s.getTarget('strength'), 25_000_000); }); test('Store: setTarget rejects invalid and keeps previous', () => { const s = freshStore(); s.setTarget('strength', 25_000_000); assert.equal(s.setTarget('strength', 'abc'), false); assert.equal(s.getTarget('strength'), 25_000_000); }); test('Store: targets are per-attribute', () => { const s = freshStore(); s.setTarget('strength', 25_000_000); s.setTarget('speed', 50_000_000); assert.equal(s.getTarget('strength'), 25_000_000); assert.equal(s.getTarget('speed'), 50_000_000); }); test('Store: persists across instances via storage', () => { const storage = createLocalStorage(); const a = new Store({ storage }); a.setTarget('strength', 25_000_000); const b = new Store({ storage }); assert.equal(b.getTarget('strength'), 25_000_000); }); test('Store: corrupted JSON is wiped with a warning', () => { const storage = createLocalStorage(); storage.setItem('tat.targets', '{not json'); const warnings = []; const s = new Store({ storage, onWarn: (m) => warnings.push(m) }); assert.equal(s.getTarget('strength'), null); assert.equal(warnings.length, 1); assert.equal(storage.getItem('tat.targets'), null); const warningsBefore = warnings.length; new Store({ storage, onWarn: (m) => warnings.push(m) }); assert.equal(warnings.length, warningsBefore); }); test('Store: latches off further saves after a storage write failure', () => { const data = new Map(); const throwingStorage = { getItem(k) { return data.has(k) ? data.get(k) : null; }, setItem() { throw new Error('quota exceeded'); }, removeItem(k) { data.delete(k); }, clear() { data.clear(); }, }; const warnings = []; const s = new Store({ storage: throwingStorage, onWarn: (m) => warnings.push(m) }); assert.equal(s.setTarget('strength', 25_000_000), false); assert.equal(s.setTarget('speed', 50_000_000), false); assert.equal(warnings.length, 1); assert.equal(s.getTarget('strength'), 25_000_000); assert.equal(s.getTarget('speed'), 50_000_000); }); test('Store: recordTrain appends to per-attribute history', () => { const s = freshStore(); s.recordTrain('strength', 247, NOW); s.recordTrain('strength', 250, NOW - 1000); assert.equal(s.history.strength.length, 2); assert.equal(s.history.strength[0].delta, 247); }); test('Store: recordTrain prunes entries older than 30 days', () => { const s = freshStore(); s.recordTrain('strength', 100, NOW - 31 * DAY); s.recordTrain('strength', 200, NOW); assert.equal(s.history.strength.length, 1); assert.equal(s.history.strength[0].delta, 200); }); test('Store: getSummary returns computed summary for attribute', () => { const s = freshStore(); s.recordTrain('strength', 247, NOW - 1000); s.recordTrain('strength', 247, NOW - 2000); const sum = s.getSummary('strength', NOW); assert.equal(sum.trainsToday, 2); assert.equal(sum.perDay, Math.floor((2 / 7) * 247)); }); test('Store: recordTrain rejects non-positive and non-number deltas', () => { const s = freshStore(); assert.equal(s.recordTrain('strength', 0, NOW), false); assert.equal(s.recordTrain('strength', -5, NOW), false); assert.equal(s.recordTrain('strength', '10', NOW), false); assert.equal(s.recordTrain('strength', NaN, NOW), false); assert.equal(s.history.strength, undefined); }); test('Store: getSummary on unknown attribute returns zeros', () => { const s = freshStore(); const sum = s.getSummary('nonexistent', NOW); assert.equal(sum.trainsToday, 0); assert.equal(sum.sevenDayAvgPerDay, 0); assert.equal(sum.perDay, 0); });