ArchiveBox/archivebox/tests/test_opencode_agent.py
2026-06-03 17:19:48 -07:00

345 lines
12 KiB
Python

import os
import sqlite3
import pytest
import requests
from asgiref.sync import async_to_sync
from archivebox.tests.conftest import ADMIN_TEST_HOST
pytestmark = pytest.mark.django_db
def test_opencode_disabled_route_does_not_start_server(client, monkeypatch):
from abx_plugins.plugins.opencode import views
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": False})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: pytest.fail("opencode should not start when disabled"))
response = client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST)
assert response.status_code == 404
def test_opencode_agent_requires_superuser(client, db, monkeypatch, django_user_model):
from abx_plugins.plugins.opencode import views
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: pytest.fail("opencode should not start before auth passes"))
response = client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST)
assert response.status_code == 302
assert "/admin/login/" in response.headers["Location"]
user = django_user_model.objects.create_user(username="regular", password="testpassword")
client.force_login(user)
response = client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST)
assert response.status_code == 403
def test_opencode_agent_superuser_gets_wrapper(admin_client, db, monkeypatch):
from abx_plugins.plugins.opencode import views
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: (True, ""))
response = admin_client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST)
assert response.status_code == 200
assert b'<iframe src="/admin/agent/opencode/' in response.content
assert b'/session"' in response.content
assert b'id="header"' in response.content
assert b'id="progress-monitor"' in response.content
def test_opencode_proxy_blocks_cross_origin_mutation(admin_client, db, monkeypatch):
from abx_plugins.plugins.opencode import views
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: pytest.fail("opencode should not start before origin check passes"))
response = admin_client.post(
"/admin/agent/opencode/session",
data=b"{}",
content_type="application/json",
HTTP_HOST=ADMIN_TEST_HOST,
HTTP_ORIGIN="https://evil.example",
)
assert response.status_code == 403
def test_opencode_proxy_allows_same_origin_fetch_metadata(admin_client, db, monkeypatch):
from abx_plugins.plugins.opencode import views
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: (True, ""))
def fake_request(method, url, **kwargs):
upstream = requests.Response()
upstream.status_code = 200
upstream._content = b"{}"
upstream.headers["Content-Type"] = "application/json"
return upstream
monkeypatch.setattr(views.requests, "request", fake_request)
response = admin_client.post(
"/admin/agent/opencode/pty/test/connect-token",
data=b"{}",
content_type="application/json",
HTTP_HOST=ADMIN_TEST_HOST,
HTTP_SEC_FETCH_SITE="same-origin",
)
assert response.status_code == 200
def test_opencode_proxy_allows_pty_connect_token_without_origin(admin_client, db, monkeypatch):
from abx_plugins.plugins.opencode import views
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: (True, ""))
def fake_request(method, url, **kwargs):
upstream = requests.Response()
upstream.status_code = 200
upstream._content = b"{}"
upstream.headers["Content-Type"] = "application/json"
return upstream
monkeypatch.setattr(views.requests, "request", fake_request)
response = admin_client.post(
"/admin/agent/opencode/pty/test/connect-token",
data=b"{}",
content_type="application/json",
HTTP_HOST=ADMIN_TEST_HOST,
)
assert response.status_code == 200
def test_opencode_proxy_blocks_cross_site_fetch_metadata(admin_client, db, monkeypatch):
from abx_plugins.plugins.opencode import views
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True})
monkeypatch.setattr(
views,
"_ensure_opencode",
lambda settings: pytest.fail("opencode should not start before fetch metadata check passes"),
)
response = admin_client.post(
"/admin/agent/opencode/session",
data=b"{}",
content_type="application/json",
HTTP_HOST=ADMIN_TEST_HOST,
HTTP_SEC_FETCH_SITE="cross-site",
)
assert response.status_code == 403
def test_opencode_project_current_is_seeded_data_project(admin_client, tmp_path, db, monkeypatch):
from abx_plugins.plugins.opencode import views
workdir = tmp_path / "data"
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True, "OPENCODE_WORKDIR": str(workdir)})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: (True, ""))
response = admin_client.get(
f"/admin/agent/opencode/project/current?directory={workdir}",
HTTP_HOST=ADMIN_TEST_HOST,
)
assert response.status_code == 200
assert response.json()["id"] == "global"
assert response.json()["worktree"] == str(workdir.resolve())
assert response.json()["name"] == "data"
def test_opencode_path_reports_data_as_worktree(admin_client, tmp_path, db, monkeypatch):
from abx_plugins.plugins.opencode import views
workdir = tmp_path / "data"
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True, "OPENCODE_WORKDIR": str(workdir)})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: (True, ""))
response = admin_client.get(
f"/admin/agent/opencode/path?directory={workdir}",
HTTP_HOST=ADMIN_TEST_HOST,
)
assert response.status_code == 200
assert response.json()["directory"] == str(workdir.resolve())
assert response.json()["worktree"] == str(workdir.resolve())
def test_opencode_proxy_does_not_use_basic_auth(admin_client, db, monkeypatch):
from abx_plugins.plugins.opencode import views
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: (True, ""))
def fake_request(method, url, **kwargs):
assert "Authorization" not in kwargs["headers"]
upstream = requests.Response()
upstream.status_code = 200
upstream._content = b"{}"
upstream.headers["Content-Type"] = "application/json"
return upstream
monkeypatch.setattr(views.requests, "request", fake_request)
response = admin_client.get("/admin/agent/opencode/global/config", HTTP_HOST=ADMIN_TEST_HOST)
assert response.status_code == 200
def test_opencode_rewrites_vite_preload_assets():
from abx_plugins.plugins.opencode import views
body = b'const BL="modulepreload",UL=function(t){return"/"+t};const icon="/assets/sprite.svg#anthropic"'
rewritten = views._rewrite_text(body, {"origin": "http://127.0.0.1:4096"}).decode()
assert 'return"/"+t' not in rewritten
assert 'return"/admin/agent/opencode/"+t' in rewritten
assert '"/admin/agent/opencode/assets/sprite.svg#anthropic"' in rewritten
def test_opencode_proxy_streams_sse_without_large_buffer(admin_client, db, monkeypatch):
from abx_plugins.plugins.opencode import views
async def fake_event_chunks(request, settings, path):
assert path == "global/event"
yield b"data: {}\n\n"
async def collect(response):
return b"".join([chunk async for chunk in response.streaming_content])
monkeypatch.setattr(views, "_machine_config", lambda: {"OPENCODE_ENABLED": True})
monkeypatch.setattr(views, "_ensure_opencode", lambda settings: (True, ""))
monkeypatch.setattr(views, "_event_chunks", fake_event_chunks)
response = admin_client.get("/admin/agent/opencode/global/event", HTTP_HOST=ADMIN_TEST_HOST)
assert async_to_sync(collect)(response) == b"data: {}\n\n"
assert response.headers["X-Accel-Buffering"] == "no"
def test_opencode_sse_chunks_are_not_rewritten(monkeypatch):
from abx_plugins.plugins.opencode import views
class FakeRequest:
method = "GET"
GET = {}
headers = {}
class FakeUpstream:
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return None
async def aiter_raw(self, chunk_size=None):
assert chunk_size == 512
yield b'event: message.part.updated\ndata: {"delta":"a\\nb"}\n\n'
class FakeClient:
def __init__(self, **kwargs):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return None
def stream(self, *args, **kwargs):
return FakeUpstream()
monkeypatch.setattr(views.httpx, "AsyncClient", FakeClient)
async def collect():
settings = {"timeout": 30, "origin": "http://127.0.0.1:4096"}
return b"".join([chunk async for chunk in views._event_chunks(FakeRequest(), settings, "global/event")])
assert async_to_sync(collect)() == b'event: message.part.updated\ndata: {"delta":"a\\nb"}\n\n'
def test_opencode_starts_without_opening_browser(tmp_path, monkeypatch):
from abx_plugins.plugins.opencode import views
popen_kwargs = {}
health_checks = iter([False, False, True])
class FakeProcess:
def poll(self):
return None
def fake_popen(cmd, **kwargs):
popen_kwargs.update(kwargs)
return FakeProcess()
monkeypatch.setattr(views, "_health", lambda settings: next(health_checks))
monkeypatch.setattr(views.shutil, "which", lambda binary: "/usr/bin/false")
monkeypatch.setattr(views.subprocess, "Popen", fake_popen)
settings = views._settings({"OPENCODE_WORKDIR": str(tmp_path)})
ok, error = views._ensure_opencode(settings)
assert ok, error
assert popen_kwargs["cwd"] == tmp_path.resolve()
assert popen_kwargs["env"]["BROWSER"] == "false"
assert popen_kwargs["env"]["GIT_CEILING_DIRECTORIES"] == f"{tmp_path.resolve()}{os.pathsep}{tmp_path.parent.resolve()}"
assert popen_kwargs["env"]["OPENCODE_DISABLE_PROJECT_CONFIG"] == "true"
assert popen_kwargs["env"]["XDG_DATA_HOME"] == str(tmp_path / "opencode" / "data")
def test_opencode_state_dir_is_separate_from_workdir(tmp_path):
from abx_plugins.plugins.opencode import views
workdir = tmp_path / "data"
settings = views._settings({"OPENCODE_WORKDIR": str(workdir)})
views._ensure_project_files(settings)
assert settings["workdir"] == workdir
assert settings["opencode_dir"] == workdir / "opencode"
assert settings["config_home"] == workdir / "opencode" / "config"
assert settings["data_home"] == workdir / "opencode" / "data"
assert settings["state_home"] == workdir / "opencode" / "state"
skill = workdir / "opencode" / "config" / "opencode" / "skills" / "archivebox" / "SKILL.md"
assert skill.exists()
assert f"ArchiveBox collection directory: {workdir.resolve()}" in skill.read_text()
def test_opencode_seeds_global_project_under_embedded_data_dir(tmp_path):
from abx_plugins.plugins.opencode import views
workdir = tmp_path / "data"
settings = views._settings({"OPENCODE_WORKDIR": str(workdir)})
db_path = settings["data_home"] / "opencode" / "opencode.db"
db_path.parent.mkdir(parents=True)
with sqlite3.connect(db_path) as db:
db.execute(
"""
CREATE TABLE project (
id text PRIMARY KEY,
worktree text NOT NULL,
name text,
time_created integer NOT NULL,
time_updated integer NOT NULL,
sandboxes text NOT NULL
)
""",
)
views._ensure_global_project(settings)
with sqlite3.connect(db_path) as db:
row = db.execute("SELECT id, worktree, name, sandboxes FROM project").fetchone()
assert row == ("global", str(workdir.resolve()), "data", "[]")