主站 - 新增 m0006 invite_codes 迁移;注册强制邀请码(首个管理员除外),消码与建号 同一事务,并发提交只有一个能成功 - 新增 /api/hub-admin/* 服务端点(共享 HUB_ADMIN_TOKEN,先于鉴权校验),供数据 中枢桥接读写会话/密码/模型池/会员/邀请码,并提供供应商模型列表拉取 - 前端:注册表单加邀请码(桌面 login、index.html、移动端);「系统管理」改为 「数据中枢」入口指向 8766,原模型池与会员管理分区移除,仅留「行情管理」; 随之清理陈旧 CSS 数据中枢 - 取消独立账号:删除 hub_admin/hub_sessions 与登录、改密、锁定逻辑,改为校验 主站 xiaobai_session,仅管理员可进,CSRF 由会话派生,危险操作二次确认走主站 - 控制台新增数据源凭证可编辑区(原有内容一项不删)、供应商制模型池(自动拉取 /models,失败退回卡内手动录入)、会员管理与邀请码页 - 日夜双主题:颜色收敛为同名 token 换值,SVG 改用 inline style 以吃到变量 自测 - 主站 verify_baseline 通过(498 项);数据中枢 235 项通过 - tools/verify_datahub_console.py 端到端跑通两服务真实对话; tools/verify_datahub_console_ui.py 浏览器跑通门禁/凭证/模型池/会员/主题/1030 窄屏 Co-authored-by: multica-agent <github@multica.ai>
156 lines
6.3 KiB
Python
156 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from backend.database import Migration, MigrationError, MigrationRunner
|
|
from database import ReviewDatabase
|
|
|
|
|
|
class DatabaseMigrationTests(unittest.TestCase):
|
|
def test_fresh_database_records_the_adopted_schema_once(self) -> None:
|
|
with tempfile.TemporaryDirectory() as root:
|
|
path = Path(root) / "review.db"
|
|
database = ReviewDatabase(path)
|
|
with database.connect() as connection:
|
|
rows = connection.execute(
|
|
"SELECT version, name FROM schema_migrations"
|
|
).fetchall()
|
|
self.assertEqual(
|
|
[(row["version"], row["name"]) for row in rows],
|
|
[
|
|
("0001", "adopt_legacy_schema"),
|
|
("0002", "create_job_runs"),
|
|
("0003", "extend_llm_audit"),
|
|
("0004", "add_mentor_note"),
|
|
("0005", "create_account_switch_grants"),
|
|
("0006", "create_invite_codes"),
|
|
],
|
|
)
|
|
columns = {
|
|
str(row["name"])
|
|
for row in connection.execute(
|
|
"PRAGMA table_info(mentor_preferences)"
|
|
)
|
|
}
|
|
self.assertIn("note", columns)
|
|
ReviewDatabase(path)
|
|
with database.connect() as connection:
|
|
count = connection.execute(
|
|
"SELECT COUNT(*) AS count FROM schema_migrations"
|
|
).fetchone()["count"]
|
|
self.assertEqual(count, 6)
|
|
|
|
def test_database_with_recorded_0004_and_note_column_starts_without_reapply(
|
|
self,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory() as root:
|
|
path = Path(root) / "review.db"
|
|
database = ReviewDatabase(path)
|
|
with database.connect() as connection:
|
|
note_rows = [
|
|
str(row["name"])
|
|
for row in connection.execute(
|
|
"PRAGMA table_info(mentor_preferences)"
|
|
)
|
|
]
|
|
self.assertIn("note", note_rows)
|
|
ReviewDatabase(path)
|
|
with database.connect() as connection:
|
|
count = connection.execute(
|
|
"SELECT COUNT(*) AS count FROM schema_migrations"
|
|
).fetchone()["count"]
|
|
self.assertEqual(count, 6)
|
|
|
|
def test_old_database_without_0004_upgrades_and_adds_note_column(self) -> None:
|
|
with tempfile.TemporaryDirectory() as root:
|
|
path = Path(root) / "review.db"
|
|
database = ReviewDatabase(path)
|
|
with database.connect() as connection:
|
|
connection.execute(
|
|
"DELETE FROM schema_migrations WHERE version = '0004'"
|
|
)
|
|
connection.execute(
|
|
"ALTER TABLE mentor_preferences DROP COLUMN note"
|
|
)
|
|
ReviewDatabase(path)
|
|
with database.connect() as connection:
|
|
versions = {
|
|
str(row["version"])
|
|
for row in connection.execute(
|
|
"SELECT version FROM schema_migrations"
|
|
)
|
|
}
|
|
note_rows = [
|
|
str(row["name"])
|
|
for row in connection.execute(
|
|
"PRAGMA table_info(mentor_preferences)"
|
|
)
|
|
]
|
|
self.assertEqual(versions, {"0001", "0002", "0003", "0004", "0005", "0006"})
|
|
self.assertIn("note", note_rows)
|
|
|
|
def test_database_with_unknown_migration_is_rejected(self) -> None:
|
|
with tempfile.TemporaryDirectory() as root:
|
|
path = Path(root) / "review.db"
|
|
database = ReviewDatabase(path)
|
|
with database.connect() as connection:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO schema_migrations
|
|
(version, name, checksum, applied_at)
|
|
VALUES ('9999', 'unknown_legacy', 'x', '2026-08-01T00:00:00+00:00')
|
|
"""
|
|
)
|
|
with self.assertRaises(MigrationError):
|
|
ReviewDatabase(path)
|
|
|
|
def test_connection_factory_enables_required_pragmas(self) -> None:
|
|
with tempfile.TemporaryDirectory() as root:
|
|
database = ReviewDatabase(Path(root) / "review.db")
|
|
with database.connect() as connection:
|
|
self.assertEqual(connection.execute("PRAGMA foreign_keys").fetchone()[0], 1)
|
|
self.assertEqual(connection.execute("PRAGMA journal_mode").fetchone()[0], "wal")
|
|
self.assertEqual(connection.execute("PRAGMA busy_timeout").fetchone()[0], 20000)
|
|
|
|
def test_failed_migration_rolls_back_and_is_not_recorded(self) -> None:
|
|
connection = sqlite3.connect(":memory:")
|
|
self.addCleanup(connection.close)
|
|
connection.row_factory = sqlite3.Row
|
|
|
|
def fail(conn: sqlite3.Connection) -> None:
|
|
conn.execute("CREATE TABLE should_rollback (id INTEGER)")
|
|
raise RuntimeError("stop")
|
|
|
|
migration = Migration("9000", "failure", fail, "failure:v1")
|
|
with self.assertRaises(MigrationError):
|
|
MigrationRunner().apply(connection, (migration,))
|
|
tables = {
|
|
row["name"]
|
|
for row in connection.execute(
|
|
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
|
)
|
|
}
|
|
self.assertNotIn("should_rollback", tables)
|
|
self.assertEqual(
|
|
connection.execute("SELECT COUNT(*) FROM schema_migrations").fetchone()[0],
|
|
0,
|
|
)
|
|
|
|
def test_applied_migration_checksum_is_immutable(self) -> None:
|
|
connection = sqlite3.connect(":memory:")
|
|
self.addCleanup(connection.close)
|
|
connection.row_factory = sqlite3.Row
|
|
first = Migration("9001", "example", lambda conn: None, "example:v1")
|
|
changed = Migration("9001", "example", lambda conn: None, "example:v2")
|
|
runner = MigrationRunner()
|
|
runner.apply(connection, (first,))
|
|
with self.assertRaises(MigrationError):
|
|
runner.apply(connection, (changed,))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|