From 526710d764bde56f91314018203baf45c4505579 Mon Sep 17 00:00:00 2001 From: Haoxiang Cheng <2739441541@qq.com> Date: Fri, 16 Oct 2026 14:02:56 +0800 Subject: [PATCH 1/2] fix: show public research agents to signed-out visitors --- .../tests/test_admin_analytics_frontend.py | 2 +- .../backend/tests/test_analytics_frontend.py | 2 +- .../test_backtest_comparison_frontend.py | 2 +- .../backend/tests/test_frontend_fast_boot.py | 2 +- .../tests/test_research_guest_shelf.py | 54 +++++++++++++++++++ dashboard/frontend/app.html | 2 +- dashboard/frontend/app.js | 29 ++++++---- 7 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 dashboard/backend/tests/test_research_guest_shelf.py diff --git a/dashboard/backend/tests/test_admin_analytics_frontend.py b/dashboard/backend/tests/test_admin_analytics_frontend.py index c906e506..eead095f 100644 --- a/dashboard/backend/tests/test_admin_analytics_frontend.py +++ b/dashboard/backend/tests/test_admin_analytics_frontend.py @@ -162,7 +162,7 @@ def test_app_lifecycle_and_cache_versions_are_wired(): # Lockstep owner for the console's bumped tags and the /admin page's pins: # every bump edits this test in the same change (Global Constraints). assert 'styles.css?v=159' in APP_HTML - assert 'app.js?v=159' in APP_HTML + assert 'app.js?v=160' in APP_HTML assert 'js/admin-tabs.js?v=12' in APP_HTML for tag in ( 'href="admin.css?v=13"', diff --git a/dashboard/backend/tests/test_analytics_frontend.py b/dashboard/backend/tests/test_analytics_frontend.py index 7c722877..649ea82d 100644 --- a/dashboard/backend/tests/test_analytics_frontend.py +++ b/dashboard/backend/tests/test_analytics_frontend.py @@ -41,7 +41,7 @@ def _function_body(source: str, signature: str) -> str: def test_analytics_script_loads_between_app_and_page_scripts(): - app_at = APP_HTML.index('') + app_at = APP_HTML.index('') analytics_at = APP_HTML.index( '' ) diff --git a/dashboard/backend/tests/test_backtest_comparison_frontend.py b/dashboard/backend/tests/test_backtest_comparison_frontend.py index ce90d9c6..47688294 100644 --- a/dashboard/backend/tests/test_backtest_comparison_frontend.py +++ b/dashboard/backend/tests/test_backtest_comparison_frontend.py @@ -191,7 +191,7 @@ def test_exact_raw_ties_mark_every_tied_series_best(): def test_comparison_script_and_semantic_table_ship_before_app(): helper = '' - app = '' + app = '' assert 'href="styles.css?v=159"' in APP_HTML assert APP_HTML.index(helper) < APP_HTML.index(app) for element_id in ( diff --git a/dashboard/backend/tests/test_frontend_fast_boot.py b/dashboard/backend/tests/test_frontend_fast_boot.py index 0968817a..05359603 100644 --- a/dashboard/backend/tests/test_frontend_fast_boot.py +++ b/dashboard/backend/tests/test_frontend_fast_boot.py @@ -194,7 +194,7 @@ def test_cache_busters_bumped(): # the next bump, so the exact one looks like the broken guard and gets # "fixed" by loosening it. That collision has already cost this repo one # round of follow-ups (#347/#348). - assert "app.js?v=159" in APP_HTML + assert "app.js?v=160" in APP_HTML assert "js/agent-editor.js?v=32" in APP_HTML assert "styles.css?v=159" in APP_HTML assert "js/leaderboard.js?v=38" in APP_HTML diff --git a/dashboard/backend/tests/test_research_guest_shelf.py b/dashboard/backend/tests/test_research_guest_shelf.py new file mode 100644 index 00000000..7cb44320 --- /dev/null +++ b/dashboard/backend/tests/test_research_guest_shelf.py @@ -0,0 +1,54 @@ +"""Execute the real shelf renderer for guest, owner, and network failure states.""" +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +APP = (Path(__file__).parents[2] / 'frontend' / 'app.js').read_text() +RENDER = APP[APP.index('async function renderResearchShelf()'):APP.index('/** The Community research card', APP.index('async function renderResearchShelf()'))] + + +@pytest.mark.parametrize('mode', ['guest', 'owner', 'failure']) +def test_research_shelf_access_states(mode): + if not shutil.which('node'): + pytest.skip('Node required') + script = r''' +const mode = MODE; +const section = {style:{display:'none'}}; +let handler, login = false, workbench = false; +const card = {dataset:{templateId:'research'},querySelector: s => s === '.research-open-btn' ? {addEventListener: (_,fn)=>handler=fn} : null}; +const grid = {innerHTML:'',closest:()=>section,querySelectorAll:()=>[card]}; +const empty = {}, count = {}; +const document = {getElementById:id=>({agentsGridResearch:grid,agentsEmptyResearch:empty,agentsCountResearch:count}[id])}; +const agent = {template_id:'research',name:'Research demo',research:{},added:true}; +const fetchResearchAgents = async()=> {if(mode==='owner') return [agent,{...agent,name:'Unadded',added:false}]; throw {status:mode==='guest'?401:503};}; +let catalogCalls=0; +const API = {get:async()=>{catalogCalls++;return {templates:[agent,{name:'Trading',template_id:'trading'}]};}}; +const API_BASE=''; +const templateMarketplaceShelf = a=>a.research?'research':'open'; +const escapeHtml=s=>String(s??''); +const openAuthModal=()=>login=true; +const openResearchWorkbench=()=>workbench=true; +const communityShelfButtonHtml=()=>''; +RENDER +(async()=>{await renderResearchShelf();if(handler)handler();console.log(JSON.stringify({html:grid.innerHTML,display:section.style.display,login,workbench,catalogCalls,error:empty.textContent}));})().catch(e=>{console.error(e);process.exit(1)}); +'''.replace('MODE', json.dumps(mode)).replace('RENDER', RENDER) + result = json.loads(subprocess.check_output(['node', '-e', script], text=True)) + assert result['display'] == '' + if mode == 'guest': + assert 'Research demo' in result['html'] + assert 'Sign in to use' in result['html'] + assert 'research-remove-btn' not in result['html'] + assert 'Trading' not in result['html'] + assert result['login'] and not result['workbench'] + assert result['catalogCalls'] == 1 + elif mode == 'owner': + assert 'Open workbench' in result['html'] + assert 'Unadded' not in result['html'] + assert result['workbench'] and not result['login'] + assert result['catalogCalls'] == 0 + else: + assert 'temporarily unavailable' in result['error'] + assert result['catalogCalls'] == 0 diff --git a/dashboard/frontend/app.html b/dashboard/frontend/app.html index b40dc8df..e81e7b5b 100644 --- a/dashboard/frontend/app.html +++ b/dashboard/frontend/app.html @@ -2512,7 +2512,7 @@

