#!/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/Mnote_data/openhub/OpenHub"; const backendSessionPath = path.join(openHubRoot, "smart-query-backend/app/api/session.py"); const frontendApiPath = path.join(openHubRoot, "smart-query-frontend/src/services/api.js"); const diffViewerPath = path.join(openHubRoot, "smart-query-frontend/src/components/DiffViewer.jsx"); const embedPath = path.join(openHubRoot, "smart-query-frontend/src/mnoteEmbed.js"); const smartQueryPath = path.join(openHubRoot, "smart-query-frontend/src/pages/SmartQueryPage.jsx"); const mnoteRuntimePath = path.join(repoRoot, "rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js"); const task774ResultPath = path.join(repoRoot, "tmp/7-68-runtime/openhub-send-smoke-result.json"); const outputDir = path.join(repoRoot, "tmp/7-68-runtime"); const resultPath = path.join(outputDir, "openhub-changed-files-bridge-smoke-result.json"); const baseUrl = process.env.MNOTE_BASE_URL || "http://127.0.0.1:3000"; const testAccount = process.env.MNOTE_E2E_ACCOUNT || "mnote-e2e"; const testPassword = process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!"; const workspaceRoot = process.env.MNOTE_OPENHUB_SMOKE_ROOT || "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space"; function read(filePath) { return fs.readFileSync(filePath, "utf8"); } function assertCheck(failures, name, passed, details = undefined) { if (!passed) failures.push({ name, details }); } function loadTask774Probe() { if (!fs.existsSync(task774ResultPath)) return null; const payload = JSON.parse(fs.readFileSync(task774ResultPath, "utf8")); const sessionId = payload?.fileEdit?.stream?.sessionId; const fixturePath = payload?.fileEdit?.fixturePath; if (!payload?.ok || !sessionId || !fixturePath) return null; return { sessionId, fixturePath }; } async function signInCookie() { const response = await fetch(`${baseUrl}/api/auth`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ action: "auth:signIn", args: { provider: "password", params: { account: testAccount, password: testPassword, flow: "signIn", }, }, }), signal: AbortSignal.timeout(12_000), }); const text = await response.text(); if (!response.ok) { throw new Error(`/api/auth 登录失败: HTTP ${response.status} ${text.slice(0, 400)}`); } const setCookie = response.headers.get("set-cookie") || ""; const cookies = setCookie .split(/,(?=\s*[^;,\s]+=)/) .map((part) => part.split(";")[0].trim()) .filter(Boolean); if (!cookies.some((cookie) => cookie.startsWith("mnote_session="))) { throw new Error(`/api/auth 未返回 mnote_session cookie: ${setCookie.slice(0, 400)}`); } return cookies.join("; "); } async function bootstrapScopeQuery(cookieHeader) { const response = await fetch(`${baseUrl}/api/page-ai/openhub/bootstrap`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json", cookie: cookieHeader, }, body: JSON.stringify({ workspaceId: "local-ws:mnote-e2e:my-space", rootUri: `file://${workspaceRoot}`, pageResourceId: "task776-openhub-changed-files-bridge", pageTitle: "OpenHub changed files bridge smoke", allowedRoots: [`file://${workspaceRoot}`], }), signal: AbortSignal.timeout(12_000), }); const payload = await response.json().catch(() => null); if (!response.ok) { throw new Error(`OpenHub bootstrap 失败: HTTP ${response.status} ${JSON.stringify(payload).slice(0, 500)}`); } const iframeUrl = String(payload?.openhubIframeUrl || ""); const query = iframeUrl.includes("?") ? iframeUrl.slice(iframeUrl.indexOf("?")) : ""; if (!query.includes("mnoteScope=")) { throw new Error(`OpenHub bootstrap 未返回完整 mnoteScope query: ${iframeUrl.slice(0, 200)}`); } return query; } async function probeLiveEndpoint(probe) { if (!probe) { return { skipped: true, reason: "缺少 task774 真实文件编辑结果,先运行 MNOTE_OPENHUB_FILE_EDIT=1 node scripts/task774-openhub-mnote-send-and-file-edit-e2e.js" }; } const cookieHeader = await signInCookie(); const query = await bootstrapScopeQuery(cookieHeader); const url = `${baseUrl}/page-ai/openhub/ai/api/sessions/${encodeURIComponent(probe.sessionId)}/diff${query}`; const response = await fetch(url, { headers: { accept: "application/json", cookie: cookieHeader }, signal: AbortSignal.timeout(12_000), }); const text = await response.text(); let payload = null; try { payload = JSON.parse(text); } catch {} const changed = Array.isArray(payload?.diffs) ? payload.diffs : []; const matched = changed.some((item) => item.path === probe.fixturePath); return { skipped: false, ok: response.ok && matched, status: response.status, matched, expectedPath: probe.fixturePath, queryFromFreshBootstrap: true, changedPaths: changed.map((item) => item.path), diffAvailable: payload?.diffAvailable, source: payload?.source, limitation: payload?.limitation, snippet: text.slice(0, 800), }; } async function main() { fs.mkdirSync(outputDir, { recursive: true }); const failures = []; const backendSession = read(backendSessionPath); const frontendApi = read(frontendApiPath); const diffViewer = read(diffViewerPath); const embed = read(embedPath); const smartQuery = read(smartQueryPath); const mnoteRuntime = read(mnoteRuntimePath); assertCheck( failures, "OpenHub backend exposes session diff endpoint", backendSession.includes('@router.get("/api/sessions/{session_id}/diff")') && backendSession.includes("_changed_files_from_messages") && backendSession.includes("opencode_tool_events") ); assertCheck( failures, "backend extracts path only from write-like opencode tool metadata", backendSession.includes("_is_write_tool") && backendSession.includes("_iter_tool_path_values") && backendSession.includes('"filePath"') && backendSession.includes('"source": "opencode_tool_event"') ); assertCheck( failures, "backend guards workspace path and does not synthesize diff content", backendSession.includes("os.path.commonpath") && backendSession.includes('"diffAvailable": False') && backendSession.includes('"content": ""') && backendSession.includes("不生成或伪造 diff") ); assertCheck( failures, "frontend service still calls session diff endpoint", frontendApi.includes("getSessionDiff") && frontendApi.includes("/sessions/${sessionId}/diff") ); assertCheck( failures, "frontend postMessage bridge emits mnote open-file payload", embed.includes("postMNoteOpenFile") && embed.includes("type: 'mnote:open-file'") && embed.includes("source: payload.source || 'openhub-diff'") ); assertCheck( failures, "DiffViewer exposes changed path open action", diffViewer.includes("postMNoteOpenFile") && diffViewer.includes("rootRelativePath") && diffViewer.includes("diffAvailable") ); assertCheck( failures, "SmartQueryPage loads changed files after stream and exposes hidden bridge payload", smartQuery.includes("loadChangedFiles(finalConversationId)") && smartQuery.includes("data-mnote-openhub-changed-file") && smartQuery.includes("handleOpenChangedFile") ); assertCheck( failures, "MNote host listens for OpenHub open-file bridge", mnoteRuntime.includes("pageAiInstallMNoteOpenFileBridge") && mnoteRuntime.includes("message.type !== 'mnote:open-file'") && mnoteRuntime.includes("openhub-diff") && mnoteRuntime.includes("pageAiOpenOpencodeChangedFile") ); const liveProbeInput = loadTask774Probe(); let liveProbe; try { liveProbe = await probeLiveEndpoint(liveProbeInput); if (!liveProbe.skipped) { assertCheck(failures, "live MNote proxy returns real changed path from task774 session", liveProbe.ok, liveProbe); } } catch (error) { liveProbe = { skipped: false, ok: false, error: error instanceof Error ? error.message : String(error), reason: "MNote/OpenHub 服务不可达或 task774 session 已不可读", }; assertCheck(failures, "live MNote proxy returns real changed path from task774 session", false, liveProbe); } const result = { ok: failures.length === 0, task: "task776-openhub-changed-files-bridge-smoke", liveProbe, failures, }; fs.writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf8"); console.log(JSON.stringify(result, null, 2)); if (!result.ok) process.exit(1); } main().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : String(error)); process.exit(1); });