File size: 13,974 Bytes
0ffbb74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
"""Hosted demo (Hugging Face Space): live-run guards, key safety, shipped files.



See docs/hosting-plan.md, sections 2-5.

"""

import csv
import getpass
import json
import re
import runpy
import subprocess
import sys
from datetime import date
from pathlib import Path
from types import SimpleNamespace

import httpx2  # the HTTP library of the openai SDK (not httpx)
import pytest
from test_demo import _lens_script_client, _scripted_client

from beampilot import app as demo_app
from beampilot.agent import AgentConfig

REPO = Path(__file__).resolve().parents[1]
SENTINEL = "dummy-key-SENTINEL-7f3a9c"


@pytest.fixture(autouse=True)
def clean_env(monkeypatch):
    for name in ("BEAMPILOT_LIVE_RUNS", "BEAMPILOT_SPEND_CAP_USD"):
        monkeypatch.delenv(name, raising=False)
    monkeypatch.setattr(demo_app, "_GLOBAL", demo_app.GlobalBudget())


def _request(name: str) -> SimpleNamespace:
    demo_app._BROWSERS.pop(name, None)
    return SimpleNamespace(session_hash=name)


# Spend cap (BEAMPILOT_SPEND_CAP_USD)


def test_spend_cap_is_read_from_the_environment(monkeypatch) -> None:
    monkeypatch.setenv("BEAMPILOT_SPEND_CAP_USD", "0.3")
    assert demo_app.spend_cap_from_env() == 0.3
    assert demo_app.GlobalBudget().spend_cap_usd == 0.3
    for bad in ("", "lots", "-1", "0", "inf", "nan"):
        monkeypatch.setenv("BEAMPILOT_SPEND_CAP_USD", bad)
        assert demo_app.spend_cap_from_env() == demo_app.DEFAULT_SPEND_CAP_USD == 5.0


def test_spend_cap_admits_a_run_only_if_a_worst_case_run_still_fits() -> None:
    budget = demo_app.GlobalBudget(spend_cap_usd=1.0)
    assert budget.reserve() is None  # 0 + 0.55 <= 1
    assert budget.reserved_usd == pytest.approx(0.55)
    budget.add_cost(0.40)
    budget.release()
    assert budget.reserved_usd == 0.0
    assert budget.reserve() is None  # 0.40 + 0.55 = 0.95 <= 1
    budget.add_cost(0.10)
    budget.release()
    # 0.50 + 0.55 = 1.05 > 1: the next run could overshoot the cap.
    assert budget.out_of_budget() == demo_app.BUDGET_TEXT
    assert budget.reserve() == demo_app.BUDGET_TEXT
    assert budget.runs == 2 and budget.active == 0


def test_a_run_that_never_started_frees_its_reserve() -> None:
    budget = demo_app.GlobalBudget(spend_cap_usd=0.6)
    assert budget.reserve() is None
    budget.release(started=False)
    assert budget.runs == 0 and budget.reserved_usd == 0.0
    assert budget.out_of_budget() is None


def test_live_runs_add_their_cost_and_stop_at_the_cap(monkeypatch, tmp_path) -> None:
    monkeypatch.setattr(demo_app, "_client", _scripted_client)
    monkeypatch.setattr(demo_app, "LIVE_RUNS_DIR", tmp_path)
    monkeypatch.setattr(demo_app, "_GLOBAL", demo_app.GlobalBudget(spend_cap_usd=0.6))
    list(demo_app.live_run("clean", 0, "", _request("spend-1")))
    (saved,) = tmp_path.glob("*.json")
    cost = json.loads(saved.read_text(encoding="utf-8"))["cost_usd"]
    assert cost > 0
    assert demo_app._GLOBAL.spent_usd == pytest.approx(cost, abs=1e-5)
    assert demo_app._GLOBAL.reserved_usd == 0.0
    demo_app._GLOBAL.add_cost(0.6 - 0.55 - demo_app._GLOBAL.spent_usd + 0.01)
    update = next(demo_app.live_run("clean", 0, "", _request("spend-2")))
    assert update[2] == demo_app.BUDGET_TEXT


