#!/usr/bin/env bash
# check-remove-cleanup.sh — 驗**使用者真的會走的那條路**:
# 從各庫頁按「移除」→ 對話框 → 勾「順便清掉」→ 看到清單 → 按確定(arcrun-rag#138)。
#
# 🔴 為什麼要這一支:Go 那邊的迴歸網只證明「函式做對事」,證明不了
# 「按鈕存在、清單真的攤出來、第四個參數真的傳下去」。#138 的驗收條件裡
# 「使用者要能在動手前看到將要刪掉哪些東西」**只有前端驗得到**。
# (同 check-render.sh 的理由:改完 UI 不准拿沒看過的畫面交件。)
#
# 做法沿用 check-render.sh:把 dist 複製到暫存、插一支 mock.js 假扮 window.go,
# 再用 headless Chrome 真的載入、真的點下去,最後 --dump-dom 讀結果。
set -uo pipefail
cd "$(dirname "$0")"
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
[ -x "$CHROME" ] || { echo "❌ 找不到 Chrome,跳過"; exit 0; }
[ -d frontend/dist ] || { echo "❌ 沒有 frontend/dist——先 (cd frontend && npm run build)"; exit 1; }
W=$(mktemp -d)
SRV_PID=""
cleanup() { rm -rf "$W"; [ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null; }
trap cleanup EXIT
lsof -ti tcp:8798 2>/dev/null | xargs -r kill 2>/dev/null; sleep 0.3
cp -R frontend/dist/* "$W/"
cat > "$W/mock.js" <<'JS'
// 假的 window.go:一個知識庫、一個看守中的資料夾,PlanFolderCleanup 回一份
// 形狀與 collector.CleanupPlan 完全一致的清單(含「留著不動」那一半)。
window.__calls = [];
window.go = { main: { App: {
GetState: async () => ({
version: 'v0', statusBig: '看守中', statusSub: '', syncing: false,
engine: 'workers-ai', geminiKey: '', extractedOK: 0,
accounts: [{ name: '測試庫', host: 'arcrun-cypher-executor.example.workers.dev',
folders: [{ path: '/kb/pms', accIdx: 0, retiring: false }] }],
}),
PlanFolderCleanup: async (acc, path) => {
window.__calls.push(['PlanFolderCleanup', acc, path]);
return {
root: path, files: 52, bytes: 1234,
remove: [
{ rel: '.arcrun-rag', is_dir: true, kind: 'workspace', evidence: '依據甲', files: 1, bytes: 10 },
{ rel: '.wiki', is_dir: true, kind: 'wiki-dir', evidence: '依據乙', files: 8, bytes: 100 },
{ rel: 'docs/.wiki', is_dir: true, kind: 'wiki-dir', evidence: '依據乙', files: 43, bytes: 900 },
],
keep: [{ rel: 'pms_v1_legacy', reason: '這個資料夾也還在同步清單裡' }],
};
},
RemoveFolder: async (acc, path, takedown, cleanupLocal) => {
window.__calls.push(['RemoveFolder', acc, path, takedown, cleanupLocal]);
},
SyncNow: async () => {}, PickFolder: async () => '', AddFolder: async () => {},
SetAI: async () => {}, OpenURL: () => {}, Connect: async () => {},
CheckUpdate: async () => ({}), OpenLogFolder: () => {}, LogFolderPath: () => '',
ListApps: async () => ({ apps: [] }),
} } };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const say = (k, v) => {
const d = document.createElement('div');
d.setAttribute('data-check', k); d.textContent = String(v);
document.body.appendChild(d);
};
addEventListener('DOMContentLoaded', async () => {
await sleep(600); // 等 main.js 拿到 state、畫完側欄
// 走到各庫頁(使用者點側邊欄那個庫)
const nav = [...document.querySelectorAll('.nav')].find((b) => (b.dataset.p || '').startsWith('lib:'));
if (nav) nav.click(); else location.hash = '';
await sleep(300);
const rm = document.querySelector('[data-rm]');
say('有移除按鈕', !!rm);
if (!rm) { say('done', 1); return; }
rm.click();
await sleep(200);
const box = document.getElementById('rmClean');
say('有清理勾選框', !!box);
say('預設不勾', box ? box.checked === false : false);
if (!box) { say('done', 1); return; }
box.checked = true; box.dispatchEvent(new Event('change'));
await sleep(400);
const planTxt = (document.getElementById('rmPlan') || {}).textContent || '';
say('清單有列出要刪的路徑', planTxt.includes('.arcrun-rag') && planTxt.includes('docs/.wiki'));
say('清單有列出留著不動的', planTxt.includes('pms_v1_legacy'));
say('清單有講幾個檔', planTxt.includes('52'));
document.getElementById('c2').click();
await sleep(400);
const call = window.__calls.find((c) => c[0] === 'RemoveFolder');
say('有呼叫 RemoveFolder', !!call);
say('第四個參數帶了 true', call ? call[4] === true : false);
say('路徑正確', call ? call[2] === '/kb/pms' : false);
say('done', 1);
});
JS
python3 - "$W" <<'PY'
import sys
p = sys.argv[1] + '/index.html'
s = open(p, encoding='utf-8').read()
s = s.replace('src="/assets/', 'src="./assets/').replace('href="/assets/', 'href="./assets/')
s = s.replace('\n