97 lines
3.1 KiB
JavaScript
97 lines
3.1 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { parseTarget, computeEstimate, pruneHistory, MS_PER_DAY } from '../src/pure.js';
|
|
|
|
const DAY = MS_PER_DAY;
|
|
const NOW = 1_700_000_000_000; // fixed reference
|
|
|
|
test('parseTarget: integer numbers', () => {
|
|
assert.equal(parseTarget(25), 25);
|
|
assert.equal(parseTarget(25000000), 25_000_000);
|
|
assert.equal(parseTarget(2.5e7), 25_000_000);
|
|
});
|
|
|
|
test('parseTarget: string numbers with commas', () => {
|
|
assert.equal(parseTarget('25'), 25);
|
|
assert.equal(parseTarget('25,000,000'), 25_000_000);
|
|
assert.equal(parseTarget('25,000,000.5'), 25_000_000);
|
|
});
|
|
|
|
test('parseTarget: magnitude suffixes (case-insensitive)', () => {
|
|
assert.equal(parseTarget('25K'), 25_000);
|
|
assert.equal(parseTarget('25m'), 25_000_000);
|
|
assert.equal(parseTarget('1.5B'), 1_500_000_000);
|
|
assert.equal(parseTarget('100t'), 100_000_000_000_000);
|
|
});
|
|
|
|
test('parseTarget: rejects invalid input', () => {
|
|
assert.equal(parseTarget(0), null);
|
|
assert.equal(parseTarget(-1), null);
|
|
assert.equal(parseTarget('abc'), null);
|
|
assert.equal(parseTarget(''), null);
|
|
assert.equal(parseTarget(null), null);
|
|
assert.equal(parseTarget(undefined), null);
|
|
assert.equal(parseTarget('25M.5'), null);
|
|
assert.equal(parseTarget('1.5.5M'), null);
|
|
assert.equal(parseTarget('0'), null);
|
|
});
|
|
|
|
test('computeEstimate: typical case', () => {
|
|
const r = computeEstimate(14_328_501, 25_000_000, 247, 4520);
|
|
assert.equal(r.remaining, 10_671_499);
|
|
assert.equal(r.trainsToGo, 43_205);
|
|
assert.equal(r.days, 2_361);
|
|
assert.ok(r.eta instanceof Date);
|
|
});
|
|
|
|
test('computeEstimate: target reached', () => {
|
|
const r = computeEstimate(25_000_000, 25_000_000, 247, 4520);
|
|
assert.equal(r.remaining, 0);
|
|
assert.equal(r.trainsToGo, 0);
|
|
assert.equal(r.days, 0);
|
|
assert.equal(r.eta, null);
|
|
});
|
|
|
|
test('computeEstimate: target below current', () => {
|
|
const r = computeEstimate(30_000_000, 25_000_000, 247, 4520);
|
|
assert.equal(r.remaining, 0);
|
|
assert.equal(r.trainsToGo, 0);
|
|
assert.equal(r.days, 0);
|
|
assert.equal(r.eta, null);
|
|
});
|
|
|
|
test('computeEstimate: zero perTrain or perDay does not crash', () => {
|
|
const a = computeEstimate(100, 200, 0, 50);
|
|
assert.equal(a.trainsToGo, 0);
|
|
assert.equal(a.days, 2);
|
|
|
|
const b = computeEstimate(100, 200, 50, 0);
|
|
assert.equal(b.trainsToGo, 2);
|
|
assert.equal(b.days, 0);
|
|
assert.equal(b.eta, null);
|
|
});
|
|
|
|
test('pruneHistory: keeps entries within 30 days, drops older', () => {
|
|
const entries = [
|
|
{ ts: NOW, delta: 100 },
|
|
{ ts: NOW - 1 * DAY, delta: 100 },
|
|
{ ts: NOW - 29 * DAY, delta: 100 },
|
|
{ ts: NOW - 30 * DAY, delta: 100 }, // exactly 30 days, dropped (strict <)
|
|
{ ts: NOW - 31 * DAY, delta: 100 },
|
|
];
|
|
const out = pruneHistory(entries, NOW);
|
|
assert.equal(out.length, 3);
|
|
assert.deepEqual(out.map((e) => e.ts), [NOW, NOW - 1 * DAY, NOW - 29 * DAY]);
|
|
});
|
|
|
|
test('pruneHistory: does not mutate input', () => {
|
|
const entries = [{ ts: NOW - 31 * DAY, delta: 100 }];
|
|
const copy = [...entries];
|
|
pruneHistory(entries, NOW);
|
|
assert.deepEqual(entries, copy);
|
|
});
|
|
|
|
test('pruneHistory: empty input', () => {
|
|
assert.deepEqual(pruneHistory([], NOW), []);
|
|
});
|