Merge branch 'main' into codex/fix-antimeridian-polygon-containment

This commit is contained in:
Sameh Khamis committed 2026-10-16 15:26:29 -07:00
commit 189978fc97
22 files changed
+821 -16

No files matched your search

+12 -4
View File
@@ -72,19 +72,27 @@ export function sceneSeekState(scene, progress, durationForShot, holdForShot) {
let shotIndex = scene.shots.length - 1;
for (let index = 0; index < scene.shots.length; index += 1) {
const endElapsedSec = startElapsedSec + durations[index];
if (targetSec < endElapsedSec || index === scene.shots.length - 1) {
// Compare in progress units: a shot's startProgress (start / total)
// times total can round below its start and select the previous shot.
if (
normalized < endElapsedSec / totalSec ||
index === scene.shots.length - 1
) {
shotIndex = index;
break;
}
startElapsedSec = endElapsedSec;
}
// Keep the elapsed time inside the selected shot, so it agrees with
// shotIndex when targetSec rounded below the shot's start.
const elapsedSec = Math.max(targetSec, startElapsedSec);
const shot = scene.shots[shotIndex];
const flightDurationSec = shot.durationSec || DEFAULT_SHOT_DURATION_SEC;
const holdDurationSec = holdForShot(scene, shot);
const shotDurationSec = Math.max(0.001, flightDurationSec + holdDurationSec);
const shotElapsedSec = Math.max(
0,
Math.min(shotDurationSec, targetSec - startElapsedSec),
Math.min(shotDurationSec, elapsedSec - startElapsedSec),
);
const cameraProgress = clamp01(
shotElapsedSec / Math.max(0.001, flightDurationSec),
@@ -99,8 +107,8 @@ export function sceneSeekState(scene, progress, durationForShot, holdForShot) {
);
const move = resolveCameraMove(scene, shot);
return {
sceneProgress: totalSec > 0 ? targetSec / totalSec : 0,
sceneElapsedSec: targetSec,
sceneProgress: totalSec > 0 ? elapsedSec / totalSec : 0,
sceneElapsedSec: elapsedSec,
sceneDurationSec: totalSec,
shotIndex,
shot,
+36
View File
@@ -49,3 +49,39 @@ test('camera seeking preserves cubic easing and shortest-angle orientation', ()
);
assert.deepEqual(cameraAtProgress(null, camera(20, 350), 0), camera(20, 350));
});
test('seeking to a shot start lands in that shot for every shot count and duration', () => {
const misses = [];
for (let count = 2; count <= 12; count += 1) {
for (let flightTenths = 1; flightTenths <= 100; flightTenths += 1) {
for (let holdTenths = 0; holdTenths <= 50; holdTenths += 1) {
const shots = Array.from({ length: count }, (_, index) => ({
id: `s${index}`,
// Every third shot is longer, so boundaries are not all equal steps.
durationSec: flightTenths / 10 + (index % 3 === 2 ? 1.7 : 0),
holdSec: holdTenths / 10,
camera: camera(20, 0),
}));
const mixed = { shots };
for (const [index, shot] of shots.entries()) {
const { startProgress, startElapsedSec } = sceneTimingForShot(
mixed,
shot,
duration,
);
const seek = sceneSeekState(mixed, startProgress, duration, hold);
// One or two ulps earlier still belongs to the previous shot.
const before = startProgress * (1 - Number.EPSILON);
const previous = sceneSeekState(mixed, before, duration, hold);
if (
seek.shotIndex !== index ||
seek.sceneElapsedSec < startElapsedSec ||
previous.shotIndex !== Math.max(0, index - 1)
)
misses.push(`${count}x${flightTenths}/${holdTenths}#${index}`);
}
}
}
}
assert.deepEqual(misses.slice(0, 5), []);
});
+9 -4
View File
@@ -39,8 +39,6 @@ export class MilitaryFlightRecords {
? prevMeta.altitudeFt * 0.3048
: 0
: 3048);
const track = aircraft.courseDeg || 0;
const speedMps = aircraft.speedMps ?? 0;
const verticalRateMps = aircraft.verticalRateMps;
const callsign = aircraft.callsign;
const type = aircraft.typeCode;
@@ -148,8 +146,15 @@ export class MilitaryFlightRecords {
// altitudeFt/baro).
geoAltitudeM,
renderAltitudeM,
speedMps: stickyNumber(speedMps, prevMeta?.speedMps, null),
track: stickyNumber(track, prevMeta?.track, null),
// A poll that omits track/speed holds the last values, like the civil
// records; 0 only until one is known. A grounded row without a speed
// reads 0, so a parked plane never coasts at its landing speed.
speedMps: stickyNumber(
aircraft.speedMps,
onGround ? 0 : prevMeta?.speedMps,
0,
),
track: stickyNumber(aircraft.courseDeg, prevMeta?.track, 0),
// Analyst seam (additive): sticky like the other kinematics.
verticalRateMps: stickyNumber(
verticalRateMps,
+36
View File
@@ -114,3 +114,39 @@ test('military emergency status follows the latest poll instead of sticking', ()
);
assert.equal(next.meta.emergency, null);
});
test('military records hold the last track and speed when a poll omits them', () => {
const records = store();
records.receive(observation({ courseDeg: 270, speedMps: 200 }), context());
const missing = records.receive(
observation({ courseDeg: null, speedMps: null }),
context(),
);
assert.equal(missing.meta.track, 270);
assert.equal(missing.meta.speedMps, 200);
const north = records.receive(
observation({ courseDeg: 0, speedMps: 0 }),
context(),
);
assert.equal(north.meta.track, 0);
assert.equal(north.meta.speedMps, 0);
const unseen = store().receive(
observation({ courseDeg: undefined, speedMps: undefined }),
context(),
);
assert.equal(unseen.meta.track, 0);
assert.equal(unseen.meta.speedMps, 0);
const landing = store();
landing.receive(observation({ courseDeg: 90, speedMps: 70 }), context());
const parked = landing.receive(
observation({
onGround: true,
baroAltitudeM: null,
courseDeg: null,
speedMps: null,
}),
context(),
);
assert.equal(parked.meta.speedMps, 0);
assert.equal(parked.meta.track, 90);
});
+26
View File
@@ -126,6 +126,32 @@ test('production reader rejects oversized, malformed and runtime-failure bodies'
}
});
test('element data that mentions a failure phrase is data, and sets no cooldown', async () => {
const names = ['Timed Out Escape Rooms', 'Runtime Error Bar', 'Out of Memory Lane', 'Too Many Requests Cafe', 'rate_limited'];
for (const [i, name] of names.entries()) {
const body = JSON.stringify({ elements: [{ type: 'node', id: 1, lat: 1, lon: 2, tags: { name } }] });
let count = 0;
const options = { endpoints: [`https://phrase-${i}.example/`], fetchImpl: async () => {
count++; return new Response(body);
} };
const payload = await fetchOverpassPayload('first', 1024, options);
assert.equal(payload.status, 200, name);
assert.ok(overpassPayloadIsData(payload), name);
assert.ok(payload.body.includes(name), name);
await fetchOverpassPayload('different', 1024, options);
assert.equal(count, 2, name);
}
const remark = JSON.stringify({ elements: [], remark: 'runtime error: rate_limited' });
const limited = await fetchOverpassPayload('x', 1024, { endpoints: ['https://remark.example/'],
fetchImpl: async () => new Response(remark, { headers: { 'Retry-After': '90' } }) });
assert.equal(limited.status, 429);
assert.equal(limited.retryAfterMs, 90_000);
const error = await fetchOverpassPayload('x', 1024, { endpoints: ['https://error.example/'],
fetchImpl: async () => new Response('{"error":"rate_limited"}', { headers: { 'Retry-After': '120' } }) });
assert.equal(error.status, 429);
assert.equal(error.retryAfterMs, 120_000);
});
function proxyHandler() {
const plugin = createViteConfig({ mode: 'test' }).plugins.find(p => p.name === 'overpass-proxy');
const routes = new Map();
+319
View File
@@ -0,0 +1,319 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs/promises';
import {
mkdtemp,
mkdir,
writeFile,
readFile,
readdir,
rm,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
FRAGMENT_DIR,
STAGING_DIR,
assembleChangelog,
assembleFragments,
orderFragmentNames,
recoverInterruptedBatch,
} from '../../scripts/assemble-changelog.mjs';
const EXISTING =
'# Changelog\n\n- An entry that was already released (someone, #1).\n';
const repositoryRoot = fileURLToPath(new URL('../..', import.meta.url));
async function fixture(t, fragments = {}) {
const root = await mkdtemp(path.join(tmpdir(), 'gev-changelog-'));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(path.join(root, FRAGMENT_DIR));
await writeFile(
path.join(root, FRAGMENT_DIR, 'README.md'),
'# How to add one\n',
);
await writeFile(path.join(root, 'CHANGELOG.md'), EXISTING);
for (const [name, body] of Object.entries(fragments)) {
await writeFile(path.join(root, FRAGMENT_DIR, name), body);
}
return root;
}
test('the batch reads newest pull request first', () => {
assert.deepEqual(orderFragmentNames(['7-b.md', '112-c.md', '19-a.md']), [
'112-c.md',
'19-a.md',
'7-b.md',
]);
});
test('the directory README is documentation, never an entry', () => {
assert.deepEqual(orderFragmentNames(['README.md', '5-a.md']), ['5-a.md']);
});
test('two fragments claiming one pull request fail loudly, not silently', () => {
assert.throws(
() => orderFragmentNames(['5-first.md', '5-second.md']),
/Two fragments claim pull request 5/,
);
});
test('a name that is not <pull request>-<slug>.md is refused', () => {
for (const name of ['notes.md', '5_underscore.md', '5-Caps.md', '5-a.txt']) {
assert.throws(() => orderFragmentNames([name]), /Unexpected file/, name);
}
});
test('fragment text is spliced in unchanged, above the existing entries', () => {
// Wrapped, indented and punctuated the way a real entry is: the continuation
// indent and the blank line inside the entry are the parts a reflow would eat.
const entry = [
'- A new thing, with `code` and a — dash. The second line is indented the',
' way the surrounding entries wrap, and it keeps its two spaces.',
'',
' A second paragraph, because some entries have one (someone, #9).',
].join('\n');
const result = assembleChangelog(EXISTING, [`${entry}\n\n\n`]);
assert.equal(
result,
`# Changelog\n\n${entry}\n\n- An entry that was already released (someone, #1).\n`,
);
// The entry survives byte for byte: drawTool.test.mjs reads this file as
// prose and slices a fixed window, so reflowing would move text across it.
assert.ok(result.includes(entry));
});
test('a CHANGELOG without the heading is refused rather than prepended to', () => {
assert.throws(
() => assembleChangelog('- orphan entry\n', ['- new\n']),
/must start with "# Changelog"/,
);
});
test('assembling consumes the fragments and leaves the README', async (t) => {
const root = await fixture(t, {
'20-second.md': '- Second (someone, #20).',
'100-first.md': '- First (someone, #100).',
});
const { consumed } = await assembleFragments(root, '--write');
assert.deepEqual(consumed, ['100-first.md', '20-second.md']);
const changelog = await readFile(path.join(root, 'CHANGELOG.md'), 'utf8');
assert.match(
changelog,
/# Changelog\n\n- First \(someone, #100\)\.\n\n- Second \(someone, #20\)\.\n\n- An entry that was already released/,
);
assert.deepEqual(await readdir(path.join(root, FRAGMENT_DIR)), ['README.md']);
});
test('a rejected batch leaves the directory and the file untouched', async (t) => {
const root = await fixture(t, {
'8-first.md': '- First (someone, #8).',
'8-again.md': '- Again (someone, #8).',
});
await assert.rejects(
assembleFragments(root, '--write'),
/Two fragments claim pull request 8/,
);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
assert.deepEqual((await readdir(path.join(root, FRAGMENT_DIR))).sort(), [
'8-again.md',
'8-first.md',
'README.md',
]);
});
test('check mode changes nothing on disk', async (t) => {
const root = await fixture(t, { '3-a.md': '- Something (someone, #3).' });
const { consumed } = await assembleFragments(root, '--check');
assert.deepEqual(consumed, ['3-a.md']);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
assert.deepEqual((await readdir(path.join(root, FRAGMENT_DIR))).sort(), [
'3-a.md',
'README.md',
]);
});
test('an empty directory is a no-op, so a release can always run it', async (t) => {
const root = await fixture(t);
const { consumed } = await assembleFragments(root, '--write');
assert.deepEqual(consumed, []);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
});
test('a mode that is neither check nor write is refused', async (t) => {
const root = await fixture(t);
await assert.rejects(assembleFragments(root, '--force'), /--check\|--write/);
});
// Failure injection. Every filesystem call the write path makes is a point
// where a release can die: a full disk, a permissions error, a killed process.
const IO_CALLS = ['readdir', 'readFile', 'mkdir', 'writeFile', 'rename', 'rm'];
const BATCH = {
'20-second.md': '- Second (someone, #20).',
'100-first.md': '- First (someone, #100).',
'7-third.md': '- Third (someone, #7).',
};
/**
* The real filesystem, except that `call` throws the `nth` time it runs. With
* `crash`, every later call throws as well, the way a killed process makes no
* further calls: the in-process rollback cannot run either, so the next
* invocation is the only thing that can recover.
*/
function failing(call, nth, { crash = false } = {}) {
let seen = 0;
let dead = false;
const io = { ...fs };
for (const name of IO_CALLS) {
io[name] = async (...args) => {
if (dead) throw new Error('process already gone');
if (name === call && ++seen === nth) {
dead = crash;
throw new Error(`injected ${call} failure #${nth}`);
}
return fs[name](...args);
};
}
return io;
}
/** How many times one uninterrupted `--write` makes each filesystem call. */
async function countCalls(t) {
const root = await fixture(t, BATCH);
const counts = Object.fromEntries(IO_CALLS.map((name) => [name, 0]));
const io = { ...fs };
for (const name of IO_CALLS) {
io[name] = (...args) => {
counts[name] += 1;
return fs[name](...args);
};
}
await assembleFragments(root, '--write', io);
return {
counts,
expected: await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
};
}
test('a write that fails at any call never lets a retry duplicate or lose an entry', async (t) => {
const { counts, expected } = await countCalls(t);
// The write path itself: one mkdir, one writeFile, a rename per fragment plus
// the commit rename, and the cleanup rm. Fewer would mean a step went missing.
assert.ok(counts.rename >= Object.keys(BATCH).length + 1);
for (const crash of [false, true]) {
for (const call of IO_CALLS) {
for (let nth = 1; nth <= counts[call]; nth += 1) {
const label = `${crash ? 'crash' : 'error'} at ${call} #${nth}`;
const root = await fixture(t, BATCH);
await assert.rejects(
assembleFragments(root, '--write', failing(call, nth, { crash })),
/injected|already gone/,
label,
);
await assembleFragments(root, '--write');
const changelog = await readFile(
path.join(root, 'CHANGELOG.md'),
'utf8',
);
assert.equal(changelog, expected, label);
for (const pr of [100, 20, 7, 1]) {
assert.equal(
changelog.split(`(someone, #${pr}).`).length - 1,
1,
`${label}: #${pr} appears exactly once`,
);
}
assert.deepEqual(
await readdir(path.join(root, FRAGMENT_DIR)),
['README.md'],
label,
);
}
}
}
});
test('an error before the commit rolls the batch back in the same run', async (t) => {
const root = await fixture(t, BATCH);
// The second rename has already moved one fragment into the staging directory.
await assert.rejects(
assembleFragments(root, '--write', failing('rename', 2)),
/injected rename failure #2/,
);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
assert.deepEqual(
(await readdir(path.join(root, FRAGMENT_DIR))).sort(),
[...Object.keys(BATCH), 'README.md'].sort(),
);
});
test('check mode reports a batch an interrupted release left staged', async (t) => {
const root = await fixture(t, BATCH);
await assert.rejects(
assembleFragments(root, '--write', failing('rename', 2, { crash: true })),
);
await assert.rejects(
assembleFragments(root, '--check'),
new RegExp(`${STAGING_DIR}/ holds a batch an interrupted release`),
);
assert.equal(await recoverInterruptedBatch(root), 'rolled-back');
assert.equal(await recoverInterruptedBatch(root), 'none');
});
test('recovery refuses to roll a fragment back over a file of the same name', async (t) => {
const root = await fixture(t, BATCH);
await assert.rejects(
assembleFragments(root, '--write', failing('rename', 2, { crash: true })),
);
await writeFile(
path.join(root, FRAGMENT_DIR, '100-first.md'),
'- Rewritten.',
);
await assert.rejects(
assembleFragments(root, '--write'),
/Cannot roll back .*100-first\.md/,
);
assert.equal(
await readFile(path.join(root, 'CHANGELOG.md'), 'utf8'),
EXISTING,
);
});
// The two below run against the repository itself, so a fragment that the
// release could not assemble fails `npm test` in the pull request that adds it
// rather than at release time, when its author has moved on.
test('every pending fragment in the repository can be assembled', async () => {
const names = await readdir(path.join(repositoryRoot, FRAGMENT_DIR));
assert.ok(
names.includes('README.md'),
`${FRAGMENT_DIR}/README.md documents the convention and keeps the directory tracked`,
);
// Throws on a name that is not <pull request>-<slug>.md, and on two fragments
// claiming one pull request number. Every other file is an entry.
assert.equal(orderFragmentNames(names).length, names.length - 1);
});
test('CHANGELOG.md still carries the heading a release splices under', async () => {
const changelog = await readFile(
path.join(repositoryRoot, 'CHANGELOG.md'),
'utf8',
);
assert.ok(changelog.startsWith('# Changelog\n'));
});
+2
View File
@@ -111,6 +111,8 @@ export const aircraftInArea = defineTool({
(record) =>
areaContains(area, { lat: record.latitude, lon: record.longitude }) &&
!(args.airborne_only && record.onGround) &&
((args.min_altitude_m == null && args.max_altitude_m == null) ||
Number.isFinite(altitude(record))) &&
(args.min_altitude_m == null ||
altitude(record) >= args.min_altitude_m) &&
(args.max_altitude_m == null ||
+59
View File
@@ -388,3 +388,62 @@ test('the real OpenSky source marks an hour-old snapshot stale for searches', as
assert.deepEqual(result.data.stale_feeds, ['civil']);
assert.match(result.summary, /may be stale\.$/);
});
test('altitude bands require a reported altitude from normalized OpenSky records', async () => {
const { normalizeOpenSkyAircraft } =
await import('../../sources/live/aircraft.js');
const state = (id, baro, geometric) =>
normalizeOpenSkyAircraft([
id,
'TEST',
'Test',
1767225600,
1767225600,
0,
0,
baro,
false,
10,
90,
0,
[],
geometric,
]);
const records = [
state('abc001', null, null),
state('abc002', 0, null),
state('abc003', null, 50),
state('abc004', 500, null),
];
const catalog = composeCatalog({
tools: coreTools,
services: {
aircraft: { getSnapshot: async () => ({ records }) },
},
});
const area = { bbox: [-1, -1, 1, 1] };
assert.equal(
(await catalog.call('aircraft_in_area', { area })).data.total,
4,
'unfiltered answers retain unknown altitude',
);
for (const band of [
{ max_altitude_m: 100 },
{ min_altitude_m: -500, max_altitude_m: 100 },
]) {
const result = await catalog.call('aircraft_in_area', { area, ...band });
assert.deepEqual(
result.data.rows.map((row) => row.id),
['abc002', 'abc003'],
);
assert.deepEqual(
result.data.rows.map((row) => row.altitude_m),
[0, 50],
);
}
const minimum = await catalog.call('aircraft_in_area', {
area,
min_altitude_m: -500,
});
assert.equal(minimum.data.total, 3);
});