Appearance
Conversation Lifecycle Verification
驗證
conversation-lifecycle-governancespec 在 local / staging / production 的真實行為。涵蓋 stale conversation resolver(governance 1.1-1.2)與 conversation delete purge(governance 1.3-1.5)兩條主線。前提:
- 已依
production-deploy-checklist.md/DEPLOYMENT_RUNBOOK.md完成目標環境部署(或具備 local 開發環境)- 目標 D1 可以
wrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote ...查詢;staging 請先設DB_NAME=agentic-rag-db-staging- Web Admin 帳號可登入且具備上傳、切版、刪除對話權限
- 至少有一份
access_level=internal文件可問答(對應ACCEPTANCE_RUNBOOK.md的 Doc A / Doc A')規則:人工檢查項目由使用者走完後回報 OK / 問題 / skip,Claude 才能代勾。
1. 情境總覽
| 驗證主題 | Task | 對應 Scenario |
|---|---|---|
| Stale follow-up 偵測 | 1.1, 1.2 | Current version change marks a conversation stale |
| Same-document 快路徑 | 1.1, 1.2 | Same-document follow-up survives while current valid |
| 刪除立即消失 | 1.3 | Deleted conversation disappears from user surfaces |
title / content_text 不可回復 | 1.4 | Audit residue never restores original content |
| Audit residue 不外洩 | 1.5 | 只能走稽核路徑,禁止回到一般 UI / API / model context |
2. Stale Conversation Resolver 驗證(governance 1.1-1.2)
2.1 前置
Admin 登入 Web,以
ACCEPTANCE_RUNBOOK.mdPhase 2 上傳 Doc A(internal)。確認 Doc A 目前只有 1 個 current version:
bashwrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \ "SELECT id, document_id, version_number, is_current \ FROM document_versions \ WHERE document_id = '<Doc A id>' \ ORDER BY version_number;"僅有一筆
is_current = 1。進入
/chat新建對話 C1,提問 Doc A 可答的問題 Q1,取得引用[1](記錄documentVersionId)。記錄 C1 的
conversation_id(瀏覽器 URL 或 devtools / D1)。
2.2 Same-document follow-up 快路徑(版本未切換)
操作:
- 在 C1 中對同一文件追問 Q2(Q2 仍可由 Doc A current version 回答)。
- 觀察回答的引用版本。
PASS 條件:
Q2 回答中的引用
document_version_id與 Q1 相同D1
query_logs對 Q1、Q2 兩筆可查到並列(同 conversation,快路徑命中)bashwrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \ "SELECT id, channel, status, created_at \ FROM query_logs \ WHERE channel = 'web' \ ORDER BY created_at DESC LIMIT 4;"沒有出現 stale 標記(參考 server log 的
stale=false或對應欄位)
失敗排除:
- 若 Q2 重新做 fresh retrieval 但版本未切換 → 代表 resolver 過度保守;檢查
stale resolver是否誤判 - 若 Q2 沿用舊 citation 但 citation 版本並非 current → Resolver 未觸發,檢查
is_currentquery 與citations_json.document_version_id的比對邏輯
2.3 版本切換後 follow-up 必走 fresh retrieval
操作:
Admin 對 Doc A 上傳新版 Doc A'(內容明顯不同、可回答 Q3)。
確認切版完成:
bashwrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \ "SELECT version_number, is_current, published_at \ FROM document_versions \ WHERE document_id = '<Doc A id>' \ ORDER BY version_number;"舊版
is_current = 0,新版is_current = 1。回到同一個 C1 對話(不建立新對話),追問 Q3(只有 Doc A' 才能答)。
觀察回答與引用。
PASS 條件:
Q3 的回答來自 Doc A' 新版(內容對得上)
Q3 的引用
document_version_id等於新版,不是 Q1 / Q2 的舊版query_logs最新一筆紀錄到channel='web'、conversation_id=C1、且可由 server log(如 evlog)看到 stale 決策訊號(例如stale=true或等價欄位)舊 assistant message 的
citations_json不被回寫(audit-safe,不得倒刪)bashwrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \ "SELECT id, role, LENGTH(content_redacted) AS content_len \ FROM messages \ WHERE query_log_id IN ( \ SELECT id FROM query_logs ORDER BY created_at DESC LIMIT 6) \ ORDER BY created_at;"
失敗排除:
- Q3 仍沿用舊版 → Resolver 沒重算;檢查
shared resolver是否真的以 D1is_current為真相 - Q3 回答來自舊版內容(幻覺 + 舊 snapshot) → Fresh retrieval 沒跑;檢查 chat follow-up 分支
- Q3 引用混雜新舊版 → 排序/過濾錯誤,檢查 retrieval filter 是否排除
is_current = 0
2.4 回歸:再切回或雙版本
操作(optional,只在懷疑 resolver 時跑):
- 若平台允許回復舊版,把 Doc A 回切到 Doc A,再次在 C1 追問。
- 觀察引用是否跟隨目前 current。
PASS 條件:引用 document_version_id 始終只會是 D1 當下 is_current=1 的那一版,不受對話歷史影響。
3. Conversation Delete Purge 驗證(governance 1.3-1.5)
3.1 前置
Admin 或 User 登入,進入
/chat新建對話 C_DEL。以可辨識關鍵字
PURGE-CANARY-<timestamp>作為使用者問題,確保之後可在 D1 搜尋到該字樣是否真被清除。對話結束後,在 D1 確認該對話存在:
bashwrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \ "SELECT id, title, deleted_at \ FROM conversations \ ORDER BY created_at DESC LIMIT 5;"該筆
deleted_at應為 NULL。
3.2 刪除後立即從 user surfaces 消失
操作:
- 從
/chat左側對話列表點「刪除」C_DEL。 - 重新整理頁面 / 等待 client 同步。
- 檢查:
- 左側對話列表不再出現 C_DEL
- 直接在瀏覽器打
/chat/<C_DEL id>應該 404 / redirect / 空狀態 - 呼叫
GET /api/conversations(如適用)回傳不含 C_DEL - 呼叫
GET /api/conversations/<id>(如適用)回傳 404 或等價拒絕
PASS 條件:上述四項全部成立。若 deleted_at 已被寫入但 API 仍回傳 → filter 未加,governance 1.3 FAIL。
D1 驗證:
bash
wrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \
"SELECT id, title, deleted_at \
FROM conversations \
WHERE id = '<C_DEL id>';"deleted_at應為非 NULL ISO 時間字串
3.3 title / messages.content_text 不可回復
操作:
刪除後 30 秒內(同一 cleanup 週期內或觸發 purge 後),執行:
bash
wrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \
"SELECT id, title, deleted_at \
FROM conversations WHERE id = '<C_DEL id>';"
wrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \
"SELECT id, role, content_redacted, LENGTH(content_redacted) AS content_len, risk_flags_json \
FROM messages \
WHERE query_log_id IN ( \
SELECT id FROM query_logs WHERE created_at >= datetime('now', '-10 minutes')) \
ORDER BY created_at;"欄位對照:governance spec 使用
messages.content_text作為「原文」的語意名稱,當前 schema 將其存於messages.content_redacted(已進行 redaction 的留存欄位)。此處驗證的是「刪除後該欄位是否被清空或以不可回復形式處理」。
PASS 條件:
conversations.title不得是原本的自動標題或使用者第一則訊息片段;應為空字串、[deleted]之類 placeholder、或欄位直接變 NULLmessages.content_redacted不得包含PURGE-CANARY-<timestamp>或任何可還原原文的片段全庫 grep 亦不可還原:
bashwrangler d1 execute "${DB_NAME:-agentic-rag-db}" --remote --command \ "SELECT COUNT(*) AS hits FROM messages WHERE content_redacted LIKE '%PURGE-CANARY-%';"hits必須為 0
失敗排除:
- 只有
deleted_at寫入但 title / content 不動 → purge policy 沒跑;governance 1.4 FAIL - Title 被改為 placeholder 但 content 原文留著 → 不完整 purge;governance 1.4 FAIL
- Content 被清但仍可由
citations_json/query_logs.query_redacted_text重組原文 → residue 設計有洞,governance 1.5 FAIL
3.4 Audit residue 不外洩到一般路徑
操作:
- 以相同使用者身份登入,嘗試任何 UI / API path:
/chat列表/chat/<C_DEL id>/api/conversations、/api/conversations/<id>/messages(如適用)
- 模擬 follow-up:在新對話提問「我之前問過 PURGE-CANARY-... 的內容是什麼」,觀察 model response 是否洩漏原文。
PASS 條件:
- 一般使用者路徑全部無法還原原文
- Model 回答不得包含
PURGE-CANARY-<timestamp>字樣(若包含 → context assembly 仍抓得到已刪對話,governance 1.5 FAIL) - 僅稽核用 D1 表(如
query_logs的 redacted 欄位、經 redaction 的 audit residue)可能保留,但內容應為 redacted 形式
失敗排除:
- 新對話 model 回覆含原文 → 檢查 chat context assembly 是否過濾
conversations.deleted_at IS NOT NULL - 稽核表查得到明文 → 檢查 purge policy 是否對
messages.content_redacted真的執行 clear / hard delete
3.5 Refusal 訊息持久化於 messages.refused(persist-refusal-and-label-new-chat)
migration 0013 起,每個 refusal 助理回合(audit-block / pipeline_refusal / pipeline_error)都被寫入 messages 並標記 refused = 1,使重新載入歷史對話 時 RefusalMessage.vue 能完整還原(含「可能原因」「建議下一步」區塊)。
3.5.1 驗證寫入
bash
# 觸發 audit-block:以含 api_key=... 的 query 提問
# 之後查詢
wrangler d1 execute <DB> --remote \
--command "SELECT id, role, refused, content_text FROM messages \
WHERE conversation_id = '<conv-id>' ORDER BY created_at"預期:
- 至少一筆
role = user、refused = 0 - 緊接著一筆
role = assistant、refused = 1、content_text為 「抱歉,我無法回答這個問題。」
3.5.2 驗證 reload UI
- 登入 → 在
/觸發 refusal(audit-block 或 pipeline 拒答) - 切換至其他對話再切回,或重新整理瀏覽器
- 助理回合應渲染
RefusalMessage.vue(不是普通訊息泡泡)
3.5.3 不回填舊資料(design Non-Goal 4)
migration 之前產生的 conversation 若曾觸發拒答,原本就沒寫入 assistant 列, migration 0013 不會逆向補寫。重載時這些對話仍只看見使用者的提問——這是 刻意行為,而非 bug。新建立的對話從第一次拒答起即正確持久化。
bash
# 確認舊 row 全為 refused = 0
wrangler d1 execute <DB> --remote \
--command "SELECT COUNT(*) AS legacy FROM messages WHERE refused = 1 \
AND created_at < '<migration-deploy-time>'"
# legacy = 0 才正常4. Integration Test 對應
本文件為人工驗證;對應的自動化測試應落在:
test/integration/chat/stale-follow-up.test.ts(或同等命名)— 驗證 §2test/integration/conversations/delete-purge.test.ts(或同等命名)— 驗證 §3test/integration/messages-refused-migration.test.ts— 驗證 §3.5 migration 形狀test/integration/web-chat-persistence.test.ts— 驗證 §3.5 寫入路徑(三條 refusal + accepted)test/integration/conversation-messages-refused.test.ts— 驗證 §3.5 reload API contract
若自動化測試缺漏 → 回到 governance 1.6 任務補測。
5. 回報格式
每項檢查完成後,以下列格式回報:
Stale §2.2 OK
Stale §2.3 OK
Delete §3.2 OK
Delete §3.3 問題: title 被清但 content 仍含原文
Delete §3.4 skip(無法重現 follow-up 幻覺)6. 常見陷阱
wrangler d1 execute忘加--remote→ 查到 local sqlite,看不到 production 真實狀態- 未等 purge 延遲完成(若為 async job)就查 D1 → 誤判為 FAIL;應在刪除後確認「delete policy 觸發形式」(sync on delete vs. scheduled sweep)
PURGE-CANARY-<timestamp>留在 browser localStorage / IndexedDB → 不是 server 殘留,應在新 session / 無痕視窗測試- 測試期間其他使用者同時在用同一環境 → 可能污染最新
query_logs,應以conversation_id或 timestamp 限縮查詢