Spaces:
Running on Zero
Running on Zero
| """Phase 3 acceptance: the UI renders, and every displayed number is traceable. | |
| These run against the real cached store when it is present, and skip cleanly | |
| when it is not (a fresh clone with no `.cache/store` yet). | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import sys | |
| from pathlib import Path | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| import pytest | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| import app as bitapp | |
| from src import charts, comparisons, config, runtime, strategies | |
| from src.ui import components as C | |
| from src.ui.format import money, num, pct | |
| from src.runtime import RunRequest | |
| def _store_ready() -> bool: | |
| try: | |
| m = runtime.get_store().load_manifest() | |
| return len(m.prices) > 0 | |
| except Exception: | |
| return False | |
| pytestmark = pytest.mark.skipif(not _store_ready(), | |
| reason="no cached signal store available") | |
| def rec(): | |
| return runtime.execute(RunRequest( | |
| strategy="SMA Crossover", asset="BTC-USD", timeframe="1d", date_range="3Y", | |
| )) | |
| def three_runs(): | |
| reqs = [ | |
| RunRequest(strategy="SMA Crossover", asset="BTC-USD", timeframe="1d", date_range="3Y"), | |
| RunRequest(strategy="RSI Mean Reversion", asset="ETH-USD", timeframe="1d", date_range="3Y"), | |
| RunRequest(strategy="Buy & Hold (benchmark)", asset="SPY", timeframe="1d", date_range="3Y"), | |
| ] | |
| return [runtime.execute(r) for r in reqs] | |
| # -------------------------------------------------------------------------- | |
| # App construction | |
| # -------------------------------------------------------------------------- | |
| def test_app_object_exists(): | |
| assert bitapp.demo is not None | |
| def test_theme_css_carries_the_design_tokens(): | |
| from src.ui import theme | |
| css = theme.full_css() | |
| for token in ("--bg-canvas", "--accent-amber", "--fin-up", "--font-styrene"): | |
| assert token in css, f"{token} missing from theme CSS" | |
| assert "#161512" in css # stone-950 canvas | |
| assert "#af9209" in css # accent amber | |
| def test_disclaimer_is_present_in_the_footer(): | |
| assert "not indicative of future results" in C.footer() | |
| assert "Not financial advice" in C.footer() or \ | |
| "not a licensed investment adviser" in C.footer() | |
| def test_empty_state_offers_the_worked_example(): | |
| assert "No run loaded" in C.empty_state() | |
| assert "worked example" in C.empty_state() | |
| def test_glossary_covers_every_design_term(): | |
| terms = {t for t, _ in bitapp.GLOSSARY} | |
| assert {"SHARPE", "SORTINO", "MAX DRAWDOWN", "PROFIT FACTOR", | |
| "R-MULTIPLE", "MAE / MFE", "WALK-FORWARD", "OOS"} <= terms | |
| # -------------------------------------------------------------------------- | |
| # The stat band must match engine output exactly | |
| # -------------------------------------------------------------------------- | |
| def test_stat_band_values_match_engine_metrics(rec): | |
| html = C.stat_band(rec) | |
| m = rec.result.metrics_all | |
| assert pct(m.total_return) in html | |
| assert pct(m.cagr) in html | |
| assert num(m.sharpe) in html | |
| assert num(m.sortino) in html | |
| assert pct(m.max_drawdown) in html | |
| assert num(m.profit_factor) in html | |
| assert f">{m.trade_count}<" in html | |
| def test_stat_band_shows_is_and_oos_for_every_stat(rec): | |
| html = C.stat_band(rec) | |
| assert html.count("IS ") >= 9 | |
| assert html.count("· OOS") >= 9 | |
| assert num(rec.result.metrics_oos.sharpe) in html | |
| assert num(rec.result.metrics_is.sharpe) in html | |
| def test_stat_band_reports_costs_actually_paid(rec): | |
| html = C.stat_band(rec) | |
| assert money(rec.result.costs_paid) in html | |
| assert rec.result.costs_paid > 0, "costs default to ON, so this must be positive" | |
| def test_empty_segment_renders_an_em_dash_not_a_zero(): | |
| """A segment with no bars must not read as 0.00.""" | |
| short = runtime.execute(RunRequest( | |
| strategy="SMA Crossover", asset="BTC-USD", timeframe="1h", date_range="1Y")) | |
| if short.result.metrics_oos.bars: | |
| pytest.skip("this range did produce OOS windows") | |
| html = C.stat_band(short) | |
| assert "· OOS —" in html | |
| assert "no out-of-sample windows" in html | |
| def test_trade_table_rows_match_the_engine_trade_list(rec): | |
| df = bitapp.trades_frame(rec) | |
| assert len(df) == len(rec.result.trades) | |
| if len(df): | |
| assert df["Net"].iloc[0] == pytest.approx( | |
| round(float(rec.result.trades["net_pnl"].iloc[0]), 2)) | |
| assert set(df["Segment"]) <= {"IS", "OOS", "holdout"} | |
| assert (df["Costs"] >= 0).all() | |
| def test_report_quotes_the_same_numbers_as_the_stat_band(rec): | |
| md = bitapp.report_markdown(rec) | |
| assert pct(rec.result.metrics_all.total_return) in md | |
| assert money(rec.result.costs_paid) in md | |
| assert rec.run_id in md | |
| def test_costs_off_is_called_out_as_not_real(): | |
| off = runtime.execute(RunRequest( | |
| strategy="SMA Crossover", asset="BTC-USD", timeframe="1d", | |
| date_range="3Y", costs_on=False)) | |
| _, _, _, _, _, _, _, _, note = bitapp.build_overview(off, log_scale=False, cvd=False) | |
| assert "COSTS ARE OFF" in note | |
| assert off.result.costs_paid == 0.0 | |
| # -------------------------------------------------------------------------- | |
| # Charts | |
| # -------------------------------------------------------------------------- | |
| def test_overview_builds_every_figure(rec): | |
| figs = bitapp.build_overview(rec, log_scale=False, cvd=False) | |
| assert len(figs) == 9 | |
| for f in figs[:8]: | |
| assert isinstance(f, go.Figure) | |
| assert isinstance(figs[8], str) | |
| def test_log_scale_and_colorblind_variants_render(rec): | |
| for log_s in (False, True): | |
| for cb in (False, True): | |
| figs = bitapp.build_overview(rec, log_scale=log_s, cvd=cb) | |
| assert isinstance(figs[0], go.Figure) | |
| def test_equity_chart_marks_the_holdout_band(rec): | |
| fig = charts.equity_curve(rec.result.equity, rec.result.benchmark_equity, | |
| plan=rec.result.plan) | |
| if rec.result.plan.holdout_start is not None: | |
| texts = [str(a.text) for a in fig.layout.annotations] | |
| assert any("HOLDOUT" in t for t in texts) | |
| def test_charts_survive_empty_inputs(): | |
| empty = pd.Series(dtype="float64") | |
| assert isinstance(charts.equity_curve(empty), go.Figure) | |
| assert isinstance(charts.underwater_chart(empty), go.Figure) | |
| assert isinstance(charts.pnl_histogram(pd.DataFrame()), go.Figure) | |
| assert isinstance(charts.mae_mfe_scatter(pd.DataFrame()), go.Figure) | |
| assert isinstance(charts.walk_forward_bars([]), go.Figure) | |
| assert isinstance(charts.monte_carlo_cone(None), go.Figure) | |
| def test_colorblind_palette_differs_from_default(): | |
| assert charts.up_color(cvd=True) != charts.up_color(cvd=False) | |
| assert charts.down_color(cvd=True) != charts.down_color(cvd=False) | |
| # -------------------------------------------------------------------------- | |
| # Comparison tab with three runs | |
| # -------------------------------------------------------------------------- | |
| def test_comparison_renders_three_runs(three_runs): | |
| curves = {r.label[:28]: r.result.equity for r in three_runs} | |
| rets = {r.label[:28]: r.result.equity.pct_change().dropna() for r in three_runs} | |
| assert len(curves) == 3 | |
| assert isinstance(charts.overlaid_returns(curves), go.Figure) | |
| assert isinstance(charts.small_multiples(curves), go.Figure) | |
| corr = charts.correlation_matrix(rets) | |
| assert isinstance(corr, go.Figure) | |
| assert len(corr.data[0].z) == 3 | |
| def test_return_overlay_caps_the_series_it_will_draw(): | |
| """The old six-run session picker was replaced by the catalog view; the | |
| readability cap now lives in the chart itself.""" | |
| import numpy as np | |
| idx = pd.date_range("2024-01-01", periods=30, freq="D", tz="UTC") | |
| curves = {f"s{i}": pd.Series(np.linspace(0, 1, 30), index=idx) for i in range(50)} | |
| assert len(charts.multi_return_overlay(curves, max_series=6).data) == 6 | |
| def test_precomputed_heatmap_loads_from_the_store(): | |
| heat = comparisons.load_table(runtime.get_store(), comparisons.HEATMAP) | |
| if heat.empty: | |
| pytest.skip("comparison tables not generated yet") | |
| assert {"asset", "strategy", "timeframe", "oos_sharpe"} <= set(heat.columns) | |
| assert isinstance(charts.strategy_timeframe_heatmap(heat), go.Figure) | |
| def test_regime_breakdown_covers_the_named_regimes(rec): | |
| df = runtime.regime_breakdown(rec) | |
| if df.empty: | |
| pytest.skip("no regime variation in this window") | |
| assert set(df["regime"]) <= {"BULL", "BEAR", "CHOP"} | |
| # -------------------------------------------------------------------------- | |
| # Coverage map & share links | |
| # -------------------------------------------------------------------------- | |
| def test_coverage_map_renders_from_the_live_manifest(): | |
| df = runtime.coverage_frame() | |
| assert not df.empty | |
| assert {"Model", "Asset", "TF", "Coverage", "Rows", "Real?"} <= set(df.columns) | |
| def test_placeholder_slices_are_labelled_in_the_coverage_map(): | |
| df = runtime.coverage_frame() | |
| assert set(df["Real?"]) <= {"real", "PLACEHOLDER"} | |
| def test_share_link_round_trips(): | |
| req = RunRequest(strategy="RSI Mean Reversion", asset="ETH-USD", | |
| timeframe="1h", date_range="1Y", params={"rsi_period": 21}) | |
| again = RunRequest.decode(req.encode()) | |
| assert again.strategy == req.strategy | |
| assert again.asset == req.asset | |
| assert again.params["rsi_period"] == 21 | |
| def test_hostile_share_links_are_rejected(payload): | |
| import base64 | |
| token = base64.urlsafe_b64encode(payload.encode()).decode().rstrip("=") | |
| with pytest.raises(ValueError): | |
| RunRequest.decode(token) | |
| def test_unavailable_presets_are_listed_but_refuse_to_run(): | |
| assert "Custom (code)" in strategies.PRESETS | |
| assert not strategies.PRESETS["Custom (code)"].available | |
| with pytest.raises(runtime.RunError, match="never executes untrusted code"): | |
| runtime.execute(RunRequest(strategy="Custom (code)", asset="BTC-USD")) | |
| # -------------------------------------------------------------------------- | |
| # Performance budget | |
| # -------------------------------------------------------------------------- | |
| def test_default_three_year_daily_run_is_under_two_seconds(): | |
| runtime.cache_clear() | |
| req = RunRequest(strategy="SMA Crossover", asset="BTC-USD", | |
| timeframe="1d", date_range="3Y") | |
| runtime.execute(req) # warm the slice cache | |
| import time | |
| t0 = time.perf_counter() | |
| runtime.execute(req) | |
| assert time.perf_counter() - t0 < 2.0 | |
| # -------------------------------------------------------------------------- | |
| # Design-system fidelity | |
| # | |
| # The original bug this guards: base.css was vendored into assets/ but never | |
| # loaded, so the app silently inherited Gradio's rounded, roomy defaults and | |
| # only approximated the design. | |
| # -------------------------------------------------------------------------- | |
| def test_every_global_stylesheet_the_design_system_declares_is_loaded(): | |
| """The DS manifest lists six globalCssPaths; all of them must reach the page.""" | |
| from src.ui import theme | |
| css = theme.full_css() | |
| # colors / typography / spacing / base, each identified by a rule only it has | |
| markers = { | |
| "colors.css": "--accent-amber:#af9209", | |
| "typography.css": "--text-2xs:8px", | |
| "spacing.css": "--space-4:16px", | |
| "base.css": "border-radius:0 !important", | |
| } | |
| for name, marker in markers.items(): | |
| assert marker in css, f"{name} is not loaded into the page" | |
| def test_base_css_square_corner_reset_is_present(): | |
| from src.ui import theme | |
| assert "border-radius:0 !important" in theme.full_css() | |
| # The app's own shell must not reintroduce a rounded default on top of the | |
| # reset. base.css legitimately defines opt-in `.bit-rounded*` utilities, so | |
| # only our shell layer is checked here. | |
| assert "border-radius" not in theme.SHELL_CSS | |
| assert "border-radius" not in theme.GRADIO_RESET | |
| def test_base_css_scrollbar_and_selection_rules_survive(): | |
| from src.ui import theme | |
| css = theme.full_css() | |
| assert "::-webkit-scrollbar" in css | |
| assert "::selection" in css | |
| def test_data_surfaces_stay_selectable_despite_base_css(): | |
| """base.css sets user-select:none globally; tables must opt back in.""" | |
| from src.ui import theme | |
| css = theme.full_css() | |
| assert "user-select: text" in css | |
| assert ".bit-table" in css.split("user-select: text")[0][-400:] or \ | |
| "table, table *" in css | |
| def test_gradio_layout_spacing_is_configured_not_left_default(): | |
| """Gradio defaults to layout_gap *spacing_xxl and block_padding *spacing_xl.""" | |
| from src.ui import theme | |
| t = theme.bit_theme() | |
| assert t.layout_gap == "8px" | |
| assert t.block_padding == "0px" | |
| assert t.form_gap_width == "0px" | |
| def test_gradio_radius_is_squared_off(): | |
| from src.ui import theme | |
| t = theme.bit_theme() | |
| for attr in ("block_radius", "input_radius", "button_large_radius", | |
| "button_small_radius", "container_radius"): | |
| assert getattr(t, attr) == "0px", f"{attr} is not square" | |
| def test_type_scale_matches_the_designs_dominant_sizes(): | |
| """The design's workhorse is --text-xs (10px) with --text-2xs (8px) micro.""" | |
| from src.ui import theme | |
| t = theme.bit_theme() | |
| assert t.body_text_size == "10px" | |
| assert t.block_label_text_size == "8px" | |
| def test_no_hardcoded_pixel_font_sizes_outside_the_theme(): | |
| """Component markup must size through tokens, never literal px.""" | |
| import re | |
| from pathlib import Path | |
| root = Path(__file__).resolve().parent.parent | |
| for rel in ("src/ui/components.py", "src/ui/compare_tab.py", "app.py"): | |
| text = (root / rel).read_text() | |
| assert not re.search(r"font-size:\s*\d", text), \ | |
| f"{rel} hardcodes a pixel font-size instead of using a token" | |
| def test_every_bit_class_used_in_markup_has_a_css_rule(): | |
| """A class with no rule renders unstyled and silently breaks the design.""" | |
| import re | |
| from pathlib import Path | |
| from src.ui import theme | |
| root = Path(__file__).resolve().parent.parent | |
| used = set() | |
| for rel in ("src/ui/components.py", "src/ui/compare_tab.py", "app.py"): | |
| text = (root / rel).read_text() | |
| # Only real class attributes -- a bare `bit-…` token can appear in a | |
| # comment or an asset filename and is not a class the page uses. | |
| for attr in (re.findall(r'class="([^"]*)"', text) | |
| + re.findall(r"elem_classes=[\"']([^\"']+)", text)): | |
| for cls in attr.split(): | |
| if not cls.startswith("bit-"): | |
| continue | |
| # Class names are built in f-strings, e.g. `bit-chip{cls}` or | |
| # `bit-podium-{rank}`. Keep the static prefix; a name that is | |
| # only a prefix (ends in `-`) has variant rules instead. | |
| static = cls.split("{", 1)[0] | |
| if static and not static.endswith("-"): | |
| used.add(static) | |
| defined = set(re.findall(r"\.(bit-[a-z0-9-]+)", theme.full_css())) | |
| missing = sorted(c for c in used if c not in defined and not c.endswith("-")) | |
| assert not missing, f"classes used in markup but never styled: {missing}" | |
| def test_fonts_are_served_for_every_weight_the_design_uses(): | |
| from src.ui import theme | |
| css = theme.full_css() | |
| assert css.count("@font-face") >= 5 | |
| assert "Styrene A" in css and "Mac Minecraft" in css | |
| def test_app_constructs_without_any_hugging_face_credentials(monkeypatch): | |
| """Reading and backtesting are open to anyone, so the app must build with | |
| no token at all. Gradio's off-Space OAuth mock calls whoami and raises | |
| without one, which previously made the whole app unconstructable.""" | |
| import importlib | |
| for var in ("HF_TOKEN", "HF_WRITE_TOKEN", "HUGGING_FACE_HUB_TOKEN", "SPACE_ID"): | |
| monkeypatch.delenv(var, raising=False) | |
| import app as fresh | |
| importlib.reload(fresh) | |
| assert fresh.demo is not None | |
| # -------------------------------------------------------------------------- | |
| # Design-markup rendering (shell) and the click bridge | |
| # -------------------------------------------------------------------------- | |
| def test_left_panel_is_design_markup_not_gradio_components(): | |
| """Every control in the left panel must be a real button or input.""" | |
| from src.ui import shell | |
| st = bitapp.default_state() | |
| html = bitapp.render_left(st) | |
| assert 'width:286px' in html, "zone width is not the design's 286px" | |
| assert html.count("<button") >= 12, "controls are not real buttons" | |
| assert "data-bit=" in html, "buttons are not wired to the bridge" | |
| # Gradio's own control DOM must not appear here. | |
| assert "<fieldset" not in html | |
| assert 'type="radio"' not in html | |
| def test_left_panel_uses_the_designs_exact_spacing(): | |
| html = bitapp.render_left(bitapp.default_state()) | |
| for value in ("padding:9px 12px", # panel header | |
| "padding:0 12px 12px", # section body | |
| "gap:10px", # section body gap | |
| "padding:3px 7px", # chips | |
| "padding:8px 12px"): # accordion header | |
| assert value in html, f"design spacing {value!r} missing" | |
| def test_top_bar_uses_the_real_mark_and_design_header_treatment(): | |
| from src.ui import shell | |
| html = shell.top_bar(context="BTC-USD", status="RUN COMPLETE", tone="ok") | |
| assert "M50 10H90V90H50Z" in html, "not the design's mark" | |
| assert "padding:8px 12px" in html | |
| assert "margin-top:3px" in html, "optical alignment on the title is missing" | |
| assert "mono-data" in html, "design-system utility class not used" | |
| def test_selected_chip_is_the_only_active_one(): | |
| st = bitapp.default_state() | |
| st["timeframe"] = "1h" | |
| html = bitapp.render_left(st) | |
| actives = re.findall( | |
| r'<button data-bit="(tf:[^"]+)"[^>]*background:var\(--accent-amber\)', html) | |
| assert actives == ["tf:1h"] | |
| def test_html_entities_are_escaped_exactly_once(): | |
| html = bitapp.render_left(bitapp.default_state()) | |
| assert "&amp;" not in html, "double-escaped entity" | |
| assert "Universe & Data" in html | |
| def test_actions_fold_into_state(action, key, expected): | |
| st, ran = bitapp.apply_action(bitapp.default_state(), f"{action}|nonce") | |
| assert st[key] == expected | |
| assert ran is False | |
| def test_run_and_example_actions_request_a_run(): | |
| _, ran = bitapp.apply_action(bitapp.default_state(), "run:|n") | |
| assert ran is True | |
| st, ran = bitapp.apply_action(bitapp.default_state(), "example:|n") | |
| assert ran is True and st["strategy"] == "Chronos Forecast Follower" | |
| def test_accordion_action_toggles(): | |
| st = bitapp.default_state() | |
| before = st["acc"]["costs"] | |
| st, _ = bitapp.apply_action(st, "acc:costs|n") | |
| assert st["acc"]["costs"] is not before | |
| def test_numeric_param_commits_through_the_bridge(): | |
| st, _ = bitapp.apply_action(bitapp.default_state(), "param:commission_bps=25|n") | |
| assert st["commission_bps"] == 25.0 | |
| def test_strategy_param_commits_and_survives_preset_defaults(): | |
| st = bitapp.default_state() | |
| st, _ = bitapp.apply_action(st, "param:fast_ma=33|n") | |
| assert st["params"]["fast_ma"] == 33.0 | |
| def test_malformed_actions_leave_state_untouched(bad): | |
| before = bitapp.default_state() | |
| after, ran = bitapp.apply_action(bitapp.default_state(), f"{bad}|n") | |
| assert ran is False | |
| assert after["timeframe"] == before["timeframe"] | |
| assert after["asset"] == before["asset"] | |
| assert after["strategy"] == before["strategy"] | |
| assert after["commission_bps"] == before["commission_bps"] | |
| def test_unknown_action_keys_are_dropped(): | |
| from src.ui.bridge import parse_action | |
| assert parse_action("evil:rm -rf|n") is None | |
| assert parse_action("os.system:x|n") is None | |
| def test_bridge_survives_values_containing_spaces(): | |
| """Preset names contain spaces; the nonce separator must not collide.""" | |
| from src.ui.bridge import emit, parse_action | |
| raw = emit("strategy", "Buy & Hold (benchmark)") | |
| assert parse_action(raw + "|nonce").value == "Buy & Hold (benchmark)" | |
| def test_tables_render_as_markup_not_dataframes(): | |
| df = pd.DataFrame({"A": [1, 2], "B": ["x", "y"]}) | |
| html = bitapp.render_table(df) | |
| assert "<table" in html and "pixel-text" in html | |
| assert "bit-selectable" in html, "table text must stay selectable" | |
| def test_empty_table_says_so(): | |
| assert "no rows" in bitapp.render_table(pd.DataFrame(), empty="no rows") | |
| def test_zone_widths_are_pinned_in_css(): | |
| from src.ui import theme | |
| css = theme.full_css() | |
| assert "flex:0 0 286px" in css, "left aside is not pinned to the design width" | |
| # The right tray was removed; its rules should not linger. | |
| assert "bit-zone-right" not in css | |
| def test_bridge_target_is_hidden_but_present(): | |
| from src.ui import theme | |
| css = theme.full_css() | |
| assert "#bit-action" in css | |
| assert "opacity:0" in css.split("#bit-action")[1][:220] | |
| def test_gradio_content_wrapper_is_stretched_full_bleed(): | |
| """Gradio centres the app in a padded `.contain`, which cost 208px of a | |
| 1680px viewport. The design is a full-bleed dashboard.""" | |
| from src.ui import theme | |
| import re | |
| css = theme.full_css() | |
| # Match the rule whose selector list starts with `.contain`, not the | |
| # `.container` rule -- one is a prefix of the other. | |
| m = re.search(r"\.gradio-container \.contain,(.*?)\{(.*?)\}", css, re.S) | |
| assert m, "no full-bleed rule for Gradio's .contain wrapper" | |
| body = m.group(2) | |
| assert "max-width:100% !important" in body | |
| assert "padding:0 !important" in body | |
| # -------------------------------------------------------------------------- | |
| # Regressions from the header/nav restructure | |
| # -------------------------------------------------------------------------- | |
| def test_header_carries_nav_login_and_glossary(): | |
| from src.ui import shell | |
| html = shell.top_bar(tab="backtest", glossary=bitapp.GLOSSARY, on_space=True) | |
| assert 'data-bit="tab:compare"' in html | |
| assert 'data-bit="tab:backtest"' in html | |
| assert "bit-help" in html, "glossary tooltip missing from the header" | |
| def test_signed_in_header_shows_the_handle(): | |
| from src.ui import shell | |
| assert "@alice" in shell.top_bar(user="alice", on_space=True) | |
| def test_sign_in_uses_a_real_login_button_not_a_handrolled_link(): | |
| """Gradio only mounts /login/huggingface when it sees a LoginButton in the | |
| app. A hand-rolled anchor points at a route that returns 404.""" | |
| import inspect | |
| from src.ui import shell | |
| src = inspect.getsource(bitapp.build_app) | |
| assert "gr.LoginButton" in src, "no LoginButton, so OAuth routes never mount" | |
| # and the markup must not fake one | |
| assert "/login/huggingface" not in shell.top_bar(on_space=True) | |
| def test_off_space_header_explains_sign_in_is_unavailable(): | |
| from src.ui import shell | |
| assert "SIGN IN" in shell.top_bar(on_space=False) | |
| def test_context_chip_only_appears_on_the_backtest_tab(): | |
| """On Compare, a run context chip would describe something not on screen.""" | |
| st = bitapp.default_state() | |
| st["tab"] = "compare" | |
| class FakeRec: | |
| run_id = "abcd1234" | |
| elapsed_s = 1.0 | |
| # The chip is the only place the asset and timeframe appear together. | |
| # "WALK-FORWARD" alone is ambiguous -- it is also a glossary term. | |
| compare_header = bitapp.render_top(st, FakeRec()) | |
| assert "BTC-USD · 1D" not in compare_header | |
| st["tab"] = "backtest" | |
| assert "BTC-USD · 1D" in bitapp.render_top(st, FakeRec()) | |
| def test_tab_action_switches_views(): | |
| st, _ = bitapp.apply_action(bitapp.default_state(), "tab:backtest|n1") | |
| assert st["tab"] == "backtest" | |
| st, _ = bitapp.apply_action(st, "tab:compare|n2") | |
| assert st["tab"] == "compare" | |
| def test_unknown_tab_value_is_ignored(): | |
| st, _ = bitapp.apply_action(bitapp.default_state(), "tab:../admin|n") | |
| assert st["tab"] == "compare" | |
| def test_the_same_click_cannot_run_twice(): | |
| """Two event bindings deliver one click; the nonce must de-duplicate it.""" | |
| st = bitapp.default_state() | |
| st, first = bitapp.apply_action(st, "run:|nonce-a") | |
| _, replay = bitapp.apply_action(st, "run:|nonce-a") | |
| assert first is True and replay is False | |
| def test_a_fresh_click_still_runs_after_a_deduped_one(): | |
| st = bitapp.default_state() | |
| st, _ = bitapp.apply_action(st, "run:|nonce-a") | |
| _, again = bitapp.apply_action(st, "run:|nonce-b") | |
| assert again is True | |
| def test_running_switches_to_the_backtest_tab(): | |
| st = bitapp.default_state() | |
| assert st["tab"] == "compare" | |
| st, should_run = bitapp.apply_action(st, "example:|n") | |
| assert should_run is True | |
| def test_right_sidebar_is_gone(): | |
| import inspect | |
| src = inspect.getsource(bitapp.build_app) | |
| assert "bit-zone-right" not in src | |
| assert "right_panel" not in src | |
| def test_bridge_elements_are_rendered_not_visible_false(): | |
| """`visible=False` removes an element from the DOM, which left the click | |
| bridge with nothing to write into.""" | |
| import inspect | |
| src = inspect.getsource(bitapp.build_app) | |
| box = src[src.index("action_box = gr.Textbox"):] | |
| assert "visible=True" in box[:260] | |
| trig = src[src.index("action_trigger = gr.Button"):] | |
| assert "visible=True" in trig[:200] | |
| def test_bridge_js_is_loaded_without_outputs(): | |
| """Gradio treats a `js=` return value as the output values, so the bridge | |
| installer must not share a load call that has outputs.""" | |
| import inspect | |
| src = inspect.getsource(bitapp.build_app) | |
| assert "demo.load(fn=None, inputs=None, outputs=None, js=BRIDGE_LOAD_JS)" in src | |
| def test_no_oauth_annotated_load_handler(): | |
| """A `demo.load` handler that takes `gr.OAuthProfile` asks for an | |
| authenticated session on every page render. An unauthenticated visitor is | |
| sent to sign in, returns, fires load again, and is sent back -- an infinite | |
| redirect. The profile belongs on user-initiated handlers only.""" | |
| import inspect | |
| import re | |
| src = inspect.getsource(bitapp.build_app) | |
| # Strip comments so the explanation of this rule does not trip it. | |
| code = "\n".join(ln for ln in src.splitlines() | |
| if not ln.lstrip().startswith("#")) | |
| assert "OAuthProfile" not in code, \ | |
| "an OAuth-annotated handler is registered in build_app" | |
| for call in re.findall(r"demo\.load\((.*?)\)\n", code, re.S): | |
| assert "profile" not in call, f"load handler takes a profile: {call[:80]}" | |
| def test_profile_is_read_only_on_user_initiated_handlers(): | |
| """Where the profile *is* needed, it must hang off a click, not a load.""" | |
| import inspect | |
| from src import extension | |
| for fn in (extension.extend_ui, extension.add_model_ui): | |
| params = inspect.signature(fn).parameters | |
| assert "profile" in params, f"{fn.__name__} should receive the profile" | |
| def test_login_button_is_the_only_sign_in_mechanism(): | |
| import inspect | |
| from src.ui import shell | |
| src = inspect.getsource(bitapp.build_app) | |
| assert "gr.LoginButton" in src | |
| # The header must not also hand-roll a link to the OAuth route. | |
| assert "/login/huggingface" not in shell.top_bar(on_space=True) | |