Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
202 lines
7.5 KiB
JavaScript
202 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
||
// S1: management defaultModel → effective → Pi status/start truth.
|
||
// Requires mnote-web with MNOTE_WEB_ALLOW_DEV_FIXTURES=1 (dev:hot default).
|
||
// Prefer MNOTE_PAGE_AI_PI_LAB_RUNTIME=mock for a fast local loop.
|
||
|
||
"use strict";
|
||
|
||
const fs = require("node:fs");
|
||
const os = require("node:os");
|
||
const path = require("node:path");
|
||
|
||
const BASE = process.env.MNOTE_S1_BASE || process.env.MNOTE_PI_LAB_BASE || "http://127.0.0.1:3017";
|
||
const ACTOR_ID = process.env.MNOTE_S1_ACTOR_ID || "s1-effective-model-admin";
|
||
const AUTH = process.env.MNOTE_S1_AUTH || "Bearer s1-effective-model";
|
||
const ROOT =
|
||
process.env.MNOTE_S1_ROOT ||
|
||
fs.mkdtempSync(path.join(os.tmpdir(), "mnote-s1-effective-model-"));
|
||
|
||
function assert(condition, message) {
|
||
if (!condition) throw new Error(message);
|
||
}
|
||
|
||
async function fetchJson(url, options = {}) {
|
||
const headers = {
|
||
"Content-Type": "application/json",
|
||
Authorization: AUTH,
|
||
"x-mnote-actor-id": ACTOR_ID,
|
||
"x-mnote-actor-type": "admin",
|
||
...(options.headers || {}),
|
||
};
|
||
const res = await fetch(url, { ...options, headers });
|
||
const text = await res.text();
|
||
let body = {};
|
||
try {
|
||
body = text ? JSON.parse(text) : {};
|
||
} catch {
|
||
body = { raw: text };
|
||
}
|
||
return { status: res.status, body };
|
||
}
|
||
|
||
async function seedWorkspace(rootUri, rootPath) {
|
||
const seed = await fetchJson(`${BASE}/api/dev/seed`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
seeds: [
|
||
{
|
||
kind: "setupWorkspace",
|
||
user_id: ACTOR_ID,
|
||
email: `${ACTOR_ID}@example.com`,
|
||
username: ACTOR_ID,
|
||
display_name: ACTOR_ID,
|
||
role: "admin",
|
||
password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!",
|
||
workspace_id: `local-ws:${ACTOR_ID}:s1`,
|
||
workspace_name: "S1 effective model",
|
||
root_uri: rootUri,
|
||
root_path: rootPath,
|
||
source_kind: "local_folder",
|
||
permission: "write",
|
||
capabilities: ["ai"],
|
||
grant_source: "s1_effective_model_smoke",
|
||
grant_created_by: ACTOR_ID,
|
||
},
|
||
],
|
||
}),
|
||
});
|
||
if (seed.status === 403 && seed.body && seed.body.code === "dev_seed_disabled") {
|
||
throw new Error(
|
||
"S1 smoke 需要 /api/dev/seed;请以 MNOTE_WEB_ALLOW_DEV_FIXTURES=1 启动 mnote-web(npm run dev:hot 默认开启)。",
|
||
);
|
||
}
|
||
assert(
|
||
seed.status === 200 && seed.body.ok === true,
|
||
`/api/dev/seed failed: ${seed.status} ${JSON.stringify(seed.body)}`,
|
||
);
|
||
}
|
||
|
||
async function main() {
|
||
fs.mkdirSync(ROOT, { recursive: true });
|
||
fs.writeFileSync(path.join(ROOT, "s1.md"), "# S1 effective model\n", "utf8");
|
||
const rootUri = `file://${ROOT}`;
|
||
|
||
console.log(`\n🧪 S1 effective model → Pi start (base=${BASE}, actor=${ACTOR_ID})\n`);
|
||
|
||
await seedWorkspace(rootUri, ROOT);
|
||
|
||
// Baseline admin settings (global owner for admin actor).
|
||
const adminBefore = await fetchJson(`${BASE}/api/ai-admin/settings`);
|
||
assert(adminBefore.status === 200 && adminBefore.body.ok === true, `admin get: ${adminBefore.status} ${JSON.stringify(adminBefore.body)}`);
|
||
const originalDefault = String(adminBefore.body.defaultModel || "omniroute/freefirst");
|
||
let allowed = Array.isArray(adminBefore.body.allowedModels)
|
||
? adminBefore.body.allowedModels.map(String)
|
||
: [];
|
||
// Ensure both candidates exist for the switch.
|
||
for (const model of ["omniroute/freefirst", "omniroute/gpt-5.4-mini", "omniroute/pi-fast", "omniroute/pi-reason"]) {
|
||
if (!allowed.includes(model)) allowed.push(model);
|
||
}
|
||
|
||
const targetA = "omniroute/pi-fast";
|
||
const targetB = "omniroute/pi-reason";
|
||
console.log(` original defaultModel=${originalDefault}`);
|
||
|
||
// ── Phase A: set default to pi-fast ─────────────────────────────────
|
||
const putA = await fetchJson(`${BASE}/api/ai-admin/settings`, {
|
||
method: "PUT",
|
||
body: JSON.stringify({
|
||
defaultModel: targetA,
|
||
allowedModels: allowed,
|
||
}),
|
||
});
|
||
assert(putA.status === 200 && putA.body.ok === true, `admin put A: ${putA.status} ${JSON.stringify(putA.body)}`);
|
||
|
||
const effectiveA = await fetchJson(`${BASE}/api/ai-settings/effective`);
|
||
assert(effectiveA.status === 200, `effective A: ${effectiveA.status}`);
|
||
assert(
|
||
String(effectiveA.body.defaultModel) === targetA,
|
||
`effective A defaultModel=${effectiveA.body.defaultModel} expected ${targetA}`,
|
||
);
|
||
console.log(` [A] effective.defaultModel=${effectiveA.body.defaultModel}`);
|
||
|
||
const statusA = await fetchJson(`${BASE}/api/page-ai/pi/status`);
|
||
assert(statusA.status === 200 && statusA.body.ok === true, `status A: ${statusA.status} ${JSON.stringify(statusA.body)}`);
|
||
const statusRefA = String(
|
||
statusA.body.defaultModel ||
|
||
`${statusA.body.defaultModelProvider}/${statusA.body.defaultModelId}`,
|
||
);
|
||
assert(
|
||
statusRefA === targetA,
|
||
`status A default ${statusRefA} != ${targetA}`,
|
||
);
|
||
console.log(` [A] status.defaultModel=${statusRefA}`);
|
||
|
||
const startA = await fetchJson(`${BASE}/api/page-ai/pi/start`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
sessionId: `pi_lab_s1_a_${Date.now()}`,
|
||
rootUri,
|
||
// omit modelProvider/modelId → must use default
|
||
}),
|
||
});
|
||
assert(startA.status === 200 && startA.body.ok === true, `start A: ${startA.status} ${JSON.stringify(startA.body)}`);
|
||
const sessionA = startA.body.session || {};
|
||
const startedA = `${sessionA.modelProvider}/${sessionA.modelId}`;
|
||
assert(startedA === targetA, `start A model ${startedA} != ${targetA}`);
|
||
console.log(` [A] start session model=${startedA}`);
|
||
|
||
// ── Phase B: change default to pi-reason; next start must pick it up ─
|
||
const putB = await fetchJson(`${BASE}/api/ai-admin/settings`, {
|
||
method: "PUT",
|
||
body: JSON.stringify({
|
||
defaultModel: targetB,
|
||
allowedModels: allowed,
|
||
}),
|
||
});
|
||
assert(putB.status === 200 && putB.body.ok === true, `admin put B: ${putB.status} ${JSON.stringify(putB.body)}`);
|
||
|
||
const effectiveB = await fetchJson(`${BASE}/api/ai-settings/effective`);
|
||
assert(String(effectiveB.body.defaultModel) === targetB, `effective B=${effectiveB.body.defaultModel}`);
|
||
console.log(` [B] effective.defaultModel=${effectiveB.body.defaultModel}`);
|
||
|
||
const statusB = await fetchJson(`${BASE}/api/page-ai/pi/status`);
|
||
const statusRefB = String(
|
||
statusB.body.defaultModel ||
|
||
`${statusB.body.defaultModelProvider}/${statusB.body.defaultModelId}`,
|
||
);
|
||
assert(statusRefB === targetB, `status B default ${statusRefB} != ${targetB}`);
|
||
console.log(` [B] status.defaultModel=${statusRefB}`);
|
||
|
||
const startB = await fetchJson(`${BASE}/api/page-ai/pi/start`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
sessionId: `pi_lab_s1_b_${Date.now()}`,
|
||
rootUri,
|
||
}),
|
||
});
|
||
assert(startB.status === 200 && startB.body.ok === true, `start B: ${startB.status} ${JSON.stringify(startB.body)}`);
|
||
const sessionB = startB.body.session || {};
|
||
const startedB = `${sessionB.modelProvider}/${sessionB.modelId}`;
|
||
assert(
|
||
startedB === targetB,
|
||
`start B model ${startedB} != ${targetB} (policy change must apply on next start)`,
|
||
);
|
||
console.log(` [B] start session model=${startedB}`);
|
||
|
||
// Best-effort restore
|
||
if (originalDefault) {
|
||
await fetchJson(`${BASE}/api/ai-admin/settings`, {
|
||
method: "PUT",
|
||
body: JSON.stringify({ defaultModel: originalDefault, allowedModels: allowed }),
|
||
});
|
||
console.log(` restored defaultModel=${originalDefault}`);
|
||
}
|
||
|
||
console.log("\n✅ S1 effective model → Pi start smoke passed\n");
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error("\n❌ S1 smoke failed:", err.message || err);
|
||
process.exit(1);
|
||
});
|