File size: 3,536 Bytes
c0608db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a01d7ac
 
 
 
 
 
 
 
 
3285f4c
 
 
a01d7ac
 
 
 
 
 
c0608db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import numpy as np
import pandas as pd
import pytest

from tabicl_mcp import server
from tabicl_mcp.report import build_report

CSV = "x1,x2,label\n" + "\n".join(
    f"{i % 10},{(i * 7) % 5},{'yes' if i % 10 > 4 else 'no'}" for i in range(60)
)


def test_load_data_inline():
    result = server.load_data(csv_content=CSV, target_column="label")
    assert result["dataset_id"].startswith("ds_")
    assert result["n_rows"] == 60
    assert result["target"]["suggested_task"] == "classification"


def test_load_data_requires_one_source():
    assert "error" in server.load_data()
    assert "error" in server.load_data(csv_content=CSV, url="https://x.com/a.csv")


def test_load_data_bad_target_warns():
    result = server.load_data(csv_content=CSV, target_column="nope")
    assert "target_warning" in result


def test_evaluate_unknown_dataset_id_is_friendly():
    result = server.evaluate(target_column="label", dataset_id="ds_missing")
    assert "not found" in result["error"]


def test_export_predictions_pages():
    df = pd.DataFrame({"a": range(10), "p": range(10)})
    result_id = server.D.CACHE.put(df, prefix="pred")
    page = server.export_predictions(result_id, offset=8, limit=5)
    assert page["total_rows"] == 10
    assert page["returned_rows"] == 2
    assert "csv" in page


@pytest.mark.slow
def test_full_flow_and_report(tmp_path, monkeypatch):
    monkeypatch.setattr(server, "REPORTS_DIR", str(tmp_path))
    loaded = server.load_data(csv_content=CSV)
    ds = loaded["dataset_id"]

    evaluation = server.evaluate(target_column="label", dataset_id=ds)
    assert "metrics" in evaluation, evaluation

    report = server.create_report(target_column="label", dataset_id=ds)
    assert "report_file" in report, report
    html = open(report["report_file"], encoding="utf-8").read()
    assert "<svg" in html and "Accuracy" in html


def test_upload_page_roundtrip():
    from starlette.testclient import TestClient

    app = server.mcp.streamable_http_app()
    with TestClient(app) as client:
        assert "Upload a CSV" in client.get("/").text
        resp = client.post("/upload", files={"file": ("t.csv", b"a,b\n1,2\n3,4\n", "text/csv")})
        assert "ds_" in resp.text
        assert "✔ Uploaded" in resp.text and "2 rows × 2 columns" in resp.text
        import re

        ds_id = re.search(r"ds_\w+", resp.text).group(0)
        assert server.D.CACHE.get(ds_id).df.shape == (2, 2)

        bad = client.post("/upload", files={"file": ("t.csv", b"", "text/csv")})
        assert "✘" in bad.text


def test_build_report_smoke_without_model():
    evaluation = {
        "task_type": "classification",
        "target_column": "label",
        "note": "test",
        "metrics": {
            "accuracy": 0.9,
            "balanced_accuracy": 0.88,
            "f1_macro": 0.89,
            "roc_auc": 0.95,
            "confusion_matrix": {"labels": ["no", "yes"], "matrix": [[40, 5], [4, 51]]},
        },
        "class_distribution": {"no": 45, "yes": 55},
    }
    importance = {
        "importances": [
            {"feature": "income", "importance": 0.31, "std": 0.02},
            {"feature": "age", "importance": 0.12, "std": 0.01},
        ]
    }
    html = build_report(
        title="Test report",
        dataset_summary={"n_rows": 100, "n_columns": 3},
        evaluation=evaluation,
        importance=importance,
    )
    assert "Test report" in html
    assert "income" in html
    assert "pred: yes" in html
    assert "<script" not in html  # self-contained, no JS