Merge branch 'main' into codex/fix-kml-root-scanner

This commit is contained in:
Said Turksever authored and GitHub committed 2026-10-23 15:26:25 +01:00
commit 6194989018
43 files changed
+1261 -207

No files matched your search

+52
View File
@@ -712,6 +712,58 @@ test('unusable directory responses preserve warm client state atomically', async
assert.equal(getRadioUIState().stale, true);
assert.equal(getRadioUIState().degraded, true);
}
globalThis.fetch = async () => new Response(JSON.stringify({
stations: [replacement, { id: 'malformed' }], stale: false, degraded: false,
acceptedGeneration: 2, catalogInstance: 'qa-instance-a', updatedAt: now,
}));
await radioLayer.update();
assert.equal(getRadioUIState().stationCount, 1);
assert.equal(getRadioUIState().rejectedStationCount, 1);
assert.equal(getRadioUIState().degraded, true);
assert.equal(getRadioUIState().stale, false);
assert.equal(getRadioAcceptedCatalogSnapshot().generation, 2);
assert.equal(getRadioAcceptedCatalogSnapshot().stations[0].id, replacement.id);
// Server-side structural rejections are combined with client rejections
// after admission; the healthy generation is still accepted.
globalThis.fetch = async () => new Response(JSON.stringify({
stations: [replacement, { id: 'malformed' }], stale: false, degraded: false,
acceptedGeneration: 3, catalogInstance: 'qa-instance-a', updatedAt: now,
rejectedStationCount: 2,
}));
await radioLayer.update();
assert.equal(getRadioUIState().rejectedStationCount, 3);
assert.equal(getRadioUIState().degraded, true);
assert.equal(getRadioAcceptedCatalogSnapshot().generation, 3);
globalThis.fetch = async () => new Response(JSON.stringify({
stations: [replacement], stale: false, degraded: false,
acceptedGeneration: 4, catalogInstance: 'qa-instance-a', updatedAt: now,
rejectedStationCount: 10_000,
}));
await radioLayer.update();
assert.equal(getRadioUIState().rejectedStationCount, 10_000);
assert.equal(getRadioAcceptedCatalogSnapshot().generation, 4);
for (const rejectedStationCount of [-1, 1.5, '2', null, 10_001]) {
globalThis.fetch = async () => new Response(JSON.stringify({
stations: [replacement], stale: false, degraded: false,
acceptedGeneration: 5, catalogInstance: 'qa-instance-a', updatedAt: now,
rejectedStationCount,
}));
await radioLayer.update();
assert.equal(getRadioAcceptedCatalogSnapshot().generation, 4);
assert.equal(getRadioUIState().stale, true);
}
globalThis.fetch = async () => new Response(JSON.stringify({
stations: [{ id: 'malformed' }], stale: false, degraded: false,
acceptedGeneration: 5, catalogInstance: 'qa-instance-a', updatedAt: now,
}));
await radioLayer.update();
assert.equal(getRadioAcceptedCatalogSnapshot().generation, 4);
assert.equal(getRadioUIState().stale, true);
assert.equal(getRadioUIState().rejectedStationCount, 10_000);
radioLayer.destroy();
radioLayer.init(viewer);
assert.equal(getRadioUIState().rejectedStationCount, 0);
assert.equal(getRadioUIState().degraded, false);
} finally {
globalThis.fetch = originalFetch;
radioLayer.destroy();
+67 -1
View File
@@ -11,7 +11,14 @@ import {
publicRadioStation,
publicRadioHttpsUrl,
} from '../../vite.config.js';
import { rankRadioStationsForRequest } from './radio.js';
import {
getRadioAcceptedCatalogSnapshot,
getRadioUIState,
radioLayer,
rankRadioStationsForRequest,
} from './radio.js';
import { RADIO_REJECTED_STATION_COUNT_MAX as SERVER_REJECTED_MAX } from '../../server/providers/radio/constants.js';
import { RADIO_REJECTED_STATION_COUNT_MAX as CLIENT_REJECTED_MAX } from '../layers/radio/policy.js';
const UUID = '12345678-1234-4234-8234-123456789abc';
const publicLookup = async () => [{ address: '93.184.216.34', family: 4 }];
@@ -533,3 +540,62 @@ test('a mirror answering an invalid reason phrase rejects instead of crashing',
const crashed = mockMirrorStatus(t, 200, 'OK\x7f');
await assert.rejects(Promise.race([fetchMirror(), crashed]), TypeError);
});
test('case expansion is capped after tag normalization in the portable and server station path', () => {
const row = normalizeRadioBrowserStation(station({tags: 'İ'.repeat(80) + ', NEWS__RADIO ,news-radio'}));
assert.equal(row.tags[0].length, 80);
assert.equal(row.tags[0], 'i\u0307'.repeat(40));
assert.deepEqual(row.tags.slice(1), ['news radio']);
});
test('server structural rejections reach the client diagnostic without discarding a healthy generation', async () => {
const originalFetch = globalThis.fetch;
const middleware = createProxy({
now: () => Date.now(),
fetchImpl: async (url) => {
if (String(url).includes('/json/servers')) return responseJson([{ name: 'de1.api.radio-browser.info' }]);
const rows = healthyRowsForQuery(url);
if (queryTag(url)) return responseJson(rows);
// 400 valid broad rows, one row missing its required name, and one
// schema-valid row that normalization filters by policy (not counted).
const { name: _name, ...nameless } = station({ stationuuid: '50000000-0000-4000-8000-000000000001' });
return responseJson([...rows, nameless, station({ stationuuid: '50000000-0000-4000-8000-000000000002', lastcheckok: 0 })]);
},
});
const response = await invoke(middleware, '/stations');
assert.equal(response.status, 200);
const body = JSON.parse(response.body);
assert.equal(body.degraded, false);
assert.equal(body.degradedReason, null);
assert.equal(body.acceptedGeneration, 1);
assert.equal(body.rejectedStationCount, 1);
assert.equal(SERVER_REJECTED_MAX, CLIENT_REJECTED_MAX);
const viewer = {
camera: { positionWC: { x: 7_000_000, y: 0, z: 0 } },
scene: { canvas: { disableRootEvents: true, onwheel: null, addEventListener() {}, removeEventListener() {} } },
dataSources: { add() {}, remove() {} },
entities: { add(entity) { return entity; }, remove() {} },
};
radioLayer.destroy();
try {
globalThis.fetch = async () => new Response(response.body, {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
radioLayer.init(viewer);
radioLayer.enable();
await radioLayer.update();
const state = getRadioUIState();
assert.equal(state.stationCount, body.stations.length);
assert.equal(state.rejectedStationCount, 1);
assert.equal(state.degraded, true);
assert.equal(state.stale, false);
assert.equal(getRadioAcceptedCatalogSnapshot().generation, 1);
assert.equal(getRadioAcceptedCatalogSnapshot().instance, body.catalogInstance);
} finally {
globalThis.fetch = originalFetch;
radioLayer.destroy();
}
});