Refund Credits purchase

- + diff --git a/dashboard/frontend/app.js b/dashboard/frontend/app.js index bc31f5c9..e25474f1 100644 --- a/dashboard/frontend/app.js +++ b/dashboard/frontend/app.js @@ -13375,19 +13375,29 @@ async function renderResearchShelf() { const emptyEl = document.getElementById('agentsEmptyResearch'); const countEl = document.getElementById('agentsCountResearch'); if (!grid) return; + const section = grid.closest('.agents-category'); + if (section) section.style.display = ''; let agents = []; + let guest = false; try { - agents = await fetchResearchAgents(); + try { + agents = await fetchResearchAgents(); + } catch (error) { + if (error?.status !== 401) throw error; + guest = true; + const catalog = await API.get(`${API_BASE}/api/v1/agents/marketplace`); + agents = (catalog.templates || []).filter(agent => templateMarketplaceShelf(agent) === 'research'); + } } catch (error) { - // A 401 here just means the visitor is not signed in — the shelf hides - // rather than advertising an error for a surface guests cannot use. grid.innerHTML = ''; - if (emptyEl) emptyEl.hidden = true; - const section = grid.closest('.agents-category'); - if (section) section.style.display = error?.status === 401 ? 'none' : ''; + if (countEl) countEl.hidden = true; + if (emptyEl) { + emptyEl.hidden = false; + emptyEl.textContent = 'Research agents are temporarily unavailable. Reopen this page to retry.'; + } return; } - const added = agents.filter((agent) => agent.added); + const added = guest ? agents : agents.filter((agent) => agent.added); if (countEl) { countEl.textContent = String(added.length); countEl.hidden = added.length === 0; @@ -13416,13 +13426,14 @@ async function renderResearchShelf() { ${escapeHtml(((agent.research || {}).output_formats || []).join(' / '))}

- - + + ${guest ? '' : ``}
`; }).join(''); grid.querySelectorAll('.research-agent-card').forEach((card) => { card.querySelector('.research-open-btn')?.addEventListener('click', () => { + if (guest) { openAuthModal('login'); return; } openResearchWorkbench(card.dataset.templateId); }); card.querySelector('.research-remove-btn')?.addEventListener('click', async (event) => { From 3d4403cb6bfb2773d8824c9a71c721860775e985 Mon Sep 17 00:00:00 2001 From: Haoxiang Cheng <2739441541@qq.com> Date: Fri, 16 Oct 2026 14:14:33 +0800 Subject: [PATCH 2/2] fix: discard stale research shelf renders after sign-in --- .../tests/test_research_guest_shelf.py | 43 ++++++++++++++++++- dashboard/frontend/app.js | 6 +++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/dashboard/backend/tests/test_research_guest_shelf.py b/dashboard/backend/tests/test_research_guest_shelf.py index 7cb44320..e8290c22 100644 --- a/dashboard/backend/tests/test_research_guest_shelf.py +++ b/dashboard/backend/tests/test_research_guest_shelf.py @@ -7,7 +7,7 @@ import pytest APP = (Path(__file__).parents[2] / 'frontend' / 'app.js').read_text() -RENDER = APP[APP.index('async function renderResearchShelf()'):APP.index('/** The Community research card', APP.index('async function renderResearchShelf()'))] +RENDER = APP[APP.index('let researchShelfRenderVersion = 0;'):APP.index('/** The Community research card', APP.index('async function renderResearchShelf()'))] @pytest.mark.parametrize('mode', ['guest', 'owner', 'failure']) @@ -52,3 +52,44 @@ def test_research_shelf_access_states(mode): else: assert 'temporarily unavailable' in result['error'] assert result['catalogCalls'] == 0 + + +@pytest.mark.parametrize('reject_old', [False, True]) +def test_pending_guest_response_cannot_overwrite_signed_in_shelf(reject_old): + if not shutil.which('node'): + pytest.skip('Node required') + script = r""" +const section = {style:{}}; +const grid = {innerHTML:'',closest:()=>section,querySelectorAll:()=>[]}; +const empty = {}, count = {}; +const document = {getElementById:id=>({agentsGridResearch:grid,agentsEmptyResearch:empty,agentsCountResearch:count}[id])}; +let signedIn = false, resolveCatalog, rejectCatalog, started; +const catalogStarted = new Promise(resolve=>started=resolve); +const catalog = new Promise((resolve,reject)=>{resolveCatalog=resolve;rejectCatalog=reject;}); +const agent = {template_id:'research',name:'Owned research',research:{},added:true}; +const fetchResearchAgents = async()=>{if(signedIn)return [agent];throw {status:401};}; +const API = {get:()=>{started();return catalog;}}; +const API_BASE=''; +const templateMarketplaceShelf=()=> 'research'; +const escapeHtml=s=>String(s??''); +const communityShelfButtonHtml=()=>''; +RENDER +(async()=>{ + const oldRender=renderResearchShelf(); + await catalogStarted; + signedIn=true; + await renderResearchShelf(); + const expected=grid.innerHTML; + if(REJECT)rejectCatalog(new Error('Late failure')); + else resolveCatalog({templates:[{...agent,name:'Guest research'}]}); + await oldRender; + console.log(JSON.stringify({unchanged:expected===grid.innerHTML,html:grid.innerHTML,error:empty.textContent})); +})().catch(e=>{console.error(e);process.exit(1)}); +""".replace('RENDER', RENDER).replace('REJECT', json.dumps(reject_old)) + result = json.loads(subprocess.check_output(['node', '-e', script], text=True)) + assert result['unchanged'] + assert 'Owned research' in result['html'] + assert 'Open workbench' in result['html'] + assert 'research-remove-btn' in result['html'] + assert 'Guest research' not in result['html'] + assert not result.get('error') diff --git a/dashboard/frontend/app.js b/dashboard/frontend/app.js index e25474f1..a49e3213 100644 --- a/dashboard/frontend/app.js +++ b/dashboard/frontend/app.js @@ -13370,7 +13370,10 @@ function researchStatusLabel(status) { } /** The My Agents "Research Agents" shelf: cards for cloned research agents. */ +let researchShelfRenderVersion = 0; + async function renderResearchShelf() { + const renderVersion = ++researchShelfRenderVersion; const grid = document.getElementById('agentsGridResearch'); const emptyEl = document.getElementById('agentsEmptyResearch'); const countEl = document.getElementById('agentsCountResearch'); @@ -13383,12 +13386,14 @@ async function renderResearchShelf() { try { agents = await fetchResearchAgents(); } catch (error) { + if (renderVersion !== researchShelfRenderVersion) return; if (error?.status !== 401) throw error; guest = true; const catalog = await API.get(`${API_BASE}/api/v1/agents/marketplace`); agents = (catalog.templates || []).filter(agent => templateMarketplaceShelf(agent) === 'research'); } } catch (error) { + if (renderVersion !== researchShelfRenderVersion) return; grid.innerHTML = ''; if (countEl) countEl.hidden = true; if (emptyEl) { @@ -13397,6 +13402,7 @@ async function renderResearchShelf() { } return; } + if (renderVersion !== researchShelfRenderVersion) return; const added = guest ? agents : agents.filter((agent) => agent.added); if (countEl) { countEl.textContent = String(added.length);