STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_layerx_router.py
Go to the documentation of this file.
1"""The Layer X routes beyond the happy path (backend/routers/layerx.py).
2
3The router had one test, a burn that worked. These are the paths that decide whether a person
4loses work or reads a wrong state: the one-job rule, what pruning keeps, pins, names and notes,
5deleting, the listing's index, the study's refusals and the measurement bounds. They run without
6a burn except where a preflight is the thing under test.
7"""
8
9from __future__ import annotations
10
11import json
12import os
13import sys
14
15import pytest
16
17sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18
19from fastapi import FastAPI # noqa: E402
20from fastapi.testclient import TestClient # noqa: E402
21
22from backend.routers import layerx as lx # noqa: E402
23from backend.session import UserSession, get_session # noqa: E402
24
25
26@pytest.fixture()
27def api(tmp_path, monkeypatch):
28 monkeypatch.setenv("USERDATA_DIR", str(tmp_path))
29 session = UserSession(f"router-{tmp_path.name}")
30 app = FastAPI()
31 app.include_router(lx.router)
32 app.dependency_overrides[get_session] = lambda: session
33 yield TestClient(app), session
34 with lx._JOBS_LOCK:
35 for job_id in [j.id for j in lx._JOBS.values() if j.user == session.user]:
36 lx._JOBS.pop(job_id, None)
37
38
39def _saved(session, run_id: str, kind: str = "run", impulse: float = 24000.0) -> lx.Job:
40 job = lx.Job(session.user, lx.Settings(drawing_id="d"), "design", kind=kind)
41 job.id = run_id
42 job.status, job.finished = "done", 1.0
43 job.result = {"summary": {"burn_time_s": 3.5, "total_impulse_Ns": impulse}, "provenance": {"drawing": {"name": "stand"}}}
44 lx._persist(lx._runs_dir(session), job)
45 return job
46
47
49 client, session = api
50 busy = lx.Job(session.user, lx.Settings(drawing_id="d"), "design")
51 busy.status = "running"
52 with lx._JOBS_LOCK:
53 lx._JOBS[busy.id] = busy
54 r = client.post("/api/layerx/runs", json={"drawing_id": "anything"})
55 assert r.status_code == 409 and busy.id in r.json()["detail"]
56
57
59 client, session = api
60 _saved(session, "20261002-000001-aaaaaa", impulse=24100.0)
61 runs_dir = lx._runs_dir(session)
62 assert (runs_dir / "_index" / "20261002-000001-aaaaaa.json").is_file()
63 # The listing never opens the run file: make it unreadable and the entry is still there.
64 (runs_dir / "20261002-000001-aaaaaa.json").write_text(json.dumps({"id": "20261002-000001-aaaaaa", "result": "x" * 10}))
65 listed = client.get("/api/layerx/runs").json()
66 assert listed[0]["summary"]["total_impulse_Ns"] == 24100.0
67 meta = client.patch("/api/layerx/runs/20261002-000001-aaaaaa", json={"name": " 600 psia, GN2 ", "pinned": True}).json()
68 assert meta == {"name": "600 psia, GN2", "pinned": True}
69 listed = client.get("/api/layerx/runs").json()
70 assert listed[0]["meta"]["pinned"] is True
71 assert client.patch("/api/layerx/runs/20261002-000009-ffffff", json={"pinned": True}).status_code == 404
72 assert client.patch("/api/layerx/runs/../../etc", json={"pinned": True}).status_code == 404
73
74
76 client, session = api
77 monkeypatch.setattr(lx, "KEEP_RUNS", 2)
78 _saved(session, "20261002-000001-aaaaaa")
79 client.patch("/api/layerx/runs/20261002-000001-aaaaaa", json={"pinned": True})
80 _saved(session, "20261002-000002-bbbbbb")
81 _saved(session, "20261002-000003-cccccc", kind="trade") # another kind: not counted against runs
82 _saved(session, "20261002-000004-dddddd")
83 _saved(session, "20261002-000005-eeeeee")
84 ids = {r["id"] for r in client.get("/api/layerx/runs").json()}
85 assert ids == {"20261002-000001-aaaaaa", "20261002-000003-cccccc", "20261002-000004-dddddd", "20261002-000005-eeeeee"}
86 assert not (lx._runs_dir(session) / "_index" / "20261002-000002-bbbbbb.json").exists()
87
88
90 client, session = api
91 _saved(session, "20261002-000001-aaaaaa")
92 client.patch("/api/layerx/runs/20261002-000001-aaaaaa", json={"note": "first fire prediction"})
93 assert client.delete("/api/layerx/runs/20261002-000001-aaaaaa").json()["deleted"]
94 runs_dir = lx._runs_dir(session)
95 assert not any(runs_dir.rglob("20261002-000001-aaaaaa*"))
96 assert client.get("/api/layerx/runs").json() == []
97 live = lx.Job(session.user, lx.Settings(drawing_id="d"), "design")
98 live.status = "running"
99 with lx._JOBS_LOCK:
100 lx._JOBS[live.id] = live
101 assert client.delete(f"/api/layerx/runs/{live.id}").status_code == 409
102
103
105 from engine.layerx.measurements import Override
106
107 base = {"target": "node:OXT", "parameter": "volume", "value": 15.1, "unit": "L", "source": "weighed"}
108 assert Override.from_dict(base).value == 15.1
109 with pytest.raises(ValueError, match="negative"):
110 Override.from_dict({**base, "value": -1.0})
111 assert Override.from_dict({**base, "parameter": "elevation_change", "value": -0.3}).value == -0.3 # a height may fall
112 with pytest.raises(ValueError, match="longer than"):
113 Override.from_dict({**base, "source": "x" * 400})
114
115
117 """The list swapped in the replay's delivered impulse but kept the twin's mean thrust, so impulse
118 over burn time was not the mean it printed (LE4 (3): 23737.9 N s delivered beside 6183.8 N)."""
119 result = {"summary": {"total_impulse_Ns": 23729.9, "mean_thrust_N": 6183.8, "burn_time_s": 3.8364},
120 "delivered": {"summary": {"total_impulse_Ns": 23737.9, "mean_thrust_N": 6187.4}}}
121 s = lx._listing_summary(result)
122 assert (s["total_impulse_Ns"], s["mean_thrust_N"]) == (23737.9, 6187.4)
123 assert lx._listing_summary({"summary": {"total_impulse_Ns": 1.0, "mean_thrust_N": 2.0}})["mean_thrust_N"] == 2.0
124
125
127 """POST /engine-card with center_psia -5 said "the config states no tank pressure" though it stated 578."""
128 from engine.layerx.card import card_for_config_text
129
130 with pytest.raises(ValueError, match=r"center_psia -5\.0 is not an absolute pressure"):
131 card_for_config_text("lox_tank: {initial_pressure_psi: 578.0}\n", center_psia=-5.0)
132
133
135 """Every Layer X run was recorded as design 'default' whatever design the bar had open: the design
136 bar loads a design with POST /config/load, which keeps the session's first config_path, and the
137 router filed jobs under that file's stem. sweepFor's 'never borrow another design's sweep' was then
138 defeated. A job started with the bar's X-Design-Id is filed under that design's name."""
139 from fastapi import HTTPException
140
141 from backend.routers import documents
142
143 client, session = api
144 me = session.user
145 records = {(me, "d1"): {"id": "d1", "name": "ethalox 6800N doublet 578psia"},
146 ("carol", "d2"): {"id": "d2", "name": "Carol's private design"},
147 ("dave", "d3"): {"id": "d3", "name": "Dave's, shared", "sharedWith": [me]}}
148 monkeypatch.setattr(documents.store, "find_record", lambda owner, doc_id: records.get((owner, doc_id)))
149 monkeypatch.setattr(documents.store, "read_featured", lambda: None)
150 filed = []
151
152 def reserve(session_, settings, kind, design=None):
153 filed.append(design)
154 raise HTTPException(status_code=409, detail="captured")
155
156 monkeypatch.setattr(lx, "_reserve", reserve)
157 body = {"drawing_id": "d"}
158 assert client.post("/api/layerx/runs", json=body, headers={"X-Design-Id": "d1"}).status_code == 409
159 assert client.post("/api/layerx/uncertainty", json=body, headers={"X-Design-Id": "d1"}).status_code == 409
160 assert filed == ["ethalox 6800N doublet 578psia"] * 2
161 # Someone else's design is named only when the caller may see it.
162 client.post("/api/layerx/runs", json=body, headers={"X-Design-Id": "d2", "X-Design-Owner": "carol"})
163 client.post("/api/layerx/runs", json=body, headers={"X-Design-Id": "d3", "X-Design-Owner": "dave"})
164 assert filed[2:] == [lx._design_name(session), "Dave's, shared"]
165 # No design named: the session's file, as before.
166 client.post("/api/layerx/runs", json=body)
167 assert filed[4] == lx._design_name(session)
168
169
171 client, session = api
172 job = lx.Job(session.user, lx.Settings(drawing_id="d"), "default", design_sha256="abc123")
173 assert job.view()["design"] == "default" and job.view()["design_sha256"] == "abc123"
174 assert job.view(with_result=False)["design_sha256"] == "abc123"
175
176
178 """A server started at 9ebd9fa0 and never restarted filed its second burn as '170af951-dirty' once
179 other commits landed: git describe was asked per run. The code a process runs is what it imported."""
180 import subprocess
181
182 loaded = lx._code_version()
183
184 class Moved:
185 stdout = "170af951-dirty\n"
186
187 monkeypatch.setattr(subprocess, "run", lambda *a, **k: Moved())
188 assert lx._code_version() == loaded
189 assert lx._design_snapshot(type("C", (), {"model_dump": lambda self, mode=None: {}})())["code"] == loaded
190
a class to store JSON values
Definition json.hpp:19401
test_one_job_at_a_time_is_a_409_before_any_work(api)
test_a_job_is_filed_under_the_design_that_is_open(api, monkeypatch)
api(tmp_path, monkeypatch)
lx.Job _saved(session, str run_id, str kind="run", float impulse=24000.0)
test_delete_removes_the_run_its_entry_and_its_notes_but_not_a_live_job(api)
test_a_card_centre_that_is_not_a_pressure_names_the_callers_number()
test_pruning_keeps_the_newest_per_kind_and_every_pinned_run(api, monkeypatch)
test_a_run_names_the_code_the_server_loaded_not_the_checkout_now(monkeypatch)
test_the_listing_reads_the_index_and_carries_names_and_pins(api)
test_the_listing_quotes_one_burn_the_delivered_one()
test_a_job_records_the_design_fingerprint_beside_its_name(api)