219 lines
9.3 KiB
JavaScript
219 lines
9.3 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const assert = require("node:assert");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { chromium } = require("playwright");
|
|
|
|
const ROOT = process.env.MNOTE_REPO_ROOT || "/mnt/Data1T/mnote";
|
|
const { BASE_URL, UI_TIMEOUT_MS } = require(path.join(ROOT, "scripts", "tree-shell-smoke-helpers"));
|
|
const {
|
|
setupWorkspaceAccess,
|
|
seedAiRuntime,
|
|
} = require(path.join(ROOT, "scripts", "lib", "control-plane-dev-seed"));
|
|
const TASK = "task557-page-ai-run-resume-smoke";
|
|
const OUTPUT_DIR = path.join(ROOT, "tmp", TASK);
|
|
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
|
const SCREENSHOT_PATH = path.join(OUTPUT_DIR, "page-ai-run-resume.png");
|
|
const CHROME = process.env.MNOTE_PAGE_AI_CHROME || "/usr/bin/google-chrome";
|
|
const RESUME_TIMEOUT_MS = Number(process.env.MNOTE_PAGE_AI_RESUME_TIMEOUT_MS || 90_000);
|
|
const ACTOR_ID = "mnote-e2e";
|
|
const WORKSPACE_ID = "local-ws:mnote-e2e:my-space";
|
|
const ROOT_PATH = "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space";
|
|
const ROOT_URI = `file://${ROOT_PATH}`;
|
|
const OWNER_REL = "knowledge-rag-fixtures-7-57/PageAiRunResumeSmoke.md";
|
|
const DOCUMENT_ID = `local-md:${OWNER_REL.replaceAll("/", "~2F")}`;
|
|
|
|
function ensureFixture() {
|
|
const ownerPath = path.join(ROOT_PATH, OWNER_REL);
|
|
fs.mkdirSync(path.dirname(ownerPath), { recursive: true });
|
|
fs.writeFileSync(ownerPath, "# Page AI Run Resume Smoke\n\n用于验证 Page AI host run journal afterSeq 恢复。\n", "utf8");
|
|
}
|
|
|
|
async function seedRun(requestContext) {
|
|
const stamp = Date.now();
|
|
const runId = `run_task557_resume_${stamp}`;
|
|
const sessionId = `mnote_task557_resume_${stamp}`;
|
|
const runtimeJson = JSON.stringify({ runId, status: "running", lastEvent: "message.delta" });
|
|
const payloadJson = JSON.stringify({
|
|
requestId: `task557_resume_${stamp}`,
|
|
agentId: "reasonix",
|
|
message: "resume smoke user prompt",
|
|
});
|
|
await seedAiRuntime(requestContext, BASE_URL, {
|
|
id: `arr_${stamp}`,
|
|
userId: ACTOR_ID,
|
|
workspaceId: WORKSPACE_ID,
|
|
documentId: DOCUMENT_ID,
|
|
sessionId,
|
|
runId,
|
|
title: "Resume smoke",
|
|
profile: "reasonix",
|
|
acpRuntime: "reasonix",
|
|
traceId: `trace_task557_${stamp}`,
|
|
status: "running",
|
|
runtimeJson: JSON.parse(runtimeJson),
|
|
payloadJson: JSON.parse(payloadJson),
|
|
events: [
|
|
{ id: `are_${stamp}_1`, eventType: "message.delta", payloadJson: { delta: "old-token" } },
|
|
{ id: `are_${stamp}_2`, eventType: "message.delta", payloadJson: { delta: "resumed-token" } },
|
|
],
|
|
timeoutMs: UI_TIMEOUT_MS,
|
|
});
|
|
return { runId, sessionId };
|
|
}
|
|
|
|
async function signIn(context) {
|
|
const response = await context.request.post(`${BASE_URL}/api/auth`, {
|
|
data: {
|
|
action: "auth:signIn",
|
|
args: {
|
|
provider: "password",
|
|
params: {
|
|
account: ACTOR_ID,
|
|
password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!",
|
|
flow: "signIn",
|
|
},
|
|
},
|
|
},
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
assert(response.ok(), `/api/auth 登录失败: ${response.status()} ${await response.text()}`);
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
ensureFixture();
|
|
const browser = await chromium.launch({
|
|
headless: process.env.MNOTE_PAGE_AI_VERIFY_HEADED !== "1",
|
|
executablePath: fs.existsSync(CHROME) ? CHROME : undefined,
|
|
});
|
|
const context = await browser.newContext({ viewport: { width: 1360, height: 900 } });
|
|
const page = await context.newPage();
|
|
const pageErrors = [];
|
|
const consoleErrors = [];
|
|
const apiRequests = [];
|
|
const apiResponses = [];
|
|
page.on("pageerror", (error) => pageErrors.push(String(error?.message || error)));
|
|
page.on("console", (message) => {
|
|
if (["error", "warning"].includes(message.type())) consoleErrors.push(`${message.type()}: ${message.text()}`);
|
|
});
|
|
page.on("request", (request) => {
|
|
if (/\/api\/(page-ai|hermes\/client)/.test(request.url())) {
|
|
apiRequests.push({ method: request.method(), url: request.url() });
|
|
}
|
|
});
|
|
page.on("response", (response) => {
|
|
if (/\/api\/(page-ai|hermes\/client)/.test(response.url())) {
|
|
apiResponses.push({ status: response.status(), url: response.url() });
|
|
}
|
|
});
|
|
try {
|
|
await signIn(context);
|
|
await setupWorkspaceAccess(context.request, BASE_URL, {
|
|
actorId: ACTOR_ID,
|
|
email: "mnote.e2e@example.com",
|
|
workspaceId: WORKSPACE_ID,
|
|
workspaceName: "MNote E2E Space",
|
|
rootPath: ROOT_PATH,
|
|
rootUri: ROOT_URI,
|
|
capabilities: ["ai"],
|
|
timeoutMs: UI_TIMEOUT_MS,
|
|
});
|
|
const seeded = await seedRun(context.request);
|
|
await page.addInitScript(({ documentId, runId, sessionId }) => {
|
|
window.localStorage.setItem(`hermes_page_ai_session:${documentId}:active-run`, JSON.stringify({
|
|
schema: "mnote.page_ai_active_run_snapshot.v1",
|
|
hostRunId: runId,
|
|
sessionId,
|
|
lastSeq: "000000000000000001",
|
|
status: "running",
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
}));
|
|
}, { documentId: DOCUMENT_ID, runId: seeded.runId, sessionId: seeded.sessionId });
|
|
|
|
const documentUrl = new URL(`${BASE_URL}/documents/${encodeURIComponent(DOCUMENT_ID)}`);
|
|
documentUrl.searchParams.set("sourceKind", "local_folder");
|
|
documentUrl.searchParams.set("rootUri", ROOT_URI);
|
|
documentUrl.searchParams.set("workspaceId", WORKSPACE_ID);
|
|
await page.goto(documentUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
|
try {
|
|
await page.waitForFunction(
|
|
(runId) => document.documentElement.getAttribute("data-mnote-page-ai-run-journal-resumed") === runId,
|
|
seeded.runId,
|
|
{ timeout: RESUME_TIMEOUT_MS },
|
|
);
|
|
await page.waitForFunction(
|
|
() => document.documentElement.getAttribute("data-mnote-page-ai-active-run-last-seq") === "000000000000000002",
|
|
null,
|
|
{ timeout: RESUME_TIMEOUT_MS },
|
|
);
|
|
} catch (error) {
|
|
const debugState = await page.evaluate(() => {
|
|
const drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
|
return {
|
|
attrs: Object.fromEntries(Array.from(document.documentElement.attributes).map((attr) => [attr.name, attr.value])),
|
|
drawerHidden: drawer ? drawer.hidden : null,
|
|
drawerText: drawer ? (drawer.textContent || "").slice(0, 1000) : "",
|
|
storageKeys: Object.keys(window.localStorage || {}).filter((key) => key.includes("page_ai") || key.includes("hermes_page_ai")),
|
|
};
|
|
}).catch((stateError) => ({ stateError: String(stateError) }));
|
|
debugState.apiRequests = apiRequests;
|
|
debugState.apiResponses = apiResponses;
|
|
debugState.pageErrors = pageErrors;
|
|
debugState.consoleErrors = consoleErrors;
|
|
await page.screenshot({ path: path.join(OUTPUT_DIR, "failure.png"), fullPage: true }).catch(() => undefined);
|
|
fs.writeFileSync(path.join(OUTPUT_DIR, "failure-state.json"), `${JSON.stringify(debugState, null, 2)}\n`, "utf8");
|
|
throw error;
|
|
}
|
|
const state = await page.evaluate((runId) => {
|
|
const drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
|
const text = drawer ? drawer.textContent || "" : "";
|
|
return {
|
|
resumedRunId: document.documentElement.getAttribute("data-mnote-page-ai-run-journal-resumed") || "",
|
|
activeHostRunId: document.documentElement.getAttribute("data-mnote-page-ai-active-host-run-id") || "",
|
|
lastSeq: document.documentElement.getAttribute("data-mnote-page-ai-active-run-last-seq") || "",
|
|
hasResumedToken: text.includes("resumed-token"),
|
|
hasSkippedToken: text.includes("old-token"),
|
|
runStatus: document.documentElement.getAttribute("data-mnote-page-ai-run-status") || "",
|
|
drawerText: text.slice(0, 500),
|
|
expectedRunId: runId,
|
|
};
|
|
}, seeded.runId);
|
|
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
|
assert.equal(state.resumedRunId, seeded.runId, `未标记 journal resumed: ${JSON.stringify(state, null, 2)}`);
|
|
assert.equal(state.activeHostRunId, seeded.runId, `active host run id 不匹配: ${JSON.stringify(state, null, 2)}`);
|
|
assert.equal(state.lastSeq, "000000000000000002", `lastSeq 未推进: ${JSON.stringify(state, null, 2)}`);
|
|
assert.equal(state.hasResumedToken, true, `未渲染 afterSeq 后的新事件: ${JSON.stringify(state, null, 2)}`);
|
|
assert.equal(state.hasSkippedToken, false, `重复渲染了 afterSeq 之前的事件: ${JSON.stringify(state, null, 2)}`);
|
|
assert.deepEqual(pageErrors, [], `页面异常: ${pageErrors.join("\n")}`);
|
|
const result = {
|
|
ok: true,
|
|
task: TASK,
|
|
baseUrl: BASE_URL,
|
|
documentId: DOCUMENT_ID,
|
|
runId: seeded.runId,
|
|
sessionId: seeded.sessionId,
|
|
state,
|
|
consoleErrors,
|
|
apiRequests,
|
|
apiResponses,
|
|
screenshot: SCREENSHOT_PATH,
|
|
};
|
|
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
console.log(JSON.stringify(result, null, 2));
|
|
} finally {
|
|
await browser.close().catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
fs.writeFileSync(path.join(OUTPUT_DIR, "failure.json"), `${JSON.stringify({ ok: false, task: TASK, error: error.stack || error.message || String(error) }, null, 2)}\n`, "utf8");
|
|
console.error(error.stack || error.message || String(error));
|
|
process.exit(1);
|
|
});
|