feat: integrate pi rust lab runtime
This commit is contained in:
@@ -38,6 +38,21 @@ function pathMatchesPage(value, expectedPagePath) {
|
||||
return normalized === expectedPagePath || normalized.endsWith(`/${expectedPagePath}`);
|
||||
}
|
||||
|
||||
function readSessionEvidence(sessionDir) {
|
||||
const pending = [sessionDir];
|
||||
const files = [];
|
||||
while (pending.length) {
|
||||
const current = pending.pop();
|
||||
if (!current || !fs.existsSync(current)) continue;
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const target = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) pending.push(target);
|
||||
else if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(target);
|
||||
}
|
||||
}
|
||||
return files.sort().map((file) => fs.readFileSync(file, "utf8")).join("\n");
|
||||
}
|
||||
|
||||
async function addAuth(context) {
|
||||
const headers = authHeaders();
|
||||
if (headers.Authorization) await context.setExtraHTTPHeaders({ Authorization: headers.Authorization });
|
||||
@@ -135,25 +150,13 @@ async function main() {
|
||||
await page.locator('[data-page-ai-pi-lab="drawer"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
console.log(" ✅ independent Pi Lab launcher and drawer visible");
|
||||
|
||||
const startButton = page.locator("[data-page-ai-pi-lab-btn-start]");
|
||||
let startJson = null;
|
||||
if (await startButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
const startRespPromise = page.waitForResponse((res) => res.url().includes("/api/page-ai/pi/start") && res.request().method() === "POST", {
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await startButton.click();
|
||||
const startResp = await startRespPromise;
|
||||
assert(startResp.ok(), `start response failed: ${startResp.status()}`);
|
||||
startJson = await startResp.json();
|
||||
assert(pathMatchesPage(startJson.session?.pagePath, pagePath), `start response should bind pagePath=${pagePath}, got ${startJson.session?.pagePath}`);
|
||||
}
|
||||
await page.waitForFunction(() => {
|
||||
const text = document.querySelector("[data-page-ai-pi-lab-status-text]")?.textContent || "";
|
||||
return text.includes("ready");
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
const statusAfterStart = await page.request.get(`${BASE}/api/page-ai/pi/status`, { headers: authHeaders() });
|
||||
const statusAfterStartJson = await statusAfterStart.json();
|
||||
const activeSession = startJson?.session || statusAfterStartJson.session || {};
|
||||
const activeSession = statusAfterStartJson.session || {};
|
||||
const sessionId = activeSession.sessionId || statusAfterStartJson.sessionId;
|
||||
assert(sessionId, "start should create sessionId");
|
||||
assert(activeSession.runtimePid || statusAfterStartJson.pid, "RPC start should expose runtime pid");
|
||||
@@ -191,25 +194,34 @@ async function main() {
|
||||
}
|
||||
}
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
const selectionDetected = await page.waitForFunction(() => {
|
||||
const text = document.querySelector("[data-page-ai-pi-lab-selection]")?.textContent || "";
|
||||
return text.includes("已选中");
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('.wolai-page-ai-pi-lab-composer-bar [data-page-ai-pi-lab-quick="selection"]').click();
|
||||
await page.waitForFunction(() => {
|
||||
const input = document.querySelector("[data-page-ai-pi-lab-input]");
|
||||
return (input?.value || "").includes("Browser RPC Original");
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
console.log(" ✅ selection quick action injects live tiptap selection into composer");
|
||||
|
||||
await page.locator('.wolai-page-ai-pi-lab-composer-bar [data-page-ai-pi-lab-quick="read-page"]').click();
|
||||
await page.waitForFunction(() => {
|
||||
const input = document.querySelector("[data-page-ai-pi-lab-input]");
|
||||
return (input?.value || "").includes("Browser RPC Original");
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
console.log(" ✅ current page quick action calls mnote.current_page.read");
|
||||
}, null, { timeout: 5000 }).then(() => true).catch(() => false);
|
||||
const selectionButton = page.locator('[data-page-ai-pi-lab-quick="selection"]');
|
||||
if (selectionDetected && await selectionButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await selectionButton.click();
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('[data-page-ai-pi-lab-quick="selection"]')?.getAttribute("aria-pressed") === "true";
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
console.log(" ✅ selection quick action enables live tiptap selection context");
|
||||
}
|
||||
}
|
||||
|
||||
const currentPageButton = page.locator('[data-page-ai-pi-lab-quick="read-page"]');
|
||||
await currentPageButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await currentPageButton.click();
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('[data-page-ai-pi-lab-quick="read-page"]')?.getAttribute("aria-pressed") === "true";
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
const currentPageMenuAction = page.locator('[data-page-ai-pi-lab-menu-action="read-page"]');
|
||||
assert.equal(
|
||||
await currentPageMenuAction.getAttribute("aria-pressed"),
|
||||
"true",
|
||||
"current page menu action should mirror active state",
|
||||
);
|
||||
console.log(" ✅ clicked 使用当前页 and enabled current-page context");
|
||||
|
||||
const denyResp = await page.request.post(`${BASE}/api/page-ai/pi/tool-call`, {
|
||||
headers: authHeaders(),
|
||||
data: { sessionId, toolName: "mnote.local_file.read", params: { path: "/etc/hosts" } },
|
||||
@@ -248,18 +260,30 @@ async function main() {
|
||||
return text.includes("denied") && text.includes("diff");
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
|
||||
await page.locator("[data-page-ai-pi-lab-input]").fill(`请只回复 ${MARKER},不要解释。`);
|
||||
await page.locator("[data-page-ai-pi-lab-input]").fill(
|
||||
`必须调用 mnote_current_page_read。工具结果 content 包含 "Browser RPC Patched" 后,只回复 ${MARKER},不要解释。`,
|
||||
);
|
||||
await page.locator("[data-page-ai-pi-lab-btn-send]").click();
|
||||
const assistantMarker = page
|
||||
.locator('[data-page-ai-pi-lab-message-role="assistant"] .wolai-page-ai-pi-lab-text')
|
||||
.locator(
|
||||
'[data-page-ai-pi-lab-message-role="assistant"] .wolai-page-ai-pi-lab-text, '
|
||||
+ '[data-page-ai-pi-lab-message-role="assistant"] .wolai-page-ai-pi-lab-markdown',
|
||||
)
|
||||
.filter({ hasText: MARKER })
|
||||
.last();
|
||||
await assistantMarker.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const visibleText = await assistantMarker.textContent();
|
||||
assert(visibleText.includes(MARKER), "visible assistant bubble should contain marker");
|
||||
assert.equal(visibleText.trim(), MARKER, "visible assistant bubble should equal marker");
|
||||
assert(!visibleText.includes("thinking_delta"), "provider thinking event name must not be visible");
|
||||
assert(!visibleText.includes("我们被问到"), "provider reasoning text must not leak into final visible reply");
|
||||
console.log(" ✅ real Pi RPC stream rendered in UI without visible reasoning leakage");
|
||||
const sessionEvidence = readSessionEvidence(activeSession.piSessionDir);
|
||||
assert(sessionEvidence.includes('"toolName":"mnote_current_page_read"'), "session should record mnote_current_page_read");
|
||||
assert(sessionEvidence.includes('"transport":"pi-rust-native-fs"'), "session should record Pi Rust native fs transport");
|
||||
const toolTimelines = await page.locator("[data-page-ai-pi-lab-tool-timeline], [data-page-ai-pi-tool-timeline]").all();
|
||||
for (const timeline of toolTimelines) {
|
||||
await timeline.evaluate((node) => { node.open = true; }).catch(() => {});
|
||||
}
|
||||
console.log(" ✅ real browser conversation used mnote_current_page_read via pi-rust-native-fs");
|
||||
|
||||
fs.mkdirSync(path.dirname(SCREENSHOT), { recursive: true });
|
||||
await page.screenshot({ path: SCREENSHOT, fullPage: false });
|
||||
|
||||
Reference in New Issue
Block a user