Merge branch 'main' into i18n-spanish

This commit is contained in:
Meir Michanie authored and GitHub committed 2026-10-14 22:01:40 +02:00
commit 29f2885cda
17 files changed
+257 -16

No files matched your search

+5 -2
View File
@@ -145,17 +145,20 @@ test('Codex OAuth reader enforces expiry margin and bounds tokens without exp by
test('Codex login resolves an installed executable before PATH and starts browser auth detached', async () => {
const checked = [];
// Built with path.join so the stub matches the resolver on Windows too,
// where the separators are backslashes.
const installed = path.join('/home/fixture', '.local', 'bin', 'codex');
assert.equal(
resolveCodexExecutable({
environment: {},
home: '/home/fixture',
access(candidate) {
checked.push(candidate);
if (candidate.endsWith('/.local/bin/codex')) return;
if (candidate === installed) return;
throw new Error('missing');
},
}),
path.join('/home/fixture', '.local', 'bin', 'codex'),
installed,
);
assert.equal(checked.length, 1);
+27 -1
View File
@@ -1,3 +1,28 @@
/**
* A v4 UUID for the client lease. `crypto.randomUUID` only exists in secure
* contexts, so a page opened over plain HTTP from another machine (the
* HOST=0.0.0.0 LAN opt-in) has none; `getRandomValues` exists in every context
* and yields the same shape the server's lease check accepts.
*/
export function createLeaseId(cryptoImpl = globalThis.crypto) {
if (typeof cryptoImpl.randomUUID === 'function') {
return cryptoImpl.randomUUID();
}
const bytes = cryptoImpl.getRandomValues(new Uint8Array(16));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0'));
return [
hex.slice(0, 4),
hex.slice(4, 6),
hex.slice(6, 8),
hex.slice(8, 10),
hex.slice(10),
]
.map((group) => group.join(''))
.join('-');
}
/** One decoder per active camera; both surfaces consume this video element. */
export function attachCctvVideo(
video,
@@ -7,13 +32,14 @@ export function attachCctvVideo(
loadHls = () => import('hls.js'),
onFailure = () => {},
fetchImpl = globalThis.fetch,
cryptoImpl = globalThis.crypto,
} = {},
) {
// Preserve replay for existing finite video feeds; live HLS must not loop.
video.loop = feedType !== 'hls';
let disposed = false;
let hls = null;
const leaseId = feedType === 'hls' ? globalThis.crypto.randomUUID() : null;
const leaseId = feedType === 'hls' ? createLeaseId(cryptoImpl) : null;
const mediaUrl =
feedType === 'hls'
? `${url}${url.includes('?') ? '&' : '?'}lease=${leaseId}`
+38 -1
View File
@@ -1,6 +1,15 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { attachCctvVideo } from './videoPlayback.js';
import { webcrypto } from 'node:crypto';
import { attachCctvVideo, createLeaseId } from './videoPlayback.js';
const LEASE_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
/** What an insecure context exposes: getRandomValues but no randomUUID. */
const insecureCrypto = {
getRandomValues: (array) => webcrypto.getRandomValues(array),
};
function video() {
const v = new EventTarget();
Object.assign(v, {
@@ -69,6 +78,34 @@ test('native HLS releases its client lease without response-header access', asyn
assert.equal(releases[0].init.method, 'DELETE');
});
test('lease ids are v4 UUIDs with or without crypto.randomUUID', () => {
assert.match(createLeaseId(), LEASE_V4);
const ids = new Set();
for (let i = 0; i < 64; i++) ids.add(createLeaseId(insecureCrypto));
assert.equal(ids.size, 64);
for (const id of ids) assert.match(id, LEASE_V4);
});
test('HLS starts on a plain-HTTP LAN page without crypto.randomUUID', async () => {
const source = video();
source.canPlayType = () => 'probably';
const playback = attachCctvVideo(source, '/api/cctv/media/a', 'hls', {
loadHls: async () => ({ default: { isSupported: () => false } }),
fetchImpl: async () => {},
cryptoImpl: insecureCrypto,
});
try {
await playback.ready;
const lease = new URL(
source.src,
'http://192.168.1.10:4173/',
).searchParams.get('lease');
assert.match(lease, LEASE_V4);
} finally {
playback.dispose();
}
});
test('finite video feeds retain looping while live HLS does not loop', async () => {
for (const feedType of ['mp4', 'webm', 'hls']) {
const source = video();
@@ -0,0 +1,12 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseCoordinateQuery } from './coordinateParser.js';
test('search refuses omitted components and repeated separators', () => {
for (const q of ['30,, -97', ',30,-97', '30,-97,', '30;; -97', '30,; -97'])
assert.equal(parseCoordinateQuery(q), null, q);
for (const q of ['30,-97', '30; -97', '30 -97', 'N 30, W 97'])
assert.deepEqual(
[parseCoordinateQuery(q).lat, parseCoordinateQuery(q).lon],
[30, -97],
);
});
+2 -2
View File
@@ -49,8 +49,8 @@ function splitComponents(query) {
const parts = /[,;]/.test(text)
? text.split(/[,;]/)
: text.split(/\s+(?=[+-]?[.\d]|[NSEW]\s*[+-]?[.\d])/i);
const trimmed = parts.map((part) => part.trim()).filter(Boolean);
return trimmed.length === 2 ? trimmed : null;
const trimmed = parts.map((part) => part.trim());
return trimmed.length === 2 && trimmed.every(Boolean) ? trimmed : null;
}
/** Present a parsed pair the way a coordinate is normally written. */
+4 -2
View File
@@ -31,11 +31,13 @@ export const snowShader = {
float depth = 0.5 + layer * 0.5;
float speed = 0.4 + layer * 0.3;
float size = mix(0.01, 0.025, layer);
float windForce = sin(time * 0.5 + layer * 3.14) * (0.05 + wind * 0.2);
float windForce = 0.05 + wind * 0.2;
vec2 snowUV = uv * vec2(1.0, 0.5) * (4.0 + layer * 4.0);
snowUV.y += time * speed;
snowUV.x += time * windForce;
// Offset is the integral of a swaying velocity sin(time * 0.5 + phase) * windForce,
// so the sway stays bounded however long the app has been running.
snowUV.x -= 2.0 * cos(time * 0.5 + layer * 3.14) * windForce;
vec2 cell = floor(snowUV);
vec2 f = fract(snowUV);
+45
View File
@@ -704,6 +704,51 @@ test('narrow-screen rails pin every hosted panel glow inside its panel box', ()
}
});
test('right layout ignores a CSS-hidden expanded panel when deciding exclusivity', () => {
const f = fixture('right');
const weather = element('weather-panel', { height: 300 });
weather.computed.display = 'none';
f.stack.children.push(weather);
weather.parentElement = f.stack;
f.run();
assert.equal(f.stack.classList.contains('layout-exclusive'), false);
assert.equal(f.stack.dataset.expandedCount, '0');
assert.equal(f.first.getAttribute('aria-hidden'), undefined);
assert.equal(f.second.getAttribute('aria-hidden'), undefined);
assert.equal(
weather.style.getPropertyValue('--right-panel-allocated-height'),
'',
);
});
test('right layout still restores panels that exclusive presentation hides', () => {
const f = fixture('right');
// Model layers.css: an exclusive rail hides its collapsed panels.
f.options.windowRef.getComputedStyle = (node) => ({
...node.computed,
display:
f.stack.classList.contains('layout-exclusive') &&
node.classList.contains('collapsed')
? 'none'
: node.computed.display,
});
f.expand(f.first, 250);
f.second.classList.add('layout-auto-collapsed');
f.stack.classList.add('layout-exclusive');
f.run();
assert.equal(f.stack.classList.contains('layout-exclusive'), true);
assert.equal(f.second.getAttribute('aria-hidden'), 'true');
f.options.hud.visible = false;
f.run();
assert.equal(f.second.classList.contains('collapsed'), false);
assert.equal(f.second.classList.contains('layout-auto-collapsed'), false);
assert.deepEqual(f.collapsed, ['second']);
});
test('right layout ignores a hidden panel: no lane, no gap, no auto-collapse', () => {
const f = fixture('right');
const imagery = element('recent-imagery-panel', { height: 0 });
+9 -2
View File
@@ -69,10 +69,17 @@ export function layoutRightPanelRail({
// A collapse may schedule one follow-up, which only measures and allocates.
const isCollapseRetry = pendingCollapseRetries.delete(stack);
// A floating (lifted-out) panel is not laid out by the rail.
// A floating (lifted-out) panel is not laid out by the rail. Neither is one
// that other CSS hides, such as an event view. Exclusive presentation hides
// collapsed panels itself; those stay in the rail so they can be restored.
const hiddenByRail = (panel) =>
stack.classList.contains('layout-exclusive') &&
panel.classList.contains('collapsed');
const panels = [...stack.children].filter(
(panel) =>
panel.matches('[data-panel-id]:not(.panel-floating)') && !panel.hidden,
panel.matches('[data-panel-id]:not(.panel-floating)') &&
!panel.hidden &&
(hiddenByRail(panel) || getComputedStyle(panel).display !== 'none'),
);
if (!hud.visible || hud.variant !== 'tactical') {
for (const panel of panels.filter((item) =>
+51 -1
View File
@@ -1,14 +1,40 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
ALLOCATION_TEST_FILES,
UNIT_TEST_ROOTS,
allocationTestArgs,
assertNode24AllocationRuntime,
buildUnitTestPlan,
discoverUnitTestFiles,
isCalibratedAllocationRuntime,
} from '../scripts/run-unit-tests.mjs';
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url));
/** Every `*.test.mjs` a contributor could add, found without consulting the runner. */
function testFilesOnDisk(root) {
// Build outputs and dependencies are not places a contributor writes a test,
// and walking them would be slow enough to matter. Everything else counts.
const skip = new Set(['node_modules', 'dist', 'build', 'public']);
const found = [];
const visit = (directory) => {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (entry.name.startsWith('.') || skip.has(entry.name)) continue;
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) visit(absolute);
else if (entry.isFile() && entry.name.endsWith('.test.mjs')) {
found.push(path.relative(root, absolute).split(path.sep).join('/'));
}
}
};
visit(root);
return found.sort();
}
test('unit runner serializes only GC-bracketed allocation microbenchmarks', () => {
const ordinary = [
'src/data/manager.test.mjs',
@@ -37,6 +63,30 @@ test('unit runner serializes only GC-bracketed allocation microbenchmarks', () =
);
});
test('the runner discovers every test file in the repository', () => {
// CI's unit-test job runs `npm test`, which runs exactly what this function
// returns. A `*.test.mjs` outside the searched roots therefore never runs
// there, while still reading — in review and to its author — like covered
// code. Compare the two sets rather than their sizes, so the failure names the
// files instead of only disagreeing about a count. The final equality also
// catches the reverse: files the runner finds that this walk skips.
const discovered = discoverUnitTestFiles(repositoryRoot);
const onDisk = testFilesOnDisk(repositoryRoot);
const undiscovered = onDisk.filter((file) => !discovered.includes(file));
assert.deepEqual(
undiscovered,
[],
`test files the runner never executes: ${undiscovered.join(', ')}`,
);
assert.deepEqual(discovered, onDisk);
for (const file of discovered) {
assert.ok(
UNIT_TEST_ROOTS.some((root) => file.startsWith(`${root}/`)),
`${file} should sit under one of ${UNIT_TEST_ROOTS.join(', ')}`,
);
}
});
test('allocation runtime calibration is explicit and pinned to Node 24', () => {
assert.equal(assertNode24AllocationRuntime('24.19.0'), '24.19.0');
assert.throws(