test_app_summary_shortcircuit.py 4.27 KB
#!/usr/bin/env python3
"""Dev Agent RED/GREEN 测试:POST /api/articles/summarize 摘要状态机 (app.py)。

运行: python3 test_app_summary_shortcircuit.py
实现前应先失败 = RED;实现后 ALL PASS = GREEN。
用 Flask test_client(REMOTE_ADDR=127.0.0.1 ⇒ /api/ 免登录)+ monkeypatch _spawn_hermes_job
避免真的派发 hermes 任务。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="appsc_"), "test.db")
config.LOCAL_DB = TMP

import local_db  # noqa: E402
local_db.init_db()

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 seed_article(url, title, status, attempts):
    conn = sqlite3.connect(TMP)
    conn.execute(
        "INSERT INTO articles (url,title,dedup_key,summary_status,summary_attempts) VALUES (?,?,?,?,?)",
        (url, title, url, status, attempts))
    conn.commit()
    conn.close()


def job_count():
    conn = sqlite3.connect(TMP)
    n = conn.execute("SELECT COUNT(*) FROM hermes_jobs").fetchone()[0]
    conn.close()
    return n


def attempts_of(url):
    conn = sqlite3.connect(TMP)
    row = conn.execute("SELECT summary_attempts, summary_status FROM articles WHERE dedup_key=?",
                       (url,)).fetchone()
    conn.close()
    return row


# ── import app AFTER patching config ──
import app as appmod  # noqa: E402

spawn_calls = []
appmod._spawn_hermes_job = lambda *a, **k: spawn_calls.append((a, k))  # monkeypatch

client = appmod.app.test_client()

# TC1: failed 文章 → 短路,不派发,status='failed',attempts 不增
U1 = "https://example.com/ac1-failed-unique"
seed_article(U1, "AC1失败样本", "failed", 3)
jb = job_count()
r = client.post("/api/articles/summarize", json={"url": U1, "title": "AC1失败样本"})
j = r.get_json()
check("TC1a HTTP 200 + ok", r.status_code == 200 and bool(j.get("ok")), f"{r.status_code} {j}")
check("TC1b data.status == failed", (j.get("data") or {}).get("status") == "failed", str(j))
check("TC1c no spawn", job_count() == jb and len(spawn_calls) == 0, f"jobs {jb}->{job_count()} spawn={len(spawn_calls)}")
check("TC1d attempts unchanged ==3", attempts_of(U1)[0] == 3, str(attempts_of(U1)))

# TC2: attempts>=3 但 status 仍是 pending(防御)→ 同样短路
U2 = "https://example.com/ac2-attempts3"
seed_article(U2, "AC2已达上限", "pending", 3)
spawn_calls.clear()
jb = job_count()
r = client.post("/api/articles/summarize", json={"url": U2, "title": "AC2已达上限"})
j = r.get_json()
check("TC2a attempts>=3 shortcircuit failed", (j.get("data") or {}).get("status") == "failed", str(j))
check("TC2b no spawn", len(spawn_calls) == 0 and job_count() == jb)

# TC3: pending 文章 → 派发前 set_dispatching(status=summarizing, attempts+1)
U3 = "https://example.com/ac3-pending"
seed_article(U3, "AC3正常", "pending", 0)
spawn_calls.clear()
r = client.post("/api/articles/summarize", json={"url": U3, "title": "AC3正常"})
j = r.get_json()
check("TC3a started + spawn called", (j.get("data") or {}).get("status") == "started" and len(spawn_calls) == 1,
      f"{j} spawn={len(spawn_calls)}")
st = attempts_of(U3)
check("TC3b dispatching -> summarizing attempts=1", st[1] == "summarizing" and st[0] == 1, str(st))

# TC4: cache 命中 → 先返回 'cached',不计 attempts、不派发
U4 = "https://example.com/ac4-cached"
seed_article(U4, "AC4已缓存", "pending", 0)
conn = sqlite3.connect(TMP)
conn.execute("INSERT INTO daily_reports (chatroom_id,chatroom_name,report_date,content,dedup_key,article_url)"
             " VALUES (?,?,?,?,?,?)",
             ("g", "G", "summary", "y" * 80, U4, U4))
conn.commit()
conn.close()
spawn_calls.clear()
r = client.post("/api/articles/summarize", json={"url": U4, "title": "AC4已缓存"})
j = r.get_json()
check("TC4a cache hit", (j.get("data") or {}).get("status") == "cached", str(j))
check("TC4b cache hit no attempts/no spawn",
      attempts_of(U4)[0] == 0 and len(spawn_calls) == 0, f"{attempts_of(U4)} spawn={len(spawn_calls)}")

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