cecc6c0b41
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
5.0 KiB
Python
134 lines
5.0 KiB
Python
"""Slice M1+M3 — the §22 multi-project spine.
|
|
|
|
Migration 026 introduces the `projects` and `project_members` tables, seeds
|
|
the single `default` project (the N=1 case, §22.13), and threads a
|
|
`project_id` column onto every slug-bearing table, backfilled to `default`.
|
|
M3 retires the META_REPO startup backfill; content_repo now comes from the
|
|
registry mirror (projects.yaml in REGISTRY_REPO). These tests prove the
|
|
spine lands without disturbing the single-project app.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from test_propose_vertical import ( # noqa: F401
|
|
app_with_fake_gitea,
|
|
tmp_env,
|
|
)
|
|
|
|
# The 19 tables migration 026 threads project_id onto (docs/design/
|
|
# multi-project-spec.md §5 amendment list).
|
|
SLUG_TABLES = [
|
|
"cached_rfcs", "cached_branches", "cached_prs", "branch_visibility",
|
|
"branch_contribute_grants", "stars", "threads", "changes", "pr_seen",
|
|
"branch_chat_seen", "watches", "notifications", "actions",
|
|
"pr_resolution_branches", "funder_consents", "rfc_invitations",
|
|
"rfc_collaborators", "proposed_use_cases", "contribution_requests",
|
|
]
|
|
|
|
|
|
def test_default_project_seeded_and_backfilled(app_with_fake_gitea):
|
|
from app import db
|
|
|
|
app, _ = app_with_fake_gitea
|
|
with TestClient(app):
|
|
rows = list(db.conn().execute(
|
|
"SELECT id, name, visibility, content_repo FROM projects"
|
|
))
|
|
assert len(rows) == 1
|
|
row = rows[0]
|
|
assert row["id"] == "default"
|
|
# public preserves the pre-multi-project open-by-default posture.
|
|
assert row["visibility"] == "public"
|
|
# M3: content_repo now comes from the registry mirror (projects.yaml),
|
|
# not the retired META_REPO startup backfill.
|
|
assert row["content_repo"] == "meta"
|
|
|
|
|
|
def test_project_id_on_every_slug_table(app_with_fake_gitea):
|
|
from app import db
|
|
|
|
app, _ = app_with_fake_gitea
|
|
with TestClient(app):
|
|
for table in SLUG_TABLES:
|
|
cols = {r["name"]: r for r in db.conn().execute(
|
|
f"PRAGMA table_info({table})"
|
|
)}
|
|
assert "project_id" in cols, f"{table} missing project_id"
|
|
col = cols["project_id"]
|
|
# NOT NULL with the constant 'default' backfill default.
|
|
assert col["notnull"] == 1, f"{table}.project_id should be NOT NULL"
|
|
assert col["dflt_value"] == "'default'", f"{table}.project_id default"
|
|
|
|
|
|
def test_existing_row_backfills_to_default(app_with_fake_gitea):
|
|
"""A row inserted the old way (no project_id) lands in the default
|
|
project — the trick that keeps every pre-multi-project INSERT working."""
|
|
from app import db
|
|
|
|
app, _ = app_with_fake_gitea
|
|
with TestClient(app):
|
|
db.conn().execute(
|
|
"INSERT INTO cached_rfcs (slug, title, state) VALUES (?, ?, ?)",
|
|
("human", "Human", "active"),
|
|
)
|
|
got = db.conn().execute(
|
|
"SELECT project_id FROM cached_rfcs WHERE slug = 'human'"
|
|
).fetchone()["project_id"]
|
|
assert got == "default"
|
|
|
|
|
|
def test_project_members_table_shape(app_with_fake_gitea):
|
|
from app import db
|
|
|
|
app, _ = app_with_fake_gitea
|
|
with TestClient(app):
|
|
cols = {r["name"] for r in db.conn().execute(
|
|
"PRAGMA table_info(project_members)"
|
|
)}
|
|
assert cols == {"project_id", "user_id", "role", "granted_by", "granted_at"}
|
|
# The role CHECK rejects an unknown role.
|
|
db.conn().execute(
|
|
"INSERT INTO users (id, display_name, role) VALUES (1, 'Ben', 'owner')"
|
|
)
|
|
db.conn().execute(
|
|
"INSERT INTO project_members (project_id, user_id, role) "
|
|
"VALUES ('default', 1, 'project_admin')"
|
|
)
|
|
import sqlite3
|
|
try:
|
|
db.conn().execute(
|
|
"INSERT INTO project_members (project_id, user_id, role) "
|
|
"VALUES ('default', 1, 'nonsense')"
|
|
)
|
|
assert False, "CHECK should reject an unknown project role"
|
|
except sqlite3.IntegrityError:
|
|
pass
|
|
|
|
|
|
def test_registry_mirror_is_idempotent(app_with_fake_gitea):
|
|
"""Re-running the registry mirror is safe — it upserts (overwrites) the
|
|
projects row from projects.yaml each time without raising. M3 retirement
|
|
of seed_default_project: the registry mirror is now the sole authority."""
|
|
from app import db
|
|
|
|
app, _ = app_with_fake_gitea
|
|
with TestClient(app):
|
|
# After startup the row should have content_repo from projects.yaml.
|
|
got = db.conn().execute(
|
|
"SELECT content_repo FROM projects WHERE id = 'default'"
|
|
).fetchone()["content_repo"]
|
|
assert got == "meta"
|
|
|
|
|
|
def test_registry_repo_config_wired(app_with_fake_gitea, monkeypatch):
|
|
from app.config import load_config
|
|
|
|
monkeypatch.setenv("REGISTRY_REPO", "wiggleverse-registry")
|
|
assert load_config().registry_repo == "wiggleverse-registry"
|
|
# M3: REGISTRY_REPO is now required — absent raises RuntimeError.
|
|
monkeypatch.delenv("REGISTRY_REPO", raising=False)
|
|
import pytest
|
|
with pytest.raises(RuntimeError, match="REGISTRY_REPO"):
|
|
load_config()
|