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.
213 lines
7.1 KiB
JavaScript
213 lines
7.1 KiB
JavaScript
#!/usr/bin/env node
|
|
// S6: Pi session resume/history from control-plane (seedAiRuntime + hydrate).
|
|
// Requires mnote-web with MNOTE_WEB_ALLOW_DEV_FIXTURES=1 + MNOTE_PAGE_AI_PI_LAB_RUNTIME=mock.
|
|
|
|
"use strict";
|
|
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
|
|
const BASE = process.env.MNOTE_S6_BASE || process.env.MNOTE_PI_LAB_BASE || "http://127.0.0.1:3017";
|
|
const ACTOR_ID = process.env.MNOTE_S6_ACTOR_ID || "s6-hydrate-admin";
|
|
const AUTH = process.env.MNOTE_S6_AUTH || "Bearer s6-hydrate";
|
|
const ROOT =
|
|
process.env.MNOTE_S6_ROOT ||
|
|
fs.mkdtempSync(path.join(os.tmpdir(), "mnote-s6-hydrate-"));
|
|
const STAMP = Date.now();
|
|
|
|
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 main() {
|
|
fs.mkdirSync(ROOT, { recursive: true });
|
|
const pagePath = `s6-hydrate-${STAMP}.md`;
|
|
fs.writeFileSync(path.join(ROOT, pagePath), "# S6 hydrate\n", "utf8");
|
|
const rootUri = `file://${ROOT}`;
|
|
const workspaceId = `local-ws:${ACTOR_ID}:s6`;
|
|
const sessionId = `pi_lab_s6_${STAMP}`;
|
|
const runId = `pi_run_${sessionId}`;
|
|
const piSessionDir = path.join(ROOT, "pi-session");
|
|
const piSessionFile = path.join(piSessionDir, `${STAMP}_session.jsonl`);
|
|
fs.mkdirSync(piSessionDir, { recursive: true });
|
|
const historyText = `S6_HYDRATE_REPLY_${STAMP}`;
|
|
fs.writeFileSync(
|
|
piSessionFile,
|
|
[
|
|
JSON.stringify({
|
|
id: "u1",
|
|
type: "message",
|
|
message: { role: "user", content: "hydrate me" },
|
|
seq: 1,
|
|
}),
|
|
JSON.stringify({
|
|
id: "a1",
|
|
parentId: "u1",
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: [{ type: "text", text: historyText }],
|
|
},
|
|
seq: 2,
|
|
}),
|
|
].join("\n") + "\n",
|
|
"utf8",
|
|
);
|
|
|
|
console.log(`\n🧪 S6 Pi resume/history hydrate (base=${BASE}, session=${sessionId})\n`);
|
|
|
|
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: workspaceId,
|
|
workspace_name: "S6 hydrate",
|
|
root_uri: rootUri,
|
|
root_path: ROOT,
|
|
source_kind: "local_folder",
|
|
permission: "write",
|
|
capabilities: ["ai"],
|
|
grant_source: "s6_hydrate_smoke",
|
|
grant_created_by: ACTOR_ID,
|
|
},
|
|
{
|
|
kind: "seedAiRuntime",
|
|
user_id: ACTOR_ID,
|
|
workspace_id: workspaceId,
|
|
document_id: pagePath,
|
|
session_id: sessionId,
|
|
run_id: runId,
|
|
title: "S6 hydrate session",
|
|
profile: "pi_lab",
|
|
acp_runtime: "pi",
|
|
status: "runtime_running",
|
|
runtime_json: {
|
|
runtimeMode: "mock",
|
|
rootUri,
|
|
workspaceId,
|
|
pagePath,
|
|
pageTitle: "S6 hydrate",
|
|
modelProvider: "omniroute",
|
|
modelId: "pi-fast",
|
|
thinkingLevel: "medium",
|
|
piSessionDir,
|
|
piSessionFile,
|
|
messageCount: 2,
|
|
},
|
|
payload_json: { message: "s6 hydrate" },
|
|
events: [
|
|
{
|
|
eventType: "user_prompt",
|
|
payloadJson: { message: "hydrate me" },
|
|
},
|
|
{
|
|
eventType: "session_seeded",
|
|
payloadJson: { source: "s6_smoke" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
if (seed.status === 403 && seed.body && seed.body.code === "dev_seed_disabled") {
|
|
throw new Error(
|
|
"S6 smoke 需要 /api/dev/seed;请以 MNOTE_WEB_ALLOW_DEV_FIXTURES=1 启动 mnote-web。",
|
|
);
|
|
}
|
|
assert(
|
|
seed.status === 200 && seed.body.ok === true,
|
|
`/api/dev/seed failed: ${seed.status} ${JSON.stringify(seed.body)}`,
|
|
);
|
|
console.log(" seeded workspace + ai runtime");
|
|
|
|
// list_sessions should surface the seeded Pi Lab run from control-plane.
|
|
const listed = await fetchJson(
|
|
`${BASE}/api/page-ai/pi/sessions?workspaceId=${encodeURIComponent(workspaceId)}&limit=50`,
|
|
);
|
|
assert(listed.status === 200 && listed.body.ok === true, `list: ${listed.status} ${JSON.stringify(listed.body)}`);
|
|
assert(
|
|
String(listed.body.schema) === "mnote.page_ai_pi.list_sessions.v1",
|
|
`list schema: ${listed.body.schema}`,
|
|
);
|
|
const sessions = Array.isArray(listed.body.sessions) ? listed.body.sessions : [];
|
|
const found = sessions.find((s) => String(s.sessionId) === sessionId);
|
|
assert(found, `list_sessions missing ${sessionId}: ${JSON.stringify(sessions).slice(0, 500)}`);
|
|
console.log(` list_sessions includes ${sessionId} status=${found.status}`);
|
|
|
|
// get_session_history should return the control-plane run (even without hot cache).
|
|
const history = await fetchJson(`${BASE}/api/page-ai/pi/sessions/${encodeURIComponent(sessionId)}`);
|
|
assert(
|
|
history.status === 200 && history.body.ok !== false,
|
|
`history: ${history.status} ${JSON.stringify(history.body).slice(0, 400)}`,
|
|
);
|
|
// Accept either structured history messages or run metadata with runtime json.
|
|
const bodyText = JSON.stringify(history.body);
|
|
assert(
|
|
bodyText.includes(sessionId) ||
|
|
bodyText.includes(runId) ||
|
|
bodyText.includes("hydrate") ||
|
|
bodyText.includes(historyText) ||
|
|
bodyText.includes("pi_lab"),
|
|
`history payload should identify session: ${bodyText.slice(0, 500)}`,
|
|
);
|
|
console.log(" get_session_history ok");
|
|
|
|
// Cold path: abort uses get_session_for_context → hydrate_session_from_control_plane.
|
|
// Session is NOT in process hot cache after seed-only; hydrate must restore it.
|
|
const abort = await fetchJson(`${BASE}/api/page-ai/pi/abort`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ sessionId }),
|
|
});
|
|
assert(
|
|
abort.status === 200 && abort.body.ok === true && abort.body.aborted === true,
|
|
`hydrate via abort failed (session not restored from control-plane): ${abort.status} ${JSON.stringify(abort.body)}`,
|
|
);
|
|
console.log(" hydrate via abort: session restored from control-plane");
|
|
|
|
// Second operation after hydrate still works (hot cache now warm).
|
|
const abort2 = await fetchJson(`${BASE}/api/page-ai/pi/abort`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ sessionId }),
|
|
});
|
|
assert(
|
|
abort2.status === 200 && abort2.body.ok === true,
|
|
`warm abort: ${abort2.status} ${JSON.stringify(abort2.body)}`,
|
|
);
|
|
console.log(" warm-cache abort ok");
|
|
|
|
console.log("\n✅ S6 Pi resume/history hydrate smoke passed\n");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("\n❌ S6 smoke failed:", err.message || err);
|
|
process.exit(1);
|
|
});
|