Files
mnote/recycle/20260712-pi-ts-openhub-retirement/scripts/task775-openhub-mnote-scope-isolation-smoke.js
T

256 lines
7.8 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const TASK = "task775-openhub-mnote-scope-isolation-smoke";
const OPENHUB_BASE_URL = (process.env.MNOTE_OPENHUB_BASE_URL || "http://127.0.0.1:18080").replace(/\/+$/, "");
const WORKSPACE_ROOT = process.env.MNOTE_OPENHUB_SMOKE_ROOT
|| "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space";
const ROOT_URI = process.env.MNOTE_OPENHUB_SCOPE_ROOT_URI || `file://${WORKSPACE_ROOT}`;
const WORKSPACE_KEY = process.env.MNOTE_OPENHUB_SCOPE_WORKSPACE_KEY || "local-ws:mnote-e2e:my-space";
const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_OPENHUB_SCOPE_ISOLATION_TIMEOUT_MS || 15_000);
const STREAM_PRIME_MS = Number(process.env.MNOTE_OPENHUB_SCOPE_ISOLATION_STREAM_PRIME_MS || 3_000);
class SmokeFailure extends Error {
constructor(kind, message, details = {}) {
super(message);
this.name = "SmokeFailure";
this.kind = kind;
this.details = details;
}
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function buildScopeHeaders(ownerLabel, sessionScope) {
return {
"content-type": "application/json",
"X-MNote-User-Key": `task775:${ownerLabel}`,
"X-MNote-Workspace-Key": WORKSPACE_KEY,
"X-MNote-Session-Scope": sessionScope,
"X-MNote-Root-Uri": ROOT_URI,
"X-MNote-Page-Resource-Id": "task775-openhub-scope-isolation",
"X-MNote-Tool-Permission-Scope": JSON.stringify({
source: TASK,
allowedRoots: [ROOT_URI],
}),
"X-MNote-WeKnora-Tool-Scope": JSON.stringify({
source: TASK,
enabled: false,
}),
};
}
async function fetchWithTimeout(url, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, {
...init,
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
}
async function readJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : null;
} catch {
return text;
}
}
async function assertOpenHubReachable(headers) {
const response = await fetchWithTimeout(`${OPENHUB_BASE_URL}/api/sessions?page=1&page_size=1`, {
method: "GET",
headers,
});
const payload = await readJsonResponse(response);
if (!response.ok) {
throw new SmokeFailure("openhub_unreachable", `OpenHub MNote scope API 不可用:HTTP ${response.status}`, {
baseUrl: OPENHUB_BASE_URL,
payload,
});
}
return {
status: response.status,
success: payload?.success === true,
};
}
async function primeSessionWithOwnerA(headers, sessionId, marker) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), STREAM_PRIME_MS);
let response;
let streamSnippet = "";
try {
response = await fetch(`${OPENHUB_BASE_URL}/api/query/stream`, {
method: "POST",
headers,
body: JSON.stringify({
question: `请只回复 ${marker},不要解释。`,
conversation_id: sessionId,
agent: "build",
model: {
providerID: process.env.MNOTE_OPENHUB_SCOPE_MODEL_PROVIDER || "opencodego",
modelID: process.env.MNOTE_OPENHUB_SCOPE_MODEL_ID || "deepseek-v4-flash",
currentUsage: 0,
monthlyLimit: 0,
},
}),
signal: controller.signal,
});
if (!response.ok) {
const payload = await readJsonResponse(response);
throw new SmokeFailure("query_stream_failed", `Owner A 创建 session/message 失败:HTTP ${response.status}`, {
payload,
});
}
if (response.body) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (streamSnippet.length < 2_000) {
const readResult = await Promise.race([
reader.read(),
delay(250).then(() => ({ timedOut: true })),
]);
if (readResult.timedOut) break;
if (readResult.done) break;
streamSnippet += decoder.decode(readResult.value, { stream: true });
if (streamSnippet.includes(marker) || streamSnippet.includes("\"type\"")) break;
}
} finally {
await reader.cancel().catch(() => undefined);
}
}
} catch (error) {
if (error && error.name !== "AbortError") {
throw error;
}
} finally {
clearTimeout(timer);
}
return {
status: response?.status || 0,
streamSnippet: streamSnippet.slice(0, 500),
};
}
async function fetchMessages(headers, sessionId) {
const response = await fetchWithTimeout(
`${OPENHUB_BASE_URL}/api/sessions/${encodeURIComponent(sessionId)}/messages`,
{
method: "GET",
headers,
},
);
const payload = await readJsonResponse(response);
return {
status: response.status,
ok: response.ok,
payload,
};
}
async function waitForOwnerAMessages(headers, sessionId, marker) {
let last = null;
for (let attempt = 0; attempt < 12; attempt += 1) {
last = await fetchMessages(headers, sessionId);
const messages = Array.isArray(last.payload?.data) ? last.payload.data : [];
const hasMarker = messages.some((message) => String(message.content || "").includes(marker));
if (last.ok && hasMarker) {
return {
status: last.status,
readable: true,
count: messages.length,
roles: messages.map((message) => message.role),
markerFound: true,
sample: messages.slice(-3).map((message) => ({
role: message.role,
content: String(message.content || "").slice(0, 240),
})),
};
}
await delay(500);
}
throw new SmokeFailure("owner_a_messages_not_readable", "Owner A 未能读取到自己创建的 session/messages", {
last,
});
}
function assertOwnerBBlocked(ownerBResult) {
if (ownerBResult.status === 403 || ownerBResult.status === 404) {
return {
blocked: true,
status: ownerBResult.status,
detail: ownerBResult.payload?.detail || ownerBResult.payload,
};
}
throw new SmokeFailure("owner_b_not_blocked", "Owner B 读取到了或可访问 Owner A 的 session/messages", {
status: ownerBResult.status,
payload: ownerBResult.payload,
});
}
async function main() {
const runId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
const sessionId = `task775-openhub-scope-${runId}`;
const sessionScope = `task775-openhub-scope-isolation:${runId}`;
const marker = `MNOTE_OPENHUB_SCOPE_ISOLATION_${runId}`;
const headersA = buildScopeHeaders("owner-a", sessionScope);
const headersB = buildScopeHeaders("owner-b", sessionScope);
let result = {
ok: false,
task: TASK,
mode: "direct-openhub-mnote-headers-backend-scope-isolation",
openhubBaseUrl: OPENHUB_BASE_URL,
sessionId,
};
try {
const health = await assertOpenHubReachable(headersA);
const stream = await primeSessionWithOwnerA(headersA, sessionId, marker);
const ownerA = await waitForOwnerAMessages(headersA, sessionId, marker);
const ownerB = assertOwnerBBlocked(await fetchMessages(headersB, sessionId));
result = {
...result,
ok: true,
health,
stream,
ownerA,
ownerB,
summary: {
sessionId,
ownerAReadable: ownerA.readable,
ownerBBlocked: ownerB.blocked,
ownerBStatus: ownerB.status,
},
};
} catch (error) {
result = {
...result,
ok: false,
failureKind: error instanceof SmokeFailure ? error.kind : "unexpected_error",
error: error instanceof Error ? error.stack || error.message : String(error),
details: error instanceof SmokeFailure ? error.details : undefined,
};
}
console.log(JSON.stringify(result, null, 2));
if (!result.ok) process.exit(1);
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});