Files
skills/tests/e2e/test_orchestrator_e2e.py
T

2546 lines
167 KiB
Python
Raw Normal View History

from __future__ import annotations
import array
import base64
import contextlib
import http.server
import json
import math
import os
import shutil
import ssl
import subprocess
import sys
import tempfile
import threading
import time
import unittest
from collections.abc import Callable
from pathlib import Path
from unittest import mock
from urllib.parse import urlsplit
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
from hub_case import HubCase
from hub_engines import wav_bytes
from hub_support import ASK, HOST_STATE, OPTIONS, RUNNER, iso, verifier
from kvb_orchestrator import skill_scripts
from test_hub_cleanup import DONE_TIP, FAKE_PKGMGR, host
from test_hub_offices import COUNCIL_SCRIPT
from test_hub_offices import host as office_host
OUT = Path(os.environ.get("E2E_OUT", "/tmp/orchestrator-phase4-shots"))
WIDTHS = ((1920, 1080), (1366, 768), (390, 844))
VIEWS = ("orbit", "chord", "swimlane", "tree", "3d")
TASK_VIEWS = ("list", "board", "rasci", "gantt", "burn", "3d")
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", "Firewall", "Radio"]
TABS = ["Stream", "Agents", "Artifacts", "Tasks"]
HELPERS = """
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const until = async (check, ms) => {
const end = Date.now() + ms;
while (Date.now() < end) {
const found = check();
if (found) {
return found;
}
await sleep(200);
}
return null;
};
const all = (selector, root) => [...(root || document).querySelectorAll(selector)];
const text = (node) => (node ? node.textContent.trim() : null);
const name = (node) => (node ? node.getAttribute("aria-label") || text(node) : null);
const toolbarRows = () => {
const bar = document.querySelector('#main [role="toolbar"][aria-label="Tasks"]');
const boxes = bar ? [...bar.children].filter((node) => node.getClientRects().length).map((node) => node.getBoundingClientRect()).sort((a, b) => a.top - b.top) : [];
let [rows, bottom] = [0, -Infinity];
for (const box of boxes) {
if (box.top >= bottom - 1) {
rows += 1;
bottom = box.bottom;
} else {
bottom = Math.max(bottom, box.bottom);
}
}
return rows;
};
const taskLayout = () => all("#main .graph.tasks > *").map((node) => (node.matches('[role="toolbar"]') ? "toolbar" : node.matches('[aria-label="Task filters"]') ? "chips" : "body"));
"""
PROBE = """(async () => {@HELPERS@
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;
};
const icons = all("svg.bi").filter((svg) => !svg.closest(".sprite"));
const banner = document.getElementById("mayday");
return {
hash: location.hash,
title: document.title,
h1: text(document.querySelector("h1")),
header: all("header.top button").map((button) => button.id),
tabs: all("#tabs > .tab-group > button").map(name),
subs: all("#tabs .subtabs button").map(name),
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="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]")),
custom: Boolean(document.querySelector('#main details.menu input[type="datetime-local"]')),
toolbar: all('#main [role="toolbar"] > *').map((node) => name(node.tagName === "DETAILS" ? node.querySelector("summary") : node.querySelector("button") || node)),
spot: (() => {
const box = document.getElementById("radio-spot");
return box ? {hidden: box.hidden, channels: [...box.querySelector("form.composer select").options].map((option) => option.value)} : null;
})(),
composers: all("#main form.composer").map((form) => (form.closest("#radio-spot") ? "spot" : "other")),
inbox: all("#main .inbox-list > li").length,
icons: icons.length,
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="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")),
canvas: Boolean(document.querySelector("#main .graph3d canvas")),
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="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("")],
flows: all("#main .flow").map((flow) => flow.dataset.flow),
flowEdges: all("#main svg.chart path.flow").map((path) => path.dataset.side),
openWork: all("#main .open-work").length,
panel: all("#drawer, .drawer, #main aside, #main .panel, #main .graph-body").length,
nodeChips: all("#main .space-list, #main .chip[data-node]").length,
wide: document.documentElement.scrollWidth > window.innerWidth + 1,
tall: document.documentElement.scrollHeight > window.innerHeight,
};
})()""".replace("@HELPERS@", HELPERS)
ORG_OPEN = """(async () => {@HELPERS@
const row = await until(() => document.querySelector('#main .org-row[data-office="identity/oidc"]'), 8000);
if (!row) {
return {error: text(document.getElementById("main"))};
}
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)
ORG_FOLDED = """(async () => {@HELPERS@
const toggle = document.activeElement;
const folded = {focused: toggle.dataset.focus, expanded: toggle.getAttribute("aria-expanded"), label: name(toggle), hidden: document.getElementById(toggle.getAttribute("aria-controls")).hidden, seen: all("#main .org-row[data-office]").filter((entry) => entry.offsetParent !== null).map((entry) => entry.dataset.office)};
toggle.click();
await sleep(200);
const before = all("#main .agent-order li").map((item) => item.dataset.name);
all("#main .agent-order li")[1].querySelector('[data-focus^="up:"]').focus();
return {...folded, again: toggle.getAttribute("aria-expanded"), before};
})()""".replace("@HELPERS@", 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) === "Save order");
const disabled = save.disabled;
save.click();
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="Work"]').value, focused: document.activeElement.tagName};
})()""".replace("@HELPERS@", HELPERS)
CONFIRM_TAKEOVER = """(async () => {@HELPERS@
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)
AFTER_TAKEOVER = """(async () => {@HELPERS@
document.querySelector(".sheet-head button").click();
const row = await until(() => {
const found = document.querySelector('#main .org-row[data-office="identity/oidc"] .org-holder');
return found && !text(found).endsWith("Stage") ? found : null;
}, 15000);
return {holder: text(row)};
})()""".replace("@HELPERS@", HELPERS)
INBOX_CHIP = """(async () => {@HELPERS@
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("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"))])}};
})()""".replace("@HELPERS@", HELPERS)
PIN = """(async () => {@HELPERS@
const row = await until(() => all("#main .task-row").find((item) => item.dataset.task === "work/docs"), 8000);
if (!row) {
return {error: text(document.getElementById("main"))};
}
row.querySelector('button[aria-label="Priority"]').click();
await sleep(400);
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) === "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)
ACTIONS = """(async () => {@HELPERS@
const press = async (key) => {
document.dispatchEvent(new KeyboardEvent("keydown", {key, bubbles: true}));
await sleep(300);
return location.hash;
};
const found = {keys: [await press("2"), await press("3"), await press("4"), await press("1")]};
document.getElementById("settings-open").click();
await sleep(300);
found.ambience = all(".sheet-box form > section:first-child button").map((button) => button.id);
found.sections = all(".sheet-box form.settings > section > h3").map(text);
found.radioFields = all('.sheet-box [aria-label="Squelch"], .sheet-box [aria-label="Classes"], .sheet-box [aria-label="Speakers"], .sheet-box .sound-row').length;
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();
await sleep(300);
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="Categories"] button:nth-child(2)').click();
await sleep(900);
document.querySelector('#main button.linkish[title^="Open "]').click();
await sleep(500);
found.popup = all("#popup input[type=radio]").map((input) => [input.value, input.checked]);
found.popupUnlabelled = all("#popup svg.bi").map((svg) => svg.parentElement).filter((owner) => !text(owner) && !(owner.getAttribute("title") && owner.getAttribute("aria-label"))).length;
found.centred = [document.getElementById("popup").open, document.querySelector("#popup .leader line").getAttribute("display")];
return found;
})()""".replace("@HELPERS@", HELPERS)
POPUPS = """
const dialog = document.getElementById("popup");
const box = dialog.querySelector(".popup-box");
const line = dialog.querySelector(".leader line");
const click = (target) => target.dispatchEvent(new MouseEvent("click", {bubbles: true, cancelable: true}));
const tint = document.createElement("i");
tint.style.color = "var(--alarm)";
document.body.append(tint);
const red = getComputedStyle(tint).color;
tint.remove();
const tall = () => document.documentElement.scrollHeight > innerHeight;
const look = (node) => ({picked: node.classList.contains("picked"), size: Math.round(node.querySelector("circle").getBoundingClientRect().width), red: getComputedStyle(node.querySelector("circle")).fill === red});
const opened = () => {
const rect = box.getBoundingClientRect();
const ends = line.getAttribute("display") === "none" ? null : ["x1", "y1", "x2", "y2"].map((key) => Number(line.getAttribute(key)));
return {
open: dialog.open, fade: [dialog.classList.contains("fade"), getComputedStyle(dialog).animationName], title: text(box.querySelector(".panel-title")), opacity: Number(getComputedStyle(box).opacity), line: ends,
onBox: Boolean(ends) && (Math.abs(ends[2] - rect.left) < 1 || Math.abs(ends[2] - rect.right) < 1 || Math.abs(ends[3] - rect.top) < 1 || Math.abs(ends[3] - rect.bottom) < 1),
inside: rect.left >= 0 && rect.top >= 0 && rect.right <= innerWidth && rect.bottom <= innerHeight,
tall: tall(), wide: document.documentElement.scrollWidth > innerWidth + 1, panel: all("#drawer, .drawer, #main aside, #main .panel, #main .graph-body").length,
};
};
const away = async (pick) => {
const outside = document.elementFromPoint(2, innerHeight - 2);
click(outside);
await sleep(250);
return {...look(pick()), open: dialog.open, outside: outside === dialog};
};
"""
POPUP_ORBIT = """(async () => {@HELPERS@@POPUPS@
const pick = () => document.querySelector('#main svg .node[data-pick="s2"]');
if (!(await until(pick, 8000))) {
return {error: text(document.getElementById("main"))};
}
const before = look(pick());
const flat = !tall();
click(pick());
await sleep(400);
const art = box.querySelector(".popup-head .avatar.big");
const face = art ? {key: art.dataset.face, svg: Boolean(art.querySelector("svg.face-art .mouth")), text: art.textContent} : null;
const shown = {...opened(), ...look(pick()), sections: all("h3", box).map((head) => head.firstChild.textContent), composer: Boolean(box.querySelector("form textarea")), offices: text(box.querySelector(".popup-body p")), face};
const gone = await away(pick);
click(pick());
await sleep(300);
const again = dialog.open;
(document.activeElement || document.body).dispatchEvent(new KeyboardEvent("keydown", {key: "Escape", bubbles: true}));
await sleep(250);
const escaped = {...look(pick()), open: dialog.open, again};
click(pick());
await sleep(500);
return {before, flat, shown, gone, escaped, last: dialog.open};
})()""".replace("@HELPERS@", HELPERS).replace("@POPUPS@", POPUPS)
POPUP_ARTIFACT = """(async () => {@HELPERS@@POPUPS@
const first = await until(() => document.querySelector('#main svg .node[data-pick^="branch:"]'), 8000);
if (!first) {
return {error: text(document.getElementById("main"))};
}
const id = first.dataset.pick;
const pick = () => all("#main svg .node[data-pick]").find((node) => node.dataset.pick === id);
const before = look(pick());
const flat = !tall();
click(pick());
await sleep(400);
const shown = {...opened(), ...look(pick()), label: first.getAttribute("aria-label").split(",")[0]};
const gone = await away(pick);
click(pick());
await sleep(400);
return {before, flat, shown, gone, last: dialog.open};
})()""".replace("@HELPERS@", HELPERS).replace("@POPUPS@", POPUPS)
PICK_3D = """(async () => {@HELPERS@
const field = await until(() => document.querySelector('#main input[aria-label="Find a node"]'), 8000);
const ready = await until(() => document.querySelector("#main .graph3d canvas") && !text(document.querySelector("#main .space-note")).startsWith("Loading"), 8000);
if (!field || !ready) {
return {error: text(document.querySelector("#main .graph"))};
}
const type = async (value) => {
field.value = value;
field.dispatchEvent(new Event("input", {bubbles: true}));
await sleep(150);
return all("#main .hits .chip").map((chip) => [chip.dataset.hit, text(chip)]);
};
const tree = await (await fetch("/api/tree", {cache: "no-store"})).json();
const found = {empty: await type(""), twins: tree.repos.flatMap((repo) => repo.branches).filter((branch) => branch.name === "main").length, twin: await type("main"), hits: await type("@QUERY@")};
found.flat = document.documentElement.scrollHeight <= innerHeight;
const chip = all("#main .hits .chip").find((node) => node.dataset.hit.startsWith("@PREFIX@"));
if (!chip) {
return {error: found};
}
chip.click();
await sleep(600);
const dialog = document.getElementById("popup");
const line = dialog.querySelector(".leader line");
const host = () => document.querySelector("#main .graph3d");
found.opened = {
open: dialog.open, title: text(dialog.querySelector(".popup-box .panel-title")), label: text(chip), hit: chip.dataset.hit, picked: host().dataset.picked || null,
line: line.getAttribute("display") !== "none", lists: all("#main .space-list, #main .chip[data-node]").length, flat: document.documentElement.scrollHeight <= innerHeight,
};
dialog.dispatchEvent(new MouseEvent("click", {bubbles: true}));
await sleep(300);
found.closed = {open: dialog.open, picked: host().dataset.picked || null};
chip.click();
await sleep(600);
return found;
})()""".replace("@HELPERS@", HELPERS)
CALL = """(async () => {@HELPERS@
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('#popup input[type=radio][value="call"]').click();
const field = document.querySelector("#popup textarea");
field.value = "E2E call ping";
field.dispatchEvent(new Event("input", {bubbles: true}));
document.querySelector('#popup button[type="submit"]').click();
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@
const card = await until(() => all("#main details.repo").find((details) => details.textContent.includes("github.com/me/demo")), 8000);
if (!card) {
return {error: text(document.getElementById("main"))};
}
card.open = true;
await sleep(300);
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="Dry run"] pre').map(text)};
})()""".replace("@HELPERS@", HELPERS)
CONFIRM = """(async () => {@HELPERS@
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("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 = """
const shown = (node) => node.getClientRects().length > 0 && node.getBoundingClientRect().width > 0;
const words = (label) => [...label.childNodes].filter((node) => node.nodeType === 3).map((node) => node.textContent).join("").trim();
const called = (node) => node.getAttribute("aria-label") || (node.closest("label") ? words(node.closest("label")) : "") || text(node) || node.type;
const clashes = (root) => {
const boxes = all("input, select, textarea, button", root).filter(shown).map((node) => ({node, box: node.getBoundingClientRect()}));
const found = [];
boxes.forEach((a, index) => boxes.slice(index + 1).forEach((b) => {
const x = Math.min(a.box.right, b.box.right) - Math.max(a.box.left, b.box.left);
const y = Math.min(a.box.bottom, b.box.bottom) - Math.max(a.box.top, b.box.top);
if (x > 0.5 && y > 0.5) {
found.push(`${called(a.node)} × ${called(b.node)}`);
}
}));
const outside = boxes.filter(({box}) => box.right > root.getBoundingClientRect().right + 0.5).map(({node}) => called(node));
return {found, outside};
};
"""
RULES_OVERLAP = """(async () => {@HELPERS@@CLASHES@
const open = document.querySelector(".sheet-head button");
if (open) {
open.click();
}
const rules = await until(() => document.querySelector("#main .rule-form"), 8000);
const kind = rules.querySelector("select");
const found = {};
for (const value of ["decision", "review", "ask"]) {
kind.value = value;
kind.dispatchEvent(new Event("change", {bubbles: true}));
await sleep(150);
found[value] = {...clashes(rules), labels: all("label", rules).filter(shown).map(words)};
}
return found;
})()""".replace("@HELPERS@", HELPERS).replace("@CLASHES@", OVERLAP)
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) === "+ 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")) === "Location");
place.scrollIntoView({block: "start"});
return clashes(settings);
})()""".replace("@HELPERS@", HELPERS).replace("@CLASHES@", OVERLAP)
MEDIA_LOG = """() => {
const keep = (entry) => {
try {
const list = JSON.parse(sessionStorage.getItem("e2e-media") || "[]");
list.push({...entry, page: Math.round(performance.timeOrigin)});
sessionStorage.setItem("e2e-media", JSON.stringify(list));
} catch (error) {
return;
}
};
const play = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function () {
window.e2eAudio = this;
const src = this.getAttribute("src") || "";
const result = play.call(this);
result.then(() => keep({play: "ok", src}), (error) => keep({play: error.name, src}));
return result;
};
document.addEventListener("securitypolicyviolation", (event) => keep({csp: event.effectiveDirective, blocked: event.blockedURI}));
}"""
MEDIA = """
const log = () => JSON.parse(sessionStorage.getItem("e2e-media") || "[]");
const sample = () => (window.e2eAudio ? {paused: window.e2eAudio.paused, time: window.e2eAudio.currentTime, error: window.e2eAudio.error ? window.e2eAudio.error.code : null, src: window.e2eAudio.getAttribute("src")} : null);
const advancing = async () => {
const first = sample();
await sleep(1500);
const second = sample();
return {first, second, playing: Boolean(first && second && !second.paused && second.time - first.time > 0.5)};
};
"""
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) === "+ Station").click();
await sleep(200);
const row = all(".stream-row", form).pop();
const [label, address, start, end] = all("input.field", row);
const set = (input, value) => {
input.value = value;
input.dispatchEvent(new Event("input", {bubbles: true}));
};
set(label, "@NAME@");
set(address, "@STATION@");
set(start, clock(new Date(Date.now() - 3600000)));
set(end, clock(new Date(Date.now() + 3600000)));
all('.days input[type="checkbox"]', row).forEach((box) => {
box.checked = true;
});
all("button", form).find((button) => button.type === "submit").click();
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 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@
await until(() => log().some((entry) => entry.play), 8000);
return {log: log(), media: await advancing(), toasts: all("#toasts .toast").map(text)};
})()""".replace("@HELPERS@", HELPERS).replace("@MEDIA@", MEDIA)
RADIO_VIEW = """(async () => {@HELPERS@@MEDIA@
location.hash = "#stream/radio";
const row = await until(() => document.querySelector('#main [data-station="0"]'), 8000);
if (!row) {
const initial = JSON.parse(document.getElementById("state").textContent);
return {error: "no station row", log: log(), wrapped: !String(HTMLMediaElement.prototype.play).includes("[native code]"), streams: initial.settings.streams, media: initial.media, now: new Date().toString()};
}
const label = row.querySelector(".station-name");
const style = getComputedStyle(label);
return {
state: row.dataset.state,
note: text(row.querySelector(".station-note")),
title: label.title,
line: [style.whiteSpace, style.textOverflow, label.scrollWidth > label.clientWidth, label.getBoundingClientRect().height < 2 * parseFloat(style.lineHeight)],
media: await advancing(),
log: log(),
toasts: all("#toasts .toast").map(text),
};
})()""".replace("@HELPERS@", HELPERS).replace("@MEDIA@", MEDIA)
RADIO_CLICK = """(async () => {@HELPERS@@MEDIA@
const row = document.querySelector('#main [data-station="0"]');
if (!row) {
return {error: "no station row", log: log(), media: await advancing()};
}
const play = row.querySelector('button[data-action="play"]');
const clicked = Boolean(sample() && sample().paused && play);
if (clicked) {
play.click();
}
await until(() => sample() && !sample().paused, 6000);
const media = await advancing();
const now = document.querySelector('#main [data-station="0"]');
return {clicked, state: now.dataset.state, note: text(now.querySelector(".station-note")), media, log: log()};
})()""".replace("@HELPERS@", HELPERS).replace("@MEDIA@", MEDIA)
SPEAKING = """
const caption = () => {
const node = document.querySelector("#main .radio-caption");
return node && !node.hidden ? node : null;
};
const ring = (chip) => (chip ? getComputedStyle(chip.querySelector(".speaker-face"), "::after").animationName : null);
const mouth = (art) => (art ? [art.classList.contains("speaking"), getComputedStyle(art.querySelector(".mouth")).animationName] : null);
const faces = () => ({
speaking: mouth(document.querySelector('#main .speakers .avatar[data-face="s2"]')),
silent: mouth(document.querySelector('#main .speakers .avatar[data-face="s1"]')),
keys: [...new Set(all("#main .avatar.speaking").map((art) => art.dataset.face))],
});
const cue = async (word) => {
await until(() => document.querySelector("#main .speakers .speaker"), 8000);
await fetch("/api/tts", {method: "POST", headers: {"X-Kvb-Orchestrator": "1", "Content-Type": "application/json"}, body: JSON.stringify({text: word, voice: ""}), cache: "no-store"});
const found = await until(caption, 25000);
const chip = document.querySelector("#main .speaker.speaking");
return found ? {who: text(found.querySelector("strong")), line: text(found.querySelector(".caption-line")), chip: chip ? chip.dataset.key : null, ring: ring(chip), faces: faces()} : {error: "no caption", toasts: all("#toasts .toast").map(text)};
};
"""
SPEAK = """(async () => {@HELPERS@@SPEAKING@
return cue("@CUE@");
})()""".replace("@HELPERS@", HELPERS).replace("@SPEAKING@", SPEAKING)
SPEAK_GRAPH = """(async () => {@HELPERS@@SPEAKING@
location.hash = "#agents/orbit";
const node = await until(() => document.querySelector("#main svg .node.speaking"), 6000);
const silent = document.querySelector('#main svg .node[data-pick="s1"] .mouth');
return {
node: node ? node.getAttribute("aria-label") : null, ring: node ? getComputedStyle(node.querySelector("circle")).animationName : null,
mouth: node ? getComputedStyle(node.querySelector(".mouth")).animationName : null, silent: silent ? getComputedStyle(silent).animationName : null,
};
})()""".replace("@HELPERS@", HELPERS).replace("@SPEAKING@", SPEAKING)
SPEAK_END = """(async () => {@HELPERS@@SPEAKING@
const ended = Boolean(await until(() => !document.querySelector("#main svg .node") || !document.querySelector("#main svg .node.speaking"), 20000));
location.hash = "#stream/radio";
await until(() => document.querySelector("#main .radio-caption"), 6000);
await sleep(300);
return {ended, caption: Boolean(caption()), chips: all("#main .speaker.speaking").length, faces: all("#main .avatar.speaking").length};
})()""".replace("@HELPERS@", HELPERS).replace("@SPEAKING@", SPEAKING)
SPOT_OPEN = """
const spotToggle = await until(() => all('#main [role="toolbar"] button').find((node) => name(node) === "Radio"), 8000);
if (spotToggle && spotToggle.getAttribute("aria-expanded") !== "true") {
spotToggle.click();
await sleep(300);
}
const spot = document.getElementById("radio-spot");
"""
COMPOSE = """(async () => {@HELPERS@@SPOT_OPEN@
const form = spot ? spot.querySelector("form.composer") : null;
if (!form) {
return {error: "no composer"};
}
const [channel, recipient] = all("select", form);
await until(() => [...recipient.options].some((option) => option.value === "@TO@"), 8000);
channel.value = "call";
channel.dispatchEvent(new Event("change", {bubbles: true}));
recipient.value = "@TO@";
const record = all("button", form).find((button) => button.hasAttribute("aria-pressed"));
record.click();
await sleep(1500);
const recording = record.getAttribute("aria-pressed");
record.click();
const field = form.querySelector("textarea");
await until(() => field.value && form.querySelector('.voice-note input[type="checkbox"]'), 60000);
const replay = form.querySelector(".voice-note audio");
const attach = form.querySelector('.voice-note input[type="checkbox"]');
return {
channels: all("option", channel).map((option) => option.value),
recipients: all("option", recipient).map((option) => option.value),
recording,
text: field.value,
replay: replay ? replay.getAttribute("src") : null,
attach: attach ? [text(attach.closest("label")), attach.checked] : null,
status: text(form.querySelector(".composer-status")),
toasts: all("#toasts .toast").map(text),
clashes: (() => {@CLASHES@
return clashes(form);
})(),
};
})()""".replace("@HELPERS@", HELPERS).replace("@SPOT_OPEN@", SPOT_OPEN).replace("@CLASHES@", OVERLAP)
COMPOSE_SEND = """(async () => {@HELPERS@
const form = document.querySelector("#main form.composer");
const field = form.querySelector("textarea");
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 to")), 20000);
return {sent, left: field.value, voice: Boolean(form.querySelector(".voice-note audio"))};
})()""".replace("@HELPERS@", HELPERS)
CLEAR_MAYDAY = """(async () => {@HELPERS@
const questions = [];
let accept = false;
window.confirm = (question) => {
questions.push(question);
return accept;
};
const posted = [];
const send = window.fetch.bind(window);
window.fetch = (path, init) => {
if (String(path).startsWith("/api/board/")) {
posted.push(String(path));
}
return send(path, init);
};
await until(() => document.querySelector('#main [role="toolbar"]'), 8000);
const toolbar = all('#main [role="toolbar"] button, #main [role="toolbar"] summary').map(name);@SPOT_OPEN@
const button = await until(() => spot && all(".spot-head button", spot).find((node) => name(node) === "Cancel mayday" && !node.disabled), 8000);
if (!button) {
return {error: "no enabled Cancel mayday button in the radio spot", toolbar};
}
const shown = new Set(all("#toasts .toast"));
const fresh = () => all("#toasts .toast").filter((node) => !shown.has(node)).map(text);
button.click();
await sleep(800);
const declined = {question: questions.slice(), posted: posted.slice(), toasts: fresh()};
accept = true;
button.click();
const done = await until(() => fresh().find((toast) => toast.startsWith("Cancel mayday")), 15000);
return {toolbar, title: button.title, declined, accepted: {question: questions.slice(declined.question.length), posted: posted.slice(declined.posted.length)}, done};
})()""".replace("@HELPERS@", HELPERS).replace("@SPOT_OPEN@", SPOT_OPEN)
NIGHT_MENU = """(async () => {@HELPERS@
const questions = [];
window.confirm = (question) => {
questions.push(question);
return true;
};
let asked = 0;
const get = navigator.credentials.get.bind(navigator.credentials);
navigator.credentials.get = (options) => {
asked += 1;
return @SIGNS@ ? get(options) : Promise.reject(new DOMException("The operator canceled.", "NotAllowedError"));
};
const posted = [];
const send = window.fetch.bind(window);
window.fetch = (path, init) => {
if (String(path).startsWith("/api/board/")) {
const body = JSON.parse(init.body);
posted.push([String(path), Object.keys(body), body.action || null]);
}
return send(path, init);
};
const menu = await until(() => all('#main [role="toolbar"] details.menu').find((node) => name(node.querySelector("summary")) === "Day/Night"), 8000);
if (!menu) {
return {error: text(document.getElementById("main"))};
}
menu.open = true;
const buttons = all(".actions button", menu);
const runs = [];
for (const button of buttons) {
const label = name(button);
const [before, prompts, sent] = [asked, questions.length, posted.length];
const shown = new Set(all("#toasts .toast"));
button.click();
const toast = await until(() => all("#toasts .toast").filter((box) => !shown.has(box)).map(text).find((entry) => entry.startsWith(`${label}:`)), 15000);
runs.push({label, question: questions.slice(prompts), asked: asked - before, posted: posted.slice(sent), toast});
}
return {labels: buttons.map(name), runs};
})()""".replace("@HELPERS@", HELPERS)
NIGHT_LOOK = """(async () => {@HELPERS@
const menu = await until(() => all('#main [role="toolbar"] details.menu').find((node) => name(node.querySelector("summary")) === "Day/Night"), 8000);
if (!menu) {
return {error: text(document.getElementById("main"))};
}
menu.open = true;
await sleep(200);
const box = menu.querySelector(".menu-box");
return {
children: [...box.children].map((node) => [node.tagName.toLowerCase(), node.className]),
badge: all(".pill", box).map(text),
buttons: all("button", box).map((button) => [name(button), button.title]),
paragraphs: all("p", box).length,
wide: document.documentElement.scrollWidth > window.innerWidth + 1,
};
})()""".replace("@HELPERS@", HELPERS)
FITS = "tall: document.documentElement.scrollHeight > window.innerHeight, wide: document.documentElement.scrollWidth > window.innerWidth + 1"
SPOT_SEND = """(async () => {@HELPERS@
const questions = [];
window.confirm = (question) => {
questions.push(question);
return true;
};
const toggle = await until(() => all('#main [role="toolbar"] button').find((node) => name(node) === "Radio"), 8000);
if (!toggle) {
return {error: text(document.getElementById("main"))};
}
const bar = document.querySelector('#main [role="toolbar"]');
const spot = document.getElementById("radio-spot");
const closed = [spot.hidden, toggle.getAttribute("aria-expanded")];
toggle.click();
await sleep(400);
const form = spot.querySelector("form.composer");
const [channel, recipient] = all("select", form);
const field = form.querySelector("textarea");
const found = {
closed,
open: [spot.hidden, toggle.getAttribute("aria-expanded"), document.activeElement === field],
inline: [getComputedStyle(spot).position, spot.getBoundingClientRect().top >= bar.getBoundingClientRect().bottom, spot.previousElementSibling === bar],
toolbar: all('#main [role="toolbar"] > *').map((node) => name(node.tagName === "DETAILS" ? node.querySelector("summary") : node)),
channels: all("option", channel).map((option) => option.value),
record: all("button", form).filter((button) => button.hasAttribute("aria-pressed")).map(text),
heads: all(".spot-head button", spot).map((button) => [name(button), button.title]),
@FITS@,
};
await until(() => [...recipient.options].some((option) => option.value === "Stage"), 8000);
found.runs = [];
for (const cls of found.channels) {
channel.value = cls;
channel.dispatchEvent(new Event("change", {bubbles: true}));
const direct = !recipient.closest("label").hidden;
if (direct) {
recipient.value = "Stage";
}
field.value = `@TEXT@ ${cls}`;
field.dispatchEvent(new Event("input", {bubbles: true}));
const shown = new Set(all("#toasts .toast"));
const prompts = questions.length;
form.querySelector('button[type="submit"]').click();
const toast = await until(() => all("#toasts .toast").filter((node) => !shown.has(node)).map(text).find((entry) => entry.includes(" to ")), 20000);
found.runs.push({cls, direct, toast, question: questions.slice(prompts), left: field.value});
}
document.getElementById("toasts").replaceChildren();
return found;
})()""".replace("@HELPERS@", HELPERS).replace("@FITS@", FITS)
RADIO_JUMP = """(async () => {@HELPERS@
const jump = await until(() => all("#main .radio-view button").find((button) => name(button) === "Send …"), 8000);
if (!jump) {
return {error: text(document.getElementById("main"))};
}
const composers = all("#main form.composer").length;
jump.click();
const spot = await until(() => location.hash.startsWith("#agents/") && document.querySelector("#radio-spot:not([hidden])"), 4000);
await sleep(200);
return {composers, hash: location.hash, open: Boolean(spot), focused: Boolean(spot) && document.activeElement === spot.querySelector("textarea")};
})()""".replace("@HELPERS@", HELPERS)
GEAR_SAVE = """(async () => {@HELPERS@@CLASHES@
const spot = document.getElementById("radio-spot");
const gear = all(".spot-head button", spot).find((button) => name(button) === "Radio settings");
gear.click();
const form = await until(() => spot.querySelector("form.radio-settings"), 4000);
if (!form) {
return {error: "no radio settings behind the gear"};
}
const classes = () => all('[aria-label="Classes"] input', form);
const found = {
expanded: gear.getAttribute("aria-expanded"),
sections: all("section > h3", form).map(text),
tones: all(".sound-row strong", form).map(text),
classes: classes().map((input) => [input.parentElement.textContent.trim(), input.checked, input.disabled]),
clashes: clashes(form),
@FITS@,
};
const squelch = form.querySelector('input[aria-label="Squelch"]');
squelch.value = "@SQUELCH@";
squelch.dispatchEvent(new Event("input", {bubbles: true}));
form.querySelector('input[aria-label="All radio on"]').checked = @TONE@;
classes().find((input) => input.parentElement.textContent.trim() === "Post").checked = @POST@;
const before = new Set(all("#toasts .toast"));
all("button", form).find((button) => button.type === "submit").click();
found.toast = await until(() => all("#toasts .toast").filter((node) => !before.has(node)).map(text).find((entry) => entry.includes("ettings")), 15000);
await sleep(300);
found.after = [gear.getAttribute("aria-expanded"), Boolean(spot.querySelector("form.radio-settings"))];
document.getElementById("settings-open").click();
const sheet = await until(() => document.querySelector(".sheet-box form.settings"), 4000);
found.general = {sections: all("section > h3", sheet).map(text), radio: all('[aria-label="Squelch"], [aria-label="Classes"], [aria-label="Speakers"], .sound-row', sheet).length};
document.querySelector(".sheet-head button").click();
return found;
})()""".replace("@HELPERS@", HELPERS).replace("@CLASHES@", OVERLAP).replace("@FITS@", FITS)
GEAR_READ = """(async () => {@HELPERS@@SPOT_OPEN@
const gear = spot ? all(".spot-head button", spot).find((button) => name(button) === "Radio settings") : null;
if (!gear) {
return {error: text(document.getElementById("main"))};
}
gear.click();
const form = await until(() => spot.querySelector("form.radio-settings"), 4000);
return {
squelch: Number(form.querySelector('input[aria-label="Squelch"]').value),
tone: form.querySelector('input[aria-label="All radio on"]').checked,
post: all('[aria-label="Classes"] input', form).find((input) => input.parentElement.textContent.trim() === "Post").checked,
@FITS@,
};
})()""".replace("@HELPERS@", HELPERS).replace("@SPOT_OPEN@", SPOT_OPEN).replace("@FITS@", FITS)
GRAPH_HOOK = """() => {
let made = null;
Object.defineProperty(window, "ForceGraph3D", {
configurable: true,
get: () => made && ((options) => {
const graph = made(options);
window.e2eGraphs = [...(window.e2eGraphs || []), graph];
return graph;
}),
set: (value) => {
made = value;
},
});
}"""
CAMERA = """(async () => {@HELPERS@
const ready = await until(() => window.e2eGraphs && document.querySelector("#main .graph3d canvas") && !text(document.querySelector("#main .space-note")).startsWith("Loading"), 15000);
if (!ready) {
return {error: text(document.querySelector("#main .graph"))};
}
const graph = window.e2eGraphs[window.e2eGraphs.length - 1];
const position = () => {
const at = graph.cameraPosition();
return [at.x, at.y, at.z];
};
await sleep(5500);
let [fed, stops] = [0, 0];
const stopped = graph.onEngineStop();
graph.onEngineStop(() => {
stops += 1;
stopped();
});
const feed = graph.graphData;
graph.graphData = function (...args) {
fed += args.length;
return feed.apply(graph, args);
};
const built = position();
const canvas = document.querySelector("#main .graph3d canvas");
const rect = canvas.getBoundingClientRect();
for (let turn = 0; turn < 4; turn += 1) {
canvas.dispatchEvent(new WheelEvent("wheel", {deltaY: -120, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, bubbles: true, cancelable: true}));
await sleep(60);
}
await sleep(900);
const moved = position();
const refreshes = [];
for (let round = 0; round < 2; round += 1) {
const [feeds, ends] = [fed, stops];
document.dispatchEvent(new Event("visibilitychange"));
const rebuilt = Boolean(await until(() => fed > feeds, 8000));
const settled = Boolean(await until(() => stops > ends, 10000));
await sleep(900);
refreshes.push({rebuilt, settled, camera: position()});
}
all("#main button").find((button) => name(button) === "Reset view").click();
await sleep(1500);
return {hooked: typeof stopped === "function", built, moved, refreshes, reset: position()};
})()""".replace("@HELPERS@", HELPERS)
def tone(seconds: int, rate: int) -> bytes:
frames = array.array("h", (int(6000 * math.sin(2 * math.pi * 440 * n / rate)) for n in range(seconds * rate)))
return wav_bytes(1, 2, rate, frames.tobytes())
class Station(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
if self.server.moved:
self.send_response(302)
self.send_header("Location", self.server.moved)
self.send_header("Content-Length", "0")
self.end_headers()
return
body = self.server.body
self.send_response(200)
self.send_header("Content-Type", "audio/wav")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
with contextlib.suppress(OSError):
self.wfile.write(body)
def log_message(self, *args: object) -> None:
return
LABELS = """
const unlabelled = (root) => all("svg.bi", root).filter((svg) => !svg.closest(".sprite")).map((svg) => svg.parentElement).filter((owner) => !text(owner) && !(owner.getAttribute("title") && owner.getAttribute("aria-label"))).map((owner) => owner.outerHTML.slice(0, 160));
const wide = () => document.documentElement.scrollWidth > window.innerWidth + 1;
const rowOf = (words) => all("#main .inbox-list > li").find((li) => li.textContent.includes(words));
const press = (root, label) => all("button", root).find((button) => name(button) === label).click();
const out = () => document.querySelector("#main .console .console-out");
const atEnd = () => out().scrollTop + out().clientHeight >= out().scrollHeight - 12;
const line = () => text(document.querySelector("#main .console .console-status"));
"""
RUN_START = """(async () => {@HELPERS@@LABELS@
const row = await until(() => rowOf("Row"), 8000);
if (!row) {
return {error: text(document.getElementById("main"))};
}
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("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("Row 80"), 20000);
await sleep(300);
return {held: out().scrollTop, running: line()};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
RUN_DONE = """(async () => {@HELPERS@@LABELS@
out().scrollTop = out().scrollHeight;
await until(() => document.querySelector("#main .console").dataset.status === "done", 20000);
await sleep(500);
const lines = out().textContent.trim().split("\\n");
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("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)),
console: line(),
followed: atEnd(),
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) === "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, "Deny via …");
const box = await until(() => document.querySelector(".sheet-box"), 4000);
return {
title: text(box.querySelector(".panel-title")),
channels: all(".deny-channels input[type=radio]", box).map((input) => [input.value, input.checked]),
hints: all(".deny-channel small", box).map(text),
reason: Boolean(box.querySelector("textarea")),
unlabelled: unlabelled(box),
wide: wide(),
};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
DENY = """(async () => {@HELPERS@@LABELS@
const row = await until(() => rowOf("Bash: make deploy"), 8000);
press(row, "Deny via …");
const box = await until(() => document.querySelector(".sheet-box"), 4000);
box.querySelector('input[value="call"]').click();
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("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)
WATCH = """
const questions = [];
window.confirm = (question) => {
questions.push(question);
return true;
};
let asked = 0;
const get = navigator.credentials.get.bind(navigator.credentials);
navigator.credentials.get = (options) => {
asked += 1;
return @SIGNS@ ? get(options) : Promise.reject(new DOMException("The operator canceled.", "NotAllowedError"));
};
const posted = [];
const send = window.fetch.bind(window);
window.fetch = (path, init) => {
if (["/api/choices/sign", "/api/choices", "/api/sign", "/api/inbox/approve"].includes(String(path))) {
const body = JSON.parse(init.body);
posted.push([String(path), Object.keys(body).sort(), body.class || null, body.to || null]);
}
return send(path, init);
};
const bar = () => document.getElementById("choice-request");
const fresh = async (shown, prefix) => until(() => all("#toasts .toast").filter((box) => !shown.has(box)).map(text).find((entry) => entry.startsWith(prefix)), 20000);
const fits = () => ({tall: document.documentElement.scrollHeight > window.innerHeight, wide: document.documentElement.scrollWidth > window.innerWidth + 1});
"""
CHOICE_VIEW = """(async () => {@HELPERS@@LABELS@
const found = await until(() => document.getElementById("choice-request") && all("#main .inbox-list > li .options").length === @CARDS@ && document.querySelector('#choice-request input[type="checkbox"]'), 8000);
if (!found) {
return {error: text(document.getElementById("main"))};
}
const box = document.getElementById("choice-request");
const chip = document.querySelector('#main [aria-label="Task filters"] button[data-filter="operator"]');
const closed = all("button[data-channel]", box).filter((button) => button.checkVisibility()).length;
return {
kinds: [...new Set(all("#main .inbox-list > li").map((li) => text(li.querySelector(".pill"))))].sort(),
filter: [text(chip).replace(/[0-9]+$/, ""), chip.getAttribute("aria-pressed")],
channels: all("button[data-channel]", box).map((button) => [button.dataset.channel, name(button), button.disabled]),
fields: all("textarea, input:not([type=checkbox])", box).length,
agents: all('input[type="checkbox"]', box).map((input) => input.value),
multi: [name(box.querySelector("summary")), box.open, closed, text(box.querySelector(".menu-box h3 .count")), all('input[type="checkbox"]', box).filter((input) => input.checked).length],
bar: [box.parentElement.getAttribute("aria-label"), toolbarRows(), taskLayout()],
cards: all("#main .inbox-list > li").filter((li) => li.querySelector(".options")).map((li) => [text(li.querySelector(".pill")), all(".option strong", li).map(text)]),
choices: all("#main [data-column], #main .group-title, #main h2, #main h3").filter((node) => node.dataset.column === "choices" || text(node) === "Choices").length,
unlabelled: unlabelled(document),
tall: document.documentElement.scrollHeight > window.innerHeight,
wide: wide(),
};
})()""".replace("@HELPERS@", HELPERS).replace("@LABELS@", LABELS)
CHOICE_SEND = """(async () => {@HELPERS@@WATCH@
const menu = bar();
menu.querySelector("summary").click();
await sleep(200);
const boxes = all('input[type="checkbox"]', menu).slice(0, 2);
boxes.forEach((box) => box.click());
const counted = text(menu.querySelector(".menu-box h3 .count"));
const opened = [menu.open, all("button[data-channel]", menu).filter((button) => button.checkVisibility()).length];
const picked = boxes.map((box) => box.value).sort((a, b) => a.localeCompare(b));
const runs = [];
for (const button of all("button[data-channel]", bar())) {
const [before, prompts, sent] = [asked, questions.length, posted.length];
const shown = new Set(all("#toasts .toast"));
const disabled = button.disabled;
button.click();
const toast = await fresh(shown, "Choice request");
runs.push({channel: button.dataset.channel, disabled, asked: asked - before, question: questions.slice(prompts), posted: posted.slice(sent), toast});
}
return {picked, counted, opened, runs, fields: all("textarea, input:not([type=checkbox])", bar()).length, ...fits()};
})()""".replace("@HELPERS@", HELPERS).replace("@WATCH@", WATCH)
CHOICE_ANSWER = """(async () => {@HELPERS@@WATCH@
bar().querySelector("summary").click();
await sleep(200);
const closed = [bar().open, all("button[data-channel]", bar()).filter((button) => button.checkVisibility()).length];
const card = await until(() => all("#main .inbox-list > li").find((li) => li.textContent.includes("@QUESTION@")), 8000);
if (!card) {
return {error: text(document.getElementById("main"))};
}
const option = all(".option", card).find((button) => text(button.querySelector("strong")) === "Wait");
const title = option.title;
const shown = new Set(all("#toasts .toast"));
option.click();
const toast = await fresh(shown, "Choice: Wait");
const gone = @SIGNS@ ? Boolean(await until(() => !all("#main .inbox-list > li").some((li) => li.textContent.includes("@QUESTION@")), 20000)) : null;
return {title, asked, posted, toast, gone, closed, ...fits()};
})()""".replace("@HELPERS@", HELPERS).replace("@WATCH@", WATCH)
TASKS_FIT = """(async () => {@HELPERS@
await until(() => document.getElementById("choice-request") && document.querySelector("#main .tasks-body > *"), 8000);
document.getElementById("toasts").replaceChildren();
await sleep(300);
return {
hash: location.hash,
bar: all("#choice-request button[data-channel]").map((button) => button.dataset.channel),
rows: toolbarRows(),
layout: taskLayout(),
columns: all("#main .task-column").map((column) => column.dataset.column),
choices: all("#main [data-column], #main .group-title, #main h2, #main h3").filter((node) => node.dataset.column === "choices" || text(node) === "Choices").length,
tall: document.documentElement.scrollHeight > window.innerHeight,
wide: document.documentElement.scrollWidth > window.innerWidth + 1,
};
})()""".replace("@HELPERS@", HELPERS)
PLAN_HELPERS = """
const fits = () => ({tall: document.documentElement.scrollHeight > window.innerHeight, wide: document.documentElement.scrollWidth > window.innerWidth + 1, rows: toolbarRows(), layout: taskLayout()});
const modes = () => all('#main [aria-label="Estimation"] button').map((button) => [button.dataset.estimation, button.getAttribute("aria-pressed")]);
const requestShown = () => {
const button = document.getElementById("request-estimates");
return button ? !button.hidden && button.getClientRects().length > 0 : null;
};
const choose = async (select, value) => {
select.value = value;
select.dispatchEvent(new Event("change", {bubbles: true}));
await sleep(300);
};
const fresh = async (shown, prefix) => until(() => all("#toasts .toast").filter((box) => !shown.has(box)).map(text).find((entry) => entry.startsWith(prefix)), 20000);
"""
GANTT = """(async () => {@HELPERS@@PLAN_HELPERS@
const svg = await until(() => document.querySelector("#main svg.gantt"), 8000);
if (!svg) {
return {error: text(document.getElementById("main"))};
}
const forecast = document.querySelector('#main select[aria-label="Forecast"]');
const scroll = document.querySelector("#main .gantt-scroll");
const marks = () => all("#main svg.gantt [data-forecast]").map((mark) => [mark.dataset.task, mark.dataset.forecast === "true", Number(mark.dataset.start), Number(mark.dataset.end), mark.tagName.toLowerCase()]);
const found = {
hash: location.hash,
forecast: [forecast.value, all("option", forecast).map((option) => [option.value, text(option)])],
modes: modes(),
request: requestShown(),
bars: marks(),
edges: all("#main svg.gantt path.gantt-edge").map((path) => [path.dataset.kind, path.dataset.from, path.dataset.to, path.getAttribute("marker-end")]),
hatched: all("#main svg.gantt .gantt-bar.forecast").map((bar) => getComputedStyle(bar).fill.includes("gantt-hatch")),
inner: Boolean(scroll && scroll.closest(".tasks-body")),
...fits(),
};
await choose(forecast, "real");
found.real = marks().filter(([, hatched]) => hatched).length;
await choose(forecast, "points");
found.pointsOnly = marks().filter(([, hatched]) => hatched).map(([task, , start, end, tag]) => [task, end - start, tag]);
await choose(forecast, "always");
return found;
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS)
BURN = """(async () => {@HELPERS@@PLAN_HELPERS@
const svg = await until(() => document.querySelector("#main svg.burn"), 8000);
if (!svg) {
return {error: text(document.getElementById("main"))};
}
const scope = document.querySelector('#main select[aria-label="Scope"]');
const series = () => all("#main svg.burn path.burn-line").map((path) => [path.dataset.series, Number(path.dataset.last)]);
const found = {hash: location.hash, modes: modes(), request: requestShown(), scope: [scope.value, all("option", scope).map((option) => option.value)], series: series(), ...fits()};
await choose(scope, "agent:Docs");
found.agent = series();
await choose(scope, "all");
return found;
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS)
POINTS_ON = """(async () => {@HELPERS@@PLAN_HELPERS@
const before = {modes: modes(), request: requestShown()};
all('#main [aria-label="Estimation"] button').find((button) => button.dataset.estimation === "points").click();
const shown = await until(() => requestShown(), 8000);
return {before, modes: modes(), request: Boolean(shown), ...fits()};
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS)
REQUEST = """(async () => {@HELPERS@@PLAN_HELPERS@
const svg = await until(() => document.querySelector("#main svg.burn") && requestShown(), 8000);
if (!svg) {
return {error: text(document.getElementById("main")), modes: modes()};
}
const series = all("#main svg.burn path.burn-line").map((path) => [path.dataset.series, Number(path.dataset.last)]);
const shown = new Set(all("#toasts .toast"));
document.getElementById("request-estimates").click();
const toast = await fresh(shown, "Request estimates");
return {modes: modes(), series, toast, ...fits()};
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS)
BULK = """(async () => {@HELPERS@@PLAN_HELPERS@
const list = await until(() => all("#main .task-row").length && document.getElementById("bulk-value"), 8000);
if (!list) {
return {error: text(document.getElementById("main"))};
}
let asked = 0;
const get = navigator.credentials.get.bind(navigator.credentials);
navigator.credentials.get = (options) => {
asked += 1;
return @SIGNS@ ? get(options) : Promise.reject(new DOMException("The operator canceled.", "NotAllowedError"));
};
const posted = [];
const send = window.fetch.bind(window);
window.fetch = (path, init) => {
if (["/api/sign", "/api/tasks"].includes(String(path))) {
const body = JSON.parse(init.body);
posted.push([String(path), Object.keys(body).sort(), body.items.map((item) => [item.id, item.argv.join(" ")])]);
}
return send(path, init);
};
const rows = () => all("#main .task-row").map((row) => row.dataset.task);
const sort = document.querySelector('#main select[aria-label="Sort"]');
const direction = document.querySelector("#main button[data-direction]");
const found = {sorts: all("option", sort).map((option) => option.value), prio: rows()};
await choose(sort, "status");
found.status = rows();
direction.click();
await sleep(300);
found.statusDown = [rows(), name(direction)];
direction.click();
await choose(sort, "prio");
found.picks = all("#main .task-row input.task-pick").map((box) => box.closest(".task-row").dataset.task);
for (const task of ["work/next", "work/child"]) {
all("#main .task-row").find((row) => row.dataset.task === task).querySelector("input.task-pick").click();
}
const button = document.getElementById("bulk-value");
found.armed = [button.disabled, text(button.querySelector(".count"))];
await choose(document.querySelector('#main select[aria-label="Business value of the selection"]'), "@VALUE@");
const shown = new Set(all("#toasts .toast"));
button.click();
found.toast = await fresh(shown, "Business value @VALUE@ for 2 tasks");
found.asked = asked;
found.posted = posted;
found.after = [button.disabled, all("#main .task-row input.task-pick:checked").length];
Object.assign(found, fits());
all('#main [aria-label="Estimation"] button').find((entry) => entry.dataset.estimation === "none").click();
found.reset = Boolean(await until(() => requestShown() === false, 8000));
return found;
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS)
ETA_LIST = """(async () => {@HELPERS@@PLAN_HELPERS@
const list = await until(() => all("#main .task-row .task-eta").length >= 8, 8000);
if (!list) {
return {error: text(document.getElementById("main"))};
}
const number = (value) => (value === "" ? null : Number(value));
const read = () => Object.fromEntries(all("#main .task-row").map((row) => {
const eta = row.querySelector(".task-eta");
return [row.dataset.task, [number(eta.dataset.start), number(eta.dataset.eta), eta.dataset.forecast === "true", text(eta), eta.title]];
}));
const toggle = async (mode) => {
const before = JSON.stringify(read());
all('#main [aria-label="Estimation"] button').find((button) => button.dataset.estimation === mode).click();
await until(() => JSON.stringify(read()) !== before, 8000);
await sleep(300);
return read();
};
const found = {none: read(), modes: modes(), ...fits()};
found.points = await toggle("points");
found.back = await toggle("none");
return found;
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS)
TREND_READ = """
const trendOf = () => {
const select = document.querySelector('#main select[aria-label="Trend"]');
const days = document.querySelector('#main input[aria-label="Velocity days"]');
const note = document.querySelector("#main .burn-forecast");
const marker = document.querySelector("#main svg.burn .forecast-line");
return {
value: select.value, options: all("option", select).map((option) => [option.value, text(option)]), days: [Number(days.value), days.getClientRects().length > 0],
note: note ? [note.dataset.forecast === "" ? null : Number(note.dataset.forecast), text(note)] : null, marker: marker ? Number(marker.dataset.forecast) : null,
lines: all("#main svg.burn path.burn-trend").map((path) => [path.dataset.series, Number(path.dataset.from), Number(path.dataset.to)]),
points: all('#main svg.burn circle.burn-point[data-series="remaining"]').map((circle) => [Number(circle.dataset.t), Number(circle.dataset.value)]),
};
};
const chart = async () => until(() => document.querySelector("#main svg.burn") && document.querySelector('#main select[aria-label="Trend"]'), 8000);
"""
TREND_SET = """(async () => {@HELPERS@@PLAN_HELPERS@@TREND_READ@
if (!(await chart())) {
return {error: text(document.getElementById("main"))};
}
const before = trendOf();
const shown = new Set(all("#toasts .toast"));
await choose(document.querySelector('#main select[aria-label="Trend"]'), "@MODE@");
const toasts = [await fresh(shown, "Trend: ")];
if ("@MODE@" === "velocity") {
const days = document.querySelector('#main input[aria-label="Velocity days"]');
const again = new Set(all("#toasts .toast"));
days.value = "@DAYS@";
days.dispatchEvent(new Event("change", {bubbles: true}));
toasts.push(await fresh(again, "Trend: velocity over @DAYS@ days"));
}
await sleep(400);
return {before, after: trendOf(), toasts, ...fits()};
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS).replace("@TREND_READ@", TREND_READ)
TREND_RELOADED = """(async () => {@HELPERS@@PLAN_HELPERS@@TREND_READ@
if (!(await chart())) {
return {error: text(document.getElementById("main"))};
}
await sleep(300);
return {...trendOf(), ...fits()};
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS).replace("@TREND_READ@", TREND_READ)
STEP_TIPS = """(async () => {@HELPERS@@PLAN_HELPERS@@TREND_READ@
if (!(await chart())) {
return {error: text(document.getElementById("main"))};
}
const steps = all('#main svg.burn circle.burn-point[data-series="remaining"]').map((circle) => ({
t: Number(circle.dataset.t), started: circle.dataset.started, ended: circle.dataset.ended, joined: circle.dataset.joined, tip: text(circle.querySelector("title")),
series: all(`#main svg.burn circle.burn-point[data-step="${circle.dataset.step}"]`).map((mark) => mark.dataset.series),
}));
const shown = new Set(all("#toasts .toast"));
await choose(document.querySelector('#main select[aria-label="Trend"]'), "off");
const reset = await fresh(shown, "Trend: off");
await sleep(300);
return {steps, reset, note: Boolean(document.querySelector("#main .burn-forecast")), lines: all("#main svg.burn path.burn-trend").length, ...fits()};
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS).replace("@TREND_READ@", TREND_READ)
TASKS_3D = """(async () => {@HELPERS@@PLAN_HELPERS@
const note = () => text(document.querySelector("#main .space-note")) || "";
const ready = await until(() => window.e2eGraphs && document.querySelector("#main .graph3d canvas") && !note().startsWith("Loading"), 15000);
if (!ready) {
return {error: text(document.querySelector("#main .graph"))};
}
const graph = () => window.e2eGraphs[window.e2eGraphs.length - 1];
const id = (end) => (typeof end === "object" ? end.id : end);
const look = () => {
const data = graph().graphData();
const color = graph().linkColor();
return {
note: note(), layouts: all('#main [aria-label="Layout"] button').map((button) => [button.dataset.layout, button.getAttribute("aria-pressed")]),
nodes: data.nodes.map((node) => [node.id, Number.isFinite(node.fz) ? node.fz : null, Number.isFinite(node.ts) ? node.ts : null]),
links: data.links.map((link) => [id(link.source), id(link.target), link.kind, color(link)]),
label: (() => {
const shown = graph().nodeLabel()(data.nodes.find((node) => node.id === "work/t5"));
return shown instanceof Element ? shown.textContent : String(shown);
})(),
};
};
const layout = async (mode, word) => {
all('#main [aria-label="Layout"] button').find((button) => button.dataset.layout === mode).click();
await until(() => note().endsWith(word), 8000);
await sleep(400);
return look();
};
const css = getComputedStyle(document.documentElement);
const palette = Object.fromEntries(["violet", "info", "warn"].map((role) => [role, css.getPropertyValue(`--${role}`).trim()]));
const force = look();
const time = await layout("time", "Time");
const field = document.querySelector('#main input[aria-label="Find a node"]');
field.value = "work/t5";
field.dispatchEvent(new Event("input", {bubbles: true}));
await sleep(200);
const chip = all("#main .hits .chip").find((node) => node.dataset.hit === "work/t5");
if (!chip) {
return {error: "no search hit", force, time};
}
chip.click();
await sleep(600);
const dialog = document.getElementById("popup");
const eta = dialog.querySelector(".task-eta");
const popup = {
open: dialog.open, title: text(dialog.querySelector(".popup-box .panel-title")), id: text(dialog.querySelector(".popup-head .task-id")), picked: document.querySelector("#main .graph3d").dataset.picked || null,
eta: eta ? eta.dataset.eta : null, open_button: name(dialog.querySelector(".popup-body .actions button")), line: dialog.querySelector(".leader line").getAttribute("display") !== "none",
};
(document.activeElement || document.body).dispatchEvent(new KeyboardEvent("keydown", {key: "Escape", bubbles: true}));
await sleep(300);
popup.closed = [dialog.open, document.querySelector("#main .graph3d").dataset.picked || null];
const back = await layout("none", "Force");
await sleep(1200);
return {palette, force, time, popup, back: back.note, ...fits()};
})()""".replace("@HELPERS@", HELPERS).replace("@PLAN_HELPERS@", PLAN_HELPERS)
PLAN_VALUES = {1920: "13", 1366: "21", 390: "34"}
TREND_DAYS = {1920: 7, 1366: 3, 390: 14}
STATUS_ORDER = ["queued", "awaiting", "claimed", "running", "blocked", "stale", "done", "stopped"]
CHOICE_REQUEST = "send me your open decisions as choices"
CHOICE_CHANNELS = ["post", "call", "newsletter", "broadcast", "pan-pan", "security", "mayday"]
NIGHT_HINTS = [
("Drain", "Drain: the agents finish their task and take nothing new; your passkey signs it"),
("Suspend", "Suspend: the agents park their work, write the handover and idle; your passkey signs it"),
("Resume", "Resume: all agents continue from their handover; your passkey signs it"),
]
SLOW ="seq -f 'Row %g' 1 60; sleep 4; seq -f 'Row %g' 61 80; sleep 4; echo finished"
def labelled(probe: dict) -> dict:
return {key: probe[key] for key in ("unlabelled", "broken")}
@unittest.skipUnless(shutil.which(os.environ.get("CHROMIUM", "chromium")), "no Chromium")
class TestOrchestratorE2E(HubCase):
def setUp(self):
super().setUp()
OUT.mkdir(parents=True, exist_ok=True)
now = time.time()
self.root = self.write_tree()
feature = f"{self.root}/worktrees/github.com/me/demo/feature"
self.claim("s1", "Skills session")
self.write_council({
"work/s2/claim.json": {"office": "work/s2", "session": "s2", "runtime": "claude", "name": "Stage", "since": iso(now - 7200), "beat": now - 2000, "work": "x"},
"chair/claim.json": {"office": "chair", "session": "s1", "runtime": "claude", "name": "Skills session", "since": iso(now - 7200), "beat": now, "work": "allocate"},
"identity/oidc/claim.json": {"office": "identity/oidc", "session": "s2", "runtime": "claude", "name": "Stage", "since": iso(now - 7200), "beat": now - 2000, "work": "oidc line"},
"rasci.json": {
"work/stage": {"title": "Stage line", "status": "running", "worker": "Stage (this session)", "roles": {"Stage (this session)": "RA"}},
"work/feature": {
"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": "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"}]},
"radio-silence.json": {"since": now - 60, "until": now + 3600, "by": "Docs", "text": "disk full"},
"news.jsonl": self.jsonl([
{"ts": now - 600, "from": "Skills", "to": ["Stage"], "text": "rebase please", "class": "post", "v": 2},
{"ts": now - 500, "from": "Stage", "to": ["Skills"], "text": "gate red", "class": "call", "v": 2},
{"ts": now - 400, "from": "Maintainer", "to": [], "text": "main is green", "class": "newsletter", "v": 2},
{"ts": now - 300, "from": "Maintainer", "to": [], "text": "rotate keys", "class": "security", "v": 2},
{"ts": now - 60, "from": "Docs", "to": [], "text": "MAYDAY disk full", "class": "mayday", "v": 2},
]),
})
self.subagents(now)
self.request("feat: orchestrator", "")
self.create_ask({**ASK, "timeout": 3600})
self.base = f"http://{self.host}/"
def subagents(self, now: float) -> None:
def line(ts: float) -> dict:
return {"type": "user", "sessionId": "s1", "timestamp": iso(ts), "message": {"role": "user", "content": "x"}}
folder = self.projects / "-repo" / "s1" / "subagents"
folder.mkdir(parents=True)
(self.projects / "-repo" / "s1.jsonl").write_text(self.jsonl([line(now - 3600), line(now - 30)]), encoding="utf-8")
for ident, finished in (("abc", True), ("def", False)):
(folder / f"agent-{ident}.meta.json").write_text(json.dumps({"agentType": "Explore", "description": f"look {ident}", "spawnDepth": 1, "finished": finished}), encoding="utf-8")
(folder / f"agent-{ident}.jsonl").write_text(self.jsonl([line(now - 1800), line(now - 60)]), encoding="utf-8")
def drive(self, steps: list[dict], authenticator: dict | None, driver: str = "cdp.mjs", binary: str = os.environ.get("CHROMIUM", "chromium"), init: str | None = None) -> dict:
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as handle:
json.dump({"steps": steps, "authenticator": authenticator, "init": init}, handle)
self.addCleanup(os.unlink, handle.name)
done = subprocess.run(["node", str(HERE / driver), binary, handle.name], capture_output=True, text=True, timeout=900, check=False)
self.assertEqual(done.returncode, 0, done.stderr[-2000:])
return json.loads(done.stdout)
def beside(self, work: Callable[[], None]) -> None:
stop = threading.Event()
def loop() -> None:
while not stop.wait(0.2):
work()
thread = threading.Thread(target=loop, daemon=True)
thread.start()
self.addCleanup(thread.join, 10)
self.addCleanup(stop.set)
def shot(self, name: str) -> str:
return str(OUT / f"chromium-{name}.png")
def authenticator(self) -> dict:
signer = self.pin()
private = "".join(line for line in signer.pem.read_text(encoding="utf-8").splitlines() if not line.startswith("-----"))
return {"credentialId": base64.b64encode(signer.raw_id).decode(), "privateKey": private, "rpId": "localhost"}
def speaks(self, cues: tuple[str, ...]) -> None:
hub = self.httpd.hub
hub.settings.put({**hub.settings.get(), "speech": {"on": True, "classes": ["mayday", "pan-pan", "security"], "muted": []}}, hub.settings.get())
spoken = tone(6, 8000)
def synthesize(body: bytes) -> bytes:
word = json.loads(body)["text"]
if word in cues:
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
patch = mock.patch.object(hub.speech, "synthesize", synthesize)
patch.start()
self.addCleanup(patch.stop)
def station(self, tls: ssl.SSLContext | None = None, moved: str | None = None) -> str:
"""Args:
tls: serves https with this context instead of plain http.
moved: answers every request with a 302 to this URL instead of
the tone.
"""
httpd = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Station)
httpd.body, httpd.moved = tone(30, 22050), moved
if tls:
httpd.socket = tls.wrap_socket(httpd.socket, server_side=True)
thread = threading.Thread(target=httpd.serve_forever, args=(0.05,), daemon=True)
thread.start()
self.addCleanup(httpd.server_close)
self.addCleanup(thread.join, 10)
self.addCleanup(httpd.shutdown)
return f"{'https' if tls else 'http'}://127.0.0.1:{httpd.server_address[1]}{DLF_PATH if moved else '/tone.wav'}"
def redirected(self) -> tuple[str, list[str]]:
"""Returns:
A station that replays the recorded Deutschlandfunk chain, a 302
from st01.sslstream.dlf.de to an f111.rndfnk.com node, as a 302 to
a second https origin, and that origin as its released via.
"""
folder = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, folder)
key, cert = folder / "key.pem", folder / "cert.pem"
subprocess.run([
"openssl", "req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256", "-nodes", "-subj", "/CN=127.0.0.1",
"-addext", "subjectAltName=IP:127.0.0.1", "-days", "1", "-keyout", str(key), "-out", str(cert),
], capture_output=True, timeout=60, check=True)
tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
tls.load_cert_chain(cert, key)
node = urlsplit(self.station(tls))
origin = f"https://{node.netloc}"
return self.station(tls, f"{origin}{DLF_NODE}"), [origin]
def check_overlaps(self, browser: str, driver: str, binary: str) -> None:
steps = []
for width, height in WIDTHS:
steps.append({"width": width, "height": height, "url": f"{self.base}#stream/rules", "wait": 900, "probe": RULES_OVERLAP, "shot": str(OUT / f"{browser}-{width}-rules-form.png")})
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": ("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):
self.assertNotIn("error", rules)
self.assertNotIn("error", settings)
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("Directory (glob)", rules["review"]["labels"])
self.assertEqual(settings, {"found": [], "outside": []})
def check_radio(self, browser: str, driver: str, binary: str, redirected: bool = False) -> dict:
"""Args:
driver: cdp.mjs saves the station through the settings form and
signs its new origin with a virtual passkey; bidi.mjs has no
virtual authenticator, so the station is saved before the page
loads.
redirected: plays the recorded Deutschlandfunk redirect chain,
saved before the page loads with its resolved via.
"""
station, via = self.redirected() if redirected else (self.station(), [])
browser = f"{browser}-dlf" if redirected else browser
signs = driver == "cdp.mjs" and not redirected
if signs:
first = {"width": 1366, "height": 768, "url": f"http://localhost:{self.port}/#stream", "wait": 900, "probe": ADD_STATION.replace("@STATION@", station).replace("@NAME@", STATION_NAME), "shot": None, "gesture": True}
else:
hub = self.httpd.hub
hub.settings.put({**hub.settings.get(), "streams": [{"name": STATION_NAME, "url": station, "from": "00:00", "to": "24:00", "days": list(range(7)), "on": True, "via": via}]}, hub.settings.get())
first = {"width": 1366, "height": 768, "url": f"{self.base}#stream", "wait": 900, "probe": None, "shot": None}
steps = [
first,
{"width": 390, "height": 844, "url": None, "wait": 3000, "probe": RADIO_VIEW, "shot": str(OUT / f"{browser}-390-radio-station.png")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": RADIO_CLICK, "shot": str(OUT / f"{browser}-1366-radio-playing.png"), "gesture": True},
]
data = self.drive(steps, self.authenticator() if signs else None, driver, binary, MEDIA_LOG)
(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("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])
blocked = [entry for entry in view["log"] if entry.get("play") == "NotAllowedError"]
if not view["media"]["playing"]:
self.assertTrue(blocked, view)
self.assertEqual(view["state"], "blocked", view)
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)
self.assertEqual(click["state"], "playing", click)
self.assertEqual([entry for entry in click["log"] if entry.get("csp") == "media-src"], [])
return data
def popped(self, result: dict, title: str) -> None:
before, shown, gone = result["before"], result["shown"], result["gone"]
self.assertEqual((before["picked"], before["red"]), (False, False))
self.assertEqual((shown["open"], shown["picked"], shown["red"], shown["fade"], shown["title"]), (True, True, True, [True, "fade"], title), shown)
self.assertGreater(shown["size"], before["size"] * 1.4)
self.assertLess(shown["opacity"], 1)
self.assertIsNotNone(shown["line"], shown)
self.assertTrue(shown["onBox"] and shown["inside"], shown)
self.assertEqual((not shown["tall"], shown["wide"], shown["panel"]), (result["flat"], False, 0))
self.assertEqual(gone, {"picked": False, "size": before["size"], "red": False, "open": False, "outside": True})
self.assertTrue(result["last"])
def check_popups(self, browser: str, driver: str, binary: str) -> None:
steps = []
for width, height in WIDTHS:
steps.append({"width": width, "height": height, "url": f"{self.base}#agents/orbit", "wait": 1200, "probe": POPUP_ORBIT, "shot": str(OUT / f"{browser}-{width}-agents-orbit-popup.png")})
steps.append({"width": width, "height": height, "url": f"{self.base}#artifacts/orbit", "wait": 1200, "probe": POPUP_ARTIFACT, "shot": str(OUT / f"{browser}-{width}-artifacts-orbit-popup.png")})
for tab, query, prefix in (("artifacts", "feature", "branch:"), ("agents", "sta", "s2")):
steps.append({"width": 1366, "height": 768, "url": f"{self.base}#{tab}/3d", "wait": 900, "probe": PICK_3D.replace("@QUERY@", query).replace("@PREFIX@", prefix), "shot": str(OUT / f"{browser}-1366-{tab}-3d-picked.png")})
data = self.drive(steps, None, driver, binary)
(OUT / f"{browser}-popups.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual([error for error in data["errors"] if "(style-src-elem)" not in error and "/favicon.ico" not in error], [])
results = data["results"]
for index, (width, _) in enumerate(WIDTHS):
agent, artifact = results[2 * index : 2 * index + 2]
with self.subTest(browser=browser, width=width):
self.assertNotIn("error", agent)
self.assertNotIn("error", artifact)
self.assertTrue(agent["flat"], "#agents/orbit scrolls")
self.popped(agent, "Stage")
self.assertEqual(agent["shown"]["face"], {"key": "s2", "svg": True, "text": ""})
self.assertEqual(agent["shown"]["sections"], ["Tasks", "Claims", "Subagents", "History"])
self.assertTrue(agent["shown"]["composer"])
self.assertRegex(agent["shown"]["offices"], r"^Offices: .*identity/oidc")
escaped = agent["escaped"]
self.assertEqual((escaped["again"], escaped["open"], escaped["picked"], escaped["red"], escaped["size"]), (True, False, False, False, agent["before"]["size"]))
self.popped(artifact, artifact["shown"]["label"])
for found, tab in zip(results[-2:], ("artifacts", "agents"), strict=True):
with self.subTest(browser=browser, tab=tab):
self.assertNotIn("error", found)
self.assertEqual(found["empty"], [])
labels = [label for _, label in found["hits"]]
self.assertEqual(len(labels), len(set(labels)), labels)
self.assertLessEqual(len(labels), 8)
opened = found["opened"]
self.assertEqual((opened["open"], opened["title"], opened["picked"], opened["line"], opened["lists"], opened["flat"]), (True, opened["label"], opened["hit"], True, 0, found["flat"]), opened)
self.assertEqual(found["closed"], {"open": False, "picked": None})
artifacts, agents = results[-2:]
self.assertEqual((artifacts["twins"], [label for _, label in artifacts["twin"]]), (2, ["main"]))
self.assertTrue(artifacts["opened"]["hit"].startswith("branch:"), artifacts["opened"])
self.assertEqual((agents["hits"], agents["opened"]["title"]), ([["s2", "Stage"]], "Stage"))
def test_chromium_opens_agent_and_node_popups_and_finds_nodes_at_three_widths(self):
self.check_popups("chromium", "cdp.mjs", os.environ.get("CHROMIUM", "chromium"))
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_opens_agent_and_node_popups_and_finds_nodes_at_three_widths(self):
self.check_popups("firefox", "bidi.mjs", os.environ.get("FIREFOX", "firefox"))
def test_chromium_form_controls_never_overlap(self):
self.check_overlaps("chromium", "cdp.mjs", os.environ.get("CHROMIUM", "chromium"))
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_form_controls_never_overlap(self):
self.check_overlaps("firefox", "bidi.mjs", os.environ.get("FIREFOX", "firefox"))
def test_chromium_plays_a_saved_station(self):
self.check_radio("chromium", "cdp.mjs", os.environ.get("CHROMIUM", "chromium"))
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_plays_a_saved_station(self):
self.check_radio("firefox", "bidi.mjs", os.environ.get("FIREFOX", "firefox"))
def test_chromium_plays_deutschlandfunk_through_its_redirect(self):
self.check_radio("chromium", "cdp.mjs", os.environ.get("CHROMIUM", "chromium"), redirected=True)
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_plays_deutschlandfunk_through_its_redirect(self):
self.check_radio("firefox", "bidi.mjs", os.environ.get("FIREFOX", "firefox"), redirected=True)
def test_chromium_walks_every_screen_at_three_widths(self):
steps = [{"width": width, "height": height, "url": f"{self.base}#{screen}", "wait": 900, "probe": PROBE, "shot": self.shot(f"{width}-{screen.replace('/', '-')}")} for width, height in WIDTHS for screen in SCREENS]
steps += [{"width": 1366, "height": 768, "url": f"{self.base}#{old}", "wait": 600, "probe": PROBE, "shot": None} for old in ("graph", "graph/chord", "tree")]
steps.append({"width": 1366, "height": 768, "url": f"{self.base}#stream", "wait": 900, "probe": ACTIONS, "shot": None})
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-grant")})
data = self.drive(steps, None)
(OUT / "chromium-walk.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
probes = data["results"][: len(WIDTHS) * len(SCREENS)]
for probe, step in zip(probes, steps, strict=False):
with self.subTest(url=step["url"], width=step["width"]):
self.assertNotIn("error", probe)
self.assertEqual((probe["title"], probe["h1"]), ("(2) Agent Orchestrator", "Agent Orchestrator"))
self.assertEqual(probe["header"], ["palette-open", "settings-open"])
self.assertEqual((probe["tabs"], probe["subs"]), (TABS, SUBS))
self.assertEqual(probe["badges"], {"tab-stream": None, "tab-inbox": "2", "tab-rules": None, "tab-radio": None, "tab-agents": "1", "tab-artifacts": None, "tab-tasks": None})
self.assertEqual(probe["banner"], "MAYDAY · Docs: disk full")
self.assertFalse(probe["rail"])
self.assertEqual((probe["time"], probe["replay"]), ("24 h ▾", True))
self.assertGreater(probe["icons"], 8)
self.assertEqual(labelled(probe), {"unlabelled": [], "broken": []})
self.assertFalse(probe["wide"], "the page scrolls sideways")
self.assertEqual((probe["panel"], probe["nodeChips"]), (0, 0))
self.assertEqual(probe["composers"], ["spot"] if probe["hash"].startswith("#agents/") else [])
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"], [["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", ["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"], ["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+ nodes · \d+ edges · Force$")
agents = by_hash["#agents/orbit"]
self.assertEqual(agents["toolbar"], ["Radio", "Priority", "Day/Night"])
self.assertEqual(agents["spot"], {"hidden": True, "channels": CHOICE_CHANNELS})
hierarchy = by_hash["#agents/hierarchy"]
self.assertEqual([office for office, _ in hierarchy["org"]], ["chair", "identity/oidc", "work/s1", "work/s2"])
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"], [["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 ("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"], ["List", "Board", "RASCI", "Gantt", "Burndown and burnup", "3D"])
self.assertEqual(by_hash["#tasks/3d"]["layouts"], ["Force", "Time"])
self.assertTrue(by_hash["#tasks/3d"]["canvas"], by_hash["#tasks/3d"]["note"])
self.assertRegex(by_hash["#tasks/3d"]["note"], r"^6 nodes · \d+ edges · Force$")
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", []]])
self.assertEqual(by_hash["#tasks/rasci"]["matrix"], [6, 6, "CIRARASRARAR"])
self.assertEqual([probe["taskFilters"][3][1] for probe in probes if probe["hash"] == "#stream/inbox"], ["true"] * len(WIDTHS))
extra = data["results"][len(probes) :]
self.assertEqual([probe["hash"] for probe in extra[:3]], ["#agents/orbit", "#agents/chord", "#artifacts/tree"])
actions = extra[3]
self.assertNotIn("error", actions)
self.assertEqual(actions["keys"], ["#agents/chord", "#artifacts/tree", "#tasks/3d", "#stream"])
self.assertEqual(actions["ambience"], ["sky-look", "music-toggle", "leitstand", "notify"])
self.assertEqual((actions["settingsUnlabelled"], actions["popupUnlabelled"]), (0, 0))
self.assertEqual((actions["sections"], actions["radioFields"]), (["Ambience", "Location", "Appearance", "Music", "Notifications", "Internet radio", "Passkey"], 0))
for tool in ("Radio …", "Cancel mayday", "Priority …", "Drain", "Suspend", "Resume", "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["popup"], [["post", True], ["call", False]])
self.assertEqual(actions["centred"], [True, "none"])
calm, chips = extra[4:]
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"]]})
def test_chromium_sends_a_call_from_the_agent_panel_and_shows_its_receipt(self):
handled = []
def answer() -> None:
for script, args in self.scripts.calls[len(handled) :]:
handled.append(script)
if script.endswith("radio.py") and args[:2] == ["send", "call"]:
to, text, now = args[args.index("--to") + 1], args[-1], time.time()
rows = [{"ts": now, "from": "Operator", "to": [to], "text": text, "class": "call", "v": 2}, {"ts": now + 0.5, "from": to, "to": ["Operator"], "text": text, "class": "receipt", "v": 2, "call": now}]
with (self.council / "news.jsonl").open("a", encoding="utf-8") as handle:
handle.write(self.jsonl(rows))
self.beside(answer)
local = f"http://localhost:{self.port}/"
data = self.drive([{"width": 1366, "height": 768, "url": f"{local}#stream", "wait": 900, "probe": CALL, "shot": self.shot("1366-call-receipt")}], self.authenticator())
self.assertEqual(data["errors"], [])
result = data["results"][0]
self.assertNotIn("error", result)
self.assertIsNotNone(result["receipt"], result)
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"))
steps = [
{"width": 1366, "height": 768, "url": f"{self.base}#stream/radio", "wait": 900, "probe": SPEAK.replace("@CUE@", "e2e-cue-1"), "shot": self.shot("1366-radio-speaking"), "gesture": True},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": SPEAK_GRAPH, "shot": self.shot("1366-agents-speaking")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": SPEAK_END, "shot": None},
{"width": 390, "height": 844, "url": None, "wait": 0, "probe": SPEAK.replace("@CUE@", "e2e-cue-2"), "shot": self.shot("390-radio-speaking-reduced-motion"), "gesture": True, "media": [{"name": "prefers-reduced-motion", "value": "reduce"}]},
]
data = self.drive(steps, None)
(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.talks(speaking, graph)
self.assertEqual(ended, {"ended": True, "caption": False, "chips": 0, "faces": 0})
self.assertEqual((calm["who"], calm["line"], calm["ring"]), ("Stage speaking", "Disk almost full (e2e-cue-2)", "none"), calm)
self.assertEqual(calm["faces"]["speaking"], [True, "none"], calm)
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_shows_who_speaks_and_moves_only_the_speakers_mouth(self):
self.speaks(("e2e-cue-1",))
steps = [
{"width": 1366, "height": 768, "url": f"{self.base}#stream/radio", "wait": 900, "probe": SPEAK.replace("@CUE@", "e2e-cue-1"), "shot": str(OUT / "firefox-1366-radio-speaking.png"), "gesture": True},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": SPEAK_GRAPH, "shot": str(OUT / "firefox-1366-agents-speaking.png")},
]
data = self.drive(steps, None, "bidi.mjs", os.environ.get("FIREFOX", "firefox"))
(OUT / "firefox-speaking.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual([error for error in data["errors"] if "/favicon.ico" not in error], [])
self.talks(*data["results"])
def talks(self, speaking: dict, graph: dict) -> None:
"""Args:
speaking: the SPEAK probe while Stage (s2) is read aloud.
graph: the SPEAK_GRAPH probe on #agents/orbit right after it.
"""
self.assertEqual((speaking["who"], speaking["line"], speaking["chip"], speaking["ring"]), ("Stage speaking", "Disk almost full (e2e-cue-1)", "s2", "speaking"), speaking)
self.assertEqual(speaking["faces"], {"speaking": [True, "talk"], "silent": [False, "none"], "keys": ["s2"]}, speaking)
self.assertEqual((graph["node"] or "").split(",")[0], "Stage", graph)
self.assertEqual((graph["ring"], graph["mouth"], graph["silent"]), ("speaking-node", "talk", "none"), graph)
def test_chromium_records_transcribes_and_sends_a_signed_call_with_voice(self):
local = f"http://localhost:{self.port}/"
steps = [
{"width": 1366, "height": 768, "url": f"{local}#agents/orbit", "wait": 1200, "probe": COMPOSE.replace("@TO@", "Stage"), "shot": self.shot("1366-radio-spot-composer"), "gesture": True},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": COMPOSE_SEND.replace("@ATTACH@", "false"), "shot": None},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": COMPOSE.replace("@TO@", "Stage"), "shot": None, "gesture": True},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": COMPOSE_SEND.replace("@ATTACH@", "true"), "shot": self.shot("1366-radio-spot-composer-sent")},
]
data = self.drive(steps, self.authenticator())
(OUT / "chromium-composer.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
composed, plain, again, sent = data["results"]
self.assertNotIn("error", composed)
self.assertEqual(composed["channels"], ["post", "call", "newsletter", "broadcast", "pan-pan", "security", "mayday"])
self.assertIn("Stage", composed["recipients"])
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"], ["Attach recording", False])
self.assertEqual(composed["clashes"], {"found": [], "outside": []})
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 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 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"))
def test_chromium_clears_the_mayday_from_the_radio_spot_only_after_confirmation_and_passkey_at_three_widths(self):
local = f"http://localhost:{self.port}/"
steps = [{"width": width, "height": height, "url": f"{local}?{width}#agents/orbit", "wait": 1200, "probe": CLEAR_MAYDAY, "shot": self.shot(f"{width}-radio-spot-mayday-cleared")} for width, height in WIDTHS]
data = self.drive(steps, self.authenticator())
(OUT / "chromium-mayday.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
question = "Lift the radio silence of the active mayday?"
for result, (width, _) in zip(data["results"], WIDTHS, strict=True):
with self.subTest(width=width):
self.assertNotIn("error", result)
self.assertEqual(result["toolbar"], ["Radio", "Priority", "Day/Night", "Drain", "Suspend", "Resume"])
self.assertEqual(result["title"], "Cancel mayday: lift the radio silence so the agents write again; your passkey signs it")
self.assertEqual(result["declined"], {"question": [question], "posted": [], "toasts": []})
self.assertEqual(result["accepted"], {"question": [question], "posted": ["/api/board/sign", "/api/board/clear-mayday"]})
self.assertTrue((result["done"] or "").startswith("Cancel mayday: done"), result)
self.assertEqual(self.scripts.calls, [(skill_scripts.RADIO, ["clear", "--force", "--session", "Operator"])] * len(WIDTHS))
def test_chromium_sends_every_class_from_the_radio_spot_and_keeps_its_gear_settings_through_a_reload_at_three_widths(self):
local = f"http://localhost:{self.port}/"
values = {1920: ("0.35", "false", "true"), 1366: ("0.45", "true", "false"), 390: ("0.55", "false", "true")}
steps = [{"width": 1366, "height": 768, "url": f"{local}#stream/radio", "wait": 1200, "probe": RADIO_JUMP, "shot": self.shot("1366-radio-tab-jump-to-spot")}]
for width, height in WIDTHS:
squelch, tone, post = values[width]
gear = GEAR_SAVE.replace("@SQUELCH@", squelch).replace("@TONE@", tone).replace("@POST@", post)
steps += [
{"width": width, "height": height, "url": f"{local}?{width}#agents/orbit", "wait": 1200, "probe": SPOT_SEND.replace("@TEXT@", f"spot {width}"), "shot": self.shot(f"{width}-radio-spot-sent")},
{"width": width, "height": height, "url": None, "wait": 0, "probe": gear, "shot": None},
{"width": width, "height": height, "url": f"{local}?{width}-reloaded#agents/orbit", "wait": 1200, "probe": GEAR_READ, "shot": self.shot(f"{width}-radio-spot-gear")},
]
data = self.drive(steps, self.authenticator())
(OUT / "chromium-radio-spot.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
labels = dict(zip(CHOICE_CHANNELS, ["Post", "Call", "Newsletter", "Broadcast", "Pan-Pan", "Sécurité", "Mayday"], strict=True))
mayday = "Send a mayday to all? All agents pause writing steps until you cancel it."
jump, *walked = data["results"]
self.assertEqual(jump, {"composers": 0, "hash": "#agents/orbit", "open": True, "focused": True})
for index, (width, _) in enumerate(WIDTHS):
sent, saved, read = walked[3 * index : 3 * (index + 1)]
squelch, tone, post = values[width]
with self.subTest(width=width):
for probe in (sent, saved, read):
self.assertNotIn("error", probe)
self.assertEqual((sent["closed"], sent["open"]), ([True, "false"], [False, "true", True]))
self.assertEqual(sent["inline"], ["static", True, True])
self.assertEqual((sent["toolbar"], sent["channels"], sent["record"]), (["Radio", "Priority", "Day/Night"], CHOICE_CHANNELS, ["Record"]))
self.assertEqual(sent["heads"], [["Radio settings", "Radio settings: tones, squelch and read aloud"], ["Cancel mayday", "Cancel mayday: lift the radio silence so the agents write again; your passkey signs it"]])
self.assertEqual((sent["tall"], sent["wide"]), (False, False))
self.assertEqual([run["cls"] for run in sent["runs"]], CHOICE_CHANNELS)
for run in sent["runs"]:
direct = run["cls"] in ("post", "call")
self.assertEqual((run["direct"], run["question"], run["left"]), (direct, [mayday] if run["cls"] == "mayday" else [], ""), run)
self.assertTrue((run["toast"] or "").startswith(f"{labels[run['cls']]} to {'Stage' if direct else 'all'}: sent"), run)
self.assertEqual((saved["expanded"], saved["sections"], saved["clashes"]), ("true", ["Tones", "Read radio aloud"], {"found": [], "outside": []}))
self.assertEqual(saved["tones"], ["MAYDAY / PAN-PAN", "Reviews, asks, decisions", "Post and calls to you", "All radio"])
self.assertEqual([entry[0] for entry in saved["classes"]], ["Post", "Call", "Newsletter", "Broadcast", "MAYDAY", "PAN-PAN", "Sécurité"])
self.assertEqual([entry[1:] for entry in saved["classes"] if entry[0] == "MAYDAY"], [[True, True]])
self.assertEqual((saved["toast"], saved["after"]), ("Radio settings saved", ["false", False]))
self.assertEqual(saved["general"], {"sections": ["Ambience", "Location", "Appearance", "Music", "Notifications", "Internet radio", "Passkey"], "radio": 0})
self.assertEqual((saved["tall"], saved["wide"]), (False, False))
self.assertEqual(read, {"squelch": float(squelch), "tone": tone == "true", "post": post == "true", "tall": False, "wide": False})
heads = {"post": ["post", "--to", "Stage"], "call": ["send", "call", "--to", "Stage"], "newsletter": ["post"]}
radio = [args for script, args in self.scripts.calls if script == skill_scripts.RADIO]
sends = [(width, cls) for width, _ in WIDTHS for cls in CHOICE_CHANNELS]
self.assertEqual([args[:-1] for args in radio], [[*heads.get(cls, ["send", cls]), "--session", "Operator"] for _, cls in sends])
for args, (width, cls) in zip(radio, sends, strict=True):
with self.subTest(width=width, cls=cls):
self.assertTrue(args[-1].endswith(f"] spot {width} {cls}"), args)
stored = self.httpd.hub.settings.get()
self.assertEqual((stored["squelch"], stored["sounds"]["radio"]["on"], "post" in stored["speech"]["classes"]), (0.55, False, True))
def test_chromium_keeps_the_3d_camera_through_two_state_refreshes_at_three_widths(self):
steps = [{"width": width, "height": height, "url": f"{self.base}?{width}#agents/3d", "wait": 900, "probe": CAMERA, "shot": self.shot(f"{width}-agents-3d-steady-camera")} for width, height in WIDTHS]
data = self.drive(steps, None, init=GRAPH_HOOK)
(OUT / "chromium-camera.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
for result, (width, _) in zip(data["results"], WIDTHS, strict=True):
with self.subTest(width=width):
self.assertNotIn("error", result)
self.assertTrue(result["hooked"], result)
self.assertGreater(math.dist(result["built"], result["moved"]), 1, result)
for refresh in result["refreshes"]:
self.assertEqual((refresh["rebuilt"], refresh["settled"]), (True, True), result)
self.assertLess(math.dist(refresh["camera"], result["moved"]), 0.01, result)
self.assertGreater(math.dist(result["reset"], result["moved"]), 1, result)
def check_night_menu(self, browser: str, driver: str, binary: str, signs: bool) -> None:
"""Args:
signs: cdp.mjs signs every prompt with its virtual passkey; bidi.mjs
has no virtual authenticator, so the probe answers each prompt
the way a canceled passkey dialog does.
"""
authenticator = self.authenticator()
probe = NIGHT_MENU.replace("@SIGNS@", "true" if signs else "false")
looks = [{"width": width, "height": height, "url": f"http://localhost:{self.port}/?{width}#agents/orbit", "wait": 1200, "probe": NIGHT_LOOK, "shot": str(OUT / f"{browser}-{width}-night-menu-tooltips.png")} for width, height in WIDTHS]
step = {"width": 1366, "height": 768, "url": f"http://localhost:{self.port}/#agents/orbit", "wait": 1200, "probe": probe, "shot": str(OUT / f"{browser}-1366-night-menu.png")}
data = self.drive([*looks, step], authenticator if signs else None, driver, binary)
(OUT / f"{browser}-night-menu.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual([error for error in data["errors"] if "(style-src-elem)" not in error and "/favicon.ico" not in error], [])
*seen, found = data["results"]
for look, (width, _) in zip(seen, WIDTHS, strict=True):
with self.subTest(browser=browser, width=width):
self.assertNotIn("error", look)
self.assertEqual(look["children"], [["span", ""], ["div", "actions"]])
self.assertEqual((look["badge"], look["paragraphs"], look["wide"]), (["Day"], 0, False))
self.assertEqual(look["buttons"], [list(entry) for entry in NIGHT_HINTS])
self.assertNotIn("error", found)
self.assertEqual(found["labels"], ["Drain", "Suspend", "Resume"])
for run, action in zip(found["runs"], ("drain", "suspend", "resume"), strict=True):
with self.subTest(browser=browser, action=action):
self.assertEqual(run["asked"], 1, run)
self.assertEqual(len(run["question"]), 1, run)
self.assertIn(f"kvb-agent-{action}", run["question"][0])
signed = [["/api/board/sign", ["action"], action]]
if signs:
self.assertEqual(run["posted"], [*signed, [f"/api/board/{action}", ["proof"], None]])
self.assertTrue((run["toast"] or "").startswith(f"{run['label']}: done"), run)
else:
self.assertEqual(run["posted"], signed)
self.assertTrue((run["toast"] or "").startswith(f"{run['label']}: not signed"), run)
nights = [args for script, args in self.scripts.calls if script == skill_scripts.NIGHT]
self.assertEqual(nights, [[action, "--session", "Operator"] for action in ("drain", "suspend", "resume")] if signs else [])
if not signs:
self.assertEqual(self.scripts.calls, [])
def test_chromium_runs_drain_suspend_and_resume_only_after_the_passkey_signs(self):
self.check_night_menu("chromium", "cdp.mjs", os.environ.get("CHROMIUM", "chromium"), signs=True)
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_asks_the_passkey_for_drain_suspend_and_resume_and_runs_nothing_unsigned(self):
self.check_night_menu("firefox", "bidi.mjs", os.environ.get("FIREFOX", "firefox"), signs=False)
def test_chromium_deletes_a_merged_branch_after_dry_run_and_signature(self):
tree = json.loads(self.tree_file.read_text(encoding="utf-8"))
tree["delete_ready"], tree["delete_reason"] = True, ""
self.tree_file.write_text(json.dumps(tree), encoding="utf-8")
demo = f"{self.root}/repos/github.com/me/demo"
Path(demo).mkdir(parents=True)
fake = self.outside / "bin" / "pkgmgr"
fake.parent.mkdir()
fake.write_text(FAKE_PKGMGR.format(python=sys.executable), encoding="utf-8")
fake.chmod(0o755)
worktree = f"{self.root}/worktrees/github.com/me/demo/feature-done"
(fake.parent / "plan.json").write_text(json.dumps([{"branch": "feature/done", "worktree": worktree, "local_sha": DONE_TIP, "head": DONE_TIP, "state": "merged-clean", "skip": None}]), encoding="utf-8")
following = json.loads(json.dumps(tree))
following["repos"][0]["items"] = [item for item in following["repos"][0]["items"] if item["branch"] != "feature/done"]
(fake.parent / "next-tree.json").write_text(json.dumps(following), encoding="utf-8")
authenticator = self.authenticator()
trust = verifier.Trust(approvers=self.approvers, replay=self.replay / "cleanup.json", origin=self.origin)
runner = host.Cleanup(trust, self.tree_file, fake)
jobs, results = self.cleanup / "jobs", self.cleanup / "results"
self.beside(lambda: any(jobs.iterdir()) and host.cycle(jobs, results, runner))
local = f"http://localhost:{self.port}/"
steps = [
{"width": 1366, "height": 768, "url": f"{local}#artifacts/tree", "wait": 1200, "probe": OPEN_DELETE, "shot": self.shot("1366-delete-dry-run")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": CONFIRM, "shot": self.shot("1366-delete-result")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": AFTER, "shot": self.shot("1366-delete-tree-after")},
]
data = self.drive(steps, authenticator)
(OUT / "chromium-delete.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
opened, confirmed, after = data["results"]
self.assertNotIn("error", opened)
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, 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")]])
calls = [json.loads(line)["argv"] for line in (fake.parent / "calls.jsonl").read_text(encoding="utf-8").splitlines()]
self.assertEqual(calls, [
["branch", "cleanup", "--json", "--local", "--branch", "feature/done"],
["branch", "cleanup", "--apply", "--json", "--local", "--branch", "feature/done"],
["branch", "tree", "--format", "json", "--output", str(self.tree_file)],
])
def test_chromium_folds_the_hierarchy_by_keyboard_reorders_agents_and_hands_an_office_over_with_a_signature(self):
signer = self.pin()
trust = verifier.Trust(approvers=self.approvers, replay=self.replay / "offices.json", origin=self.origin)
runner = office_host.Offices(trust, self.council, COUNCIL_SCRIPT, self.outside / "home")
jobs, results = self.offices / "jobs", self.offices / "results"
self.beside(lambda: any(jobs.iterdir()) and office_host.cycle(jobs, results, runner))
private = "".join(line for line in signer.pem.read_text(encoding="utf-8").splitlines() if not line.startswith("-----"))
authenticator = {"credentialId": base64.b64encode(signer.raw_id).decode(), "privateKey": private, "rpId": "localhost"}
local = f"http://localhost:{self.port}/"
steps = [
{"width": 1366, "height": 768, "url": f"{local}#agents/hierarchy", "wait": 1200, "probe": ORG_OPEN, "shot": self.shot("1366-hierarchy-open")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "keys": ["Enter"], "probe": ORG_FOLDED, "shot": None},
{"width": 1366, "height": 768, "url": None, "wait": 0, "keys": ["Enter"], "probe": ORDER_MOVED, "shot": self.shot("1366-hierarchy-order")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": OPEN_TAKEOVER, "shot": self.shot("1366-hierarchy-takeover")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": CONFIRM_TAKEOVER, "shot": self.shot("1366-hierarchy-takeover-result")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": AFTER_TAKEOVER, "shot": self.shot("1366-hierarchy-after")},
]
data = self.drive(steps, authenticator)
(OUT / "chromium-hierarchy.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
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", "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("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: 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"))
written = [json.loads(path.read_text(encoding="utf-8")) for path in results.glob("*.json")]
self.assertEqual([[(item["office"], item["action"], item["status"]) for item in result["items"]] for result in written], [[("identity/oidc", "takeover", "done")]])
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": "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"}
self.assertFalse(self.tool("rule_request", arguments)["isError"])
def layout(self, probe: dict) -> None:
self.assertNotIn("error", probe)
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"], ["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))
def test_chromium_runs_a_command_into_its_console_denies_by_call_and_accepts_a_rule(self):
self.inbox_work()
signer = self.pin()
trust = ["--approvers", str(self.approvers), "--replay", str(self.replay / "runner.json")]
def run_jobs() -> None:
if any(job["status"] == "queued" for job in self.httpd.hub.jobs.listed()):
subprocess.run([sys.executable, str(RUNNER), "--port", str(self.port), "--interval", "0.1", *trust, "--once"], capture_output=True, timeout=120, check=False)
self.beside(run_jobs)
private = "".join(line for line in signer.pem.read_text(encoding="utf-8").splitlines() if not line.startswith("-----"))
authenticator = {"credentialId": base64.b64encode(signer.raw_id).decode(), "privateKey": private, "rpId": "localhost"}
inbox = f"http://localhost:{self.port}/#stream/inbox"
steps = [
{"width": 1366, "height": 768, "url": inbox, "wait": 1200, "probe": RUN_START, "shot": self.shot("1366-inbox-console-running")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": RUN_HELD, "shot": self.shot("1366-inbox-console-scrolled-up")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": RUN_DONE, "shot": self.shot("1366-inbox-console-done")},
]
for width, height in WIDTHS:
fresh = f"http://localhost:{self.port}/?{width}#stream/inbox"
steps.append({"width": width, "height": height, "url": fresh, "wait": 1200, "probe": LAYOUT, "shot": self.shot(f"{width}-inbox-console")})
steps.append({"width": width, "height": height, "url": None, "wait": 0, "probe": DENY_SHEET, "shot": self.shot(f"{width}-inbox-deny-sheet")})
steps += [
{"width": 1366, "height": 768, "url": inbox, "wait": 1200, "probe": DENY, "shot": self.shot("1366-inbox-deny-undo")},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": DENIED, "shot": None},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": ACCEPT, "shot": self.shot("1366-inbox-rule-accepted")},
]
data = self.drive(steps, authenticator)
(OUT / "chromium-inbox.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
started, held, done, *rest = data["results"]
self.assertNotIn("error", 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"]), ("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("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], ["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("Denial (Call): Bash: make deploy"), denied)
self.assertFalse(denied["sheet"])
self.assertTrue(gone["gone"])
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", "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: 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)])
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_draws_the_inbox_console_and_the_rule_request_at_three_widths(self):
self.inbox_work()
now = time.time()
fields = {"task": "operator/greet", "command": SLOW, "directory": str(self.outside), "sha256": "", "status": "done", "proof": None}
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]
data = self.drive(steps, None, "bidi.mjs", os.environ.get("FIREFOX", "firefox"))
(OUT / "firefox-inbox.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual([error for error in data["errors"] if "(style-src-elem)" not in error and "/favicon.ico" not in error], [])
for probe, (width, _, shot) in zip(data["results"], shots, strict=True):
with self.subTest(width=width):
self.layout(probe)
self.assertEqual(probe["console"], "doneExit code 0 after 9 s")
self.assertTrue(shot.is_file() and shot.stat().st_size > 1000, shot)
def choice_steps(self, browser: str, signs: bool, questions: dict[int, str]) -> list[dict]:
flag = "true" if signs else "false"
local = f"http://localhost:{self.port}/"
steps = []
for index, (width, height) in enumerate(WIDTHS):
cards = len(WIDTHS) - index if signs else len(WIDTHS)
steps += [
{"width": width, "height": height, "url": f"{local}?{width}#stream/inbox", "wait": 1200, "probe": CHOICE_VIEW.replace("@CARDS@", str(cards)), "shot": str(OUT / f"{browser}-{width}-tasks-choices.png")},
{"width": width, "height": height, "url": None, "wait": 0, "probe": CHOICE_SEND.replace("@SIGNS@", flag), "shot": str(OUT / f"{browser}-{width}-choice-request.png")},
{"width": width, "height": height, "url": None, "wait": 0, "probe": CHOICE_ANSWER.replace("@SIGNS@", flag).replace("@QUESTION@", questions[width]), "shot": str(OUT / f"{browser}-{width}-choice-answer.png")},
*({"width": width, "height": height, "url": f"{local}?{width}#tasks/{view}", "wait": 900, "probe": TASKS_FIT, "shot": str(OUT / f"{browser}-{width}-tasks-{view}-fit.png")} for view in TASK_VIEWS),
]
return steps
def requested(self, sent: dict, signs: bool) -> None:
"""Args:
sent: the CHOICE_SEND probe: two agents ticked, then every channel
button pressed once.
"""
labels = dict(zip(CHOICE_CHANNELS, ["Post", "Call", "Newsletter", "Broadcast", "Pan-Pan", "Sécurité", "Mayday"], strict=True))
self.assertEqual(len(sent["picked"]), 2, sent)
self.assertEqual((sent["counted"], sent["fields"], sent["tall"], sent["wide"]), ("2", 0, False, False))
self.assertEqual(sent["opened"], [True, len(CHOICE_CHANNELS)])
self.assertEqual([run["channel"] for run in sent["runs"]], CHOICE_CHANNELS)
for run in sent["runs"]:
with self.subTest(channel=run["channel"]):
to = sent["picked"] if run["channel"] in ("post", "call") else ["*"]
signed = [["/api/choices/sign", ["class", "to"], run["channel"], to]]
self.assertEqual((run["disabled"], run["asked"], len(run["question"])), (False, 1, int(run["channel"] == "mayday")), run)
title = f"Choice request ({labels[run['channel']]}) to {', '.join(to) if to != ['*'] else 'all'}"
self.assertEqual(run["posted"], [*signed, ["/api/choices", ["class", "proof", "to"], run["channel"], to]] if signs else signed)
self.assertTrue((run["toast"] or "").startswith(f"{title}: {'sent' if signs else 'not signed'}"), run)
def answered(self, result: dict, signs: bool) -> None:
self.assertNotIn("error", result)
self.assertEqual((result["title"], result["asked"], result["closed"], result["tall"], result["wide"]), ("Wait: your passkey signs this answer, a post to Stage", 1, [False, 0], False, False))
signed = [["/api/sign", ["items"], None, None]]
self.assertEqual(result["posted"], [*signed, ["/api/inbox/approve", ["items", "proof"], None, None]] if signs else signed)
self.assertTrue((result["toast"] or "").startswith("Choice: Wait" + ("" if signs else ": not signed")), result)
self.assertEqual(result["gone"], True if signs else None)
def check_choices(self, browser: str, driver: str, binary: str, signs: bool) -> None:
"""Args:
signs: cdp.mjs signs every prompt with its virtual passkey; bidi.mjs
has no virtual authenticator, so the probes answer each prompt
the way a canceled passkey dialog does.
"""
self.inbox_work()
self.create_ask({**ASK, "command": "make choices", "timeout": 3600})
authenticator = self.authenticator()
questions = {width: f"Pick a name at {width} px?" for width, _ in WIDTHS}
decisions = {width: self.tool("decision_request", {"question": question, "options": OPTIONS, "agent": "Stage", "recommended": ""})["structuredContent"]["id"] for width, question in questions.items()}
data = self.drive(self.choice_steps(browser, signs, questions), authenticator if signs else None, driver, binary)
(OUT / f"{browser}-choices.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual([error for error in data["errors"] if "(style-src-elem)" not in error and "/favicon.ico" not in error], [])
per = 3 + len(TASK_VIEWS)
for index, (width, _) in enumerate(WIDTHS):
view, sent, answer, *fitted = data["results"][per * index : per * (index + 1)]
with self.subTest(browser=browser, width=width):
self.assertNotIn("error", view)
self.assertEqual(view["kinds"], ["Ask", "Choice", "Operator", "Review", "Rule request"])
self.assertEqual(view["filter"], ["Waits on Operator", "true"])
self.assertEqual(view["channels"], [[channel, label, channel in ("post", "call")] for channel, label in zip(CHOICE_CHANNELS, ["Post", "Call", "Newsletter", "Broadcast", "Pan-Pan", "Sécurité", "Mayday"], strict=True)])
self.assertEqual((view["fields"], view["choices"], view["multi"], view["unlabelled"], view["tall"], view["wide"]), (0, 0, ["Request choices", False, 0, "0", 0], [], False, False))
self.assertEqual(view["bar"], ["Tasks", view["bar"][1], ["toolbar", "chips", "body"]])
self.assertEqual(view["bar"][1] == 1, width != 390, view["bar"])
self.assertGreaterEqual(len(view["agents"]), 2, view)
self.assertIn(["Choice", ["Merge now", "Wait"]], view["cards"])
self.requested(sent, signs)
self.answered(answer, signs)
for fit, name in zip(fitted, TASK_VIEWS, strict=True):
self.assertEqual((fit["hash"], fit["bar"], fit["choices"], fit["tall"], fit["wide"]), (f"#tasks/{name}", CHOICE_CHANNELS, 0, False, False), fit)
self.assertEqual((fit["rows"] == 1, fit["layout"]), (width != 390, ["toolbar", "chips", "body"]), fit)
if name == "board":
self.assertEqual(fit["columns"], ["queued", "running", "blocked", "done", "stopped"])
hub = self.httpd.hub
if not signs:
self.assertEqual((self.scripts.calls, hub.messages.listed()), ([], []))
self.assertEqual([hub.decisions.get(decision)["status"] for decision in decisions.values()], ["pending"] * len(WIDTHS))
return
requests = [record for record in hub.messages.listed() if record["text"] == CHOICE_REQUEST]
self.assertEqual((len(requests), sorted({record["class"] for record in requests})), (len(WIDTHS) * 9, sorted(CHOICE_CHANNELS)))
for record in requests:
self.assertTrue(self.tool("operator_message", {"id": record["id"], "text": CHOICE_REQUEST, "to": record["to"]})["structuredContent"]["match"], record["id"])
radio = [args for script, args in self.scripts.calls if script == skill_scripts.RADIO]
self.assertEqual(len(radio), len(requests) + len(WIDTHS))
for width, decision in decisions.items():
with self.subTest(width=width):
state = self.tool("decision_wait", {"id": decision, "timeout_seconds": 0})["structuredContent"]
self.assertEqual((state["status"], state["choice"], state["answer"]), ("decided", "Wait", f"Choice for decision {decision} ({questions[width]}): Wait"))
found = self.tool("operator_message", {"id": state["message"], "text": state["answer"], "to": "Stage"})["structuredContent"]
self.assertEqual((found["match"], found["class"], found["to"]), (True, "post", "Stage"))
self.assertIn(["post", "--to", "Stage", "--session", "Operator", f"[kvb-orchestrator msg {state['message']}] {state['answer']}"], radio)
def test_chromium_requests_choices_on_every_channel_and_signs_a_choice_at_three_widths(self):
self.check_choices("chromium", "cdp.mjs", os.environ.get("CHROMIUM", "chromium"), signs=True)
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_asks_the_passkey_for_every_choice_request_and_answer_and_sends_nothing_unsigned(self):
self.check_choices("firefox", "bidi.mjs", os.environ.get("FIREFOX", "firefox"), signs=False)
def plan_work(self, now: float) -> None:
register = json.loads((self.council / "rasci.json").read_text(encoding="utf-8"))
register["work/feature"]["start"] = iso(now - 1800)
register["work/stage"]["start"] = iso(now - 900)
register.update({
"work/done-a": {"title": "Done A", "status": "done", "worker": "Stage", "branch": "feature/base", "start": iso(now - 36000), "end": iso(now - 28800), "roles": {"Stage": "RA"}, "estimates": {"Stage": {"value": 5, "complexity": 2}}},
"work/done-b": {"title": "Done B", "status": "done", "worker": "Docs", "start": iso(now - 25200), "end": iso(now - 10800), "roles": {"Docs": "RA"}, "estimates": {"Docs": {"value": 3, "complexity": 2}}},
"work/done-c": {"title": "Done C", "status": "done", "worker": "Docs", "start": iso(now - 14400), "end": iso(now - 10800), "roles": {"Docs": "RA"}},
"work/next": {"title": "Next step", "status": "queued", "after": ["work/feature"], "roles": {"Skills": "R", "Maintainer": "A"}, "estimates": {"Skills": {"value": 8, "complexity": 3}}},
"work/child": {"title": "Child of the base", "status": "queued", "base": "feature/base", "roles": {"Docs": "RA"}},
})
queue = json.loads((self.council / "queue.json").read_text(encoding="utf-8"))
queue["entries"] += [
{"id": "deploy-4", "seq": 4, "kind": "deploy", "task": "work/stage", "workdir": "/repo", "session": "s2", "name": "Stage", "hotfix": None, "status": "active", "enqueued": now - 400, "granted": now - 300, "cmd": "make deploy"},
{"id": "test-5", "seq": 5, "kind": "test", "task": "work/docs", "workdir": "/repo", "session": "s2", "name": "Docs", "hotfix": None, "status": "waiting", "enqueued": now - 60, "granted": None, "cmd": "make test"},
]
self.write_council({"rasci.json": register, "queue.json": queue})
def plan_steps(self, browser: str, signs: bool) -> list[dict]:
local = f"http://localhost:{self.port}/"
flag = "true" if signs else "false"
steps = []
for width, height in WIDTHS:
bulk = BULK.replace("@SIGNS@", flag).replace("@VALUE@", PLAN_VALUES[width])
steps += [
{"width": width, "height": height, "url": f"{local}?{width}#tasks/gantt", "wait": 1200, "probe": GANTT, "shot": str(OUT / f"{browser}-{width}-tasks-gantt.png")},
{"width": width, "height": height, "url": f"{local}?{width}#tasks/burn", "wait": 900, "probe": BURN, "shot": str(OUT / f"{browser}-{width}-tasks-burn.png")},
{"width": width, "height": height, "url": None, "wait": 0, "probe": POINTS_ON, "shot": None},
{"width": width, "height": height, "url": f"{local}?{width}-reloaded#tasks/burn", "wait": 1200, "probe": REQUEST, "shot": str(OUT / f"{browser}-{width}-tasks-burn-points.png")},
{"width": width, "height": height, "url": f"{local}?{width}-points#tasks/gantt", "wait": 1200, "probe": GANTT, "shot": str(OUT / f"{browser}-{width}-tasks-gantt-points.png")},
{"width": width, "height": height, "url": f"{local}?{width}-list#tasks/list", "wait": 1200, "probe": bulk, "shot": str(OUT / f"{browser}-{width}-tasks-business-value.png")},
]
return steps
def planned(self, probe: dict, points: bool) -> None:
hour = 3600
edges = [["after", "work/feature", "work/next"], ["base", "work/done-a", "work/child"], ["queue", "queue/deploy-4", "queue/gate-3"], ["queue", "queue/gate-3", "queue/test-5"]]
self.assertEqual(probe["forecast"], ["always", [["always", "always"], ["points", "points only"], ["real", "real only"]]])
self.assertEqual(probe["modes"], [["none", str(not points).lower()], ["points", str(points).lower()]])
self.assertEqual((probe["request"], probe["inner"]), (points, True))
self.assertEqual(sorted([kind, start, end] for kind, start, end, _ in probe["edges"]), sorted(edges))
self.assertEqual({tip for *_, tip in probe["edges"]}, {"url(#gantt-tip)"})
bars = {task: (forecast, start, end, tag) for task, forecast, start, end, tag in probe["bars"]}
self.assertNotIn("operator/push", bars)
self.assertEqual((bars["work/old"][0], bars["work/old"][3]), (False, "circle"))
self.assertEqual([bars[task][0] for task in ("work/feature", "work/stage", "queue/deploy-4", "work/done-a")], [False] * 4)
self.assertGreaterEqual(bars["work/feature"][2] - bars["work/feature"][1], 1800)
forecasts = ("work/next", "work/docs", "work/child", "queue/gate-3", "queue/test-5")
lengths = {task: round(bars[task][2] - bars[task][1], 3) for task in forecasts}
self.assertEqual(lengths, {"work/next": (4.5 if points else 2) * hour, "work/docs": (3 if points else 2) * hour, "work/child": 2 * hour, "queue/gate-3": 2 * hour, "queue/test-5": 2 * hour})
self.assertTrue(all(bars[task][0] for task in forecasts))
self.assertEqual((bars["work/next"][1], bars["queue/test-5"][1]), (bars["work/feature"][2], bars["queue/gate-3"][2]))
self.assertTrue(probe["hatched"] and all(probe["hatched"]), probe["hatched"])
self.assertEqual(probe["real"], 0)
only = {task: (round(length, 3), tag) for task, length, tag in probe["pointsOnly"]}
dot = (0, "circle")
self.assertEqual(only, {"work/next": (4.5 * hour, "rect") if points else dot, "work/docs": (3 * hour, "rect") if points else dot, "work/child": dot, "queue/gate-3": dot, "queue/test-5": dot})
def check_plan(self, browser: str, driver: str, binary: str, signs: bool) -> None:
"""Args:
signs: cdp.mjs signs the bulk business value with its virtual
passkey; bidi.mjs has no virtual authenticator, so the probe
answers the prompt the way a canceled passkey dialog does.
"""
self.plan_work(time.time())
authenticator = self.authenticator()
data = self.drive(self.plan_steps(browser, signs), authenticator if signs else None, driver, binary)
(OUT / f"{browser}-plan.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual([error for error in data["errors"] if "(style-src-elem)" not in error and "/favicon.ico" not in error], [])
rows = self.snapshot()["tasks"]
ranked = list(enumerate(rows))
status_up = [row["task"] for _, row in sorted(ranked, key=lambda pair: (STATUS_ORDER.index(pair[1]["status"]), pair[0]))]
status_down = [row["task"] for _, row in sorted(ranked, key=lambda pair: (-STATUS_ORDER.index(pair[1]["status"]), pair[0]))]
for index, (width, _) in enumerate(WIDTHS):
gantt, burn, points_on, request, gantt_points, bulk = data["results"][6 * index : 6 * (index + 1)]
value = PLAN_VALUES[width]
items = [["work/next", f"estimate work/next Operator --value {value}"], ["work/child", f"estimate work/child Operator --value {value}"]]
with self.subTest(browser=browser, width=width):
for probe in (gantt, burn, points_on, request, gantt_points, bulk):
self.assertNotIn("error", probe)
self.assertEqual((probe["tall"], probe["wide"], probe["layout"]), (False, False, ["toolbar", "chips", "body"]), probe)
self.assertEqual(probe["rows"] == 1, width != 390, probe)
self.planned(gantt, points=False)
self.planned(gantt_points, points=True)
self.assertEqual((burn["modes"], burn["request"]), ([["none", "true"], ["points", "false"]], False))
self.assertEqual(burn["scope"][0], "all")
self.assertEqual(burn["scope"][1][:2], ["all", "filter"])
self.assertIn("agent:Docs", burn["scope"][1])
self.assertIn("line:identity/oidc", burn["scope"][1])
self.assertEqual(burn["series"], [["scope", 9], ["done", 4], ["remaining", 5]])
self.assertEqual(burn["agent"], [["scope", 5], ["done", 2], ["remaining", 3]])
self.assertEqual(points_on["before"], {"modes": [["none", "true"], ["points", "false"]], "request": False})
self.assertEqual((points_on["modes"], points_on["request"]), ([["none", "false"], ["points", "true"]], True))
self.assertEqual(request["modes"], [["none", "false"], ["points", "true"]])
self.assertEqual(request["series"], [["scope", 12], ["done", 4], ["remaining", 8], ["unestimated", 2]])
self.assertTrue(request["toast"].startswith(f"Request estimates: {'done' if signs else 'not signed'}"), request["toast"])
self.assertEqual(bulk["sorts"], ["prio", "wsjf", "value", "complexity", "status", "start", "end", "agent", "title"])
self.assertEqual((bulk["prio"], bulk["status"], bulk["statusDown"]), ([row["task"] for row in rows], status_up, [status_down, "Descending"]))
self.assertEqual((bulk["picks"], bulk["armed"]), (["work/feature", "work/next", "work/docs", "work/child", "work/stage"], [False, "2"]))
self.assertTrue(bulk["toast"].startswith(f"Business value {value} for 2 tasks: {'handed to the runner' if signs else 'not signed'}"), bulk["toast"])
signed = [["/api/sign", ["items"], items]]
self.assertEqual((bulk["asked"], bulk["posted"]), (1, [*signed, ["/api/tasks", ["items", "proof"], items]] if signs else signed))
self.assertEqual((bulk["after"], bulk["reset"]), ([True, 0] if signs else [False, 2], True))
self.assertEqual([args for script, args in self.scripts.calls if script == skill_scripts.ESTIMATE], [["request", "--session", "Operator"]] * (len(WIDTHS) if signs else 0))
self.assertEqual(self.httpd.hub.settings.get()["estimation"], "none")
jobs = self.runner_jobs()
if not signs:
self.assertEqual(jobs, [])
register = json.loads((self.council / "rasci.json").read_text(encoding="utf-8"))
self.assertEqual([task for task, row in register.items() if "Operator" in row.get("estimates", {})], [])
return
self.assertEqual([job["argv"][-1] for job in jobs], [PLAN_VALUES[width] for width, _ in WIDTHS for _ in range(2)])
self.run_runner()
register = json.loads((self.council / "rasci.json").read_text(encoding="utf-8"))
self.assertEqual({task: register[task]["estimates"]["Operator"] for task in ("work/next", "work/child")}, {"work/next": {"value": 34, "at": register["work/next"]["estimates"]["Operator"]["at"]}, "work/child": {"value": 34, "at": register["work/child"]["estimates"]["Operator"]["at"]}})
rows = {row["task"]: row for row in self.snapshot()["tasks"]}
self.assertEqual((rows["work/next"]["wsjf"], rows["work/child"]["wsjf"], rows["work/child"]["value"]), ({"value": 34, "complexity": 3, "score": 11.33}, None, 34))
def test_chromium_draws_the_gantt_and_burn_switches_the_estimation_requests_estimates_and_signs_a_bulk_business_value_at_three_widths(self):
self.check_plan("chromium", "cdp.mjs", os.environ.get("CHROMIUM", "chromium"), signs=True)
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_draws_the_gantt_and_burn_and_asks_the_passkey_for_a_bulk_business_value_storing_nothing_unsigned(self):
self.check_plan("firefox", "bidi.mjs", os.environ.get("FIREFOX", "firefox"), signs=False)
def trend_work(self, now: float) -> None:
day = 86400
register = {
"work/t1": {"title": "First", "status": "done", "worker": "Stage", "start": iso(now - 10 * day), "end": iso(now - 8 * day), "roles": {"Stage": "RA"}, "estimates": {"Stage": {"value": 5, "complexity": 1}}},
"work/t2": {"title": "Second", "status": "done", "worker": "Docs", "start": iso(now - 10 * day), "end": iso(now - 6 * day), "roles": {"Docs": "RA"}, "estimates": {"Docs": {"value": 3, "complexity": 2}}},
"work/t3": {"title": "Third", "status": "done", "worker": "Docs", "branch": "feature/t3", "start": iso(now - 10 * day), "end": iso(now - 4 * day), "roles": {"Docs": "RA"}, "estimates": {"Docs": {"value": 3, "complexity": 3}}},
"work/t4": {"title": "Fourth", "status": "running", "worker": "Stage", "start": iso(now - day), "roles": {"Stage": "RA"}, "estimates": {"Stage": {"value": 8, "complexity": 5}}},
"work/t5": {"title": "Fifth", "status": "queued", "after": ["work/t4"], "roles": {"Skills": "R"}, "estimates": {"Skills": {"value": 8, "complexity": 3}}},
"work/t6": {"title": "Sixth", "status": "queued", "base": "feature/t3", "roles": {"Docs": "RA"}},
}
queue = {"seq": 2, "entries": [
{"id": "deploy-1", "seq": 1, "kind": "deploy", "task": "work/t4", "workdir": "/repo", "session": "s2", "name": "Stage", "hotfix": None, "status": "active", "enqueued": now - 400, "granted": now - 300, "cmd": "make deploy"},
{"id": "test-2", "seq": 2, "kind": "test", "task": "work/t5", "workdir": "/repo", "session": "s2", "name": "Docs", "hotfix": None, "status": "waiting", "enqueued": now - 60, "granted": None, "cmd": "make test"},
]}
self.write_council({"rasci.json": register, "queue.json": queue})
def trend_steps(self, browser: str) -> list[dict]:
local = f"http://localhost:{self.port}/"
steps = []
for width, height in WIDTHS:
days = str(TREND_DAYS[width])
steps += [
{"width": width, "height": height, "url": f"{local}?{width}#tasks/list", "wait": 1200, "probe": ETA_LIST, "shot": str(OUT / f"{browser}-{width}-tasks-eta.png")},
{"width": width, "height": height, "url": f"{local}?{width}#tasks/burn", "wait": 900, "probe": TREND_SET.replace("@MODE@", "regression").replace("@DAYS@", days), "shot": str(OUT / f"{browser}-{width}-tasks-burn-regression.png")},
{"width": width, "height": height, "url": f"{local}?{width}-regression#tasks/burn", "wait": 1200, "probe": TREND_RELOADED, "shot": None},
{"width": width, "height": height, "url": None, "wait": 0, "probe": TREND_SET.replace("@MODE@", "velocity").replace("@DAYS@", days), "shot": str(OUT / f"{browser}-{width}-tasks-burn-velocity.png")},
{"width": width, "height": height, "url": f"{local}?{width}-velocity#tasks/burn", "wait": 1200, "probe": TREND_RELOADED, "shot": None},
{"width": width, "height": height, "url": None, "wait": 0, "probe": STEP_TIPS, "shot": str(OUT / f"{browser}-{width}-tasks-burn-steps.png")},
{"width": width, "height": height, "url": f"{local}?{width}-3d#tasks/3d", "wait": 1200, "probe": TASKS_3D, "shot": str(OUT / f"{browser}-{width}-tasks-3d.png")},
{"width": width, "height": height, "url": f"{local}?{width}-camera#tasks/3d", "wait": 900, "probe": CAMERA, "shot": str(OUT / f"{browser}-{width}-tasks-3d-steady-camera.png")},
]
return steps
def eta_checked(self, probe: dict, mode: str, rows: dict, edges: list[dict], window: tuple[float, float]) -> None:
"""Args:
probe: task -> [start, eta, forecast, text, title] as the list shows it.
mode: the estimation mode the list was drawn in.
rows: the snapshot's task rows by task.
edges: the snapshot's task edges.
window: the earliest and latest time the page can have taken as now.
"""
day = 86400
points = {"none": {}, "points": {"work/t4": 10 * day, "work/t5": 6 * day}}[mode]
self.assertEqual(sorted(probe), sorted(rows))
for task, (start, eta, forecast, words, title) in probe.items():
with self.subTest(mode=mode, task=task):
row = rows[task]
self.assertTrue(words.startswith("ETA ") and words != "ETA –", words)
if row["ended"]:
self.assertEqual((eta, forecast, title), (row["end"], False, "ETA: the end of the ended task"))
continue
self.assertTrue(forecast)
self.assertAlmostEqual(eta - start, points.get(task, 4 * day), delta=0.01)
ahead = [probe[edge["from"]][1] for edge in edges if edge["to"] == task and edge["from"] in probe and edge["from"] != task]
if row["start"] is not None:
self.assertEqual(start, row["start"])
elif ahead and max(ahead) > window[1]:
self.assertAlmostEqual(start, max(ahead), delta=0.01)
else:
self.assertTrue(window[0] <= start <= window[1], (start, window))
def trend_forecast(self, points: list[list[float]], mode: str, days: int) -> tuple[float, float | None]:
"""Args:
points: [t, remaining] of the drawn burndown points.
Returns:
The trend's start and the time its remaining line reaches 0
(None when it never falls), computed here independently of plan.js.
"""
last_t, last_y = points[-1]
if mode == "regression":
mean_t = sum(t for t, _ in points) / len(points)
mean_y = sum(y for _, y in points) / len(points)
slope = sum((t - mean_t) * (y - mean_y) for t, y in points) / sum((t - mean_t) ** 2 for t, _ in points)
return points[0][0], (mean_t - mean_y / slope if slope < 0 else None)
start = max(points[0][0], last_t - days * 86400)
before = [y for t, y in points if t <= start][-1]
slope = (last_y - before) / (last_t - start)
return start, (last_t - last_y / slope if slope < 0 else None)
def trended(self, probe: dict, mode: str, days: int) -> None:
start, forecast = self.trend_forecast(probe["points"], mode, days)
self.assertEqual(probe["value"], mode)
self.assertEqual(probe["days"], [days, mode == "velocity"])
self.assertEqual(sorted(series for series, *_ in probe["lines"]), ["done", "remaining", "scope"])
for _, begin, end in probe["lines"]:
self.assertAlmostEqual(begin, start, delta=0.01)
self.assertAlmostEqual(end, forecast if forecast is not None else probe["points"][-1][0], delta=0.01)
if forecast is None:
self.assertEqual((probe["note"], probe["marker"]), ([None, "No forecast: the trend never reaches 0 remaining, so done never meets scope"], None))
return
self.assertAlmostEqual(probe["note"][0], forecast, delta=0.01)
self.assertAlmostEqual(probe["marker"], forecast, delta=0.01)
self.assertTrue(probe["note"][1].startswith("Forecast: "), probe["note"])
def check_trend_eta_3d(self, browser: str, driver: str, binary: str) -> None:
self.trend_work(time.time())
begun = time.time()
data = self.drive(self.trend_steps(browser), None, driver, binary, init=GRAPH_HOOK)
ended = time.time()
(OUT / f"{browser}-trend-eta-3d.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual([error for error in data["errors"] if "(style-src-elem)" not in error and "/favicon.ico" not in error], [])
snapshot = self.snapshot()
rows = {row["task"]: row for row in snapshot["tasks"]}
edges = snapshot["task_edges"]
self.assertEqual(sorted([edge["kind"], edge["from"], edge["to"]] for edge in edges), [["after", "work/t4", "work/t5"], ["base", "work/t3", "work/t6"], ["queue", "queue/deploy-1", "queue/test-2"]])
trends = [["off", "off"], ["regression", "linear regression"], ["velocity", "velocity"]]
per = 8
for index, (width, _) in enumerate(WIDTHS):
eta, regression, reloaded, velocity, again, tips, shown, camera = data["results"][per * index : per * (index + 1)]
days = TREND_DAYS[width]
kept = TREND_DAYS[WIDTHS[index - 1][0]] if index else 7
with self.subTest(browser=browser, width=width):
for probe in (eta, regression, reloaded, velocity, again, tips, shown):
self.assertNotIn("error", probe)
self.assertEqual((probe["tall"], probe["wide"], probe["layout"]), (False, False, ["toolbar", "chips", "body"]), probe)
self.assertEqual(probe["rows"] == 1, width != 390, probe["rows"])
for mode in ("none", "points", "back"):
self.eta_checked(eta[mode], "points" if mode == "points" else "none", rows, edges, (begun - 1, ended + 1))
self.assertEqual(eta["modes"], [["none", "true"], ["points", "false"]])
self.assertEqual((regression["before"]["value"], regression["before"]["options"], regression["before"]["note"], regression["before"]["lines"]), ("off", trends, None, []))
self.assertEqual(regression["before"]["days"], [kept, False])
self.assertEqual(regression["toasts"], ["Trend: linear regression"])
self.trended(regression["after"], "regression", kept)
self.trended(reloaded, "regression", kept)
self.assertEqual(velocity["toasts"], [f"Trend: velocity over {kept} days", f"Trend: velocity over {days} days"])
self.trended(velocity["after"], "velocity", days)
self.trended(again, "velocity", days)
self.assertEqual([step["t"] for step in tips["steps"]], [t for t, _ in again["points"]])
listed = [[sorted(filter(None, step[key].split(" "))) for key in ("started", "ended", "joined")] for step in tips["steps"]]
self.assertEqual(listed, [[["work/t1", "work/t2", "work/t3"], [], ["work/t5", "work/t6"]], [[], ["work/t1"], []], [[], ["work/t2"], []], [[], ["work/t3"], []], [["work/t4"], [], []], [[], [], []]])
self.assertEqual([step["series"] for step in tips["steps"]], [["scope", "done", "remaining"]] * len(tips["steps"]))
self.assertIn("Ended: work/t1", tips["steps"][1]["tip"])
self.assertIn("No work package started, ended or joined the scope here", tips["steps"][-1]["tip"])
self.assertEqual((tips["reset"], tips["note"], tips["lines"]), ("Trend: off", False, 0))
self.tasks_3d(shown, snapshot)
self.assertTrue(camera["hooked"], camera)
self.assertGreater(math.dist(camera["built"], camera["moved"]), 1, camera)
for refresh in camera["refreshes"]:
self.assertEqual((refresh["rebuilt"], refresh["settled"]), (True, True), camera)
self.assertLess(math.dist(refresh["camera"], camera["moved"]), 0.01, camera)
self.assertEqual(self.httpd.hub.settings.get()["trend"], {"mode": "off", "days": TREND_DAYS[WIDTHS[-1][0]]})
def tasks_3d(self, shown: dict, snapshot: dict) -> None:
tasks = sorted(row["task"] for row in snapshot["tasks"])
colors = {"after": shown["palette"]["violet"], "base": shown["palette"]["info"], "queue": shown["palette"]["warn"]}
edges = sorted([edge["from"], edge["to"], edge["kind"], colors[edge["kind"]]] for edge in snapshot["task_edges"])
self.assertEqual(len(set(colors.values())), 3, colors)
for layout, word in (("force", "Force"), ("time", "Time")):
drawn = shown[layout]
with self.subTest(layout=layout):
self.assertEqual(drawn["note"], f"{len(tasks)} nodes · {len(edges)} edges · {word}")
self.assertEqual(drawn["layouts"], [["none", str(layout == "force").lower()], ["time", str(layout == "time").lower()]])
self.assertEqual(sorted(node for node, *_ in drawn["nodes"]), tasks)
self.assertEqual(sorted(drawn["links"]), edges)
self.assertEqual(drawn["label"], "work/t5 · Fifth")
self.assertEqual([fz for _, fz, _ in shown["force"]["nodes"]], [None] * len(tasks))
timed = sorted(shown["time"]["nodes"], key=lambda node: node[2])
self.assertTrue(all(fz is not None for _, fz, _ in timed), timed)
self.assertEqual([fz for _, fz, _ in timed], sorted(fz for _, fz, _ in timed))
self.assertLess(timed[0][1], timed[-1][1])
popup = shown["popup"]
self.assertEqual((popup["open"], popup["title"], popup["id"], popup["picked"], popup["open_button"], popup["line"]), (True, "Fifth", "work/t5", "work/t5", "Open task", True), popup)
self.assertTrue(popup["eta"], popup)
self.assertEqual((popup["closed"], shown["back"]), ([False, None], f"{len(tasks)} nodes · {len(edges)} edges · Force"))
def test_chromium_draws_the_trend_forecast_point_tips_eta_and_tasks_3d_at_three_widths(self):
self.check_trend_eta_3d("chromium", "cdp.mjs", os.environ.get("CHROMIUM", "chromium"))
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_draws_the_trend_forecast_point_tips_eta_and_tasks_3d_at_three_widths(self):
self.check_trend_eta_3d("firefox", "bidi.mjs", os.environ.get("FIREFOX", "firefox"))
def test_chromium_pins_a_task_priority_after_signature_and_the_runner_applies_it(self):
signer = self.pin()
private = "".join(line for line in signer.pem.read_text(encoding="utf-8").splitlines() if not line.startswith("-----"))
authenticator = {"credentialId": base64.b64encode(signer.raw_id).decode(), "privateKey": private, "rpId": "localhost"}
local = f"http://localhost:{self.port}/"
data = self.drive([{"width": 1366, "height": 768, "url": f"{local}#tasks/list", "wait": 1200, "probe": PIN, "shot": self.shot("1366-tasks-priority-signed")}], authenticator)
(OUT / "chromium-task-priority.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
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"], ["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()
register = json.loads((self.council / "rasci.json").read_text(encoding="utf-8"))
self.assertEqual((register["work/docs"]["prio"], register["work/feature"]["prio"]), (2, 1))
self.assertEqual([(row["task"], row["prio"]) for row in self.snapshot()["tasks"][:2]], [("work/feature", 1), ("work/docs", 2)])
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_walks_every_screen_at_three_widths(self):
shots = [(width, height, screen, OUT / f"firefox-{width}-{screen.replace('/', '-')}.png") for width, height in WIDTHS for screen in SCREENS]
for *_, shot in shots:
shot.unlink(missing_ok=True)
steps = [{"width": width, "height": height, "url": f"{self.base}#{screen}", "wait": 900, "probe": PROBE, "shot": str(shot)} for width, height, screen, shot in shots]
data = self.drive(steps, None, "bidi.mjs", os.environ.get("FIREFOX", "firefox"))
(OUT / "firefox-walk.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
blocked = [error for error in data["errors"] if error.startswith("Content-Security-Policy")]
self.assertEqual([error for error in data["errors"] if error not in blocked], [])
# 3d-force-graph injects its own <style>, which the unchanged CSP blocks; the page carries those rules itself.
self.assertEqual([error for error in blocked if "(style-src-elem)" not in error and "/favicon.ico" not in error], [])
for probe, (width, _, screen, shot) in zip(data["results"], shots, strict=True):
with self.subTest(width=width, screen=screen):
self.assertNotIn("error", probe)
self.assertEqual(probe["hash"], f"#{screen}")
self.assertEqual((probe["tabs"], probe["subs"]), (TABS, SUBS))
self.assertEqual(labelled(probe), {"unlabelled": [], "broken": []})
self.assertFalse(probe["wide"], "the page scrolls sideways")
self.assertEqual((probe["panel"], probe["nodeChips"]), (0, 0))
self.assertTrue(shot.is_file() and shot.stat().st_size > 1000, shot)
if screen.endswith("/3d"):
self.assertTrue(probe["canvas"], probe["note"])
if __name__ == "__main__":
unittest.main()