"""Offline tests for the retrosynthesis_planning skill.""" import base64 import importlib.util import json import re import sys from urllib.parse import urlsplit import pytest from openai4s.config import get_config from openai4s.skills_loader import SkillLoader def _llm_prompt(request): if isinstance(request, dict): return request["prompt"] return request def _import_skill(): sys.path.insert(0, str(get_config().skills_dir)) from retrosynthesis_planning.kernel import ( # noqa: PLC0415 OpenAI4SLLMReactionEvidenceProvider, annotate_routes_with_llm, build_aizynth_command, build_llm_annotation_prompt, build_markdown_report, build_molecule_structure_src, build_pubchem_query_url, build_pubchem_structure_image_url, collect_molecule_briefs, collect_reaction_briefs, collect_reaction_evidence, command_to_shell, normalize_routes, parse_llm_annotations, rank_routes, render_route_tree_html, ) return { "annotate_routes_with_llm": annotate_routes_with_llm, "build_aizynth_command": build_aizynth_command, "build_llm_annotation_prompt": build_llm_annotation_prompt, "build_markdown_report": build_markdown_report, "build_molecule_structure_src": build_molecule_structure_src, "build_pubchem_query_url": build_pubchem_query_url, "build_pubchem_structure_image_url": build_pubchem_structure_image_url, "command_to_shell": command_to_shell, "collect_molecule_briefs": collect_molecule_briefs, "collect_reaction_evidence": collect_reaction_evidence, "collect_reaction_briefs": collect_reaction_briefs, "normalize_routes": normalize_routes, "OpenAI4SLLMReactionEvidenceProvider": OpenAI4SLLMReactionEvidenceProvider, "parse_llm_annotations": parse_llm_annotations, "rank_routes": rank_routes, "render_route_tree_html": render_route_tree_html, } def _andor_payload_json(html): match = re.search( r"const graph = JSON\.parse\(/\* openai4s-andor-data \*/ (.*?)\)\.graph;\n" r" graph\.nodes\.forEach", html, flags=re.DOTALL, ) assert match return json.loads(match.group(1)) def _graph_payload(html): return json.loads(_andor_payload_json(html))["graph"] def _svg_source_text(source): """Decode the self-contained SVG payload used by dashboard structure images.""" prefix = "data:image/svg+xml;base64," assert source.startswith(prefix) return base64.b64decode(source.removeprefix(prefix)).decode("utf-8") def _assert_structure_uris_are_canonical(html): """Every depiction is one canonical, self-contained SVG data URI. The producer already substitutes a placeholder if RDKit is unavailable, so a second DOM-loaded fallback only duplicates bytes and creates a URL sink. """ assert "data-fallback-src" not in html assert "dataset.fallbackSrc" not in html sources = re.findall(r'(?:src|href)="(data:image/svg\+xml;base64,[^"]+)"', html) assert sources prefix = "data:image/svg+xml;base64," for source in sources: encoded = source.removeprefix(prefix) decoded = base64.b64decode(encoded, validate=True) assert base64.b64encode(decoded).decode("ascii") == encoded ROUTE_PAYLOAD = { "routes": [ { "scores": {"state score": 0.64}, "solved": True, "tree": { "type": "mol", "smiles": "CC(=O)Oc1ccccc1C(=O)O", "children": [ { "type": "reaction", "template": "ester hydrolysis/disconnection", "metadata": {"classification": "0.0 Unrecognized"}, "children": [ {"type": "mol", "smiles": "CC(=O)O", "in_stock": True}, { "type": "mol", "smiles": "O=C(O)c1ccccc1O", "in_stock": True, }, ], } ], }, }, { "score": 0.91, "solved": False, "tree": { "type": "mol", "smiles": "CC(=O)Oc1ccccc1C(=O)O", "children": [ { "type": "reaction", "children": [ {"type": "mol", "smiles": "CC(=O)Cl", "in_stock": True}, { "type": "mol", "smiles": "unknown-intermediate", "in_stock": False, }, ], } ], }, }, ] } REALISTIC_AIZYNTH_ROUTE = [ { "type": "mol", "smiles": "CC(=O)Oc1ccccc1C(=O)O", "in_stock": False, "children": [ { "type": "reaction", "smiles": "[C:1]>>A.B", "is_reaction": True, "children": [ {"type": "mol", "smiles": "CC(=O)OC(C)=O", "in_stock": True}, {"type": "mol", "smiles": "O=C(O)c1ccccc1O", "in_stock": True}, ], } ], "scores": { "state score": 0.9976287063411217, "number of reactions": 1, "number of pre-cursors": 2, "number of pre-cursors in stock": 2, }, "metadata": {"created_at_iteration": 1, "is_solved": True}, } ] def test_retrosynthesis_skill_is_discovered(): skills = SkillLoader().discover() assert "retrosynthesis_planning" in skills skill = skills["retrosynthesis_planning"] assert skill.has_kernel assert "retrosynthesis_planning.kernel" in (skill.import_hint or "") assert "retrosynthesis" in skill.description.lower() def test_retrosynthesis_skill_is_searchable(): hits = SkillLoader().search("retrosynthesis route planning purchasable precursor") assert any(hit["name"] == "retrosynthesis_planning" for hit in hits) def test_model_backed_reaction_tasks_are_separate_discoverable_skills(): skills = SkillLoader().discover() expected = { "single-step-retrosynthesis": "RetroChimera", "retrosynthesis_planning": "AiZynthFinder", "reaction-forward-prediction": "ReactionT5v2-forward", "reaction-atom-mapping": "RXNMapper", "reaction-condition-recommendation": "Parrot", "reaction-yield-estimation": "ReactionT5v2-yield", } for name, model in expected.items(): assert name in skills body = skills[name].doc assert model in body assert "complete reaction" in skills["reaction-atom-mapping"].description assert "fully specified" in skills["reaction-yield-estimation"].description assert "Do not recurse" in skills["single-step-retrosynthesis"].doc def test_science_scenario_specifies_six_independent_problem_contracts(): scenario = ( get_config().skills_dir / "retrosynthesis_planning" / "SCENARIO_zh.md" ).read_text(encoding="utf-8") assert scenario.count("### Science Query") == 6 for heading in ( "## Problem 1. 单步逆合成前体生成", "## Problem 2. 多步逆合成路线规划", "## Problem 3. 反应原子映射与反应中心识别", "## Problem 4. 正向反应产物预测与 round-trip 验证", "## Problem 5. 反应条件推荐", "## Problem 6. 反应收率估计", "## 全 Scenario 的硬性约束", ): assert heading in scenario assert "不是一个单模型问题" in scenario assert "不应被写成一条固定 pipeline" in scenario def test_six_detailed_science_scenarios_are_independent_benchmark_specs(): scenario_dir = get_config().skills_dir / "retrosynthesis_planning" / "scenarios" expected = ( "01_single_step_retrosynthesis.md", "02_multistep_route_planning.md", "03_atom_mapping.md", "04_forward_prediction.md", "05_condition_recommendation.md", "06_yield_estimation.md", ) distinguishing_conditions = { "01_single_step_retrosynthesis.md": "跨类别 disconnection search", "02_multistep_route_planning.md": "budgeted search policy", "03_atom_mapping.md": "对称原子置换", "04_forward_prediction.md": "outcome prediction", "05_condition_recommendation.md": "condition tuple", "06_yield_estimation.md": "worst-group", } required_sections = ( "## Scenario Overview", "## 数据可获取性与 Benchmark 构建方案", "## Science Query", "## 阶段介绍", "## Input Data 与 Ground Truth 组织", "## `intermediate_results.json` 最低要求", "## 建议的 Reference Repository 结构", "## 评估自动化实现难度", "## 评测指标", "## 代码与数据的硬性约束", "## Domain-Specific Failure Cases", "## 参考文献与一手资源", ) for filename in expected: body = (scenario_dir / filename).read_text(encoding="utf-8") for heading in required_sections: assert heading in body, f"{filename} is missing {heading}" assert "private_evaluator/" in body assert "Ground Truth" in body assert distinguishing_conditions[filename] in body assert (scenario_dir / "README.md").exists() assert (scenario_dir / "README_zh.md").exists() def _single_step_benchmark(): sys.path.insert(0, str(get_config().skills_dir)) from retrosynthesis_planning import single_step_benchmark # noqa: PLC0415 return single_step_benchmark def _toy_canonicalizer(smiles): value = smiles.strip() if value == "not-smiles": raise ValueError("invalid fixture molecule") aliases = { "OCC": "CCO", "CN": "CN", "NC": "CN", } return aliases.get(value, value) def test_class_unknown_benchmark_rejects_hidden_reaction_fields(): benchmark = _single_step_benchmark() with pytest.raises(benchmark.SingleStepProtocolError, match="class-unknown"): benchmark.validate_public_targets( [ { "target_id": "target-1", "product_smiles": "CCOC=O", "reaction_class": "1", } ], canonicalizer=_toy_canonicalizer, ) def test_single_step_benchmark_preserves_invalid_and_duplicate_beams(): benchmark = _single_step_benchmark() targets = benchmark.validate_public_targets( [{"target_id": "target-1", "product_smiles": "CCOC=O"}], canonicalizer=_toy_canonicalizer, ) normalized = benchmark.normalize_prediction_payloads( targets, [ { "target_id": "target-1", "predictions": [ {"rank": 1, "reactants_smiles": "N.OCC", "score": 0.7}, {"rank": 2, "reactants_smiles": "CCO.N", "score": 0.6}, {"rank": 3, "reactants_smiles": "", "score": 0.2}, {"rank": 4, "reactants_smiles": None, "score": None}, { "rank": 5, "reactants_smiles": "not-smiles", "score": 0.1, }, ], } ], top_k=5, canonicalizer=_toy_canonicalizer, ) beams = normalized[0].predictions assert [beam.status for beam in beams] == [ "valid", "valid", "empty", "invalid", "invalid", ] assert beams[0].signature == "CCO.N" assert beams[1].signature == "CCO.N" assert beams[1].duplicate_of_rank == 1 assert len(beams) == 5 def test_single_step_private_evaluator_scores_targets_before_aggregation(): benchmark = _single_step_benchmark() targets = benchmark.validate_public_targets( [ {"target_id": "target-1", "product_smiles": "CCOC=O"}, {"target_id": "target-2", "product_smiles": "CCBr"}, ], canonicalizer=_toy_canonicalizer, ) predictions = benchmark.normalize_prediction_payloads( targets, [ { "target_id": "target-1", "predictions": [ {"rank": 1, "reactants_smiles": "N.OCC", "score": 0.7}, {"rank": 2, "reactants_smiles": "Cl.C", "score": 0.2}, ], }, { "target_id": "target-2", "predictions": [ {"rank": 1, "reactants_smiles": "not-smiles", "score": 0.6}, {"rank": 2, "reactants_smiles": "Br.C", "score": 0.4}, ], }, ], top_k=3, canonicalizer=_toy_canonicalizer, ) references = benchmark.normalize_references( targets, [ { "target_id": "target-1", "precursor_sets": ["CCO.N", "C.I"], }, {"target_id": "target-2", "precursor_sets": ["C.Br"]}, ], canonicalizer=_toy_canonicalizer, ) metrics = benchmark.evaluate_predictions(predictions, references, top_k=3) assert metrics["top_k_exact_match_accuracy"] == {"1": 0.5, "3": 1.0} assert metrics["multi_reference_recall_at_k"] == {"1": 0.25, "3": 0.75} assert metrics["mean_reciprocal_rank"] == 0.75 assert metrics["invalid_prediction_rate"] == 0.25 assert metrics["unused_budget_slot_rate"] == pytest.approx(1 / 3) assert [row["first_hit_rank"] for row in metrics["targets"]] == [1, 2] def test_single_step_benchmark_cli_writes_public_and_private_artifacts(tmp_path): benchmark = _single_step_benchmark() targets_path = tmp_path / "targets.csv" predictions_path = tmp_path / "predictions.jsonl" references_path = tmp_path / "references.jsonl" intermediate_path = tmp_path / "intermediate_results.json" metrics_path = tmp_path / "metrics.json" targets_path.write_text( "target_id,product_smiles\ntarget-1,CCOC=O\n", encoding="utf-8" ) predictions_path.write_text( json.dumps( { "target_id": "target-1", "predictions": [{"rank": 1, "reactants_smiles": "N.OCC", "score": 0.7}], } ) + "\n", encoding="utf-8", ) references_path.write_text( json.dumps({"target_id": "target-1", "precursor_sets": ["CCO.N"]}) + "\n", encoding="utf-8", ) assert ( benchmark.main( [ "normalize", "--targets", str(targets_path), "--predictions", str(predictions_path), "--output", str(intermediate_path), "--top-k", "3", ], canonicalizer=_toy_canonicalizer, ) == 0 ) assert ( benchmark.main( [ "evaluate", "--targets", str(targets_path), "--predictions", str(predictions_path), "--references", str(references_path), "--output", str(metrics_path), "--top-k", "3", ], canonicalizer=_toy_canonicalizer, ) == 0 ) intermediate = json.loads(intermediate_path.read_text(encoding="utf-8")) metrics = json.loads(metrics_path.read_text(encoding="utf-8")) assert intermediate["task_condition"] == "reaction_class_unknown" assert intermediate["targets"][0]["predictions"][0]["signature"] == "CCO.N" assert metrics["top_k_exact_match_accuracy"] == {"1": 1.0, "3": 1.0} def test_aspirin_example_dashboard_is_documented(): skill_root = get_config().skills_dir / "retrosynthesis_planning" skill_doc = (skill_root / "SKILL.md").read_text(encoding="utf-8") example = skill_root / "examples" / "aspirin_retrosynthesis.html" html = example.read_text(encoding="utf-8") assert "examples/aspirin_retrosynthesis.html" in skill_doc assert "Interactive Retrosynthesis Knowledge Graph" in html assert "Example LLM route analysis" in html assert "phenolic O-acylation" in html assert "pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles" not in html # The example is regenerated from committed source data, not hand-edited. assert (skill_root / "examples" / "build_example.py").exists() assert (skill_root / "examples" / "aspirin_routes.json").exists() assert (skill_root / "examples" / "aspirin_annotations.json").exists() # Regression lock: the target must not render with the 'unknown' color. graph = _graph_payload(html) target = next( n for n in graph["nodes"] if n.get("smiles") == "CC(=O)Oc1ccccc1C(=O)O" ) assert target["className"] == "target" molecule_sources = [ node["structureSrc"] for node in graph["nodes"] if node.get("kind") == "molecule" ] assert molecule_sources assert all( "structure renderer fallback" not in _svg_source_text(source) for source in molecule_sources ) _assert_structure_uris_are_canonical(html) def test_normalize_and_rank_routes(): funcs = _import_skill() routes = funcs["normalize_routes"](ROUTE_PAYLOAD) ranked = funcs["rank_routes"](routes) assert len(ranked) == 2 assert ranked[0]["solved"] is True assert ranked[0]["score"] == pytest.approx(0.64) assert ranked[0]["steps"] == 1 assert ranked[0]["starting_materials"] == ["CC(=O)O", "O=C(O)c1ccccc1O"] def test_normalize_realistic_aizynth_route_uses_metadata_solved(): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](REALISTIC_AIZYNTH_ROUTE)) assert ranked[0]["solved"] is True assert ranked[0]["score"] == pytest.approx(0.9976287063411217) assert ranked[0]["steps"] == 1 assert ranked[0]["starting_materials"] == ["CC(=O)OC(C)=O", "O=C(O)c1ccccc1O"] def test_render_html_and_report(): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) html = funcs["render_route_tree_html"]( ranked, target_smiles="CC(=O)Oc1ccccc1C(=O)O" ) assert "" in html assert "Route Ranking" in html assert "Interactive Retrosynthesis Knowledge Graph" in html assert 'id="andor-data"' in html assert 'id="andor-svg"' in html assert "Not predicted in this AiZynthFinder route export" in html assert "Molecule Briefs" in html assert '>[C:1](=[O:2])Cl.[N:3]", }, "children": [ { "type": "mol", "smiles": shared_intermediate, "children": [ { "type": "reaction", "template": "amide coupling", "children": [ { "type": "mol", "smiles": "O=C(Cl)c1ccc(Cl)cc1", "in_stock": True, }, { "type": "mol", "smiles": "Cc1ccc(N)cc1", "in_stock": True, }, ], } ], }, { "type": "mol", "smiles": "late-stage-intermediate", "in_stock": False, }, ], } ], }, }, { "score": 0.78, "solved": True, "tree": { "type": "mol", "smiles": target, "children": [ { "type": "reaction", "template": "heteroaryl installation", "children": [ {"type": "mol", "smiles": shared_intermediate}, { "type": "mol", "smiles": "CC1=NC(=NO1)B(O)O", "in_stock": True, }, ], } ], }, }, ] } ranked = funcs["rank_routes"](funcs["normalize_routes"](complex_payload)) def fake_llm(prompt): payload = json.loads(_llm_prompt(prompt).split("Route data:\n", 1)[1]) return json.dumps( { "routes": { str(route["rank"]): { "route_strategy": "LLM reads the route as a convergent medicinal-chemistry disconnection with a late-stage bond-forming event.", "key_disconnections": [ "Disconnect amide or heteroaryl linkage into validated partners" ], "reaction_sequence": [ "Assemble the aryl amide core", "Install the heteroaryl fragment under screened coupling conditions", ], "conditions_strategy": "Use a small DOE around base, solvent, temperature, and catalyst/activating agent.", "yield_outlook": "30-70% hypothetical route-step range; route yield will depend on the difficult late-stage step.", "route_risks": [ "Poor solubility", "Metal coordination by heteroatoms", "Purification burden", ], "recommended_next_steps": [ "Run small-scale condition screen", "Confirm exact-match literature or internal precedent", ], "chemist_verdict": "optimize: promising but evidence-limited route.", } for route in payload["routes"] }, "molecules": { molecule["smiles"]: { "description": f"{molecule['role']} molecule with route-specific feasibility notes." } for molecule in payload["molecules"] }, "reactions": { reaction["reaction_key"]: { "reaction_type": "Chemoselective bond-forming disconnection", "description": "LLM assigns a conservative named disconnection based on the reaction template.", "mechanistic_rationale": "The route is interpreted as a late-stage bond construction requiring chemoselectivity control.", "bond_changes": [ "Forge the key C-N or C-C linkage indicated by the template" ], "suggested_conditions": { "reagents": "coupling partner, base, and activating agent chosen by literature precedent", "solvent": "polar aprotic solvent screen", "temperature": "ambient to 80 C screen", }, "expected_yield_range": "30-70% hypothetical range", "yield_rationale": "Complex heteroaryl substrates often require optimization and may suffer from solubility or selectivity limits.", "selectivity_risks": [ "Heteroaryl coordination", "Competing N-acylation or protodeboronation", ], "safety_notes": [ "Assess exotherm and metal/base compatibility before scale-up" ], "validation_plan": [ "Search exact substructure precedent", "Run small-scale condition screen", ], "confidence": "medium", } for reaction in payload["reactions"] }, } ) html = funcs["render_route_tree_html"]( ranked, target_smiles=target, max_routes=5, llm=fake_llm, ) graph = _graph_payload(html) assert len(graph["nodes"]) >= 9 assert sum(node["id"] == f"mol:{target}" for node in graph["nodes"]) == 1 assert ( sum(node["id"] == f"mol:{shared_intermediate}" for node in graph["nodes"]) == 1 ) assert "Chemoselective bond-forming disconnection" in html assert "late-stage-intermediate" in html assert "data:image/svg+xml;base64" in html _assert_structure_uris_are_canonical(html) assert "0.0 Unrecognized" not in html def test_build_aizynth_command_is_shell_safe(): funcs = _import_skill() command = funcs["build_aizynth_command"]( "CC(=O)Oc1ccccc1C(=O)O", config_path="~/Documents/Openai4S/retro_data/config.yml", output_path="aspirin routes.json", conda_env="retro", ) shell = funcs["command_to_shell"](command) assert command[:4] == ["conda", "run", "-n", "retro"] assert "aizynthcli" in command assert "--config" in command assert "'aspirin routes.json'" in shell or '"aspirin routes.json"' in shell # --- LLM response robustness ------------------------------------------------- # host.llm has no JSON mode, so a conversation model routinely wraps its JSON in # prose or a mid-string fence. The dashboard must survive every one of these. CHATTY_JSON = '{"routes": {"1": {"route_strategy": "Recovered strategy."}}}' @pytest.mark.parametrize( "reply", [ CHATTY_JSON, f"```json\n{CHATTY_JSON}\n```", f"Sure! Here is the analysis:\n```json\n{CHATTY_JSON}\n```\nLet me know.", f"Here you go: {CHATTY_JSON} — hope that helps!", ], ) def test_llm_annotations_survive_prose_wrapped_json(reply): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) annotations = funcs["annotate_routes_with_llm"](ranked, lambda _: reply) assert annotations["routes"]["1"]["route_strategy"] == "Recovered strategy." html = funcs["render_route_tree_html"](ranked, llm=lambda _: reply) assert "Recovered strategy." in html assert "LLM Route Analysis" in html @pytest.mark.parametrize("reply", ["I cannot help with that.", ""]) def test_llm_annotations_warn_and_degrade_when_unparseable(reply): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) with pytest.warns(RuntimeWarning, match="not valid JSON"): assert funcs["annotate_routes_with_llm"](ranked, lambda _: reply) == {} with pytest.warns(RuntimeWarning, match="not valid JSON"): html = funcs["render_route_tree_html"](ranked, llm=lambda _: reply) # The renderer falls back to its own readout instead of losing the dashboard. assert "Route Planning Readout" in html assert "Route 1" in html @pytest.mark.parametrize("reply", ["null", "[1, 2]", '"a string"']) def test_llm_annotations_ignore_non_object_json(reply): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) assert funcs["annotate_routes_with_llm"](ranked, lambda _: reply) == {} html = funcs["render_route_tree_html"](ranked, llm=lambda _: reply) assert "Route Planning Readout" in html def test_parse_llm_annotations_raises_when_no_json_present(): funcs = _import_skill() with pytest.raises(ValueError): funcs["parse_llm_annotations"]("there is no object here") # --- target identification ---------------------------------------------------- def test_target_molecule_is_styled_as_target_in_knowledge_graph(): funcs = _import_skill() target = "CC(=O)Oc1ccccc1C(=O)O" ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) graph = _graph_payload( funcs["render_route_tree_html"](ranked, target_smiles=target) ) node = next(n for n in graph["nodes"] if n.get("smiles") == target) assert node["meta"] == "target" assert ( node["className"] == "target" ), "target must use the target color, not 'unknown'" # ...and the per-route SVG must agree with the knowledge graph. html = funcs["render_route_tree_html"](ranked, target_smiles=target) assert 'class="node target"' in html def test_target_is_identified_without_an_explicit_target_smiles(): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) graph = _graph_payload(funcs["render_route_tree_html"](ranked)) roots = [ n for n in graph["nodes"] if n["kind"] == "molecule" and n["meta"] == "target" ] assert len(roots) == 1 def test_target_matches_a_non_canonical_smiles(): pytest.importorskip("rdkit") funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) # Same molecule as the tree root, written differently. graph = _graph_payload( funcs["render_route_tree_html"](ranked, target_smiles="OC(=O)c1ccccc1OC(C)=O") ) node = next( n for n in graph["nodes"] if n["kind"] == "molecule" and n["meta"] == "target" ) assert node["className"] == "target" # --- payload hygiene ---------------------------------------------------------- def test_graph_payload_escapes_html_delimiters(): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) hostile = {"routes": {"1": {"route_strategy": ""}}} html = funcs["render_route_tree_html"](ranked, annotations=hostile) raw = _andor_payload_json(html) assert "<" not in raw and ">" not in raw assert json.loads(raw) # still valid JSON after escaping def test_structure_sources_never_round_trip_through_mutable_dom_state(): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) html = funcs["render_route_tree_html"](ranked) assert "JSON.parse(dataEl.textContent)" not in html assert "data-fallback-src" not in html assert "dataset.fallbackSrc" not in html assert "/* openai4s-andor-data */" in html assert "const graph = JSON.parse(/* openai4s-andor-data */" in html assert "safeStructureSource" in html assert "return encodeURI(source)" in html def test_structure_source_allowlist_is_exact_and_canonical(): funcs = _import_skill() guard = funcs["render_route_tree_html"].__globals__["_canonical_structure_src"] encoded = base64.b64encode(b"").decode("ascii") valid = "data:image/svg+xml;base64," + encoded assert guard(valid) == valid for invalid in ( "javascript:alert(1)", "Data:image/svg+xml;base64," + encoded, "data:image/svg+xml;charset=utf-8;base64," + encoded, " data:image/svg+xml;base64," + encoded, "data:image/svg+xml;base64,%3Csvg%2F%3E", "data:image/svg+xml;base64,PHN2\r\nZy8+", "data:image/svg+xml;base64,AB==", "data:image/svg+xml;base64,not padding", ): assert guard(invalid) == "" def test_an_oversized_depiction_degrades_to_the_labelled_placeholder(): """`_canonical_structure_src`'s 1 MiB cap is production-reachable. A ~2 kB peptide SMILES renders to a >1 MiB RDKit data URI, which the guard rejects. Without a fallback the briefs panel emitted ``, which resolves to the document's own URL and re-requests the whole dashboard as an image; the SVG tree and the graph payload dropped the depiction with no explanation at all. """ pytest.importorskip("rdkit") funcs = _import_skill() module = funcs["render_route_tree_html"].__globals__ oversized = "N" + "C(=O)C(N)Cc1ccccc1" * 120 + "O" raw = module["build_molecule_structure_src"](oversized) assert len(raw) > (1 << 20), "the cap is no longer reachable; revisit this test" assert module["_canonical_structure_src"](raw) == "" src = module["_structure_src"](oversized) assert src.startswith("data:image/svg+xml;base64,") assert "structure renderer fallback" in _svg_source_text(src) def test_molecule_briefs_warn_when_truncated(): funcs = _import_skill() ranked = funcs["rank_routes"](funcs["normalize_routes"](ROUTE_PAYLOAD)) with pytest.warns(RuntimeWarning, match="truncated"): briefs = funcs["collect_molecule_briefs"](ranked, max_molecules=1) assert len(briefs) == 1