# Live switch (BEAMPILOT_LIVE_RUNS) and the page banner


def test_live_switch_values(monkeypatch) -> None:
    for off in ("off", "OFF", " false ", "0", "no"):
        monkeypatch.setenv("BEAMPILOT_LIVE_RUNS", off)
        assert not demo_app.live_switch_on()
    for on in ("on", "", "yes", "1"):
        monkeypatch.setenv("BEAMPILOT_LIVE_RUNS", on)
        assert demo_app.live_switch_on()


def test_switched_off_live_runs_never_start(monkeypatch) -> None:
    def no_client():
        raise AssertionError("no client may be made when live runs are off")

    monkeypatch.setattr(demo_app, "_client", no_client)
    monkeypatch.setenv("BEAMPILOT_LIVE_RUNS", "off")
    request = _request("switch-off")
    update = next(demo_app.live_run("clean", 0, "", request))
    assert update[2] == demo_app.LIVE_OFF_TEXT
    assert demo_app._GLOBAL.runs == 0 and demo_app._browser(request).runs == 0


def test_banner_and_start_button_at_page_load(monkeypatch, tmp_path) -> None:
    monkeypatch.chdir(tmp_path)  # no .env here
    monkeypatch.delenv("MODEL_API_KEY", raising=False)
    banner, enabled = demo_app.live_banner()
    assert demo_app.NO_KEY_TEXT in banner and not enabled
    assert "MODEL_API_KEY" in banner  # the variable's name, never a value

    monkeypatch.setenv("BEAMPILOT_LIVE_RUNS", "off")  # checked first
    banner, enabled = demo_app.live_banner()
    assert demo_app.LIVE_OFF_TEXT in banner and not enabled

    monkeypatch.delenv("BEAMPILOT_LIVE_RUNS")
    monkeypatch.setenv("MODEL_API_KEY", SENTINEL)
    assert demo_app.live_banner() == ("", True)
    assert SENTINEL not in json.dumps(demo_app.live_banner())

    monkeypatch.setenv("BEAMPILOT_SPEND_CAP_USD", "0.3")  # below one worst-case run
    monkeypatch.setattr(demo_app, "_GLOBAL", demo_app.GlobalBudget())
    banner, enabled = demo_app.live_banner()
    assert demo_app.BUDGET_TEXT in banner and not enabled


def test_live_runs_have_a_queue_of_one_and_replays_do_not_wait() -> None:
    pytest.importorskip("gradio")
    app = demo_app.build_app()
    fns = {getattr(f.fn, "__name__", ""): f for f in app.fns.values()}
    assert demo_app.MAX_CONCURRENT_RUNS == 1
    assert fns["start_run"].concurrency_limit == 1
    assert fns["start_run"].concurrency_id == "live"
    assert fns["replay"].concurrency_id != "live"
    assert fns["replay"].concurrency_limit == "default"  # REPLAY_CONCURRENCY


# Live lens runs use the v3 limits; normal runs keep theirs.


def _saved_config(tmp_path: Path) -> dict:
    (saved,) = tmp_path.glob("*.json")
    return json.loads(saved.read_text(encoding="utf-8"))["config"]


def test_live_lens_runs_use_the_v3_limits(monkeypatch, tmp_path) -> None:
    monkeypatch.setattr(demo_app, "_client", _lens_script_client)
    monkeypatch.setattr(demo_app, "LIVE_RUNS_DIR", tmp_path)
    request = _request("lens-limits")
    list(demo_app.live_run("waist_outside_range", 0, "", request, lens_kit=True))
    config = _saved_config(tmp_path)
    assert config["max_model_calls"] == 80
    assert config["max_total_tokens"] == 3_000_000
    assert config["max_cost_usd"] == 0.50
    assert config["change_lens"] is True
    assert demo_app._browser(request).session.scenario.max_positions == 28


