test_summary_status.py 4.02 KB
#!/usr/bin/env python3
"""Dev Agent RED/GREEN 测试:文章摘要状态机 (local_db)。

运行: python3 test_summary_status.py
实现前应先失败(AttributeError / 断言失败)= RED;实现后 ALL PASS = GREEN。
不污染生产 DB:改写 config.LOCAL_DB 指向临时库。
"""
import os
import sys
import sqlite3
import tempfile

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

import config

TMP = os.path.join(tempfile.mkdtemp(prefix="summary_status_"), "test.db")
config.LOCAL_DB = TMP

import local_db  # noqa: E402

FAILS = []


def check(name, cond, detail=""):
    print(f"[{'PASS' if cond else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
    if not cond:
        FAILS.append(name)


def state_of(url):
    return local_db.article_summary_get_state(None, url)


# TC1: init_db 幂等 + 新增 3 列存在
local_db.init_db()
local_db.init_db()
cols = [r[1] for r in sqlite3.connect(TMP).execute("PRAGMA table_info(articles)")]
check("TC1a summary_status column", "summary_status" in cols, str(cols))
check("TC1b summary_attempts column", "summary_attempts" in cols)
check("TC1c summary_updated_at column", "summary_updated_at" in cols)

# TC2: 回填——已有摘要的文章标 ok;未摘要的保持 pending
conn = sqlite3.connect(TMP)
conn.execute("INSERT INTO articles (url,title,dedup_key) VALUES (?,?,?)",
             ("https://mp.weixin.qq.com/s?__biz=A&mid=1&idx=1&sn=abc", "已摘要", "A|1|1|abc"))
NOURL = "https://example.com/nosum"
conn.execute("INSERT INTO articles (url,title,dedup_key) VALUES (?,?,?)",
             (NOURL, "未摘要", NOURL))
conn.execute(
    "INSERT INTO daily_reports (chatroom_id,chatroom_name,report_date,content,dedup_key,article_url)"
    " VALUES (?,?,?,?,?,?)",
    ("g", "G", "summary", "x" * 60, "A|1|1|abc", "https://mp.weixin.qq.com/s?__biz=A&mid=1&idx=1&sn=abc"))
conn.commit()
conn.close()
local_db.init_db()  # 触发回填
st = local_db.article_summary_get_state("A|1|1|abc")
check("TC2a backfill summarized -> ok", bool(st) and st["summary_status"] == "ok", str(st))
check("TC2b unsummarized stays pending", state_of(NOURL)["summary_status"] == "pending")

# TC3: 状态转移 pending -> summarizing -> (3 次失败) -> failed
local_db.article_summary_set_dispatching(None, NOURL)
s = state_of(NOURL)
check("TC3a dispatching -> summarizing attempts=1",
      s["summary_status"] == "summarizing" and s["summary_attempts"] == 1, str(s))
local_db.article_summary_mark_failed(None, NOURL)
check("TC3b fail #1 -> pending", state_of(NOURL)["summary_status"] == "pending")
local_db.article_summary_set_dispatching(None, NOURL)
local_db.article_summary_mark_failed(None, NOURL)
local_db.article_summary_set_dispatching(None, NOURL)
local_db.article_summary_mark_failed(None, NOURL)
s = state_of(NOURL)
check("TC3c fail #3 -> failed attempts=3",
      s["summary_status"] == "failed" and s["summary_attempts"] == 3, str(s))

# TC3d: failed 后 set_dispatching 不再自增(短路保护)
before = state_of(NOURL)["summary_attempts"]
local_db.article_summary_set_dispatching(None, NOURL)
check("TC3d failed not re-dispatched", state_of(NOURL)["summary_attempts"] == before)

# TC4: mark_ok
local_db.article_summary_mark_ok("A|1|1|abc")
check("TC4 mark_ok", local_db.article_summary_get_state("A|1|1|abc")["summary_status"] == "ok")

# TC5: state_counts
c = local_db.article_summary_state_counts()
check("TC5 state_counts ok/failed/total",
      c.get("ok") == 1 and c.get("failed") == 1 and c.get("total") == 2, str(c))

# TC6: failed_titles
t = local_db.article_summary_failed_titles()
check("TC6 failed_titles contains 未摘要", "未摘要" in t, str(t))

# TC7: 返回语义不变——article_get_unsummarized_new 仍可用(不因新列改变行为)
try:
    rows = local_db.article_get_unsummarized_new(limit=5)
    check("TC7 unsummarized_new callable", isinstance(rows, list), f"{len(rows)} rows")
except Exception as e:
    check("TC7 unsummarized_new callable", False, repr(e))

print()
if FAILS:
    print(f"RESULT: {len(FAILS)} FAILED -> {FAILS}")
    sys.exit(1)
print("RESULT: ALL PASS")