#!/usr/bin/env node "use strict"; const fs = require("node:fs"); const path = require("node:path"); const repoRoot = path.resolve(__dirname, ".."); const openHubRoot = process.env.OPENHUB_RESEARCH_ROOT || "/mnt/Data1T/tmp/mnote-openhub-research/OpenHub"; const files = { chatInput: path.join(openHubRoot, "smart-query-frontend/src/components/ChatInput.jsx"), smartQueryPage: path.join(openHubRoot, "smart-query-frontend/src/pages/SmartQueryPage.jsx"), api: path.join(openHubRoot, "smart-query-frontend/src/services/api.js"), openHubFrontendSrc: path.join(openHubRoot, "smart-query-frontend/src"), mnoteOpenHubRoute: path.join(repoRoot, "rust/crates/mnote-web/src/routes/page_ai_openhub.rs"), }; function read(file) { return fs.readFileSync(file, "utf8"); } function walkFiles(dir, result = []) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { walkFiles(fullPath, result); } else { result.push(fullPath); } } return result; } function extractBetween(source, startNeedle, endNeedle) { const start = source.indexOf(startNeedle); if (start < 0) return ""; const end = source.indexOf(endNeedle, start); return source.slice(start, end < 0 ? undefined : end); } function check(failures, name, passed, detail = "") { if (!passed) { failures.push(detail ? `${name}: ${detail}` : name); } } const chatInput = read(files.chatInput); const smartQueryPage = read(files.smartQueryPage); const api = read(files.api); const controlRow = extractBetween(chatInput, ""); const handleSend = extractBetween(smartQueryPage, "const handleSend = async", "const handleKeyPress"); const legacyFloatingSelectors = [ "mnote-openhub-context-bar", "mnote-openhub-native-context", "data-mnote-openhub-ai-quick-actions", "mnote_openhub_bridge_markup", "insertAddress(", ]; const frontendLegacyHits = []; for (const file of walkFiles(files.openHubFrontendSrc)) { const rel = path.relative(openHubRoot, file); const source = read(file); for (const selector of legacyFloatingSelectors) { if (source.includes(selector)) { frontendLegacyHits.push(`${rel} -> ${selector}`); } } } const mnoteRoute = fs.existsSync(files.mnoteOpenHubRoute) ? read(files.mnoteOpenHubRoute) : ""; const mnoteRouteLegacyHits = legacyFloatingSelectors.filter((selector) => mnoteRoute.includes(selector) ); const failures = []; check( failures, "ChatInput owns two native MNote context buttons", chatInput.includes("isMNoteOpenHubEmbed()") && chatInput.includes("requestMNoteActiveTabAddress") && chatInput.includes("data-mnote-openhub-current-tab-toggle") && chatInput.includes("data-mnote-openhub-current-folder-toggle"), "missing embed gate, context request helper, or native button data attributes" ); check( failures, "MNote context buttons are in the controls row beside agent/model controls", controlRow.includes("\}/.test(controlRow) && /icon=\{\}/.test(controlRow) && !/>当前页面<\/Button>/.test(controlRow) && !/>文件夹<\/Button>/.test(controlRow), "expected two icon-only buttons with distinct tooltip and aria labels" ); check( failures, "ChatInput sends mnote_context as hidden metadata instead of writing URL into textarea question", /handleSend\(undefined,\s*context\s*\?\s*\{/.test(handleMNoteSend) && handleMNoteSend.includes("kind: mnoteContextMode") && handleMNoteSend.includes("value: context.value") && !/setQuestion\s*\(\s*context\./.test(handleMNoteSend) && !/setQuestion\s*\(\s*mnoteContext/.test(handleMNoteSend) && !/question\s*=\s*context\.value/.test(handleMNoteSend), "context must flow through handleSend second argument and must not mutate the visible question value" ); check( failures, "SmartQueryPage accepts mnote_context and forwards it to queryDataService.queryDataStream", /const\s+handleSend\s*=\s*async\s*\(\s*overrideQuestion\s*,\s*mnoteContext\s*=\s*null\s*\)/.test( smartQueryPage ) && handleSend.includes("queryDataService.queryDataStream(") && /abortControllerRef\.current\.signal\s*,\s*mnoteContext/.test(handleSend), "handleSend must accept mnoteContext and pass it through the stream send call" ); check( failures, "OpenHub send body carries hidden mnote_context", /queryDataStream:\s*async\s*\([^)]*mnoteContext\s*=\s*null/.test(api) && api.includes("requestBody.mnote_context = mnoteContext") && !/question\s*:\s*.*mnoteContext/.test(api), "api.js must put mnoteContext into requestBody.mnote_context, not append it to question" ); check( failures, "OpenHub frontend no longer depends on legacy MNote floating button selectors", frontendLegacyHits.length === 0, frontendLegacyHits.join("; ") ); check( failures, "MNote OpenHub route no longer injects legacy floating context buttons", mnoteRouteLegacyHits.length === 0, mnoteRouteLegacyHits.join("; ") ); if (failures.length) { console.error("OpenHub native MNote context source smoke failed:"); for (const failure of failures) console.error(`- ${failure}`); process.exit(1); } console.log("OpenHub native MNote context source smoke passed.");