Files
mnote/recycle/20260712-pi-ts-openhub-retirement/scripts/task776-openhub-changed-files-bridge-smoke.js
T

235 lines
8.8 KiB
JavaScript

#!/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);
});