Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx",
"test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx src/hooks/useDrandCountdown.test.ts",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
Expand Down
158 changes: 158 additions & 0 deletions apps/web/src/hooks/useDrandCountdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import assert from "node:assert/strict";
import { test } from "node:test";

import { computeCountdown, formatCountdown } from "./useDrandCountdown";

// Fixed Drand network parameters matching the quicknet defaults.
const GENESIS = 1_692_803_367;
const PERIOD = 3;

// Target round used across boundary tests.
const TARGET_ROUND = 1_000_000;
// targetTime = genesis + period * round
const TARGET_TIME = GENESIS + PERIOD * TARGET_ROUND; // 1695803367

// ---------------------------------------------------------------------------
// computeCountdown
// ---------------------------------------------------------------------------

test("computeCountdown: one second before R reports not published with 1s remaining", () => {
const result = computeCountdown(
TARGET_ROUND,
TARGET_TIME - 1, // nowSecs
GENESIS,
PERIOD,
);

assert.equal(result.published, false);
assert.equal(result.targetTime, TARGET_TIME);
assert.equal(result.secondsRemaining, 1);
assert.equal(result.currentRound, TARGET_ROUND - 1);
});

test("computeCountdown: exactly at R reports published with 0s remaining", () => {
const result = computeCountdown(
TARGET_ROUND,
TARGET_TIME, // nowSecs
GENESIS,
PERIOD,
);

assert.equal(result.published, true);
assert.equal(result.targetTime, TARGET_TIME);
assert.equal(result.secondsRemaining, 0);
assert.equal(result.currentRound, TARGET_ROUND);
});

test("computeCountdown: after R reports published with 0s remaining", () => {
const result = computeCountdown(
TARGET_ROUND,
TARGET_TIME + 1, // nowSecs
GENESIS,
PERIOD,
);

assert.equal(result.published, true);
assert.equal(result.targetTime, TARGET_TIME);
assert.equal(result.secondsRemaining, 0);
// currentRound is still the target round until the next period boundary
assert.equal(result.currentRound, TARGET_ROUND);
});

test("computeCountdown: published round (well in the past) reports secondsRemaining 0", () => {
const result = computeCountdown(
TARGET_ROUND,
TARGET_TIME + 900_000, // far in the future relative to R
GENESIS,
PERIOD,
);

assert.equal(result.published, true);
assert.equal(result.secondsRemaining, 0);
});

test("computeCountdown: future round reports correct targetTime and remaining seconds", () => {
const result = computeCountdown(
TARGET_ROUND + 5,
TARGET_TIME, // nowSecs pinned at TARGET_ROUND
GENESIS,
PERIOD,
);

const expectedTarget = GENESIS + PERIOD * (TARGET_ROUND + 5);

assert.equal(result.published, false);
assert.equal(result.targetTime, expectedTarget);
assert.equal(result.secondsRemaining, PERIOD * 5);
assert.equal(result.currentRound, TARGET_ROUND);
});

test("computeCountdown: round zero with genesis at epoch 0", () => {
const result = computeCountdown(0, 0, 0, 3);

assert.equal(result.currentRound, 0);
assert.equal(result.targetTime, 0);
assert.equal(result.published, true);
assert.equal(result.secondsRemaining, 0);
});

test("computeCountdown: large target round computes correctly", () => {
const round = 100_000_000;
const expectedTarget = GENESIS + PERIOD * round;

const result = computeCountdown(round, expectedTarget - 1, GENESIS, PERIOD);

assert.equal(result.published, false);
assert.equal(result.targetTime, expectedTarget);
assert.equal(result.secondsRemaining, 1);
assert.equal(result.currentRound, round - 1);
});

// ---------------------------------------------------------------------------
// formatCountdown
// ---------------------------------------------------------------------------

test("formatCountdown: zero seconds returns 'published'", () => {
assert.equal(formatCountdown(0), "published");
});

test("formatCountdown: negative seconds returns 'published'", () => {
assert.equal(formatCountdown(-1), "published");
assert.equal(formatCountdown(-100), "published");
});

test("formatCountdown: single digit seconds", () => {
assert.equal(formatCountdown(1), "1s");
assert.equal(formatCountdown(9), "9s");
});

test("formatCountdown: double digit seconds, no minutes", () => {
assert.equal(formatCountdown(10), "10s");
assert.equal(formatCountdown(59), "59s");
});

test("formatCountdown: exactly one minute", () => {
assert.equal(formatCountdown(60), "1m 0s");
});

test("formatCountdown: minutes with seconds", () => {
assert.equal(formatCountdown(61), "1m 1s");
assert.equal(formatCountdown(119), "1m 59s");
assert.equal(formatCountdown(120), "2m 0s");
assert.equal(formatCountdown(3599), "59m 59s");
});

test("formatCountdown: exactly one hour", () => {
assert.equal(formatCountdown(3600), "1h 0m 0s");
});

test("formatCountdown: hours with minutes and seconds", () => {
assert.equal(formatCountdown(3661), "1h 1m 1s");
assert.equal(formatCountdown(7200), "2h 0m 0s");
assert.equal(formatCountdown(7323), "2h 2m 3s");
});

test("formatCountdown: large hour values", () => {
const seconds = 24 * 3600 + 30 * 60 + 45; // 24h 30m 45s
assert.equal(formatCountdown(seconds), "24h 30m 45s");
});
36 changes: 26 additions & 10 deletions apps/web/src/hooks/useDrandCountdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,38 @@ export interface DrandCountdown {
published: boolean;
}

function timeOfRound(round: number): number {
return QUICKNET_GENESIS + QUICKNET_PERIOD * round;
/**
* Pure countdown calculation — accepts fixed timestamps, genesis, and period
* so tests can pin the wall clock. Produces deterministic
* { currentRound, targetTime, published, secondsRemaining }.
*/
export function computeCountdown(
targetRound: number,
nowSecs: number,
genesis: number,
period: number,
): { currentRound: number; targetTime: number; published: boolean; secondsRemaining: number } {
const currentRound = Math.floor((nowSecs - genesis) / period);
const targetTime = genesis + period * targetRound;
const published = currentRound >= targetRound;

return {
currentRound,
targetTime,
published,
secondsRemaining: published ? 0 : Math.max(0, targetTime - nowSecs),
};
}

function localCountdown(targetRound: number): Omit<DrandCountdown, "loading" | "error"> {
const now = Math.floor(Date.now() / 1000);
const targetTime = timeOfRound(targetRound);
const currentRound = Math.floor((now - QUICKNET_GENESIS) / QUICKNET_PERIOD);
const published = currentRound >= targetRound;
const { currentRound, targetTime, published, secondsRemaining } =
computeCountdown(targetRound, now, QUICKNET_GENESIS, QUICKNET_PERIOD);

return {
currentRound,
targetRound,
secondsRemaining: published ? 0 : Math.max(0, targetTime - now),
secondsRemaining,
targetTime,
published,
};
Expand All @@ -54,10 +72,8 @@ export function useDrandCountdown(targetRound: number, pollMs = 1000): DrandCoun
const genesis = info.genesis_time;
const period = info.period;
const now = Math.floor(Date.now() / 1000);
const currentRound = Math.floor((now - genesis) / period);
const targetTime = genesis + period * targetRound;
const published = currentRound >= targetRound;
const secondsRemaining = published ? 0 : Math.max(0, targetTime - now);
const { currentRound, targetTime, published, secondsRemaining } =
computeCountdown(targetRound, now, genesis, period);

if (!cancelled) {
setState({
Expand Down