Files
2026-07-13 13:12:33 +08:00

151 lines
4.6 KiB
Python

"""V014 migration: allow user_input rows in meta_skill_run_steps."""
from __future__ import annotations
import sqlite3
from pathlib import Path
import pytest
from yoyo import get_backend, read_migrations
from opensquilla.persistence.migrator import apply_pending
MIGRATIONS_DIR = Path(__file__).resolve().parents[2] / "migrations"
def _open_conn(db_path: str) -> sqlite3.Connection:
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA foreign_keys = ON")
return conn
def _insert_run(conn: sqlite3.Connection, run_id: str = "r1") -> None:
conn.execute(
"""
INSERT INTO meta_skill_runs (
run_id, meta_skill_name, meta_skill_digest, plan_snapshot_json,
triggered_by, status, started_at_ms, inputs_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
run_id, "meta-travel-planner", "deadbeef", "{}",
"soft_meta_invoke", "running", 1_000_000, "{}",
),
)
conn.commit()
def _insert_step(conn: sqlite3.Connection, kind: str, step_id: str = "s1") -> None:
conn.execute(
"""
INSERT INTO meta_skill_run_steps (
run_id, step_id, step_kind, declared_skill, effective_skill,
status, started_at_ms, rendered_inputs_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"r1", step_id, kind, "trip_collect", "trip_collect",
"running", 1_000_001, "{}",
),
)
conn.commit()
def _column_names(conn: sqlite3.Connection, table: str) -> set[str]:
return {row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
def test_v014_accepts_user_input_step_kind(tmp_path: Path) -> None:
db = str(tmp_path / "v014.db")
applied = apply_pending(db, MIGRATIONS_DIR)
assert "V014__meta_skill_run_steps_allow_user_input" in applied
conn = _open_conn(db)
try:
_insert_run(conn)
_insert_step(conn, "user_input")
row = conn.execute(
"SELECT step_kind FROM meta_skill_run_steps WHERE step_id = 's1'"
).fetchone()
assert row == ("user_input",)
finally:
conn.close()
def test_v014_keeps_prior_step_kinds_and_rejects_unknown(tmp_path: Path) -> None:
db = str(tmp_path / "v014_existing.db")
apply_pending(db, MIGRATIONS_DIR)
conn = _open_conn(db)
try:
_insert_run(conn)
for idx, kind in enumerate(
("agent", "llm_classify", "llm_chat", "tool_call", "skill_exec")
):
_insert_step(conn, kind, step_id=f"s{idx}")
with pytest.raises(sqlite3.IntegrityError):
_insert_step(conn, "made_up", step_id="bad")
finally:
conn.close()
def test_v015_rollback_then_v014_rollback_recreates_step_table(tmp_path: Path) -> None:
db = str(tmp_path / "v015_v014_rollback.db")
apply_pending(db, MIGRATIONS_DIR)
conn = _open_conn(db)
try:
_insert_run(conn)
_insert_step(conn, "agent")
assert "usage_json" in _column_names(conn, "meta_skill_run_steps")
finally:
conn.close()
backend = get_backend(f"sqlite:///{db}")
migrations = read_migrations(str(MIGRATIONS_DIR))
by_id = {migration.id: migration for migration in migrations}
backend.rollback_migrations([by_id["V015__meta_skill_step_usage"]])
conn = _open_conn(db)
try:
assert "usage_json" not in _column_names(conn, "meta_skill_run_steps")
finally:
conn.close()
backend.rollback_migrations([
by_id["V014__meta_skill_run_steps_allow_user_input"]
])
conn = _open_conn(db)
try:
row = conn.execute(
"SELECT step_id, step_kind FROM meta_skill_run_steps WHERE step_id='s1'"
).fetchone()
assert row == ("s1", "agent")
with pytest.raises(sqlite3.IntegrityError):
_insert_step(conn, "user_input", step_id="after-rollback")
finally:
conn.close()
def test_v014_rollback_blocks_when_user_input_step_rows_exist(tmp_path: Path) -> None:
db = str(tmp_path / "v014_user_input_rollback.db")
apply_pending(db, MIGRATIONS_DIR)
conn = _open_conn(db)
try:
_insert_run(conn)
_insert_step(conn, "user_input")
finally:
conn.close()
backend = get_backend(f"sqlite:///{db}")
migrations = read_migrations(str(MIGRATIONS_DIR))
by_id = {migration.id: migration for migration in migrations}
backend.rollback_migrations([by_id["V015__meta_skill_step_usage"]])
with pytest.raises(RuntimeError, match="user_input"):
backend.rollback_migrations([
by_id["V014__meta_skill_run_steps_allow_user_input"]
])