#!/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); });