Ctrl K
ComunidadGuías…

tests/test_numbers_agree.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""系統報給你的每一個數字,都要有第二種算法同意它。

2026-08-31 這一天,同一個缺陷出現六次,形狀完全一樣:**同一件事有兩個定義**。

    資料路徑        xkb_paths vs 自己拼,28 處
    有幾張卡片      glob vs rglob,1538 vs 1539
    還有多少沒消化   工具算一套、健檢算一套,結論被當成待辦
    self-derived   三個寫法,913 條繞過回音室降權
    有多少待處理     治理說 3、健檢說 77
    有幾條提案      導向前數 vs 導向後數,說 1、實際 0

前五個是使用者一個一個問出來的——那是最差的發現方式,也是為什麼修到後來
會讓人覺得沒完沒了。第六個是把清單列出來一次掃出來的。

所以這個測試存在的理由不是「再加一道防線」,是**換一種發現方式**:
XKB 會報的數字是有限的,每一個都在這裡被獨立算第二次。對不上就是
兩個定義又出現了,不必等到某天早上的通知說錯話。
"""
from __future__ import annotations

import re
import sys
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
sys.path.insert(0, str(ROOT / "tools"))

import xkb_paths


class NumbersAgreeTest(unittest.TestCase):
    """每一項都拿系統的答案,對上一個不共用實作的答案。"""

    def test_card_count_has_one_answer(self) -> None:
        """cards/ 第一層的 .md。子目錄放的是治理判定重複而移走的,不算。"""
        independent = len([p for p in xkb_paths.CARDS_DIR.iterdir()
                           if p.is_file() and p.suffix == ".md"])
        self.assertEqual(len(xkb_paths.card_files()), independent)
        self.assertEqual(len(xkb_paths.card_ids()), independent)

    def test_pending_bookmarks_have_one_answer(self) -> None:
        """一個宣告自己是 knowledge-card 的檔案已經是成品,不是待辦。"""
        from xkb_pending_work import uncarded_bookmarks

        card_ids = xkb_paths.card_ids()
        independent = 0
        for path in xkb_paths.BOOKMARKS_DIR.rglob("*.md"):
            if not path.is_file() or path.stem in card_ids:
                continue
            if "type: knowledge-card" in path.read_text(encoding="utf-8", errors="ignore")[:400]:
                continue
            independent += 1
        reported = len(uncarded_bookmarks(xkb_paths.BOOKMARKS_DIR, xkb_paths.CARDS_DIR))
        self.assertEqual(reported, independent)

    def test_governance_counts_match_what_governance_would_do(self) -> None:
        """通知說的待處理,必須等於治理下一輪真的會處理的。

        健檢原本數的是「staging 裡還是 pending 的」,而治理刻意不動 staging,
        所以一個已經被看過、判定不放行的候選永遠算成待辦——報 77,實際 3。
        永遠亮著的紅燈跟永遠不亮的綠燈一樣,看久了就不看了。
        """
        import xkb_review

        counts = xkb_review.governance_health_counts(30)
        batch = xkb_review.governance_batch(limit=10_000, dry_run=True, ttl_days=30)
        self.assertEqual(counts["pending"], batch["stats"]["discovered"])
        self.assertEqual(
            counts["proposal"],
            len([t for t in batch["topic_suggestions"] if t["action"] == "proposal"]),
            "提案要在導向 general 之後才數,跟治理同一個順序",
        )

    def test_bloated_pages_exclude_what_was_already_digested(self) -> None:
        """結論本身也是條列。把它算成待消化,就會每天叫你做已經做完的事。"""
        from xkb_synthesize_topic import undigested

        for path in sorted(xkb_paths.WIKI_TOPICS_DIR.glob("*.md")):
            body = path.read_text(encoding="utf-8", errors="ignore")
            # 人寫的部分,加上「尚未消化」那一段。結論不算——它已經消化過了;
            # 但尚未消化那一段要算,它就是在等下一次。第一版把三段一起切掉,
            # 於是 openclaw-agent-workflows 的 480 條變成 0。
            head = re.split(
                r"^## (?:結論(消化自累積筆記)|尚未消化|出處)\s*$",
                body, maxsplit=1, flags=re.M,
            )[0]
            waiting = ""
            if "## 尚未消化\n" in body:
                waiting = body.split("## 尚未消化\n", 1)[1].split("\n## ", 1)[0]
            trimmed = head + "\n" + waiting
            # 純連結是出處,不是素材。算進去的話 ai-seo-and-geo 會從 11 條
            # 變成 55 條——多出來的 44 條長這樣:
            #     - [標題](https://x.com/...) — 2025-02,xkb
            # 尾巴是日期與來源標記,所以「開頭是連結」就足以判定,不需要
            # 整行都是連結。第一版寫成整行才算,於是漏掉全部 44 條。
            #
            # 這裡的「獨立」不是指寫出不一樣的規則,是指不呼叫同一段程式:
            # 規則在兩個地方各自寫一次,其中一邊改了而另一邊沒跟上時,
            # 這個測試會失敗——那正是今天六個 bug 的共同形狀。
            link_only = re.compile(r"^\s*-\s*\[[^\]]+\]\(https?://")
            independent = [line for line in trimmed.splitlines()
                           if line.lstrip().startswith("- ")
                           and len(line.strip()) > 60
                           and not link_only.match(line)]
            self.assertEqual(
                len(undigested(body)[1]), len(independent),
                f"{path.name}:待消化的條數有兩個答案",
            )


if __name__ == "__main__":
    unittest.main()