From 4cf2faa50a07b62d26bded92a430cb66df9cbba7 Mon Sep 17 00:00:00 2001 From: Aaron Personal Date: Sat, 8 Aug 2026 14:34:14 +0530 Subject: [PATCH 1/3] tests: add unit tests for session metric detail page --- tests/unit/dashboard/test_api.py | 95 ++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/unit/dashboard/test_api.py b/tests/unit/dashboard/test_api.py index e11e56c..1315fae 100644 --- a/tests/unit/dashboard/test_api.py +++ b/tests/unit/dashboard/test_api.py @@ -1471,3 +1471,98 @@ def test_requires_authentication(self, templates_client_no_auth): resp = templates_client_no_auth.get("/api/templates/run/anything/status") assert resp.status_code == 302 assert "/connect" in resp.headers["Location"] + + +class TestSessionMetricDetailPage: + """GET /session///metric/ — response codes, auth + enforcement, and content for the metric detail route.""" + + def test_returns_200_for_known_metric(self, metrics_client): + """A metric with recorded points must return 200 and render its data.""" + resp = metrics_client.get("/session/metrics-proj/m1/metric/train_loss") + assert resp.status_code == 200 + assert b"train_loss" in resp.data + + def test_content_includes_correct_point_values(self, metrics_client): + """The embedded metrics JSON blob must contain this metric's exact + step/value pairs, not another metric's or the full unfiltered list.""" + resp = metrics_client.get("/session/metrics-proj/m1/metric/train_loss") + html = resp.get_data(as_text=True) + assert '"step": 0' in html or '"step":0' in html + assert '"value": 0.9' in html or '"value":0.9' in html + # val_loss/acc points should not leak into this page's data blob + assert '"name": "val_loss"' not in html and '"name":"val_loss"' not in html + + def test_content_includes_breadcrumb_to_session(self, metrics_client): + """Breadcrumb must link back to the parent session page.""" + resp = metrics_client.get("/session/metrics-proj/m1/metric/train_loss") + assert b"/session/metrics-proj/m1" in resp.data + + def test_returns_404_for_unknown_metric(self, metrics_client): + """A metric name with no recorded points must 404, not render empty.""" + resp = metrics_client.get("/session/metrics-proj/m1/metric/does_not_exist") + assert resp.status_code == 404 + + def test_returns_404_for_unknown_session(self, metrics_client): + resp = metrics_client.get("/session/metrics-proj/nope/metric/train_loss") + assert resp.status_code == 404 + + def test_returns_404_for_unknown_project(self, metrics_client): + resp = metrics_client.get("/session/nope-proj/m1/metric/train_loss") + assert resp.status_code == 404 + + def test_returns_404_for_metric_on_session_with_no_metrics(self, metrics_client): + """m2 has zero nodes — any metric name 404s here too, since + there's no metric registry independent of recorded data.""" + resp = metrics_client.get("/session/metrics-proj/m2/metric/train_loss") + assert resp.status_code == 404 + + def test_requires_authentication(self, client_no_auth): + """Unauthenticated requests must redirect to /connect, same as the + parent session route.""" + resp = client_no_auth.get("/session/alpha/a1/metric/train_loss") + assert resp.status_code == 302 + assert "/connect" in resp.headers["Location"] + + def test_session_page_links_to_metric_detail(self, metrics_client): + """Each Graphs-tab chart card must link to its own detail page.""" + resp = metrics_client.get("/session/metrics-proj/m1") + assert resp.status_code == 200 + assert b"/session/metrics-proj/m1/metric/train_loss" in resp.data + assert b"/session/metrics-proj/m1/metric/val_loss" in resp.data + assert b"/session/metrics-proj/m1/metric/acc" in resp.data + + def test_session_page_without_metrics_does_not_break(self, tmp_path, monkeypatch): + """A completed session with no elements must still render + its Graphs tab (and links) without erroring — regression guard for + the empty-metrics case predating this feature.""" + import fenn.dashboard.app as app_module + + _write( + tmp_path / "nometrics.fn", + _FN_TMPL.format( + project="plain", + sid="nometrics", + started="2026-05-21 07:00:00", + ended="2026-05-21 07:00:10", + dur=10, + status="completed", + ), + ) + monkeypatch.setenv( + "FENN_DASHBOARD_OVERRIDES_PATH", str(tmp_path / "dashboard_overrides.json") + ) + scanner = FennScanner(extra_dirs=[str(tmp_path)]) + + original = app_module.scanner + app_module.scanner = scanner + app.config["TESTING"] = True + try: + with app.test_client() as c: + with c.session_transaction() as sess: + sess["user"] = {"email": "test@example.com"} + resp = c.get("/session/plain/nometrics") + assert resp.status_code == 200 + assert b"/session/plain/nometrics/metric/train_loss" in resp.data + finally: + app_module.scanner = original From 329d6938f0cb73fbb0a5d1468acf253b0cd035bf Mon Sep 17 00:00:00 2001 From: Aaron Personal Date: Sat, 8 Aug 2026 14:34:57 +0530 Subject: [PATCH 2/3] fix: update session ID format regex to include adjectives and nouns --- tests/unit/test_reproducibility.py | 4 ++-- tests/unit/utils/test_utils.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_reproducibility.py b/tests/unit/test_reproducibility.py index 079441a..db2a41b 100644 --- a/tests/unit/test_reproducibility.py +++ b/tests/unit/test_reproducibility.py @@ -98,8 +98,8 @@ def test_returns_string(self): def test_format_matches_pattern(self): session_id = generate_session_id() - # Expected: YYYYMMDD_HHMM_<4 hex chars> - assert re.match(r"^\d{8}_\d{4}_[0-9a-f]{4}$", session_id), ( + # Expected: YYYYMMDD_HHMM___<4 hex chars> + assert re.match(r"^\d{8}_\d{4}_[a-z]+_[a-z]+_[0-9a-f]{4}$", session_id), ( f"Session ID '{session_id}' does not match expected format" ) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index ca01f9b..4ecdc16 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -11,8 +11,10 @@ def test_session_id_returns_string(): def test_session_id_format(): result = generate_session_id() - # Should match: 20260412_1233_a3f9 - assert re.match(r"\d{8}_\d{4}_[a-f0-9]{4}", result), f"Unexpected format: {result}" + # Should match: 20260412_1233_autumn_moon_a3f9 + assert re.match(r"^\d{8}_\d{4}_[a-z]+_[a-z]+_[a-f0-9]{4}$", result), ( + f"Unexpected format: {result}" + ) def test_session_id_is_unique(): From bcc521319ea3393a320cbd707ec26493c362bbd3 Mon Sep 17 00:00:00 2001 From: Aaron Personal Date: Sat, 8 Aug 2026 14:44:54 +0530 Subject: [PATCH 3/3] style: pass ruff format check --- fenn/reproducibility.py | 79 ++++++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/fenn/reproducibility.py b/fenn/reproducibility.py index b0d8d8c..ef4a11b 100644 --- a/fenn/reproducibility.py +++ b/fenn/reproducibility.py @@ -33,21 +33,76 @@ def generate_session_id() -> str: # A curated list of "beautiful" words adjectives = [ - "autumn", "hidden", "bitter", "misty", "silent", - "empty", "dry", "dark", "summer", "icy", "delicate", - "quiet", "white", "cool", "spring", "winter", "patient", - "twilight", "dawn", "crimson", "wispy", "weathered", - "blue", "billowing", "broken", "cold", "damp", "falling", - "frosty", "green", "long", "late", "lingering" + "autumn", + "hidden", + "bitter", + "misty", + "silent", + "empty", + "dry", + "dark", + "summer", + "icy", + "delicate", + "quiet", + "white", + "cool", + "spring", + "winter", + "patient", + "twilight", + "dawn", + "crimson", + "wispy", + "weathered", + "blue", + "billowing", + "broken", + "cold", + "damp", + "falling", + "frosty", + "green", + "long", + "late", + "lingering", ] nouns = [ - "waterfall", "river", "breeze", "moon", "rain", - "wind", "sea", "morning", "snow", "lake", "sunset", - "pine", "shadow", "leaf", "dawn", "glitter", "forest", - "hill", "cloud", "meadow", "sun", "glade", "bird", - "brook", "butterfly", "bush", "dew", "dust", "field", - "fire", "flower", "firefly", "feather", "grass" + "waterfall", + "river", + "breeze", + "moon", + "rain", + "wind", + "sea", + "morning", + "snow", + "lake", + "sunset", + "pine", + "shadow", + "leaf", + "dawn", + "glitter", + "forest", + "hill", + "cloud", + "meadow", + "sun", + "glade", + "bird", + "brook", + "butterfly", + "bush", + "dew", + "dust", + "field", + "fire", + "flower", + "firefly", + "feather", + "grass", ] # Select words