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);