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