def test_normal_live_runs_keep_their_limits(monkeypatch, tmp_path) -> None:
    monkeypatch.setattr(demo_app, "_client", _scripted_client)
    monkeypatch.setattr(demo_app, "LIVE_RUNS_DIR", tmp_path)
    list(demo_app.live_run("clean", 0, "", _request("normal-limits")))
    config, default = _saved_config(tmp_path), AgentConfig()
    assert config["max_model_calls"] == default.max_model_calls
    assert config["max_total_tokens"] == default.max_total_tokens
    assert config["max_cost_usd"] == default.max_cost_usd
    assert not config.get("change_lens")


# The key never reaches the UI, a run file or the console.


def test_dummy_key_never_reaches_ui_run_file_or_console(

    monkeypatch, tmp_path, capfd

) -> None:
    """The real client with a fake server that echoes the key in its error."""
    import openai

    seen = []

    def server(request: httpx2.Request) -> httpx2.Response:
        auth = request.headers.get("authorization", "")
        seen.append(auth)
        message = f"Incorrect API key provided: {auth}"
        return httpx2.Response(401, json={"error": {"message": message}})

    real = openai.OpenAI

    def offline_openai(*args, **kwargs):
        transport = httpx2.MockTransport(server)
        return real(*args, http_client=httpx2.Client(transport=transport), **kwargs)

    monkeypatch.setattr(openai, "OpenAI", offline_openai)
    monkeypatch.setattr(demo_app, "_CLIENT", None)  # build a fresh client
    monkeypatch.setattr(demo_app, "LIVE_RUNS_DIR", tmp_path)
    monkeypatch.setenv("MODEL_API_KEY", SENTINEL)
    updates = list(demo_app.live_run("clean", 0, "", _request("sentinel")))

    assert seen and SENTINEL in seen[0]  # the key really went to the server
    shown = json.dumps(updates, default=str)
    assert SENTINEL not in shown
    assert "AuthenticationError (HTTP 401)" in shown
    files = list(tmp_path.glob("*.json"))
    assert files
    for path in files:
        assert SENTINEL not in path.read_text(encoding="utf-8")
    out, err = capfd.readouterr()
    assert SENTINEL not in out and SENTINEL not in err


# Shipped replay files


def _personal_strings() -> set[str]:
    """Local user name, home folder and git e-mail of whoever runs the tests."""
    values = {getpass.getuser(), Path.home().name}
    try:
        email = subprocess.run(
            ["git", "config", "user.email"],
            capture_output=True,
            text=True,
            cwd=REPO,
            timeout=10,
        ).stdout.strip()
    except (OSError, subprocess.SubprocessError):
        email = ""
    if "@" in email:
        values |= {email, email.split("@", 1)[1]}
    generic = {"runner", "root", "user", "admin", "home"}
    return {v.lower() for v in values if len(v) >= 4 and v.lower() not in generic}


FORBIDDEN = {
    "Windows path": re.compile(r"[A-Za-z]:(\\\\|/)"),
    "home path": re.compile(r"/(home|Users)/\w"),
    "e-mail address": re.compile(r"[\w.+-]+@[\w-]+\.[A-Za-z]{2,}"),
    "key-like string": re.compile(r"\bsk-[A-Za-z0-9_-]{8,}|Bearer\s", re.IGNORECASE),
    "key name": re.compile(r"api[_-]?key|MODEL_API_KEY|BASETEN", re.IGNORECASE),
}


def test_shipped_run_files_hold_no_paths_names_or_keys() -> None:
    personal = _personal_strings()
    runs = demo_app.demo_runs()
    assert len(runs) == 5
    for path in runs:
        text = path.read_text(encoding="utf-8")
        for kind, pattern in FORBIDDEN.items():
            match = pattern.search(text)
            assert match is None, f"{path.name}: {kind} {match.group(0)!r}"
        lowered = text.lower()
        assert not [v for v in personal if v in lowered], path.name


