Spaces:
Running on Zero
Running on Zero
File size: 14,966 Bytes
29bb346 6fbd77b b2e433d 6fbd77b 5b12fdd f2831be 6fbd77b 1534206 6fbd77b 1534206 6fbd77b 73fc26e c47f9fb de12820 f2831be de12820 b2e433d 6fbd77b 1534206 6fbd77b 613e06a c47f9fb 613e06a c47f9fb 6fbd77b 5b12fdd 1534206 a8a3c84 1534206 29bb346 1534206 5b12fdd 4d2437d f943246 4d2437d eef685d a9dc81e 6fbd77b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | import json
import unittest
from unittest.mock import patch
from pydantic import ValidationError
from app import DEPTHS, SEARCH_CACHE, ResearchReport, SectionPlan, align_sections_to_plan, cached_search, chat_json, deduplicate, infer_section_source_ids, normalize_comparison, normalize_process, parse_json_object, prepare_evidence_inputs, preview_frame, render_presentation, render_section_content, render_value, safe_slug, search_literature, serializable_validation_errors
def base_report(section):
return {
"title": "Test report", "subtitle": "Topic-adaptive structured output",
"research_type": "engineering decision",
"executive_summary": ["A substantive conclusion supported by the report."],
"sections": [dict(section, id=f"section-{i}") for i in range(3)],
"references": [], "limitations": ["Recent literature was not externally verified."],
"english_understanding": {},
}
class AppTests(unittest.TestCase):
def test_slug(self):
self.assertEqual(safe_slug("Diffusion Transformer 研究"), "diffusion-transformer")
def test_deduplicate_versions(self):
self.assertEqual(len(deduplicate([{"id": "1234.5678v1"}, {"id": "1234.5678v2"}])), 1)
def test_evidence_inputs_are_bounded_and_prioritize_deep_reads(self):
papers = [{"id": str(i), "title": f"Paper {i}", "summary": "Summary"} for i in range(10)]
result = prepare_evidence_inputs(papers, {"8": "deep text", "9": "deep text"}, 3)
self.assertEqual(len(result), 3)
self.assertEqual([item["id"] for item in result[:2]], ["8", "9"])
self.assertIn("selected_text_extract", result[0])
@patch("app.openalex_search", return_value=[])
@patch("app.arxiv_search")
def test_search_balances_multiple_query_intents(self, mocked_arxiv, _mocked_openalex):
mocked_arxiv.side_effect = [
[{"id": f"classic-{i}", "title": f"Classic {i}"} for i in range(5)],
[{"id": f"recent-{i}", "title": f"Recent {i}"} for i in range(5)],
]
papers, warnings = search_literature(["classic", "recent"], 4)
self.assertEqual([paper["id"] for paper in papers], ["classic-0", "recent-0", "classic-1", "recent-1"])
self.assertEqual(warnings, [])
@patch("app.openalex_search", return_value=[{"id": "oa-1", "title": "Fallback"}])
@patch("app.arxiv_search", return_value=[])
def test_search_uses_openalex_when_arxiv_returns_empty(self, _mocked_arxiv, mocked_openalex):
papers, _ = search_literature(["topic"], 5)
self.assertEqual(papers[0]["id"], "oa-1")
mocked_openalex.assert_called_once()
@patch("app.openalex_search", side_effect=TimeoutError("offline"))
@patch("app.arxiv_search", side_effect=TimeoutError("offline"))
def test_search_stops_repeating_failed_providers(self, mocked_arxiv, mocked_openalex):
papers, warnings = search_literature([f"query-{i}" for i in range(5)], 20)
self.assertEqual(papers, [])
self.assertEqual(mocked_arxiv.call_count, 2)
self.assertEqual(mocked_openalex.call_count, 2)
self.assertTrue(any("已停止剩余重复请求" in warning for warning in warnings))
def test_search_cache_avoids_duplicate_external_calls(self):
SEARCH_CACHE.clear()
search_fn = unittest.mock.Mock(return_value=[{"id": "cached", "title": "Cached paper"}])
first = cached_search("test-provider", "same query", 5, search_fn)
second = cached_search("test-provider", "same query", 5, search_fn)
self.assertEqual(first, second)
self.assertEqual(search_fn.call_count, 1)
def test_depth_increases_analysis_not_unbounded_search_calls(self):
self.assertEqual([DEPTHS[name]["queries"] for name in ("quick", "standard", "deep")], [2, 3, 4])
self.assertEqual([DEPTHS[name]["deep"] for name in ("quick", "standard", "deep")], [2, 4, 8])
def test_missing_standard_and_deep_sections_are_exposed_for_repair(self):
plans = [
SectionPlan(id=f"section-{i}", title=f"Section {i}", purpose="Explain one distinct technical decision.", layout="text")
for i in range(4)
]
partial = {"sections": [{"id": "section-0", "takeaway": "Only the first section was returned.", "content": {"points": ["A substantive point"]}}]}
aligned = align_sections_to_plan(partial, plans)
self.assertEqual(len(aligned["sections"]), 4)
self.assertEqual([item["id"] for item in aligned["sections"]], [plan.id for plan in plans])
self.assertEqual(aligned["sections"][1]["content"], {})
def test_parser_removes_invalid_string_line_continuation(self):
self.assertEqual(parse_json_object('{"title":"A\\\nB","value":1}'), {"title": "AB", "value": 1})
@patch("app.generate_text", side_effect=['{"title":"broken\\\n', '{"title":"valid"}'])
def test_json_generation_retries_malformed_response(self, mocked_generate):
self.assertEqual(chat_json("system", "prompt", 2048), {"title": "valid"})
self.assertEqual(mocked_generate.call_count, 2)
self.assertIn("Regenerate from scratch", mocked_generate.call_args.args[0])
def test_render_is_downloadable_document(self):
section = {"title": "Scope", "purpose": "Define the technical decision boundary.", "takeaway": "The boundary determines the useful evidence.", "layout": "text", "content": {"points": ["A meaningful point", "Another meaningful point"]}, "source_ids": []}
output = render_presentation(base_report(section), [], "2026-01-01")
self.assertIn("<!doctype html>", output)
self.assertIn('<button id="print" title="打印或保存为 PDF">保存 PDF</button>', output)
self.assertIn('id="overview"', output)
self.assertIn('id="agenda"', output)
self.assertIn('data-slide-target="section-1"', output)
def test_preview_allows_browser_print_dialog(self):
frame = preview_frame("<!doctype html><title>Deck</title>")
self.assertIn('sandbox="allow-scripts allow-popups allow-modals"', frame)
def test_source_chip_links_to_rendered_reference(self):
section = {"title": "Evidence", "purpose": "Connect a claim to retrieved evidence.", "takeaway": "The cited result supports this bounded conclusion.", "layout": "evidence", "content": {"claim": "A supported claim", "conditions": ["A stated condition"]}, "source_ids": ["paper-1"]}
report = base_report(section)
report["references"] = ["paper-1"]
paper = {"id": "paper-1", "title": "A Retrieved Paper", "authors": ["A. Author"], "published": "2025-01-01", "url": "https://example.com/paper"}
output = render_presentation(report, [paper], "2026-01-01")
self.assertIn('href="#ref-paper-1"', output)
self.assertIn('id="ref-paper-1"', output)
def test_empty_report_is_rejected(self):
with self.assertRaises(ValidationError):
ResearchReport.model_validate({})
def test_technical_evolution_report(self):
section = {"title": "Evolution", "purpose": "Explain changes across technical generations.", "takeaway": "Each generation resolves a prior bottleneck.", "layout": "timeline", "content": {"nodes": [{"year": "2020", "title": "A", "innovation": "First mechanism"}, {"year": "2024", "title": "B", "innovation": "Second mechanism"}]}, "source_ids": []}
self.assertEqual(ResearchReport.model_validate(base_report(section)).sections[0].layout, "timeline")
def test_paper_analysis_report(self):
section = {"title": "Paper", "purpose": "Assess the paper contribution and evidence.", "takeaway": "The method trades accuracy for efficiency.", "layout": "paper-analysis", "content": {"method": "A detailed method", "evidence": ["A reported experiment"], "limitations": ["A narrow benchmark"]}, "source_ids": []}
self.assertEqual(ResearchReport.model_validate(base_report(section)).sections[0].layout, "paper-analysis")
def test_evidence_gap_report(self):
section = {"title": "Evidence gap", "purpose": "Separate known facts from hypotheses.", "takeaway": "More evidence is required before choosing a method.", "layout": "evidence-gap", "content": {"interpretation": "The term may be ambiguous.", "related_topics": ["Adjacent field"], "hypotheses": ["Possible interpretation"], "required_inputs": ["Definition or internal data"]}, "source_ids": []}
self.assertEqual(ResearchReport.model_validate(base_report(section)).sections[0].layout, "evidence-gap")
def test_evidence_gap_accepts_equivalent_semantic_fields(self):
section = {"title": "Evidence gap", "purpose": "Separate available evidence from unresolved questions.", "takeaway": "Deployment requires evidence that is not yet public.", "layout": "evidence-gap", "content": {"known_evidence": ["A related result exists"], "unknowns": ["No direct benchmark"], "next_steps": ["Run an internal evaluation"]}, "source_ids": []}
validated = ResearchReport.model_validate(base_report(section))
self.assertEqual(validated.sections[0].content["unknowns"], ["No direct benchmark"])
def test_paper_analysis_accepts_equivalent_semantic_fields(self):
section = {"title": "Paper analysis", "purpose": "Analyze a paper without enforcing field spelling.", "takeaway": "The result is useful under a bounded condition.", "layout": "paper-analysis", "content": {"method_summary": "A compact architecture", "experimental_results": ["A controlled comparison"], "caveats": ["A narrow dataset"]}, "source_ids": []}
self.assertEqual(ResearchReport.model_validate(base_report(section)).sections[0].layout, "paper-analysis")
def test_invalid_empty_section(self):
section = {"title": "Empty", "purpose": "This section should fail validation.", "takeaway": "There is no substantive supporting content.", "layout": "text", "content": {}, "source_ids": []}
with self.assertRaises(ValidationError):
ResearchReport.model_validate(base_report(section))
def test_validation_feedback_is_json_serializable(self):
section = {"title": "Invalid", "purpose": "Trigger a custom validator error.", "takeaway": "The timeline is intentionally incomplete.", "layout": "timeline", "content": {"nodes": []}, "source_ids": []}
try:
ResearchReport.model_validate(base_report(section))
except ValidationError as exc:
errors = serializable_validation_errors(exc)
else:
self.fail("Expected report validation to fail")
json.dumps(errors)
self.assertNotIn("ctx", errors[0])
def test_comparison_requires_rows(self):
section = {"title": "Compare", "purpose": "Compare engineering alternatives fairly.", "takeaway": "No choice is universally optimal here.", "layout": "comparison", "content": {"columns": ["speed", "quality"], "rows": []}, "source_ids": []}
with self.assertRaises(ValidationError):
ResearchReport.model_validate(base_report(section))
def test_nested_json_is_rendered_as_semantic_html(self):
output = render_value({"findings": [{"claim": "Works well", "conditions": ["GPU", "FP16"]}]})
self.assertIn("<h4>", output)
self.assertIn("Works well", output)
self.assertIn("GPU", output)
self.assertNotIn("{"", output)
self.assertNotIn("['GPU'", output)
def test_detail_and_subpoints_stay_in_one_point_card(self):
output = render_value({
"detail": "生成模型学习数据分布。",
"subpoints": ["显式密度模型", "隐式生成模型", "扩散模型"],
})
self.assertIn('class="point-detail"', output)
self.assertIn('class="point-subpoints"', output)
self.assertNotIn("<h4>Detail</h4>", output)
self.assertNotIn("<h4>Subpoints</h4>", output)
self.assertLess(output.index("生成模型学习数据分布"), output.index("显式密度模型"))
def test_timeline_keeps_nonstandard_fields(self):
output = render_section_content({"layout": "timeline", "content": {"nodes": [
{"stage": "Early", "method": "Baseline", "problem": "High cost", "tradeoffs": ["Memory", "Latency"]},
{"stage": "Later", "method": "Improved", "problem": "Low recall", "tradeoffs": ["Complexity"]},
]}})
self.assertIn("High cost", output)
self.assertIn("Memory", output)
self.assertNotIn("{'", output)
def test_comparison_does_not_duplicate_dimension_column(self):
content = {
"columns": ["方法维度", "Debevec & Malik (1997) - HDR重建", "Mertens et al. (2007) - 多曝光融合"],
"rows": [
{"name": "核心目标", "values": ["恢复场景物理辐射度图", "直接生成视觉质量高的显示图像"]},
{"dimension": "是否需要CRF标定", "values": ["是", "否"]},
],
}
row_label, columns, rows = normalize_comparison(content)
self.assertEqual(row_label, "方法维度")
self.assertEqual(len(columns), 2)
self.assertTrue(all(len(values) == 2 for _, values in rows))
output = render_section_content({"layout": "comparison", "content": content})
self.assertEqual(output.count("<th>方法维度</th>"), 1)
self.assertIn("恢复场景物理辐射度图", output)
def test_process_is_split_into_interactive_branches(self):
content = {"steps": [
{"name": "输入多曝光LDR图像序列", "description": "公共输入"},
{"name": "HDR重建:辐射度标定", "description": "恢复CRF"},
{"name": "HDR重建:权重融合", "description": "辐射域融合"},
{"name": "多曝光融合:多尺度分解", "description": "拉普拉斯金字塔"},
{"name": "多曝光融合:权重图生成与融合", "description": "质量权重"},
]}
common, branches = normalize_process(content)
self.assertEqual(len(common), 1)
self.assertEqual([branch["name"] for branch in branches], ["HDR重建", "多曝光融合"])
output = render_section_content({"layout": "process", "content": content})
self.assertIn("flow-branches", output)
self.assertIn("<details", output)
self.assertIn("恢复CRF", output)
def test_named_retrieved_paper_recovers_missing_source_id(self):
section = {"title": "经典路线", "takeaway": "Debevec and Malik established the calibrated HDR pipeline.", "content": {"details": "The 1997 method estimates the camera response function."}, "source_ids": []}
papers = [{"id": "paper-1", "title": "Recovering High Dynamic Range Radiance Maps from Photographs", "authors": ["Paul Debevec", "Jitendra Malik"], "published": "1997-01-01"}]
self.assertEqual(infer_section_source_ids(section, papers), ["paper-1"])
if __name__ == "__main__":
unittest.main()
|