256 lines
7.8 KiB
JavaScript
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);
|
|
});
|