fix: 归档脚本微审修复(fsync/文件名防覆盖/VACUUM容错/导出删除对账)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+36
-7
@@ -6,7 +6,9 @@
|
||||
2. --dry-run 只列不动: 库零变化、不导出、不 VACUUM;
|
||||
3. 清理后 VACUUM → page_count 减少(PRAGMA page_count 断言, 比文件大小稳);
|
||||
4. 导出 JSON 含被删行的完整字段(五表按会话分组);
|
||||
5. 门禁同 import_legacy: 租户须已 bootstrap(platform.db 有 slug)。
|
||||
5. 门禁同 import_legacy: 租户须已 bootstrap(platform.db 有 slug);
|
||||
6. 导出写盘失败 → 显性中止, 库零变化(导出先于删除的底线);
|
||||
导出/删除行数对账一致(文件名带时分秒, 同日重跑不覆盖)。
|
||||
|
||||
测试方式(照 test_import_legacy.py): import + sys.argv 注入, 非 subprocess;
|
||||
夹具全在 tmp(archives 目录取 tenants 目录兄弟 data/ 派生, 同 registry
|
||||
@@ -17,6 +19,7 @@ uploads_root 的布局纪律, tmp 内自动隔离)。时间形态双覆盖: ORM
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -266,9 +269,10 @@ class Env:
|
||||
def archives_dir(self) -> Path:
|
||||
return self.tmp_path / "data" / "archives"
|
||||
|
||||
def export_path(self, days: int) -> Path:
|
||||
date = datetime.now(timezone.utc).strftime("%Y%m%d")
|
||||
return self.archives_dir / "jiancai" / f"{date}-{days}d.json"
|
||||
def export_files(self, days: int) -> list[Path]:
|
||||
"""<日期>-<时分秒>-<N>d.json(文件名带时分秒防同日重跑覆盖,
|
||||
测试用 glob 取, 免跨秒竞态)。"""
|
||||
return sorted(self.archives_dir.glob(f"jiancai/*-{days}d.json"))
|
||||
|
||||
|
||||
# ---- 1. 归档: 旧会话五表清空, 近期不动 ------------------------------------------
|
||||
@@ -285,7 +289,11 @@ def test_archive_old_conversations(tmp_path, monkeypatch, capsys):
|
||||
assert _ids(env.tenant_db, table, CONV_ID_COL[table]) == KEPT[table], \
|
||||
table
|
||||
assert {OLD1, OLD2} & _ids(env.tenant_db, "conversations") == set()
|
||||
assert env.export_path(365).is_file() # 导出落 data/archives/<tenant>/
|
||||
exports = env.export_files(365)
|
||||
assert len(exports) == 1 # 导出落 data/archives/<tenant>/
|
||||
assert re.match(r"^\d{8}-\d{6}-365d\.json$", exports[0].name), \
|
||||
"文件名须为 <日期>-<时分秒>-<N>d.json(同日重跑不覆盖)"
|
||||
assert "导出/删除对账: 一致" in out
|
||||
assert "导出: " in out
|
||||
|
||||
|
||||
@@ -354,7 +362,7 @@ def test_export_complete(tmp_path, monkeypatch, capsys):
|
||||
|
||||
run_archive(monkeypatch, *env.archive_args())
|
||||
capsys.readouterr()
|
||||
path = env.export_path(365)
|
||||
path = env.export_files(365)[0]
|
||||
|
||||
doc = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert doc["tenant"] == "jiancai"
|
||||
@@ -436,6 +444,27 @@ def test_no_old_conversations_noop(tmp_path, monkeypatch, capsys):
|
||||
|
||||
assert "归档会话: 0 条" in out
|
||||
assert "VACUUM: 未执行" in out
|
||||
assert not env.export_path(365).exists()
|
||||
assert env.export_files(365) == []
|
||||
assert not env.archives_dir.exists()
|
||||
assert _sha256(env.tenant_db) == sha # 库文件字节级零变化
|
||||
|
||||
|
||||
# ---- 导出写失败: 库零变化 -------------------------------------------------------
|
||||
|
||||
def test_export_write_failure_leaves_db_untouched(tmp_path, monkeypatch, capsys):
|
||||
"""导出写盘失败(磁盘满/权限)→ 显性中止, 库零变化: 五表行级快照不变,
|
||||
不产生导出文件, 删除从未发生。'导出先于删除'的底线用例。"""
|
||||
env = Env(tmp_path, monkeypatch)
|
||||
_seed(env.tenant_db)
|
||||
snapshot = {t: _full_rows(env.tenant_db, t) for t in FIVE_TABLES}
|
||||
|
||||
def boom(path, doc):
|
||||
raise OSError(28, "No space left on device")
|
||||
|
||||
monkeypatch.setattr(archive, "_write_export", boom)
|
||||
with pytest.raises(SystemExit, match="导出写盘失败"):
|
||||
run_archive(monkeypatch, *env.archive_args())
|
||||
|
||||
assert {t: _full_rows(env.tenant_db, t) for t in FIVE_TABLES} == snapshot
|
||||
assert env.export_files(365) == []
|
||||
assert not (env.archives_dir / "jiancai").exists()
|
||||
|
||||
Reference in New Issue
Block a user