fix: restore pi rust builtin tool surface
- expose Pi Rust built-in tools by permission mode instead of replacing them with MNote file tools - update Pi Lab smoke coverage for native ls/read/bash usage - document the overreplacement regression and verification evidence
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# 7-62 Page AI Pi Rust 内置工具被 MNote 工具过度替代
|
||||
|
||||
## 状态
|
||||
|
||||
- 已修复
|
||||
- 日期:2026-07-11
|
||||
- Owner:`07-ai`
|
||||
|
||||
## 症状
|
||||
|
||||
Pi Lab 切到 `pi_agent_rust` 后,普通对话可用,但文件/目录类任务会退化为只看到 `mnote_local_file_read` / `mnote_local_file_patch` 等 MNote bridge 工具。用户要求列目录或删除测试文件时,模型会判断“没有删除工具”“不能直接列目录”,甚至尝试把 MNote read 当作文件系统能力。
|
||||
|
||||
## 根因
|
||||
|
||||
MNote 启动 Pi Rust RPC 时固定传 `--tools <allowlist>`。此前 allowlist 主要由 MNote bridge 工具组成,Pi Rust 官方 8 个内置工具只有在旧外部 `pi-permission-system` 条件下才会加入。当前默认使用本地官方 `permission-gate` 镜像时,这个条件不成立,导致 `read/write/edit/hashline_edit/bash/grep/find/ls` 被系统性裁掉。
|
||||
|
||||
这和 Pi Rust 官方定位冲突:Pi Rust 的文件、搜索和 shell 能力应由 Pi 原生 builtins 承载,MNote 只补当前页、allowed roots、URL/reference、知识库和宿主上下文。
|
||||
|
||||
## 修复
|
||||
|
||||
- `full_access`:恢复 Pi Rust 官方 8 个 builtins:`read/write/edit/hashline_edit/bash/grep/find/ls`。
|
||||
- `auto_edit`:开放读写编辑和检索类 builtins,但不开放 `bash`。
|
||||
- `plan`:仅开放只读 builtins:`read/grep/find/ls`。
|
||||
- `mnote_allowed_roots_describe` 返回的 `managedPiBuiltinTools` / `deniedPiBuiltinTools` / `permissionProvider` / `note` 按 permission mode 精确说明,避免继续诱导模型把 MNote file tool 当主文件系统工具。
|
||||
- full_access smoke 改为要求真实调用 Pi Rust `ls/read/bash`,并断言不调用 `mnote_local_file_read` / `mnote_local_file_patch`。
|
||||
|
||||
## 验证
|
||||
|
||||
- `node scripts/task-pi-lab-static-smoke.js`:246 checks passed
|
||||
- `node --check scripts/task-pi-lab-full-access-builtin-delete-smoke.js`
|
||||
- `node --check scripts/task-pi-lab-static-smoke.js`
|
||||
- `node --check scripts/task-pi-lab-rpc-api-smoke.js`
|
||||
- `node --check scripts/task-pi-lab-user-exact-web-smoke.js`
|
||||
- `cargo test -p mnote-web page_ai_pi::tests::permission_modes_expose_pi_builtins_by_mode -- --nocapture`:passed
|
||||
- `cargo test -p mnote-web page_ai_pi::tests::start_uses_ai_settings_for_model_skills_mcp_and_tools -- --nocapture`:passed
|
||||
|
||||
## 遗留
|
||||
|
||||
`confirm` 模式仍未接入 Pi Rust 原生 tool approval 流;在 MNote 能可靠承接 approval 前,不应把 `bash/write/edit` 暴露给 confirm 模式并假装已有审批闭环。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,25 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
setupWorkspaceAccess,
|
||||
seedAiPolicy,
|
||||
} = require("./lib/control-plane-dev-seed");
|
||||
|
||||
const BASE = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
|
||||
const STAMP = Date.now();
|
||||
const OUT = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_OUT || path.join(os.tmpdir(), `mnote-pi-full-access-builtin-disabled-${STAMP}`);
|
||||
const OUT = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_OUT || path.join(os.tmpdir(), `mnote-pi-full-access-builtin-tools-${STAMP}`);
|
||||
const TIMEOUT = Number.parseInt(process.env.UI_TIMEOUT_MS || "360000", 10);
|
||||
const ACTOR_ID = process.env.MNOTE_E2E_ACTOR_ID || "mnote-e2e";
|
||||
const WORKSPACE_ID = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_WORKSPACE_ID || "local-ws:mnote-e2e:my-space";
|
||||
const ROOT_PATH = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_ROOT_PATH || "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space";
|
||||
const WORKSPACE_ID = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_WORKSPACE_ID || `local-ws:${ACTOR_ID}:pi-full-access-builtins-${STAMP}`;
|
||||
const ROOT_PATH = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_ROOT_PATH || path.join(OUT, "workspace");
|
||||
const ROOT_URI = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_ROOT_URI || `file://${ROOT_PATH}`;
|
||||
const MODEL_PROVIDER = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_MODEL_PROVIDER || "omniroute";
|
||||
const MODEL_ID = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_MODEL_ID || "freefirst";
|
||||
const MARKER = `PI_FULL_ACCESS_BUILTIN_DISABLED_OK_${STAMP}`;
|
||||
const PAGE_PATH = `pi-full-access-builtin-disabled-${STAMP}.md`;
|
||||
const MODEL_ID = process.env.MNOTE_PI_FULL_ACCESS_BUILTIN_MODEL_ID || "gpt-5.4-mini";
|
||||
const MARKER = `PI_FULL_ACCESS_CONTROLLED_TOOLS_OK_${STAMP}`;
|
||||
const PAGE_PATH = `pi-full-access-builtin-tools-${STAMP}.md`;
|
||||
const SCRATCH_PATH = `pi-full-access-builtin-tools-${STAMP}.txt`;
|
||||
const BUILTIN_TOOLS = ["read", "write", "edit", "bash", "grep", "find", "ls", "hashline_edit"];
|
||||
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" : "")
|
||||
@@ -63,7 +69,7 @@ function piExtensionConfig(name, description, source, toolNames, riskLevel, requ
|
||||
return { name, description, source, toolNames, riskLevel, requiredScopes, enabled: true };
|
||||
}
|
||||
|
||||
function policyForFullAccessBuiltinDisabled() {
|
||||
function policyForFullAccessBuiltinTools() {
|
||||
return {
|
||||
defaultModel: `${MODEL_PROVIDER}/${MODEL_ID}`,
|
||||
allowedModels: [`${MODEL_PROVIDER}/${MODEL_ID}`],
|
||||
@@ -84,39 +90,34 @@ function policyForFullAccessBuiltinDisabled() {
|
||||
|
||||
async function seedWorkspace(page) {
|
||||
mkdirp(ROOT_PATH);
|
||||
fs.writeFileSync(path.join(ROOT_PATH, PAGE_PATH), "# Pi full access builtin disabled smoke\n", "utf8");
|
||||
const grantResponse = await page.request.fetch(`${BASE}/api/admin/access-policy/grants`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: {
|
||||
userId: ACTOR_ID,
|
||||
rootUri: ROOT_URI,
|
||||
rootPath: ROOT_PATH,
|
||||
permission: "write",
|
||||
recursive: true,
|
||||
capabilities: ["ai"],
|
||||
},
|
||||
timeout: TIMEOUT,
|
||||
fs.writeFileSync(
|
||||
path.join(ROOT_PATH, PAGE_PATH),
|
||||
"# Pi full access builtin tools smoke\n\nBUILTIN_READ_MARKER\n",
|
||||
"utf8",
|
||||
);
|
||||
fs.rmSync(path.join(ROOT_PATH, SCRATCH_PATH), { force: true });
|
||||
await setupWorkspaceAccess(page.request, BASE, {
|
||||
actorId: ACTOR_ID,
|
||||
email: "mnote.e2e@example.com",
|
||||
username: ACTOR_ID,
|
||||
displayName: ACTOR_ID,
|
||||
role: "admin",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
workspaceName: "Pi full access builtin tools smoke",
|
||||
rootPath: ROOT_PATH,
|
||||
rootUri: ROOT_URI,
|
||||
permission: "write",
|
||||
capabilities: ["ai", "read", "write"],
|
||||
timeoutMs: TIMEOUT,
|
||||
});
|
||||
const grantText = await grantResponse.text();
|
||||
let grantBody = {};
|
||||
try {
|
||||
grantBody = grantText ? JSON.parse(grantText) : {};
|
||||
} catch {
|
||||
grantBody = { raw: grantText };
|
||||
}
|
||||
if (!grantResponse.ok() && grantBody.code !== "local_access_policy_grant_duplicate") {
|
||||
throw new Error(`POST /api/admin/access-policy/grants failed: ${grantResponse.status()} ${grantText.slice(0, 800)}`);
|
||||
}
|
||||
await requestJson(page, "/api/ai-admin/settings", {
|
||||
method: "PUT",
|
||||
data: {
|
||||
...policyForFullAccessBuiltinDisabled(),
|
||||
quota: { daily: 200 },
|
||||
},
|
||||
await seedAiPolicy(page.request, BASE, {
|
||||
id: `pi-full-access-builtins-${ACTOR_ID}-${WORKSPACE_ID}`,
|
||||
userId: ACTOR_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
allowedRootsJson: [{ rootUri: ROOT_URI, rootPath: ROOT_PATH, permission: "write" }],
|
||||
modelPolicyJson: policyForFullAccessBuiltinTools(),
|
||||
quotaJson: { daily: 200 },
|
||||
timeoutMs: TIMEOUT,
|
||||
});
|
||||
await requestJson(page, `/api/ai-settings/effective?workspaceId=${encodeURIComponent(WORKSPACE_ID)}`);
|
||||
}
|
||||
@@ -130,7 +131,7 @@ async function abortExistingSession(page) {
|
||||
}
|
||||
|
||||
async function startRealPi(page) {
|
||||
const sessionId = `pi-full-access-builtin-disabled-${STAMP}`;
|
||||
const sessionId = `pi-full-access-builtin-tools-${STAMP}`;
|
||||
const start = await requestJson(page, "/api/page-ai/pi/start", {
|
||||
method: "POST",
|
||||
data: {
|
||||
@@ -138,7 +139,7 @@ async function startRealPi(page) {
|
||||
rootUri: ROOT_URI,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
pagePath: PAGE_PATH,
|
||||
pageTitle: "Pi full access builtin disabled smoke",
|
||||
pageTitle: "Pi full access builtin tools smoke",
|
||||
modelProvider: MODEL_PROVIDER,
|
||||
modelId: MODEL_ID,
|
||||
thinkingLevel: "medium",
|
||||
@@ -150,6 +151,7 @@ async function startRealPi(page) {
|
||||
assert.equal(start.session.runtimePolicySnapshot.permissionMode, "full_access", "runtime policy should persist full_access");
|
||||
assert.equal(start.session.runtimeMode, "rpc", `Pi 必须以 rpc 模式启动,实际=${start.session.runtimeMode}`);
|
||||
assert(start.session.runtimePid, "真实 Pi RPC 启动后应有 runtimePid");
|
||||
start.session.managedPiBuiltinTools = start.managedPiBuiltinTools || [];
|
||||
return start.session;
|
||||
}
|
||||
|
||||
@@ -181,6 +183,41 @@ function readSessionJsonl(sessionDir) {
|
||||
return { sessionFile, raw: fs.readFileSync(sessionFile, "utf8") };
|
||||
}
|
||||
|
||||
function readBuiltinToolResults(raw) {
|
||||
return raw
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter((entry) => entry?.type === "message"
|
||||
&& entry?.message?.role === "toolResult"
|
||||
&& BUILTIN_TOOLS.includes(entry.message.toolName))
|
||||
.map((entry) => entry.message);
|
||||
}
|
||||
|
||||
async function waitForCompleteSessionJsonl(sessionDir) {
|
||||
const deadline = Date.now() + Math.min(TIMEOUT, 15000);
|
||||
let snapshot = readSessionJsonl(sessionDir);
|
||||
while (Date.now() < deadline) {
|
||||
const calledTools = BUILTIN_TOOLS.filter((tool) => snapshot.raw.includes(`"name":"${tool}"`));
|
||||
if (calledTools.includes("ls")
|
||||
&& calledTools.includes("read")
|
||||
&& calledTools.includes("bash")
|
||||
&& !snapshot.raw.includes('"name":"mnote_local_file_read"')
|
||||
&& snapshot.raw.includes(MARKER)) {
|
||||
return snapshot;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
snapshot = readSessionJsonl(sessionDir);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
mkdirp(OUT);
|
||||
const browser = await chromium.launch({
|
||||
@@ -223,20 +260,26 @@ async function main() {
|
||||
result.checks.noLegacyNpmAskUser = !enabledSources.includes("npm:pi-ask-user");
|
||||
result.checks.noExternalPermissionSystem = !enabledSources.includes("npm:@gotgenes/pi-permission-system");
|
||||
result.checks.officialPermissionGateConfigured = enabledSources.includes("pi-rust-official:permission-gate");
|
||||
result.checks.managedBuiltinToolsDisabledAtStart = (session.runtimePolicySnapshot.managedBuiltinTools || []).length === 0;
|
||||
result.checks.managedBuiltinToolsAtStart = session.managedPiBuiltinTools || [];
|
||||
assert.equal(result.checks.permissionSystemConfigAbsent, true, "full_access smoke should not generate legacy pi-permission-system config");
|
||||
assert.equal(result.checks.noLegacyNpmAskUser, true, "full_access smoke should not use unavailable npm:pi-ask-user");
|
||||
assert.equal(result.checks.noExternalPermissionSystem, true, "full_access smoke should not load incompatible pi-permission-system");
|
||||
assert.equal(result.checks.officialPermissionGateConfigured, true, "policy should include Pi Rust official permission-gate");
|
||||
assert.deepEqual(
|
||||
[...result.checks.managedBuiltinToolsAtStart].sort(),
|
||||
[...BUILTIN_TOOLS].sort(),
|
||||
"full_access should expose Pi Rust official built-in tools instead of replacing them with MNote file tools",
|
||||
);
|
||||
|
||||
await openPiUi(page);
|
||||
await page.screenshot({ path: path.join(OUT, "01-full-access-started.png"), fullPage: false });
|
||||
result.screenshots.started = path.join(OUT, "01-full-access-started.png");
|
||||
|
||||
const prompt = [
|
||||
"请调用 mnote_allowed_roots_describe 工具,读取 MNote 返回的 allowedRoots、deniedPiBuiltinTools、managedPiBuiltinTools、permissionProvider。",
|
||||
"不要调用 bash/read/write/edit/hashline_edit/grep/find/ls 这些 Pi 内置工具。",
|
||||
"用一句话说明:full_access 下 MNote 当前仍默认禁用 Pi Rust 内置文件/命令工具,文件权限由 MNote bridge 管控。",
|
||||
"当前是 full_access 验收。必须真实调用 Pi Rust 内置工具,不能只描述。",
|
||||
"先调用 ls 列出当前工作目录,再调用 read 读取当前页文件。",
|
||||
"然后调用 bash 执行 pwd。确认当前页内容包含 BUILTIN_READ_MARKER。",
|
||||
"不要调用 mnote_local_file_read 或 mnote_local_file_patch;MNote 工具只用于上下文和知识库,不应替代 Pi 原生文件工具。",
|
||||
`最终单独输出一行:${MARKER}`,
|
||||
].join("\n");
|
||||
await page.locator("[data-page-ai-pi-lab-input]").fill(prompt);
|
||||
@@ -254,21 +297,36 @@ async function main() {
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
await markerLocator.waitFor({ state: "visible", timeout: 1000 });
|
||||
await page.screenshot({ path: path.join(OUT, "02-full-access-builtin-disabled-answer.png"), fullPage: false });
|
||||
result.screenshots.answer = path.join(OUT, "02-full-access-builtin-disabled-answer.png");
|
||||
await page.screenshot({ path: path.join(OUT, "02-full-access-builtin-tools-answer.png"), fullPage: false });
|
||||
result.screenshots.answer = path.join(OUT, "02-full-access-builtin-tools-answer.png");
|
||||
|
||||
const sessionJsonl = readSessionJsonl(session.piSessionDir);
|
||||
const sessionJsonl = await waitForCompleteSessionJsonl(session.piSessionDir);
|
||||
result.session.sessionFile = sessionJsonl.sessionFile;
|
||||
result.answerText = ((await markerLocator.textContent({ timeout: TIMEOUT })) || "").trim();
|
||||
result.checks.allowedRootsToolCalled = /mnote_allowed_roots_describe/.test(sessionJsonl.raw);
|
||||
result.checks.deniedBuiltinsRecorded = /deniedPiBuiltinTools/.test(sessionJsonl.raw) && /hashline_edit/.test(sessionJsonl.raw);
|
||||
result.checks.managedBuiltinsEmptyRecorded = /managedPiBuiltinTools/.test(sessionJsonl.raw);
|
||||
result.checks.noRawBuiltinCalled = !/"name":"(bash|read|write|edit|hashline_edit|grep|find|ls)"/.test(sessionJsonl.raw);
|
||||
result.checks.calledBuiltinTools = BUILTIN_TOOLS.filter((tool) => sessionJsonl.raw.includes(`"name":"${tool}"`));
|
||||
result.checks.calledMnoteLocalFileRead = sessionJsonl.raw.includes('"name":"mnote_local_file_read"');
|
||||
result.checks.calledMnoteLocalFilePatch = sessionJsonl.raw.includes('"name":"mnote_local_file_patch"');
|
||||
const builtinToolResults = readBuiltinToolResults(sessionJsonl.raw);
|
||||
result.checks.builtinToolResultCount = builtinToolResults.length;
|
||||
result.checks.failedBuiltinTools = builtinToolResults
|
||||
.filter((message) => message.isError === true)
|
||||
.map((message) => message.toolName);
|
||||
result.checks.readContainsMarker = sessionJsonl.raw.includes("BUILTIN_READ_MARKER");
|
||||
result.checks.lsSawPage = sessionJsonl.raw.includes(PAGE_PATH);
|
||||
result.checks.noBridgeSessionFailure = !/page_ai_pi_lab_session_not_found|page_ai_pi_lab_bridge_token_invalid|mnote_pi_rust_service_bridge_unavailable|mnote_pi_bridge_session_id_missing/i.test(sessionJsonl.raw);
|
||||
result.checks.scratchContent = fs.existsSync(path.join(ROOT_PATH, SCRATCH_PATH))
|
||||
? fs.readFileSync(path.join(ROOT_PATH, SCRATCH_PATH), "utf8")
|
||||
: "";
|
||||
result.checks.noPermissionRequiredPrompt = await page.locator("text=Permission Required").count() === 0;
|
||||
assert(result.checks.allowedRootsToolCalled, "Pi session JSONL should record mnote_allowed_roots_describe call");
|
||||
assert(result.checks.deniedBuiltinsRecorded, "Pi session JSONL should include deniedPiBuiltinTools");
|
||||
assert(result.checks.managedBuiltinsEmptyRecorded, "Pi session JSONL should include managedPiBuiltinTools");
|
||||
assert(result.checks.noRawBuiltinCalled, "Pi raw builtin tools should remain disabled by default");
|
||||
assert(result.checks.calledBuiltinTools.includes("ls"), "full_access should allow Pi Rust builtin ls");
|
||||
assert(result.checks.calledBuiltinTools.includes("read"), "full_access should allow Pi Rust builtin read");
|
||||
assert(result.checks.calledBuiltinTools.includes("bash"), "full_access should allow Pi Rust builtin bash");
|
||||
assert.equal(result.checks.calledMnoteLocalFileRead, false, "Pi Rust builtin read must not be replaced by mnote_local_file_read");
|
||||
assert.equal(result.checks.calledMnoteLocalFilePatch, false, "This smoke must not use mnote_local_file_patch");
|
||||
assert.equal(result.checks.readContainsMarker, true, "Pi Rust builtin read result should contain the seeded page marker");
|
||||
assert.equal(result.checks.lsSawPage, true, "Pi Rust builtin ls should list the seeded page file");
|
||||
assert.equal(result.checks.noBridgeSessionFailure, true, "MNote bridge context must not fail while Pi builtins are available");
|
||||
assert.equal(result.checks.scratchContent, "", "negative full_access smoke should not create scratch files via raw builtins");
|
||||
assert(result.checks.noPermissionRequiredPrompt, "official permission-gate smoke should not show legacy Permission Required prompt");
|
||||
|
||||
await requestJson(page, "/api/page-ai/pi/abort", { method: "POST", data: { sessionId: session.sessionId } }).catch(() => ({}));
|
||||
|
||||
@@ -80,7 +80,9 @@ async function seedWorkspaceAccess(rootUri, rootPath) {
|
||||
}),
|
||||
});
|
||||
if (seed.status === 403 && seed.body && seed.body.code === "dev_seed_disabled") {
|
||||
return { ok: false, skipped: true, reason: "dev_seed_disabled" };
|
||||
throw new Error(
|
||||
"Pi RPC smoke 需要 /api/dev/seed;请使用 `npm run dev:hot` 启动并重试。dev:hot 默认开启 MNOTE_WEB_ALLOW_DEV_FIXTURES=1,修改启动环境后必须重启 Node 主进程。",
|
||||
);
|
||||
}
|
||||
assert(seed.status === 200 && seed.body.ok === true, `/api/dev/seed failed: ${seed.status} ${JSON.stringify(seed.body)}`);
|
||||
return { ok: true, skipped: false };
|
||||
@@ -215,7 +217,7 @@ async function main() {
|
||||
assert(status.body.runtimeImplementation === "pi-rust", `runtimeImplementation must default to pi-rust, got ${status.body.runtimeImplementation}`);
|
||||
assert(status.body.runtimeBinary, "status should expose runtimeBinary for Pi Rust diagnostics");
|
||||
assert(Array.isArray(status.body.managedPiBuiltinTools), "missing managedPiBuiltinTools");
|
||||
assert(status.body.managedPiBuiltinTools.length === 0, "Pi Rust should keep raw builtins disabled by default");
|
||||
assert(status.body.managedPiBuiltinTools.length === 0, "status without an active full_access session should not claim raw builtins are enabled");
|
||||
assert(status.body.uiMode === "independent_mnote_native_drawer", "Pi Lab should report independent native drawer UI mode");
|
||||
pass("status enabled/rpc with Rust bridge policy and independent Pi Lab UI mode");
|
||||
} catch (err) {
|
||||
@@ -245,7 +247,11 @@ async function main() {
|
||||
assert(start.body.session.runtimePid > 0, "runtimePid must be positive");
|
||||
assert(start.body.session.allowedRootsSnapshot, "start did not capture allowed roots snapshot");
|
||||
assert(Array.isArray(start.body.managedPiBuiltinTools), "missing managedPiBuiltinTools in start");
|
||||
assert(start.body.managedPiBuiltinTools.length === 0, "start should not expose raw Pi builtins by default");
|
||||
assert.deepEqual(
|
||||
[...start.body.managedPiBuiltinTools].sort(),
|
||||
["edit", "find", "grep", "hashline_edit", "ls", "read", "write"].sort(),
|
||||
"auto_edit should expose Pi Rust read/write/edit builtins but keep bash for full_access",
|
||||
);
|
||||
assert(start.body.mnoteToolOnly === false, "start should expose MNote bridge tools through Pi Rust extension");
|
||||
pass(`start session ${sessionId} with real Pi PID ${start.body.session.runtimePid}`);
|
||||
} catch (err) {
|
||||
@@ -288,7 +294,11 @@ async function main() {
|
||||
assert(payload.mode === "rpc", `mode must be rpc, got ${payload.mode}`);
|
||||
assert(typeof payload.pid === "number", "PID must be a number in event");
|
||||
assert(Array.isArray(payload.managedBuiltinTools), "missing managedBuiltinTools in event");
|
||||
assert(payload.managedBuiltinTools.length === 0, "runtime event should keep raw Pi builtins disabled by default");
|
||||
assert.deepEqual(
|
||||
[...payload.managedBuiltinTools].sort(),
|
||||
["edit", "find", "grep", "hashline_edit", "ls", "read", "write"].sort(),
|
||||
"runtime event should expose auto_edit Pi Rust builtins",
|
||||
);
|
||||
pass(`runtime_started event observed (mode=rpc, pid=${payload.pid})`);
|
||||
} else {
|
||||
const statusAfterStart = await fetchJson(`${BASE}/api/page-ai/pi/status`);
|
||||
@@ -490,15 +500,18 @@ async function main() {
|
||||
fail(`abort: ${err.message}`);
|
||||
}
|
||||
|
||||
// ── 14. Verify session status changed to Aborted ────────────────
|
||||
// ── 14. Verify aborted session leaves active status and persists in history ─
|
||||
try {
|
||||
const status2 = await fetchJson(`${BASE}/api/page-ai/pi/status`);
|
||||
assert(status2.body.session, "status should return current session");
|
||||
assert(status2.status === 200, `status returned ${status2.status}`);
|
||||
assert(status2.body.session == null, "aborted session should not remain auto-resumable");
|
||||
const history = await fetchJson(`${BASE}/api/page-ai/pi/sessions/${encodeURIComponent(sessionId)}`);
|
||||
assert(history.status === 200, `session history returned ${history.status}`);
|
||||
assert(
|
||||
status2.body.session.status === "aborted",
|
||||
`expected session status aborted, got ${status2.body.session.status}`
|
||||
history.body.session?.status === "aborted",
|
||||
`expected persisted session status aborted, got ${history.body.session?.status}`
|
||||
);
|
||||
pass("session status transitions to aborted");
|
||||
pass("aborted session leaves active status and persists as aborted");
|
||||
} catch (err) {
|
||||
fail(`session status aborted: ${err.message}`);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Pi Lab static code smoke
|
||||
// 验证 Pi Lab 相关源码结构正确,不依赖后端运行
|
||||
// 确认:新端点、状态机、无轮询、SSE、Pi builtin 禁用、allowed roots、receipt
|
||||
// 确认:默认模型 omniroute/freefirst 在前端 UI 和 header 中明确体现
|
||||
// 确认:默认模型 omniroute/gpt-5.4-mini 在前端 UI 和 header 中明确体现
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -37,6 +37,26 @@ const mnotePiPackage = readFile(files.mnotePiPackage);
|
||||
const mnotePiExtension = readFile(files.mnotePiExtension);
|
||||
const mnotePiMcpExtension = readFile(files.mnotePiMcpExtension);
|
||||
const mnotePiMcpClient = readFile(files.mnotePiMcpClient);
|
||||
const devHot = readFile(path.join(repoRoot, 'scripts/dev-hot.js'));
|
||||
|
||||
function functionBody(source, name) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
if (start < 0) return '';
|
||||
const brace = source.indexOf('{', start);
|
||||
if (brace < 0) return '';
|
||||
let depth = 0;
|
||||
for (let i = brace; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) return source.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
return source.slice(start);
|
||||
}
|
||||
|
||||
const showPiLabBody = functionBody(runtime, 'showPiLab');
|
||||
|
||||
const checks = [
|
||||
// === Runtime JS: existence ===
|
||||
@@ -106,23 +126,27 @@ const checks = [
|
||||
['runtime has OpenHub-style left history drawer markers', runtime.includes('data-page-ai-pi-lab-history-layer') && runtime.includes('wolai-page-ai-pi-lab-history-scrim') && runtime.includes('历史对话')],
|
||||
['runtime history drawer supports refresh/delete/export/clear', runtime.includes('data-page-ai-pi-lab-history-refresh') && runtime.includes('data-page-ai-pi-lab-history-delete') && runtime.includes('data-page-ai-pi-lab-history-export') && runtime.includes('data-page-ai-pi-lab-history-clear')],
|
||||
['runtime does not expose manual Pi runtime start button', !runtime.includes('data-page-ai-pi-lab-btn-start') && !runtime.includes('启动 Pi runtime') && !runtime.includes('预启动 Pi 会话')],
|
||||
['runtime auto starts Pi session when drawer opens', runtime.includes('function showPiLab') && runtime.includes('checkStatus().then(function ()') && runtime.includes('return startRuntime();')],
|
||||
['runtime auto starts current page Pi session when drawer opens', showPiLabBody.includes('checkStatus().then(function ()') && showPiLabBody.includes('startRuntime().then')],
|
||||
['runtime has native model controls', runtime.includes('data-page-ai-pi-lab-model-provider') && runtime.includes('data-page-ai-pi-lab-model-id') && runtime.includes('data-page-ai-pi-lab-model-custom')],
|
||||
['runtime applies model controls through Pi RPC configure endpoint', runtime.includes("CONFIGURE: '/api/page-ai/pi/configure'") && runtime.includes('applyModelConfigToRuntime') && runtime.includes('pendingModelConfigApply')],
|
||||
['runtime collapses secondary right rail sections', runtime.includes('<details class="wolai-page-ai-pi-lab-rail-section"') && runtime.includes('data-page-ai-pi-lab-receipts-section')],
|
||||
['runtime has diagnostics collapsed by default', runtime.includes('<details class="wolai-page-ai-pi-lab-diagnostics"') && !runtime.includes('<details class="wolai-page-ai-pi-lab-diagnostics" open')],
|
||||
['runtime injects CSS styles', runtime.includes('injectStyles')],
|
||||
|
||||
// === Runtime JS: default model (omniroute/freefirst) ===
|
||||
// === Runtime JS: default model (omniroute/gpt-5.4-mini) ===
|
||||
['runtime has defaultModelProvider with omniroute', runtime.includes('defaultModelProvider') && runtime.includes('omniroute')],
|
||||
['runtime has defaultModelId with freefirst', runtime.includes('defaultModelId') && runtime.includes('freefirst')],
|
||||
['runtime has defaultModelId with gpt-5.4-mini', runtime.includes('defaultModelId') && runtime.includes('gpt-5.4-mini')],
|
||||
['runtime shows default model in header label', runtime.includes('updateModelLabel') && runtime.includes('defaultModelProvider') && runtime.includes('defaultModelId')],
|
||||
['runtime consumes defaultModelProvider from backend status', runtime.includes('data.defaultModelProvider')],
|
||||
['runtime consumes defaultModelId from backend status', runtime.includes('data.defaultModelId')],
|
||||
['runtime shows "omniroute/freefirst" in empty state', runtime.includes('omniroute') && runtime.includes('freefirst') && runtime.includes('默认模型')],
|
||||
['runtime shows "omniroute/gpt-5.4-mini" in empty state', runtime.includes('omniroute') && runtime.includes('gpt-5.4-mini') && runtime.includes('默认模型')],
|
||||
['runtime comment mentions consuming backend default fields', runtime.includes('backend status/default fields')],
|
||||
['runtime has DEFAULT_MODEL_PROVIDER constant', runtime.includes('DEFAULT_MODEL_PROVIDER') && runtime.includes("'omniroute'")],
|
||||
['runtime has DEFAULT_MODEL_ID constant', runtime.includes('DEFAULT_MODEL_ID') && runtime.includes("'freefirst'")],
|
||||
['runtime has DEFAULT_MODEL_ID constant', runtime.includes('DEFAULT_MODEL_ID') && runtime.includes("'gpt-5.4-mini'")],
|
||||
['runtime preserves Omniroute/freefirst as selectable model', runtime.includes("'freefirst'") && runtime.includes('modelControlOptions')],
|
||||
['runtime labels freefirst option as omniroute/freefirst', runtime.includes("name: DEFAULT_MODEL_PROVIDER + '/freefirst'")],
|
||||
['runtime has Pi Lab floating launcher', runtime.includes('data-page-ai-pi-lab-launcher')],
|
||||
['runtime hides Pi Lab launcher while drawer is active', runtime.includes('piLabLauncherEl.hidden = !!piLabState.active')],
|
||||
['runtime documents pi-web-ui evidence', runtime.includes('@earendil-works/pi-web-ui@0.75.3')],
|
||||
['runtime uses MNote-native adapter boundary', runtime.includes('MNote-native adapter')],
|
||||
|
||||
@@ -130,6 +154,7 @@ const checks = [
|
||||
['route file exists', route.length > 0],
|
||||
['route has status endpoint', route.includes('pub async fn status')],
|
||||
['route has start endpoint', route.includes('pub async fn start')],
|
||||
['route has configure endpoint for Pi RPC model/thinking changes', route.includes('pub async fn configure') && route.includes('"type": "set_model"') && route.includes('"type": "set_thinking_level"')],
|
||||
['route has send endpoint', route.includes('pub async fn send')],
|
||||
['route has abort endpoint', route.includes('pub async fn abort')],
|
||||
['route has events SSE endpoint', route.includes('pub async fn events')],
|
||||
@@ -153,7 +178,14 @@ const checks = [
|
||||
['route enforces allowed roots', route.includes('active_allowed_roots') || route.includes('allowed_roots')],
|
||||
['route blocks path escape', route.includes('path_escape') || route.includes('path_is_inside')],
|
||||
['route has default model provider constant PI_LAB_DEFAULT_MODEL_PROVIDER=omniroute', route.includes('PI_LAB_DEFAULT_MODEL_PROVIDER') && route.includes('omniroute')],
|
||||
['route has default model id constant PI_LAB_DEFAULT_MODEL_ID=freefirst', route.includes('PI_LAB_DEFAULT_MODEL_ID') && route.includes('freefirst')],
|
||||
['route has default model id constant PI_LAB_DEFAULT_MODEL_ID=gpt-5.4-mini', route.includes('PI_LAB_DEFAULT_MODEL_ID') && route.includes('gpt-5.4-mini')],
|
||||
['route checks OmniRoute tool_calling capability before real runtime start', route.includes('ensure_session_model_supports_tools') && route.includes('page_ai_pi_model_tools_unsupported') && route.includes('tool_calling')],
|
||||
['runtime replays deferred model config after streaming', runtime.includes('maybeApplyPendingModelConfig') && runtime.includes('pendingModelConfigApply')],
|
||||
['runtime preserves queued permission mode across in-flight mode changes', runtime.includes('pendingPermissionMode') && runtime.includes('piLabState.pendingPermissionMode = piLabState.permissionMode')],
|
||||
['route writes Pi Rust models apiKey as bare env var name, not shell literal', route.includes('"apiKey": "OPENAI_API_KEY"') && !route.includes('"apiKey": "$OPENAI_API_KEY"')],
|
||||
['route derives OmniRoute tool support from verified model capability', route.includes('"supportsTools": supports_tools') && route.includes('omniroute_model_tool_calling_capability')],
|
||||
['route keeps OmniRoute streaming usage enabled', route.includes('"supportsUsageInStreaming": true') && !route.includes('"supportsUsageInStreaming": false')],
|
||||
['route sends Pi Rust directly to configured OmniRoute base URL', route.includes('"baseUrl": omniroute_base_url()') && !route.includes('omniroute_proxy_chat_completions') && !routesMod.includes('/api/page-ai/pi/omniroute-proxy/')],
|
||||
['route defaults to Pi Rust runtime implementation', route.includes('PI_LAB_RUNTIME_IMPL_RUST') && route.includes('"pi-rust"') && route.includes('MNOTE_PAGE_AI_PI_RUST_BIN')],
|
||||
['route keeps TS Pi as explicit fallback only', route.includes('PI_LAB_RUNTIME_IMPL_TS') && route.includes('MNOTE_PAGE_AI_PI_TS_BIN')],
|
||||
['route rejects arbitrary cookie_header auth bypass', route.includes('page_ai_pi_lab_invalid_session_cookie') && !route.includes('context.auth.cookie_header.is_some() {\n return Ok') ],
|
||||
@@ -167,14 +199,25 @@ const checks = [
|
||||
['route never writes bridge token into generated extension source', !route.includes('const BRIDGE_TOKEN = {bridge_token}')],
|
||||
['route rejects bridge calls for non-running sessions', route.includes('page_ai_pi_lab_bridge_session_not_running') && route.includes('PiLabSessionStatus::RuntimeRunning | PiLabSessionStatus::TurnRunning')],
|
||||
['route loads official MNote Pi package extension', route.includes('mnote_pi_extension_path') && route.includes('packages/pi-mnote/extensions/mnote-bridge.ts')],
|
||||
['MNote Pi bridge reads session and base URL from private context fallback', mnotePiExtension.includes('bridgeSessionId') && mnotePiExtension.includes('bridgeBaseUrl') && mnotePiExtension.includes('readContextFileSnapshot')],
|
||||
['MNote Pi bridge does not trust generic runtime session id env', mnotePiExtension.includes('PI_MNOTE_BRIDGE_SESSION_ID') && mnotePiExtension.includes('MNOTE_PI_BRIDGE_SESSION_ID') && !mnotePiExtension.includes('MNOTE_PI_LAB_SESSION_ID')],
|
||||
['MNote Pi bridge allows native tools when context file proves Pi Rust', mnotePiExtension.includes('isPiRustNativeRuntime') && mnotePiExtension.includes('Boolean(CONTEXT_FILE)')],
|
||||
['route starts Pi with explicit MNote extension bridge', route.includes('--extension') && route.includes('mnotePiExtension')],
|
||||
['route starts Pi with MNote tool allowlist', route.includes('--tools') && route.includes('mnoteToolNames') && route.includes('pi_lab_extension_tool_names')],
|
||||
['route passes MNote tool manifest to extension env', route.includes('MNOTE_PI_BRIDGE_TOOLS') && route.includes('PI_MNOTE_BRIDGE_TOOLS') && route.includes('mnote_pi_tool_manifest')],
|
||||
['route writes dynamic Pi Rust MNote context file', route.includes('mnote.pi.context.v1') && route.includes('PI_MNOTE_CONTEXT_FILE') && route.includes('write_pi_mnote_context_snapshot')],
|
||||
['route injects dynamic Pi Rust input context', route.includes('MNOTE_PI_CONTEXT_V1') && route.includes('pi_mnote_input_context_prefix')],
|
||||
['route disables Pi builtins by default unless external permission extension is opt-in', route.includes('pi_lab_enabled_builtin_tools') && route.includes('MNOTE_PAGE_AI_PI_ALLOW_EXTERNAL_EXTENSIONS') && route.includes('configuredPiExtensionSources') && route.includes('pi_lab_runtime_extension_sources')],
|
||||
['route binds staged MNote bridge to absolute context path and private embedded snapshot', route.includes('bind_mnote_bridge_context_path') && route.includes('DEFAULT_CONTEXT_FILE') && route.includes('EMBEDDED_CONTEXT')],
|
||||
['route sends per-prompt Pi Rust context through extension input hook', route.includes('mnote.pi.context.v1') && route.includes('write_pi_mnote_context_snapshot') && route.includes('fn pi_mnote_input_context_prefix') && route.includes('format!(\"{input_context_prefix}{message}\")')],
|
||||
['route resolves local file tools against session rootUri when params omit rootUri', route.includes('session: Option<&PiLabSession>') && route.includes('session.and_then(|session| session.root_uri.clone())')],
|
||||
['route exposes Pi JSONL replay messages from session tree', route.includes('"messages": replay_messages') && route.includes('build_pi_replay_messages(&entries)') && route.includes('pi_lab_active_path_entries')],
|
||||
['route exposes Pi Rust builtins in full_access instead of replacing them with MNote file tools', route.includes('fn pi_lab_enabled_builtin_tools') && route.includes('Some("full_access")') && route.includes('pi_lab_managed_builtin_tools()') && route.includes('Some("auto_edit")') && route.includes('Some("plan")') && route.includes('PI_LAB_MANAGED_BUILTIN_TOOLS')],
|
||||
['route exposes Rust Pi runtime diagnostics', route.includes('hashline_edit') && route.includes('runtimeImplementation') && route.includes('runtimeBinary') && route.includes('runtimeAvailable') && route.includes('runtimeInstallHint') && route.includes('runtimeError')],
|
||||
['route reports warmup runtime without exposing it as current page session', route.includes('"warmupRunning"') && route.includes('"warmupSessionId"') && route.includes('"warmupProcessCount"') && route.includes('is_pi_lab_warmup_session_id(&session.session_id)')],
|
||||
['runtime send path distinguishes warm runtime binding from cold start', runtime.includes('正在绑定当前页 Pi 会话,完成后自动发送') && runtime.includes('Pi Rust 正在启动,启动后自动发送') && runtime.includes('piLabState.warmupRunning')],
|
||||
['dev:hot warmup defaults to no real model prompt', devHot.includes('MNOTE_PAGE_AI_PI_WARMUP_SEND') && devHot.includes('?? "0"')],
|
||||
['runtime opens Pi drawer by prestarting current page session', showPiLabBody.includes('startRuntime().then') && showPiLabBody.includes('syncRuntimeState()')],
|
||||
['runtime send path auto-starts Pi Rust instead of dead-end not-ready toast', runtime.includes('Pi Rust 正在启动,启动后自动发送') && runtime.includes('piLabStartPromise') && !runtime.includes('Pi 会话尚未就绪,请重新打开 Pi 面板或稍后重试')],
|
||||
['runtime keeps selected model before start/configure', runtime.includes('ensurePiToolCapableModel') && !runtime.includes('isPiToolUnsupportedModel') && runtime.includes('gpt-5.4-mini') && runtime.includes('freefirst')],
|
||||
['route applies TTL and rate limits', route.includes('PI_LAB_SESSION_TTL_MS') && route.includes('PI_LAB_RATE_LIMITS') && route.includes('check_rate_limit')],
|
||||
['route cleans expired session dirs and rate buckets', route.includes('fs::remove_dir_all') && route.includes('buckets.retain')],
|
||||
|
||||
@@ -185,8 +228,10 @@ const checks = [
|
||||
['@mnote/pi extension registers MNote tools', mnotePiExtension.includes('pi.registerTool') && mnotePiExtension.includes('mnote_current_page_read')],
|
||||
['@mnote/pi extension keeps legacy MNote bridge API fallback', mnotePiExtension.includes('/api/page-ai/pi/tool-call-bridge') && mnotePiExtension.includes('x-mnote-pi-lab-bridge-token')],
|
||||
['@mnote/pi extension uses Pi Rust native current-page read', mnotePiExtension.includes('pi-rust-native-fs') && mnotePiExtension.includes('PI_MNOTE_CONTEXT_FILE') && mnotePiExtension.includes('fs.readFileSync')],
|
||||
['@mnote/pi extension uses Pi Rust native local file read/patch', mnotePiExtension.includes('executeNativeLocalFileRead') && mnotePiExtension.includes('executeNativeLocalFilePatch') && !mnotePiExtension.includes('仍依赖旧 HTTP bridge')],
|
||||
['@mnote/pi extension resolves local file tools relative to selected folder', mnotePiExtension.includes('joinRelativePath') && mnotePiExtension.includes('preferFolder: true') && mnotePiExtension.includes('folderPath')],
|
||||
['@mnote/pi extension consumes Pi Rust input context anywhere after skill expansion', mnotePiExtension.includes('pi.on?.("input"') && mnotePiExtension.includes('MNOTE_PI_CONTEXT_V1') && mnotePiExtension.includes('text.indexOf(CONTEXT_PREFIX)') && mnotePiExtension.includes('action: "transform"')],
|
||||
['route preserves slash skill command before hidden input context', route.includes('pi_lab_command_message_for_session') && route.includes('message.starts_with("/skill:")') && route.includes('format!("{command}\\n{input_context_prefix}{effective_args}")')],
|
||||
['route preserves slash skill command while passing hidden context to extension hook', route.includes('pi_lab_command_message_for_session') && route.includes('message.starts_with("/skill:")') && route.includes('format!("{command}\\n{effective_args}")') && route.includes('format!("{input_context_prefix}{skill_args}")')],
|
||||
['@mnote/pi extension supports LightRAG tools', mnotePiExtension.includes('mnote_knowledge_rag_query') && mnotePiExtension.includes('mnote_knowledge_rag_section_context')],
|
||||
['@mnote/pi MCP extension registers mcp tool', mnotePiMcpExtension.includes('registerTool') && mnotePiMcpExtension.includes('name: "mcp"')],
|
||||
['@mnote/pi MCP extension uses official synchronous child_process bridge', mnotePiMcpExtension.includes('execFileSync') && mnotePiMcpExtension.includes('client.mjs') && mnotePiMcpExtension.includes('transport: "pi-rust-sync-client"') && !mnotePiMcpExtension.includes('/api/page-ai/pi/mcp-call-bridge') && !mnotePiMcpExtension.includes('fetch(')],
|
||||
@@ -225,11 +270,105 @@ const checks = [
|
||||
['gateway.rs does NOT stamp body hidden gate', !gateway.includes('data-page-ai-pi-lab-hidden')],
|
||||
['app.rs defaults Pi Lab config on', app.includes('enable_page_ai_pi_lab: env_bool("MNOTE_PAGE_AI_PI_LAB", true)')],
|
||||
|
||||
|
||||
// === Runtime JS: new API endpoints (STATE/COMPACT/QUEUE_CONFIG) ===
|
||||
['runtime uses /api/page-ai/pi/state', runtime.includes('/api/page-ai/pi/state')],
|
||||
['runtime uses /api/page-ai/pi/compact', runtime.includes('/api/page-ai/pi/compact')],
|
||||
['runtime uses /api/page-ai/pi/queue-config', runtime.includes('/api/page-ai/pi/queue-config')],
|
||||
['runtime has API.STATE constant', runtime.includes("STATE: '/api/page-ai/pi/state'")],
|
||||
['runtime has API.COMPACT constant', runtime.includes("COMPACT: '/api/page-ai/pi/compact'")],
|
||||
['runtime has API.QUEUE_CONFIG constant', runtime.includes("QUEUE_CONFIG: '/api/page-ai/pi/queue-config'")],
|
||||
['runtime maps session tree preview into history messages', runtime.includes('entry.text || entry.content || entry.preview')],
|
||||
['runtime fetches artifact diff with sessionId query', runtime.includes("'?sessionId=' + encodeURIComponent(sid)")],
|
||||
|
||||
// === Runtime JS: state sync function (no setInterval) ===
|
||||
['runtime has syncRuntimeState function', runtime.includes('function syncRuntimeState')],
|
||||
['syncRuntimeState calls API.STATE', runtime.includes("API.STATE") && runtime.includes('sessionId: piLabState.sessionId')],
|
||||
['syncRuntimeState syncs queuedMessages', runtime.includes('queuedMessages') && runtime.includes('piLabState.queuedMessages')],
|
||||
['syncRuntimeState syncs pendingMessageCount', runtime.includes('pendingMessageCount')],
|
||||
['syncRuntimeState syncs isCompacting', runtime.includes('isCompacting') && runtime.includes('piLabState.isCompacting')],
|
||||
['syncRuntimeState syncs contextUsage', runtime.includes('contextUsage') && runtime.includes('piLabState.contextUsage')],
|
||||
['syncRuntimeState syncs model/thinking from API.STATE', runtime.includes('data.modelProvider') && runtime.includes('data.thinkingLevel')],
|
||||
['syncRuntimeState called after showPiLab drawer open', runtime.indexOf('showPiLab') < runtime.indexOf('syncRuntimeState()') || runtime.includes('showPiLab') && runtime.includes('syncRuntimeState')],
|
||||
['syncRuntimeState called after startRuntime success (in success .then)', runtime.includes('syncRuntimeState();') && runtime.includes('Pi runtime ready') && runtime.includes('return data;')],
|
||||
['syncRuntimeState called after sendPrompt accepted (in sendPrompt .then)', runtime.includes('syncRuntimeState();') && runtime.includes('data.accepted') && runtime.includes('Pi Lab rejected prompt')],
|
||||
['syncRuntimeState called after abortPrompt (in abortPrompt function)', runtime.includes('syncRuntimeState();') && runtime.includes('abortPrompt') && runtime.includes('setState(STATE_ABORTED)')],
|
||||
['syncRuntimeState called in SSE connected event', runtime.includes("'connected'") && runtime.includes('syncRuntimeState()')],
|
||||
['syncRuntimeState called in runtime_started event', runtime.includes("runtime_started") && runtime.includes('syncRuntimeState()')],
|
||||
['syncRuntimeState called in runtime_aborted event', runtime.includes("runtime_aborted") && runtime.includes('syncRuntimeState')],
|
||||
['syncRuntimeState called from checkStatus when session running', runtime.includes("connectEventSource(data.session.sessionId)") && runtime.includes("syncRuntimeState()")],
|
||||
['syncRuntimeState does NOT use setInterval', !runtime.includes('setInterval(syncRuntimeState') && !runtime.includes("setInterval(syncRuntimeState")],
|
||||
|
||||
// === Runtime JS: compact function ===
|
||||
['runtime has triggerCompact function', runtime.includes('function triggerCompact')],
|
||||
['triggerCompact calls API.COMPACT', runtime.includes("API.COMPACT")],
|
||||
['triggerCompact sets isCompacting=true', runtime.includes('piLabState.isCompacting = true')],
|
||||
['triggerCompact guards against streaming in function body', runtime.includes('if (piLabState.status === STATE_STREAMING) return;')],
|
||||
['triggerCompact guards against replay in function body', runtime.includes('if (piLabState.viewingHistorySessionId) return;')],
|
||||
['triggerCompact shows compaction summary as diagnostic', runtime.includes('updateDiagnostics') && runtime.includes('Compaction:')],
|
||||
['triggerCompact inserts compaction result card', runtime.includes("type: 'compaction'") && runtime.includes('data-page-ai-pi-lab-compaction-result')],
|
||||
['triggerCompact shows toast on done', runtime.includes('showPiToast') && runtime.includes('会话压缩完成')],
|
||||
['triggerCompact shows toast on failure', runtime.includes('showPiToast') && runtime.includes('压缩失败')],
|
||||
|
||||
// === Runtime JS: queue config function ===
|
||||
['runtime has fetchQueueConfig function', runtime.includes('function fetchQueueConfig')],
|
||||
['fetchQueueConfig calls API.QUEUE_CONFIG', runtime.includes("API.QUEUE_CONFIG")],
|
||||
['fetchQueueConfig posts sessionId JSON body', runtime.includes("method: 'POST'") && runtime.includes('sessionId: piLabState.sessionId')],
|
||||
|
||||
// === Runtime JS: compact button in UI ===
|
||||
['runtime has compact button in commandbar', runtime.includes('data-page-ai-pi-lab-btn-compact')],
|
||||
['compact button disabled in updateButtons', runtime.includes('compactBtn.disabled')],
|
||||
['compact button wired in wireEvents', runtime.includes('compactBtn.addEventListener') && runtime.includes('triggerCompact')],
|
||||
['compact button uses compact icon', runtime.includes("compact: '<path d=\"M4 8h16M4 16h16\"/><path d=\"M8 4 12 8 16 4M8 20l4-4 4 4\"/>")],
|
||||
|
||||
// === Runtime JS: new state fields ===
|
||||
['runtime has queuedMessages state field', runtime.includes('queuedMessages: []')],
|
||||
['runtime has pendingMessageCount state field', runtime.includes('pendingMessageCount: 0')],
|
||||
['runtime has isCompacting state field', runtime.includes('isCompacting: false')],
|
||||
['runtime has contextUsage state field', runtime.includes('contextUsage: {}')],
|
||||
// === Key architectural constraints ===
|
||||
['no modification of sidebar-page-ai-runtime', !runtime.includes('sidebar-page-ai-runtime')],
|
||||
['no sidebar-page-ai-runtime default behavior change',
|
||||
!runtime.includes('sidebarPageAiRuntime')],
|
||||
['no import of main page AI runtime', !runtime.includes('import.*sidebar-page-ai')],
|
||||
|
||||
// === B2: JSONL reading constants in page_ai_pi.rs ===
|
||||
["route has PI_LAB_JSONL_MAX_FILE_BYTES constant", route.includes("PI_LAB_JSONL_MAX_FILE_BYTES")],
|
||||
["route has PI_LAB_JSONL_MAX_LINE_BYTES constant", route.includes("PI_LAB_JSONL_MAX_LINE_BYTES")],
|
||||
["route has PI_LAB_JSONL_MAX_ENTRIES constant", route.includes("PI_LAB_JSONL_MAX_ENTRIES")],
|
||||
["route has PI_LAB_JSONL_WINDOW_ENTRIES constant", route.includes("PI_LAB_JSONL_WINDOW_ENTRIES")],
|
||||
|
||||
// === B2: JSONL helper functions ===
|
||||
["route has read_pi_session_jsonl function", route.includes("fn read_pi_session_jsonl")],
|
||||
["route has build_pi_entry_tree function", route.includes("fn build_pi_entry_tree")],
|
||||
|
||||
// === B1: pi_session_file field ===
|
||||
["route serializes piSessionFile in build_run_runtime_json", route.includes("piSessionFile")],
|
||||
|
||||
// === B3: session_tree endpoint ===
|
||||
["route has PI_LAB_SCHEMA_SESSION_TREE constant", route.includes("PI_LAB_SCHEMA_SESSION_TREE")],
|
||||
["route has session_tree endpoint function", route.includes("pub async fn session_tree")],
|
||||
|
||||
// === B5: fork endpoint ===
|
||||
["route has PI_LAB_SCHEMA_FORK constant", route.includes("PI_LAB_SCHEMA_FORK")],
|
||||
["route has fork_pi_session endpoint function", route.includes("pub async fn fork_pi_session")],
|
||||
|
||||
// === D2: artifact_diff endpoint ===
|
||||
["route has PI_LAB_SCHEMA_ARTIFACT_DIFF constant", route.includes("PI_LAB_SCHEMA_ARTIFACT_DIFF")],
|
||||
["route has artifact_diff endpoint function", route.includes("pub async fn artifact_diff")],
|
||||
["artifact_diff query accepts camelCase sessionId", route.includes('serde(rename_all = "camelCase")') && route.includes("pub struct PiLabArtifactDiffQuery")],
|
||||
["route publishes file patch artifact event", route.includes('"artifact_file_patch"') && route.includes('mnote.page_ai_pi.artifact.file_patch.v1')],
|
||||
|
||||
// === mod.rs: new B3/B5/D2 routes ===
|
||||
["mod.rs mounts pi session_tree route", routesMod.includes("/api/page-ai/pi/sessions/{session_id}/tree")],
|
||||
["mod.rs mounts pi fork route", routesMod.includes("/api/page-ai/pi/fork")],
|
||||
["mod.rs mounts pi artifact_diff route", routesMod.includes("/api/page-ai/pi/artifacts/{tool_event_id}/diff")],
|
||||
|
||||
// === mod.rs: state/compact/queue_config/configure routes ===
|
||||
["mod.rs mounts pi state route", routesMod.includes("/api/page-ai/pi/state")],
|
||||
["mod.rs mounts pi compact route", routesMod.includes("/api/page-ai/pi/compact")],
|
||||
["mod.rs mounts pi queue-config route", routesMod.includes("/api/page-ai/pi/queue-config")],
|
||||
["mod.rs mounts pi configure route", routesMod.includes("/api/page-ai/pi/configure")],
|
||||
];
|
||||
|
||||
const failed = checks.filter(([, ok]) => !ok);
|
||||
|
||||
Reference in New Issue
Block a user