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);
|
|||
|
|
});
|