From 6e0c05a07b9e6d58b5dbee6d34b93f6147a30960 Mon Sep 17 00:00:00 2001 From: Punch Date: Thu, 4 Jun 2026 03:16:14 +0000 Subject: [PATCH] Fix unclaimed filter to restrict to active-round specs only --- app/routes/specs.py | 4 +- tests/conftest.py | 32 ++++++++++++-- tests/fixtures/rounds_active/round_001.json | 2 +- tests/fixtures/specs_round/r01_001_easy.json | 25 +++++++++++ tests/test_api.py | 45 +++++++++++++------- 5 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 tests/fixtures/specs_round/r01_001_easy.json diff --git a/app/routes/specs.py b/app/routes/specs.py index 82d73df..1a5427d 100644 --- a/app/routes/specs.py +++ b/app/routes/specs.py @@ -52,7 +52,9 @@ async def list_specs( unclaimed: bool | None = Query(None, description="When true, return only specs with no passing submission (no SOTA set yet)"), ): specs = spec_store.load_all() - if active: + # unclaimed=true implies active-round context: non-competition specs (e.g. + # Thingiverse catalog entries) have no round and are never "claimable". + if active or unclaimed: active_ids = _active_round_ids() specs = [s for s in specs if s.round_id in active_ids] if tier is not None: diff --git a/tests/conftest.py b/tests/conftest.py index b2960c6..58ead32 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,14 +15,38 @@ def set_env(tmp_path, monkeypatch): monkeypatch.setenv("SPECS_DIR", "tests/fixtures/specs") -@pytest.fixture -def client(set_env): - # Re-import app after env is set so db.py reads the patched DB_PATH +def _make_client(): import importlib import app.db + import app.specs + import app.routes.specs import app.main importlib.reload(app.db) + importlib.reload(app.specs) + importlib.reload(app.routes.specs) importlib.reload(app.main) from app.main import app - with TestClient(app) as c: + return TestClient(app) + + +@pytest.fixture +def client(set_env): + # Re-import app after env is set so db.py reads the patched DB_PATH + with _make_client() as c: + yield c + + +@pytest.fixture +def unclaimed_client(tmp_path, monkeypatch): + """Client with an active round configured (needed for ?unclaimed= filter tests). + + Uses specs_round/ fixtures (r01_001_easy has round_id=round_001 via ID prefix) + and rounds_active/ so the round is active. The ?unclaimed filter restricts to + active-round specs only. + """ + db_path = str(tmp_path / "test.db") + monkeypatch.setenv("DB_PATH", db_path) + monkeypatch.setenv("SPECS_DIR", "tests/fixtures/specs_round") + monkeypatch.setenv("ROUNDS_DIR", "tests/fixtures/rounds_active") + with _make_client() as c: yield c diff --git a/tests/fixtures/rounds_active/round_001.json b/tests/fixtures/rounds_active/round_001.json index 4bc4206..95095a6 100644 --- a/tests/fixtures/rounds_active/round_001.json +++ b/tests/fixtures/rounds_active/round_001.json @@ -7,6 +7,6 @@ "ends": null, "scoring_metric": "mass_grams", "scoring_direction": "minimize", - "specs": [{"id": "r01_001_easy", "tier": "easy"}], + "specs": [{"id": "r01_001_easy", "tier": "easy"}, {"id": "001_bracket", "tier": "easy"}], "notes": null } diff --git a/tests/fixtures/specs_round/r01_001_easy.json b/tests/fixtures/specs_round/r01_001_easy.json new file mode 100644 index 0000000..84b771f --- /dev/null +++ b/tests/fixtures/specs_round/r01_001_easy.json @@ -0,0 +1,25 @@ +{ + "id": "r01_001_easy", + "version": "1.0", + "name": "Cantilever Bracket — PLA — 11 kg @ 86 mm [easy, mass]", + "description": "An easy-difficulty cantilever bracket in PLA (FDM). Minimize mass while surviving the load.", + "material": "pla", + "constraints": { + "load_newtons": 111.78, + "load_point_mm": [86.5, 55.8, 45.0], + "safety_factor": 1.5, + "bolt_pattern_mm": [ + [0.0, 0.0], [51.1, 0.0], [102.2, 0.0], + [0.0, 51.1], [51.1, 51.1], [102.2, 51.1] + ], + "bolt_diameter_clearance_mm": 6.5, + "mount_face_x_mm": 0.0, + "build_volume_mm": [189.6, 111.6, 90.0], + "max_overhang_deg": 50.0, + "min_wall_thickness_mm": 1.0 + }, + "scoring": { + "metric": "mass_grams", + "direction": "minimize" + } +} diff --git a/tests/test_api.py b/tests/test_api.py index b160071..686d4e9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -44,32 +44,47 @@ def test_get_spec_missing(client: TestClient): assert r.status_code == 404 -def test_specs_unclaimed_filter_no_submissions(client: TestClient): - """?unclaimed=true returns all specs when there are no passing submissions.""" - r = client.get("/specs?unclaimed=true") +def test_specs_unclaimed_filter_no_submissions(unclaimed_client: TestClient): + """?unclaimed=true returns active-round specs with no passing submissions.""" + r = unclaimed_client.get("/specs?unclaimed=true") assert r.status_code == 200 - assert len(r.json()) == 1 # fixture has one spec; no submissions yet + data = r.json() + # r01_001_easy is in active round_001; 001_bracket has no round_id (not claimable) + assert len(data) == 1 + assert data[0]["id"] == "r01_001_easy" -def test_specs_claimed_filter_no_submissions(client: TestClient): - """?unclaimed=false returns empty list when there are no passing submissions.""" - r = client.get("/specs?unclaimed=false") +def test_specs_claimed_filter_no_submissions(unclaimed_client: TestClient): + """?unclaimed=false returns empty list when no active-round specs have passing submissions.""" + r = unclaimed_client.get("/specs?unclaimed=false") assert r.status_code == 200 assert len(r.json()) == 0 # nothing claimed yet -def test_specs_unclaimed_filter_after_submission(client: TestClient): +ROUND_SUBMISSION = { + "spec_id": "r01_001_easy", + "agent_path": "agents/slim-spine", + "contributor": "TestMiner", + "commit_hash": "abc1234", + "mass_grams": 108.48, + "fea_stress_mpa": 7.50, + "fea_allowable_mpa": 25.0, + "passed": True, + "pr_number": 2, + "notes": "Slim spine agent on round spec", +} + + +def test_specs_unclaimed_filter_after_submission(unclaimed_client: TestClient): """After a passing submission, ?unclaimed=true excludes the spec.""" - # Create a passing submission for 001_bracket - client.post("/submissions", json=GOOD_SUBMISSION) - # Now 001_bracket is claimed - r_unclaimed = client.get("/specs?unclaimed=true") + unclaimed_client.post("/submissions", json=ROUND_SUBMISSION) + r_unclaimed = unclaimed_client.get("/specs?unclaimed=true") assert r_unclaimed.status_code == 200 - assert len(r_unclaimed.json()) == 0 # no unclaimed specs - # ?unclaimed=false should return it - r_claimed = client.get("/specs?unclaimed=false") + assert len(r_unclaimed.json()) == 0 # r01_001_easy is now claimed + r_claimed = unclaimed_client.get("/specs?unclaimed=false") assert r_claimed.status_code == 200 assert len(r_claimed.json()) == 1 + assert r_claimed.json()[0]["id"] == "r01_001_easy" def test_create_submission(client: TestClient):