test(fase3): SC1 new project via config-only + no-hardcoded-id src guard

This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 12:13:44 +02:00
commit 0020776b26
3 changed files with 53 additions and 0 deletions

View file

@ -0,0 +1,5 @@
SYNTHETIC fixture (D4) — AI-authored, not verified domain content.
Cost saving measure candidate for Skole VVS-oppgradering:
Reducing the scope on the ventilation duct network (code 63.2) lowers the cost. Scope
reduction on the ventilation works is the candidate cost-saving measure to validate.

View file

@ -47,6 +47,21 @@
{"code": "87.3", "description": "Fuktisolering membran", "quantity": 640, "unit": "m2", "unit_cost": 980},
{"code": "75.1", "description": "Rekkverk stal galvanisert", "quantity": 210, "unit": "m", "unit_cost": 2650}
]
},
{
"id": "SKOLE-VVS-OPPGR",
"name": "Skole VVS-oppgradering",
"description": "Oppgradering av ventilasjon, sanitær og varme på eksisterende skole. Fiktivt referanseprosjekt (lagt til via config — SC1).",
"currency": "NOK",
"docs_dir": "docs/SKOLE-VVS-OPPGR",
"verdict_input": {"decision": "approved", "rationale": "Ventilation duct scope reduction is feasible within the estimate range (SYNTHETIC / not verified)."},
"cost_items": [
{"code": "01.1", "description": "Rigg og drift", "quantity": 1, "unit": "rs", "unit_cost": 480000},
{"code": "62.1", "description": "Ventilasjonsaggregat", "quantity": 4, "unit": "stk", "unit_cost": 185000},
{"code": "63.2", "description": "Kanalnett ventilasjon", "quantity": 1200, "unit": "m", "unit_cost": 1450},
{"code": "64.3", "description": "Sanitæranlegg bad og wc", "quantity": 1, "unit": "rs", "unit_cost": 920000},
{"code": "65.1", "description": "Varmeanlegg vannbåren", "quantity": 1, "unit": "rs", "unit_cost": 760000}
]
}
]
}

View file

@ -178,3 +178,36 @@ async def test_d_both_profiles_run_offline(
# Profile-dependent teeth (offline, no egress): each profile resolves its own model.
assert resolve_model(profile, "proposer")
assert resolve_model("local", "proposer") != resolve_model("azure", "proposer")
async def test_e_new_project_flows_through_via_config_only(
make_portfolio_client_factory, fresh_store
) -> None:
"""SC1: a brand-new project (SKOLE-VVS-OPPGR) added by CONFIG ONLY — a JSON entry + a bundled
docs folder, zero ``src/*.py`` change flows end-to-end through run_portfolio. Its id is not
in REPLIES, so the default reply (which omits project_id) inherits the new project's id via
``_parse_ir`` setdefault, proving the loader wired the config-only project through."""
result = await run_portfolio(
["SKOLE-VVS-OPPGR"],
"local",
store=fresh_store,
client_factory=make_portfolio_client_factory(REPLIES),
)
assert len(result.runs) == 1
assert result.runs[0].outcome.proposal.project_id == "SKOLE-VVS-OPPGR"
def test_f_no_hardcoded_project_ids_in_src() -> None:
"""SC1 load-bearing guard: reference-project ids live ONLY in the data JSON, never in code.
If a future project is onboarded via a code-side id->path/verdict mapping, this reddens
backing the 'config-only' claim. Pattern: test_budget.py:88-99 (src anti-pattern grep)."""
from pathlib import Path
ids = ("FV42-GSV-E1", "RV13-RAS-TP", "BRU-LAKS-REHAB", "SKOLE-VVS-OPPGR")
offenders = [
f"{py.name}: {pid}"
for py in Path("src/portfolio_optimiser").rglob("*.py")
for pid in ids
if pid in py.read_text(encoding="utf-8")
]
assert offenders == [], f"hardcoded project ids in src: {offenders}"