mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
dbf389783e
Built-in persona base prompts move from inline DB text / base.md into prompts/personas/<slug>.md — code-owned, PR-reviewable, drift-proof. base.md / base_coordinator.md become personas/engineer.md / orchestrator.md. Prompt source is now explicit in storage instead of inferred in app logic: a new base_prompt_file column plus CHECK (base_prompt IS NOT NULL OR base_prompt_file IS NOT NULL) — two nullable columns, never both empty. Resolution is a coalesce (base_prompt else load(base_prompt_file)), frozen into the workstream stamp at creation. base_prompt_file marks a persona as built-in (code-only, un-archivable); an operator override on a built-in is allowed and wins over the file. "Inherit the kind default" is a workstream-creation act (is_default), not a persona-row state. Migration 063: - seeds reference their file (base_prompt NULL); no runtime file reads — the backfill's frozen prompt text is inlined as a point-in-time snapshot so migration history stays self-contained and reproducible. - every existing workstream is stamped by kind (creative -> writer, else the kind default), set-based (INSERT..SELECT via temp tables) with the persona column added after the bulk writes to shorten its lock window. Storage guards (both backends): operators must supply base_prompt; built-ins can't be archived or have base_prompt_file set via the API; clearing an operator persona's only source is rejected. Follow-ups reviewed alongside (#756): soft-set visibility docstring scoped to per-process; _apply_persona_snapshot / _current_persona_snapshot own the stamp round-trip; spawn approval-header args (skill/name/target_node) flattened+capped like persona; server-side tool injection generalized to replace-only (client-def gated, incl. the xAI include forwarding). Seed copy revised (researcher soft; de-costumed prose; engineer de-biased). New test_schema_parity asserts create_all matches the alembic head. Closes #683 groundwork; ruff + strict mypy clean, full suite green.
88 lines
3.6 KiB
Python
88 lines
3.6 KiB
Python
"""Schema parity: `metadata.create_all` must match `alembic upgrade head`.
|
|
|
|
The codebase defines its schema twice — `_schema.py` (the SQLAlchemy metadata
|
|
that `create_all` builds, used for fast ephemeral test DBs and
|
|
``SQLiteBackend(create_tables=True)``) and the Alembic migration chain (which
|
|
builds production DBs incrementally). They are kept in sync BY HAND.
|
|
|
|
Nothing else enforces that they agree, so a column added to a migration but not
|
|
to `_schema.py` (or the reverse) would silently give `create_all`-based tests a
|
|
different schema than production — and most tests use `create_all`, so a
|
|
migration bug could pass CI unnoticed. This test is that enforcement: it fails
|
|
the moment the two paths drift on a table, column, or named constraint.
|
|
|
|
(It does NOT check seed DATA: `create_all` builds structure only, so migration
|
|
seeds — e.g. the built-in personas — exist only on migrated DBs. Tests that
|
|
need seed rows must run migrations or seed explicitly; that gap is by design.)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import command
|
|
from alembic.config import Config
|
|
|
|
_MIGRATIONS = str(Path(__file__).resolve().parent.parent / "turnstone/core/storage/migrations")
|
|
|
|
|
|
def _inspect_migrated(db_path: Path) -> sa.Inspector:
|
|
cfg = Config()
|
|
cfg.set_main_option("script_location", _MIGRATIONS)
|
|
cfg.set_main_option("sqlalchemy.url", f"sqlite:///{db_path}")
|
|
command.upgrade(cfg, "head")
|
|
return sa.inspect(sa.create_engine(f"sqlite:///{db_path}"))
|
|
|
|
|
|
def _inspect_create_all(db_path: Path) -> sa.Inspector:
|
|
from turnstone.core.storage._schema import metadata
|
|
|
|
engine = sa.create_engine(f"sqlite:///{db_path}")
|
|
metadata.create_all(engine)
|
|
return sa.inspect(engine)
|
|
|
|
|
|
def test_create_all_matches_migrations(tmp_path: Path) -> None:
|
|
mig = _inspect_migrated(tmp_path / "migrated.db")
|
|
meta = _inspect_create_all(tmp_path / "create_all.db")
|
|
|
|
mig_tables = set(mig.get_table_names()) - {"alembic_version"}
|
|
meta_tables = set(meta.get_table_names())
|
|
assert mig_tables == meta_tables, (
|
|
f"table drift — only in migrations: {sorted(mig_tables - meta_tables)}; "
|
|
f"only in create_all: {sorted(meta_tables - mig_tables)}"
|
|
)
|
|
|
|
col_drift: dict[str, dict[str, list[str]]] = {}
|
|
check_drift: dict[str, dict[str, list[str]]] = {}
|
|
for t in sorted(mig_tables):
|
|
mc = {c["name"] for c in mig.get_columns(t)}
|
|
ec = {c["name"] for c in meta.get_columns(t)}
|
|
if mc != ec:
|
|
col_drift[t] = {
|
|
"only_migrations": sorted(mc - ec),
|
|
"only_create_all": sorted(ec - mc),
|
|
}
|
|
# Named CHECK constraints only — unnamed ones reflect as backend noise.
|
|
mck = {c["name"] for c in mig.get_check_constraints(t) if c.get("name")}
|
|
eck = {c["name"] for c in meta.get_check_constraints(t) if c.get("name")}
|
|
if mck != eck:
|
|
check_drift[t] = {
|
|
"only_migrations": sorted(mck - eck),
|
|
"only_create_all": sorted(eck - mck),
|
|
}
|
|
|
|
assert not col_drift, f"column drift: {col_drift}"
|
|
assert not check_drift, f"check-constraint drift: {check_drift}"
|
|
|
|
|
|
def test_personas_prompt_source_check_present_on_both_paths(tmp_path: Path) -> None:
|
|
# Guards the personas feature specifically: the base_prompt/base_prompt_file
|
|
# source CHECK must exist on BOTH build paths, not just the one under test.
|
|
mig = _inspect_migrated(tmp_path / "m.db")
|
|
meta = _inspect_create_all(tmp_path / "c.db")
|
|
for insp in (mig, meta):
|
|
names = {c.get("name") for c in insp.get_check_constraints("personas")}
|
|
assert "ck_personas_prompt_source" in names
|