Files
skills/tests/test_hub_ambience.py
T
Kevin Veen-BirkenbachandClaude Opus 5.5 c035373518 feat(orchestrator)!: S0 English everywhere, Sécurité, NATO hashes, ICAO digits
Requirement 001 phase 6, S0:

- Speech: the security class shows as "Sécurité" and is spoken as in
  maritime radio, "say-cure-ee-tay" three times. A hex run of 7 or more
  characters with a letter and a digit (commit, session, message ids) is
  spelled in the NATO alphabet; every other digit is spoken on its own
  in ICAO pronunciation (42305: four two tree zero fife). The spelling
  runs inside clip() before the length cut, so a phrase still fits
  SPEECH_LIMIT; the displayed text keeps its digits and hashes.
- English: every orchestrator label, tooltip, message, error, script
  output, document and test string is English (Regeln -> Firewall,
  Hierarchie -> Hierarchy, Steuerung -> Controls, Ausführen -> Run, ...);
  the page declares lang="en" and formats dates as en-GB. Task filter
  ids are English. German stays only in the skill trigger phrases that
  quote the operator, and as identifiers: the view keys (view=) and the
  leitstand id.
- tests/test_english.py scans every tracked text file for umlauts and
  German words, with an allowlist of the quoted trigger phrases.

BREAKING CHANGE: the Stream search tokens art: and seit: are now kind:
and since:; an old art: or seit: token counts as a plain word.

Evidence: make test 657 OK, ruff and markdownlint clean
(/tmp/s0-make-test-1.log); e2e 17 OK, none skipped (/tmp/s0-e2e-1.log);
MEDUSA 86 files, 24 findings on reworded strings accepted after an
independent skeptic (21 path:line, 0 rejected, no behaviour change in
signing, approving, rules, denying, offices, events), rescan 0 open.

🎯 Fix: 93 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:26:51 +00:00

88 lines
4.4 KiB
Python

from __future__ import annotations
import json
import subprocess
import unittest
from hub_support import AMBIENCE, STREAMS
SLOTS = [(0, 420, "Morning"), (0, 539, "Morning"), (0, 540, None), (1, 419, None), (5, 480, None), (5, 1380, "Night"), (6, 90, "Night"), (6, 120, None), (4, 1380, None), (6, 1380, None)]
PROBE = """
const A = require(process.argv[1]);
const at = (text) => Date.parse(text);
const pick = (body, keys) => Object.fromEntries(keys.map((key) => [key, body[key]]));
const surfaces = [];
for (const mode of ["light", "dark"]) {
for (const surface of ["bg", "panel"]) {
for (const sky of A.SKY.flatMap(([, top, bottom]) => [top, bottom])) {
const behind = A.mix(A.hex(sky), A.hex(A.THEMES[mode][surface]), A.GLASS);
surfaces.push({mode, surface, sky, text: A.contrast(A.hex(A.THEMES[mode].text), behind), muted: A.contrast(A.hex(A.THEMES[mode].muted), behind)});
}
}
}
const slots = JSON.parse(process.argv[3]).map(([day, minute]) => ({day, minute}));
console.log(JSON.stringify({
sun: pick(A.sun(at("1992-10-13T00:00:00Z"), 0, 0), ["ra", "dec", "eot"]),
solstice: A.sun(at("2024-06-20T20:51:00Z"), 0, 0).dec,
london: A.sun(at("2024-06-21T03:43:00Z"), 51.5074, -0.1278).alt,
moon: pick(A.moon(at("1992-04-12T00:00:00Z"), 0, 0), ["ra", "dec", "lambda", "beta", "distance", "lit", "limb", "waxing"]),
full: A.moon(at("2024-04-23T23:49:00Z"), 52.5, 13.4).lit,
fresh: A.moon(at("2024-04-08T18:21:00Z"), 52.5, 13.4).lit,
quarter: pick(A.moon(at("2024-04-15T19:13:00Z"), 52.5, 13.4), ["lit", "waxing"]),
sidereal: [A.sidereal(at("1987-04-10T00:00:00Z")), A.sidereal(at("1987-04-10T19:21:00Z"))],
stages: [-30, -15, -9, -3, 3, 20].map(A.stage),
modes: [-10, 3.9, 4, 40].map(A.modeAt),
black: A.contrast([0, 0, 0], [255, 255, 255]),
surfaces,
slot: A.slotOf(new Date(2024, 5, 21, 7, 30)),
playing: slots.map((slot) => (A.activeStream(JSON.parse(process.argv[2]), slot) || {name: null}).name),
}));
"""
class TestAmbience(unittest.TestCase):
@classmethod
def setUpClass(cls):
command = ["node", "-e", PROBE, str(AMBIENCE), json.dumps(STREAMS), json.dumps([slot[:2] for slot in SLOTS])]
done = subprocess.run(command, capture_output=True, text=True, timeout=60, check=True)
cls.data = json.loads(done.stdout)
def test_the_sun_matches_meeus_and_noaa_references(self):
sun = self.data["sun"]
self.assertAlmostEqual(sun["ra"], 198.38083, delta=0.01)
self.assertAlmostEqual(sun["dec"], -7.78507, delta=0.01)
self.assertAlmostEqual(sun["eot"], 13.71, delta=0.1)
self.assertAlmostEqual(self.data["solstice"], 23.438, delta=0.01)
self.assertAlmostEqual(self.data["london"], -0.833, delta=0.3)
self.assertAlmostEqual(self.data["sidereal"][0], 197.693195, delta=0.001)
self.assertAlmostEqual(self.data["sidereal"][1], 128.7378734, delta=0.001)
def test_the_moon_matches_meeus_within_the_low_precision_series(self):
moon = self.data["moon"]
for key, expected, delta in (("lambda", 133.162655, 0.5), ("beta", -3.229126, 0.3), ("ra", 134.68847, 0.5), ("dec", 13.768368, 0.3), ("distance", 368409.7, 3000), ("lit", 0.6786, 0.01), ("limb", 285.0, 2.0)):
with self.subTest(key=key):
self.assertAlmostEqual(moon[key], expected, delta=delta)
self.assertTrue(moon["waxing"])
self.assertGreater(self.data["full"], 0.99)
self.assertLess(self.data["fresh"], 0.01)
self.assertAlmostEqual(self.data["quarter"]["lit"], 0.5, delta=0.03)
self.assertTrue(self.data["quarter"]["waxing"])
def test_sky_stages_modes_and_text_contrast_hold_in_every_sky(self):
self.assertEqual(self.data["stages"], ["night", "astronomical", "nautical", "civil", "golden", "day"])
self.assertEqual(self.data["modes"], ["dark", "dark", "light", "light"])
self.assertAlmostEqual(self.data["black"], 21.0, places=6)
self.assertEqual(len(self.data["surfaces"]), 88)
for row in self.data["surfaces"]:
with self.subTest(mode=row["mode"], surface=row["surface"], sky=row["sky"]):
self.assertGreaterEqual(row["text"], 7.0)
self.assertGreaterEqual(row["muted"], 4.5)
def test_streams_play_on_their_weekdays_and_hours(self):
self.assertEqual(self.data["slot"], {"day": 4, "minute": 450})
self.assertEqual(self.data["playing"], [slot[2] for slot in SLOTS])
if __name__ == "__main__":
unittest.main()