Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions tests/test_web_app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import re
import tempfile
import unittest
from datetime import datetime
Expand All @@ -10,6 +11,13 @@
from web import app as web_app


REPO_ROOT = Path(__file__).resolve().parents[1]


def _read(path: str) -> str:
return (REPO_ROOT / path).read_text(encoding="utf-8")


class WebAppTests(unittest.TestCase):
def setUp(self):
self.client = web_app.app.test_client()
Expand Down Expand Up @@ -47,6 +55,193 @@ def test_manual_post_api_returns_gone_in_fixed_flow_mode(self):
self.assertIn("removed", payload["error"].lower())


class DashboardRefreshTests(unittest.TestCase):
def setUp(self):
self.client = web_app.app.test_client()

def test_dashboard_and_stats_routes_return_200(self):
with tempfile.TemporaryDirectory() as tmpdir:
old_db_path = database.DB_PATH
old_database_url = database.DATABASE_URL
for attr in ("pg_conn", "sqlite_conn", "conn"):
if hasattr(database._local, attr):
try:
getattr(database._local, attr).close()
except Exception:
pass
setattr(database._local, attr, None)
try:
database.DATABASE_URL = ""
database.DB_PATH = Path(tmpdir) / "stats.db"
database.init_db()

self.assertEqual(self.client.get("/").status_code, 200)
self.assertEqual(self.client.get("/api/stats").status_code, 200)
finally:
for attr in ("pg_conn", "sqlite_conn", "conn"):
if hasattr(database._local, attr):
try:
getattr(database._local, attr).close()
except Exception:
pass
setattr(database._local, attr, None)
database.DATABASE_URL = old_database_url
database.DB_PATH = old_db_path

def test_dashboard_main_tabs_are_phase1_six_with_advanced_collapsed(self):
html = _read("web/templates/index.html")
main_tabs = re.findall(r'<button class="nav-item(?: active)?" data-tab="([^"]+)"', html)
self.assertEqual(
main_tabs,
["overview", "explore", "evidence", "generated-papers", "insights", "papers"],
)
self.assertRegex(html, r"<details[^>]+class=\"[^\"]*advanced-nav[^\"]*\"[^>]*>")
self.assertNotRegex(html, r"<details[^>]+class=\"[^\"]*advanced-nav[^\"]*\"[^>]*open")

def test_dashboard_i18n_keys_match_and_static_labels_are_marked(self):
i18n = _read("web/static/js/i18n.js")
en_match = re.search(r"en:\s*\{(?P<body>.*?)\n\s*\},\n\s*zh:", i18n, re.S)
zh_match = re.search(r"zh:\s*\{(?P<body>.*?)\n\s*\},?\n\s*\};", i18n, re.S)
self.assertIsNotNone(en_match)
self.assertIsNotNone(zh_match)
key_pattern = r'^\s*"([^"]+)":'
en_keys = set(re.findall(key_pattern, en_match.group("body"), re.M))
zh_keys = set(re.findall(key_pattern, zh_match.group("body"), re.M))
self.assertEqual(en_keys, zh_keys)
self.assertIn("navigator.language", i18n)
self.assertIn("startsWith('zh')", i18n)
self.assertIn("localStorage", i18n)

html = _read("web/templates/index.html")
self.assertGreaterEqual(html.count("data-i18n="), 40)
for key in ("nav.overview", "nav.explore", "nav.evidence", "nav.generated", "nav.insights", "nav.papers"):
self.assertIn(f'data-i18n="{key}"', html)
for key in (
"empty.experimentGroups",
"search.researchAreas",
"discoveries.tier1",
"discoveries.tier2",
"insights.type.crossDomainBridge",
"agenda.noSelection",
"manuscript.routePreview",
):
self.assertIn(f'"{key}"', i18n)

