Skip to content
Merged
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
55 changes: 55 additions & 0 deletions apps/site/next-data/generators/__tests__/partners.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';

// The shuffle reads a different hash byte depending on the size of the weighted
// list, so these timestamps only apply to a list of exactly 20 entries. This
// fixture keeps them meaningful when the real partner list changes.
const PARTNERS = [
{ id: 'A', weight: 3 },
{ id: 'B', weight: 2 },
{ id: 'C', weight: 3 },
{ id: 'D', weight: 3 },
{ id: 'E', weight: 2 },
{ id: 'F', weight: 2 },
{ id: 'G', weight: 3 },
{ id: 'H', weight: 2 },
];

mock.module('#site/next.json.mjs', {
namedExports: { partners: PARTNERS },
});

const { default: generatePartners } =
await import('#site/next-data/generators/partners.mjs');

// The shuffle seed is `Math.floor(Date.now() / 300000)`, so the result can only
// change on a 5-minute boundary. Each incident below is a real build failure,
// and each neighbouring time is the same code on either side of that window.
const TIMES = [
['2026-09-09T19:59:59.999Z', 'just before the 20:00 window'],
['2026-09-09T20:00:00.000Z', 'start of the 20:00 window'],
['2026-09-09T20:00:26.000Z', 'failed build: Windows, /en/download'],
['2026-09-09T20:01:26.000Z', 'failed build: Ubuntu, /en'],
['2026-09-09T20:04:59.999Z', 'end of the 20:00 window'],
['2026-09-09T20:05:00.000Z', 'just after the 20:00 window'],
['2026-11-06T17:14:51.000Z', 'passed build: Ubuntu, same commit as below'],
['2026-11-06T17:14:59.999Z', 'just before the 17:15 window'],
['2026-11-06T17:15:00.000Z', 'start of the 17:15 window'],
['2026-11-06T17:17:26.000Z', 'failed build: Windows, /en'],
['2026-11-06T17:19:59.999Z', 'end of the 17:15 window'],
['2026-11-06T17:20:00.000Z', 'just after the 17:15 window'],
];

describe('generatePartners', () => {
for (const [now, description] of TIMES) {
it(`returns every partner at ${now} (${description})`, async t => {
t.mock.timers.enable({ apis: ['Date'], now: new Date(now) });

const partners = await generatePartners();

assert.ok(!partners.includes(undefined), 'result contains undefined');
assert.equal(partners.length, PARTNERS.length);
assert.deepEqual(new Set(partners), new Set(PARTNERS));
});
}
});
45 changes: 45 additions & 0 deletions apps/site/util/__tests__/array.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { shuffle } from '#site/util/array';

const range = length => Array.from({ length }, (_, i) => i);

const sorted = array => [...array].sort((a, b) => a - b);

describe('shuffle', () => {
it('returns the same order for the same seed', async () => {
const input = range(20);

assert.deepEqual(await shuffle(input, 42), await shuffle(input, 42));
});

it('does not mutate the input', async () => {
const input = range(20);

await shuffle(input, 42);

assert.deepEqual(input, range(20));
});

// In these seeds, the first hash byte that the shuffle reads is 255
for (const seed of [5963280, 5979951]) {
it(`keeps every entry when the hash byte is 255 (seed ${seed})`, async () => {
const input = range(20);

assert.deepEqual(sorted(await shuffle(input, seed)), input);
});
}

// About 1 in 256 seeds has a 255 byte at any given position, so this range
// covers that case for each length without relying on hand-picked seeds
for (const length of [2, 3, 20, 32, 33, 100]) {
it(`returns a permutation of ${length} entries for any seed`, async () => {
const input = range(length);

for (let seed = 5979000; seed < 5981000; seed++) {
assert.deepEqual(sorted(await shuffle(input, seed)), input);
}
});
}
});
4 changes: 2 additions & 2 deletions apps/site/util/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export const shuffle = async <T>(
for (let i = shuffled.length - 1; i > 0; i--) {
// Use hash bytes to generate deterministic "random" index
const hashIndex = (i + seed) % 32;
// Normalize to 0-1
const randomValue = hash[hashIndex] / 255;
// Normalize to [0, 1), since a value of 1 would put `j` out of bounds
const randomValue = hash[hashIndex] / 256;

const j = Math.floor(randomValue * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
Expand Down
Loading