- Job logs: each job's stdout/stderr is teed into outputs/<id>/job.log
(survives restarts) and served at GET /jobs/{id}/log as text/plain, so
full run logs can be shared for debugging and refinement.
- Model picker: GET /models proxies OpenRouter's public model list with
per-1M-token pricing (1h cache, 502 on failure); the UI shows a model
dropdown with costs when OpenRouter compute is selected, and the pick
overrides vision+text models for that job (Classic and Agent modes).
- Conflicts in the report view are grouped by discipline pair
(collapsible sections, severity-ordered within groups) instead of one
flat severity-only list.
78 lines
2.5 KiB
Python
78 lines
2.5 KiB
Python
import threading
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
import backend.jobs as jobs
|
|
from backend.main import app
|
|
|
|
|
|
class _SyncThread:
|
|
"""Drop-in threading.Thread replacement that runs the target inline."""
|
|
|
|
def __init__(self, target=None, args=(), kwargs=None, **_ignored):
|
|
self._target = target
|
|
self._args = args
|
|
self._kwargs = kwargs or {}
|
|
|
|
def start(self):
|
|
self._target(*self._args, **self._kwargs)
|
|
|
|
|
|
@pytest.fixture
|
|
def job_env(monkeypatch, tmp_path):
|
|
monkeypatch.setattr("backend.config.OUTPUT_DIR", str(tmp_path))
|
|
monkeypatch.setattr(threading, "Thread", _SyncThread)
|
|
monkeypatch.setattr("backend.jobs.send_conflict_report", lambda *a, **k: True)
|
|
pdf = tmp_path / "set.pdf"
|
|
pdf.write_bytes(b"%PDF-1.4\n")
|
|
yield tmp_path
|
|
jobs._jobs.clear()
|
|
|
|
|
|
def test_job_log_captures_pipeline_output(job_env, monkeypatch):
|
|
def fake_runner(pdf_path, **kwargs):
|
|
print("STAGE banner: fake wave ran")
|
|
return {"source": "set.pdf", "summary": {"conflicts_found": 0}}
|
|
|
|
monkeypatch.setattr("backend.jobs.run_pipeline", fake_runner)
|
|
job_id = jobs.create_job(str(job_env / "set.pdf"), "set.pdf", pipeline_mode="classic")
|
|
|
|
log_path = job_env / job_id / "job.log"
|
|
assert log_path.is_file()
|
|
content = log_path.read_text()
|
|
assert "STAGE banner: fake wave ran" in content
|
|
assert job_id in content # header line
|
|
|
|
|
|
def test_job_log_endpoint_serves_log_and_404s(job_env, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"backend.jobs.run_pipeline",
|
|
lambda pdf_path, **kw: {"source": "s", "summary": {}},
|
|
)
|
|
job_id = jobs.create_job(str(job_env / "set.pdf"), "set.pdf", pipeline_mode="classic")
|
|
|
|
client = TestClient(app)
|
|
ok = client.get(f"/jobs/{job_id}/log")
|
|
assert ok.status_code == 200
|
|
assert ok.headers["content-type"].startswith("text/plain")
|
|
assert "Job " + job_id in ok.text
|
|
assert client.get("/jobs/nope/log").status_code == 404
|
|
|
|
|
|
def test_model_override_set_and_cleared_around_run(job_env, monkeypatch):
|
|
from backend import llm
|
|
|
|
seen = {}
|
|
|
|
def fake_runner(pdf_path, **kwargs):
|
|
seen["override"] = llm._model_override
|
|
return {"source": "set.pdf", "summary": {}}
|
|
|
|
monkeypatch.setattr("backend.jobs.run_pipeline", fake_runner)
|
|
jobs.create_job(str(job_env / "set.pdf"), "set.pdf",
|
|
pipeline_mode="classic", model="openai/gpt-4o")
|
|
|
|
assert seen["override"] == "openai/gpt-4o"
|
|
assert llm._model_override is None # cleared after the run
|