| from arena.codebase_archive import LocalCodebaseArchiveStore |
| from arena.gradio_app import GradioArena, build_app |
| from arena.model_catalog import DEFAULT_LOCAL_MODEL, ProviderModel |
| from arena.run_store import InMemoryRunStore |
| from arena.service import ArenaService |
| from arena.swarm_runtime import SwarmRuntime |
| from arena.validator_graph import DeterministicValidatorExecutor, ValidatorGraph |
|
|
| from conftest import make_local_session |
|
|
|
|
| def make_service(): |
| store = LocalCodebaseArchiveStore() |
| return ArenaService( |
| session_factory=make_local_session, |
| run_store=InMemoryRunStore(), |
| swarm_runtime=SwarmRuntime( |
| session_factory=make_local_session, |
| archive_store=store, |
| ), |
| validator_graph=ValidatorGraph( |
| executor=DeterministicValidatorExecutor(), |
| rubric_enabled=False, |
| ), |
| archive_store=store, |
| ) |
|
|
|
|
| def create_local_run(arena: GradioArena, prompt: str, criteria: str = "", tests: str = ""): |
| return arena.create_run(prompt, criteria, tests, "local", DEFAULT_LOCAL_MODEL, "", "") |
|
|
|
|
| def test_gradio_local_provider_runs_inside_create_callback() -> None: |
| service = make_service() |
| calls: list[str] = [] |
| original_create_run = service.create_run |
| original_start_run = service.start_run |
|
|
| def create_run(request): |
| calls.append("create") |
| return original_create_run(request) |
|
|
| def start_run(request): |
| calls.append("start") |
| return original_start_run(request) |
|
|
| service.create_run = create_run |
| service.start_run = start_run |
| arena = GradioArena(service) |
|
|
| create_local_run(arena, "Build a CLI", tests="echo ok") |
|
|
| assert calls == ["create"] |
|
|
|
|
| def test_gradio_create_run_returns_state_and_summary() -> None: |
| arena = GradioArena(make_service()) |
|
|
| state, status_html, run_json, tasks, events, token, download, _, chat = create_local_run( |
| arena, |
| "Build a small FastAPI app", |
| "pytest must pass\ninclude README", |
| "echo ok", |
| ) |
|
|
| assert state.role == "run" |
| assert state.run_id == run_json["id"] |
| assert isinstance(status_html, str) |
| assert run_json["prompt"] == "Build a small FastAPI app" |
| assert [item["text"] for item in run_json["criteria"]] == ["pytest must pass", "include README"] |
| assert run_json["user_tests"] == ["echo ok"] |
| assert run_json["status"] in ("working", "validating", "completed") |
| assert isinstance(run_json.get("codebase_zip_url", ""), str) |
| assert token == "" |
| assert state.token == "" |
| assert "Build a small FastAPI app" in chat |
|
|
|
|
| def test_gradio_create_run_accepts_empty_model_dropdown_value() -> None: |
| arena = GradioArena(make_service()) |
|
|
| state, _, run_json, *_ = create_local_run( |
| arena, |
| "Build a small Gradio demo", |
| "Works as a Gradio app", |
| "python -m py_compile app.py", |
| ) |
|
|
| assert state.role == "run" |
| assert run_json["prompt"] == "Build a small Gradio demo" |
|
|
|
|
| def test_gradio_hosted_provider_requires_user_api_key() -> None: |
| arena = GradioArena(make_service()) |
|
|
| state, status_html, run_json, *rest = arena.create_run( |
| "Build a small Gradio demo", |
| "Works as a Gradio app", |
| "python -m py_compile app.py", |
| "openrouter", |
| "qwen/qwen3.5-9b", |
| "", |
| "", |
| ) |
|
|
| assert state.role == "" |
| assert "enter your own API key" in status_html |
| assert "enter your own API key" in rest[4] |
| assert run_json == {} |
|
|
|
|
| def test_gradio_refresh_models_accepts_empty_optional_inputs() -> None: |
| class FakeService: |
| def refresh_provider_models(self, config): |
| assert config.provider == "openrouter" |
| assert config.key_value() == "" |
| assert config.base_url == "" |
| return [ |
| ProviderModel( |
| id="nvidia/nemotron-3.5-content-safety:free", |
| label="NVIDIA: Nemotron 3.5 Content Safety", |
| provider="openrouter", |
| parameter_count_b=4.0, |
| eligible=True, |
| ), |
| ProviderModel( |
| id="too-large/model-70b", |
| label="Too Large 70B", |
| provider="openrouter", |
| parameter_count_b=70.0, |
| eligible=False, |
| status="too_large", |
| ), |
| ] |
|
|
| arena = GradioArena(FakeService()) |
|
|
| dropdown, models = arena.refresh_models("openrouter", None, None) |
|
|
| assert dropdown.choices == [ |
| ("nvidia/nemotron-3.5-content-safety:free", "nvidia/nemotron-3.5-content-safety:free") |
| ] |
| assert dropdown.value == "nvidia/nemotron-3.5-content-safety:free" |
| assert [model["id"] for model in models] == ["nvidia/nemotron-3.5-content-safety:free"] |
| assert models[0]["parameter_count_b"] == 4.0 |
|
|
|
|
| def test_gradio_send_message_records_followup_in_chat() -> None: |
| arena = GradioArena(make_service()) |
| state, *_ = create_local_run(arena, "Build a CLI") |
|
|
| _, _, run_json, _, events, _, _, _, chat = arena.send_message(state, "Can you keep working on this?") |
|
|
| assert run_json["events"][-1]["message"] == "User: Can you keep working on this?" |
| assert events[-1][1] == "User: Can you keep working on this?" |
| assert "Can you keep working on this?" in chat |
| assert 'class="chat-bubble is-user"' in chat |
|
|
|
|
| def test_gradio_feedback_records_mom_test_event() -> None: |
| arena = GradioArena(make_service()) |
| state, *_ = create_local_run(arena, "Build a CLI") |
|
|
| _, _, run_json, _, events, _, _, _, chat = arena.record_feedback( |
| state, |
| "Mom", |
| "I would use this with buyers.", |
| "scale", |
| "Needs Vietnamese copy.", |
| ) |
|
|
| assert "Mom Test feedback" in run_json["events"][-1]["message"] |
| assert "Decision: scale" in run_json["events"][-1]["message"] |
| assert "I would use this with buyers." in events[-1][1] |
| assert "Needs Vietnamese copy." in chat |
|
|
|
|
| def test_gradio_refresh_shows_current_status() -> None: |
| arena = GradioArena(make_service()) |
| state, *_ = create_local_run(arena, "Build a CLI", tests="echo ok") |
|
|
| _, status_html, run_json, *_ = arena.refresh(state) |
|
|
| assert run_json["id"] == state.run_id |
| assert run_json["status"] in ("working", "validating", "completed") |
|
|
|
|
| def test_gradio_refresh_shows_completed_tasks_and_events() -> None: |
| """Bug E: After a run completes, the tasks and events dataframes must show the run's data.""" |
| import time |
|
|
| service = make_service() |
| arena = GradioArena(service) |
| state, *_ = create_local_run(arena, "Build a CLI", tests="test -f README.md") |
|
|
| for _ in range(60): |
| run = service.get_run(state.run_id) |
| if run.status == "completed": |
| break |
| time.sleep(0.5) |
|
|
| _, _, _, tasks, events, *_ = arena.refresh(state) |
|
|
| assert len(tasks) >= 1, f"Expected at least 1 task, got {tasks}" |
| assert len(events) >= 1, f"Expected at least 1 event, got {events}" |
|
|
|
|
| def test_gradio_status_badge_renders_html() -> None: |
| arena = GradioArena(make_service()) |
| state, status_html, *_ = create_local_run(arena, "Build a CLI", tests="echo ok") |
|
|
| assert "<span" in status_html |
| assert _extract_status_from_html(status_html) in ("working", "validating", "completed") |
|
|
|
|
| def test_gradio_app_constructs_without_crashing() -> None: |
| app = build_app(make_service()) |
|
|
| assert type(app).__name__ == "Blocks" |
| assert app.title == "Backyard Demo Builder" |
|
|
|
|
| def test_gradio_defaults_target_backyard_demo_builder() -> None: |
| app = build_app(make_service()) |
| values = "\n".join( |
| str(getattr(block, "value", "") or "") |
| for block in app.blocks.values() |
| if hasattr(block, "value") |
| ) |
|
|
| assert "real-estate agent" in values |
| assert "handoff_spec.md" in values |
| assert "field_notes.md" in values |
| assert "Backyard Demo Builder" in values |
| assert "Record feedback" in values or any( |
| getattr(block, "value", None) == "Record feedback" |
| for block in app.blocks.values() |
| ) |
|
|
|
|
| def test_gradio_two_consecutive_runs_both_complete() -> None: |
| """Bug D: A second run on the same service must complete, not hang.""" |
| import time |
|
|
| service = make_service() |
| arena = GradioArena(service) |
|
|
| state1, *_ = create_local_run(arena, "Build a CLI", tests="test -f README.md") |
| for _ in range(60): |
| if service.get_run(state1.run_id).status == "completed": |
| break |
| time.sleep(0.5) |
| assert service.get_run(state1.run_id).status == "completed", "First run did not complete" |
|
|
| state2, *_ = create_local_run(arena, "Build another CLI", tests="test -f README.md") |
| for _ in range(60): |
| if service.get_run(state2.run_id).status == "completed": |
| break |
| time.sleep(0.5) |
| assert service.get_run(state2.run_id).status == "completed", "Second run did not complete" |
|
|
|
|
| def test_gradio_outputs_tuple_has_nine_distinct_outputs() -> None: |
| """Bug F: The 9-tuple outputs list must bind to 9 distinct Gradio components. |
| |
| Before the fix, position 1 (status bar) and position 7 (status panel with summary) |
| were both bound to the same gr.HTML element. The status bar was dead code because |
| the full status panel overwrote it. |
| """ |
| app = build_app(make_service()) |
|
|
| for fn in app.fns.values(): |
| if not fn.outputs or len(fn.outputs) != 9: |
| continue |
| ids = [o.elem_id if hasattr(o, "elem_id") and o.elem_id else id(o) for o in fn.outputs] |
| assert len(ids) == len(set(ids)), ( |
| f"Duplicate output binding detected in {fn.name}: {ids}. " |
| "Each output slot must bind to a distinct Gradio component." |
| ) |
|
|
|
|
| def test_gradio_does_not_poll_refresh_while_idle() -> None: |
| """Idle timer refreshes compete with manual model refresh in Gradio's queue.""" |
| app = build_app(make_service()) |
|
|
| assert all(fn.name != "refresh_1" for fn in app.fns.values()) |
|
|
|
|
| def test_css_has_mobile_breakpoint_with_hero_scaling() -> None: |
| """Bug G/H: The mobile media query must scale the hero H1 and rebalance the top bar.""" |
| from arena.gradio_app import CSS |
|
|
| assert "@media" in CSS, "CSS must include responsive media queries for mobile" |
| assert "max-width: 880px" in CSS, "CSS must include the 880px mobile breakpoint" |
|
|
| start = CSS.index("@media (max-width: 880px)") |
| rest = CSS[start + 1:] |
| next_media = rest.find("@media") |
| mobile_block = rest[:next_media] if next_media != -1 else rest |
|
|
| assert "hero-title" in mobile_block, ( |
| "Mobile media query must include hero-title rules to prevent H1 wrap on small viewports" |
| ) |
| assert "wordmark" in mobile_block, ( |
| "Mobile media query must rebalance the top bar wordmark so it fits on one line" |
| ) |
|
|
|
|
| def test_gradio_workbench_children_live_inside_workbench_wrapper() -> None: |
| """Bug I: The workbench Row must render inside the .workbench wrapper, not as a sibling. |
| |
| Gradio 6 renders gr.Row as a sibling of raw HTML elements, not nested. |
| If we wrap a gr.Row in custom <div class="workbench">…</div>, the wrapper |
| collapses to 1px and the form/dispatch land outside it. That 1px wrapper |
| shows up as a stray horizontal line in the middle of the page. |
| """ |
| app = build_app(make_service()) |
|
|
| workbench = None |
| for block in app.blocks.values(): |
| if getattr(block, "elem_id", None) is None and "workbench" in (getattr(block, "elem_classes", []) or []): |
| workbench = block |
| break |
| if getattr(block, "elem_id", None) == "run-shell": |
| pass |
|
|
| app_html = app.fns |
| assert app_html, "App must have registered event handlers" |
|
|
| blocks_list = list(app.blocks.values()) |
| workbench_blocks = [b for b in blocks_list if "workbench" in (getattr(b, "elem_classes", []) or [])] |
| assert workbench_blocks, "App must have at least one workbench-classed block" |
|
|
| workbench_block = workbench_blocks[0] |
| assert workbench_block is not None |
| assert workbench_block.elem_classes and "workbench" in workbench_block.elem_classes |
| assert workbench_block.__class__.__name__ in ("Row", "Group", "Column"), ( |
| f"Workbench wrapper must be a Gradio Row/Group/Column so the form and dispatch " |
| f"land inside it, not a raw HTML block. Got type={workbench_block.__class__.__name__!r}." |
| ) |
|
|
|
|
| def test_gradio_run_state_rail_uses_dynamic_status_panel() -> None: |
| """The rail must not keep stale static 'No active run' text after a run starts.""" |
| app = build_app(make_service()) |
|
|
| html = "\n".join( |
| getattr(block, "value", "") or "" |
| for block in app.blocks.values() |
| if block.__class__.__name__ == "HTML" |
| ) |
| status_panels = [ |
| block for block in app.blocks.values() |
| if getattr(block, "elem_id", None) == "status-panel" |
| ] |
|
|
| assert "No active run. Start one to populate telemetry." not in html |
| assert len(status_panels) == 1 |
|
|
|
|
| def test_css_fills_full_viewport_no_centering_black_hole() -> None: |
| """Bug J: The .gradio-container must not cap the page width below 100%. |
| |
| At 1920x1200 the page should fill the frame. A max-width: 1280px |
| rule leaves a 320px black bar on each side that the user calls |
| 'a black hole on the right panel' — uncomfortable to look at. |
| """ |
| from arena.gradio_app import CSS |
|
|
| container_rule = next( |
| (rule for rule in CSS.split("}") if ".gradio-container" in rule and "max-width" in rule), |
| None, |
| ) |
| assert container_rule is not None, "Expected an explicit .gradio-container max-width rule" |
|
|
| import re |
| px_match = re.search(r"max-width:\s*(\d+)px", container_rule) |
| pct_match = re.search(r"max-width:\s*100%", container_rule) |
|
|
| if pct_match: |
| return |
|
|
| assert px_match, f"Could not parse max-width from rule: {container_rule!r}" |
|
|
| px = int(px_match.group(1)) |
| assert px >= 1800, ( |
| f"gradio-container max-width={px}px is too narrow for 1920x1200 frames. " |
| "Bump to >= 1800 or remove the constraint so the page fills the viewport." |
| ) |
|
|
|
|
| def test_hero_splits_into_main_and_side_rail() -> None: |
| """The hero must split into a main column (manifesto) and a side rail |
| (numbered marks) on wide viewports so the 1920x1200 frame has visual |
| density on the right side, not a black hole. |
| """ |
| app = build_app(make_service()) |
| html_blocks = [ |
| b for b in app.blocks.values() |
| if b.__class__.__name__ == "HTML" and "hero" in (getattr(b, "value", "") or "") |
| ] |
| assert html_blocks, "App must include at least one hero HTML block" |
| html = html_blocks[0].value |
| assert "hero-main" in html, "Hero must have a .hero-main column for the manifesto" |
| assert "hero-side" in html, "Hero must have a .hero-side column with numbered marks" |
| assert "mark-num" in html, "Hero side rail must include numbered marks (01/02/03/04)" |
| assert "mark-text" in html, "Hero side rail must include mark descriptions" |
|
|
|
|
| def test_workbench_has_three_columns_at_wide_viewport() -> None: |
| """On >= 1100px, the workbench must render a 3-column grid: form / dispatch / rail. |
| |
| A 2-column layout leaves the right half of a 1920x1200 frame empty |
| once the form finishes growing — the black-hole symptom. The third |
| column (telemetry, keyboard hints, run state) is the fix. |
| """ |
| from arena.gradio_app import CSS |
|
|
| root_block = next( |
| (block for block in CSS.split("}") if ":root" in block and "--workbench-cols:" in block), |
| None, |
| ) |
| assert root_block is not None, "Expected --workbench-cols variable in :root" |
|
|
| import re |
| match = re.search(r"--workbench-cols:\s*([^;]+);", root_block) |
| assert match, f"Could not parse --workbench-cols from: {root_block!r}" |
| cols = match.group(1).strip() |
| track_count = len([t for t in re.split(r"\s+(?![^()]*\))", cols) if t.strip()]) |
| assert track_count == 3, ( |
| f"--workbench-cols must define 3 tracks, got {track_count}: {cols!r}" |
| ) |
|
|
|
|
| def _extract_status_from_html(html: str) -> str: |
| import re |
| match = re.search(r">(\w+)</span>", html) |
| return match.group(1) if match else "" |
|
|