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