chore: checkpoint pi lab rust integration work

This commit is contained in:
Agent Board
2026-07-11 20:39:18 +08:00
parent d47f6447fd
commit d16eccfe10
39 changed files with 4843 additions and 455 deletions
+145 -13
View File
@@ -23,7 +23,7 @@ const PAGE_DIR = `pi-input-controls-${STAMP}`;
const PAGE_PATH = `${PAGE_DIR}/pi-input-controls-${STAMP}.md`;
const ROOT_PAGE_PATH = `pi-input-controls-root-${STAMP}.md`;
const MODEL_PROVIDER = process.env.MNOTE_PI_INPUT_MODEL_PROVIDER || "omniroute";
const MODEL_ID = process.env.MNOTE_PI_INPUT_MODEL_ID || "freefirst";
const MODEL_ID = process.env.MNOTE_PI_INPUT_MODEL_ID || "gpt-5.4-mini";
const CHROMIUM_EXECUTABLE = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE
|| (fs.existsSync("/usr/bin/chromium-browser") ? "/usr/bin/chromium-browser" : "")
|| (fs.existsSync("/usr/bin/chromium") ? "/usr/bin/chromium" : "")
@@ -238,6 +238,13 @@ async function openActionMenu(page) {
return menu;
}
async function waitForSendEnabled(page) {
await page.waitForFunction(() => {
const button = document.querySelector("[data-page-ai-pi-lab-btn-send]");
return button && !button.disabled;
}, null, { timeout: TIMEOUT });
}
async function sendViaMenuAndCapture(page, action, text) {
await page.locator("[data-page-ai-pi-lab-input]").fill(text);
await emit(page, { type: "message_update", assistantMessageEvent: { type: "text_start" } });
@@ -266,6 +273,12 @@ async function main() {
page.on("console", (message) => {
if (["error", "warning"].includes(message.type())) consoleMessages.push(`${message.type()}: ${message.text()}`);
});
page.on("response", (response) => {
if (response.status() >= 400) {
const postData = response.request().postData();
consoleMessages.push(`response: ${response.status()} ${response.url()}${postData ? ` body=${postData}` : ""}`);
}
});
page.on("pageerror", (error) => consoleMessages.push(`pageerror: ${error.message}`));
const result = {
@@ -289,9 +302,80 @@ async function main() {
thinkingLevel: session.thinkingLevel,
};
await openPiUi(page);
await page.waitForFunction(() => document.querySelector("[data-page-ai-pi-lab-thinking-label]")?.textContent?.includes("思考 高"), null, {
timeout: TIMEOUT,
});
result.checks.thinkingInitialValue = await page.locator("[data-page-ai-pi-lab-thinking]").inputValue();
assert.equal(result.checks.thinkingInitialValue, "high", "thinking selector should reflect current session");
const contextProbe = await page.evaluate(({ rootUri, workspaceId, pagePath }) => {
const originalRuntime = window.__mnoteDocumentPaneRuntime;
const readContext = (activeEditor) => {
window.__mnoteDocumentPaneRuntime = {
getOpenEditorsSnapshot() {
return { activeEditor };
},
};
return window.__mnotePiLabTest.getCurrentContext();
};
const directory = readContext({
documentId: "local-folder:.opencode",
workspacePath: {
documentId: "local-folder:.opencode",
relativePath: ".opencode",
resourceKind: "directory",
rootUri,
workspaceId,
},
});
const markdown = readContext({
documentId: `local-md:${pagePath}`,
workspacePath: {
documentId: `local-md:${pagePath}`,
relativePath: pagePath,
resourceKind: "page",
rootUri,
workspaceId,
},
});
if (originalRuntime === undefined) delete window.__mnoteDocumentPaneRuntime;
else window.__mnoteDocumentPaneRuntime = originalRuntime;
return { directory, markdown };
}, { rootUri: ROOT_URI, workspaceId: WORKSPACE_ID, pagePath: PAGE_PATH });
result.checks.standaloneDirectoryPagePath = contextProbe.directory.pagePath;
result.checks.standaloneMarkdownPagePath = contextProbe.markdown.pagePath;
assert.equal(contextProbe.directory.pagePath, "", "standalone Pi page must not attach a directory as current page");
assert.equal(contextProbe.markdown.pagePath, PAGE_PATH, "standalone Pi page should follow the active Markdown page");
const toolOnlyId = `tool-only-${STAMP}`;
await emit(page, {
type: "tool_execution_start",
toolCallId: toolOnlyId,
toolName: "todo",
args: {},
});
await emit(page, {
type: "tool_execution_end",
toolCallId: toolOnlyId,
toolName: "todo",
result: { content: [{ type: "text", text: "No todos" }] },
isError: false,
});
await emit(page, {
type: "message_end",
message: { role: "assistant", content: [], stopReason: "stop" },
});
await emit(page, {
type: "agent_end",
messages: [{ role: "assistant", content: [], stopReason: "stop" }],
});
const toolOnlyReply = page.locator('[data-page-ai-pi-lab-message-role="assistant"]').last();
await toolOnlyReply.waitFor({ state: "visible", timeout: TIMEOUT });
result.checks.toolOnlyReply = ((await toolOnlyReply.textContent()) || "").trim();
result.checks.emptyReplyErrorCount = await page.getByText("Pi runtime 返回了空回复", { exact: false }).count();
assert.match(result.checks.toolOnlyReply, /todo(完成)/, "tool-only turn should finish with a visible tool summary");
assert.equal(result.checks.emptyReplyErrorCount, 0, "message_end and agent_end must not duplicate an empty-reply error");
result.checks.thinkingInitialLabel = (await page.locator("[data-page-ai-pi-lab-thinking-label]").textContent() || "").trim();
assert(result.checks.thinkingInitialLabel.includes("思考 高"), "thinking label should reflect current session: " + result.checks.thinkingInitialLabel);
result.checks.permissionLabel = (await page.locator("[data-page-ai-pi-lab-permission-label]").textContent() || "").trim();
assert(/确认|审批|受限|自动/.test(result.checks.permissionLabel), `permission label missing: ${result.checks.permissionLabel}`);
await page.locator("[data-page-ai-pi-lab-permission]").click();
@@ -300,8 +384,15 @@ async function main() {
result.checks.permissionModes = await page.locator("[data-page-ai-pi-lab-permission-mode]").evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-page-ai-pi-lab-permission-mode")));
assert.deepEqual(result.checks.permissionModes, ["confirm", "auto_edit", "plan", "full_access"]);
const modeStartRequestPromise = page.waitForRequest(
(request) => request.url().includes("/api/page-ai/pi/start") && request.method() === "POST",
{ timeout: TIMEOUT },
(request) => {
if (!request.url().includes("/api/page-ai/pi/start") || request.method() !== "POST") return false;
try {
return request.postDataJSON()?.permissionMode === "auto_edit";
} catch {
return false;
}
},
{ timeout: Math.min(TIMEOUT, 30000) },
);
await page.locator('[data-page-ai-pi-lab-permission-mode="auto_edit"]').click();
const modeStartBody = (await modeStartRequestPromise).postDataJSON();
@@ -319,8 +410,15 @@ async function main() {
await page.locator("[data-page-ai-pi-lab-permission]").click();
await page.locator("[data-page-ai-pi-lab-permission-menu]").waitFor({ state: "visible", timeout: TIMEOUT });
const fullAccessStartRequestPromise = page.waitForRequest(
(request) => request.url().includes("/api/page-ai/pi/start") && request.method() === "POST",
{ timeout: TIMEOUT },
(request) => {
if (!request.url().includes("/api/page-ai/pi/start") || request.method() !== "POST") return false;
try {
return request.postDataJSON()?.permissionMode === "full_access";
} catch {
return false;
}
},
{ timeout: Math.min(TIMEOUT, 30000) },
);
await page.locator('[data-page-ai-pi-lab-permission-mode="full_access"]').click();
const fullAccessStartBody = (await fullAccessStartRequestPromise).postDataJSON();
@@ -433,15 +531,20 @@ async function main() {
const historyStartRequestPromise = page.waitForRequest(
(request) => request.url().includes("/api/page-ai/pi/start") && request.method() === "POST",
{ timeout: TIMEOUT },
);
).catch((error) => error);
const historySendRequestPromise = page.waitForRequest(
(request) => request.url().includes("/api/page-ai/pi/send") && request.method() === "POST",
{ timeout: TIMEOUT },
);
).catch((error) => error);
await page.locator("[data-page-ai-pi-lab-input]").fill("history continue input controls smoke");
await waitForSendEnabled(page);
await page.locator("[data-page-ai-pi-lab-btn-send]").click();
const historyStartBody = (await historyStartRequestPromise).postDataJSON();
const historySendBody = (await historySendRequestPromise).postDataJSON();
const historyStartRequest = await historyStartRequestPromise;
const historySendRequest = await historySendRequestPromise;
if (historyStartRequest instanceof Error) throw historyStartRequest;
if (historySendRequest instanceof Error) throw historySendRequest;
const historyStartBody = historyStartRequest.postDataJSON();
const historySendBody = historySendRequest.postDataJSON();
result.checks.historyContinueStartSessionId = historyStartBody.sessionId;
result.checks.historyContinueSendSessionId = historySendBody.sessionId;
result.checks.historyContinueMessage = historySendBody.message;
@@ -452,7 +555,9 @@ async function main() {
result.screenshots.historySessionContinues = path.join(OUT, "03-history-session-continues.png");
await page.locator("[data-page-ai-pi-lab-new]").click();
await page.locator("[data-page-ai-pi-lab-thinking]").selectOption("xhigh");
await page.locator("[data-page-ai-pi-lab-thinking-menu-toggle]").click();
await page.locator("[data-page-ai-pi-lab-thinking-menu]").waitFor({ state: "visible", timeout: TIMEOUT });
await page.locator('[data-page-ai-pi-lab-thinking-option="xhigh"]').click();
const startRequestPromise = page.waitForRequest(
(request) => request.url().includes("/api/page-ai/pi/start") && request.method() === "POST",
{ timeout: TIMEOUT },
@@ -463,11 +568,29 @@ async function main() {
const startBody = startRequest.postDataJSON();
result.checks.startThinkingLevel = startBody.thinkingLevel;
assert.equal(startBody.thinkingLevel, "xhigh", "thinking selector should be sent on start");
await requestJson(page, "/api/page-ai/pi/abort", {
method: "POST",
data: { sessionId: startBody.sessionId || session.sessionId },
}).catch(() => null);
await page.waitForFunction(() => {
const status = document.querySelector("[data-page-ai-pi-lab-status-text]")?.textContent || "";
return !/starting|streaming/.test(status);
}, null, { timeout: TIMEOUT }).catch(() => null);
await startSession(page, session.sessionId, "high");
await openPiUi(page);
await clickQuickAndAssertActive(page, "read-page", true, "send current-page context on after new conversation");
await clickQuickAndAssertActive(page, "current-folder", true, "send current-folder context on after new conversation");
await clickQuickAndAssertActive(page, "selection", true, "send selection context on after new conversation");
await clickQuickAndAssertActive(page, "rag", true, "send LightRAG context on after new conversation");
const sendRequestPromise = page.waitForRequest(
const sendRequestPromise = page.waitForRequest(
(request) => request.url().includes("/api/page-ai/pi/send") && request.method() === "POST",
{ timeout: TIMEOUT },
);
const sendResponsePromise = page.waitForResponse(
(response) => response.url().includes("/api/page-ai/pi/send") && response.request().method() === "POST",
{ timeout: TIMEOUT },
);
await page.locator("[data-page-ai-pi-lab-input]").fill("send button input controls smoke");
await page.locator("[data-page-ai-pi-lab-btn-send]").click();
const sendRequest = await sendRequestPromise;
@@ -486,6 +609,15 @@ async function main() {
assert.equal(sendBody.selectedContext.currentPage.pagePath, PAGE_PATH, "selectedContext should include current page address");
assert.equal(sendBody.selectedContext.currentFolder.folderPath, PAGE_DIR, "selectedContext should include current folder address");
assert.equal(sendBody.selectedContext.lightrag.enabled, true, "selectedContext should include LightRAG toggle");
const sendResponse = await sendResponsePromise;
const sendResponseText = await sendResponse.text().catch(() => "");
result.checks.sendButtonResponseStatus = sendResponse.status();
result.checks.sendButtonResponseBody = sendResponseText.slice(0, 500);
assert(sendResponse.ok(), `send button request should succeed: ${sendResponse.status()} ${sendResponseText.slice(0, 500)}`);
await requestJson(page, "/api/page-ai/pi/abort", {
method: "POST",
data: { sessionId: sendBody.sessionId || session.sessionId },
}).catch(() => null);
await openActionMenu(page);
await Promise.all([