File size: 16,167 Bytes
31c8075 | 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 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 | 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 ""
|