/**
* E2E で作成したゴミを必ず削除する。スキップしない。
*
* 削除対象パターン:
* - 文書: path に e2e-test-search-doc / E2E_FOLDER_MONITOR / e2e-watched-file / telosdb-e2e-watch を含むもの、本文に E2E検索テスト用の文書 を含むもの
* - ファイル: tests/e2e/screenshots/docs-edit-modal.png
* - フォルダ: os.tmpdir() 内の telosdb-e2e-watch-*
* - 設定: run_on_login をオフ、monitor_paths からテスト用パス(C:\Test\Watch, D:\Documents\Notes, E:\ToRemove, telosdb-e2e-watch-*)を除去
*/
import fs from 'fs';
import path from 'path';
import os from 'os';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const E2E_SEARCH_PHRASE = 'E2E検索テスト用の文書';
const E2E_TEST_PATHS = ['C:\\Test\\Watch', 'D:\\Documents\\Notes', 'E:\\ToRemove'];
const E2E_DOC_PATH_PATTERNS = [
'e2e-test-search-doc',
'E2E_FOLDER_MONITOR',
'e2e-watched-file',
'telosdb-e2e-watch',
];
const SCREENSHOT_PATH = path.resolve(__dirname, '../screenshots/docs-edit-modal.png');
const SCREENSHOT_DIR = path.dirname(SCREENSHOT_PATH);
const DEFAULT_API = 'http://127.0.0.1:3001';
const MCP_RETRIES = 3;
const MCP_RETRY_MS = 800;
async function callMcpTool(apiBase, name, args = {}) {
const res = await fetch(`${apiBase}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
params: { name, arguments: args },
id: Date.now(),
}),
});
const data = await res.json();
if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
return data.result;
}
async function withRetry(fn) {
let lastErr;
for (let i = 0; i < MCP_RETRIES; i++) {
try {
return await fn();
} catch (e) {
lastErr = e;
if (i < MCP_RETRIES - 1) await new Promise((r) => setTimeout(r, MCP_RETRY_MS));
}
}
throw lastErr;
}
function isE2ETestDocPath(pathStr) {
if (!pathStr || typeof pathStr !== 'string') return false;
const p = pathStr.replace(/\\/g, '/');
return E2E_DOC_PATH_PATTERNS.some((pat) => p.includes(pat));
}
function parseToolResultText(raw) {
if (raw?.items) return raw;
const text = raw?.content?.[0]?.text;
if (typeof text === 'string') {
try {
return JSON.parse(text);
} catch (_) {
return { items: [] };
}
}
return { items: [] };
}
function parseSettingsGetResult(raw) {
if (raw?.monitor_paths !== undefined) return raw;
const text = raw?.content?.[0]?.text;
if (typeof text === 'string') {
try {
return JSON.parse(text);
} catch (_) {
return {};
}
}
return {};
}
/** テストで追加した文書をすべて削除。search_text と list_documents の両方で対象を探す。スキップしない。 */
export async function removeE2ETestDocumentViaMcp(apiBase = DEFAULT_API) {
await withRetry(async () => {
const seen = new Set();
const bySearchRaw = await callMcpTool(apiBase, 'search_text', {
content: E2E_SEARCH_PHRASE,
limit: 20,
});
const bySearch = parseToolResultText(bySearchRaw);
for (const it of bySearch?.items ?? []) {
const id = it.document_id ?? it.id;
if (id != null && !seen.has(id)) {
seen.add(id);
await callMcpTool(apiBase, 'delete_document', { document_id: id });
}
}
let page = 1;
const limit = 100;
for (;;) {
const raw = await callMcpTool(apiBase, 'list_documents', { page, limit });
const { items } = parseToolResultText(raw);
if (!items?.length) break;
for (const doc of items) {
const docPath = doc.path ?? '';
if (!isE2ETestDocPath(docPath)) continue;
const id = doc.id ?? doc.document_id;
if (id != null && !seen.has(id)) {
seen.add(id);
await callMcpTool(apiBase, 'delete_document', { document_id: id });
}
}
if (items.length < limit) break;
page++;
}
});
}
/** run_on_login をオフに戻す。スキップしない。 */
export async function resetRunOnLoginSetting(apiBase = DEFAULT_API) {
await withRetry(async () => {
const raw = await callMcpTool(apiBase, 'settings_get');
const cur = parseSettingsGetResult(raw);
if (!cur || cur.run_on_login !== true) return;
await callMcpTool(apiBase, 'settings_update', {
min_score: cur.min_score ?? 0.3,
limit: cur.limit ?? 5,
run_on_login: false,
standard_folders_enabled: cur.standard_folders_enabled ?? false,
monitor_paths: cur.monitor_paths ?? [],
watch_extensions: cur.watch_extensions ?? ['txt', 'md', 'json', 'html', 'css', 'js', 'mjs', 'ts', 'rs'],
});
});
}
/** テストで追加した監視パスを設定から削除。スキップしない。 */
export async function removeE2EMonitorPathsFromSettings(apiBase = DEFAULT_API) {
await withRetry(async () => {
const raw = await callMcpTool(apiBase, 'settings_get');
const cur = parseSettingsGetResult(raw);
const paths = cur?.monitor_paths ?? [];
const filtered = paths.filter((p) => {
const pathStr = typeof p === 'string' ? p : p?.path ?? '';
if (E2E_TEST_PATHS.includes(pathStr)) return false;
if (pathStr.includes('telosdb-e2e-watch-')) return false;
return true;
});
if (filtered.length === paths.length) return;
await callMcpTool(apiBase, 'settings_update', {
min_score: cur.min_score ?? 0.3,
limit: cur.limit ?? 5,
run_on_login: cur.run_on_login ?? false,
standard_folders_enabled: cur.standard_folders_enabled ?? false,
monitor_paths: filtered,
watch_extensions: cur.watch_extensions ?? ['txt', 'md', 'json', 'html', 'css', 'js', 'mjs', 'ts', 'rs'],
});
});
}
function rmSyncForce(dirPath) {
if (!fs.existsSync(dirPath)) return;
for (let r = 0; r < 3; r++) {
try {
fs.rmSync(dirPath, { recursive: true, maxRetries: 2 });
return;
} catch (e) {
if (r === 2) {
console.error('[e2e-cleanup] 削除失敗:', dirPath, e?.message);
throw e;
}
}
}
}
export function safeRemoveTempDir(dirPath) {
if (!dirPath || !fs.existsSync(dirPath)) return;
try {
fs.rmSync(dirPath, { recursive: true, maxRetries: 3 });
} catch (e) {
console.error('[e2e-cleanup] 一時ディレクトリ削除失敗:', dirPath, e?.message);
rmSyncForce(dirPath);
}
}
/** os.tmpdir() 内の telosdb-e2e-watch-* をすべて削除。スキップしない。 */
export function removeOrphanedE2ETempDirs() {
const tmp = os.tmpdir();
let entries;
try {
entries = fs.readdirSync(tmp, { withFileTypes: true });
} catch (err) {
console.error('[e2e-cleanup] tmpdir 読取失敗:', err?.message);
throw err;
}
for (const e of entries) {
if (!e.isDirectory() || !e.name.startsWith('telosdb-e2e-watch-')) continue;
const full = path.join(tmp, e.name);
try {
fs.rmSync(full, { recursive: true, maxRetries: 3 });
} catch (err) {
console.error('[e2e-cleanup] 残存一時ディレクトリ削除失敗:', full, err?.message);
rmSyncForce(full);
}
}
}
/** スクリーンショットファイルと空の screenshots ディレクトリを削除。スキップしない。 */
export function removeLeftoverScreenshot() {
try {
if (fs.existsSync(SCREENSHOT_PATH)) {
fs.unlinkSync(SCREENSHOT_PATH);
}
if (fs.existsSync(SCREENSHOT_DIR)) {
const names = fs.readdirSync(SCREENSHOT_DIR);
if (names.length === 0) fs.rmdirSync(SCREENSHOT_DIR);
}
} catch (e) {
console.error('[e2e-cleanup] スクリーンショット削除失敗:', SCREENSHOT_PATH, e?.message);
throw e;
}
}
/**
* 全クリーンアップを実行。スキップせず実行する。
* ファイルシステム → MCP の順。MCP はリトライ付き。
*/
export async function runAllCleanups(apiBase = DEFAULT_API) {
removeOrphanedE2ETempDirs();
removeLeftoverScreenshot();
await removeE2ETestDocumentViaMcp(apiBase);
await resetRunOnLoginSetting(apiBase);
await removeE2EMonitorPathsFromSettings(apiBase);
}