Merge integration (ticket 06) into ticket 07
This commit is contained in:
commit
128f153f84
11 files changed
+392
-6
No files matched your search
+4
-1
@@ -25,9 +25,12 @@ uv run --with pytest --with playwright pytest tests
|
||||
- `page_check_ai/`: **Seam 1, AI web page.** Builds the React starter with npm
|
||||
(filled with test values, in a temp folder that keeps `node_modules`), serves it with
|
||||
its own `server_stub.py`, and checks bands, band filters, drafts, the approve gate
|
||||
and "page X of Y", plus every no-AI case. Tests set run and review state on the stub
|
||||
"page X of Y" and the cost page (spend tiles, coverage pill, month breakdown,
|
||||
by-hand assumptions), plus every no-AI case. Tests set run and review state on the stub
|
||||
directly (the `stub` fixture). To check a generated dashboard folder instead, set
|
||||
`PAGE_CHECK_AI_DIR=path/to/dashboard`. Needs npm on PATH, else it skips.
|
||||
- `test_pricing.py`: the AI cost function (normal, long-context, thinking tokens,
|
||||
unknown model).
|
||||
- `test_reference_scan.py`: no patient data, internal IPs, credentials or keys in any
|
||||
bundled file, and every Superbill extract carries an `EXAMPLE ONLY` banner.
|
||||
- `evals/`: **Seam 2.** Runs the skills on a fixture project with `claude -p`,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""AI cost page: spend tiles, coverage pill, month breakdown and the by-hand assumptions.
|
||||
|
||||
ROI runs are set on the server stub (stub.ROI_RUNS); the stub prices them with pricing.py.
|
||||
The test pins its own pricing table, so it holds on a generated dashboard whose table
|
||||
lists only the project's models. See rpa-dashboard-ai/templates/contract.md.
|
||||
"""
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
NOW = datetime.now(timezone.utc)
|
||||
LAST = NOW.replace(day=1) - timedelta(days=1) # a day in last month
|
||||
|
||||
|
||||
def roi_run(job_id, when, model, inp=None, out=None, thinking=0):
|
||||
return {"job_id": job_id, "document": f"{job_id}.pdf", "created_at": when.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"model": model, "input_tokens": inp, "output_tokens": out, "thinking_tokens": thinking}
|
||||
|
||||
|
||||
TABLE = {
|
||||
"test-flash": {"input": 0.30, "output": 2.50},
|
||||
"test-pro": {"input": 1.25, "output": 10.00, "long_threshold": 200_000, "input_long": 2.50, "output_long": 15.00},
|
||||
"test-lite": {"input": 0.10, "output": 0.40},
|
||||
}
|
||||
AS_OF = "2030-01-31"
|
||||
# Costs worked by hand from TABLE:
|
||||
RUNS = [
|
||||
roi_run("r1", NOW, "test-flash", 1_000_000, 200_000), # 0.30 + 0.50 = 0.80
|
||||
roi_run("r2", NOW, "test-pro", 400_000, 60_000, 40_000), # long: 1.00 + 1.50 = 2.50
|
||||
roi_run("r3", LAST, "test-lite", 1_000_000, 1_000_000), # 0.10 + 0.40 = 0.50
|
||||
roi_run("r4", LAST, "test-flash"), # no token usage recorded
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cost_page(page, stub, monkeypatch):
|
||||
monkeypatch.setattr(sys.modules["pricing"], "PRICING", TABLE) # the stub's own pricing module
|
||||
monkeypatch.setattr(stub, "PRICING_AS_OF", AS_OF)
|
||||
stub.ROI_RUNS[:] = RUNS
|
||||
page.get_by_role("navigation").get_by_role("button", name="Cost", exact=True).click()
|
||||
return page
|
||||
|
||||
|
||||
def tile(page, name):
|
||||
return page.get_by_role("group", name=name, exact=True)
|
||||
|
||||
|
||||
def test_spend_tiles_sum_the_priced_runs(cost_page):
|
||||
expect(tile(cost_page, "Total spend")).to_contain_text("$3.80")
|
||||
expect(tile(cost_page, "This month")).to_contain_text("$3.30")
|
||||
|
||||
|
||||
def test_coverage_pill_counts_priced_of_all_documents(cost_page):
|
||||
expect(cost_page.get_by_text("3 of 4 documents priced")).to_be_visible()
|
||||
|
||||
|
||||
def test_month_breakdown_groups_runs_by_month(cost_page):
|
||||
rows = cost_page.get_by_role("table", name="Spend by month").get_by_role("row")
|
||||
expect(rows).to_have_count(3) # header + two months, newest first
|
||||
expect(rows.nth(1)).to_contain_text(NOW.strftime("%b %Y"))
|
||||
expect(rows.nth(1)).to_contain_text("2 of 2")
|
||||
expect(rows.nth(1)).to_contain_text("$3.30")
|
||||
expect(rows.nth(2)).to_contain_text(LAST.strftime("%b %Y"))
|
||||
expect(rows.nth(2)).to_contain_text("1 of 2")
|
||||
expect(rows.nth(2)).to_contain_text("$0.50")
|
||||
|
||||
|
||||
def test_runs_show_tokens_and_cost_and_unpriced_is_not_zero(cost_page):
|
||||
runs = cost_page.get_by_role("table", name="Cost per run")
|
||||
r2 = runs.get_by_role("row").filter(has_text="r2.pdf")
|
||||
for text in ("test-pro", "400,000", "60,000", "40,000", "$2.50"):
|
||||
expect(r2).to_contain_text(text)
|
||||
expect(runs.get_by_role("row").filter(has_text="r4.pdf")).to_contain_text("not priced")
|
||||
|
||||
|
||||
def test_by_hand_panel_is_labelled_assumptions_and_recomputes(cost_page):
|
||||
panel = cost_page.get_by_role("region", name="Against doing it by hand")
|
||||
expect(panel).to_contain_text("assumptions")
|
||||
# 3 priced documents x 10 min = 0.5 h at $30/h = $15.00
|
||||
panel.get_by_label("Minutes per document").fill("10")
|
||||
panel.get_by_label("Hourly rate").fill("30")
|
||||
expect(panel).to_contain_text("$15.00")
|
||||
expect(cost_page.get_by_text(AS_OF)).to_be_visible() # pricing as-of date shown
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Self-check for the AI cost function: rates per million tokens, thinking bills as output,
|
||||
long-context rates above the threshold, and an unknown model is an error, never zero."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SRC = Path(__file__).resolve().parents[1] / "rpa-dashboard-ai" / "templates" / "web" / "pricing.py"
|
||||
spec = importlib.util.spec_from_file_location("pricing", SRC)
|
||||
pricing = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(pricing)
|
||||
|
||||
TABLE = {
|
||||
"flat-model": {"input": 0.30, "output": 2.50},
|
||||
"long-model": {"input": 1.25, "output": 10.00, "long_threshold": 200_000, "input_long": 2.50, "output_long": 15.00},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def table(monkeypatch):
|
||||
monkeypatch.setattr(pricing, "PRICING", TABLE)
|
||||
|
||||
|
||||
def test_normal_call_prices_input_and_output_per_million():
|
||||
# 1M in at 0.30 + 0.2M out at 2.50 = 0.30 + 0.50
|
||||
assert pricing.cost_for_call("flat-model", 1_000_000, 200_000) == pytest.approx(0.80)
|
||||
|
||||
|
||||
def test_thinking_tokens_bill_at_the_output_rate():
|
||||
# 0.1M in at 0.30 + (0.1M out + 0.3M thinking) at 2.50 = 0.03 + 1.00
|
||||
assert pricing.cost_for_call("flat-model", 100_000, 100_000, 300_000) == pytest.approx(1.03)
|
||||
|
||||
|
||||
def test_long_context_rates_apply_above_the_threshold_only():
|
||||
# at the threshold: 0.2M at 1.25 + 0.1M at 10 = 0.25 + 1.00
|
||||
assert pricing.cost_for_call("long-model", 200_000, 100_000) == pytest.approx(1.25)
|
||||
# above it: 0.4M at 2.50 + 0.1M at 15 = 1.00 + 1.50
|
||||
assert pricing.cost_for_call("long-model", 400_000, 100_000) == pytest.approx(2.50)
|
||||
|
||||
|
||||
def test_unknown_model_raises_and_names_the_known_models():
|
||||
with pytest.raises(ValueError) as e:
|
||||
pricing.cost_for_call("mystery-model", 1000, 1000)
|
||||
assert "mystery-model" in str(e.value)
|
||||
assert "flat-model" in str(e.value) and "long-model" in str(e.value)
|
||||
|
||||
|
||||
def test_bundled_table_records_its_source_and_as_of_date():
|
||||
assert pricing.PRICING_SOURCE.startswith("https://")
|
||||
assert len(pricing.PRICING_AS_OF) == 10 and pricing.PRICING_AS_OF[4] == "-"
|
||||
Reference in new issue
Block a user