File size: 3,132 Bytes
9af2b22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""API smoke tests for plan seed, feedback priors, agent auth, rules reschedule."""

from __future__ import annotations

from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from app.config import get_settings
from app.schedule_math import ev_score
from app.schedule_reschedule import seed_starter_blocks


@pytest.fixture()
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
    monkeypatch.setenv("DATA_ROOT", str(tmp_path / "data"))
    monkeypatch.setenv("AGENT_TOKEN", "test-agent-token")
    monkeypatch.setenv("APP_SECRET_KEY", "test-secret-key")
    monkeypatch.setenv("APP_PASSWORD", "test-password")
    monkeypatch.setenv("ENV", "dev")
    get_settings.cache_clear()
    from app.main import create_app

    c = TestClient(create_app())
    assert c.post("/api/auth/login", json={"password": "test-password"}).status_code == 200
    return c


def test_ev_score_locked_formula() -> None:
    base = ev_score(p_done=0.8, utility=1.0, fun=0.5, d_hat_min=30, fse=0.2)
    raised = ev_score(
        p_done=0.8,
        utility=1.0,
        fun=0.5,
        d_hat_min=30,
        fse=0.2,
        raise_eta=True,
        raise_lambda_f=True,
    )
    assert isinstance(base, float) and isinstance(raised, float)


def test_seed_starter_keeps_p0() -> None:
    blocks = seed_starter_blocks("2026-07-19", capacity_hint=0.8)
    kinds = {b.kind for b in blocks}
    assert "food_out" in kinds and "sleep_window" in kinds
    assert all(b.locked for b in blocks if b.priority == "P0")


def test_plan_seed_and_feedback_priors(client: TestClient) -> None:
    day = "2026-07-19"
    r = client.post(f"/api/plan/{day}/seed")
    assert r.status_code == 200, r.text
    plan = r.json()["data"]
    assert plan["blocks"]
    assert "capacity_hint" in plan
    bid = plan["blocks"][0]["id"]
    fb = client.post(
        f"/api/plan/{day}/blocks/{bid}/feedback",
        json={
            "did": "done",
            "actual_min": 20,
            "quality": 4,
            "fun": 3,
            "energy_after": 1,
            "would_repeat": "yes",
        },
    )
    assert fb.status_code == 200, fb.text
    priors = client.get("/api/schedule/priors").json()["data"]["kinds"]
    assert any(p.get("n", 0) >= 1 for p in priors)


def test_agent_requires_token(client: TestClient) -> None:
    bare = TestClient(client.app)
    r = bare.get("/api/agent/context?day=2026-07-19")
    assert r.status_code in (401, 403)
    ok = bare.get(
        "/api/agent/context?day=2026-07-19",
        headers={"Authorization": "Bearer test-agent-token"},
    )
    assert ok.status_code == 200
    assert ok.json()["data"]["constraints"]["scope"] == "P1_thin_loop"


def test_reschedule_rules_source(client: TestClient) -> None:
    day = "2026-07-20"
    client.post(f"/api/plan/{day}/seed")
    r = client.post(f"/api/plan/{day}/reschedule", json={"reason": "test", "force": True})
    assert r.status_code == 200, r.text
    body = r.json()["data"]
    assert body["source"] in ("rules", "openrouter")
    p0 = [b for b in body["plan"]["blocks"] if b["priority"] == "P0"]
    assert p0