Newer
Older
TelosDB / tests / test_mcp_client.mjs
/**
 * TelosDB MCP クライアントテスト。
 * 前提: MCP が 127.0.0.1:3001 で起動していること(npm run test-and-heal または test-and-heal:pro で起動+実行される)。
 *
 * テスト内容:
 * [1] tools/list
 * [2] search_text
 * [3] Pro: スコアが全件 0.4 でないこと(警告のみの緩いチェック)
 * [4] Pro: ベクトル化 E2E — RE-INDEX 実行 → indexing_status が idle になるまで待機 → 検索 → vector_search_used が true かつスコアが全件 0.4 でないことを検証(ここで失敗すればベクトル化が効いていない)
 */
import axios from 'axios';

const API_BASE = "http://127.0.0.1:3001";

async function postMessage(axiosInstance, method, params = {}) {
    const res = await axiosInstance.post("/messages", {
        jsonrpc: "2.0",
        method: method,
        params: params,
        id: Date.now()
    });
    return res.data;
}

/**
 * Pro 時: 複数ヒットで全件スコア 0.4 なら LIKE フォールバックのみ=近似近傍が動いていない欠陥を検出する。
 * strict が true のときは失敗とする。false のときは警告のみ(既存データに依存するため)。
 */
async function assertProVectorScoresNotAllFallback(axiosInstance, strict = false) {
    const editionRes = await axiosInstance.get("/edition");
    if (editionRes.data?.edition !== "pro") return;

    const searchResult = await postMessage(axiosInstance, "tools/call", {
        name: "search_text",
        arguments: { content: "文", limit: 5 }
    });
    const raw = searchResult.result?.content?.[0]?.text;
    if (!raw) throw new Error("Pro vector check: no search response");
    let parsed;
    try {
        parsed = JSON.parse(raw);
    } catch {
        throw new Error("Pro vector check: search result is not JSON");
    }
    const items = Array.isArray(parsed) ? parsed : (parsed?.items ?? []);
    if (!Array.isArray(items) || items.length < 2) {
        if (strict) throw new Error("Pro vector check: need at least 2 docs to assert scores");
        return;
    }

    const likeFallbackScore = 0.4;
    const allFallback = items.every(
        (it) => Math.abs((it.similarity ?? 0) - likeFallbackScore) < 1e-5
    );
    if (allFallback) {
        const msg = "Pro: 全件スコア 0.4(キーワード一致のみ)。RE-INDEX でベクトル化してください。";
        if (strict) throw new Error(msg);
        console.warn("  Pro vector check:", msg);
        return;
    }
    console.log("  Pro vector check: at least one score != 0.4 (OK)");
}

/**
 * Pro ベクトル化 E2E: RE-INDEX 実行 → 完了待ち → 検索で vector_search_used とスコアを検証。
 * ベクトル化が正しく動いていることをテストする。
 */
async function testProVectorizationE2E(axiosInstance) {
    const editionRes = await axiosInstance.get("/edition");
    if (editionRes.data?.edition !== "pro") {
        console.log("  Pro vectorization E2E: skip (not Pro)");
        return;
    }

    console.log("  Pro vectorization E2E: calling RE-INDEX (lsa_retrain)...");
    await postMessage(axiosInstance, "lsa_retrain", {});
    const timeoutMs = 120_000;
    const pollMs = 2_000;
    const deadline = Date.now() + timeoutMs;
    let status = "";
    while (Date.now() < deadline) {
        const r = await axiosInstance.get("/indexing_status");
        status = (r.data && r.data.status) ? r.data.status : String(r.data ?? "");
        if (status === "idle") break;
        await new Promise((res) => setTimeout(res, pollMs));
    }
    if (status !== "idle") {
        throw new Error(`Pro vectorization E2E: RE-INDEX did not become idle within ${timeoutMs / 1000}s (status=${status})`);
    }
    console.log("  Pro vectorization E2E: RE-INDEX done, searching...");

    const searchResult = await postMessage(axiosInstance, "tools/call", {
        name: "search_text",
        arguments: { content: "文", limit: 5 }
    });
    const raw = searchResult.result?.content?.[0]?.text;
    if (!raw) throw new Error("Pro vectorization E2E: no search response");
    let parsed;
    try {
        parsed = JSON.parse(raw);
    } catch {
        throw new Error("Pro vectorization E2E: search result is not JSON");
    }
    const items = Array.isArray(parsed) ? parsed : (parsed?.items ?? []);
    const vectorSearchUsed = parsed?.vector_search_used === true;

    if (!vectorSearchUsed && items.length > 0) {
        throw new Error(
            "Pro vectorization E2E: vector_search_used が false。ベクトル化が反映されていません(vec_items/HNSW を確認してください)。"
        );
    }
    const likeFallbackScore = 0.4;
    const allFallback = items.length >= 2 && items.every(
        (it) => Math.abs((it.similarity ?? 0) - likeFallbackScore) < 1e-5
    );
    if (allFallback) {
        throw new Error(
            "Pro vectorization E2E: RE-INDEX 後も全件スコア 0.4。ベクトル検索が効いていません。"
        );
    }
    console.log("  Pro vectorization E2E: vector_search_used=true, スコア差あり (OK)");
}

async function testMcp() {
    console.log("=== TelosDB MCP Tool Test ===");
    const axiosInstance = axios.create({ baseURL: API_BASE });

    try {
        console.log("\n[1] Listing tools...");
        const toolsResult = await postMessage(axiosInstance, "tools/list");
        console.log("  Success: Found", toolsResult.result?.tools?.length, "tools.");

        console.log("\n[2] Testing search_text...");
        const searchResult = await postMessage(axiosInstance, "tools/call", {
            name: "search_text",
            arguments: { content: "宝くじ", limit: 5 }
        });
        const content = searchResult.result?.content?.[0]?.text;
        console.log("  Result:\n", content || "  No results");

        console.log("\n[3] Pro: assert vector search contributes (not all scores 0.4)...");
        await assertProVectorScoresNotAllFallback(axiosInstance);

        console.log("\n[4] Pro: vectorization E2E (RE-INDEX → 完了待ち → 検索で vector_search_used 検証)...");
        await testProVectorizationE2E(axiosInstance);

        console.log("\n=== Test Finished ===");
    } catch (e) {
        console.error("\nTest failed:", e.message || e);
        process.exit(1);
    }
}

testMcp();