test(goose): add sandbox page and Portal resume integration smokes

Verify write_file/edit_file land public HTML via call_tool, and Portal can
start/resume v1.49 sessions without 404 on the resume path.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-08 21:37:27 +08:00
parent 9393650447
commit 1274943f33
3 changed files with 237 additions and 0 deletions
+2
View File
@@ -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',
+125
View File
@@ -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);
});
+110
View File
@@ -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 = `<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Goose v1.49 Sandbox</title></head>
<body><h1>苏州一日游</h1><p>初始段落</p></body>
</html>`;
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);
});