ouroboros/tests/test_ui_repair_owner_intent.py

138 lines
8.4 KiB
Python

"""Repair and run: real UI ingress, managed worker, review and installed execution."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import subprocess
import pytest
from devtools.benchmarks.common.server_runner import _api
from ouroboros.skill_loader import compute_content_hash
from tests.system_e2e.harness import (
ArtifactOracle, ScriptedStubModel, clone_repo, keyless_settings, start_server, wait_until,
)
@pytest.mark.ui_browser
@pytest.mark.serial
def test_repair_button_records_owner_intent_and_runs_the_installed_skill(tmp_path):
if os.environ.get("OUROBOROS_RUN_UI_SMOKE") != "1":
pytest.skip("set OUROBOROS_RUN_UI_SMOKE=1")
playwright = pytest.importorskip("playwright.sync_api")
source = Path(__file__).resolve().parents[1]
clone = clone_repo(tmp_path / "source")
# clone_repo captures HEAD; this developer scenario exercises the complete
# candidate, including an uncommitted fix, in a disposable clone only.
patch = subprocess.check_output(["git", "diff", "--binary", "HEAD"], cwd=source)
if patch:
subprocess.run(["git", "apply", "--binary", "-"], input=patch, cwd=clone, check=True)
name = "repair_owner_probe"
evidence = Path(os.environ.get("OUROBOROS_UI_SCREENSHOT_DIR", str(tmp_path)))
evidence.mkdir(parents=True, exist_ok=True)
body = (
"async def status(request):\n return {'value': 'repaired'}\n"
"def register(api):\n"
" api.register_route('status', status, methods=('GET',))\n"
" api.register_ui_tab('main', 'Repair result', render={'kind':'module','entry':'widget.js','start':'auto'})\n"
" api.register_companion_process('worker')\n"
)
script = [
{"tool": "write_file", "arguments": {"root": "skill_payload", "path": "plugin.py", "content": body}},
{"tool": "run_command", "arguments": {"cmd": ["python3", "-c", "print('repair shell works')"], "cwd": "skill_payload"}},
{"tool": "skill_preflight", "arguments": {"skill": name}},
{"tool": "skill_review", "arguments": {"skill": name}},
{"tool": "toggle_skill", "arguments": {"skill": name, "enabled": True}},
]
with ScriptedStubModel(script, final_answer="The repaired skill is running.") as stub:
server = start_server(clone, tmp_path / "runtime", keyless_settings(
stub, OUROBOROS_MAX_WORKERS=1, OUROBOROS_AUTO_GRANT_REVIEWED_SKILLS=True,
))
try:
drive = Path(server.data_root)
oracle = ArtifactOracle(drive)
payload = drive / "skills" / "external" / name
payload.mkdir(parents=True)
(payload / "SKILL.md").write_text(
f"---\nname: {name}\ndescription: Repair owner source integration fixture\n"
'version: "1.0"\ntype: extension\nentry: plugin.py\nplugin_api: "2.0"\n'
'permissions: [route, widget, companion_process]\n'
'companion_processes:\n - name: worker\n runtime: python3\n'
' command: [python3, worker.py]\n restart_policy: never\n---\n'
)
(payload / "plugin.py").write_text("def register(api):\n broken syntax!\n")
(payload / "worker.py").write_text(
"import os, pathlib, threading\n"
"pathlib.Path(os.environ['OUROBOROS_SKILL_STATE_DIR'], 'ready.txt').write_text('ready')\n"
"threading.Event().wait(120)\n"
)
(payload / "widget.js").write_text(
"const style = document.createElement('style');\n"
"style.textContent = 'body{color:#e8ecf3;font:16px system-ui;padding:16px}';\n"
"document.head.appendChild(style);\n"
f"fetch('/api/extensions/{name}/status').then(r => r.json()).then(x => {{\n"
" document.getElementById('root').textContent = 'Installed: ' + x.value;\n});\n"
)
initial_hash = compute_content_hash(payload)
preflight = _api(server.base_url, "POST", f"/api/skills/{name}/review", {}, timeout=120)
assert preflight["status"] == "pending", preflight
assert not stub.script_consumed()
with playwright.sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
try:
page = browser.new_page(viewport={"width": 1280, "height": 850})
page.route("**/api/marketplace/ouroboroshub/catalog*", lambda route: route.fulfill(json={"results": []}))
page.goto(server.base_url + "/#skills")
card = page.locator(f'.skills-card[data-skill="{name}"]')
card.get_by_role("button", name="Repair and run", exact=True).wait_for()
page.screenshot(path=str(evidence / "repair-and-run-card.png"))
card.get_by_role("button", name="Repair and run", exact=True).click()
dialog = page.get_by_role("dialog")
assert "stays running" in dialog.inner_text()
page.screenshot(path=str(evidence / "repair-and-run-confirmation.png"))
with page.expect_request("**/api/command") as command:
dialog.get_by_role("button", name="Repair and run", exact=True).click()
request = command.value.post_data_json
assert "visible_text" not in request
assert request["task_constraint"]["allow_enable"] is False
admission = wait_until(lambda: oracle._json(f"state/skills/{name}/repair_admission.json"), 45)
task_id = admission["task_id"]
result = server.wait_task(task_id, timeout=240)
assert result["status"] == "completed", result
stored = oracle.task_result(task_id)
ref = stored["origin_message_ref"]
rows = [json.loads(line) for line in oracle.chat_bytes().splitlines() if line]
owner = next(row for row in rows if row.get("client_message_id") == ref["client_message_id"] and row.get("direction") == "in")
assert owner["text"] == request["cmd"] == stored["origin_message_text"]
assert "Repair and run" in owner["text"]
assert stored["task_constraint"]["allow_enable"] is False
assert admission["base_content_hash"] == initial_hash
actions = [row for row in oracle.events("owner_api_action") if row.get("task_id") == task_id]
enabled = next(row for row in actions if row.get("action") == "skill_enable" and row.get("ok"))
assert enabled["source_ref"]["ref"] == ref
assert enabled["actor"] == "agent_tool"
assert stub.script_consumed() and stub.kinds().count("skill_review") >= 3
assert "repair shell works" in json.dumps(oracle.tools_rows())
wait_until(lambda: oracle._json(f"state/skills/{name}/enabled.json").get("enabled"), 30)
companions = wait_until(lambda: _api(server.base_url, "GET", "/api/skills/daemons").get("companions", {}).get(name + ":worker"), 30)
assert companions["pid"] > 0
assert (drive / "state" / "skills" / name / "ready.txt").read_text() == "ready"
# The hash is a one-time entry pointer; app navigation owns
# later page changes in the already-loaded SPA.
page.click('[data-nav-page="widgets"]')
page.frame_locator(f'[data-widget-key="{name}:main"] iframe').locator("#root").filter(has_text="Installed: repaired").wait_for(timeout=30000)
page.screenshot(path=str(evidence / "repair-and-run-installed.png"))
(evidence / "repair-and-run.json").write_text(json.dumps({
"task_id": task_id, "origin": ref, "initial_hash": initial_hash,
"final_hash": compute_content_hash(payload), "companion_pid": companions["pid"],
"candidate_patch_sha256": hashlib.sha256(patch).hexdigest(),
"review_delivery": "real pipeline, controlled loopback reviewer models",
}, indent=2) + "\n")
_api(server.base_url, "POST", f"/api/skills/{name}/toggle", {"enabled": False})
finally:
browser.close()
finally:
server.stop()