Files
periscope/tests/test_cost_estimator.py
T
michele c362ef8a56 Split native Periscope (periscope/src) from inherited PinScope (periscope/dependency).
Keep validate.py and the finding engine as-is. AGPL LICENSE stays at the repo root.
Docker overlays dependency then src. Do not delete the inherited tree.
2026-09-20 14:31:42 +02:00

92 lines
3.5 KiB
Python

"""Cost estimator smoke-tests against the simple_project fixture."""
from __future__ import annotations
from tests.paths import SIMPLE_PROJECT
import pytest
from backend.services import projects as proj_svc
from backend.services.cost_estimator import (
CostEstimate,
estimate_pipeline_cost,
estimate_stage_cost_usd,
)
def _seed_project(storage, user_id: str, project_id: str) -> None:
"""Create a minimal project with the simple_project BOM uploaded."""
proj_svc.create_project(storage, user_id, "Fixture")
# create_project returns a new id; overwrite with a known one
meta = proj_svc.create_project(storage, user_id, "Fixture2")
# Use any CSV from simple_project as the BOM
bom_candidates = list(SIMPLE_PROJECT.glob("*.csv"))
if not bom_candidates:
pytest.skip("simple_project/ has no BOM CSV")
bom_data = bom_candidates[0].read_bytes()
proj_svc.save_bom(storage, user_id, meta.id, bom_data)
def test_estimator_returns_credit_range(storage):
"""For a real BOM, estimator returns bounded credit range."""
if not SIMPLE_PROJECT.is_dir():
pytest.skip("simple_project/ not present in repo")
meta = proj_svc.create_project(storage, "u1", "Fixture")
bom_candidates = list(SIMPLE_PROJECT.glob("*.csv"))
if not bom_candidates:
pytest.skip("simple_project/ has no BOM CSV")
proj_svc.save_bom(storage, "u1", meta.id, bom_candidates[0].read_bytes())
est = estimate_pipeline_cost(storage, "u1", meta.id)
assert isinstance(est, CostEstimate)
assert est.api_cost_low <= est.api_cost_mid <= est.api_cost_high
assert est.credits_low <= est.credits_high
assert est.api_cost_mid > 0
# Breakdown should have an IC extraction entry for the project's ICs.
# (Review entries only land here when each IC has a datasheet uploaded;
# this fixture doesn't upload PDFs, so they're skipped.)
kinds = {item.kind for item in est.breakdown}
assert "ic_extraction" in kinds
def test_estimator_counts_library_cache_hits(storage, tmp_path):
"""When an IC is in the library, its extraction cost becomes $0."""
if not SIMPLE_PROJECT.is_dir():
pytest.skip("simple_project/ not present")
# Seed a project with the BOM
meta = proj_svc.create_project(storage, "u1", "Fixture")
bom_candidates = list(SIMPLE_PROJECT.glob("*.csv"))
if not bom_candidates:
pytest.skip("no BOM")
proj_svc.save_bom(storage, "u1", meta.id, bom_candidates[0].read_bytes())
before = estimate_pipeline_cost(storage, "u1", meta.id)
# Plant a library extraction for the first IC MPN the estimator saw
ic_items = [it for it in before.breakdown if it.kind == "ic_extraction"
and it.source == "api_call_estimated"]
if not ic_items:
pytest.skip("fixture has no uncached IC")
target_mpn = ic_items[0].identifier
from backend.periscopex.utils import safe_mpn
storage.write_json(
f"library/extracted/{safe_mpn(target_mpn)}.json",
{"mpn": target_mpn, "pintable": [{"number": 1, "name": "VCC"}]},
)
after = estimate_pipeline_cost(storage, "u1", meta.id)
assert after.cached_ic_count == before.cached_ic_count + 1
assert after.api_cost_mid <= before.api_cost_mid
# Savings should be at least the baseline IC cost
assert before.api_cost_mid - after.api_cost_mid >= estimate_stage_cost_usd("ic_extraction") - 0.001
def test_estimator_errors_without_bom(storage):
meta = proj_svc.create_project(storage, "u1", "No BOM")
with pytest.raises(FileNotFoundError):
estimate_pipeline_cost(storage, "u1", meta.id)