2026-06-26 20:01:02 +08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
const fs = require("node:fs");
|
|
|
|
|
const path = require("node:path");
|
|
|
|
|
|
|
|
|
|
const repoRoot = path.resolve(__dirname, "..");
|
2026-07-03 23:20:16 +08:00
|
|
|
const openHubRoot = process.env.OPENHUB_RESEARCH_ROOT || "/mnt/Data1T/Mnote_data/openhub/OpenHub";
|
2026-06-26 20:01:02 +08:00
|
|
|
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);
|
|
|
|
|
});
|