def test_dashboard_legacy_visible_labels_are_gone(self):
frontend = "\n".join(
_read(path)
for path in (
"web/templates/index.html",
"web/static/js/app.js",
"web/static/js/agenda.js",
"web/static/js/manuscript_routing.js",
)
)
forbidden_labels = [
"Paper DB",
"Pipeline Papers",
"Paper Ideas",
"Method x Dataset Matrix",
"Method × Dataset Matrix",
"Taxonomy Map",
"Opportunity Map",
"Deep Insight",
"Deep Insights",
"IDEA #",
"RUN #",
"Main run",
"Research Paper Generation",
"Generated Papers",
"Complete Papers",
"Taxonomy Nodes",
"experiment ideas",
"PARADIGM",
"DISCOVERY",
]
for label in forbidden_labels:
self.assertNotIn(label, frontend)
self.assertIsNone(re.search(r"\bforge\b", frontend, re.I))

def test_dashboard_target_files_have_no_known_i18n_residuals(self):
frontend = "\n".join(
_read(path)
for path in (
"web/templates/index.html",
"web/static/js/app.js",
"web/static/js/agenda.js",
"web/static/js/manuscript_routing.js",
)
)
frontend = re.sub(r"<!--.*?-->", "", frontend, flags=re.S)
frontend = re.sub(r"//.*", "", frontend)
forbidden_labels = [
"Methods & Datasets",
"What People Are Working On",
"Where The Gaps Are",
"Recurring Themes",
"Paper Clusters",
"Core Entities",
"Key Links",
"NOVEL",
"PARTIAL",
"EXISTS",
"UNCHECKED",
"Universal",
"Cross-domain",
"Method:",
"Baselines:",
"Datasets:",
"Compute:",
"Strongest Challenge:",
"Fixed automatic pipeline",
"RUNNING",
"STOPPED",
"Auto Research status unavailable.",
"Paste YAML first.",
"VENUES",
"ROUTE PREVIEW",
"LINT PREVIEW",
"ERROR (",
]
for label in forbidden_labels:
self.assertNotIn(label, frontend)
self.assertIsNone(re.search(r"\bNo [A-Z][^\"'<>]* yet\b", frontend))

def test_dashboard_init_is_progressive_and_uses_idle_prefetch(self):
app_js = _read("web/static/js/app.js")
initial_block = re.search(
r"// Initial data loads(?P<body>.*?)// Stats refresh",
app_js,
re.S,
)
self.assertIsNotNone(initial_block)
initial_loads = set(re.findall(r"\b(load[A-Za-z0-9_]+)\(", initial_block.group("body")))
self.assertFalse(
initial_loads
& {
"loadTaxonomyDropdown",
"loadPapers",
"loadPaperProgressTab",
"loadGeneratedPapersTab",
"loadDiscoveriesTab",
"loadExperimentsTab",
"loadInsightsTab",
"loadProviders",
"loadOverviewGraph",
}
)
self.assertRegex(app_js, r"requestIdleCallback|setTimeout")
self.assertIn("prefetchInactiveTabs", app_js)
self.assertIn("overviewGraphLoaded", app_js)

def test_dashboard_dead_code_is_removed(self):
app_py = _read("web/app.py")
app_js = _read("web/static/js/app.js")
self.assertEqual(app_py.count("def _planned_tracks("), 1)
self.assertNotIn('label": "主实验"', app_py)
self.assertNotIn("function renderExperimentGroups(groups)", app_js)


