mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 22:02:21 +00:00
254 lines
9.6 KiB
JavaScript
254 lines
9.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { windProxy } from '../../server/providers/wind.js';
|
|
|
|
/** Boot a mounted provider middleware without a real server. */
|
|
function install(plugin) {
|
|
const routes = new Map();
|
|
plugin.configureServer({
|
|
middlewares: {
|
|
use(route, handler) {
|
|
routes.set(route, handler);
|
|
},
|
|
},
|
|
});
|
|
assert.equal(routes.size, 1);
|
|
const handler = [...routes.values()][0];
|
|
return async (url = '/', method = 'GET') => {
|
|
const res = {
|
|
statusCode: 200,
|
|
headers: {},
|
|
headersSent: false,
|
|
writeHead(status, headers) {
|
|
this.statusCode = status;
|
|
for (const [name, value] of Object.entries(headers || {}))
|
|
this.headers[name.toLowerCase()] = value;
|
|
this.headersSent = true;
|
|
},
|
|
end(body) {
|
|
this.body = body;
|
|
this.headersSent = true;
|
|
},
|
|
};
|
|
await handler({ url, method }, res);
|
|
return res;
|
|
};
|
|
}
|
|
|
|
const IDX = [
|
|
'1:0:d=2026091400:UGRD:10 m above ground:anl:',
|
|
'2:5:d=2026091400:VGRD:10 m above ground:anl:',
|
|
'3:10:d=2026091400:HGT:surface:anl:',
|
|
].join('\n');
|
|
|
|
const DECODED_U = {
|
|
ni: 4,
|
|
nj: 3,
|
|
lo1: 0,
|
|
la1: 90,
|
|
di: 90,
|
|
dj: 90,
|
|
values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
|
};
|
|
const DECODED_V = { ...DECODED_U, values: [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] };
|
|
|
|
/** Resampled-grid metadata shared by the injected model fakes. */
|
|
const GRID = { nx: 4, ny: 3, lo1: 0, la1: 90, dx: 90, dy: 90 };
|
|
|
|
/** Fetch double: `.idx` text, and range bodies of the requested length. */
|
|
function makeFetch({ fail = false, counter } = {}) {
|
|
return async (url, options = {}) => {
|
|
if (counter) counter.calls += 1;
|
|
if (fail) throw new Error('gfs upstream down');
|
|
if (String(url).endsWith('.idx')) return new Response(IDX, { status: 200 });
|
|
const match = /bytes=(\d+)-(\d+)/.exec(options.headers?.Range || '');
|
|
const length = Number(match[2]) - Number(match[1]) + 1;
|
|
return new Response(new Uint8Array(length), { status: 206 });
|
|
};
|
|
}
|
|
|
|
function proxy(options = {}) {
|
|
return windProxy({
|
|
now: () => Date.UTC(2026, 8, 14, 12),
|
|
targetDx: 90,
|
|
fetchImpl: makeFetch(),
|
|
decodeImpl: async (buffer) =>
|
|
buffer[0] === undefined ? DECODED_U : DECODED_U,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
test('wind manifest describes the GFS cycle and resampled grid', async () => {
|
|
const request = install(proxy());
|
|
const res = await request('/');
|
|
assert.equal(res.statusCode, 200);
|
|
const body = JSON.parse(res.body);
|
|
assert.equal(body.schemaVersion, 1);
|
|
assert.equal(body.model, 'gfs');
|
|
assert.equal(body.cycle.hour, 6);
|
|
assert.equal(body.cycle.date, '20260914');
|
|
assert.equal(body.units, 'm/s');
|
|
assert.deepEqual(body.grid, { nx: 4, ny: 3, lo1: 0, la1: 90, dx: 90, dy: 90 });
|
|
assert.match(body.gridUrl, /^\/api\/wind\/grid\/gfs-20260914-6-f6-90\.bin\?model=gfs$/);
|
|
assert.equal(body.stale, false);
|
|
});
|
|
|
|
test('wind grid route returns Float32 U then V', async () => {
|
|
let u = 0;
|
|
const request = install(
|
|
proxy({
|
|
decodeImpl: async () => {
|
|
u += 1;
|
|
return u === 1 ? DECODED_U : DECODED_V;
|
|
},
|
|
}),
|
|
);
|
|
const manifest = JSON.parse((await request('/')).body);
|
|
const res = await request(manifest.gridUrl.replace('/api/wind', ''));
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.headers['content-type'], 'application/octet-stream');
|
|
assert.equal(res.body.length, 4 * 3 * 4 * 2);
|
|
const floats = new Float32Array(
|
|
res.body.buffer,
|
|
res.body.byteOffset,
|
|
res.body.length / 4,
|
|
);
|
|
assert.deepEqual([...floats.slice(0, 12)], DECODED_U.values);
|
|
assert.deepEqual([...floats.slice(12)], DECODED_V.values);
|
|
});
|
|
|
|
test('wind status omits the grid url', async () => {
|
|
const request = install(proxy());
|
|
await request('/');
|
|
const body = JSON.parse((await request('/status')).body);
|
|
assert.equal('gridUrl' in body, false);
|
|
assert.equal(body.model, 'gfs');
|
|
});
|
|
|
|
test('wind refresh is cached within the TTL and refetches after it', async () => {
|
|
const counter = { calls: 0 };
|
|
let clock = Date.UTC(2026, 8, 14, 12);
|
|
const request = install(
|
|
proxy({ now: () => clock, fetchImpl: makeFetch({ counter }) }),
|
|
);
|
|
await request('/');
|
|
const afterFirst = counter.calls;
|
|
assert.equal(afterFirst, 3, 'one idx plus two range fetches');
|
|
await request('/');
|
|
assert.equal(counter.calls, afterFirst, 'a second request inside the TTL must not refetch');
|
|
clock += 2 * 3600_000;
|
|
await request('/');
|
|
assert.equal(counter.calls, afterFirst + 3, 'a request past the TTL refetches');
|
|
});
|
|
|
|
test('wind serves last-good with stale on upstream failure', async () => {
|
|
let failing = false;
|
|
let clock = Date.UTC(2026, 8, 14, 12);
|
|
const request = install(
|
|
proxy({
|
|
now: () => clock,
|
|
fetchImpl: async (...args) => {
|
|
if (failing) throw new Error('gfs upstream down');
|
|
return makeFetch()(...args);
|
|
},
|
|
}),
|
|
);
|
|
const good = JSON.parse((await request('/')).body);
|
|
failing = true;
|
|
clock += 2 * 3600_000;
|
|
const degraded = JSON.parse((await request('/')).body);
|
|
assert.equal(degraded.stale, true);
|
|
assert.equal(degraded.reason, 'Wind upstream unavailable');
|
|
const frame = await request(good.gridUrl.replace('/api/wind', ''));
|
|
assert.equal(frame.statusCode, 200, 'last-good grid stays addressable');
|
|
});
|
|
|
|
test('wind returns a JSON 404 for an unknown grid', async () => {
|
|
const request = install(proxy());
|
|
await request('/');
|
|
const res = await request('/grid/nope.bin');
|
|
assert.equal(res.statusCode, 404);
|
|
assert.deepEqual(JSON.parse(res.body), { error: 'unknown_grid' });
|
|
});
|
|
|
|
test('wind grid ids and URLs are model-scoped', async () => {
|
|
const request = install(
|
|
proxy({
|
|
models: {
|
|
gfs: async () => ({ cycle: { date: '20260914', hour: 6 }, level: 'x', units: 'm/s', grid: { ...GRID, u: new Float32Array(12), v: new Float32Array(12) } }),
|
|
ifs: async () => ({ cycle: { date: '20260914', hour: 0 }, level: 'x', units: 'm/s', grid: { ...GRID, u: new Float32Array(12), v: new Float32Array(12) } }),
|
|
},
|
|
}),
|
|
);
|
|
const gfs = JSON.parse((await request('/')).body);
|
|
const ifs = JSON.parse((await request('/?model=ifs')).body);
|
|
assert.match(gfs.gridUrl, /gfs-20260914-6-f0-90\.bin\?model=gfs$/);
|
|
assert.match(ifs.gridUrl, /ifs-20260914-0-f0-90\.bin\?model=ifs$/);
|
|
// Each model's grid resolves through its own model parameter.
|
|
assert.equal((await request(gfs.gridUrl.replace('/api/wind', ''))).statusCode, 200);
|
|
assert.equal((await request(ifs.gridUrl.replace('/api/wind', ''))).statusCode, 200);
|
|
});
|
|
|
|
test('wind rejects a non-finite grid instead of caching it', async () => {
|
|
const bad = new Float32Array(12);
|
|
bad[3] = Number.POSITIVE_INFINITY;
|
|
const request = install(
|
|
proxy({
|
|
models: {
|
|
gfs: async () => ({ cycle: { date: '20260914', hour: 6 }, level: 'x', units: 'm/s', grid: { ...GRID, u: bad, v: new Float32Array(12) } }),
|
|
},
|
|
}),
|
|
);
|
|
const body = JSON.parse((await request('/')).body);
|
|
assert.equal(body.unavailable, true);
|
|
assert.equal(body.reason, 'Wind upstream unavailable');
|
|
});
|
|
|
|
test('wind rejects unknown models, paths and methods before acquisition', async () => {
|
|
let calls = 0;
|
|
const request = install(proxy({ models: { gfs: async () => { calls++; throw new Error(); } } }));
|
|
assert.equal((await request('/?model=toString')).statusCode, 400);
|
|
assert.equal((await request('/secret')).statusCode, 404);
|
|
assert.equal((await request('/manifest', 'POST')).statusCode, 405);
|
|
assert.equal((await request('/grid/unknown.bin')).statusCode, 404);
|
|
assert.equal(calls, 0);
|
|
});
|
|
|
|
test('wind caches forecast steps separately and keeps a prior issued grid readable', async () => {
|
|
let clock = Date.UTC(2026, 8, 14, 12); let step = 6;
|
|
const request = install(proxy({ now: () => clock, models: { gfs: async () => ({ cycle: { date: '20260914', hour: 6, forecastHour: step }, level:'10 m', units:'m/s', grid: { ...GRID, u: new Float32Array(12).fill(step), v: new Float32Array(12) } }) } }));
|
|
const first = JSON.parse((await request('/')).body);
|
|
clock += 3600_000; step = 7;
|
|
const second = JSON.parse((await request('/')).body);
|
|
assert.notEqual(first.gridUrl, second.gridUrl);
|
|
assert.equal((await request(first.gridUrl.replace('/api/wind',''))).statusCode, 200);
|
|
});
|
|
|
|
test('wind failure backoff prevents unbounded repeated upstream acquisition', async () => {
|
|
let calls = 0;
|
|
const request = install(proxy({ models: { gfs: async () => { calls++; throw new Error('private upstream detail'); } } }));
|
|
const results = await Promise.all([request('/'), request('/')]);
|
|
assert.equal(calls, 1);
|
|
assert.equal(JSON.parse(results[0].body).reason, 'Wind upstream unavailable');
|
|
await request('/'); assert.equal(calls, 1);
|
|
});
|
|
|
|
test('shared upstream work survives one disconnect and aborts when the final client leaves', async () => {
|
|
const { EventEmitter } = await import('node:events');
|
|
let handler; let signal; let loads = 0;
|
|
const plugin = proxy({ models: { gfs: ({ signal: next }) => {
|
|
loads++; signal = next;
|
|
return new Promise((resolve, reject) => next.addEventListener('abort', () => reject(next.reason), { once: true }));
|
|
} } });
|
|
plugin.configureServer({ middlewares: { use: (_path, value) => { handler = value; } } });
|
|
const first = new EventEmitter(); const second = new EventEmitter();
|
|
const a = handler({ url: '/manifest', method: 'GET' }, first);
|
|
const b = handler({ url: '/manifest', method: 'GET' }, second);
|
|
assert.equal(loads, 1);
|
|
first.emit('close'); assert.equal(signal.aborted, false);
|
|
second.emit('close'); assert.equal(signal.aborted, true);
|
|
await Promise.all([a,b]);
|
|
assert.equal(first.listenerCount('close'), 0); assert.equal(second.listenerCount('close'), 0);
|
|
});
|