feat: search the full Find Work catalog (Closes #673)
This commit is contained in:
1 parent
dcf26c896c
commit
77fab85a66
18 files changed
+201
-28
No files matched your search
@@ -155,3 +155,62 @@ def test_mobile_find_work_exposes_accessible_batch_controls_and_offline_asset():
|
||||
assert "env(safe-area-inset-bottom)" in css
|
||||
assert "BASE + 'static/batch-find-work.js'" in worker
|
||||
assert '<script src="static/batch-find-work.js"></script>' in html
|
||||
|
||||
|
||||
def test_find_work_search_ignores_stale_response_and_preserves_cross_query_selection():
|
||||
script = f"""
|
||||
const createFindWork=require({json.dumps(str(PICK_WORK))});
|
||||
const pending=[];
|
||||
const rendered=[];
|
||||
const fetchJson=path=>new Promise(resolve=>pending.push({{path,resolve}}));
|
||||
const controller=createFindWork({{
|
||||
fetchJson,
|
||||
onItems:items=>rendered.push(items.map(item=>item.number)),
|
||||
onPagination:()=>{{}}, onStatus:()=>{{}}, onSelection:()=>{{}},
|
||||
}});
|
||||
const first=controller.search('api');
|
||||
const second=controller.search('dashboard');
|
||||
pending[1].resolve({{items:[{{id:2,repository:'stackchain/dashboard',number:673}}],page:1,total:1,has_more:false}});
|
||||
second.then(()=>{{
|
||||
controller.startSelection();
|
||||
controller.toggleSelection(controller.items()[0]);
|
||||
const third=controller.search('worker');
|
||||
pending[2].resolve({{items:[{{id:3,repository:'stackchain/worker',number:674}}],page:1,total:1,has_more:false}});
|
||||
return third;
|
||||
}}).then(()=>{{
|
||||
controller.toggleSelection(controller.items()[0]);
|
||||
pending[0].resolve({{items:[{{id:1,repository:'stackchain/api',number:1}}],page:1,total:1,has_more:false}});
|
||||
return first;
|
||||
}}).then(()=>process.stdout.write(JSON.stringify({{
|
||||
calls:pending.map(entry=>entry.path),
|
||||
rendered,
|
||||
items:controller.items().map(item=>item.number),
|
||||
selected:controller.selectedItems().map(item=>item.number),
|
||||
}})));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"calls": [
|
||||
"api/v1/available-issues?page=1&q=api",
|
||||
"api/v1/available-issues?page=1&q=dashboard",
|
||||
"api/v1/available-issues?page=1&q=worker",
|
||||
],
|
||||
"rendered": [[673], [674]],
|
||||
"items": [674],
|
||||
"selected": [673, 674],
|
||||
}
|
||||
|
||||
|
||||
def test_mobile_find_work_search_has_clear_live_results_and_touch_targets():
|
||||
html = HTML.read_text()
|
||||
dashboard = DASHBOARD.read_text()
|
||||
css = CSS.read_text()
|
||||
|
||||
assert 'id="find-work-search"' in html
|
||||
assert 'id="clear-find-work-search"' in html
|
||||
assert 'id="find-work-match-status"' in html
|
||||
assert 'aria-live="polite"' in html
|
||||
assert "findWorkController.search(" in dashboard
|
||||
assert "#find-work-search" in dashboard
|
||||
assert ".find-work-search" in css
|
||||
assert ".find-work-search button" in css and "min-height:44px" in css
|
||||
@@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
|
||||
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
|
||||
assert '.update-reply-actions button { min-height:44px;' in html
|
||||
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
assert "stackchain-dashboard-shell-v97" in worker
|
||||
assert "stackchain-dashboard-shell-v98" in worker
|
||||
@@ -187,7 +187,7 @@ def test_legacy_cache_marker_is_normalized_out_of_build_identity(tmp_path):
|
||||
worker = changed_frontend / "service-worker.js"
|
||||
worker.write_text(
|
||||
worker.read_text().replace(
|
||||
"const CACHE = 'stackchain-dashboard-shell-v97';",
|
||||
"const CACHE = 'stackchain-dashboard-shell-v98';",
|
||||
"const CACHE = 'stackchain-dashboard-shell-v999';",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -215,6 +215,52 @@ async def test_available_issue_endpoint_is_bounded_retryable_and_no_store(monkey
|
||||
assert calls == [True]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_available_issue_search_filters_full_catalog_before_pagination(monkeypatch):
|
||||
async def available():
|
||||
return [
|
||||
{
|
||||
"repository": "stackchain/api",
|
||||
"number": number,
|
||||
"title": "Routine API maintenance",
|
||||
}
|
||||
for number in range(1, 52)
|
||||
] + [{
|
||||
"repository": "stackchain/dashboard",
|
||||
"number": 673,
|
||||
"title": "Search the full Find Work catalog",
|
||||
}]
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "available_issue_snapshot", available)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
by_repo = await client.get("/api/v1/available-issues?page=1&q=DASHBOARD")
|
||||
by_number = await client.get("/api/v1/available-issues?page=1&q=%23673")
|
||||
by_title = await client.get("/api/v1/available-issues?page=1&q=full%20find")
|
||||
|
||||
expected = [{
|
||||
"repository": "stackchain/dashboard",
|
||||
"number": 673,
|
||||
"title": "Search the full Find Work catalog",
|
||||
}]
|
||||
assert by_repo.json() == {"items": expected, "page": 1, "total": 1, "has_more": False}
|
||||
assert by_number.json() == by_repo.json()
|
||||
assert by_title.json() == by_repo.json()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_available_issue_search_rejects_oversized_query_without_scanning(monkeypatch):
|
||||
async def must_not_scan():
|
||||
raise AssertionError("invalid query must be rejected before catalog scan")
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "available_issue_snapshot", must_not_scan)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.get("/api/v1/available-issues", params={"q": "x" * 101})
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_available_issue_endpoint_reuses_catalog_published_by_another_worker(
|
||||
monkeypatch, tmp_path
|
||||
|
||||
@@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
|
||||
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/later-sync.js'" in source
|
||||
@@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
|
||||
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
|
||||
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
|
||||
assert ".markdown-content a { min-height:44px;" in css
|
||||
assert "stackchain-dashboard-shell-v97" in worker
|
||||
assert "stackchain-dashboard-shell-v98" in worker
|
||||
@@ -45,7 +45,7 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
|
||||
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
|
||||
|
||||
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||
assert "stackchain-dashboard-shell-v97" in worker
|
||||
assert "stackchain-dashboard-shell-v98" in worker
|
||||
|
||||
|
||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||
|
||||
@@ -186,7 +186,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||
assert "promptStorage:localStorage" in dashboard
|
||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||
assert "stackchain-dashboard-shell-v97" in worker
|
||||
assert "stackchain-dashboard-shell-v98" in worker
|
||||
assert ".device-setup-panel" in css
|
||||
assert ".device-readiness-card" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
||||
@@ -410,7 +410,7 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
|
||||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||
source = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/plan-today.js'" in source
|
||||
assert "BASE + 'static/plan-today-readiness.js'" in source
|
||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||
@@ -145,7 +145,7 @@ async function dispatchPush(payload) {{
|
||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
@@ -154,14 +154,14 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
||||
def test_offline_review_next_ships_today_completion_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/today-completion.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
@@ -169,7 +169,7 @@ def test_offline_review_next_ships_today_completion_atomically():
|
||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
@@ -177,14 +177,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/later-picker.js'" in source
|
||||
|
||||
|
||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
@@ -193,21 +193,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||
|
||||
|
||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
@@ -654,7 +654,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
|
||||
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/queue-today.js'" in source
|
||||
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
|
||||
def test_readiness_runtime_is_available_in_offline_shell():
|
||||
service_worker = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v97';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v98';" in service_worker
|
||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
|
||||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v97" in source
|
||||
assert "stackchain-dashboard-shell-v98" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user