"""InstanceRepo against real SQL.""" from __future__ import annotations import pytest from svcforge_core.domain.models import TaskKind from svcforge_core.domain.states import InstanceState from svcforge_core.repo.db import DictPool from svcforge_core.repo.instances import InstanceRepo from svcforge_core.repo.tasks import TaskRepo from tests.integration.helpers import build_instance async def test_create_then_get_round_trips(pool: DictPool) -> None: repo = InstanceRepo(pool) inst = build_instance() async with pool.connection() as conn: created = await repo.create(conn, inst) assert created.id == inst.id assert created.release_name == inst.release_name got = await repo.get(inst.id, team="platform") assert got is not None assert got.service_type == "elasticsearch" assert got.state is InstanceState.REQUESTED # timestamptz round-trips as aware, or every later comparison raises TypeError. assert got.created_at.tzinfo is not None async def test_get_by_other_team_is_none_not_403(pool: DictPool) -> None: """A wrong-team id is indistinguishable from a missing one.""" repo = InstanceRepo(pool) inst = build_instance(team="platform") async with pool.connection() as conn: await repo.create(conn, inst) assert await repo.get(inst.id, team="quant") is None async def test_list_is_filtered_by_team(pool: DictPool) -> None: repo = InstanceRepo(pool) async with pool.connection() as conn: await repo.create(conn, build_instance(team="platform")) await repo.create(conn, build_instance(team="quant")) mine = await repo.list(team="platform") assert len(mine) == 1 assert all(i.team == "platform" for i in mine) async def test_update_state_cas_rejects_stale_expectation(pool: DictPool) -> None: repo = InstanceRepo(pool) inst = build_instance() async with pool.connection() as conn: await repo.create(conn, inst) ok = await repo.update_state(inst.id, InstanceState.REQUESTED, InstanceState.PROVISIONING) assert ok is True # The row already moved: the second caller must lose, and must not raise. lost = await repo.update_state(inst.id, InstanceState.REQUESTED, InstanceState.PROVISIONING) assert lost is False async def test_update_state_sets_endpoint(pool: DictPool) -> None: repo = InstanceRepo(pool) inst = build_instance() async with pool.connection() as conn: await repo.create(conn, inst) await repo.update_state(inst.id, InstanceState.REQUESTED, InstanceState.PROVISIONING) ok = await repo.update_state( inst.id, InstanceState.PROVISIONING, InstanceState.READY, endpoint="http://es:9200" ) assert ok is True got = await repo.get(inst.id, team="platform") assert got is not None assert got.endpoint == "http://es:9200" assert got.state is InstanceState.READY @pytest.mark.parametrize("explode", [True]) async def test_instance_and_task_roll_back_together(pool: DictPool, explode: bool) -> None: """The reason the queue is in Postgres, as an executable claim. If the transaction aborts, BOTH the instance and its provision task must vanish. An instance with no task never gets built; a task with no instance is an orphan. """ instances, tasks = InstanceRepo(pool), TaskRepo(pool) inst = build_instance() with pytest.raises(RuntimeError): async with pool.connection() as conn, conn.transaction(): await instances.create(conn, inst) await tasks.enqueue(conn, inst.id, TaskKind.PROVISION) if explode: raise RuntimeError("boom, mid-transaction") assert await instances.get(inst.id, team="platform") is None async with pool.connection() as conn, conn.cursor() as cur: await cur.execute("select count(*) as n from tasks") row = await cur.fetchone() assert row is not None assert row["n"] == 0