test_step6_stop_state.py 6.36 KB
#!/usr/bin/env python3
"""VERIFY: Step6 报告「三态停止标记」契约 + 超出项分型 gap
(agent-traffic-light-stop-protocol, learn_queue #1403, 2026-10-13 Step6 审计)

学到的知识(原文 L9/L12):
  1. 开工第一轮用一行固定目标清单钉住原始需求;此后每轮最后一行输出三态进度条
     🟢 还差/🟡 原始需求已达成且超出项能串成有执行标准与验收标准的 Plan/
     🔴 原始需求已达成且超出项零散不值得单开一轮(仅压三行存档)。
  2. 🟡 落盘成 plan/handoff 等启动指令;转黄/转红后一律停手等指令。
  3. 分界判据=超出项能否构成「目标 + 验收标准」的可执行 Plan,缺一即 🔴。
  判据句:「Agent 永远不会主动说做完了——每轮结尾的『推荐下一步』把半小时需求拖成一整天」。
  原文反例:红灯下罗列的「已完成」(PR 合并 / 远端分支删除)重开对话查证后一件都没做
  ⇒ 完成声明必须可查证,未查证的不计入达成。

病理 WHERE=step6_apply apply-done 的报告收尾契约
     WHY=契约末项只要求自由文本「建议: [下一步行动]」,无收尾态声明、无 🟡/🔴 分型
          ⇒ 超出项既不落盘成可执行 Plan,也不被压档丢弃,散装建议无限累积。
          实测:circle.db 中 46 份 apply_audit,含 🟢/🟡/🔴 三态标记者 = 0。

修复:
  1) `_has_stop_state(text)`(全文检测,与存储解耦,CLM #1323 同口径)
  2) Step6 报告模板:`- 建议:` → 「超出项 🟡 / 超出项 🔴 / 收尾态」三行 + 收尾纪律段
  3) `_on_apply_done` 观测登记:缺失 → gate=NO_STOP_STATE / action=CONTINUE
  4) VerifyReason 登记 NO_STOP_STATE(定义 + VERIFY_REASON_CODE 描述)

TDD:
TC1 (RED/复现旧病理) 旧契约下「以散装建议收场」的报告被静默接受 → 收不住
TC2 (GREEN/真实数据) circle.db 实测:46 份 apply_audit 含三态标记 = 0
TC3 (GREEN/判别力) 三态行→True;裸 emoji 无标签→False;只有「已完成」→False
TC4 (源码守卫/防回退) app.py 含 helper + reason code 定义/注册 + 模板三行 + 登记块
TC5 (冻结重跑 spot check) 同一输入 3 次采样,结果一致(确定性纯函数)
"""
import pathlib
import sqlite3

APP = pathlib.Path("/data/data/com.termux/files/home/wechat-viewer/app.py")
DB = pathlib.Path("/data/data/com.termux/files/home/wechat-viewer/circle.db")
FAIL = 0


def old_accepted(report_text, has_human_flag):
    """复现旧契约:只要有报告 + human gate 关键词就算收尾合格(无收尾态要求)。"""
    return bool(report_text) and bool(has_human_flag is not None)


def has_stop_state(text):
    """复现新逻辑(与 app.py `_has_stop_state` 同判据)。"""
    t = text or ""
    has_label = ("收尾态" in t) or ("停止态" in t)
    has_light = any(lamp in t for lamp in ("🟢", "🟡", "🔴"))
    return has_label and has_light


def check(name, ok, detail=""):
    global FAIL
    print(("PASS  " if ok else "FAIL  ") + name + (f"  — {detail}" if detail else ""))
    if not ok:
        FAIL += 1


def main():
    scattered_report = ("# Step 6 知识应用报告\n- 发现但未修复: 无\n"
                        "- 建议: 1) 优化索引 2) 添加回滚测试 3) 更新文档\n"
                        "- Human gate: 无\n")
    print("TC1 复现旧病理:散装建议收场的报告被旧契约静默接受")
    check("旧契约接受散装建议报告", old_accepted(scattered_report, "无") is True,
          "旧契约 = 报告存在 + human gate 关键词,不校验收尾态")
    check("新契约拒绝同一报告", has_stop_state(scattered_report) is False,
          "无收尾态标记 → NO_STOP_STATE")

    print("\nTC2 真实数据:46 份 apply_audit 含三态标记 = 0(收尾态契约从未生效)")
    try:
        conn = sqlite3.connect(str(DB))
        total = conn.execute(
            "SELECT COUNT(*) FROM learning_results WHERE result_type='apply_audit'"
        ).fetchone()[0]
        marked = conn.execute(
            "SELECT COUNT(*) FROM learning_results WHERE result_type='apply_audit' "
            "AND (result_summary LIKE '%🟡%' OR result_summary LIKE '%🔴%' "
            "OR result_summary LIKE '%🟢%')"
        ).fetchone()[0]
        conn.close()
        check("存在 apply_audit 样本", total > 0, f"total={total}")
        check("含三态标记者 = 0", marked == 0, f"marked={marked}/{total}")
    except Exception as e:  # 库不可达不阻断判别力测试
        check("真实数据可读", False, f"{e}")

    print("\nTC3 判别力:标签 + 状态灯双命中才算,单命中不算")
    check("完整三态行 → True",
          has_stop_state("- 收尾态: 🟡 超出项已成 Plan 并落盘 ~/pixelphone/plans/x.md") is True)
    check("🔴 零散收场 → True", has_stop_state("- 收尾态: 🔴 超出项零散已压档") is True)
    check("裸 emoji 无标签 → False", has_stop_state("- 🟡\n- Human gate: 无") is False)
    check("只有「已完成」→ False", has_stop_state("- 已完成: 全部修复完毕") is False)
    check("空文本 → False", has_stop_state("") is False)

    print("\nTC4 源码守卫(防回退)")
    src = APP.read_text()
    check("保存 helper `_has_stop_state`", "def _has_stop_state(" in src)
    check("保存 reason code 定义",
          'NO_STOP_STATE = "NO_STOP_STATE"' in src)
    check("保存 reason code 描述注册",
          "VerifyReason.NO_STOP_STATE: " in src)
    check("模板含「超出项 🟡 (可执行 Plan)」", "- 超出项 🟡 (可执行 Plan):" in src)
    check("模板含「超出项 🔴 (零散观察)」", "- 超出项 🔴 (零散观察):" in src)
    check("模板含「收尾态:」末行", "- 收尾态: [" in src)
    check("模板不再含裸「- 建议: [下一步行动]」", "- 建议: [下一步行动]" not in src)
    check("保存观测登记块", "node=step6_apply.stop_state" in src)

    print("\nTC5 冻结重跑 spot check:同一输入 3 次采样(确定性)")
    samples = [has_stop_state("- 收尾态: 🟡 已落盘 ~/pixelphone/plans/a.md")
               for _ in range(3)]
    check("3/3 一致", samples == [True, True, True], f"samples={samples}")

    print(f"\n{'ALL PASS' if FAIL == 0 else f'{FAIL} FAILED'}")
    return 1 if FAIL else 0


if __name__ == "__main__":
    raise SystemExit(main())