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:
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user