def test_every_replay_has_a_recording_date() -> None:
    with demo_app.RECORDED_MANIFEST.open(encoding="utf-8", newline="") as handle:
        rows = list(csv.DictReader(handle))
    assert {row["file"] for row in rows} == {p.name for p in demo_app.demo_runs()}
    for row in rows:
        assert date.fromisoformat(row["recorded"]) >= date(2026, 10, 26)
        assert row["source"]


def test_replays_are_labelled_as_recorded(tmp_path) -> None:
    for label, value in demo_app.replay_choices():
        assert re.match(r"Recorded run, 2026-\d\d-\d\d, muse-spark-1\.2: ", label)
        assert (REPO / value).exists()
    shipped = (demo_app.DEMO_RUNS_DIR / "lens_v3_L5.json").relative_to(REPO)
    title = list(demo_app.replay(shipped.as_posix(), None, 0.0))[-1][0]
    assert "Recorded run, 2026-10-31, muse-spark-1.2" in title
    assert "a replay, not a live run" in title and "L5 (seed 904)" in title
    upload = tmp_path / "lens_v3_L5.json"  # same name, but uploaded
    upload.write_bytes((REPO / shipped).read_bytes())
    title = list(demo_app.replay(None, str(upload), 0.0))[-1][0]
    assert "Recorded run, date unknown, muse-spark-1.2" in title


# Space files: requirements, front matter, entry point


def _requirement_name(spec: str) -> str:
    name = re.split(r"[<>=!~;\[ ]", spec.strip(), maxsplit=1)[0]
    return name.lower().replace("_", "-")


def test_requirements_pin_exactly_the_project_dependencies() -> None:
    tomllib = pytest.importorskip("tomllib")
    project = tomllib.loads((REPO / "pyproject.toml").read_text(encoding="utf-8"))
    wanted = {_requirement_name(d) for d in project["project"]["dependencies"]}
    lines = (REPO / "requirements.txt").read_text(encoding="utf-8").splitlines()
    specs = [line.strip() for line in lines if line.strip() and line[0] != "#"]
    assert all(re.fullmatch(r"[A-Za-z0-9_.-]+==[0-9][\w.]*", s) for s in specs)
    assert {_requirement_name(s) for s in specs} == wanted
    assert "gradio" not in wanted  # pinned by sdk_version instead


def _front_matter(path: Path) -> dict[str, str]:
    text = path.read_text(encoding="utf-8")
    assert text.startswith("---\n")
    block = text[4 : text.index("\n---\n", 4)]
    pairs = (line.split(":", 1) for line in block.splitlines() if line.strip())
    return {key.strip(): value.strip().strip('"') for key, value in pairs}


def test_space_readme_front_matter_matches_the_project() -> None:
    meta = _front_matter(REPO / "space" / "README.md")
    pyproject = (REPO / "pyproject.toml").read_text(encoding="utf-8")
    gradio_pin = re.search(r'"gradio==([\d.]+)"', pyproject).group(1)
    assert meta["sdk"] == "gradio"
    assert meta["sdk_version"] == gradio_pin
    assert meta["python_version"] == "3.12"
    assert (REPO / meta["app_file"]).is_file()
    assert meta["license"] == "mit"
    assert meta["suggested_hardware"] == "cpu-basic"
    assert 0 < len(meta["short_description"]) <= 60
    text = (REPO / "space" / "README.md").read_text(encoding="utf-8")
    assert "simulated" in text and "https://github.com/Farshadmhd/beampilot" in text
    # The GitHub README carries no front matter.
    assert not (REPO / "README.md").read_text(encoding="utf-8").startswith("---")


def test_space_entry_point_runs_the_app_from_the_src_layout(monkeypatch) -> None:
    monkeypatch.setattr(sys, "path", list(sys.path))
    namespace = runpy.run_path(str(REPO / "scripts" / "space_app.py"), run_name="x")
    assert namespace["main"] is demo_app.main
    assert str(REPO / "src") == sys.path[0]
    assert demo_app.DEMO_RUNS_DIR == REPO / "docs" / "runs"