#!/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")