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:
1 parent
a388c6ef77
commit
c035373518
86 files changed
+1027
-930
No files matched your search
+124
-124
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user