"""HTTP-level tests for /projects/{name}/jobs and /jobs surfaces. These tests stub out the runner so we don't actually clone/run subprocesses inside the API tests — see test_runner.py for the exec-side coverage. """ from __future__ import annotations import time import pytest from tests.conftest import sample_project_payload def _stub_runner(server): """Replace runner.enqueue with a no-op so we can exercise the API surface without driving the dispatcher.""" server.runner.enqueue = _aiono_op # type: ignore[assignment] async def _aiono_op(*_a, **_k): return None def test_create_job_returns_id(client): tc, ctx = client _stub_runner(ctx["server"]) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="job-proj"), ) r = tc.post( "/projects/job-proj/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "audit"}, ) assert r.status_code == 200, r.text body = r.json() assert body["ok"] is True assert isinstance(body["job_id"], str) and len(body["job_id"]) >= 16 assert body["status"] == "queued" def test_create_job_invalid_recipe(client): tc, ctx = client _stub_runner(ctx["server"]) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="invrec"), ) r = tc.post( "/projects/invrec/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "not-a-real-recipe"}, ) # Pydantic Literal validation -> 422 assert r.status_code in (400, 422) def test_create_job_subproject_without_recipe_command(client): tc, ctx = client _stub_runner(ctx["server"]) payload = sample_project_payload(name="missing-cmd") # Drop the audit command payload["subprojects"][0]["audit"] = None payload["subprojects"][0]["build"] = None payload["subprojects"][0]["test"] = None payload["subprojects"][0]["lint"] = "echo lint" tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=payload, ) r = tc.post( "/projects/missing-cmd/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "audit"}, ) assert r.status_code == 400 def test_list_jobs_filters_by_project_and_token(client): tc, ctx = client _stub_runner(ctx["server"]) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="pa"), ) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}, json=sample_project_payload(name="pb"), ) tc.post( "/projects/pa/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "test"}, ) tc.post( "/projects/pb/jobs", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}, json={"recipe": "test"}, ) r = tc.get( "/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r.status_code == 200 jobs = r.json()["jobs"] assert all(j["project_name"] == "pa" for j in jobs) def test_list_jobs_filter_status(client): tc, ctx = client _stub_runner(ctx["server"]) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="ps"), ) tc.post( "/projects/ps/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "test"}, ) r = tc.get( "/jobs?status=queued", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r.status_code == 200 assert all(j["status"] == "queued" for j in r.json()["jobs"]) r2 = tc.get( "/jobs?status=succeeded", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r2.json()["jobs"] == [] def test_get_job_includes_log_tail(client, tmp_workspace): tc, ctx = client _stub_runner(ctx["server"]) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="logged"), ) r = tc.post( "/projects/logged/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "test"}, ) job_id = r.json()["job_id"] # Plant a fake log file log_path = tmp_workspace["log_dir"] / f"{job_id}.log" log_path.write_text("\n".join(f"line-{i}" for i in range(300)) + "\n") r2 = tc.get( f"/jobs/{job_id}", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r2.status_code == 200 body = r2.json() assert "log_tail" in body assert len(body["log_tail"]) == 200 assert body["log_tail"][-1] == "line-299" def test_get_job_other_token_404(client): tc, ctx = client _stub_runner(ctx["server"]) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="cross-job"), ) r = tc.post( "/projects/cross-job/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "test"}, ) job_id = r.json()["job_id"] r2 = tc.get( f"/jobs/{job_id}", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}, ) assert r2.status_code == 404 def test_get_findings_empty_in_wave1(client): tc, ctx = client _stub_runner(ctx["server"]) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="findings-test"), ) r = tc.post( "/projects/findings-test/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "audit"}, ) job_id = r.json()["job_id"] r2 = tc.get( f"/jobs/{job_id}/findings", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r2.status_code == 200 assert r2.json()["findings"] == [] def test_get_log_streams_file(client, tmp_workspace): tc, ctx = client _stub_runner(ctx["server"]) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="logstream"), ) r = tc.post( "/projects/logstream/jobs", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json={"recipe": "test"}, ) job_id = r.json()["job_id"] log_path = tmp_workspace["log_dir"] / f"{job_id}.log" log_path.write_text("hello world\nmore lines\n") r2 = tc.get( f"/jobs/{job_id}/log", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r2.status_code == 200 assert "hello world" in r2.text def test_healthz_open_to_lan(client): tc, _ = client r = tc.get("/healthz") assert r.status_code == 200 body = r.json() assert body["ok"] is True assert body["db"] == "ok" assert "runner" in body and "max" in body["runner"]