Files
Arcrun/scripts/verify-kv-retirement.sh
T
uncle6me-web 3d3973ecbc feat(storage): 工作流與 recipe 的家搬到 KBDB,KV 降成可丟棄的快取(Arcrun#16+#17)
leo 08-12:「我要的是寫進 KBDB,不是 KV,他的 Recipes、Cypher 是一段話,文字,
數據,一個 entry」「如果零件和工作流的 recipe 不見了,是很可怕的事情」。
同日實害:一次例行更新讓九支工作流在畫面上全部消失。#97 已修掉直接原因
(別再照名字猜使用者的資源、別再擅自新建一顆空的綁上去);這裡修更下面那一句——
**資產本來就不該只存在於一個會被換掉的暫存層裡**。

做法(換 binding,不改四十幾處呼叫端):
- lib/asset-keys.ts    哪些 KV key 是資產、對應 KBDB 哪一列。**唯一**要人看懂的那張表。
- lib/durable-store.ts 讀=KV 先行、miss 回源 KBDB 並補快取;寫=先 KBDB 再 KV,
                       KBDB 失敗就拋錯(禁假綠);**列舉一律回源**——空 KV 列出來是
                       「零筆」而不是「查不到」,那正是東西消失的形狀。
- index.ts             入口把 WEBHOOKS/RECIPES 換成上面那層。逐處改寫一定會漏,
                       而漏掉的那一處就是下一次「東西不見了」的入口。
- routes/storage.ts    /storage/audit(搬前搬後各數一次)+ /storage/migrate-to-kbdb
                       (只增不刪、冪等、逐筆回報成敗)。
- kbdb                 migration 0005 seed 四列 template(零 schema 異動,手法同 0003/0004)
                       + PUT /entries/:id 指定 id 的整列 upsert(通用原語,不是為誰開特例)。
- 衍生資料(idx:*、cron-idx:_all)不進 KBDB,讀不到就從資產重算。

⚠️ 狀態=◐ 半通,**別因為程式碼看起來完整就先合併**。
已實測:5 份 migration 在本機 D1 全數套用(含 0005);兩顆 worker 都能以改動後的
程式碼在本機開起來;tsc 錯誤數 7→7(既有,未新增)。
**沒跑到**:「砍掉 KV、資產還在」那一次端到端驗證——本次施工環境的權限閘不放行
執行 vitest/node/curl。那一次已寫成 scripts/verify-kv-retirement.sh,
在能執行的機器上跑一次就是證據。建議順序:先跑腳本、綠了再合併。

規格層依 D35 走 pending-changes.md「P-KV」提案,等 leo confirm(現行 active SDD
是 workflow-discovery,本案不在它的 tasks 內,故不自建 SDD、不改 rules 那張儲存表)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:51:58 +08:00

147 lines
7.5 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# verify-kv-retirement.sh — 把「換掉 KV,資產還在」真的做一次
#
# KV 退休(Leo/Arcrun#16 + #17)。交辦的驗收條件逐字是:
# 「證明『換掉/重建那個暫存層,資產還在』——不是說明它會在,是**真的弄一次給我看**」
# 「既有的東西要能搬過去,而且搬的過程不能弄丟任何一筆(搬之前先數,搬之後再數)」
# 這支腳本就是那一次。它做的事,照順序:
#
# 1. 開一台**全新的空**本機實例(local D1 + local KV,跑真的 migrations
# 2. 用平常那條路(POST /webhooks/named、POST /recipes、POST /auth-recipes
# 放進 9 支工作流 + 3 份 recipe——9 是照 2026-08-12 那天真的消失的數量
# 3. 數一次(KV 幾筆、KBDB 幾筆)
# 4. **把整個 KV 層砍掉重建**(rm -rf 那顆 KV 的本機儲存 → 重開 worker)
# =模擬 Arcrun#97 那天發生的事:worker 被綁到一顆全新的空 KV
# 5. 再數一次,並且**真的觸發一支工作流**確認它還跑得動
#
# 通過的定義(不通就 exit 1,不留模稜兩可):
# 砍掉 KV 之後,列出來仍然是 9 支、recipe 仍在、工作流仍然跑得出結果。
#
# ⚠️ 全程只碰本機(--local + --persist-to 到暫存目錄),**不碰任何線上實例**。
# 腳本裡沒有任何 --remote、沒有任何真實帳號憑證。
#
# 用法: bash scripts/verify-kv-retirement.sh
# 需要: node 22+、pnpm、可執行 npx wrangler / curl 的 shell
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK="$(mktemp -d)"
KBDB_PORT=8801
CYPHER_PORT=8802
TOKEN="e2e-local-token"
TENANT="leo-e2e"
WF_COUNT=9
KBDB="http://127.0.0.1:${KBDB_PORT}"
CYPHER="http://127.0.0.1:${CYPHER_PORT}"
kbdb_pid=""; cypher_pid=""
cleanup() {
[ -n "$kbdb_pid" ] && kill "$kbdb_pid" 2>/dev/null || true
[ -n "$cypher_pid" ] && kill "$cypher_pid" 2>/dev/null || true
rm -rf "$WORK"
}
trap cleanup EXIT
say() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
fail() { printf '\n\033[31m❌ %s\033[0m\n' "$*"; exit 1; }
wait_for() { # wait_for <url> <label>
for _ in $(seq 1 60); do
if curl -sf -m 2 "$1" >/dev/null 2>&1; then return 0; fi
sleep 1
done
fail "$2 沒有起來($1"
}
start_cypher() {
( cd "$REPO_ROOT/cypher-executor" && \
npx wrangler dev --local --port "$CYPHER_PORT" \
--config wrangler.test.toml \
--persist-to "$WORK/cypher-state" \
--var "KBDB_BASE_URL:$KBDB" \
--var "KBDB_INTERNAL_TOKEN:$TOKEN" \
--show-interactive-dev-session=false >"$WORK/cypher.log" 2>&1 ) &
cypher_pid=$!
wait_for "$CYPHER/health" "cypher-executor"
}
# ── 1. 空實例:真的跑 migrations ───────────────────────────────────────────────
say "1. 開一台全新的空實例(local D1 + local KV,跑真的 migrations"
( cd "$REPO_ROOT/kbdb" && npx wrangler d1 migrations apply DB --local --persist-to "$WORK/kbdb-state" )
( cd "$REPO_ROOT/kbdb" && \
npx wrangler dev --local --port "$KBDB_PORT" \
--persist-to "$WORK/kbdb-state" \
--var "KBDB_INTERNAL_TOKEN:$TOKEN" \
--show-interactive-dev-session=false >"$WORK/kbdb.log" 2>&1 ) &
kbdb_pid=$!
wait_for "$KBDB/health" "kbdb"
start_cypher
# ── 2. 用平常那條路放資產進去 ─────────────────────────────────────────────────
say "2. 放進 $WF_COUNT 支工作流 + 3 份 recipe(走的是 acr push 用的同一組端點)"
for i in $(seq 1 "$WF_COUNT"); do
curl -sf -X POST "$CYPHER/webhooks/named" \
-H 'Content-Type: application/json' -H "X-Arcrun-API-Key: $TENANT" \
-d "{\"name\":\"wf_$i\",\"description\":\"驗證用工作流 $i:把 text 轉大寫\",
\"graph\":{\"id\":\"wf_$i\",\"nodes\":[{\"id\":\"upper\",\"type\":\"Component\",\"componentId\":\"comp_uppercase\"}],\"edges\":[]}}" \
>/dev/null || fail "部署 wf_$i 失敗"
done
for svc in alpha beta; do
curl -sf -X POST "$CYPHER/recipes" \
-H 'Content-Type: application/json' \
-d "{\"canonical_id\":\"${svc}_send\",\"endpoint\":\"https://example.invalid/$svc\",\"description\":\"驗證用 recipe $svc\"}" \
>/dev/null || fail "建立 recipe $svc 失敗"
done
curl -sf -X POST "$CYPHER/auth-recipes" \
-H 'Content-Type: application/json' \
-d '{"service":"alpha","primitive":"static_key","base_url":"https://example.invalid",
"required_secrets":[{"key":"alpha_token","label":"Token","help_url":"https://example.invalid/docs"}],
"inject":{"header":{"Authorization":"Bearer {{secret.alpha_token}}"}}}' \
>/dev/null || fail "建立 auth recipe 失敗"
# ── 3. 搬之前先數 ─────────────────────────────────────────────────────────────
say "3. 數一次(/storage/auditKV 幾筆、KBDB 幾筆、差幾筆)"
curl -s "$CYPHER/storage/audit" | tee "$WORK/audit-before.json"; echo
before_wf="$(curl -s "$CYPHER/webhooks/named" -H "X-Arcrun-API-Key: $TENANT" | grep -o '"total":[0-9]*' | cut -d: -f2)"
echo "部署後列出來的工作流數:$before_wf"
[ "$before_wf" = "$WF_COUNT" ] || fail "還沒開始拆就對不上:期望 $WF_COUNT,實得 $before_wf"
# ── 4. 把 KV 層砍掉重建(模擬 2026-08-12 那天) ────────────────────────────────
say "4. 砍掉整個 KV 層並重建 —— 模擬 Arcrun#97 那天『worker 被綁到一顆全新的空 KV』"
kill "$cypher_pid" 2>/dev/null || true; wait "$cypher_pid" 2>/dev/null || true; cypher_pid=""
rm -rf "$WORK/cypher-state" # ← 這一行就是「那個暫存層被換掉」
echo "已刪除:$WORK/cypher-statecypher 的整顆本機 KV"
start_cypher
# ── 5. 再數一次,而且真的跑一支 ───────────────────────────────────────────────
say "5. KV 全空之後,再數一次"
after_json="$(curl -s "$CYPHER/webhooks/named" -H "X-Arcrun-API-Key: $TENANT")"
echo "$after_json" | head -c 400; echo
after_wf="$(echo "$after_json" | grep -o '"total":[0-9]*' | cut -d: -f2)"
echo "KV 砍掉重建後列出來的工作流數:$after_wf"
[ "$after_wf" = "$WF_COUNT" ] || fail "工作流少了:期望 $WF_COUNT,實得 $after_wf —— 資產沒有被保住"
recipes_after="$(curl -s "$CYPHER/recipes" | grep -o '"count":[0-9]*' | cut -d: -f2)"
echo "KV 砍掉重建後的 recipe 數:$recipes_after"
[ "${recipes_after:-0}" -ge 2 ] || fail "recipe 少了:期望 >=2,實得 ${recipes_after:-0}"
auth_after="$(curl -s "$CYPHER/auth-recipes/alpha" | grep -c '"success":true' || true)"
[ "$auth_after" = "1" ] || fail "auth recipe 不見了"
echo "auth recipe alpha:仍在"
say "5b. 不只是列得出來——真的觸發一支工作流"
run="$(curl -s -X POST "$CYPHER/webhooks/named/wf_3/trigger" \
-H 'Content-Type: application/json' -H "X-Arcrun-API-Key: $TENANT" \
-d '{"text":"still here"}')"
echo "$run" | head -c 400; echo
echo "$run" | grep -q 'STILL HERE' || fail "工作流列得出來卻跑不動——那不算資產還在"
say "結論"
printf '\033[32m✅ 通:整個 KV 層被砍掉重建之後,%s 支工作流、recipe、auth recipe 全部還在,且工作流真的跑得出結果。\033[0m\n' "$WF_COUNT"
echo " 資產的家=KBDBD1,一份資產一列 entry);KV 只是快取,砍掉會自己長回來。"