Zum Inhalt

tests/test_gui_app_read.py

# tests/test_gui_app_read.py
import json

import pytest

pytest.importorskip("fastapi")
from fastapi.testclient import TestClient

from touchstone.gui import app as gui_app
from touchstone.gui.control import RunRegistry


class _FakeLauncher:
    def spawn(self, argv):
        return 1

    def alive(self, pid):
        return False

    def terminate(self, pid):
        return None


def _client(tmp_path):
    reg = RunRegistry(runs_dir=tmp_path, launcher=_FakeLauncher())
    return TestClient(gui_app.create_app(runs_dir=tmp_path, registry=reg))


def _mk_judged(tmp_path):
    d = tmp_path / "2026-06-20_eval_ndassist"
    d.mkdir(parents=True)
    (d / "bundle.json").write_text(
        json.dumps(
            {
                "pack_id": "ndassist",
                "pack_path": "packs/ndassist.yaml",
                "models": [{"id": "qwen2.5:3b", "quant": "q"}],
                "date": "2026-06-20",
            }
        ),
        encoding="utf-8",
    )
    (d / "responses.jsonl").write_text("", encoding="utf-8")
    (d / "scores.csv").write_text("metric_type\nnone\n", encoding="utf-8")
    return d


def test_overview_lists_bundles(tmp_path):
    _mk_judged(tmp_path)
    r = _client(tmp_path).get("/")
    assert r.status_code == 200
    assert "ndassist" in r.text


def test_overview_survives_partial_dim_scores(tmp_path):
    """A judged bundle whose judge omitted a master dimension (score='') must not
    500 the overview: classify succeeds and `/` returns 200 (BLOCKER 1)."""
    from touchstone.gui import bundles
    from touchstone.pack import load_pack

    d = tmp_path / "2026-06-20_eval_ndassist"
    d.mkdir(parents=True)
    pk = load_pack("packs/ndassist.yaml")
    (d / "bundle.json").write_text(
        json.dumps(
            {
                "pack_id": pk.id,
                "pack_path": "packs/ndassist.yaml",
                "models": [{"id": "qwen2.5:3b", "quant": "q"}],
                "date": "2026-06-20",
            }
        ),
        encoding="utf-8",
    )
    from touchstone.results import EvalResponse

    resp = EvalResponse(
        pack_id="ndassist",
        pack_version=1,
        machine="t",
        model="qwen2.5:3b",
        quant="q",
        engine="e",
        engine_version="x",
        variant="baseline",
        category="A",
        prompt_id="A1",
        repeat=0,
        response_text="ok",
        content_empty=False,
        ttft_s=0.1,
        decode_tps=1.0,
        prefill_tps=1.0,
        e2e_s=1.0,
        prompt_tokens=1,
        completion_tokens=1,
        is_cold_start=False,
        power_source="ac",
        peak_rss_mb=0.0,
        sys_used_mb=0.0,
        mem_pressure_max="normal",
        throttled=False,
        ok=True,
        error="",
        seed=42,
        t_start=0.0,
        t_end=1.0,
        reasoning_chars=0,
    )
    (d / "responses.jsonl").write_text(json.dumps(resp.as_dict()) + "\n", encoding="utf-8")
    # scores.csv: every dimension row present, but one (Q6) has a blank score.
    header = "model,variant,metric_type,metric,weight,score"
    lines = [header]
    for dim in pk.dimensions:
        score = "" if dim.id == "Q6" else "5"
        lines.append(f"qwen2.5:3b,baseline,dimension,{dim.id},{dim.weight},{score}")
    (d / "scores.csv").write_text("\n".join(lines) + "\n", encoding="utf-8")

    # classify must not raise
    summary = bundles.classify(d)
    assert summary is not None and summary.status == "judged"
    # and the `/` route returns 200 (every bundle visible, none hidden by a 500)
    r = _client(tmp_path).get("/")
    assert r.status_code == 200
    assert "ndassist" in r.text


def test_pack_explorer_renders_dimensions(tmp_path):
    r = _client(tmp_path).get("/packs/packs/ndassist.yaml")
    # route reads the pack file by path param; ndassist has dimensions Q1..Q7
    assert r.status_code == 200
    assert "Q1" in r.text or "Sicherheit" in r.text


def test_compare_renders_table(tmp_path):
    _mk_judged(tmp_path)
    r = _client(tmp_path).get("/compare")
    assert r.status_code == 200


def test_export_allowed_file(tmp_path):
    d = _mk_judged(tmp_path)
    # scores.csv was created by _mk_judged; verify it is served
    r = _client(tmp_path).get(f"/export/{d.name}/scores.csv")
    assert r.status_code == 200
    assert "metric_type" in r.text


def test_export_disallows_unknown_filename(tmp_path):
    d = _mk_judged(tmp_path)
    r = _client(tmp_path).get(f"/export/{d.name}/secrets.txt")
    assert r.status_code == 404


def test_export_rejects_path_traversal(tmp_path):
    # Plant a file one level above runs_dir to prove it cannot be reached.
    sensitive = tmp_path.parent / "sensitive.csv"
    sensitive.write_text("secret", encoding="utf-8")
    # URL-encoded ".." (%2e%2e) traversal attempt targeting a whitelisted filename.
    r = _client(tmp_path).get("/export/%2e%2e/scores.csv")
    assert r.status_code in {404, 422}  # rejected before or at path resolution