feat: ingest 進度可見性 + Ctrl-C 安全回復 (Closes #2)
issue #2:--all-pending 長時間本地 Ollama 攝入全程靜默,使用者不確定有沒有在 跑、又不敢中斷。三處補強: - 進度可見:每份來源印 [i/N] 起始行、長文件逐分段印「分段 k/n」、每個 knowledge-item 印「↳ 新增/更新」、完成印「✓」,全部 flush=True 即時顯示。 最會靜默的分段迴圈正是重點回報處。 - Ctrl-C 安全:KeyboardInterrupt 不是 Exception 子類,原本會略過善後、把使用者 留在 ingest 分支且無指引。改為獨立攔截——commit 在迴圈之後,故 main 必然未受 影響——印出可照做的回復指令(git checkout main && git stash -u && git branch -D <branch>),以結束碼 130 收場。 selfcheck_ingest 新增:驗進度標記出現;驗中斷後 main HEAD 未動、停在 ingest 分支、且照文件回復指令實測能回到乾淨 main(驗結果可用,非字串存在,§13.4)。 README §7 補一列。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,13 @@
|
||||
執行:.venv/Scripts/python tools/selfcheck_ingest.py
|
||||
"""
|
||||
import hashlib
|
||||
import io
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
||||
import kb
|
||||
@@ -82,9 +84,14 @@ def main():
|
||||
git(root, "add", "-A")
|
||||
git(root, "commit", "-q", "-m", "init")
|
||||
|
||||
# 1) 攝入:建分支、產頁、commit、回到 main
|
||||
# 1) 攝入:建分支、產頁、commit、回到 main(並驗進度輸出可見,issue #2)
|
||||
kb.llm_json = fake_llm
|
||||
ingest.main(["raw/converted/doc1.md", "--root", str(root)])
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
ingest.main(["raw/converted/doc1.md", "--root", str(root)])
|
||||
out = buf.getvalue()
|
||||
for marker in ("[1/1] 攝入", "分段 1/", "↳ 新增", "✓ ", "完成"):
|
||||
assert marker in out, f"進度輸出缺少 {marker!r}\n{out}"
|
||||
assert git(root, "branch", "--show-current") == "main"
|
||||
branch = git(root, "branch", "--list", "ingest/*").strip("* ").strip()
|
||||
assert branch, "未建立 ingest 分支"
|
||||
@@ -147,6 +154,45 @@ def main():
|
||||
except RuntimeError as e:
|
||||
assert "fallback" in str(e)
|
||||
print("PASS: 重試 + fallback 切換 + 全失敗報錯")
|
||||
|
||||
# 5) Ctrl-C 安全(issue #2):中斷時 main 不受影響、以 130 收場,
|
||||
# 且文件宣稱的回復指令真的能回到乾淨 main。第 2 份來源開始摘要時中斷,
|
||||
# 此時第 1 份的頁面已寫入(未追蹤、未 commit)——正是使用者最怕的半成品狀態。
|
||||
add_doc(root, "docA.md", "來源A:待攝入。")
|
||||
add_doc(root, "docB.md", "來源B:待攝入。")
|
||||
git(root, "add", "-A")
|
||||
git(root, "commit", "-q", "-m", "docA/docB")
|
||||
head_before = git(root, "rev-parse", "main") # 攝入不得動到的 main 狀態
|
||||
state = {"n": 0}
|
||||
def boom(cfg, task, prompt, validate):
|
||||
state["n"] += 1
|
||||
if state["n"] >= 2: # 第 2 份來源的 summarize 呼叫
|
||||
raise KeyboardInterrupt
|
||||
return ({"title": "來源A", "description": "d", "slug": "fresh-a",
|
||||
"tags": ["x"], "summary_markdown": "- a", "knowledge_items": []},
|
||||
"fake:test")
|
||||
kb.llm_json = boom
|
||||
err = io.StringIO()
|
||||
try:
|
||||
with redirect_stdout(io.StringIO()), redirect_stderr(err):
|
||||
ingest.main(["raw/converted/docA.md", "raw/converted/docB.md",
|
||||
"--root", str(root)])
|
||||
raise AssertionError("KeyboardInterrupt 應轉為 SystemExit(130)")
|
||||
except SystemExit as e:
|
||||
assert e.code == 130, e.code
|
||||
msg = err.getvalue()
|
||||
assert "git checkout main" in msg and "main 未受影響" in msg, msg
|
||||
assert git(root, "rev-parse", "main") == head_before, "main 被動到了!"
|
||||
cur = git(root, "branch", "--show-current")
|
||||
assert cur.startswith("ingest/"), f"中斷後應停在 ingest 分支,實得 {cur!r}"
|
||||
assert git(root, "status", "--porcelain"), "應有未提交的半成品"
|
||||
# 照文件指令回復,斷言真的回到乾淨 main(驗「結果可用」,非「字串存在」,§13.4)
|
||||
git(root, "checkout", "main")
|
||||
git(root, "stash", "-u")
|
||||
git(root, "branch", "-D", cur)
|
||||
assert git(root, "branch", "--show-current") == "main"
|
||||
assert not git(root, "status", "--porcelain"), "回復後工作區仍不乾淨"
|
||||
print("PASS: Ctrl-C → main 未受影響、130 收場、回復指令實測可回乾淨 main")
|
||||
print("ALL PASS")
|
||||
finally:
|
||||
kb.llm_json, kb.ollama_chat = real_llm, real_chat
|
||||
|
||||
Reference in New Issue
Block a user