Files

101 lines
4.1 KiB
Python

"""tests/test_db.py —— 三表迁移幂等 + JSON 编解码。"""
from __future__ import annotations
import asyncio
import pytest
from app.db import Database, dumps_json, loads_json
pytestmark = pytest.mark.bioflow_unit
def _run(coro):
return asyncio.new_event_loop().run_until_complete(coro)
def _tables(db):
return {r[0] for r in _run(db.fetchall("SELECT name FROM sqlite_master WHERE type='table'"))}
def test_schema_created(db):
tables = _tables(db)
assert {"bio_jobs", "bio_stages", "bio_archives"} <= tables
def test_migration_idempotent(tmp_path):
"""TC-BIO-DB:重复 connect 幂等(不报错不重复建表)。"""
path = tmp_path / "a" / "bioflow.db"
db1 = Database(path)
_run(db1.connect())
_run(db1.close())
db2 = Database(path)
_run(db2.connect())
tables = _tables(db2)
assert {"bio_jobs", "bio_stages", "bio_archives"} <= tables
_run(db2.close())
def test_incremental_column_migration(tmp_path):
"""老库(无增量列)connect 后补列(bio_jobs.error / bio_archives.report_artifact)。"""
import sqlite3
path = tmp_path / "old" / "bioflow.db"
path.parent.mkdir(parents=True)
conn = sqlite3.connect(path)
conn.execute("""CREATE TABLE bio_jobs (
id TEXT PRIMARY KEY, session_id TEXT NOT NULL DEFAULT '', title TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'created', stage_chain TEXT NOT NULL DEFAULT '[]',
params TEXT NOT NULL DEFAULT '{}', auto_continue INTEGER NOT NULL DEFAULT 0,
progress INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)""")
conn.execute("""CREATE TABLE bio_stages (
id TEXT PRIMARY KEY, job_id TEXT NOT NULL REFERENCES bio_jobs(id) ON DELETE CASCADE,
stage TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'waiting', params TEXT NOT NULL DEFAULT '{}',
manifest_path TEXT, log_tail TEXT NOT NULL DEFAULT '', attempt INTEGER NOT NULL DEFAULT 0,
started_at TEXT, finished_at TEXT, UNIQUE(job_id, stage))""")
conn.execute("""CREATE TABLE bio_archives (
id TEXT PRIMARY KEY, job_id TEXT NOT NULL, name TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '', stage_chain TEXT NOT NULL DEFAULT '[]',
params TEXT NOT NULL DEFAULT '{}', disk_path TEXT, size_bytes INTEGER NOT NULL DEFAULT 0,
reused_count INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL)""")
conn.commit()
conn.close()
db = Database(path)
_run(db.connect())
cols = {r[1] for r in _run(db.fetchall("PRAGMA table_info(bio_jobs)"))}
assert "error" in cols and "archive_id" in cols
cols2 = {r[1] for r in _run(db.fetchall("PRAGMA table_info(bio_archives)"))}
assert "report_artifact" in cols2
_run(db.close())
def test_job_crud(db):
_run(db.execute(
"INSERT INTO bio_jobs (id, session_id, title, status, stage_chain, params, created_at, updated_at) "
"VALUES ('j1', 's1', 't', 'created', ?, ?, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')",
(dumps_json(["identify"]), dumps_json({}))))
row = _run(db.fetchone("SELECT * FROM bio_jobs WHERE id='j1'"))
assert row["title"] == "t"
assert loads_json(row["stage_chain"]) == ["identify"]
assert loads_json(row["params"]) == {}
def test_stage_cascade_delete(db):
_run(db.execute(
"INSERT INTO bio_jobs (id, session_id, title, status, stage_chain, params, created_at, updated_at) "
"VALUES ('j1', 's1', 't', 'created', '[]', '{}', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')"))
_run(db.execute(
"INSERT INTO bio_stages (id, job_id, stage, status, params, log_tail, attempt) "
"VALUES ('j1:identify', 'j1', 'identify', 'waiting', '{}', '', 0)"))
_run(db.execute("DELETE FROM bio_jobs WHERE id='j1'"))
rows = _run(db.fetchall("SELECT * FROM bio_stages WHERE job_id='j1'"))
assert rows == [] # FK CASCADE
def test_dumps_loads_json():
assert dumps_json({"a": 1}) == '{"a": 1}'
assert loads_json('{"a": 1}') == {"a": 1}
assert loads_json(None, "d") == "d"
assert loads_json("bad", []) == []
assert dumps_json(object(), "x") == '"x"'