diff --git a/scripts/check-goosed-v149-all.mjs b/scripts/check-goosed-v149-all.mjs index 8fbae4b..fef1f25 100644 --- a/scripts/check-goosed-v149-all.mjs +++ b/scripts/check-goosed-v149-all.mjs @@ -35,9 +35,11 @@ const checks = [ { name: 'reply-smoke', script: 'check-goosed-v149-reply-smoke.mjs', required: true }, { name: 'resume', script: 'check-goosed-v149-resume.mjs', required: true }, { name: 'sandbox-fs', script: 'check-goosed-v149-sandbox-fs.mjs', required: true }, + { name: 'sandbox-page', script: 'check-goosed-v149-sandbox-page.mjs', required: true }, { name: 'executors', script: 'check-goosed-v149-executors.mjs', required: true }, { name: 'memory-loop', script: 'check-goosed-v149-memory-loop.mjs', required: true }, { name: 'portal-smoke', script: 'check-goosed-v149-portal-smoke.mjs', required: false }, + { name: 'portal-resume', script: 'check-goosed-v149-portal-resume.mjs', required: false }, { name: 'provider', script: 'check-goosed-v149-provider.mjs', diff --git a/scripts/check-goosed-v149-portal-resume.mjs b/scripts/check-goosed-v149-portal-resume.mjs new file mode 100644 index 0000000..a8fe6ed --- /dev/null +++ b/scripts/check-goosed-v149-portal-resume.mjs @@ -0,0 +1,125 @@ +#!/usr/bin/env node +/** + * Portal integration: start/resume session and verify conversation round-trip. + */ +import crypto from 'node:crypto'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { applyGooseV149CanaryBlockEnv } from './goose-v149-canary.mjs'; +import { loadMemindEnvFiles } from './memind-runtime-profile.mjs'; +import { + createAgentRun, + createReporter, + loginViaApi, + resolvePortalBase, + waitForRunTerminal, +} from './scenario-test-lib.mjs'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +loadMemindEnvFiles(root, process.env); +applyGooseV149CanaryBlockEnv(process.env, root); + +const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081)); +const skipPortal = process.env.GOOSE_V149_PORTAL_SMOKE_SKIP === '1'; + +function messageCount(session) { + const conversation = session?.conversation; + if (Array.isArray(conversation?.messages)) return conversation.messages.length; + if (Array.isArray(conversation)) return conversation.length; + return Number(session?.message_count ?? session?.messageCount ?? 0); +} + +async function portalReachable() { + try { + const response = await fetch(`${baseUrl}/auth/status`); + return response.ok; + } catch { + return false; + } +} + +async function main() { + if (skipPortal || !(await portalReachable())) { + console.log(`GOOSE_V149_PORTAL_RESUME_SKIP: Portal not running at ${baseUrl}`); + console.log('GOOSE_V149_PORTAL_RESUME_OK: skipped'); + return; + } + + const reporter = createReporter(); + const username = process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john'; + const password = + process.env.JOHN_PASSWORD + ?? process.env.H5_ACCESS_PASSWORD + ?? process.env.MEMIND_PASSWORD + ?? ''; + if (!password) { + throw new Error('set JOHN_PASSWORD (or H5_ACCESS_PASSWORD) for portal resume smoke'); + } + + const auth = await loginViaApi(baseUrl, { username, password }, reporter); + + const startRes = await fetch(`${baseUrl}/api/agent/start`, { + method: 'POST', + headers: { Cookie: auth.cookie, 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + const started = await startRes.json().catch(() => ({})); + if (!startRes.ok || !started?.id) { + throw new Error(`agent/start failed: ${startRes.status}`); + } + const sessionId = started.id; + + const run = await createAgentRun( + baseUrl, + auth.cookie, + sessionId, + 'portal resume smoke ping', + crypto.randomUUID(), + ); + const terminal = await waitForRunTerminal(baseUrl, auth.cookie, run.runId, 120_000); + reporter.pass('agent run', terminal.status); + + const resumeRes = await fetch(`${baseUrl}/api/agent/resume`, { + method: 'POST', + headers: { + Cookie: auth.cookie, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + session_id: sessionId, + load_model_and_extensions: true, + }), + }); + const resumeBody = await resumeRes.json().catch(() => ({})); + if (!resumeRes.ok) { + throw new Error(`resume failed: ${resumeRes.status} ${JSON.stringify(resumeBody).slice(0, 300)}`); + } + + const detailRes = await fetch(`${baseUrl}/api/sessions/${encodeURIComponent(sessionId)}`, { + headers: { Cookie: auth.cookie }, + }); + const detail = await detailRes.json().catch(() => ({})); + if (!detailRes.ok) { + throw new Error(`GET session failed: ${detailRes.status}`); + } + + const detailCount = messageCount(detail); + const resumedCount = messageCount(resumeBody?.session ?? resumeBody); + if (detailCount <= 0 && resumedCount <= 0) { + console.log( + `GOOSE_V149_PORTAL_RESUME_WARN: resume ok but conversation empty ` + + `(Portal agent/runs may not mirror into goosed PG yet; run=${terminal.status})`, + ); + } + + console.log( + `GOOSE_V149_PORTAL_RESUME_OK: session=${sessionId} detailMessages=${detailCount} ` + + `resumedMessages=${resumedCount} run=${terminal.status} base=${baseUrl}`, + ); +} + +main().catch((error) => { + console.error(`GOOSE_V149_PORTAL_RESUME_FAIL: ${error.message}`); + process.exit(1); +}); diff --git a/scripts/check-goosed-v149-sandbox-page.mjs b/scripts/check-goosed-v149-sandbox-page.mjs new file mode 100644 index 0000000..1d8dfbc --- /dev/null +++ b/scripts/check-goosed-v149-sandbox-page.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node +/** + * Verify sandbox-fs write_file + edit_file materialize HTML via /agent/call_tool. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { createV149Client } from './goose-v149-sse.mjs'; + +const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +const nodeExec = process.env.GOOSED_MCP_NODE_PATH || process.execPath; +const serverPath = + process.env.GOOSED_MCP_SERVER_PATH || path.join(memindRoot, 'mindspace-sandbox-mcp.mjs'); +const pollMs = Number(process.env.GOOSE_V149_SANDBOX_POLL_MS || 500); +const timeoutMs = Number(process.env.GOOSE_V149_SANDBOX_TIMEOUT_MS || 45_000); +const pageRel = 'public/goose-v149-sandbox-page.html'; + +const client = createV149Client(); +const sandboxRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'goose-v149-page-')); +fs.mkdirSync(path.join(sandboxRoot, 'public'), { recursive: true }); + +async function waitForWriteTool(sessionId) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const response = await client.apiFetch( + `/agent/tools?session_id=${encodeURIComponent(sessionId)}&extension_name=sandbox-fs`, + { method: 'GET' }, + ); + const tools = await response.json(); + if (tools.some((tool) => tool.name === 'sandbox-fs__write_file')) return; + await new Promise((resolve) => setTimeout(resolve, pollMs)); + } + throw new Error(`sandbox-fs write tool not ready within ${timeoutMs}ms`); +} + +async function callTool(sessionId, name, arguments_) { + return client.apiJson('/agent/call_tool', { + session_id: sessionId, + name, + arguments: arguments_, + }); +} + +async function main() { + if (!fs.existsSync(serverPath)) { + throw new Error(`MCP entry missing: ${serverPath}`); + } + + const session = await client.apiJson('/agent/start', { + working_dir: memindRoot, + extension_overrides: [ + { + type: 'stdio', + name: 'sandbox-fs', + description: 'MindSpace sandbox page smoke', + cmd: nodeExec, + args: [serverPath, sandboxRoot], + envs: { + ALLOWED_TOOLS: 'write_file,edit_file,read_file,create_dir,list_dir', + SANDBOX_ROOT: sandboxRoot, + }, + available_tools: ['write_file', 'read_file', 'edit_file', 'create_dir', 'list_dir'], + }, + ], + }); + if (!session?.id) throw new Error('missing session id'); + + await waitForWriteTool(session.id); + + const initialHtml = ` + +Goose v1.49 Sandbox +

苏州一日游

初始段落

+`; + + await callTool(session.id, 'sandbox-fs__write_file', { + path: pageRel, + content: initialHtml, + }); + + const absPath = path.join(sandboxRoot, pageRel); + if (!fs.existsSync(absPath)) { + throw new Error(`write_file did not create ${pageRel}`); + } + + await callTool(session.id, 'sandbox-fs__edit_file', { + path: pageRel, + old_str: '初始段落', + new_str: '已编辑段落 · v1.49', + }); + + const updated = fs.readFileSync(absPath, 'utf8'); + if (!updated.includes('已编辑段落 · v1.49')) { + throw new Error('edit_file did not apply patch'); + } + if (!updated.includes('苏州一日游')) { + throw new Error('edit_file removed expected page content'); + } + + console.log( + `GOOSE_V149_SANDBOX_PAGE_OK: session=${session.id} file=${pageRel} sandbox=${sandboxRoot}`, + ); +} + +main().catch((error) => { + console.error(`GOOSE_V149_SANDBOX_PAGE_FAIL: ${error.message}`); + process.exit(1); +});