- 全链路:多源JD接入/清洗交叉验证/结构化抽取(星火+离线双通道)/时序知识图谱/新岗位发现/演化分析/人岗匹配/学习路径 - WebUI 五视图:驾驶舱/图谱工作台/岗位演化/匹配(单人+批量)/学习路径 - 设置中心(config.json)/Token鉴权/SQLite状态库/语义向量通道(BGE-M3本地或联网API) - 71 个 pytest 用例;数据目录仅保留结构(.gitkeep),数据由 scripts/generate_dataset.py 复现
106 lines
5.2 KiB
Python
106 lines
5.2 KiB
Python
"""SQLite 应用状态库测试:建表幂等 / 留痕追加与查询 / 候选增删查 / 任务表 / 旧文件迁移。"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from app.storage import db
|
||
|
||
REVIEW_REC = {"candidate_id": "cand_A", "action": "accept", "operator": "张三",
|
||
"reason": "证据充分", "before": {"status": "pending"},
|
||
"after": {"status": "active"}, "time": "2026-08-01 10:00:00"}
|
||
NODE_REC = {"kind": "node_update", "node_id": "job_X", "operator": "李四",
|
||
"reason": "校正", "before": {"level": None},
|
||
"after": {"level": "高级"}, "time": "2026-08-01 11:00:00"}
|
||
CAND = {"job_id": "cand_旧候选", "title": "旧候选", "category": "新岗位候选",
|
||
"required_skills": ["Python"], "status": "pending",
|
||
"source_count": 10, "company_count": 6}
|
||
|
||
|
||
def test_init_schema_idempotent(tmp_path):
|
||
"""重复打开/建表无副作用,四张表齐备。"""
|
||
p = tmp_path / "state.db"
|
||
for _ in range(2):
|
||
conn = db.connect(p)
|
||
tables = {r[0] for r in conn.execute(
|
||
"SELECT name FROM sqlite_master WHERE type='table'")}
|
||
assert {"audit_log", "review_candidates", "tasks", "meta"} <= tables
|
||
conn.close()
|
||
|
||
|
||
def test_audit_append_and_query(tmp_path):
|
||
"""留痕追加后按新→旧返回,结构还原为旧 jsonl 行,支持过滤与 limit。"""
|
||
p = tmp_path / "state.db"
|
||
db.append_audit(p, REVIEW_REC)
|
||
db.append_audit(p, NODE_REC)
|
||
recs = db.get_audit(p)
|
||
assert [r["operator"] for r in recs] == ["李四", "张三"] # 新→旧
|
||
assert recs[1]["candidate_id"] == "cand_A" and recs[1]["action"] == "accept"
|
||
assert recs[1]["before"] == {"status": "pending"}
|
||
assert recs[0]["kind"] == "node_update" and recs[0]["after"] == {"level": "高级"}
|
||
assert db.get_audit(p, node_id="cand_A") == [recs[1]] # 过滤
|
||
assert len(db.get_audit(p, limit=1)) == 1 # limit
|
||
|
||
|
||
def test_candidates_crud(tmp_path):
|
||
"""候选:save/get/list/delete,save 为按 job_id 的 upsert。"""
|
||
p = tmp_path / "state.db"
|
||
assert db.list_candidates(p) == []
|
||
db.save_candidate(p, CAND)
|
||
db.save_candidate(p, {**CAND, "job_id": "cand_B", "title": "另一个"})
|
||
db.save_candidate(p, {**CAND, "title": "旧候选-改"}) # upsert 覆盖
|
||
assert [c["job_id"] for c in db.list_candidates(p)] == ["cand_旧候选", "cand_B"]
|
||
assert db.get_candidate(p, "cand_旧候选")["title"] == "旧候选-改"
|
||
assert db.get_candidate(p, "cand_不存在") is None
|
||
db.delete_candidate(p, "cand_B")
|
||
assert [c["job_id"] for c in db.list_candidates(p)] == ["cand_旧候选"]
|
||
|
||
|
||
def test_tasks_upsert_get_list(tmp_path):
|
||
"""任务表:upsert 更新状态与 payload,get/list 可读回。"""
|
||
p = tmp_path / "state.db"
|
||
db.upsert_task(p, "t1", kind="jd_import", status="pending", payload={"status": "pending"})
|
||
db.upsert_task(p, "t1", kind="jd_import", status="done",
|
||
payload={"status": "done", "result": {"jobs": 18}})
|
||
row = db.get_task(p, "t1")
|
||
assert row["status"] == "done" and row["payload"]["result"]["jobs"] == 18
|
||
assert db.get_task(p, "不存在") is None
|
||
assert [t["task_id"] for t in db.list_tasks(p)] == ["t1"]
|
||
|
||
|
||
def test_overrides_crud(tmp_path):
|
||
"""人工覆写层:save/get/list/delete,save 为按 name 的 upsert。"""
|
||
p = tmp_path / "state.db"
|
||
assert db.list_overrides(p) == []
|
||
db.save_override(p, "cand_X", {"kind": "job_accept", "job": {"job_id": "cand_X"}})
|
||
db.save_override(p, "job_Y", {"kind": "node_update", "node_id": "job_Y",
|
||
"updates": {"level": "高级"}})
|
||
db.save_override(p, "cand_X", {"kind": "job_accept",
|
||
"job": {"job_id": "cand_X", "title": "改"}}) # upsert 覆盖
|
||
assert db.get_override(p, "cand_X")["job"]["title"] == "改"
|
||
assert db.get_override(p, "不存在") is None
|
||
assert [o["kind"] for o in db.list_overrides(p)] == ["job_accept", "node_update"]
|
||
db.delete_override(p, "cand_X")
|
||
assert [o["kind"] for o in db.list_overrides(p)] == ["node_update"]
|
||
|
||
|
||
def test_legacy_migration(tmp_path):
|
||
"""旧 audit.jsonl / pending.json 一次性导入:表空才迁、原文件保留、重复打开不重复迁。"""
|
||
audit = tmp_path / "audit.jsonl"
|
||
audit.write_text("\n".join(json.dumps(r, ensure_ascii=False)
|
||
for r in (REVIEW_REC, NODE_REC)) + "\n", encoding="utf-8")
|
||
pending = tmp_path / "pending.json"
|
||
pending.write_text(json.dumps([CAND], ensure_ascii=False), encoding="utf-8")
|
||
p = tmp_path / "state.db"
|
||
|
||
db.connect(p, legacy_audit=audit, legacy_pending=pending).close()
|
||
recs = db.get_audit(p)
|
||
assert len(recs) == 2 and recs[1]["candidate_id"] == "cand_A"
|
||
assert [c["job_id"] for c in db.list_candidates(p)] == ["cand_旧候选"]
|
||
assert audit.exists() and pending.exists() # 原文件保留
|
||
# 再次打开不重复迁移(meta 标记 + 表非空双保险)
|
||
db.connect(p, legacy_audit=audit, legacy_pending=pending).close()
|
||
assert len(db.get_audit(p)) == 2 and len(db.list_candidates(p)) == 1
|
||
# 迁移后新写入的数据不受影响
|
||
db.append_audit(p, REVIEW_REC)
|
||
assert len(db.get_audit(p)) == 3
|