The status page's walkthrough (M6.6b)
A new package, nisse-ui: a read-only page for studying how the agents work. For each work item it shows the steps (what people and the orchestrator said, and each run), each run's turns, and each turn's request read against the log it was made from: - each message beside the events it came from, tagged unchanged, labelled, narrated, shortened (by how much) or a compaction, with what fitting dropped; - what is new since the turn before, and where the request first differs (where prompt reuse had to stop), next to the server's count; - the reply with its reasoning, and the raw JSON. Runs going in circles are flagged by nisse_core.loop.circles (repeated near-identical replies, one correction given again and again, repeated apologies), a pure function M7.8 can reuse in Driver.run. In nisse-core: - render.py: message_indexes(), from the same code render() uses, so the page's matching can't drift from what was sent. - calls.py: the record notes when recording began; the reader gains calls_for(item) and call(id). - state/readonly.py: opens an agent's SQLite file without creating its -wal/-shm files (mode=ro alone does, once the writer has closed). The page reads only through read-only connections, escapes everything, and sends a Content Security Policy that allows no script. It listens on 127.0.0.1; until M6.6d puts it behind Authelia, reach it over an SSH tunnel. Jinja2 is its one new dependency, for autoescaping. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
1 parent
c1f3d9dafb
commit
a8e2023183
31 files changed
+2550
-15
No files matched your search
@@ -0,0 +1,102 @@
|
||||
"""The page and the agents are released together (X6): these fail in the PR
|
||||
that would break the page, not in the operator's browser."""
|
||||
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from ui_fixtures import recorded, store_at
|
||||
|
||||
from nisse_core.models import calls
|
||||
from nisse_core.state.store import StateStore as CoreStore
|
||||
from nisse_dev.state import StateStore as DevStore
|
||||
from nisse_review.state import StateStore as ReviewStore
|
||||
from nisse_ui import main as ui_main
|
||||
from nisse_ui.app import App
|
||||
from nisse_ui.sources import KNOWN_SCHEMAS, discover
|
||||
|
||||
|
||||
@pytest.mark.parametrize("store", [CoreStore, DevStore, ReviewStore])
|
||||
def test_every_agents_schema_is_one_the_page_knows(store: type[CoreStore]) -> None:
|
||||
# An agent that moves its schema on must teach the page too.
|
||||
assert store.SCHEMA_VERSION in KNOWN_SCHEMAS
|
||||
|
||||
|
||||
@pytest.mark.parametrize("store_class", [DevStore, ReviewStore])
|
||||
def test_each_agents_own_store_opens(
|
||||
tmp_path: Path, store_class: type[CoreStore]
|
||||
) -> None:
|
||||
store = store_at(tmp_path, store_class)
|
||||
store.open_work_item("i1", title="Agent item", position="NEW", cause="t")
|
||||
store.close()
|
||||
app = App(discover([("a", tmp_path / "state.db")], []))
|
||||
assert app.handle("/s/a/").status == 200
|
||||
assert "Agent item" in app.handle("/s/a/item/i1").body.decode()
|
||||
|
||||
|
||||
def test_runs_the_benchmark_kept_are_sources(tmp_path: Path) -> None:
|
||||
# As bench --keep-runs leaves them: <dir>/<task>-<id>/state.db + record.
|
||||
for name in ("fix-bug-1a2b3c4d", "trace-bug-5e6f7a8b"):
|
||||
_, store = recorded(tmp_path / "kept" / name)
|
||||
store.close()
|
||||
(tmp_path / "kept" / "not-a-run").mkdir()
|
||||
sources = discover([], [tmp_path / "kept"])
|
||||
assert sorted(s.name for s in sources) == [
|
||||
"kept.fix-bug-1a2b3c4d",
|
||||
"kept.trace-bug-5e6f7a8b",
|
||||
]
|
||||
assert all(s.kind == "run" and s.calls_db.is_file() for s in sources)
|
||||
assert sources[0].calls_db == calls.path_for(sources[0].state_db)
|
||||
|
||||
|
||||
def test_source_names_are_safe_and_unique(tmp_path: Path) -> None:
|
||||
sources = discover(
|
||||
[("dev agent/1", tmp_path / "a"), ("dev agent/1", tmp_path / "b")], []
|
||||
)
|
||||
assert [s.name for s in sources] == ["dev-agent-1", "dev-agent-1-2"]
|
||||
|
||||
|
||||
def test_the_server_listens_on_this_machine_and_sends_the_headers(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source, store = recorded(tmp_path / "a")
|
||||
store.close()
|
||||
server = ui_main.ThreadingHTTPServer(
|
||||
("127.0.0.1", 0), ui_main.handler_for(App([source]))
|
||||
)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
port = server.server_address[1]
|
||||
response = httpx.get(f"http://127.0.0.1:{port}/s/agent/item/w1")
|
||||
assert response.status_code == 200
|
||||
assert "Fix the parser" in response.text
|
||||
assert "default-src 'none'" in response.headers["content-security-policy"]
|
||||
assert response.headers["x-content-type-options"] == "nosniff"
|
||||
assert response.headers["server"].startswith("nisse-ui")
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def test_the_command_line_defaults_to_this_machine_only(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
assert ui_main._source("dev=/var/lib/nisse-dev/state.db") == (
|
||||
"dev",
|
||||
Path("/var/lib/nisse-dev/state.db"),
|
||||
)
|
||||
with pytest.raises(Exception, match="NAME=STATE_DB"):
|
||||
ui_main._source("no-equals")
|
||||
|
||||
class Bound(Exception):
|
||||
pass
|
||||
|
||||
def server(address: tuple[str, int], handler: object) -> None:
|
||||
raise Bound(address)
|
||||
|
||||
monkeypatch.setattr(ui_main, "ThreadingHTTPServer", server)
|
||||
with pytest.raises(Bound) as bound:
|
||||
ui_main.main([])
|
||||
assert bound.value.args[0] == ("127.0.0.1", 8765)
|
||||
Reference in new issue
Block a user