Files
nextcraft/apps/ai-service/tests/api/test_lab.py
T
CIAgent 1a88ab3199
Release / release (push) Successful in 3m45s
CI / python (push) Successful in 1m52s
CI / node (push) Successful in 1m0s
merge: milestone/v0.8-ship-anywhere -> main (v0.8 complete — Ship Anywhere)
The v0.8 milestone: installable on any Debian-based server via docker
(Gitea registry image, honest sandbox degrade), deb (amd64+arm64,
advisory postinst), and arch-aware binary — with release consistency
enforced by pre-commit hooks + Gitea Actions (3-runner fleet on CT 113,
machine-made releases with the full 8-asset matrix + registry image), the
CLI carrying the full suite (guide, channel-aware doctor), and the admin
runbook (deploy/README.md, evidence-backed recipes).

---ci---
phase: 6
milestone: v0.8
status: complete
requirements:
  covered: [REQ-8-001, REQ-8-002, REQ-8-003, REQ-8-004, REQ-8-005]
  partial: []
---/ci---
2026-09-29 16:04:40 +00:00

88 lines
3.1 KiB
Python

"""Lab endpoint tests — LIVE trace contract (REQ-3-007).
v0.3 re-grounding: POST /v1/lab/feedback takes {learner_id, task_id}; the
digest is computed from the learner's real TraceStore events. No corpus
scenarios; empty trace is a valid "no telemetry yet" coaching path.
"""
from __future__ import annotations
from collections.abc import Iterator
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from ai_service.config import Settings
from ai_service.grading.store import SQLiteGradeStore
from ai_service.llm.mock import MockProvider
from ai_service.main import create_app
from ai_service.telemetry.ingest import TraceIntegrityMap
from ai_service.telemetry.models import TelemetryEvent
from ai_service.telemetry.store import SQLiteTraceStore
T0 = datetime(2026, 9, 12, tzinfo=UTC)
def _event(seq: int, kind: str, payload: dict, offset_s: float):
return TelemetryEvent(
learner_id="lab-learner",
task_id="lab-task",
seq=seq,
kind=kind,
payload=payload,
ts=T0 + timedelta(seconds=offset_s),
sandbox_id="sbx-lab",
)
class StreamingMock(MockProvider):
"""Deterministic token stream for the SSE path."""
def _reply_for(self, messages, response_format):
return "Feedback grounded in your live session digest."
@pytest.fixture()
def client(tmp_path: Path) -> Iterator[TestClient]:
app = create_app(Settings(provider="mock", db_path=tmp_path / "app.db"))
app.state.provider = StreamingMock()
app.state.trace_store = SQLiteTraceStore(db_path=tmp_path / "t.db")
app.state.grade_store = SQLiteGradeStore(db_path=tmp_path / "g.db")
app.state.trace_integrity = TraceIntegrityMap()
with TestClient(app) as c:
yield c
def test_live_trace_streams_full_envelope(client: TestClient) -> None:
store: SQLiteTraceStore = client.app.state.trace_store # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
for e in [
_event(0, "file_diff", {"path": "x.py"}, 0),
_event(1, "command", {"cmd": "pytest -q"}, 10),
_event(2, "test_result", {"passed": False, "exit_code": 1}, 20),
_event(3, "test_result", {"passed": True, "exit_code": 0}, 40),
]:
store.append(e)
with client.stream(
"POST", "/v1/lab/feedback", json={"learner_id": "lab-learner", "task_id": "lab-task"}
) as resp:
assert resp.status_code == 200
body = "".join(chunk.decode() for chunk in resp.iter_raw())
assert '"agent": "lab"' in body or '"agent":"lab"' in body
assert '"task_id": "lab-task"' in body
assert '"type": "delta"' in body or '"type":"delta"' in body
def test_empty_trace_coaches_the_baseline(client: TestClient) -> None:
"""No telemetry is NOT an error — Lab coaches 'run the starter test'."""
with client.stream(
"POST", "/v1/lab/feedback", json={"learner_id": "lab-learner", "task_id": "no-events"}
) as resp:
assert resp.status_code == 200
def test_missing_fields_422(client: TestClient) -> None:
resp = client.post("/v1/lab/feedback", json={"learner_id": "x"})
assert resp.status_code == 422