472 lines
20 KiB
JavaScript
472 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|||
|
|
"use strict";
|
||
|
|
|
||
|
|
const assert = require("node:assert");
|
||
|
|
const fs = require("node:fs");
|
||
|
|
const os = require("node:os");
|
||
|
|
const path = require("node:path");
|
||
|
|
const { chromium } = require("playwright");
|
||
|
|
|
||
|
|
const TASK = "task518-onlyoffice-real-iframe-session-scope-smoke";
|
||
|
|
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||
|
|
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 120_000);
|
||
|
|
const PROBE_DOCX_PATH = process.env.MNOTE_ONLYOFFICE_PROBE_DOCX
|
||
|
|
|| "/tmp/mnote-onlyoffice-manual/mnote-onlyoffice-upload-test.docx";
|
||
|
|
const OUT_DIR = path.join(process.cwd(), "tmp", TASK);
|
||
|
|
const SCREENSHOT_DIR = path.join(OUT_DIR, "screenshots");
|
||
|
|
const RESULT_PATH = path.join(OUT_DIR, "result.json");
|
||
|
|
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||
|
|
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
|
||
|
|
.find((candidate) => fs.existsSync(candidate));
|
||
|
|
|
||
|
|
function fileUrl(localPath) {
|
||
|
|
return `file://${localPath}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function localOfficeFileUrl(root, relativePath) {
|
||
|
|
const url = new URL(`${BASE_URL}/api/local-folder/files/open`);
|
||
|
|
url.searchParams.set("rootUri", fileUrl(root));
|
||
|
|
url.searchParams.set("path", relativePath);
|
||
|
|
return url.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
function onlyofficeUrl(root, relativePath, assetId) {
|
||
|
|
const url = new URL(`${BASE_URL}/onlyoffice`);
|
||
|
|
url.searchParams.set("fileUrl", localOfficeFileUrl(root, relativePath));
|
||
|
|
url.searchParams.set("fileName", path.basename(relativePath));
|
||
|
|
url.searchParams.set("fileType", "docx");
|
||
|
|
url.searchParams.set("assetId", assetId);
|
||
|
|
url.searchParams.set("documentId", "local-md:Page~2FPage.md");
|
||
|
|
url.searchParams.set("mode", "edit");
|
||
|
|
return url.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
function bridgeToolPayload(sessionId, assetId, allowedResourceIds, toolName = "mnote.onlyoffice.document.insert_text", options = {}) {
|
||
|
|
return {
|
||
|
|
toolName,
|
||
|
|
workspaceId: "local-ws:task518",
|
||
|
|
documentId: "local-md:Page~2FPage.md",
|
||
|
|
actorId: "user_1",
|
||
|
|
sessionId: `task518-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||
|
|
runId: "task518-run",
|
||
|
|
toolCallId: "task518-call",
|
||
|
|
traceId: "task518-trace",
|
||
|
|
idempotencyKey: `task518-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||
|
|
dryRun: options.dryRun !== false,
|
||
|
|
capabilityScope: ["office.write"],
|
||
|
|
args: {
|
||
|
|
onlyofficeSessionId: sessionId,
|
||
|
|
text: options.text || "task518 dry run",
|
||
|
|
aiAccessScope: {
|
||
|
|
permissionLevel: "read_write",
|
||
|
|
allowedResourceIds,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function quickLogin(page) {
|
||
|
|
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||
|
|
const quickLoginButton = page.getByRole("button", { name: "测试账号快速登录" });
|
||
|
|
if (await quickLoginButton.count()) {
|
||
|
|
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
|
||
|
|
await page.waitForURL((url) => url.pathname !== "/auth", { timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function waitForOfficeReady(page, label) {
|
||
|
|
await page.waitForFunction(
|
||
|
|
() => {
|
||
|
|
const debug = window.__MNOTE_ONLYOFFICE_DEBUG__ || null;
|
||
|
|
const frameCount = document.querySelectorAll("#onlyoffice-frame iframe, #onlyoffice-frame canvas").length;
|
||
|
|
return Boolean(debug && debug.bridgeSessionId && debug.bridgeToken)
|
||
|
|
&& Boolean(window.__MNOTE_ONLYOFFICE_EDITOR__)
|
||
|
|
&& (Boolean(window.__MNOTE_ONLYOFFICE_READY__) || frameCount > 0);
|
||
|
|
},
|
||
|
|
null,
|
||
|
|
{ timeout: UI_TIMEOUT_MS },
|
||
|
|
);
|
||
|
|
await page.screenshot({ path: path.join(SCREENSHOT_DIR, `${label}.png`), fullPage: true });
|
||
|
|
return await page.evaluate(() => ({
|
||
|
|
url: location.href,
|
||
|
|
ready: Boolean(window.__MNOTE_ONLYOFFICE_READY__),
|
||
|
|
editorExists: Boolean(window.__MNOTE_ONLYOFFICE_EDITOR__),
|
||
|
|
frameCount: document.querySelectorAll("#onlyoffice-frame iframe, #onlyoffice-frame canvas").length,
|
||
|
|
errorVisible: document.getElementById("onlyoffice-error")?.getAttribute("data-visible") || "",
|
||
|
|
debug: window.__MNOTE_ONLYOFFICE_DEBUG__ || null,
|
||
|
|
errorLog: Array.isArray(window.__MNOTE_ONLYOFFICE_ERRLOG__) ? window.__MNOTE_ONLYOFFICE_ERRLOG__.slice() : [],
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function readBridgeSessions(context) {
|
||
|
|
const response = await context.request.fetch(`${BASE_URL}/api/onlyoffice/bridge/sessions`, {
|
||
|
|
method: "GET",
|
||
|
|
timeout: UI_TIMEOUT_MS,
|
||
|
|
});
|
||
|
|
const body = await response.json().catch(async () => ({ raw: await response.text() }));
|
||
|
|
return {
|
||
|
|
status: response.status(),
|
||
|
|
body,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function waitForBridgeSession(context, sessionId) {
|
||
|
|
const deadline = Date.now() + UI_TIMEOUT_MS;
|
||
|
|
let last = null;
|
||
|
|
while (Date.now() < deadline) {
|
||
|
|
last = await readBridgeSessions(context);
|
||
|
|
if (
|
||
|
|
last.status === 200
|
||
|
|
&& Array.isArray(last.body?.sessions)
|
||
|
|
&& last.body.sessions.some((session) => session.sessionId === sessionId)
|
||
|
|
) {
|
||
|
|
return last;
|
||
|
|
}
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
|
|
}
|
||
|
|
throw new Error(`bridge session 未注册: ${sessionId}; last=${JSON.stringify(last)}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function runBridgeSelection(page) {
|
||
|
|
return await page.evaluate(async () => {
|
||
|
|
return await window.__MNOTE_ONLYOFFICE_BRIDGE__.run("selection.get", {}, 30_000);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function runBridgeExport(page, format = "html") {
|
||
|
|
return await page.evaluate(async (selectedFormat) => {
|
||
|
|
return await window.__MNOTE_ONLYOFFICE_BRIDGE__.run("document.export", { format: selectedFormat }, 30_000);
|
||
|
|
}, format);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function postToolCall(context, payload) {
|
||
|
|
const response = await context.request.fetch(`${BASE_URL}/api/hermes/tools/mnote/call`, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"content-type": "application/json",
|
||
|
|
"x-mnote-actor-id": "user_1",
|
||
|
|
},
|
||
|
|
data: payload,
|
||
|
|
timeout: UI_TIMEOUT_MS,
|
||
|
|
});
|
||
|
|
const body = await response.json().catch(async () => ({ raw: await response.text() }));
|
||
|
|
return {
|
||
|
|
status: response.status(),
|
||
|
|
headers: response.headers(),
|
||
|
|
body,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function classifyOnlyOfficeSignals(result, states, selections, scopeChecks) {
|
||
|
|
const bridgeTranslation404 = result.httpErrors.filter((entry) => (
|
||
|
|
/\/api\/onlyoffice\/bridge\/plugin\/index\/translations\//.test(entry.url)
|
||
|
|
&& entry.status === 404
|
||
|
|
));
|
||
|
|
const mnoteBridgePluginHttpErrors = result.httpErrors.filter((entry) => (
|
||
|
|
/\/api\/onlyoffice\/bridge\/plugin\//.test(entry.url)
|
||
|
|
&& !/\/api\/onlyoffice\/bridge\/plugin\/index\/translations\//.test(entry.url)
|
||
|
|
));
|
||
|
|
const builtinPluginNoise = result.httpErrors.filter((entry) => (
|
||
|
|
/\/sdkjs-plugins\/|custom.?assistant|annotation/i.test(entry.url)
|
||
|
|
));
|
||
|
|
const bridgeConsoleNoise = result.consoleErrors.filter((entry) => (
|
||
|
|
/\/api\/onlyoffice\/bridge\/plugin\/index\/translations\//.test(entry.text)
|
||
|
|
|| /Failed to load resource: the server responded with a status of 404/i.test(entry.text)
|
||
|
|
));
|
||
|
|
const errorCodeMinus18 = [
|
||
|
|
...result.consoleErrors.map((entry) => entry.text || ""),
|
||
|
|
...states.flatMap((state) => Array.isArray(state.errorLog) ? state.errorLog.map((item) => JSON.stringify(item)) : []),
|
||
|
|
].some((text) => /errorCode["'=:\s-]*-18|\b-18\b/.test(text));
|
||
|
|
const mainDocumentFailures = [
|
||
|
|
...result.networkFailures.filter((entry) => (
|
||
|
|
!/\/api\/onlyoffice\/bridge\/commands\/next/.test(entry.url)
|
||
|
|
)),
|
||
|
|
...result.httpErrors.filter((entry) => {
|
||
|
|
if (/\/api\/onlyoffice\/bridge\/plugin\/index\/translations\//.test(entry.url)) return false;
|
||
|
|
if (/\/sdkjs-plugins\/|custom.?assistant|annotation/i.test(entry.url)) return false;
|
||
|
|
if (/\/api\/onlyoffice\/bridge\/plugin\//.test(entry.url)) return false;
|
||
|
|
if (/\/api\/onlyoffice\/bridge\/commands\/next/.test(entry.url)) return false;
|
||
|
|
return /\/onlyoffice-server\/|\/api\/onlyoffice\/proxy|\/api\/local-folder\/files\/open/.test(entry.url);
|
||
|
|
}),
|
||
|
|
...states.filter((state) => state.errorVisible || !state.editorExists || !state.ready).map((state) => ({
|
||
|
|
url: state.url,
|
||
|
|
ready: state.ready,
|
||
|
|
editorExists: state.editorExists,
|
||
|
|
errorVisible: state.errorVisible,
|
||
|
|
})),
|
||
|
|
];
|
||
|
|
return {
|
||
|
|
mnoteBridgePlugin: {
|
||
|
|
configUrls: states.map((state) => state.debug?.bridgePluginConfigUrl || ""),
|
||
|
|
httpErrors: mnoteBridgePluginHttpErrors,
|
||
|
|
sessionsRegistered: states.every((state) => (
|
||
|
|
result.bridgeSessionsFinal?.body?.sessions || []
|
||
|
|
).some((session) => session.sessionId === state.debug?.bridgeSessionId)),
|
||
|
|
commandLoopOk: scopeChecks?.allowedBDryRun?.status === 200
|
||
|
|
&& scopeChecks?.allowedBWrite?.status === 200,
|
||
|
|
},
|
||
|
|
bridgeTranslationNoise: {
|
||
|
|
count: bridgeTranslation404.length,
|
||
|
|
urls: Array.from(new Set(bridgeTranslation404.map((entry) => entry.url))),
|
||
|
|
},
|
||
|
|
builtinPluginNoise: {
|
||
|
|
count: builtinPluginNoise.length,
|
||
|
|
urls: Array.from(new Set(builtinPluginNoise.map((entry) => entry.url))),
|
||
|
|
},
|
||
|
|
mainDocument: {
|
||
|
|
ready: mainDocumentFailures.length === 0 && selections.every((selection) => selection?.editorType === "word"),
|
||
|
|
failures: mainDocumentFailures,
|
||
|
|
errorCodeMinus18,
|
||
|
|
},
|
||
|
|
consoleNoise: {
|
||
|
|
bridgeTranslationOrGeneric404Count: bridgeConsoleNoise.length,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
assert(fs.existsSync(PROBE_DOCX_PATH), `缺少探测 docx: ${PROBE_DOCX_PATH}`);
|
||
|
|
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||
|
|
|
||
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task518-onlyoffice-"));
|
||
|
|
fs.mkdirSync(path.join(root, "Page"), { recursive: true });
|
||
|
|
fs.writeFileSync(path.join(root, ".mnote-placeholder"), "task518\n", "utf8");
|
||
|
|
fs.copyFileSync(PROBE_DOCX_PATH, path.join(root, "Page", "office-a.docx"));
|
||
|
|
fs.copyFileSync(PROBE_DOCX_PATH, path.join(root, "Page", "office-b.docx"));
|
||
|
|
|
||
|
|
const consoleErrors = [];
|
||
|
|
const networkFailures = [];
|
||
|
|
const httpErrors = [];
|
||
|
|
const browser = await chromium.launch({
|
||
|
|
headless: process.env.HEADFUL !== "1",
|
||
|
|
executablePath: CHROMIUM_EXECUTABLE_PATH,
|
||
|
|
});
|
||
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||
|
|
const authPage = await context.newPage();
|
||
|
|
|
||
|
|
const result = {
|
||
|
|
ok: false,
|
||
|
|
task: TASK,
|
||
|
|
baseUrl: BASE_URL,
|
||
|
|
root,
|
||
|
|
screenshots: [],
|
||
|
|
consoleErrors,
|
||
|
|
networkFailures,
|
||
|
|
httpErrors,
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
await quickLogin(authPage);
|
||
|
|
await authPage.close();
|
||
|
|
|
||
|
|
const pageA = await context.newPage();
|
||
|
|
const pageADuplicate = await context.newPage();
|
||
|
|
const pageB = await context.newPage();
|
||
|
|
for (const page of [pageA, pageADuplicate, pageB]) {
|
||
|
|
page.on("console", (message) => {
|
||
|
|
if (message.type() === "error") consoleErrors.push({ url: page.url(), text: message.text() });
|
||
|
|
});
|
||
|
|
page.on("requestfailed", (request) => {
|
||
|
|
networkFailures.push({ url: request.url(), failure: request.failure()?.errorText || "" });
|
||
|
|
});
|
||
|
|
page.on("response", (response) => {
|
||
|
|
if (response.status() >= 400) {
|
||
|
|
httpErrors.push({ url: response.url(), status: response.status() });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const assetA = "local-file:Page/office-a.docx";
|
||
|
|
const assetB = "local-file:Page/office-b.docx";
|
||
|
|
await pageA.goto(onlyofficeUrl(root, "Page/office-a.docx", assetA), {
|
||
|
|
waitUntil: "domcontentloaded",
|
||
|
|
timeout: UI_TIMEOUT_MS,
|
||
|
|
});
|
||
|
|
await pageADuplicate.goto(onlyofficeUrl(root, "Page/office-a.docx", assetA), {
|
||
|
|
waitUntil: "domcontentloaded",
|
||
|
|
timeout: UI_TIMEOUT_MS,
|
||
|
|
});
|
||
|
|
await pageB.goto(onlyofficeUrl(root, "Page/office-b.docx", assetB), {
|
||
|
|
waitUntil: "domcontentloaded",
|
||
|
|
timeout: UI_TIMEOUT_MS,
|
||
|
|
});
|
||
|
|
|
||
|
|
const stateA = await waitForOfficeReady(pageA, "office-a");
|
||
|
|
const stateADuplicate = await waitForOfficeReady(pageADuplicate, "office-a-duplicate");
|
||
|
|
const stateB = await waitForOfficeReady(pageB, "office-b");
|
||
|
|
result.screenshots.push(path.join(SCREENSHOT_DIR, "office-a.png"));
|
||
|
|
result.screenshots.push(path.join(SCREENSHOT_DIR, "office-a-duplicate.png"));
|
||
|
|
result.screenshots.push(path.join(SCREENSHOT_DIR, "office-b.png"));
|
||
|
|
assert.equal(stateA.errorVisible, "", `A 页面不应显示错误: ${JSON.stringify(stateA)}`);
|
||
|
|
assert.equal(stateADuplicate.errorVisible, "", `A duplicate 页面不应显示错误: ${JSON.stringify(stateADuplicate)}`);
|
||
|
|
assert.equal(stateB.errorVisible, "", `B 页面不应显示错误: ${JSON.stringify(stateB)}`);
|
||
|
|
assert(stateA.debug.bridgeSessionId !== stateB.debug.bridgeSessionId, `A/B sessionId 不应相同: ${JSON.stringify({ a: stateA.debug, b: stateB.debug })}`);
|
||
|
|
assert(
|
||
|
|
stateA.debug.bridgeSessionId !== stateADuplicate.debug.bridgeSessionId,
|
||
|
|
`同一 Office 文档双 tab sessionId 不应相同: ${JSON.stringify({ a: stateA.debug, duplicate: stateADuplicate.debug })}`,
|
||
|
|
);
|
||
|
|
assert.equal(stateA.debug.docKey, stateADuplicate.debug.docKey, `同一 Office 文档双 tab 应共享 docKey 但使用不同 session salt: ${JSON.stringify({ a: stateA.debug, duplicate: stateADuplicate.debug })}`);
|
||
|
|
assert.equal(stateA.debug.assetId, assetA, `A debug assetId 不匹配: ${JSON.stringify(stateA.debug)}`);
|
||
|
|
assert.equal(stateADuplicate.debug.assetId, assetA, `A duplicate debug assetId 不匹配: ${JSON.stringify(stateADuplicate.debug)}`);
|
||
|
|
assert.equal(stateB.debug.assetId, assetB, `B debug assetId 不匹配: ${JSON.stringify(stateB.debug)}`);
|
||
|
|
|
||
|
|
result.officeAInitial = stateA;
|
||
|
|
result.officeADuplicateInitial = stateADuplicate;
|
||
|
|
result.officeBInitial = stateB;
|
||
|
|
result.bridgeSessionsAfterReady = await readBridgeSessions(context);
|
||
|
|
await waitForBridgeSession(context, stateA.debug.bridgeSessionId);
|
||
|
|
await waitForBridgeSession(context, stateADuplicate.debug.bridgeSessionId);
|
||
|
|
await waitForBridgeSession(context, stateB.debug.bridgeSessionId);
|
||
|
|
result.bridgeSessionsFinal = await readBridgeSessions(context);
|
||
|
|
const selectionA = await runBridgeSelection(pageA);
|
||
|
|
const selectionADuplicate = await runBridgeSelection(pageADuplicate);
|
||
|
|
const selectionB = await runBridgeSelection(pageB);
|
||
|
|
assert.equal(selectionA.editorType, "word", `A selection 应来自 word editor: ${JSON.stringify(selectionA)}`);
|
||
|
|
assert.equal(selectionADuplicate.editorType, "word", `A duplicate selection 应来自 word editor: ${JSON.stringify(selectionADuplicate)}`);
|
||
|
|
assert.equal(selectionB.editorType, "word", `B selection 应来自 word editor: ${JSON.stringify(selectionB)}`);
|
||
|
|
|
||
|
|
const forbiddenBFromScopeA = await postToolCall(
|
||
|
|
context,
|
||
|
|
bridgeToolPayload(stateB.debug.bridgeSessionId, assetB, [assetA, `resource:onlyoffice:${stateA.debug.documentId}:${assetA}`]),
|
||
|
|
);
|
||
|
|
assert.equal(forbiddenBFromScopeA.status, 403, JSON.stringify(forbiddenBFromScopeA));
|
||
|
|
assert.equal(forbiddenBFromScopeA.headers["x-error-code"], "mnote_onlyoffice_resource_scope_forbidden");
|
||
|
|
|
||
|
|
const allowedBDryRun = await postToolCall(
|
||
|
|
context,
|
||
|
|
bridgeToolPayload(stateB.debug.bridgeSessionId, assetB, [assetB, `resource:onlyoffice:${stateB.debug.documentId}:${assetB}`]),
|
||
|
|
);
|
||
|
|
assert.equal(allowedBDryRun.status, 200, JSON.stringify(allowedBDryRun));
|
||
|
|
assert.equal(allowedBDryRun.body.result.schema, "mnote.onlyoffice.action_plan.v1");
|
||
|
|
assert.equal(allowedBDryRun.body.result.sessionId, stateB.debug.bridgeSessionId);
|
||
|
|
|
||
|
|
const writeMarker = `task518-write-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||
|
|
const allowedBWrite = await postToolCall(
|
||
|
|
context,
|
||
|
|
bridgeToolPayload(
|
||
|
|
stateB.debug.bridgeSessionId,
|
||
|
|
assetB,
|
||
|
|
[assetB, `resource:onlyoffice:${stateB.debug.documentId}:${assetB}`],
|
||
|
|
"mnote.onlyoffice.document.insert_text",
|
||
|
|
{ dryRun: false, text: writeMarker },
|
||
|
|
),
|
||
|
|
);
|
||
|
|
assert.equal(allowedBWrite.status, 200, JSON.stringify(allowedBWrite));
|
||
|
|
assert.equal(allowedBWrite.body.result.schema, "mnote.onlyoffice.action_result.v1");
|
||
|
|
assert.equal(allowedBWrite.body.result.sessionId, stateB.debug.bridgeSessionId);
|
||
|
|
await pageA.screenshot({ path: path.join(SCREENSHOT_DIR, "office-a-after-write.png"), fullPage: true });
|
||
|
|
await pageB.screenshot({ path: path.join(SCREENSHOT_DIR, "office-b-after-write.png"), fullPage: true });
|
||
|
|
result.screenshots.push(path.join(SCREENSHOT_DIR, "office-a-after-write.png"));
|
||
|
|
result.screenshots.push(path.join(SCREENSHOT_DIR, "office-b-after-write.png"));
|
||
|
|
|
||
|
|
const exportAAfterWrite = await runBridgeExport(pageA, "html");
|
||
|
|
const exportBAfterWrite = await runBridgeExport(pageB, "html");
|
||
|
|
const exportAText = String(exportAAfterWrite?.content || exportAAfterWrite?.result?.content || "");
|
||
|
|
const exportBText = String(exportBAfterWrite?.content || exportBAfterWrite?.result?.content || "");
|
||
|
|
assert(
|
||
|
|
exportBText.includes(writeMarker),
|
||
|
|
`B 导出内容应包含写入标记 ${writeMarker}: ${JSON.stringify(exportBAfterWrite).slice(0, 1000)}`,
|
||
|
|
);
|
||
|
|
assert(
|
||
|
|
!exportAText.includes(writeMarker),
|
||
|
|
`A 导出内容不应包含 B 写入标记 ${writeMarker}: ${JSON.stringify(exportAAfterWrite).slice(0, 1000)}`,
|
||
|
|
);
|
||
|
|
|
||
|
|
const scopeChecks = {
|
||
|
|
forbiddenBFromScopeA: {
|
||
|
|
status: forbiddenBFromScopeA.status,
|
||
|
|
code: forbiddenBFromScopeA.headers["x-error-code"],
|
||
|
|
},
|
||
|
|
allowedBDryRun: {
|
||
|
|
status: allowedBDryRun.status,
|
||
|
|
schema: allowedBDryRun.body.result.schema,
|
||
|
|
sessionId: allowedBDryRun.body.result.sessionId,
|
||
|
|
},
|
||
|
|
allowedBWrite: {
|
||
|
|
status: allowedBWrite.status,
|
||
|
|
schema: allowedBWrite.body.result.schema,
|
||
|
|
sessionId: allowedBWrite.body.result.sessionId,
|
||
|
|
commandId: allowedBWrite.body.result.commandId,
|
||
|
|
},
|
||
|
|
exportAfterWrite: {
|
||
|
|
writeMarker,
|
||
|
|
aContainsMarker: exportAText.includes(writeMarker),
|
||
|
|
bContainsMarker: exportBText.includes(writeMarker),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
result.bridgeSessionsFinal = await readBridgeSessions(context);
|
||
|
|
const errorClassification = classifyOnlyOfficeSignals(
|
||
|
|
result,
|
||
|
|
[stateA, stateADuplicate, stateB],
|
||
|
|
[selectionA, selectionADuplicate, selectionB],
|
||
|
|
scopeChecks,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
errorClassification.mnoteBridgePlugin.httpErrors.length,
|
||
|
|
0,
|
||
|
|
`MNote bridge 插件 config/index 失败不能归为普通噪音: ${JSON.stringify(errorClassification.mnoteBridgePlugin.httpErrors)}`,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
errorClassification.mnoteBridgePlugin.sessionsRegistered,
|
||
|
|
true,
|
||
|
|
`MNote bridge 插件应完成 session 注册: ${JSON.stringify(errorClassification.mnoteBridgePlugin)}`,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
errorClassification.mnoteBridgePlugin.commandLoopOk,
|
||
|
|
true,
|
||
|
|
`MNote bridge 插件 command loop 应可用: ${JSON.stringify(errorClassification.mnoteBridgePlugin)}`,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
errorClassification.mainDocument.errorCodeMinus18,
|
||
|
|
false,
|
||
|
|
`errorCode=-18 必须作为主文档失败暴露: ${JSON.stringify(errorClassification.mainDocument)}`,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
errorClassification.mainDocument.ready,
|
||
|
|
true,
|
||
|
|
`主文档失败不能被插件噪音吞掉: ${JSON.stringify(errorClassification.mainDocument)}`,
|
||
|
|
);
|
||
|
|
|
||
|
|
Object.assign(result, {
|
||
|
|
ok: true,
|
||
|
|
errorClassification,
|
||
|
|
officeA: {
|
||
|
|
sessionId: stateA.debug.bridgeSessionId,
|
||
|
|
assetId: stateA.debug.assetId,
|
||
|
|
docKey: stateA.debug.docKey,
|
||
|
|
frameCount: stateA.frameCount,
|
||
|
|
selection: selectionA,
|
||
|
|
},
|
||
|
|
officeADuplicate: {
|
||
|
|
sessionId: stateADuplicate.debug.bridgeSessionId,
|
||
|
|
assetId: stateADuplicate.debug.assetId,
|
||
|
|
docKey: stateADuplicate.debug.docKey,
|
||
|
|
frameCount: stateADuplicate.frameCount,
|
||
|
|
selection: selectionADuplicate,
|
||
|
|
},
|
||
|
|
officeB: {
|
||
|
|
sessionId: stateB.debug.bridgeSessionId,
|
||
|
|
assetId: stateB.debug.assetId,
|
||
|
|
docKey: stateB.debug.docKey,
|
||
|
|
frameCount: stateB.frameCount,
|
||
|
|
selection: selectionB,
|
||
|
|
},
|
||
|
|
scopeChecks,
|
||
|
|
});
|
||
|
|
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||
|
|
console.log(JSON.stringify(result, null, 2));
|
||
|
|
} catch (error) {
|
||
|
|
result.error = error && error.stack ? error.stack : String(error);
|
||
|
|
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
await context.close().catch(() => undefined);
|
||
|
|
await browser.close().catch(() => undefined);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((error) => {
|
||
|
|
console.error(error && error.stack ? error.stack : error);
|
||
|
|
process.exit(1);
|
||
|
|
});
|