Merge remote-tracking branch 'origin/main' into fix/tle-catalog-matching

This commit is contained in:
VG31OP committed 2026-10-15 20:35:01 +05:30
commit 9791ed5ff3
14 files changed
+237 -28

No files matched your search

+3 -1
View File
@@ -313,7 +313,9 @@ test('retry: a plain HTTP 500 still exhausts the existing 8s/25s transient ladde
t.mock.timers.enable({ apis: ['setTimeout', 'Date'], now: 100_000 });
globalThis.window = { setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout };
const requestTimes = [];
globalThis.fetch = async () => {
globalThis.fetch = async (url) => {
if (String(url).endsWith('/status'))
return Response.json({ configured: true });
requestTimes.push(Date.now());
return httpFailure(500);
};
+23
View File
@@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { acquisitionMsUtc } from './firmsCsv.js';
test('FIRMS acquisition dates cannot roll into a different calendar day', () => {
for (const [date, time] of [
['2026-02-30', '1200'],
['2026-04-31', '1200'],
['2026-13-01', '1200'],
['2026-07-16junk', '1200'],
['2026-07-16', '2400'],
['2026-07-16', '1260'],
['2026-07-16', null],
['2026-07-16', ''],
]) {
assert.ok(Number.isNaN(acquisitionMsUtc(date, time)), `${date}:${time}`);
}
for (const [date, time, expected] of [
['2024-02-29', '45', Date.UTC(2024, 1, 29, 0, 45)],
['2026-07-16', '0', Date.UTC(2026, 6, 16)],
]) {
assert.equal(acquisitionMsUtc(date, time), expected);
}
});
+7 -1
View File
@@ -163,7 +163,13 @@ export function acquisitionMsUtc(acqDate, acqTime) {
minutes > 59
)
return NaN;
return Date.UTC(year, month - 1, day, hours, minutes);
const ms = Date.UTC(year, month - 1, day, hours, minutes);
const parsed = new Date(ms);
return parsed.getUTCFullYear() === year &&
parsed.getUTCMonth() === month - 1 &&
parsed.getUTCDate() === day
? ms
: NaN;
}
/**
@@ -0,0 +1,43 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { normalizeRegionalArticles } from './regionalModel.js';
test('compact source dates must denote an actual calendar instant', () => {
for (const date of [
'20260230T120000Z',
'20260431T120000Z',
'20261301T120000Z',
'20260716T240000Z',
'20260716T126000Z',
'20260716T120060Z',
]) {
const [article] = normalizeRegionalArticles({
articles: [
{
title: 'Fixture',
url: 'https://fixture.example/news',
seendate: date,
},
],
});
assert.equal(article.publishedAt, null, date);
assert.equal(article.title, 'Fixture');
}
for (const [input, expected] of [
['20240229T004500Z', '2024-02-29T00:45:00Z'],
['20260716T000000Z', '2026-07-16T00:00:00Z'],
['2026-07-16T12:00:00Z', '2026-07-16T12:00:00.000Z'],
]) {
assert.equal(
normalizeRegionalArticles({
articles: [
{
title: 'Fixture',
url: 'https://fixture.example/news',
seendate: input,
},
],
})[0].publishedAt,
expected,
);
}
});
+13 -4
View File
@@ -67,11 +67,20 @@ export function normalizeRegionalArticles(payload, limit = MAX_ARTICLES) {
seen.add(signature);
const rawDate = cleanText(row?.seendate, 32);
const compactDate = /^(\d{8})T(\d{6})Z$/.exec(rawDate);
const publishedAt = compactDate
const candidateDate = compactDate
? `${compactDate[1].slice(0, 4)}-${compactDate[1].slice(4, 6)}-${compactDate[1].slice(6, 8)}T${compactDate[2].slice(0, 2)}:${compactDate[2].slice(2, 4)}:${compactDate[2].slice(4, 6)}Z`
: Number.isNaN(Date.parse(rawDate))
? null
: new Date(rawDate).toISOString();
: rawDate;
const parsedMs = Date.parse(candidateDate);
const validDate =
Number.isFinite(parsedMs) &&
(!compactDate ||
new Date(parsedMs).toISOString().slice(0, 19) ===
candidateDate.slice(0, 19));
const publishedAt = validDate
? compactDate
? candidateDate
: new Date(parsedMs).toISOString()
: null;
articles.push({
title,
url,
+7 -1
View File
@@ -35,7 +35,13 @@ export function parseTerrainPoints(raw) {
if (parts.length !== 2) return null;
const lon = Number(parts[0]);
const lat = Number(parts[1]);
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;
if (
!Number.isFinite(lon) ||
!Number.isFinite(lat) ||
Math.abs(lon) > 180 ||
Math.abs(lat) > 90
)
return null;
points.push([lon, lat]);
}
return points;
+11
View File
@@ -3,6 +3,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
parseTerrainPoints,
fetchTerrainChunkWithRetry,
resolveTerrainHeightRequest,
terrainPointKey,
@@ -227,3 +228,13 @@ test('malformed height objects remain refresh failures, not absent readings', as
assert.equal(response.cacheChanged, false);
}
});
test('terrain parser rejects out-of-range WGS84 points and accepts inclusive bounds', () => {
for (const value of ['181,0', '-181,0', '0,91', '0,-91', '1,2;0,91'])
assert.equal(parseTerrainPoints(value), null, value);
assert.deepEqual(parseTerrainPoints('-180,-90;180,90;0,0'), [
[-180, -90],
[180, 90],
[0, 0],
]);
});
+16
View File
@@ -2,6 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { parseTerrainPoints } from './data/terrainHeightsProxy.js';
import { readResponseTextCapped, coalesceProxyRequest } from './sources/httpBody.js';
const source = ['local.js', 'common/http.js', 'aircraft/enrichment.js', 'terrain.js', 'space/celestrak.js', 'space/launch-library.js', '../../src/data/spaceProviderRequests.js']
@@ -172,3 +173,18 @@ test('ADSBDB retains validation and missing-aircraft semantics', async () => {
assert.equal(absent.status, 200);
assert.deepEqual(JSON.parse(absent.body), { found: false });
});
for (const preview of [false, true]) {
test(`terrain server rejects out-of-range points before upstream resolution in ${preview ? 'preview' : 'development'}`, async () => {
let calls = 0;
const app = fixture('terrainHeightsProxy', { parseTerrainPoints,
resolveTerrainHeightRequest: async () => { calls++; return { status: 200, body: { results: [] } }; },
}, preview);
for (const points of ['181,0', '-181,0', '0,91', '0,-91', '1,2;0,91']) {
assert.equal((await app.request('/?points=' + encodeURIComponent(points))).status, 400);
}
assert.equal(calls, 0);
assert.equal((await app.request('/?points=-180,-90;180,90')).status, 200);
assert.equal(calls, 1);
});
}
@@ -0,0 +1,38 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createApplicationRequestServices } from './requests.js';
test('unknown capability replies recover after backoff', async (t) => {
t.mock.timers.enable({ apis: ['Date'], now: 100_000 });
for (const first of [
() => Response.json({ wrong: true }),
() => new Response('bad', { status: 503 }),
]) {
let probes = 0;
let queries = 0;
const services = createApplicationRequestServices({
boundaryProbe: { timeoutMs: 1000, retryMs: 1000 },
fetchImpl: async (url) => {
if (url.endsWith('/status'))
return ++probes === 1
? first()
: Response.json({ configured: false });
queries++;
return Response.json({ elements: [] });
},
});
assert.deepEqual(await services.boundaries.query('fixture'), []);
assert.deepEqual(await services.boundaries.query('fixture'), []);
assert.equal(probes, 1, 'unknown reply is not re-probed inside backoff');
t.mock.timers.tick(1000);
assert.equal(
(await services.boundaries.query('fixture')).code,
'OVERPASS_NOT_CONFIGURED',
);
assert.equal(
(await services.boundaries.query('fixture')).code,
'OVERPASS_NOT_CONFIGURED',
);
assert.equal(probes, 2);
assert.equal(queries, 2);
}
});
+9 -6
View File
@@ -66,9 +66,9 @@ export function createApplicationRequestServices({
throw new Error(`${label} unavailable (${response.status})`);
return response.data;
}
// Learn once per page whether the server has an Overpass instance, so an
// Remember valid capability decisions for the page, so an
// unconfigured server is never asked (and never answers with an error).
// Discovery is bounded; a failed or timed-out probe is forgotten and not
// Discovery is bounded; failed, malformed or timed-out probes are forgotten and not
// retried for a short backoff, during which queries go to the server as
// they would against an older server without the probe.
let boundaryCapability = null;
@@ -79,10 +79,13 @@ export function createApplicationRequestServices({
boundaryCapability = request(`${urls.boundaries}/status`, {
signal: AbortSignal.timeout(boundaryProbe.timeoutMs),
}).then(
(response) =>
response.ok && typeof response.data?.configured === 'boolean'
? response.data.configured
: null, // an older server: fall back to asking per query
(response) => {
if (response.ok && typeof response.data?.configured === 'boolean')
return response.data.configured;
boundaryCapability = null;
boundaryProbeRetryAt = Date.now() + boundaryProbe.retryMs;
return null; // capability unknown: let this query ask the server
},
(error) => {
boundaryCapability = null;
if (lifetime?.aborted) throw error;
+51
View File
@@ -0,0 +1,51 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { composeCatalog, defineTool } from './catalog.js';
for (const intercept of [false, true]) {
test(`cancelled query refuses execution${intercept ? ' after an awaited interceptor' : ''}`, async () => {
let calls = 0;
const controller = new AbortController();
const query = defineTool({
name: 'fixture_query',
kind: 'query',
title: 'Fixture',
description: 'Offline fixture',
inputSchema: { type: 'object' },
run: async () => {
calls++;
return { summary: 'fixture', data: {} };
},
});
const catalog = composeCatalog({
tools: [query],
interceptors: intercept
? [
async (call, next) => {
await Promise.resolve();
controller.abort();
return next(call);
},
]
: [],
});
if (!intercept) controller.abort();
await assert.rejects(
catalog.call('fixture_query', {}, { signal: controller.signal }),
{ name: 'AbortError' },
);
assert.equal(calls, 0);
const fresh = composeCatalog({ tools: [query] });
assert.equal(
(
await fresh.call(
'fixture_query',
{},
{ signal: new AbortController().signal },
)
).summary,
'fixture',
);
assert.equal(calls, 1);
});
}
+1
View File
@@ -178,6 +178,7 @@ export function composeCatalog({
return chain({ tool, args, signal, ...(parent ? { parent } : {}) });
};
const execute = async ({ tool, args, signal }) => {
signal?.throwIfAborted();
const problems = validateValue(tool.inputSchema, args);
if (problems.length)
throw new ToolError('invalid_arguments', problems.join('; '));