class ExperimentGroupApiTests(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.TemporaryDirectory()
Expand Down
33 changes: 0 additions & 33 deletions web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,39 +216,6 @@ def _summarize_run(run: dict, artifact_counts: dict[str, int] | None = None, cla
}


def _planned_tracks(insight: dict, runs: list[dict]) -> list[dict]:
evidence_plan = _json_load(insight.get("evidence_plan"), {})
experimental_plan = _json_load(insight.get("experimental_plan"), {})
ablations = experimental_plan.get("ablations") or []
has_plot = any((run.get("artifact_counts") or {}).get("plot", 0) > 0 for run in runs)
has_bundle = any(run.get("has_bundle") for run in runs) or (insight.get("submission_status") == "bundle_ready")
main_state = "not_started"
canonical = _pick_canonical_run(runs, insight.get("canonical_run_id"))
if canonical:
main_state = canonical.get("status") or "unknown"
return [
{"key": "main", "label": "主实验", "enabled": True, "state": main_state},
{
"key": "ablation",
"label": "消融",
"enabled": bool((evidence_plan.get("ablation") or {}).get("enabled") or ablations),
"state": f"{len(ablations)} planned" if ablations else ("enabled" if (evidence_plan.get("ablation") or {}).get("enabled") else "not_planned"),
},
{
"key": "visualization",
"label": "可视化",
"enabled": bool((evidence_plan.get("visualization") or {}).get("enabled") or has_plot),
"state": "artifacts_ready" if has_plot else ("planned" if (evidence_plan.get("visualization") or {}).get("enabled") else "not_planned"),
},
{
"key": "bundle",
"label": "论文包",
"enabled": True,
"state": "bundle_ready" if has_bundle else (insight.get("submission_status") or "not_started"),
},
]


def _planned_tracks(insight: dict, runs: list[dict]) -> list[dict]:
evidence_plan = _json_load(insight.get("evidence_plan"), {})
experimental_plan = _json_load(insight.get("experimental_plan"), {})
Expand Down
104 changes: 102 additions & 2 deletions web/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,42 @@ header#topBar {

.topbar-right {
margin-left: auto;
flex: 0 0 320px;
flex: 0 1 460px;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}

/* ── Search ───────────────────────────────────────────────────────── */

.search-container { position: relative; }
.search-container { position: relative; flex: 1 1 auto; min-width: 180px; }

.language-toggle {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-base);
}
.lang-btn {
min-width: 30px;
height: 28px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-dim);
font-family: var(--font);
font-size: 0.7rem;
font-weight: 700;
cursor: pointer;
}
.lang-btn.active {
background: var(--accent-dim);
color: var(--accent);
}

.search-icon {
position: absolute;
Expand Down Expand Up @@ -285,8 +314,50 @@ header#topBar {
color: var(--accent);
font-weight: 600;
}
.advanced-nav {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.advanced-nav summary {
list-style: none;
cursor: pointer;
padding: 8px 14px;
color: var(--text-dim);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
border-radius: 8px;
}
.advanced-nav summary::-webkit-details-marker { display: none; }
.advanced-nav summary:hover {
background: var(--bg-elevated);
color: var(--text-secondary);
}
.advanced-nav-item {
display: block;
width: 100%;
margin: 2px 0;
padding: 8px 14px 8px 26px;
border: none;
background: none;
color: var(--text-secondary);
font-family: var(--font);
font-size: 0.78rem;
font-weight: 500;
text-align: left;
border-radius: 8px;
cursor: pointer;
}
.advanced-nav-item:hover,
.advanced-nav-item.active {
background: var(--accent-dim);
color: var(--accent);
}
.sidebar.collapsed .nav-item span { opacity: 0; width: 0; }
.sidebar.collapsed .nav-item { justify-content: center; padding: 10px; }
.sidebar.collapsed .advanced-nav { display: none; }

/* Sidebar footer */
.sidebar-footer {
Expand Down Expand Up @@ -372,6 +443,35 @@ header#topBar {
}
.card:hover { border-color: var(--border-hover); }
.card-compact { padding: 14px 18px; }
.advanced-panel {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-card);
padding: 0;
}
.advanced-panel summary,
.advanced-inline summary {
cursor: pointer;
color: var(--text-secondary);
font-weight: 700;
}
.advanced-panel summary {
padding: 12px 16px;
font-size: 0.82rem;
}
.advanced-panel .card {
margin: 0 12px 12px;
}
.advanced-inline {
margin-top: 8px;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
}
.advanced-inline summary {
font-size: 0.75rem;
}

.card-header {
display: flex;
Expand Down
Loading
Loading