mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-13 11:42:20 +02:00
317 lines
9.9 KiB
Python
317 lines
9.9 KiB
Python
from bs4 import BeautifulSoup
|
|
|
|
from src.visual_report import _source_evidence_summary, generate_visual_report
|
|
|
|
|
|
def test_visual_report_toc_links_match_rendered_heading_ids():
|
|
report = """
|
|
# Automated Crypto Trading Bot Strategies
|
|
|
|
### **1.0 Introduction & Research Scope**
|
|
|
|
Intro body.
|
|
|
|
### **2.0 Determining the "Best" Configuration**
|
|
|
|
Configuration body.
|
|
"""
|
|
|
|
html = generate_visual_report(
|
|
"crypto bot strategies",
|
|
report,
|
|
sources=[],
|
|
stats={},
|
|
session_id="rp-test",
|
|
)
|
|
soup = BeautifulSoup(html, "html.parser")
|
|
|
|
links = soup.select(".toc-sidebar nav a")
|
|
assert [link.get_text(strip=True) for link in links] == [
|
|
"1.0 Introduction & Research Scope",
|
|
'2.0 Determining the "Best" Configuration',
|
|
]
|
|
|
|
for link in links:
|
|
target_id = link["href"].removeprefix("#")
|
|
target = soup.find(id=target_id)
|
|
assert target is not None
|
|
assert target.name in {"h2", "h3"}
|
|
|
|
|
|
def test_visual_report_surfaces_evidence_profile_and_source_quality():
|
|
sources = [
|
|
{
|
|
"title": "Official documentation",
|
|
"url": "https://docs.example.com/guide",
|
|
"source_kind": "primary",
|
|
"source_score": 90,
|
|
"source_reason": "official project documentation",
|
|
"retrieval": "browser",
|
|
},
|
|
{
|
|
"title": "Independent analysis",
|
|
"url": "https://analysis.example.net/report",
|
|
"source_kind": "secondary",
|
|
"source_score": 70,
|
|
"retrieval": "fetch",
|
|
},
|
|
]
|
|
|
|
summary = _source_evidence_summary(sources)
|
|
assert summary == {
|
|
"sources": 2,
|
|
"domains": 2,
|
|
"primary": 1,
|
|
"browser": 1,
|
|
"average_score": 80,
|
|
"rated": 2,
|
|
}
|
|
|
|
soup = BeautifulSoup(
|
|
generate_visual_report(
|
|
"Evidence report",
|
|
"## Findings\n\nEvidence-backed text.\n\n### Detail\n\nMore detail.",
|
|
sources=sources,
|
|
stats={},
|
|
session_id="rp-evidence",
|
|
),
|
|
"html.parser",
|
|
)
|
|
|
|
values = [node.get_text(strip=True) for node in soup.select(".evidence-value")]
|
|
assert values == ["2", "2", "1", "80/100"]
|
|
assert soup.select_one(".evidence-read-time").get_text(" ", strip=True) == "1 min read"
|
|
assert soup.select_one(".source-badge.primary").get_text(strip=True) == "primary"
|
|
assert soup.find(string="Rendered read") is not None
|
|
assert soup.find(string="90/100") is not None
|
|
|
|
|
|
def test_visual_report_has_reading_progress_and_mobile_section_navigation():
|
|
soup = BeautifulSoup(
|
|
generate_visual_report(
|
|
"Navigation report",
|
|
"# Navigation report\n\n## First section\n\nBody.\n\n### Detail\n\nMore body.",
|
|
sources=[],
|
|
stats={},
|
|
),
|
|
"html.parser",
|
|
)
|
|
|
|
assert soup.select_one(".reading-progress-bar") is not None
|
|
options = soup.select("#mobile-section-select option")
|
|
assert [option.get_text(strip=True) for option in options] == [
|
|
"First section",
|
|
"— Detail",
|
|
]
|
|
assert [option["value"] for option in options] == ["first-section", "detail"]
|
|
|
|
|
|
def test_visual_report_wraps_editorial_sections_and_captions_source_images():
|
|
soup = BeautifulSoup(
|
|
generate_visual_report(
|
|
"Visual report",
|
|
"# Visual report\n\n## Opening\n\nBody.\n\n## Analysis\n\nMore body.",
|
|
sources=[{
|
|
"title": "Official release notes",
|
|
"url": "https://example.com/releases",
|
|
"image": "https://cdn.example.com/release-cover.jpg",
|
|
}],
|
|
stats={},
|
|
),
|
|
"html.parser",
|
|
)
|
|
|
|
sections = soup.select("main.content > section.report-section")
|
|
assert [section.h2.get_text(strip=True) for section in sections] == [
|
|
"Opening",
|
|
"Analysis",
|
|
]
|
|
hero = soup.select_one("figure.hero-image")
|
|
assert hero.img["alt"] == "Official release notes"
|
|
assert hero.figcaption.get_text(" ", strip=True) == (
|
|
"Official release notes example.com"
|
|
)
|
|
|
|
|
|
def test_visual_report_parses_and_compacts_research_trace_details():
|
|
report = """# Trace report
|
|
|
|
## Findings
|
|
|
|
Body.
|
|
|
|
<details markdown="1">
|
|
<summary>Research trace</summary>
|
|
|
|
### Planned Actions
|
|
|
|
- **Round 1** `planner` -> `web_search`
|
|
|
|
```text
|
|
Sources analyzed: 3
|
|
```
|
|
|
|
</details>
|
|
"""
|
|
soup = BeautifulSoup(
|
|
generate_visual_report(
|
|
"Trace report",
|
|
report,
|
|
sources=[{"title": "Source", "url": "https://example.com/source"}],
|
|
stats={},
|
|
),
|
|
"html.parser",
|
|
)
|
|
|
|
trace = soup.select_one("details.research-trace")
|
|
assert trace is not None
|
|
assert trace.select_one("h3").get_text(strip=True) == "Planned Actions"
|
|
assert trace.select_one("li").get_text(" ", strip=True).startswith("Round 1")
|
|
assert trace.select_one("pre").get_text(strip=True) == "Sources analyzed: 3"
|
|
support_summaries = [
|
|
node.get_text(" ", strip=True)
|
|
for node in soup.select(".sources-panel > details > summary")
|
|
]
|
|
assert support_summaries == ["Sources (1)", "Research trace"]
|
|
assert soup.select_one("main.content > .sources-panel > .research-trace") is trace
|
|
|
|
|
|
def test_visual_report_samples_hero_image_for_accent_palette():
|
|
rendered = generate_visual_report(
|
|
"Palette report",
|
|
"# Palette report\n\n## Findings\n\nBody.",
|
|
sources=[{
|
|
"title": "Colorful source",
|
|
"url": "https://example.com/source",
|
|
"image": "https://cdn.example.com/colorful.jpg",
|
|
}],
|
|
stats={},
|
|
)
|
|
|
|
assert "function __applyImagePalette(url)" in rendered
|
|
assert "__applyImagePalette(heroWrap.dataset.imgUrl)" in rendered
|
|
|
|
|
|
def test_visual_explanation_renders_constrained_diagram_as_inline_svg():
|
|
report = r'''# How photosynthesis works
|
|
|
|
## Energy flow
|
|
|
|
Light energy moves through a short conversion chain.
|
|
|
|
```visual-diagram
|
|
{"title":"Energy conversion","layout":"flow","nodes":[{"label":"Sunlight","detail":"Incoming light energy"},{"label":"Chlorophyll","detail":"Captures photons"},{"label":"Stored energy","detail":"Sugars retain energy"}],"edges":[{"from":0,"to":1,"label":"absorbed by"},{"from":1,"to":2,"label":"converted into"}]}
|
|
```
|
|
|
|
The diagram summarizes the mechanism, while the surrounding prose carries citations.
|
|
'''
|
|
|
|
soup = BeautifulSoup(
|
|
generate_visual_report(
|
|
"Explain photosynthesis visually",
|
|
report,
|
|
sources=[],
|
|
stats={},
|
|
category="visual",
|
|
session_id="rp-visual",
|
|
hidden_images=["https://cdn.example.com/hidden.jpg"],
|
|
),
|
|
"html.parser",
|
|
)
|
|
|
|
assert "category-visual" in soup.body.get("class", [])
|
|
figure = soup.select_one("figure.visual-diagram-flow")
|
|
assert figure is not None
|
|
assert figure.figcaption.get_text(strip=True) == "Energy conversion"
|
|
assert figure.select_one("svg[role='img']")["aria-label"] == "Energy conversion"
|
|
assert len(figure.select("g.visual-node")) == 3
|
|
assert len(figure.select("path.visual-edge")) == 2
|
|
assert "visual-diagram" not in soup.get_text()
|
|
assert soup.select_one("#btn-restore-images") is None
|
|
assert soup.select_one("#btn-hide-toolbar").get_text(strip=True) == "Hide toolbar"
|
|
|
|
|
|
def test_visual_explanation_escapes_diagram_copy_and_discards_invalid_specs():
|
|
report = r'''# Safe diagrams
|
|
|
|
## Valid
|
|
|
|
```visual-diagram
|
|
{"title":"Safe <script>alert(1)</script>","layout":"layers","nodes":[{"label":"<img src=x onerror=alert(1)>","detail":"First"},{"label":"Second","detail":"Done"}]}
|
|
```
|
|
|
|
## Invalid
|
|
|
|
```visual-diagram
|
|
{"title":"Broken","nodes":[}
|
|
```
|
|
'''
|
|
|
|
soup = BeautifulSoup(
|
|
generate_visual_report(
|
|
"Safe visual explanation",
|
|
report,
|
|
sources=[],
|
|
stats={},
|
|
category="visual",
|
|
),
|
|
"html.parser",
|
|
)
|
|
|
|
assert len(soup.select("figure.visual-diagram")) == 1
|
|
assert soup.select_one("figure.visual-diagram-layers") is not None
|
|
assert soup.select_one("main.content script") is None
|
|
assert soup.select_one("main.content img") is None
|
|
assert "<img src=x onerror=alert(1)>" in soup.select_one(".visual-node-label").get_text()
|
|
assert "Broken" not in soup.get_text()
|
|
|
|
|
|
def test_visual_explanation_embeds_sandboxed_visual_html_and_removes_active_content():
|
|
report = r'''# How a black hole forms
|
|
|
|
```visual-html
|
|
<!doctype html>
|
|
<html><head><style>
|
|
body { background:#05070a; color:white; background-image:url(https://tracker.invalid/x); }
|
|
</style></head><body onclick="alert(1)">
|
|
<main><svg viewBox="0 0 400 200"><circle cx="200" cy="100" r="40"></circle></svg></main>
|
|
<a href="https://example.com">external</a>
|
|
<script>alert(1)</script>
|
|
</body></html>
|
|
```
|
|
'''
|
|
|
|
soup = BeautifulSoup(
|
|
generate_visual_report(
|
|
"Explain black holes visually",
|
|
report,
|
|
sources=[],
|
|
stats={},
|
|
category="visual",
|
|
),
|
|
"html.parser",
|
|
)
|
|
|
|
iframe = soup.select_one("figure.visual-html-story iframe")
|
|
assert iframe is not None
|
|
assert iframe.get("sandbox") == ["allow-same-origin"]
|
|
assert "ResizeObserver" in iframe.get("onload", "")
|
|
embedded = BeautifulSoup(iframe["srcdoc"], "html.parser")
|
|
assert embedded.select_one("svg circle") is not None
|
|
assert embedded.select_one("script") is None
|
|
assert embedded.body.get("onclick") is None
|
|
assert embedded.select_one("a").get("href") is None
|
|
assert "tracker.invalid" not in str(embedded)
|
|
|
|
|
|
def test_visual_report_accepts_plain_html_fence_only_for_visual_category():
|
|
report = """```html
|
|
<!doctype html><html><body><svg viewBox="0 0 20 20"><path d="M0 0h20v20z"/></svg></body></html>
|
|
```"""
|
|
|
|
visual = generate_visual_report("A visual topic", report, [], {}, category="visual")
|
|
standard = generate_visual_report("A standard topic", report, [], {}, category="standard")
|
|
|
|
assert 'class="visual-html-story"' in visual
|
|
assert 'class="visual-html-story"' not in standard
|