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>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-08 17:26:51 +00:00
1 parent a388c6ef77
commit c035373518
86 files changed
+1027 -930

No files matched your search

+124 -124
View File
@@ -36,7 +36,7 @@ WIDTHS = ((1920, 1080), (1366, 768), (390, 844))
VIEWS = ("orbit", "chord", "swimlane", "tree", "3d")
TASK_VIEWS = ("list", "board", "rasci")
SCREENS = ["stream", "stream/inbox", "stream/rules", "stream/radio", *(f"{tab}/{view}" for tab in ("agents", "artifacts") for view in VIEWS), "agents/hierarchy", *(f"tasks/{view}" for view in TASK_VIEWS)]
SUBS = ["Inbox", "Regeln", "Radio"]
SUBS = ["Inbox", "Firewall", "Radio"]
TABS = ["Stream", "Agents", "Artifacts", "Tasks"]
HELPERS = """
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -56,7 +56,7 @@ HELPERS = """
const name = (node) => (node ? node.getAttribute("aria-label") || text(node) : null);
"""
PROBE = """(async () => {@HELPERS@
await until(() => !document.querySelector("#main .space-note") || !text(document.querySelector("#main .space-note")).startsWith("Lade"), 8000);
await until(() => !document.querySelector("#main .space-note") || !text(document.querySelector("#main .space-note")).startsWith("Loading"), 8000);
const badge = (button) => {
const found = button.querySelector(".badge");
return found && !found.hidden ? text(found) : null;
@@ -73,9 +73,9 @@ PROBE = """(async () => {@HELPERS@
badges: Object.fromEntries(all("#tabs button").map((button) => [button.id, badge(button)])),
banner: banner.hidden ? null : text(banner),
rail: Boolean(document.getElementById("rail")),
views: all('#main [aria-label="Ansicht"] button').map(name),
layouts: all('#main [aria-label="Anordnung"] button').map(name),
chips: all('#main [aria-label="Kategorien"] button').map(name),
views: all('#main [aria-label="View"] button').map(name),
layouts: all('#main [aria-label="Layout"] button').map(name),
chips: all('#main [aria-label="Categories"] button').map(name),
menus: all("#main details.menu > summary").map(name),
time: text(document.querySelector("#time > summary")),
replay: Boolean(document.querySelector("#time .replay input[type=range]")),
@@ -87,7 +87,7 @@ PROBE = """(async () => {@HELPERS@
unlabelled: icons.map((svg) => svg.parentElement).filter((owner) => !text(owner) && !(owner.getAttribute("title") && owner.getAttribute("aria-label"))).map((owner) => owner.outerHTML.slice(0, 160)),
broken: all("svg.bi use").filter((use) => !document.getElementById(use.getAttribute("href").slice(1))).map((use) => use.getAttribute("href")),
states: all("#main span.state").map(name),
deletes: all('#main button[aria-label="Löschen"], #main button[aria-label="Alle gemergten löschen"]').map((button) => [name(button), button.disabled, button.title]),
deletes: all('#main button[aria-label="Delete"], #main button[aria-label="Delete all merged"]').map((button) => [name(button), button.disabled, button.title]),
spawn: all("#main .spawn-tree li").map((item) => [item.dataset.kind, item.dataset.running || null]),
marks: all("#main svg.chart .node").length,
note: text(document.querySelector("#main .space-note")),
@@ -95,7 +95,7 @@ PROBE = """(async () => {@HELPERS@
org: all("#main .org-chart .org-row[data-office]").map((row) => [row.dataset.office, text(row.querySelector(".org-holder"))]),
toggles: all("#main .org-toggle").map((button) => [name(button), button.getAttribute("aria-expanded")]),
order: all("#main .agent-order li").map((item) => item.dataset.name),
taskFilters: all('#main [aria-label="Aufgabenfilter"] button').map((button) => [button.dataset.filter, button.getAttribute("aria-pressed"), text(button.querySelector(".count"))]),
taskFilters: all('#main [aria-label="Task filters"] button').map((button) => [button.dataset.filter, button.getAttribute("aria-pressed"), text(button.querySelector(".count"))]),
taskRows: all("#main .task-row").map((row) => [row.dataset.task, row.classList.contains("open-work")]),
columns: all("#main .task-column").map((column) => [column.dataset.column, all(".task-card", column).map((card) => card.dataset.task)]),
matrix: [all("#main table.matrix thead th").length, all("#main table.matrix tbody tr").length, all("#main table.matrix td .letter").map(text).join("")],
@@ -111,7 +111,7 @@ ORG_OPEN = """(async () => {@HELPERS@
if (!row) {
return {error: text(document.getElementById("main"))};
}
const toggle = all("#main .org-toggle").find((button) => name(button) === "Zuklappen: identity");
const toggle = all("#main .org-toggle").find((button) => name(button) === "Collapse: identity");
toggle.focus();
return {focused: document.activeElement.dataset.focus, offices: all("#main .org-row[data-office]").map((entry) => entry.dataset.office)};
})()""".replace("@HELPERS@", HELPERS)
@@ -127,19 +127,19 @@ ORG_FOLDED = """(async () => {@HELPERS@
ORDER_MOVED = """(async () => {@HELPERS@
const after = all("#main .agent-order li").map((item) => item.dataset.name);
const focused = document.activeElement.dataset.focus || null;
const save = all("#main .agent-order button").find((button) => text(button) === "Reihenfolge speichern");
const save = all("#main .agent-order button").find((button) => text(button) === "Save order");
const disabled = save.disabled;
save.click();
const toast = await until(() => all("#toasts .toast").find((box) => text(box).startsWith("Priorität")), 8000);
const toast = await until(() => all("#toasts .toast").find((box) => text(box).startsWith("Priority")), 8000);
return {after, focused, disabled, toast: text(toast)};
})()""".replace("@HELPERS@", HELPERS)
OPEN_TAKEOVER = """(async () => {@HELPERS@
document.querySelector('#main [data-focus="takeover:identity/oidc"]').click();
await sleep(400);
return {title: text(document.querySelector(".sheet-head .panel-title")), options: all(".sheet-box select option").map((option) => option.value), work: document.querySelector('.sheet-box input[aria-label="Arbeit"]').value, focused: document.activeElement.tagName};
return {title: text(document.querySelector(".sheet-head .panel-title")), options: all(".sheet-box select option").map((option) => option.value), work: document.querySelector('.sheet-box input[aria-label="Work"]').value, focused: document.activeElement.tagName};
})()""".replace("@HELPERS@", HELPERS)
CONFIRM_TAKEOVER = """(async () => {@HELPERS@
all(".sheet-box button").find((button) => name(button) === "Übergeben").click();
all(".sheet-box button").find((button) => name(button) === "Hand over").click();
const done = await until(() => document.querySelector(".sheet-box [data-result]"), 30000);
return {result: done ? done.dataset.result : null, text: text(done), toasts: all("#toasts .toast").map(text)};
})()""".replace("@HELPERS@", HELPERS)
@@ -152,11 +152,11 @@ AFTER_TAKEOVER = """(async () => {@HELPERS@
return {holder: text(row)};
})()""".replace("@HELPERS@", HELPERS)
INBOX_CHIP = """(async () => {@HELPERS@
const chip = (id) => document.querySelector(`#main [aria-label="Aufgabenfilter"] button[data-filter="${id}"]`);
const chip = (id) => document.querySelector(`#main [aria-label="Task filters"] button[data-filter="${id}"]`);
chip("operator").click();
await until(() => location.hash === "#stream/inbox" && document.querySelector("#main .inbox-list"), 4000);
const inbox = {hash: location.hash, items: all("#main .inbox-list > li").length, counted: Number(text(chip("operator").querySelector(".count"))), pressed: chip("operator").getAttribute("aria-pressed"), current: all("#tabs button[aria-current=page]").map(name)};
chip("freigabe").click();
chip("grant").click();
await until(() => location.hash.startsWith("#tasks/"), 4000);
await sleep(300);
return {inbox, back: {hash: location.hash, rows: all("#main .task-row").map((row) => [row.dataset.task, text(row.querySelector(".pill"))])}};
@@ -166,13 +166,13 @@ PIN = """(async () => {@HELPERS@
if (!row) {
return {error: text(document.getElementById("main"))};
}
row.querySelector('button[aria-label="Priorität"]').click();
row.querySelector('button[aria-label="Priority"]').click();
await sleep(400);
const field = document.querySelector('.sheet-box input[aria-label="Priorität"]');
const field = document.querySelector('.sheet-box input[aria-label="Priority"]');
field.value = "2";
field.dispatchEvent(new Event("input", {bubbles: true}));
const preview = all(".sheet-box .task-edit .command code").map(text);
all(".sheet-box button").find((button) => name(button) === "Anheften").click();
all(".sheet-box button").find((button) => name(button) === "Pin").click();
const done = await until(() => document.querySelector(".sheet-box [data-jobs]"), 20000);
return {title: text(document.querySelector(".sheet-head .panel-title")), preview, jobs: done ? done.dataset.jobs : null, toasts: all("#toasts .toast").map(text)};
})()""".replace("@HELPERS@", HELPERS)
@@ -186,7 +186,7 @@ ACTIONS = """(async () => {@HELPERS@
document.getElementById("settings-open").click();
await sleep(300);
found.ambience = all(".sheet-box form > section:first-child button").map((button) => button.id);
found.speech = all('.sheet-box [aria-label="Klassen"] input').map((input) => [input.parentElement.textContent.trim(), input.checked, input.disabled]);
found.speech = all('.sheet-box [aria-label="Classes"] input').map((input) => [input.parentElement.textContent.trim(), input.checked, input.disabled]);
found.settingsUnlabelled = all(".sheet-box svg.bi").map((svg) => svg.parentElement).filter((owner) => !text(owner) && !(owner.getAttribute("title") && owner.getAttribute("aria-label"))).length;
document.querySelector(".sheet-head button").click();
document.getElementById("palette-open").click();
@@ -194,9 +194,9 @@ ACTIONS = """(async () => {@HELPERS@
found.palette = all(".palette-list li > span:first-child").map(text);
document.dispatchEvent(new KeyboardEvent("keydown", {key: "Escape", bubbles: true}));
await sleep(200);
document.querySelector('#main [aria-label="Kategorien"] button:nth-child(2)').click();
document.querySelector('#main [aria-label="Categories"] button:nth-child(2)').click();
await sleep(900);
document.querySelector('#main button.linkish[title$="öffnen"]').click();
document.querySelector('#main button.linkish[title^="Open "]').click();
await sleep(500);
found.drawer = all("#drawer input[type=radio]").map((input) => [input.value, input.checked]);
found.drawerUnlabelled = all("#drawer svg.bi").map((svg) => svg.parentElement).filter((owner) => !text(owner) && !(owner.getAttribute("title") && owner.getAttribute("aria-label"))).length;
@@ -217,8 +217,8 @@ PICK_3D = """(async () => {@HELPERS@
return {panel: text(document.querySelector("#main aside .panel-title")), label: chip.textContent};
})()""".replace("@HELPERS@", HELPERS)
CALL = """(async () => {@HELPERS@
document.querySelector('#main [aria-label="Kategorien"] button[aria-label="Funk"]').click();
const someone = await until(() => document.querySelector('#main button.linkish[title$="öffnen"]'), 8000);
document.querySelector('#main [aria-label="Categories"] button[aria-label="Radio"]').click();
const someone = await until(() => document.querySelector('#main button.linkish[title^="Open "]'), 8000);
someone.click();
await sleep(500);
document.querySelector('#drawer input[type=radio][value="call"]').click();
@@ -226,7 +226,7 @@ CALL = """(async () => {@HELPERS@
field.value = "E2E call ping";
field.dispatchEvent(new Event("input", {bubbles: true}));
document.querySelector('#drawer button[type="submit"]').click();
const entry = await until(() => all("#main .entry").find((row) => row.textContent.includes("Gelesen") && row.textContent.includes("E2E call ping")), 20000);
const entry = await until(() => all("#main .entry").find((row) => row.textContent.includes("Read") && row.textContent.includes("E2E call ping")), 20000);
return {to: text(someone), receipt: entry ? text(entry) : null, sent: all("#main .entry").filter((row) => row.textContent.includes("E2E call ping")).length};
})()""".replace("@HELPERS@", HELPERS)
OPEN_DELETE = """(async () => {@HELPERS@
@@ -236,22 +236,22 @@ OPEN_DELETE = """(async () => {@HELPERS@
}
card.open = true;
await sleep(300);
const every = document.querySelector('#main button[aria-label="Alle gemergten löschen"]');
const buttons = all('button[aria-label="Löschen"]', card);
const every = document.querySelector('#main button[aria-label="Delete all merged"]');
const buttons = all('button[aria-label="Delete"]', card);
const states = all("span.state", card).map(name);
buttons.find((button) => !button.disabled).click();
await sleep(500);
return {states, every: [every.disabled, every.title], buttons: buttons.map((button) => [button.disabled, button.title]), title: text(document.querySelector(".sheet-head .panel-title")), dry: all('.sheet-box [aria-label="Trockenlauf"] pre').map(text)};
return {states, every: [every.disabled, every.title], buttons: buttons.map((button) => [button.disabled, button.title]), title: text(document.querySelector(".sheet-head .panel-title")), dry: all('.sheet-box [aria-label="Dry run"] pre').map(text)};
})()""".replace("@HELPERS@", HELPERS)
CONFIRM = """(async () => {@HELPERS@
all(".sheet-box button").find((button) => name(button) === "Löschen").click();
all(".sheet-box button").find((button) => name(button) === "Delete").click();
const done = await until(() => document.querySelector(".sheet-box [data-result]"), 30000);
const job = document.querySelector(".sheet-box [data-cleanup]");
return {job: job ? job.dataset.cleanup : null, result: done ? done.dataset.result : null, text: text(done), toasts: all("#toasts .toast").map(text)};
})()""".replace("@HELPERS@", HELPERS)
AFTER = """(async () => {@HELPERS@
document.querySelector(".sheet-head button").click();
const past = await until(() => all("#main details.repo").find((details) => details.textContent.includes("Zuletzt gelöscht")), 15000);
const past = await until(() => all("#main details.repo").find((details) => details.textContent.includes("Recently deleted")), 15000);
return {past: past ? [past.querySelector("[data-result]").dataset.result, name(past.querySelector("span.state"))] : null, branches: all("#main details.repo").filter((details) => details !== past).flatMap((details) => all(".branch-name strong", details)).map(text)};
})()""".replace("@HELPERS@", HELPERS)
OVERLAP = """
@@ -291,12 +291,12 @@ RULES_OVERLAP = """(async () => {@HELPERS@@CLASHES@
SETTINGS_OVERLAP = """(async () => {@HELPERS@@CLASHES@
document.getElementById("settings-open").click();
const settings = await until(() => document.querySelector(".sheet-box form.settings"), 8000);
all("button", settings).find((button) => text(button) === "+ Sender").click();
all("button", settings).find((button) => text(button) === "+ Station").click();
const theme = all("select", settings).find((select) => [...select.options].some((option) => option.value === "custom"));
theme.value = "custom";
theme.dispatchEvent(new Event("change", {bubbles: true}));
await sleep(250);
const place = all("section", settings).find((section) => text(section.querySelector("h3")) === "Ort");
const place = all("section", settings).find((section) => text(section.querySelector("h3")) === "Location");
place.scrollIntoView({block: "start"});
return clashes(settings);
})()""".replace("@HELPERS@", HELPERS).replace("@CLASHES@", OVERLAP)
@@ -334,7 +334,7 @@ ADD_STATION = """(async () => {@HELPERS@
const clock = (date) => `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
document.getElementById("settings-open").click();
const form = await until(() => document.querySelector(".sheet-box form.settings"), 8000);
all("button", form).find((button) => text(button) === "+ Sender").click();
all("button", form).find((button) => text(button) === "+ Station").click();
await sleep(200);
const row = all(".stream-row", form).pop();
const [label, address, start, end] = all("input.field", row);
@@ -350,10 +350,10 @@ ADD_STATION = """(async () => {@HELPERS@
box.checked = true;
});
all("button", form).find((button) => button.type === "submit").click();
await until(() => all("#toasts .toast").map(text).find((toast) => toast.startsWith("Neuer Sender")), 8000);
await until(() => all("#toasts .toast").map(text).find((toast) => toast.startsWith("New station")), 8000);
return {toasts: all("#toasts .toast").map(text)};
})()""".replace("@HELPERS@", HELPERS)
STATION_NAME = "E2E Ton mit einem sehr langen Sendernamen, der in keine Zeile passt"
STATION_NAME = "E2E tone with a very long station name that fits on no single line"
DLF_PATH = "/dlf/01/high/aac/stream.aac?aggregator=web"
DLF_NODE = "/ard/dlf/01/aac/192/stream.aac?aggregator=web"
RADIO_STATE = """(async () => {@HELPERS@@MEDIA@
@@ -459,22 +459,22 @@ COMPOSE = """(async () => {@HELPERS@
COMPOSE_SEND = """(async () => {@HELPERS@
const form = document.querySelector("#main form.composer");
const field = form.querySelector("textarea");
field.value = `${field.value} bitte`;
field.value = `${field.value} please`;
field.dispatchEvent(new Event("input", {bubbles: true}));
form.querySelector('.voice-note input[type="checkbox"]').checked = @ATTACH@;
const before = all("#toasts .toast").length;
form.querySelector('button[type="submit"]').click();
const sent = await until(() => all("#toasts .toast").slice(before).map(text).find((toast) => toast.startsWith("Call an")), 20000);
const sent = await until(() => all("#toasts .toast").slice(before).map(text).find((toast) => toast.startsWith("Call to")), 20000);
return {sent, left: field.value, voice: Boolean(form.querySelector(".voice-note audio"))};
})()""".replace("@HELPERS@", HELPERS)
CLEAR_MAYDAY = """(async () => {@HELPERS@
window.confirm = () => true;
const button = await until(() => all('#main [role="toolbar"] button').find((node) => name(node) === "Mayday aufheben" && !node.disabled), 8000);
const button = await until(() => all('#main [role="toolbar"] button').find((node) => name(node) === "Cancel mayday" && !node.disabled), 8000);
if (!button) {
return {error: "no enabled Mayday aufheben button"};
return {error: "no enabled Cancel mayday button"};
}
button.click();
const done = await until(() => all("#toasts .toast").map(text).find((toast) => toast.startsWith("Mayday aufheben")), 15000);
const done = await until(() => all("#toasts .toast").map(text).find((toast) => toast.startsWith("Cancel mayday")), 15000);
return {done, toasts: all("#toasts .toast").map(text)};
})()""".replace("@HELPERS@", HELPERS)
@@ -514,20 +514,20 @@ LABELS = """
const line = () => text(document.querySelector("#main .console .console-status"));
"""
RUN_START = """(async () => {@HELPERS@@LABELS@
const row = await until(() => rowOf("Zeile"), 8000);
const row = await until(() => rowOf("Row"), 8000);
if (!row) {
return {error: text(document.getElementById("main"))};
}
press(row, "Ausführen");
const toast = await until(() => all("#toasts .toast.wait").find((box) => box.textContent.includes("Ausführen")), 8000);
press(row, "Run");
const toast = await until(() => all("#toasts .toast.wait").find((box) => box.textContent.includes("Run")), 8000);
const queued = await until(() => out() && line(), 15000);
await until(() => out().textContent.includes("Zeile 60"), 20000);
await until(() => out().textContent.includes("Row 60"), 20000);
return {toast: text(toast), queued, running: line(), overflows: out().scrollHeight > out().clientHeight, followed: atEnd()};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
RUN_HELD = """(async () => {@HELPERS@@LABELS@
out().scrollTop = 0;
await sleep(300);
await until(() => out().textContent.includes("Zeile 80"), 20000);
await until(() => out().textContent.includes("Row 80"), 20000);
await sleep(300);
return {held: out().scrollTop, running: line()};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
@@ -539,8 +539,8 @@ RUN_DONE = """(async () => {@HELPERS@@LABELS@
return {done: line(), last: lines[lines.length - 1], lines: lines.length, followed: atEnd(), actions: all(".item-actions button", out().closest("li")).map(name)};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
LAYOUT = """(async () => {@HELPERS@@LABELS@
await until(() => document.querySelector("#main .console") && rowOf("Regelwunsch"), 8000);
const rule = rowOf("Regelwunsch");
await until(() => document.querySelector("#main .console") && rowOf("Rule request"), 8000);
const rule = rowOf("Rule request");
return {
kinds: all("#main .inbox-list > li").map((li) => text(li.querySelector(".pill"))),
splits: all("#main .deny-split").map((group) => all("button", group).map(name)),
@@ -549,14 +549,14 @@ LAYOUT = """(async () => {@HELPERS@@LABELS@
fits: out().getBoundingClientRect().right <= window.innerWidth,
dry: text(rule.querySelector(".dry-run-count")),
fields: all(".rule-request dt", rule).map(text),
accept: all("button", rule).filter((button) => name(button) === "Annehmen").length,
accept: all("button", rule).filter((button) => name(button) === "Accept").length,
unlabelled: unlabelled(document),
wide: wide(),
};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
DENY_SHEET = """(async () => {@HELPERS@@LABELS@
const row = await until(() => rowOf("git push"), 8000);
press(row, "Ablehnen über …");
press(row, "Deny via …");
const box = await until(() => document.querySelector(".sheet-box"), 4000);
return {
title: text(box.querySelector(".panel-title")),
@@ -569,24 +569,24 @@ DENY_SHEET = """(async () => {@HELPERS@@LABELS@
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
DENY = """(async () => {@HELPERS@@LABELS@
const row = await until(() => rowOf("Bash: make deploy"), 8000);
press(row, "Ablehnen über …");
press(row, "Deny via …");
const box = await until(() => document.querySelector(".sheet-box"), 4000);
box.querySelector('input[value="call"]').click();
box.querySelector("textarea").value = "erst das Gate grün machen";
press(box.querySelector(".actions"), "Ablehnen");
const undo = await until(() => all("#toasts .toast.wait").find((toast) => toast.textContent.includes("Ablehnung")), 8000);
box.querySelector("textarea").value = "make the gate green first";
press(box.querySelector(".actions"), "Deny");
const undo = await until(() => all("#toasts .toast.wait").find((toast) => toast.textContent.includes("Denial")), 8000);
return {undo: text(undo), sheet: !document.getElementById("sheet").hidden};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
DENIED = """(async () => {@HELPERS@@LABELS@
return {gone: Boolean(await until(() => !rowOf("Bash: make deploy"), 20000))};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
ACCEPT = """(async () => {@HELPERS@@LABELS@
press(rowOf("Regelwunsch"), "Annehmen");
const toast = await until(() => all("#toasts .toast").find((box) => box.textContent.includes("Angelegt: Regel")), 15000);
await until(() => !rowOf("Regelwunsch"), 8000);
return {toast: text(toast), left: Boolean(rowOf("Regelwunsch"))};
press(rowOf("Rule request"), "Accept");
const toast = await until(() => all("#toasts .toast").find((box) => box.textContent.includes("Created: Rule")), 15000);
await until(() => !rowOf("Rule request"), 8000);
return {toast: text(toast), left: Boolean(rowOf("Rule request"))};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
SLOW = "seq -f 'Zeile %g' 1 60; sleep 4; seq -f 'Zeile %g' 61 80; sleep 4; echo fertig"
SLOW = "seq -f 'Row %g' 1 60; sleep 4; seq -f 'Row %g' 61 80; sleep 4; echo finished"
def labelled(probe: dict) -> dict:
@@ -609,12 +609,12 @@ class TestOrchestratorE2E(HubCase):
"rasci.json": {
"work/stage": {"title": "Stage line", "status": "running", "worker": "Stage (this session)", "roles": {"Stage (this session)": "RA"}},
"work/feature": {
"title": "Merge-Flow im Hub zeigen", "status": "running", "worker": "Skills session", "objective": "Der Operator sieht, wohin ein Branch fließt", "workdir": feature, "prio": 1,
"roles": {"Skills session": "RA", "Operator": "C", "Docs": "I"}, "key_results": [{"text": "Flow im Tree", "met": True}, {"text": "Flow in Tasks", "met": False}],
"title": "Show the merge flow in the hub", "status": "running", "worker": "Skills session", "objective": "The operator sees where a branch flows", "workdir": feature, "prio": 1,
"roles": {"Skills session": "RA", "Operator": "C", "Docs": "I"}, "key_results": [{"text": "Flow in the tree", "met": True}, {"text": "Flow in Tasks", "met": False}],
"estimates": {"Skills": {"value": 8, "complexity": 3, "at": iso(now - 600)}},
},
"work/docs": {"title": "README nachziehen", "status": "blocked", "worker": "Docs", "branch": "main", "roles": {"Docs": "R", "Maintainer": "A", "Skills": "S"}, "estimates": {"Docs": {"value": 3, "complexity": 2}}},
"work/old": {"title": "Alte Linie", "status": "done", "worker": "Stage", "end": iso(now - 86400), "roles": {"Stage": "RA"}},
"work/docs": {"title": "Update the README", "status": "blocked", "worker": "Docs", "branch": "main", "roles": {"Docs": "R", "Maintainer": "A", "Skills": "S"}, "estimates": {"Docs": {"value": 3, "complexity": 2}}},
"work/old": {"title": "Old line", "status": "done", "worker": "Stage", "end": iso(now - 86400), "roles": {"Stage": "RA"}},
"operator/push": {"title": "git push", "workdir": "/repo", "status": "done", "roles": {"Operator": "R"}},
},
"queue.json": {"seq": 3, "entries": [{"id": "gate-3", "kind": "gate", "task": "work/feature", "workdir": feature, "session": "s1", "name": "Skills", "status": "waiting", "enqueued": now - 120, "cmd": "make test"}]},
@@ -679,7 +679,7 @@ class TestOrchestratorE2E(HubCase):
def synthesize(body: bytes) -> bytes:
word = json.loads(body)["text"]
if word in cues:
row = {"ts": time.time(), "from": "Stage", "to": [], "text": f"Platte fast voll ({word})\nzweite Zeile", "class": "security", "v": 2}
row = {"ts": time.time(), "from": "Stage", "to": [], "text": f"Disk almost full ({word})\nsecond line", "class": "security", "v": 2}
with (self.council / "news.jsonl").open("a", encoding="utf-8") as handle:
handle.write(self.jsonl([row]))
return spoken
@@ -731,7 +731,7 @@ class TestOrchestratorE2E(HubCase):
steps.append({"width": width, "height": height, "url": None, "wait": 0, "probe": SETTINGS_OVERLAP, "shot": str(OUT / f"{browser}-{width}-settings-form.png")})
data = self.drive(steps, None, driver, binary)
(OUT / f"{browser}-overlap.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
hidden = {"decision": ("Werkzeug (Glob)", "Verzeichnis (Glob)"), "review": ("Werkzeug (Glob)",), "ask": ()}
hidden = {"decision": ("Tool (glob)", "Directory (glob)"), "review": ("Tool (glob)",), "ask": ()}
for index, (width, _) in enumerate(WIDTHS):
rules, settings = data["results"][2 * index : 2 * index + 2]
with self.subTest(browser=browser, width=width):
@@ -740,7 +740,7 @@ class TestOrchestratorE2E(HubCase):
for kind, found in rules.items():
self.assertEqual((found["found"], found["outside"]), ([], []), kind)
self.assertEqual([label for label in hidden[kind] if label in found["labels"]], [], kind)
self.assertIn("Verzeichnis (Glob)", rules["review"]["labels"])
self.assertIn("Directory (glob)", rules["review"]["labels"])
self.assertEqual(settings, {"found": [], "outside": []})
def check_radio(self, browser: str, driver: str, binary: str, redirected: bool = False) -> dict:
@@ -770,7 +770,7 @@ class TestOrchestratorE2E(HubCase):
(OUT / f"{browser}-radio.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
saved, view, click = data["results"]
if signs:
self.assertTrue(any(toast.startswith("Neuer Sender") for toast in saved["toasts"]), saved)
self.assertTrue(any(toast.startswith("New station") for toast in saved["toasts"]), saved)
self.assertNotIn("error", view)
self.assertEqual(view["title"], STATION_NAME)
self.assertEqual(view["line"], ["nowrap", "ellipsis", True, True])
@@ -778,8 +778,8 @@ class TestOrchestratorE2E(HubCase):
if not view["media"]["playing"]:
self.assertTrue(blocked, view)
self.assertEqual(view["state"], "blocked", view)
self.assertIn("Klick", view["note"])
self.assertTrue(any("blockiert" in toast for toast in view["toasts"]), view["toasts"])
self.assertIn("Click", view["note"])
self.assertTrue(any("blocked" in toast for toast in view["toasts"]), view["toasts"])
self.assertTrue(click["clicked"], click)
self.assertNotIn("error", click)
self.assertTrue(click["media"]["playing"], click)
@@ -815,7 +815,7 @@ class TestOrchestratorE2E(HubCase):
steps.append({"width": 1366, "height": 768, "url": f"{self.base}#artifacts/3d", "wait": 900, "probe": PICK_3D.replace("@PREFIX@", "branch:"), "shot": self.shot("1366-artifacts-3d-picked")})
steps.append({"width": 1366, "height": 768, "url": f"{self.base}#agents/3d", "wait": 900, "probe": PICK_3D.replace("@PREFIX@", "s2"), "shot": self.shot("1366-agents-3d-picked")})
steps.append({"width": 1366, "height": 768, "url": f"{self.base}#artifacts/3d", "wait": 900, "probe": PROBE, "shot": self.shot("1366-artifacts-3d-reduced-motion"), "media": [{"name": "prefers-reduced-motion", "value": "reduce"}]})
steps.append({"width": 1366, "height": 768, "url": f"{self.base}#tasks/list", "wait": 900, "probe": INBOX_CHIP, "shot": self.shot("1366-tasks-freigabe")})
steps.append({"width": 1366, "height": 768, "url": f"{self.base}#tasks/list", "wait": 900, "probe": INBOX_CHIP, "shot": self.shot("1366-tasks-grant")})
data = self.drive(steps, None)
(OUT / "chromium-walk.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
@@ -836,43 +836,43 @@ class TestOrchestratorE2E(HubCase):
self.assertEqual([probe["tall"] for probe in probes if probe["hash"] == "#agents/orbit"], [False] * len(WIDTHS))
by_hash = {probe["hash"]: probe for probe in probes}
stream = by_hash["#stream"]
self.assertEqual([probe["chips"] for probe in probes if probe["hash"] == "#stream"], [["Für dich", "Funk", "Arbeit", "Transkripte"]] * len(WIDTHS))
self.assertEqual([probe["chips"] for probe in probes if probe["hash"] == "#stream"], [["For you", "Radio", "Work", "Transcripts"]] * len(WIDTHS))
self.assertEqual((stream["menus"], stream["custom"]), (["Filter"], True))
self.assertEqual(by_hash["#stream/inbox"]["inbox"], 2)
for tab, own in (("agents", ["Hierarchie"]), ("artifacts", [])):
for tab, own in (("agents", ["Hierarchy"]), ("artifacts", [])):
with self.subTest(tab=tab):
self.assertEqual(by_hash[f"#{tab}/orbit"]["views"], ["Orbit", "Chord", "Swimlane", "Tree", "3D", *own])
self.assertEqual(by_hash[f"#{tab}/3d"]["layouts"], ["Kraft", "Zeit", "Spawn-Tiefe"])
self.assertEqual(by_hash[f"#{tab}/3d"]["layouts"], ["Force", "Time", "Spawn depth"])
self.assertTrue(by_hash[f"#{tab}/3d"]["canvas"], by_hash[f"#{tab}/3d"]["note"])
self.assertRegex(by_hash[f"#{tab}/3d"]["note"], r"^\d+ Knoten · \d+ Kanten · Kraft$")
self.assertRegex(by_hash[f"#{tab}/3d"]["note"], r"^\d+ nodes · \d+ edges · Force$")
agents = by_hash["#agents/orbit"]
self.assertEqual(agents["toolbar"], ["Funk an alle", "Mayday aufheben", "Priorität", "Tag/Nacht"])
self.assertEqual(agents["toolbar"], ["Radio to all", "Cancel mayday", "Priority", "Day/Night"])
self.assertEqual(agents["toAll"], [["newsletter", True], ["broadcast", False], ["pan-pan", False], ["security", False], ["mayday", False]])
hierarchy = by_hash["#agents/hierarchy"]
self.assertEqual([office for office, _ in hierarchy["org"]], ["chair", "identity/oidc", "work/s1", "work/s2"])
self.assertEqual(hierarchy["toggles"], [["Zuklappen: Operator", "true"], ["Zuklappen: Chair", "true"], ["Zuklappen: identity", "true"], ["Zuklappen: Präsidenten", "true"]])
self.assertEqual(hierarchy["toggles"], [["Collapse: Operator", "true"], ["Collapse: Chair", "true"], ["Collapse: identity", "true"], ["Collapse: Presidents", "true"]])
self.assertEqual(hierarchy["toolbar"], agents["toolbar"])
self.assertGreaterEqual(len(hierarchy["order"]), 2)
self.assertEqual(by_hash["#agents/tree"]["spawn"][0], ["operator", None])
self.assertIn(["twig", "true"], by_hash["#agents/tree"]["spawn"])
self.assertIn(["twig", "false"], by_hash["#agents/tree"]["spawn"])
artifacts = by_hash["#artifacts/tree"]
self.assertEqual(artifacts["deletes"], [["Alle gemergten löschen", True, "Alle gemergten löschen gesperrt: Secret Service locked"], ["Löschen", True, "Löschen gesperrt: Secret Service locked"]])
for state in ("gemergt durch #12 in main", "Upstream weg", "geschützt", "1 Commits ungepusht", "sauber", "nicht committet: 0 gestaged, 0 geändert, 1 neu", "gc überspringt: ignored files beyond known caches: notes.txt"):
self.assertEqual(artifacts["deletes"], [["Delete all merged", True, "Delete all merged blocked: Secret Service locked"], ["Delete", True, "Delete blocked: Secret Service locked"]])
for state in ("merged by #12 into main", "upstream gone", "protected", "1 commits unpushed", "clean", "not committed: 0 staged, 0 changed, 1 new", "gc skips: ignored files beyond known caches: notes.txt"):
with self.subTest(state=state):
self.assertIn(state, artifacts["states"])
for view in ("orbit", "chord"):
with self.subTest(view=view):
self.assertGreaterEqual(by_hash[f"#artifacts/{view}"]["marks"], 4)
self.assertEqual((artifacts["flows"], artifacts["openWork"]), (["file", "invalid"], 5))
for state in ("offene Arbeit: nicht committet, Aufgabe offen", "offene Arbeit: ungepusht, Aufgabe offen", "offene Arbeit: nicht gemergt", "3 Worktrees mit offener Arbeit"):
for state in ("open work: not committed, task open", "open work: unpushed, task open", "open work: not merged", "3 worktrees with open work"):
with self.subTest(state=state):
self.assertIn(state, artifacts["states"])
self.assertEqual(by_hash["#artifacts/chord"]["flowEdges"], ["pull", "push"])
order = ["work/feature", "work/docs", "work/stage", "queue/gate-3", "work/old", "operator/push"]
listed = by_hash["#tasks/list"]
self.assertEqual(listed["views"], ["Liste", "Board", "RASCI"])
self.assertEqual(listed["taskFilters"], [["alle", "true", "6"], ["offen", "false", "4"], ["arbeit", "false", "4"], ["operator", "false", "2"], ["freigabe", "false", "1"], ["beendet", "false", "2"]])
self.assertEqual(listed["views"], ["List", "Board", "RASCI"])
self.assertEqual(listed["taskFilters"], [["all", "true", "6"], ["open", "false", "4"], ["work", "false", "4"], ["operator", "false", "2"], ["grant", "false", "1"], ["ended", "false", "2"]])
self.assertEqual(listed["taskRows"], [[task, task not in ("work/old", "operator/push")] for task in order])
self.assertEqual(listed["flows"], ["file", "file"])
self.assertEqual(by_hash["#tasks/board"]["columns"], [["queued", ["queue/gate-3"]], ["running", ["work/feature", "work/stage"]], ["blocked", ["work/docs"]], ["done", ["work/old", "operator/push"]], ["stopped", []]])
@@ -885,19 +885,19 @@ class TestOrchestratorE2E(HubCase):
self.assertEqual(actions["keys"], ["#agents/chord", "#artifacts/tree", "#tasks/rasci", "#stream"])
self.assertEqual(actions["ambience"], ["sky-look", "music-toggle", "leitstand", "notify"])
self.assertEqual((actions["settingsUnlabelled"], actions["drawerUnlabelled"]), (0, 0))
self.assertEqual([name for name, _, _ in actions["speech"]], ["Post", "Call", "Newsletter", "Broadcast", "MAYDAY", "PAN-PAN", "SECURITE"])
self.assertEqual([name for name, _, _ in actions["speech"]], ["Post", "Call", "Newsletter", "Broadcast", "MAYDAY", "PAN-PAN", "Sécurité"])
self.assertEqual([entry[1:] for entry in actions["speech"] if entry[0] == "MAYDAY"], [[True, True]])
for tool in ("Funk an alle …", "Mayday aufheben", "Priorität …", "Abend", "Regeln …", "Einstellungen …", "Himmel ansehen", "Leitstand umschalten", "Benachrichtigungen aktivieren"):
for tool in ("Radio to all …", "Cancel mayday", "Priority …", "Evening", "Firewall …", "Settings …", "Look at the sky", "Toggle control room", "Enable notifications"):
with self.subTest(tool=tool):
self.assertTrue(any(tool in entry for entry in actions["palette"]), actions["palette"])
self.assertEqual(actions["drawer"], [["post", True], ["call", False]])
artifact_pick, agent_pick, calm, chips = extra[4:]
self.assertEqual(artifact_pick["panel"], artifact_pick["label"])
self.assertEqual(agent_pick["panel"], agent_pick["label"])
self.assertRegex(calm["note"], r"Kraft · ohne Bewegung \(prefers-reduced-motion\)$")
self.assertRegex(calm["note"], r"Force · without motion \(prefers-reduced-motion\)$")
self.assertEqual({key: value for key, value in chips["inbox"].items() if key != "counted"}, {"hash": "#stream/inbox", "items": chips["inbox"]["counted"], "pressed": "true", "current": ["Inbox"]})
self.assertGreaterEqual(chips["inbox"]["items"], 1)
self.assertEqual(chips["back"], {"hash": "#tasks/list", "rows": [["queue/gate-3", "Queue-Job"]]})
self.assertEqual(chips["back"], {"hash": "#tasks/list", "rows": [["queue/gate-3", "Queue job"]]})
def test_chromium_sends_a_call_from_the_agent_panel_and_shows_its_receipt(self):
handled = []
@@ -918,7 +918,7 @@ class TestOrchestratorE2E(HubCase):
result = data["results"][0]
self.assertNotIn("error", result)
self.assertIsNotNone(result["receipt"], result)
self.assertIn("Gelesen", result["receipt"])
self.assertIn("Read", result["receipt"])
def test_chromium_shows_who_speaks_while_a_message_is_read(self):
self.speaks(("e2e-cue-1", "e2e-cue-2"))
@@ -932,11 +932,11 @@ class TestOrchestratorE2E(HubCase):
(OUT / "chromium-speaking.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
speaking, graph, ended, calm = data["results"]
self.assertEqual((speaking["who"], speaking["line"], speaking["chip"], speaking["ring"]), ("Stage spricht", "Platte fast voll (e2e-cue-1)", "s2", "speaking"), speaking)
self.assertEqual((speaking["who"], speaking["line"], speaking["chip"], speaking["ring"]), ("Stage speaking", "Disk almost full (e2e-cue-1)", "s2", "speaking"), speaking)
self.assertEqual((graph["node"] or "").split(",")[0], "Stage", graph)
self.assertEqual(graph["ring"], "speaking-node")
self.assertEqual(ended, {"ended": True, "caption": False, "chips": 0})
self.assertEqual((calm["who"], calm["line"], calm["ring"]), ("Stage spricht", "Platte fast voll (e2e-cue-2)", "none"), calm)
self.assertEqual((calm["who"], calm["line"], calm["ring"]), ("Stage speaking", "Disk almost full (e2e-cue-2)", "none"), calm)
def test_chromium_records_transcribes_and_sends_a_signed_call_with_voice(self):
local = f"http://localhost:{self.port}/"
@@ -956,18 +956,18 @@ class TestOrchestratorE2E(HubCase):
self.assertEqual(composed["recording"], "true")
self.assertEqual(composed["text"], "hello from the mic second line", composed)
self.assertRegex(composed["replay"] or "", r"^/api/voice/[0-9a-f]{16}\.wav$")
self.assertEqual(composed["attach"], ["Aufnahme anhängen", False])
self.assertEqual(composed["attach"], ["Attach recording", False])
self.assertEqual(composed["clashes"], {"found": [], "outside": []})
self.assertEqual(again["attach"], ["Aufnahme anhängen", False])
self.assertEqual(again["attach"], ["Attach recording", False])
note = again["replay"].removeprefix("/api/voice/").removesuffix(".wav")
for result in (plain, sent):
self.assertEqual((result["left"], result["voice"]), ("", False), result)
self.assertTrue(result["sent"].startswith("Call an Stage: gesendet"), result)
self.assertTrue(result["sent"].startswith("Call to Stage: sent"), result)
calls = [args for script, args in self.scripts.calls if script == skill_scripts.RADIO]
self.assertEqual(len(calls), 2, self.scripts.calls)
self.assertEqual([call[:4] for call in calls], [["send", "call", "--to", "Stage"]] * 2)
self.assertTrue(calls[0][-1].endswith("] hello from the mic second line bitte"), calls[0])
self.assertTrue(calls[1][-1].endswith(f"] hello from the mic second line bitte voice: {HOST_STATE}/voice/{note}.wav"), calls[1])
self.assertTrue(calls[0][-1].endswith("] hello from the mic second line please"), calls[0])
self.assertTrue(calls[1][-1].endswith(f"] hello from the mic second line please voice: {HOST_STATE}/voice/{note}.wav"), calls[1])
status, _, body = self.get(f"/api/voice/{note}.wav")
self.assertEqual((status, body[:4]), (200, b"RIFF"))
@@ -977,7 +977,7 @@ class TestOrchestratorE2E(HubCase):
self.assertEqual(data["errors"], [])
result = data["results"][0]
self.assertNotIn("error", result)
self.assertTrue((result["done"] or "").startswith("Mayday aufheben: erledigt"), result)
self.assertTrue((result["done"] or "").startswith("Cancel mayday: done"), result)
self.assertEqual(self.scripts.calls, [(skill_scripts.RADIO, ["clear", "--force", "--session", "Operator"])])
def test_chromium_deletes_a_merged_branch_after_dry_run_and_signature(self):
@@ -1011,15 +1011,15 @@ class TestOrchestratorE2E(HubCase):
self.assertEqual(data["errors"], [])
opened, confirmed, after = data["results"]
self.assertNotIn("error", opened)
self.assertEqual(opened["every"], [False, "Alle gemergten löschen: 1 Branches nach Trockenlauf und Signatur"])
self.assertEqual(opened["buttons"], [[False, "Löschen: feature/done lokal entfernen, nach Trockenlauf und Signatur"]])
self.assertEqual(opened["title"], "feature/done löschen")
self.assertEqual(opened["every"], [False, "Delete all merged: 1 branches after dry run and signature"])
self.assertEqual(opened["buttons"], [[False, "Delete: remove feature/done locally, after dry run and signature"]])
self.assertEqual(opened["title"], "Delete feature/done")
self.assertEqual(len(opened["dry"]), 1)
self.assertTrue(opened["dry"][0].startswith("feature/done @ 5b1e6fd3a7c0: merged-clean, gemergt durch #12"), opened["dry"])
self.assertIn("bleibt: remote-branch (nur lokal)", opened["dry"][0])
self.assertEqual((confirmed["result"], confirmed["text"]), ("done", "feature/donegelöscht"), confirmed)
self.assertEqual(confirmed["toasts"], ["Löschen: erledigtfeature/done: gelöscht"])
self.assertEqual(after["past"], ["done", "gelöscht"])
self.assertTrue(opened["dry"][0].startswith("feature/done @ 5b1e6fd3a7c0: merged-clean, merged by #12"), opened["dry"])
self.assertIn("stays: remote-branch (local only)", opened["dry"][0])
self.assertEqual((confirmed["result"], confirmed["text"]), ("done", "feature/donedeleted"), confirmed)
self.assertEqual(confirmed["toasts"], ["Delete: donefeature/done: deleted"])
self.assertEqual(after["past"], ["done", "deleted"])
self.assertNotIn("feature/done", after["branches"])
written = [json.loads(path.read_text(encoding="utf-8")) for path in results.glob("*.json")]
self.assertEqual([[(item["branch"], item["status"]) for item in result["items"]] for result in written], [[("feature/done", "done")]])
@@ -1053,16 +1053,16 @@ class TestOrchestratorE2E(HubCase):
opened, folded, moved, sheet, confirmed, after = data["results"]
self.assertNotIn("error", opened)
self.assertEqual(opened["focused"], "toggle:dimension:identity")
self.assertEqual((folded["focused"], folded["expanded"], folded["label"], folded["hidden"], folded["again"]), ("toggle:dimension:identity", "false", "Aufklappen: identity", True, "true"))
self.assertEqual((folded["focused"], folded["expanded"], folded["label"], folded["hidden"], folded["again"]), ("toggle:dimension:identity", "false", "Expand: identity", True, "true"))
self.assertNotIn("identity/oidc", folded["seen"])
before = folded["before"]
self.assertEqual(moved["after"], [before[1], before[0], *before[2:]])
self.assertEqual((moved["focused"], moved["disabled"]), (f"down:{before[1]}", False))
self.assertTrue(moved["toast"].startswith("Priorität"), moved)
self.assertTrue(moved["toast"].startswith("Priority"), moved)
self.assertIn(("rasci.py", ["agents", "order", ",".join(moved["after"])]), [(Path(script).name, args) for script, args in self.scripts.calls])
self.assertEqual((sheet["title"], sheet["options"], sheet["work"], sheet["focused"]), ("identity/oidc: Übergeben", ["s1"], "oidc line", "SELECT"))
self.assertEqual((confirmed["result"], confirmed["text"]), ("done", "identity/oidcerledigt"), confirmed)
self.assertIn("Übergeben: erledigtidentity/oidc: erledigt", confirmed["toasts"])
self.assertEqual((sheet["title"], sheet["options"], sheet["work"], sheet["focused"]), ("identity/oidc: Hand over", ["s1"], "oidc line", "SELECT"))
self.assertEqual((confirmed["result"], confirmed["text"]), ("done", "identity/oidcdone"), confirmed)
self.assertIn("Hand over: doneidentity/oidc: done", confirmed["toasts"])
self.assertNotIn("error", after)
claim = json.loads((self.council / "identity" / "oidc" / "claim.json").read_text(encoding="utf-8"))
self.assertEqual((claim["session"], claim["work"]), ("s1", "oidc line"))
@@ -1071,7 +1071,7 @@ class TestOrchestratorE2E(HubCase):
def inbox_work(self) -> None:
stage = {"title": "Stage line", "status": "running", "worker": "Stage (this session)", "roles": {"Stage (this session)": "RA"}}
greet = {"title": SLOW, "workdir": str(self.outside), "description": "zählt", "check": "manual", "status": "queued", "roles": {"Operator": "R", "Skills session": "A"}}
greet = {"title": SLOW, "workdir": str(self.outside), "description": "counts", "check": "manual", "status": "queued", "roles": {"Operator": "R", "Skills session": "A"}}
push = {"title": "git push", "workdir": "/repo", "description": "ship", "check": "manual", "status": "queued", "roles": {"Operator": "R", "Skills session": "A"}}
self.write_council({"rasci.json": {"work/stage": stage, "operator/greet": greet, "operator/push": push}})
arguments = {"kind": "ask", "tool": "Bash", "pattern": "make deploy", "scope": "/repo*", "expires": int(time.time()) + 7 * 86400, "agent": "Skills", "why": "deploy runs after every green gate"}
@@ -1079,11 +1079,11 @@ class TestOrchestratorE2E(HubCase):
def layout(self, probe: dict) -> None:
self.assertNotIn("error", probe)
self.assertEqual(sorted(probe["kinds"]), ["Ask", "Operator", "Operator", "Regelwunsch", "Review"])
self.assertIn(["Ablehnen", "Ablehnen über …"], probe["splits"])
self.assertEqual(sorted(probe["kinds"]), ["Ask", "Operator", "Operator", "Review", "Rule request"])
self.assertIn(["Deny", "Deny via …"], probe["splits"])
self.assertEqual(len(probe["splits"]), 4)
self.assertEqual(probe["fields"], ["Art", "Werkzeug (Glob)", "Muster (Glob)", "Verzeichnis (Glob)", "Läuft ab am"])
self.assertEqual(probe["dry"], "Trockenlauf: Die Regel hätte 1 von 1 Asks der letzten 7 Tage freigegeben.")
self.assertEqual(probe["fields"], ["Kind", "Tool (glob)", "Pattern (glob)", "Directory (glob)", "Expires on"])
self.assertEqual(probe["dry"], "Dry run: the rule would have granted 1 of 1 asks from the last 7 days.")
self.assertEqual(probe["accept"], 1)
self.assertTrue(probe["fits"] and probe["followed"], probe)
self.assertEqual((probe["unlabelled"], probe["wide"]), ([], False))
@@ -1120,32 +1120,32 @@ class TestOrchestratorE2E(HubCase):
self.assertEqual(data["errors"], [])
started, held, done, *rest = data["results"]
self.assertNotIn("error", started)
self.assertIn("wird in", started["toast"])
self.assertTrue(started["running"].startswith("läuft"), started)
self.assertIn("takes effect in", started["toast"])
self.assertTrue(started["running"].startswith("running"), started)
self.assertTrue(started["overflows"] and started["followed"], started)
self.assertLess(held["held"], 50, held)
self.assertEqual((done["last"], done["lines"], done["followed"]), ("fertig", 81, True))
self.assertRegex(done["done"], r"^erledigtExit-Code 0 nach \d+ s$")
self.assertIn("Schließen", done["actions"])
self.assertEqual((done["last"], done["lines"], done["followed"]), ("finished", 81, True))
self.assertRegex(done["done"], r"^doneExit code 0 after \d+ s$")
self.assertIn("Close", done["actions"])
layouts, sheets = rest[0:6:2], rest[1:6:2]
for probe, sheet, (width, _) in zip(layouts, sheets, WIDTHS, strict=True):
with self.subTest(width=width):
self.layout(probe)
self.assertTrue(probe["console"].startswith("erledigtExit-Code 0"), probe)
self.assertEqual(sheet["title"], "Ablehnen")
self.assertTrue(probe["console"].startswith("doneExit code 0"), probe)
self.assertEqual(sheet["title"], "Deny")
self.assertEqual(sheet["channels"], [["post", True], ["call", False], ["newsletter", False], ["broadcast", False], ["pan-pan", False], ["security", False]])
self.assertEqual(sheet["hints"][:3], ["an Skills, sobald sein Zug endet", "an Skills, sofort, weckt ihn", "an alle, sobald ihr Zug endet"])
self.assertEqual(sheet["hints"][:3], ["to Skills, when its turn ends", "to Skills, at once, wakes it", "to all, when their turn ends"])
self.assertEqual((sheet["reason"], sheet["unlabelled"], sheet["wide"]), (True, [], False))
denied, gone, accepted = rest[6:]
self.assertTrue(denied["undo"].startswith("Ablehnung (Call): Bash: make deploy"), denied)
self.assertTrue(denied["undo"].startswith("Denial (Call): Bash: make deploy"), denied)
self.assertFalse(denied["sheet"])
self.assertTrue(gone["gone"])
self.assertEqual((accepted["toast"], accepted["left"]), ("Angelegt: Regel: ask Bash make deploy in /repo*", False))
self.assertEqual((accepted["toast"], accepted["left"]), ("Created: Rule: ask Bash make deploy in /repo*", False))
ask = self.httpd.hub.asks.listed()[0]
self.assertEqual((ask["status"], ask["reason"], ask["channel"]), ("denied", "erst das Gate grün machen", "call"))
self.assertEqual((ask["status"], ask["reason"], ask["channel"]), ("denied", "make the gate green first", "call"))
calls = [args for script, args in self.scripts.calls if script.endswith("radio.py")]
self.assertEqual(calls[-1][:4], ["send", "call", "--to", "Skills session"])
self.assertTrue(calls[-1][-1].endswith("Reason: erst das Gate grün machen"), calls[-1])
self.assertTrue(calls[-1][-1].endswith("Reason: make the gate green first"), calls[-1])
self.assertEqual([(rule["kind"], rule["pattern"], rule["proof"] is not None) for rule in self.httpd.hub.rules.listed()], [("ask", "make deploy", True)])
self.assertEqual([(job["status"], job["code"]) for job in self.httpd.hub.jobs.listed()], [("done", 0)])
@@ -1154,7 +1154,7 @@ class TestOrchestratorE2E(HubCase):
self.inbox_work()
now = time.time()
fields = {"task": "operator/greet", "command": SLOW, "directory": str(self.outside), "sha256": "", "status": "done", "proof": None}
output = "".join(f"Zeile {number}\n" for number in range(1, 81)) + "fertig\n"
output = "".join(f"Row {number}\n" for number in range(1, 81)) + "finished\n"
self.httpd.hub.jobs.add({**fields, "code": 0, "output": output, "created_ts": now - 20, "started": now - 19, "finished": now - 10, "dismissed": False})
shots = [(width, height, OUT / f"firefox-{width}-inbox-console.png") for width, height in WIDTHS]
steps = [{"width": width, "height": height, "url": f"{self.base}#stream/inbox", "wait": 1200, "probe": LAYOUT, "shot": str(shot)} for width, height, shot in shots]
@@ -1164,7 +1164,7 @@ class TestOrchestratorE2E(HubCase):
for probe, (width, _, shot) in zip(data["results"], shots, strict=True):
with self.subTest(width=width):
self.layout(probe)
self.assertEqual(probe["console"], "erledigtExit-Code 0 nach 9 s")
self.assertEqual(probe["console"], "doneExit code 0 after 9 s")
self.assertTrue(shot.is_file() and shot.stat().st_size > 1000, shot)
def test_chromium_pins_a_task_priority_after_signature_and_the_runner_applies_it(self):
@@ -1178,7 +1178,7 @@ class TestOrchestratorE2E(HubCase):
pinned = data["results"][0]
self.assertNotIn("error", pinned)
self.assertEqual((pinned["title"], pinned["preview"][0]), ("work/docs", "rasci.py set work/docs --prio 2"))
self.assertEqual(pinned["toasts"], ["Priorität work/docs: an den Runner übergebenrasci.py set work/docs --prio 2"])
self.assertEqual(pinned["toasts"], ["Priority work/docs: handed to the runnerrasci.py set work/docs --prio 2"])
jobs = self.runner_jobs()
self.assertEqual([(job["id"], job["kind"], job["argv"]) for job in jobs], [(pinned["jobs"], "task", ["set", "work/docs", "--prio", "2"])])
self.run_runner()
+3 -3
View File
@@ -59,9 +59,9 @@ HOOK = {
OPTIONS = [{"label": "Merge now", "description": "Gate is green"}, {"label": "Wait", "description": ""}]
AMBIENCE = HUB / "templates" / "ambience.js"
STREAMS = [
{"name": "Morgen", "url": "https://radio.example/morgen", "from": "07:00", "to": "09:00", "days": [0, 1, 2, 3, 4], "on": True, "via": []},
{"name": "Nacht", "url": "http://10.0.0.5:8000/nacht", "from": "22:00", "to": "02:00", "days": [5], "on": True, "via": []},
{"name": "Aus", "url": "https://other.example:8443/", "from": "00:00", "to": "24:00", "days": [0, 1, 2, 3, 4, 5, 6], "on": False, "via": []},
{"name": "Morning", "url": "https://radio.example/morning", "from": "07:00", "to": "09:00", "days": [0, 1, 2, 3, 4], "on": True, "via": []},
{"name": "Night", "url": "http://10.0.0.5:8000/night", "from": "22:00", "to": "02:00", "days": [5], "on": True, "via": []},
{"name": "Off", "url": "https://other.example:8443/", "from": "00:00", "to": "24:00", "days": [0, 1, 2, 3, 4, 5, 6], "on": False, "via": []},
]
+10 -10
View File
@@ -34,28 +34,28 @@ class TestCoach(unittest.TestCase):
return {row["area"]: row for row in json.loads(result.stdout)}
def test_a_dislike_replaces_a_like_and_forget_drops_it(self):
self._run("like", "body", "Laufen")
self._run("like", "body", "Laufen")
self._run("like", "body", "Running")
self._run("like", "body", "Running")
self._run("like", "body", "Yoga")
self._run("dislike", "body", "Laufen")
self._run("dislike", "body", "Running")
body = self._show("2026-11-03T18:00:00+00:00")["body"]
self.assertEqual(body["likes"], ["Yoga"])
self.assertEqual(body["dislikes"], ["Laufen"])
self._run("forget", "body", "Laufen")
self.assertEqual(body["dislikes"], ["Running"])
self._run("forget", "body", "Running")
self.assertEqual(self._show("2026-11-03T18:00:00+00:00")["body"]["dislikes"], [])
def test_the_latest_done_per_area_gives_the_hours_since(self):
self._run("done", "food", "Frühstück", "--at", "2026-11-03T08:00:00+00:00")
self._run("done", "food", "Mittagessen", "--at", "2026-11-03T12:30:00+00:00")
self._run("done", "sleep", "Nacht", "--at", "2026-11-02T23:00:00+00:00")
self._run("done", "food", "Breakfast", "--at", "2026-11-03T08:00:00+00:00")
self._run("done", "food", "Lunch", "--at", "2026-11-03T12:30:00+00:00")
self._run("done", "sleep", "Night", "--at", "2026-11-02T23:00:00+00:00")
rows = self._show("2026-11-03T18:00:00+00:00")
self.assertEqual((rows["food"]["last"], rows["food"]["hours_ago"]), ("Mittagessen", 5.5))
self.assertEqual((rows["food"]["last"], rows["food"]["hours_ago"]), ("Lunch", 5.5))
self.assertEqual(rows["sleep"]["hours_ago"], 19.0)
self.assertIsNone(rows["psyche"]["last"])
def test_invalid_input_exits_2_and_keeps_the_profile(self):
self.assertEqual(self._run("like", "work", "x").returncode, 2)
self.assertEqual(self._run("done", "body", "x", "--at", "gestern").returncode, 2)
self.assertEqual(self._run("done", "body", "x", "--at", "yesterday").returncode, 2)
self.assertFalse(self.profile.exists())
self.profile.parent.mkdir(parents=True)
self.profile.write_text("{broken", encoding="utf-8")
+64
View File
@@ -0,0 +1,64 @@
"""Pin the repository to English: no German text outside the operator's quoted trigger phrases."""
from __future__ import annotations
import re
import subprocess
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
WORDS = (
"und", "nicht", "oder", "wird", "werden", "wurde", "kein", "keine", "keinen", "eine", "einen", "einem", "einer",
"ist", "sind", "auf", "für", "bitte", "noch", "schon", "alle", "aus", "zum", "zur", "bei", "beim", "nach", "über",
"dieser", "diese", "dieses", "jetzt", "heißt", "sich", "auch", "wenn", "dann", "mehr", "kann", "muss", "soll",
"gibt", "ohne", "vom", "von", "läuft", "wählen", "speichern", "löschen", "abbrechen", "schließen", "bearbeiten",
"anzeigen", "öffnen", "zeigen", "aufnehmen", "vorlesen", "regeln", "regel", "regelwunsch", "steuerung",
"ausführen", "ressourcen", "hierarchie", "freigabe", "freigeben", "ablehnen", "warten", "wartet", "offen",
"erledigt", "neu", "neuer", "angenommen", "abgelehnt", "gestoppt", "abgelaufen", "unbekannt",
"fehlgeschlagen", "verweigert", "besetzt", "abberufen", "aufgabe", "aufgaben", "entfernt", "entfernen", "gemergt",
"bleibt", "stoppt", "erzwingt", "verwirft", "hintergrund", "kategorien", "ansicht", "quelle", "namen", "suchen",
"anheften", "entscheiden", "entscheidungen", "nacht", "abend", "himmel", "sonnenuntergang", "sonnenaufgang",
"aufnahme", "nachricht", "nachrichten", "einstellungen", "sprache", "stumm", "fertig", "fehler", "gesperrt",
"signiert", "heute", "gestern", "stunden", "minuten", "sekunden", "warnung", "hinweis", "sofort", "zeit",
"weiter", "kopieren", "kopiert", "starten", "gestartet", "beenden", "beendet",
)
GERMAN = re.compile(r"[äöüÄÖÜß]|\b(?:" + "|".join(WORDS) + r")\b", re.IGNORECASE)
ALLOWED = (
'"rechtlich prüfen"', '"lizenz prüfen"', '"aufräumen"', '"branches aufräumen"', '"worktrees aufräumen"',
'"was soll ich tun"', '"was muss ich tun"', '"was fehlt noch"', '"welche skills gibt es"', '"skill übersicht"',
'"übersicht"', '"zuständigkeiten"', '"verwaiste branches"', '"alte worktrees"', '"was kann weg"', '"ä"',
"(Regeln → Firewall, Hierarchie → Hierarchy, Ressourcen → Resources,", "Steuerung → Controls, Ausführen → Run)",
)
SELF = Path(__file__).resolve().relative_to(REPO_ROOT).as_posix()
def german_lines() -> list[str]:
"""Returns:
Every `path:line` of a tracked text file that carries German once the
allowed trigger phrases are removed from it.
"""
files = subprocess.run(["git", "ls-files"], cwd=REPO_ROOT, capture_output=True, text=True, check=True).stdout.split()
hits = []
for name in files:
if name == SELF:
continue
try:
text = (REPO_ROOT / name).read_text(encoding="utf-8")
except (UnicodeDecodeError, IsADirectoryError, FileNotFoundError):
continue
for number, line in enumerate(text.splitlines(), 1):
for phrase in ALLOWED:
line = line.replace(phrase, "")
if GERMAN.search(line):
hits.append(f"{name}:{number}")
return hits
class TestEnglish(unittest.TestCase):
def test_no_german_outside_quoted_trigger_phrases(self):
self.assertEqual(german_lines(), [])
if __name__ == "__main__":
print("\n".join(german_lines()))
+1 -1
View File
@@ -6,7 +6,7 @@ import unittest
from hub_support import AMBIENCE, STREAMS
SLOTS = [(0, 420, "Morgen"), (0, 539, "Morgen"), (0, 540, None), (1, 419, None), (5, 480, None), (5, 1380, "Nacht"), (6, 90, "Nacht"), (6, 120, None), (4, 1380, None), (6, 1380, None)]
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);
+2 -2
View File
@@ -344,9 +344,9 @@ class TestPasskeyPage(unittest.TestCase):
def test_the_section_and_a_failed_registration_name_the_keepassxc_localhost_setting(self):
done = subprocess.run(["node", "-e", PASSKEY_PROBE, str(PASSKEY_PAGE)], capture_output=True, text=True, timeout=60, check=True)
shown = json.loads(done.stdout)
self.assertIn("nicht registriert: NotAllowedError", shown["failed"])
self.assertIn("not registered: NotAllowedError", shown["failed"])
for where in ("section", "failed"):
for text in ("Nutzung von localhost mit Passkeys zulassen", "Allow using localhost with passkeys", "Passkeys request canceled.", "entsperrt"):
for text in ("Allow using localhost with passkeys", "Passkeys request canceled.", "unlocked"):
with self.subTest(where=where, text=text):
self.assertIn(text, shown[where])
+5 -5
View File
@@ -124,16 +124,16 @@ class TestHubBoard(HubCase):
for action, other in (("clear-mayday", "sunrise"), ("evening", "sunset"), ("sunset", "evening"), ("sunrise", "clear-mayday")):
with self.subTest(action=action):
self.assertEqual(self.post_json(f"/api/board/{action}", {}, good)[0], 400)
self.assertEqual(self.post_json(f"/api/board/{action}", {"proof": None}, good)[:3:2], (400, b"nicht signiert: no signature\n"))
self.assertEqual(self.post_json(f"/api/board/{action}", {"proof": None}, good)[:3:2], (400, b"not signed: no signature\n"))
stranger = key(-7, "stranger").proof(self.board_proof(action, signer)["payload"], self.origin)
self.assertEqual(self.post_json(f"/api/board/{action}", {"proof": stranger}, good)[:3:2], (400, b"nicht signiert: the signing key is not pinned\n"))
self.assertEqual(self.post_json(f"/api/board/{action}", {"proof": stranger}, good)[:3:2], (400, b"not signed: the signing key is not pinned\n"))
proof = self.board_proof(action, signer)
self.assertEqual(self.post_json(f"/api/board/{other}", {"proof": proof}, good)[:3:2], (400, b"nicht signiert: the signature does not list this item\n"))
self.assertEqual(self.post_json(f"/api/board/{other}", {"proof": proof}, good)[:3:2], (400, b"not signed: the signature does not list this item\n"))
self.assertEqual(self.scripts.calls, [])
proof = self.board_proof("clear-mayday", signer)
self.assertEqual(self.post_json("/api/board/clear-mayday", {"proof": proof}, good)[0], 200)
self.assertEqual(self.scripts.calls, [(skill_scripts.RADIO, ["clear", "--force", "--session", "Operator"])])
self.assertEqual(self.post_json("/api/board/clear-mayday", {"proof": proof}, good)[:3:2], (400, b"nicht signiert: this signature was already used for this item\n"))
self.assertEqual(self.post_json("/api/board/clear-mayday", {"proof": proof}, good)[:3:2], (400, b"not signed: this signature was already used for this item\n"))
for payload in ({"action": "order"}, {"action": "broadcast"}, {"action": "reboot"}, {"action": 1}, {}, {"action": "evening", "extra": 1}):
with self.subTest(payload=payload):
self.assertEqual(self.post_json("/api/board/sign", payload, good)[0], 400)
@@ -189,7 +189,7 @@ class TestHubBoard(HubCase):
{"name": "snooze", "arguments": {"ids": [ask_id], "hours": 500}},
{"name": "view", "arguments": {"name": "funk", "extra": 1}},
]}))
status, _, body = self.post_json("/api/nl", {"text": "gib alles Grüne frei"}, self.guard(self.host))
status, _, body = self.post_json("/api/nl", {"text": "approve everything green"}, self.guard(self.host))
self.assertEqual(status, 200, body)
result = json.loads(body)
self.assertEqual([action["name"] for action in result["actions"]], ["approve", "choose", "message", "view", "snooze"])
+7 -7
View File
@@ -62,12 +62,12 @@ class TestHubCleanup(HubCase):
done = branches["feature/done"]
self.assertEqual(done["cleanup"], item)
self.assertEqual(done["plan"], [
"feature/done @ 5b1e6fd3a7c0: merged-clean, gemergt durch #12",
f"entfernt den Worktree {item['worktree']}",
"löscht den lokalen Branch feature/done",
"bleibt: remote-branch (nur lokal)",
"stoppt den Compose-Stack demo-feature-done ohne -v",
"entfernt das Image demo:feature-done",
"feature/done @ 5b1e6fd3a7c0: merged-clean, merged by #12",
f"removes the worktree {item['worktree']}",
"deletes the local branch feature/done",
"stays: remote-branch (local only)",
"stops the Compose stack demo-feature-done without -v",
"removes the image demo:feature-done",
])
self.assertEqual((done["merged_by"], done["merged_into"], done["gone"]), ("#12", "main", True))
self.assertEqual({name: entry["cleanup"] for name, entry in branches.items() if name != "feature/done"}, {"feature": None, "feature/kept": None, "main": None})
@@ -76,7 +76,7 @@ class TestHubCleanup(HubCase):
def test_signing_a_delete_needs_delete_ready_and_exactly_an_offered_item(self):
item = self.ready(False)
status, body = self.sign([item])
self.assertEqual((status, body.decode()), (400, "löschen gesperrt: Secret Service locked\n"))
self.assertEqual((status, body.decode()), (400, "delete blocked: Secret Service locked\n"))
item = self.ready(True)
kept = {**item, "branch": "feature/kept", "worktree": "", "tip": "0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d"}
for name, items in (("changed tip", [{**item, "tip": OTHER_TIP}]), ("skip reason", [kept]), ("pattern", [{**item, "branch": "feature/*"}]), ("twice", [item, item]), ("extra key", [{**item, "command": "rm -rf /"}])):
+17 -17
View File
@@ -115,21 +115,21 @@ class TestHubComposer(HubCase):
def test_an_operator_message_needs_the_passkey_and_carries_its_recording(self):
self.claim("s1", "Skills session")
voice = self.note(self.clip(b"\x01\x00"))
payload = {"to": "Skills", "text": " bitte rebasen ", "upload": "", "class": "call", "voice": voice}
payload = {"to": "Skills", "text": " please rebase ", "upload": "", "class": "call", "voice": voice}
status, _, body = self.post_json("/api/messages/sign", payload, self.guard(self.host))
self.assertEqual(status, 200, body)
self.assertEqual(json.loads(body)["credentials"], [])
unpinned = key(-7, "stranger").proof(json.loads(body)["payload"], self.origin)
self.assertEqual(self.post_json("/api/messages", {**payload, "proof": unpinned}, self.guard(self.host))[:3:2], (400, b"nicht signiert: no key is pinned\n"))
self.assertEqual(self.post_json("/api/messages", {**payload, "proof": unpinned}, self.guard(self.host))[:3:2], (400, b"not signed: no key is pinned\n"))
signer = self.pin()
proof = self.message_proof(payload, signer)
for changed, reason in (
({"proof": None}, b"nicht signiert: no signature\n"),
({"proof": key(-7, "stranger").proof(proof["payload"], self.origin)}, b"nicht signiert: the signing key is not pinned\n"),
({"proof": None}, b"not signed: no signature\n"),
({"proof": key(-7, "stranger").proof(proof["payload"], self.origin)}, b"not signed: the signing key is not pinned\n"),
({"proof": signer.proof(proof["payload"], "http://127.0.0.1:1")}, None),
({"text": "bitte mergen", "proof": proof}, b"nicht signiert: the signature does not list this item\n"),
({"class": "post", "proof": proof}, b"nicht signiert: the signature does not list this item\n"),
({"voice": "", "proof": proof}, b"nicht signiert: the signature does not list this item\n"),
({"text": "please merge", "proof": proof}, b"not signed: the signature does not list this item\n"),
({"class": "post", "proof": proof}, b"not signed: the signature does not list this item\n"),
({"voice": "", "proof": proof}, b"not signed: the signature does not list this item\n"),
):
with self.subTest(changed=list(changed)):
status, _, answer = self.post_json("/api/messages", {**payload, **changed}, self.guard(self.host))
@@ -141,10 +141,10 @@ class TestHubComposer(HubCase):
self.assertEqual(status, 201, body)
sent = json.loads(body)
recording = f"{HOST_STATE}/voice/{voice}.wav"
self.assertEqual(self.scripts.calls, [(skill_scripts.RADIO, ["send", "call", "--to", "Skills session", "--session", "Operator", f"[kvb-orchestrator msg {sent['id']}] bitte rebasen voice: {recording}"])])
self.assertEqual(self.post_json("/api/messages", {**payload, "proof": proof}, self.guard(self.host))[:3:2], (400, b"nicht signiert: this signature was already used for this item\n"))
verified = self.check(sent["id"], "bitte rebasen", "Skills session")["structuredContent"]
self.assertEqual((verified["match"], verified["text"], verified["class"], verified["voice"]), (True, "bitte rebasen", "call", recording))
self.assertEqual(self.scripts.calls, [(skill_scripts.RADIO, ["send", "call", "--to", "Skills session", "--session", "Operator", f"[kvb-orchestrator msg {sent['id']}] please rebase voice: {recording}"])])
self.assertEqual(self.post_json("/api/messages", {**payload, "proof": proof}, self.guard(self.host))[:3:2], (400, b"not signed: this signature was already used for this item\n"))
verified = self.check(sent["id"], "please rebase", "Skills session")["structuredContent"]
self.assertEqual((verified["match"], verified["text"], verified["class"], verified["voice"]), (True, "please rebase", "call", recording))
row = self.snapshot()["messages"][-1]
self.assertEqual((row["id"], row["class"], row["voice"], row["signed"]), (sent["id"], "call", voice, True))
self.assertEqual(len(self.scripts.calls), 1)
@@ -176,18 +176,18 @@ class TestHubComposer(HubCase):
self.claim("s1", "Skills session")
signer = self.pin()
voice = self.note(self.clip(b"\x01\x00"))
status, _, body = self.say({"to": "Skills", "text": "bitte\n rebasen", "class": "call", "voice": voice}, signer)
status, _, body = self.say({"to": "Skills", "text": "please\n rebase", "class": "call", "voice": voice}, signer)
self.assertEqual(status, 201, body)
sent = json.loads(body)["id"]
row = self.scripts.calls[-1][1][-1].split("] ", 1)[1]
for text, to in (("bitte rebasen", "Skills"), ("bitte rebasen", "Skills session"), (row, "Skills")):
for text, to in (("please rebase", "Skills"), ("please rebase", "Skills session"), (row, "Skills")):
with self.subTest(text=text, to=to):
result = self.check(sent, text, to)
self.assertFalse(result["isError"], result)
found = result["structuredContent"]
self.assertEqual((found["match"], found["text"], found["to"], found["class"]), (True, "bitte\n rebasen", "Skills", "call"))
self.assertEqual((found["match"], found["text"], found["to"], found["class"]), (True, "please\n rebase", "Skills", "call"))
self.assertEqual(found["voice"], f"{HOST_STATE}/voice/{voice}.wav")
for text, to in (("bitte mergen", "Skills"), ("bitte rebasen", "Stage"), ("bitte rebasen", "*"), ("bitte", "Skills")):
for text, to in (("please merge", "Skills"), ("please rebase", "Stage"), ("please rebase", "*"), ("please", "Skills")):
with self.subTest(text=text, to=to):
result = self.check(sent, text, to)
self.assertTrue(result["isError"], result)
@@ -256,7 +256,7 @@ class TestHubComposer(HubCase):
payload = {"to": "Skills", "text": "hear this", "upload": "", "class": "call", "voice": voice}
proof = self.message_proof(payload, signer)
wav.write_bytes(self.clip(b"\x02\x00"))
self.assertEqual(self.post_json("/api/messages", {**payload, "proof": proof}, self.guard(self.host))[:3:2], (400, b"nicht signiert: the signature does not list this item\n"))
self.assertEqual(self.post_json("/api/messages", {**payload, "proof": proof}, self.guard(self.host))[:3:2], (400, b"not signed: the signature does not list this item\n"))
self.assertEqual(self.scripts.calls, [])
wav.write_bytes(original)
status, _, body = self.post_json("/api/messages", {**payload, "proof": proof}, self.guard(self.host))
@@ -273,7 +273,7 @@ class TestHubComposer(HubCase):
with mock.patch.object(signed.passkey, "serialization", None), mock.patch.object(tempfile, "tempdir", str(missing)):
status, _, body = self.post_json("/api/messages", {**payload, "proof": proof}, self.guard(self.host))
self.assertEqual(status, 400, body)
self.assertTrue(body.startswith(b"nicht signiert: "), body)
self.assertTrue(body.startswith(b"not signed: "), body)
self.assertEqual(self.scripts.calls, [])
+1 -1
View File
@@ -86,7 +86,7 @@ class TestHubConsole(HubCase):
def test_the_page_draws_the_console_as_text_and_follows_its_end(self):
html = self.index()
for text in ('"/api/inbox/dismiss"', "console-out", "wartet auf den Runner", "Exit-Code", "textContent", "scrollHeight"):
for text in ('"/api/inbox/dismiss"', "console-out", "waiting for the runner", "Exit code", "textContent", "scrollHeight"):
with self.subTest(text=text):
self.assertIn(text, html)
for name in ("page-console.js", "page-deny.js", "page-rule-request.js"):
+5 -5
View File
@@ -80,9 +80,9 @@ class TestHubDeny(HubCase):
denial = self.httpd.hub.messages.get(message_id)["denial"]
self.httpd.hub.messages.change(message_id, {"denial": {**denial, "reason": "push anyway"}})
self.assertTrue(self.tool("operator_message", {"id": message_id, "text": told, "to": "Skills session"})["isError"])
logged = [row for row in self.events("kinds=approval")["events"] if row["title"].startswith("Ablehnung ask")]
logged = [row for row in self.events("kinds=approval")["events"] if row["title"].startswith("Denial ask")]
self.assertEqual(len(logged), 1)
self.assertIn("call an Skills session", logged[0]["text"])
self.assertIn("call to Skills session", logged[0]["text"])
self.assertNotIn(ask_id, [item["id"] for item in self.snapshot()["inbox"]])
def test_every_class_but_mayday_reaches_the_agent_or_all(self):
@@ -121,7 +121,7 @@ class TestHubDeny(HubCase):
self.assertTrue(self.radio()[0][-1].endswith("Denied by the operator: operator command git push in /repo (from Skills). Reason: push after the review"), self.radio()[0])
self.assertEqual(len(self.radio()), 1)
texts = [row["text"] for row in self.events("kinds=approval")["events"]]
self.assertTrue(any("kein Empfänger" in text for text in texts), texts)
self.assertTrue(any("no recipient" in text for text in texts), texts)
def test_a_forged_denial_is_refused_before_anything_is_stored_or_sent(self):
self.claim("s1", "Skills session")
@@ -134,7 +134,7 @@ class TestHubDeny(HubCase):
with self.subTest(name=name), mock.patch.object(batches, "UNDO", 0.05):
status, _, body = self.approve(entries, proof)
self.assertEqual(status, 400, body)
self.assertIn("nicht signiert", body.decode())
self.assertIn("not signed", body.decode())
self.assertFalse(self.httpd.hub.batches.holds("ask", ask_id))
time.sleep(0.2)
self.assertEqual((self.ask_status(ask_id, "0"), self.radio()), ("pending", []))
@@ -182,7 +182,7 @@ class TestHubDeny(HubCase):
self.assertEqual(self.prepare([entry])["payload"]["items"], [signing.item_hash(signing.denial_item(subject, "call", "not now", ""))])
with mock.patch.object(batches, "UNDO", 0.05):
self.assertEqual(self.approve([entry], self.signed([entry], self.pin()))[0], 202)
self.settle(lambda: any("kein Empfänger" in row["text"] for row in self.events("kinds=approval")["events"]))
self.settle(lambda: any("no recipient" in row["text"] for row in self.events("kinds=approval")["events"]))
self.assertEqual((self.ask_status(ask_id, "0"), self.radio()), ("denied", []))
def answer(self, signer: Key, item: dict, channel: str, reason: str) -> dict:
+1 -1
View File
@@ -81,7 +81,7 @@ class TestHubGraph(HubCase):
("rename", "s1", "IAM", "IAM"),
("radio", "s1", "IAM", "IAM"),
])
self.assertEqual(rows[1]["title"], "Skills heißt jetzt IAM")
self.assertEqual(rows[1]["title"], "Skills is now called IAM")
self.assertAlmostEqual(rows[1]["ts"], now - 50, places=3)
for name in ("Skills", "IAM"):
with self.subTest(name=name):
+4 -4
View File
@@ -20,7 +20,7 @@ class TestHubInbox(HubCase):
decision_id = self.decision("Merge now")
inbox = {item["id"]: item for item in self.snapshot()["inbox"]}
self.assertEqual([inbox[key]["green"] for key in (green, red, ask_id, risky, decision_id)], [True, False, True, False, True])
self.assertEqual(inbox[risky]["warnings"], ["löscht rekursiv"])
self.assertEqual(inbox[risky]["warnings"], ["deletes recursively"])
batch = [self.entry("review", green, "", ""), self.entry("ask", ask_id, "", ""), self.entry("decision", decision_id, "Merge now", "")]
signer = key(-7, "main")
self.assertEqual(self.approve([self.entry("review", green, "", ""), self.entry("review", red, "", "")], None)[0], 400)
@@ -77,8 +77,8 @@ class TestHubInbox(HubCase):
with self.subTest(payload=payload):
self.assertEqual(self.post_json("/api/inbox/mark", payload, self.guard(self.host))[0], 400)
titles = [row["title"] for row in self.events("kinds=approval")["events"]]
self.assertIn("Freigabe review: feat: green", titles)
self.assertTrue(any(title.startswith("Rückgängig: ") for title in titles))
self.assertIn("Grant review: feat: green", titles)
self.assertTrue(any(title.startswith("Undone: ") for title in titles))
def test_the_sign_endpoint_and_the_page_offer_the_pinned_keys_only(self):
ask_id = self.create_ask(ASK)
@@ -96,7 +96,7 @@ class TestHubInbox(HubCase):
self.approvers.chmod(0o666)
self.assertEqual(self.snapshot()["approvers"], [])
html = self.index()
for text in ("Passkey registrieren", "navigator.credentials.get", "navigator.credentials.create", "approval.py pin --port", "~/.config/kvb-orchestrator/approvers.json", "nicht signiert"):
for text in ("Register passkey", "navigator.credentials.get", "navigator.credentials.create", "approval.py pin --port", "~/.config/kvb-orchestrator/approvers.json", "not signed"):
with self.subTest(text=text):
self.assertIn(text, html)
+2 -2
View File
@@ -20,11 +20,11 @@ class TestRenameOnce(unittest.TestCase):
self.assertEqual(counts, [0, 0])
for build in (3000.0, 4000.0):
book.observe([*claims, ("s1", "Skills", 300.0, "title")], build)
self.assertEqual([row["title"] for row in names.Directory(book.listed()).renames(0, 5000)], ["i18n 2.0 heißt jetzt Skills"])
self.assertEqual([row["title"] for row in names.Directory(book.listed()).renames(0, 5000)], ["i18n 2.0 is now called Skills"])
def test_a_name_pair_shows_once_per_session(self):
entries = [{"session": "s1", "name": name, "first_seen": ts, "source": "claim"} for name, ts in (("A", 1), ("B", 2), ("A", 3), ("B", 4))]
self.assertEqual([row["title"] for row in names.Directory(entries).renames(0, 10)], ["A heißt jetzt B", "B heißt jetzt A"])
self.assertEqual([row["title"] for row in names.Directory(entries).renames(0, 10)], ["A is now called B", "B is now called A"])
if __name__ == "__main__":
+3 -3
View File
@@ -34,16 +34,16 @@ class TestNavModule(unittest.TestCase):
cls.data = json.loads(done.stdout)
def test_the_navigation_has_four_entries_and_stream_has_inbox_rules_and_radio(self):
stream = ["stream", "Stream", [["inbox", "Inbox"], ["rules", "Regeln"], ["radio", "Radio"]]]
stream = ["stream", "Stream", [["inbox", "Inbox"], ["rules", "Firewall"], ["radio", "Radio"]]]
self.assertEqual(self.data["tabs"], [stream, ["agents", "Agents", []], ["artifacts", "Artifacts", []], ["tasks", "Tasks", []]])
def test_agents_and_artifacts_offer_five_views_in_order_and_tasks_three(self):
shared = [["orbit", "Orbit"], ["chord", "Chord"], ["swimlane", "Swimlane"], ["tree", "Tree"], ["3d", "3D"]]
self.assertEqual((self.data["views"], self.data["artifacts"]), (shared, shared))
self.assertEqual((self.data["taskViews"], self.data["tasks"]), ([["list", "Liste"], ["board", "Board"], ["rasci", "RASCI"]],) * 2)
self.assertEqual((self.data["taskViews"], self.data["tasks"]), ([["list", "List"], ["board", "Board"], ["rasci", "RASCI"]],) * 2)
def test_only_agents_add_the_hierarchy_after_3d(self):
self.assertEqual(self.data["agents"][-2:], [["3d", "3D"], ["hierarchy", "Hierarchie"]])
self.assertEqual(self.data["agents"][-2:], [["3d", "3D"], ["hierarchy", "Hierarchy"]])
self.assertEqual(self.data["icon"], "bi-diagram-2")
def test_hashes_route_and_the_old_ones_redirect(self):
+2 -2
View File
@@ -8,8 +8,8 @@ from hub_support import AMBIENCE
from kvb_orchestrator.settings import stream_origin
DLF = "https://st01.sslstream.dlf.de/dlf/01/high/aac/stream.aac?aggregator=web"
OTHER = "https://radio.example/morgen"
URLS = [DLF, "https://Radio.Example:443/zweiter?x=1", "http://10.0.0.5:8000/nacht", "https://other.example:8443/", "http://radio.example:80/x"]
OTHER = "https://radio.example/morning"
URLS = [DLF, "https://Radio.Example:443/second?x=1", "http://10.0.0.5:8000/night", "https://other.example:8443/", "http://radio.example:80/x"]
PROBE = """
const A = require(process.argv[1]);
const [DLF, OTHER] = JSON.parse(process.argv[2]);
+4 -4
View File
@@ -221,7 +221,7 @@ class TestRedirects(unittest.TestCase):
def test_only_https_urls_are_probed(self):
before = len(cdn().dialed)
self.assertEqual(self.resolver().resolve(["http://10.0.0.5:8000/nacht", STATION]), {"http://10.0.0.5:8000/nacht": ([], ""), STATION: (["https://*.cdn.test"], "")})
self.assertEqual(self.resolver().resolve(["http://10.0.0.5:8000/night", STATION]), {"http://10.0.0.5:8000/night": ([], ""), STATION: (["https://*.cdn.test"], "")})
self.assertEqual(len(cdn().dialed) - before, 2)
def test_media_sources_widen_one_label_and_pass_the_settings_check(self):
@@ -284,9 +284,9 @@ class TestRedirectSettings(HubCase):
self.assertEqual(set(prepared), {"payload", "challenge", "credentials"})
self.assertEqual(prepared["payload"]["items"], [signing.item_hash(signing.stream_item(["https://attacker.radio.test"], [streams[0]["url"]]))])
unsigned = self.post_json("/api/settings", {**self.payload(streams), "proof": None}, self.guard(self.host))
self.assertEqual(unsigned[:3:2], (400, b"nicht signiert: no signature\n"))
self.assertEqual(unsigned[:3:2], (400, b"not signed: no signature\n"))
stranger = {**self.payload(streams), "proof": key(-7, "stranger").proof(prepared["payload"], self.origin)}
self.assertEqual(self.post_json("/api/settings", stranger, self.guard(self.host))[:3:2], (400, b"nicht signiert: the signing key is not pinned\n"))
self.assertEqual(self.post_json("/api/settings", stranger, self.guard(self.host))[:3:2], (400, b"not signed: the signing key is not pinned\n"))
self.assertEqual(self.dials(), before)
self.assertNotIn("attacker.radio.test", cdn().httpd.asked)
self.assertFalse((self.state / "settings.json").exists())
@@ -326,7 +326,7 @@ class TestRedirectSettings(HubCase):
self.assertEqual(self.prepared(streams)["payload"]["items"], [signing.item_hash(signing.stream_item(["https://station.radio.test"], [other["url"]]))])
before = self.dials()
unsigned = self.post_json("/api/settings", {**self.payload(streams), "proof": None}, self.guard(self.host))
self.assertEqual(unsigned[:3:2], (400, b"nicht signiert: no signature\n"))
self.assertEqual(unsigned[:3:2], (400, b"not signed: no signature\n"))
self.assertEqual(self.dials(), before)
self.assertEqual(self.httpd.hub.settings.get(), saved)
+1 -1
View File
@@ -111,7 +111,7 @@ class TestHubRuleRequests(HubCase):
with self.subTest(name=name):
status, _, body = self.post_json(f"/api/rule-requests/{request_id}/accept", {"rule": rule, "assertion": assertion}, self.guard(self.host))
self.assertEqual(status, 400, body)
self.assertIn("nicht signiert", body.decode())
self.assertIn("not signed", body.decode())
self.assertEqual((self.waited(request_id)["status"], self.httpd.hub.rules.listed()), ("pending", []))
self.assertIsNotNone(self.item(request_id))
now = int(time.time())
+7 -7
View File
@@ -35,27 +35,27 @@ class TestHubSettings(HubCase):
def test_a_new_stream_url_needs_the_passkey(self):
good = self.guard(self.host)
payload = self.settings_payload(STREAMS)
self.assertEqual(self.post_json("/api/settings", payload, good)[:3:2], (400, b"nicht signiert: no signature\n"))
self.assertEqual(self.post_json("/api/settings", payload, good)[:3:2], (400, b"not signed: no signature\n"))
self.assertFalse((self.state / "settings.json").exists())
signer = self.pin()
request = self.prepared(payload)["payload"]
stranger = key(-7, "stranger").proof(request, self.origin)
self.assertEqual(self.post_json("/api/settings", {**payload, "proof": stranger}, good)[:3:2], (400, b"nicht signiert: the signing key is not pinned\n"))
self.assertEqual(self.post_json("/api/settings", {**payload, "proof": stranger}, good)[:3:2], (400, b"not signed: the signing key is not pinned\n"))
self.assertEqual(self.media_src(), "media-src 'self'")
proof = signer.proof(request, self.origin)
status, _, body = self.post_json("/api/settings", {**payload, "proof": proof}, good)
self.assertEqual(status, 200, body)
self.assertEqual(self.media_src(), "media-src 'self' http://10.0.0.5:8000 https://other.example:8443 https://radio.example")
moved = self.settings_payload([{**STREAMS[0], "url": "https://radio.example/abend"}, STREAMS[1]])
self.assertEqual(self.post_json("/api/settings", moved, good)[:3:2], (400, b"nicht signiert: no signature\n"))
moved = self.settings_payload([{**STREAMS[0], "url": "https://radio.example/evening"}, STREAMS[1]])
self.assertEqual(self.post_json("/api/settings", moved, good)[:3:2], (400, b"not signed: no signature\n"))
status, _, body = self.save(moved, signer)
self.assertEqual(status, 200, body)
self.assertEqual(self.media_src(), "media-src 'self' http://10.0.0.5:8000 https://radio.example")
self.assertEqual(self.post_json("/api/settings", self.settings_payload([]), good)[0], 200)
self.assertEqual(self.post_json("/api/settings", {**payload, "proof": proof}, good)[:3:2], (400, b"nicht signiert: this signature was already used for this item\n"))
self.assertEqual(self.post_json("/api/settings", {**payload, "proof": proof}, good)[:3:2], (400, b"not signed: this signature was already used for this item\n"))
self.assertEqual(self.save(moved, signer)[0], 200)
evil = self.settings_payload([*moved["streams"], {**STREAMS[0], "url": "https://evil.example/listen"}])
self.assertEqual(self.post_json("/api/settings", {**evil, "proof": signer.proof(self.prepared(payload)["payload"], self.origin)}, good)[:3:2], (400, b"nicht signiert: the signature does not list this item\n"))
self.assertEqual(self.post_json("/api/settings", {**evil, "proof": signer.proof(self.prepared(payload)["payload"], self.origin)}, good)[:3:2], (400, b"not signed: the signature does not list this item\n"))
self.assertEqual(self.post_json("/api/settings", evil, good)[0], 400)
self.assertEqual(self.media_src(), "media-src 'self' http://10.0.0.5:8000 https://radio.example")
status, _, body = self.save(evil, signer)
@@ -163,7 +163,7 @@ class TestHubSettings(HubCase):
def test_csp_media_src_lists_exactly_the_stream_origins(self):
self.assertEqual(self.media_src(), "media-src 'self'")
streams = [*STREAMS, {**STREAMS[0], "name": "Gleich", "url": "https://Radio.Example:443/zweiter?x=1"}]
streams = [*STREAMS, {**STREAMS[0], "name": "Same", "url": "https://Radio.Example:443/second?x=1"}]
status, _, body = self.save(self.settings_payload(streams), self.pin())
self.assertEqual(status, 200, body)
origins = ["http://10.0.0.5:8000", "https://other.example:8443", "https://radio.example"]
+1 -1
View File
@@ -57,7 +57,7 @@ const ctx = Object.fromEntries(["clearRect", "beginPath", "moveTo", "lineTo", "q
calls.count += 1;
}]));
const colors = Object.fromEntries(["line", "accent", "muted", "violet", "direct", "broadcast", "alarm", "security", "grant", "panel", "text"].map((role) => [role, "#123456"]));
const style = {colors, font: "12px sans", strong: "bold 12px sans", level: "Tiefe", cursor: "Stand", selected: "s3", format: String};
const style = {colors, font: "12px sans", strong: "bold 12px sans", level: "Depth", cursor: "As of", selected: "s3", format: String};
const builds = Array.from({length: 15}, () => clock(() => S.scene(input))[0]);
let stats = null;
const paints = Array.from({length: 30}, (_, frame) => {
+13 -1
View File
@@ -216,7 +216,7 @@ class TestPhraseology(unittest.TestCase):
(("mayday", "Docs", [], "MAYDAY disk full"), "MAYDAY MAYDAY MAYDAY, this is Docs Docs Docs, disk full, over"),
(("mayday", "Docs", ["Skills"], "mayday, mayday: disk full"), "MAYDAY MAYDAY MAYDAY, this is Docs Docs Docs, disk full, over"),
(("pan-pan", "Docs", ["Skills"], "PAN-PAN: slow"), "PAN-PAN PAN-PAN PAN-PAN, all stations, this is Docs, slow, over"),
(("security", "Maintainer", ["Skills", "Docs"], "SECURITE rotate"), "SECURITE SECURITE SECURITE, all stations, this is Maintainer, rotate, over"),
(("security", "Maintainer", ["Skills", "Docs"], "SECURITE rotate"), "say-cure-ee-tay say-cure-ee-tay say-cure-ee-tay, all stations, this is Maintainer, rotate, over"),
(("call", "Skills", ["Maintainer"], "ready"), "Maintainer, Maintainer, this is Skills, ready, over"),
(("post", "Skills", ["Alpha", "Beta"], "ready"), "Alpha and Beta, Alpha and Beta, this is Skills, ready, over"),
(("call", "Docs", ["Ops"], "MAYDAY is only a word here"), "Ops, Ops, this is Docs, MAYDAY is only a word here, over"),
@@ -230,6 +230,18 @@ class TestPhraseology(unittest.TestCase):
with self.subTest(arguments=arguments):
self.assertEqual(phrasing.phrase(*arguments), expected)
def test_hashes_spell_in_nato_and_digits_in_icao_only_when_spoken(self):
cases = (
("8e180be", "eight echo one eight zero bravo echo"),
("42305", "four two tree zero fife"),
("fix 8E180BE, PR #1326", "fix eight echo one eight zero bravo echo, PR #one tree two six"),
("8e180b decade deadbeef", "eight e one eight zero b decade deadbeef"),
)
for text, expected in cases:
with self.subTest(text=text):
self.assertEqual(phrasing.phrase("broadcast", "Docs", [], text), f"all stations, this is Docs, {expected}, out")
self.assertEqual(phrasing.pronounced(phrasing.pronounced("8e180be 42305")), phrasing.pronounced("8e180be 42305"))
def test_a_phrase_fits_the_speech_limit_on_one_line(self):
for cls in council.CLASSES:
with self.subTest(cls=cls):
+1 -1
View File
@@ -162,7 +162,7 @@ class TestHubState(HubCase):
"all stations, this is Maintainer, board, out",
"MAYDAY MAYDAY MAYDAY, this is Docs Docs Docs, disk full, over",
"PAN-PAN PAN-PAN PAN-PAN, all stations, this is Docs, slow, over",
"SECURITE SECURITE SECURITE, all stations, this is Maintainer, rotate, over",
"say-cure-ee-tay say-cure-ee-tay say-cure-ee-tay, all stations, this is Maintainer, rotate, over",
])
speech = self.httpd.hub.speech
self.assertEqual([row["voice"] for row in state["radio"]], [speech.voice_of(row["source"]) for row in state["radio"]])
+2 -2
View File
@@ -149,10 +149,10 @@ class TestHubStream(HubCase):
"Cron: 7 * * * *": (True, "Skills", "iterate"),
"Wakeup: in 60 s": (False, "Skills", "/loop\nfirst"),
"Wakeup: in 1200 s": (True, "Skills", "/loop\nfallback"),
"Hintergrund: gate": (True, "Skills", "make test"),
"Background: gate": (True, "Skills", "make test"),
"Timer: *-*-* 08:00:00": (True, "", "Daily dream\nkvb-dream-notify.timer"),
"Timer: 1h": (False, "", "old.timer"),
"Queue: gate · läuft": (True, "Skills", "make test\nwork/hub"),
"Queue: gate · running": (True, "Skills", "make test\nwork/hub"),
})
self.write_council({"work/s1/claim.json": {"office": "work/s1", "session": "s1", "runtime": "claude", "name": "Skills session", "since": "2026-11-05T08:00:00+00:00", "beat": now - 3600, "work": "x"}})
self.assertEqual(crons()["Cron: 7 * * * *"][0], False)
+7 -7
View File
@@ -118,7 +118,7 @@ class TestHubSurface(HubCase):
self.assertEqual(status, 400)
def test_the_stream_has_four_categories_and_transcripts_stay_out_by_default(self):
self.assertEqual({key: value["label"] for key, value in events.VIEWS.items()}, {"fuer-dich": "Für dich", "funk": "Funk", "arbeit": "Arbeit", "transkripte": "Transkripte"})
self.assertEqual({key: value["label"] for key, value in events.VIEWS.items()}, {"fuer-dich": "For you", "funk": "Radio", "arbeit": "Work", "transkripte": "Transcripts"})
self.assertEqual(events.VIEWS["transkripte"]["kinds"], ["transcript"])
self.assertNotIn("transcript", events.VIEWS["fuer-dich"]["kinds"] + events.VIEWS["funk"]["kinds"] + events.VIEWS["arbeit"]["kinds"])
self.assertEqual(sorted(events.VIEWS["fuer-dich"]["kinds"]), sorted(events.VIEWS["funk"]["kinds"] + events.VIEWS["arbeit"]["kinds"]))
@@ -139,13 +139,13 @@ class TestHubSurface(HubCase):
self.assertEqual(texts("deploy"), ["radio:Docs:old deploy", "radio:Docs:fresh deploy", "radio:Ops:deploy done", "review:feat: deploy:feat: deploy"])
self.assertEqual(texts("agent:Docs"), ["radio:Docs:old deploy", "radio:Docs:fresh deploy"])
self.assertEqual(texts("art:funk deploy"), ["radio:Docs:old deploy", "radio:Docs:fresh deploy", "radio:Ops:deploy done"])
self.assertEqual(texts("art:review"), ["review:feat: deploy:feat: deploy"])
self.assertEqual(texts("seit:1h agent:Docs"), ["radio:Docs:fresh deploy"])
self.assertEqual(texts("seit:5m done"), ["radio:Ops:deploy done"])
self.assertEqual(texts("kind:radio deploy"), ["radio:Docs:old deploy", "radio:Docs:fresh deploy", "radio:Ops:deploy done"])
self.assertEqual(texts("kind:review"), ["review:feat: deploy:feat: deploy"])
self.assertEqual(texts("since:1h agent:Docs"), ["radio:Docs:fresh deploy"])
self.assertEqual(texts("since:5m done"), ["radio:Ops:deploy done"])
since = time.strftime("%Y-%m-%dT%H:%M", time.localtime(now - 600))
self.assertEqual(texts(f"seit:{since} art:funk"), ["radio:Docs:fresh deploy", "radio:Ops:deploy done"])
for q in ("art:bogus", "seit:soon", "seit:5y", "agent:"):
self.assertEqual(texts(f"since:{since} kind:Radio"), ["radio:Docs:fresh deploy", "radio:Ops:deploy done"])
for q in ("kind:bogus", "since:soon", "since:5y", "agent:"):
with self.subTest(q=q):
self.assertEqual(self.get(f"/api/events?q={quote(q)}")[0], 400)
+2 -2
View File
@@ -179,10 +179,10 @@ class TestHubTasks(HubCase):
forged = {**self.signed([change(PRIO)], self.pin()), "assertion": {}}
status, _, body = self.post_json("/api/tasks", {"items": [change(PRIO)], "proof": forged}, self.guard(self.host))
self.assertEqual(status, 400, body)
self.assertIn("nicht signiert", body.decode() if isinstance(body, bytes) else body)
self.assertIn("not signed", body.decode() if isinstance(body, bytes) else body)
snapshot = self.snapshot()
self.assertEqual([job for job in snapshot["jobs"] if job["kind"] == "task"], [])
self.assertNotIn("Aufgabe signiert geändert", json.dumps(snapshot, ensure_ascii=False))
self.assertNotIn("Task change signed", json.dumps(snapshot, ensure_ascii=False))
def test_a_task_the_hub_cannot_sign_whole_is_not_editable(self):
long_slug = "work/" + "x" * 600