mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
H2C / H2D AMS-HT units report ams_id 128+ (one ams_id per unit, single tray), but spoolman_slot_assignments.ck_ams_id_range only admitted 0-7 and 255. Every attempt to link a Spoolman spool to an AMS-HT slot died with `CHECK constraint failed: ck_ams_id_range`. The internal spool_assignment table has no such constraint and works fine. Widen the formula to (0-7) OR (128-191) OR 255 in the model, the CREATE TABLE DDL, and an idempotent in-place migration for existing installs (Postgres: DROP/ADD CONSTRAINT; SQLite: detect stale formula in sqlite_master, rebuild via _v2 rename pattern).
102 lines
4.5 KiB
Python
102 lines
4.5 KiB
Python
"""T-Gap 4: PostgreSQL DDL dialect tests for spoolman_slot_assignments table."""
|
||
|
||
import re
|
||
|
||
import pytest
|
||
|
||
|
||
def _extract_spoolman_slot_ddl(is_sqlite: bool) -> str:
|
||
"""Extract the spoolman_slot_assignments DDL from database.py."""
|
||
import inspect
|
||
from unittest.mock import patch
|
||
|
||
import backend.app.core.database as db_module
|
||
|
||
source = inspect.getsource(db_module)
|
||
# Find the block that creates spoolman_slot_assignments
|
||
start = source.find("CREATE TABLE IF NOT EXISTS spoolman_slot_assignments")
|
||
assert start != -1, "spoolman_slot_assignments DDL not found"
|
||
# Scan forward to find the matching end of the SQL string
|
||
block = source[start : start + 2000]
|
||
return block
|
||
|
||
|
||
class TestSpoolmanSlotDdl:
|
||
"""Verify the DDL for spoolman_slot_assignments contains required constraints."""
|
||
|
||
def test_sqlite_ddl_has_named_unique_constraint(self):
|
||
ddl = _extract_spoolman_slot_ddl(is_sqlite=True)
|
||
assert "uq_slot_assignment" in ddl, "Named UNIQUE constraint missing from SQLite DDL"
|
||
|
||
def test_sqlite_ddl_has_ams_id_check(self):
|
||
ddl = _extract_spoolman_slot_ddl(is_sqlite=True)
|
||
# ams_id allows 0–7 for physical AMS units and 255 for external slot
|
||
assert re.search(r"ams_id.*CHECK.*ams_id.*>=.*0.*AND.*ams_id.*<=.*7.*OR.*ams_id.*=.*255", ddl, re.DOTALL), (
|
||
"ams_id CHECK constraint missing from SQLite DDL"
|
||
)
|
||
|
||
def test_sqlite_ddl_has_tray_id_check(self):
|
||
ddl = _extract_spoolman_slot_ddl(is_sqlite=True)
|
||
assert re.search(r"tray_id.*CHECK.*tray_id.*>=.*0.*AND.*tray_id.*<=.*3", ddl, re.DOTALL), (
|
||
"tray_id CHECK constraint missing from SQLite DDL"
|
||
)
|
||
|
||
def test_postgres_ddl_has_named_unique_constraint(self):
|
||
ddl = _extract_spoolman_slot_ddl(is_sqlite=False)
|
||
# PostgreSQL DDL appears after the SQLite block
|
||
pg_start = ddl.find("SERIAL PRIMARY KEY")
|
||
assert pg_start != -1 or "uq_slot_assignment" in ddl, "uq_slot_assignment not found in DDL"
|
||
|
||
def test_orm_model_has_named_unique_constraint(self):
|
||
from sqlalchemy import inspect as sa_inspect
|
||
|
||
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
||
|
||
table = SpoolmanSlotAssignment.__table__
|
||
constraint_names = {c.name for c in table.constraints}
|
||
assert "uq_slot_assignment" in constraint_names, (
|
||
f"uq_slot_assignment not in ORM constraints: {constraint_names}"
|
||
)
|
||
|
||
def test_orm_model_has_ams_id_check_constraint(self):
|
||
from sqlalchemy import CheckConstraint
|
||
|
||
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
||
|
||
table = SpoolmanSlotAssignment.__table__
|
||
check_names = {c.name for c in table.constraints if isinstance(c, CheckConstraint)}
|
||
assert "ck_ams_id_range" in check_names, f"ck_ams_id_range not in ORM check constraints: {check_names}"
|
||
|
||
def test_orm_model_has_tray_id_check_constraint(self):
|
||
from sqlalchemy import CheckConstraint
|
||
|
||
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
||
|
||
table = SpoolmanSlotAssignment.__table__
|
||
check_names = {c.name for c in table.constraints if isinstance(c, CheckConstraint)}
|
||
assert "ck_tray_id_range" in check_names, f"ck_tray_id_range not in ORM check constraints: {check_names}"
|
||
|
||
def test_ams_id_check_admits_ams_ht_range(self):
|
||
"""#1274: ck_ams_id_range must accept AMS-HT (ams_id 128-191).
|
||
|
||
H2C / H2D AMS-HT units report ams_id starting at 128 (one ams_id per
|
||
unit, single tray). The pre-fix constraint only allowed 0-7 and 255,
|
||
so every AMS-HT slot link failed with CHECK violation. Both the ORM
|
||
formula and the SQLite/Postgres DDL strings must include the new range.
|
||
"""
|
||
from sqlalchemy import CheckConstraint
|
||
|
||
from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
|
||
|
||
table = SpoolmanSlotAssignment.__table__
|
||
ams_check = next(c for c in table.constraints if isinstance(c, CheckConstraint) and c.name == "ck_ams_id_range")
|
||
formula = str(ams_check.sqltext)
|
||
assert "128" in formula and "191" in formula, (
|
||
f"ck_ams_id_range formula does not cover AMS-HT range: {formula!r}"
|
||
)
|
||
|
||
# Same check at the raw-DDL level so a stale DDL definition can't
|
||
# silently ship with the loosened ORM formula.
|
||
ddl = _extract_spoolman_slot_ddl(is_sqlite=True)
|
||
assert "128" in ddl and "191" in ddl, "AMS-HT range missing from CREATE TABLE DDL"
|