"""V1-0 write-time invariant tests. Covers the Engineering V1 completion plan Phase V1-0 acceptance: - F-1 shared-header fields: extractor_version + canonical_home + hand_authored land in the entities table with working defaults - F-8 provenance enforcement: create_entity raises without source_refs unless hand_authored=True - F-5 synchronous conflict-detection hook on any active-entity write (create_entity with status="active" + the pre-existing promote_entity path); fail-open per conflict-model.md:256 - Q-3 "flag, never block": a conflict never 4xx-blocks the write - Q-4 partial trust: get_entities scope_only filters candidates out Plan: docs/plans/engineering-v1-completion-plan.md Spec: docs/architecture/engineering-v1-acceptance.md """ from __future__ import annotations import pytest from atocore.engineering.service import ( EXTRACTOR_VERSION, create_entity, get_entities, get_entity, init_engineering_schema, ) from atocore.models.database import get_connection, init_db # ---------- F-1: shared-header fields ---------- def test_entity_row_has_shared_header_fields(tmp_data_dir): init_db() init_engineering_schema() with get_connection() as conn: cols = {row["name"] for row in conn.execute("PRAGMA table_info(entities)").fetchall()} assert "extractor_version" in cols assert "canonical_home" in cols assert "hand_authored" in cols def test_created_entity_has_default_extractor_version_and_canonical_home(tmp_data_dir): init_db() init_engineering_schema() e = create_entity( entity_type="component", name="Pivot Pin", project="p04-gigabit", source_refs=["test:fixture"], ) assert e.extractor_version == EXTRACTOR_VERSION assert e.canonical_home == "entity" assert e.hand_authored is False # round-trip through get_entity to confirm the row mapper returns # the same values (not just the return-by-construct path) got = get_entity(e.id) assert got is not None assert got.extractor_version == EXTRACTOR_VERSION assert got.canonical_home == "entity" assert got.hand_authored is False def test_explicit_extractor_version_is_persisted(tmp_data_dir): init_db() init_engineering_schema() e = create_entity( entity_type="decision", name="Pick GF-PTFE pads", project="p04-gigabit", source_refs=["interaction:abc"], extractor_version="custom-v2.3", ) got = get_entity(e.id) assert got.extractor_version == "custom-v2.3" # ---------- F-8: provenance enforcement ---------- def test_create_entity_without_provenance_raises(tmp_data_dir): init_db() init_engineering_schema() with pytest.raises(ValueError, match="source_refs required"): create_entity( entity_type="component", name="No Provenance", project="p04-gigabit", hand_authored=False, # explicit — bypasses the test-conftest auto-flag ) def test_create_entity_with_hand_authored_needs_no_source_refs(tmp_data_dir): init_db() init_engineering_schema() e = create_entity( entity_type="component", name="Human Entry", project="p04-gigabit", hand_authored=True, ) assert e.hand_authored is True got = get_entity(e.id) assert got.hand_authored is True # source_refs stays empty — the hand_authored flag IS the provenance assert got.source_refs == [] def test_create_entity_with_empty_source_refs_list_is_treated_as_missing(tmp_data_dir): init_db() init_engineering_schema() with pytest.raises(ValueError, match="source_refs required"): create_entity( entity_type="component", name="Empty Refs", project="p04-gigabit", source_refs=[], hand_authored=False, ) # ---------- F-5: synchronous conflict-detection hook ---------- def test_active_create_runs_conflict_detection_hook(tmp_data_dir, monkeypatch): """status=active writes trigger detect_conflicts_for_entity.""" init_db() init_engineering_schema() called_with: list[str] = [] def _fake_detect(entity_id: str): called_with.append(entity_id) return [] import atocore.engineering.conflicts as conflicts_mod monkeypatch.setattr(conflicts_mod, "detect_conflicts_for_entity", _fake_detect) e = create_entity( entity_type="component", name="Active With Hook", project="p04-gigabit", source_refs=["test:hook"], status="active", ) assert called_with == [e.id] def test_candidate_create_does_not_run_conflict_hook(tmp_data_dir, monkeypatch): """status=candidate writes do NOT trigger detection — the hook is for active rows only, per V1-0 scope. Candidates are checked at promote time.""" init_db() init_engineering_schema() called: list[str] = [] def _fake_detect(entity_id: str): called.append(entity_id) return [] import atocore.engineering.conflicts as conflicts_mod monkeypatch.setattr(conflicts_mod, "detect_conflicts_for_entity", _fake_detect) create_entity( entity_type="component", name="Candidate No Hook", project="p04-gigabit", source_refs=["test:cand"], status="candidate", ) assert called == [] # ---------- Q-3: flag, never block ---------- def test_conflict_detector_failure_does_not_block_write(tmp_data_dir, monkeypatch): """Per conflict-model.md:256: detection errors must not fail the write. The entity is still created; only a warning is logged.""" init_db() init_engineering_schema() def _boom(entity_id: str): raise RuntimeError("synthetic detector failure") import atocore.engineering.conflicts as conflicts_mod monkeypatch.setattr(conflicts_mod, "detect_conflicts_for_entity", _boom) # The write still succeeds — no exception propagates. e = create_entity( entity_type="component", name="Hook Fails Open", project="p04-gigabit", source_refs=["test:failopen"], status="active", ) assert get_entity(e.id) is not None # ---------- Q-4 (partial): trust-hierarchy — scope_only filters candidates ---------- def test_scope_only_active_does_not_return_candidates(tmp_data_dir): """V1-0 partial Q-4: active-scoped listing never returns candidates. Full trust-hierarchy coverage (no-auto-project-state, etc.) ships in V1-E per plan.""" init_db() init_engineering_schema() active = create_entity( entity_type="component", name="Active Alpha", project="p04-gigabit", source_refs=["test:alpha"], status="active", ) candidate = create_entity( entity_type="component", name="Candidate Beta", project="p04-gigabit", source_refs=["test:beta"], status="candidate", ) listed = get_entities(project="p04-gigabit", status="active", scope_only=True) ids = {e.id for e in listed} assert active.id in ids assert candidate.id not in ids