Files
periscope/tests/test_datasheet_excerpt_tool.py
michele a96ee2e88a Stop periscope/src importing PinScope validate.py after live C2 smoke.
PCB review and src checks use native parse, constraints lookup, and
review_session. Inherited validate.py and validation_tools.py stay in
dependency/.
2026-09-20 15:21:32 +02:00

225 lines
8.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for get_datasheet_excerpt: safety guards, budget cap, cache."""
from __future__ import annotations
from tests.paths import SIMPLE_PROJECT, TAXONOMY
import json
from pathlib import Path
import pytest
from pypdf import PdfWriter
from backend.periscopex.models import DesignGraph
from backend.periscopex.utils import safe_mpn
from backend.periscopex.review_tools import (
EXCERPT_TOPICS,
ExcerptState,
execute_tool,
get_datasheet_excerpt,
)
from backend.services.llm.types import PdfBlock
from backend.services.review_session import _signal_neighbors
GRAPH = SIMPLE_PROJECT / "design_graph.json"
IC_MPNS = {
"U1": "SPX3819M5-L-3-3/TR",
"U2": "CH340E",
"U3": "MSPM0G3507SPTR",
}
def _blank_pdf(path: Path, pages: int = 3) -> None:
w = PdfWriter()
for _ in range(pages):
w.add_blank_page(width=200, height=200)
with path.open("wb") as fh:
w.write(fh)
@pytest.fixture
def graph_and_pdfs(tmp_path):
graph = DesignGraph.model_validate(json.loads(GRAPH.read_text()))
pdf_dir = tmp_path / "datasheets"
pdf_dir.mkdir()
for mpn in IC_MPNS.values():
_blank_pdf(pdf_dir / f"{safe_mpn(mpn)}.pdf")
return graph, pdf_dir
def _state(graph, current_ic, pdf_dir, **kwargs):
return ExcerptState(
current_ic=current_ic,
connected_designators=_signal_neighbors(graph, current_ic),
graph=graph,
pdf_dir=pdf_dir,
storage=None,
**kwargs,
)
def test_rejects_non_neighbor(graph_and_pdfs):
"""U1's review cannot fetch U2/U3 — they're not signal neighbors."""
graph, pdf_dir = graph_and_pdfs
state = _state(graph, "U1", pdf_dir)
text, attachment = get_datasheet_excerpt(
graph, {}, "U2", "absolute_max", state,
)
assert attachment is None
assert "not a signal neighbor" in text
assert state.fetch_count == 0
def test_rejects_self(graph_and_pdfs):
"""The IC under review can't excerpt its own datasheet — it's already in
context. Steer the model to use the existing PDF."""
graph, pdf_dir = graph_and_pdfs
state = _state(graph, "U2", pdf_dir)
text, attachment = get_datasheet_excerpt(
graph, {}, "U2", "absolute_max", state,
)
assert attachment is None
assert "already reviewing" in text
def test_rejects_unknown_topic(graph_and_pdfs):
"""Bad topic enum returns a clean error listing valid topics."""
graph, pdf_dir = graph_and_pdfs
state = _state(graph, "U2", pdf_dir)
text, attachment = get_datasheet_excerpt(
graph, {}, "U3", "bogus_topic", state,
)
assert attachment is None
assert "Unknown topic" in text
assert "absolute_max" in text
def test_returns_pdfblock_for_valid_neighbor(graph_and_pdfs):
"""U2's review can fetch U3 (a signal neighbor) — returns PdfBlock."""
graph, pdf_dir = graph_and_pdfs
state = _state(graph, "U2", pdf_dir)
text, attachment = get_datasheet_excerpt(
graph, {}, "U3", "pin_voltage_levels", state,
)
assert isinstance(attachment, PdfBlock)
assert attachment.cacheable is True
assert "U3" in text
assert "pin_voltage_levels" in text
assert state.fetch_count == 1
assert state.page_count > 0
def test_budget_cap_fetch_count(graph_and_pdfs):
"""After fetch_budget hits, further fetches are rejected with budget message."""
graph, pdf_dir = graph_and_pdfs
# Tight budget so the test is fast
state = _state(graph, "U3", pdf_dir, fetch_budget=2, page_budget=100)
# Two valid fetches succeed
for topic in ("absolute_max", "pin_voltage_levels"):
_, att = get_datasheet_excerpt(graph, {}, "U2", topic, state)
assert att is not None, f"topic {topic} should have succeeded"
# Third fetch should be budget-blocked
text, attachment = get_datasheet_excerpt(
graph, {}, "U2", "electrical_characteristics", state,
)
assert attachment is None
assert "budget exhausted" in text
def test_per_neighbor_page_budget_blocks_third_topic_on_same_neighbor(graph_and_pdfs):
"""A single neighbor can be fetched up to its per-neighbor page budget,
then further topics on it are blocked — bounds one interface's cost
without touching the global budget."""
graph, pdf_dir = graph_and_pdfs
# Blank PDFs fall back to 3 pages/fetch; budget of 4 admits the first two
# fetches (0→3, 3→6) and blocks the third once the neighbor is over.
state = _state(
graph, "U3", pdf_dir,
fetch_budget=10, page_budget=1000, per_neighbor_page_budget=4,
)
_, a1 = get_datasheet_excerpt(graph, {}, "U2", "absolute_max", state)
assert a1 is not None
_, a2 = get_datasheet_excerpt(graph, {}, "U2", "pin_voltage_levels", state)
assert a2 is not None
text, a3 = get_datasheet_excerpt(graph, {}, "U2", "electrical_characteristics", state)
assert a3 is None
assert "Per-neighbor" in text and "U2" in text
def test_global_page_budget_still_bounds_total_fanout(graph_and_pdfs):
"""The global page budget is a hard ceiling even when a neighbor is under
its per-neighbor sub-budget — bounds hub-IC fan-out."""
graph, pdf_dir = graph_and_pdfs
state = _state(
graph, "U3", pdf_dir,
fetch_budget=10, page_budget=2, per_neighbor_page_budget=100,
)
_, a1 = get_datasheet_excerpt(graph, {}, "U2", "absolute_max", state)
assert a1 is not None # capped to 2 pages → page_count hits global budget
text, a2 = get_datasheet_excerpt(graph, {}, "U2", "pin_voltage_levels", state)
assert a2 is None
assert "page budget exhausted" in text
def test_cache_shared_across_states(graph_and_pdfs):
"""Same (designator, topic, ds_md5) → second fetch reuses trimmed PDF.
Mirrors the cross-IC case: U2's review fetches U3@abs_max, then U3's
review fetches U3@abs_max from a separate ExcerptState sharing the same
cache dict.
"""
graph, pdf_dir = graph_and_pdfs
shared_cache: dict = {}
s1 = _state(graph, "U2", pdf_dir, cache=shared_cache)
s2 = _state(graph, "U3", pdf_dir, cache=shared_cache)
_, att1 = get_datasheet_excerpt(graph, {}, "U3", "absolute_max", s1)
assert att1 is not None
path1 = att1.path
# U3's review reusing the same neighbor (loopback: U3 fetching itself is
# blocked, so use a different IC). We assert path identity for the U2
# case from s1's perspective is preserved across a fresh state with the
# same cache by re-fetching from s1's neighbor U3 with state s_other.
s_other = _state(graph, "U2", pdf_dir, cache=shared_cache)
_, att2 = get_datasheet_excerpt(graph, {}, "U3", "absolute_max", s_other)
assert att2 is not None
assert att2.path == path1 # cache hit — same trimmed file
def test_no_state_returns_clean_error(graph_and_pdfs):
"""Calling without state (regression guard) returns an explicit message."""
graph, _ = graph_and_pdfs
text, attachment = get_datasheet_excerpt(graph, {}, "U3", "absolute_max", None)
assert attachment is None
assert "without per-review state" in text
def test_execute_tool_dispatcher_routes_correctly(graph_and_pdfs):
"""The dispatcher recognises the new tool name and forwards state."""
graph, pdf_dir = graph_and_pdfs
state = _state(graph, "U2", pdf_dir)
text, attachment = execute_tool(
graph, {}, "get_datasheet_excerpt",
{"designator": "U3", "topic": "absolute_max"},
state=state,
)
assert isinstance(attachment, PdfBlock)
assert state.fetch_count == 1
def test_existing_tools_return_tuple_with_none_attachment(graph_and_pdfs):
"""Backward compat: existing tools now return (text, None)."""
graph, _ = graph_and_pdfs
text, att = execute_tool(graph, {}, "get_pintable", {"designator": "U2"})
assert isinstance(text, str) and text
assert att is None
def test_topics_cover_u2_001_use_case():
"""The U2-001 false positive needs to verify MSPM0G3507 PA1's 5V-tolerance
— assert that the pin_voltage_levels topic regex matches typical
datasheet phrasings for this."""
pat = EXCERPT_TOPICS["pin_voltage_levels"]
assert pat.search("This pin is 5V-tolerant under normal operating conditions.")
assert pat.search("VIH = 0.7 × VDD")
assert pat.search("Input voltage range: -0.3V to VDD+0.3V")