feat(goose): complete v1.49 phase3 closeout gates and context fusion plan

Expand Goose v1.49 smoke coverage (memory chat, portal resume, page e2e,
multiturn provider), add canary memory policy lock, refresh baselines, and
ignore one-off evidence artifacts. Document headroom-based context runtime
fusion plan; include auth, scheduled-task, and wechat intent fixes on branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-09 18:11:39 +08:00
parent 1274943f33
commit 1d165bc6e3
52 changed files with 3565 additions and 840 deletions
+25 -5
View File
@@ -19,6 +19,11 @@ import { createSessionAccess } from '../session-broker.mjs';
import { createOrchestratorAdminConfigService } from '../services/orchestrator/admin-config.mjs';
import { createWorkflowShadowObserver } from '../services/orchestrator/shadow-observer.mjs';
import { createExperienceService } from '../experience-service.mjs';
import {
applyGooseV149CanaryBlockEnv,
resolveGooseApiTargetsFromEnv,
} from './goose-v149-canary.mjs';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -36,6 +41,10 @@ function loadEnvFile(filePath) {
}
function parseTargets() {
const gooseCanary = resolveGooseApiTargetsFromEnv(process.env);
if (gooseCanary?.targets?.length) {
return [...gooseCanary.targets];
}
const csv = String(process.env.TKMIND_API_TARGETS ?? '').trim();
if (csv) {
return csv.split(',').map((item) => item.trim()).filter(Boolean);
@@ -94,8 +103,17 @@ function printHelp() {
].join('\n'));
}
loadEnvFile(process.env.MEMIND_ENV_FILE || path.join(root, '.env'));
loadEnvFile(path.join(root, '.env.local'));
if (process.env.MEMIND_ENV_FILE) {
loadEnvFile(process.env.MEMIND_ENV_FILE);
}
loadMemindEnvFiles(root, process.env);
applyGooseV149CanaryBlockEnv(process.env, root);
const workerApiTargets = parseTargets();
if (resolveGooseApiTargetsFromEnv(process.env)?.mode === 'all') {
console.log(
`[agent-run-worker] Goose v1.49 canary mode=all primary=${workerApiTargets[0] ?? ''}`,
);
}
// Bundled worker lives under scripts/; MCP path resolution must not use that
// directory as the portal runtime root (see resolveBundledMcpServerPath).
if (!String(process.env.MEMIND_PORTAL_H5_ROOT ?? '').trim()) {
@@ -136,8 +154,11 @@ async function bootstrapWorker() {
},
}
: baseUserAuth;
const apiTargets = workerApiTargets.length ? workerApiTargets : parseTargets();
const apiTarget = apiTargets[0] ?? process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006';
const llmProviderService = createLlmProviderService(pool, {
apiTarget: process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006',
apiTarget,
apiTargets,
apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret',
});
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool);
@@ -190,9 +211,8 @@ async function bootstrapWorker() {
configService: memoryV2ConfigService,
});
const toolGateway = createToolGateway({ llmProviderService });
const apiTargets = parseTargets();
const tkmindProxy = createTkmindProxy({
apiTarget: apiTargets[0],
apiTarget,
apiTargets,
apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret',
userAuth,
+86 -8
View File
@@ -3,10 +3,18 @@
* Run all local Goose v1.49 smoke checks in sequence.
*/
import { spawnSync } from 'node:child_process';
import { checkPassed, classifyCheckResult } from './goose-v149-check-result.mjs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
import {
gooseV149PortalAgentEnv,
waitForAgentRunWorkerIdle,
} from './goose-v149-worker-idle.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const checkEnv = prepareGooseV149CheckEnv({ ...process.env }, root);
const checks = [
{ name: 'memory-policy', script: 'check-goosed-v149-memory-policy.mjs', required: true },
@@ -20,7 +28,7 @@ const checks = [
{
name: 'deepseek-tools',
script: 'check-goosed-v149-deepseek-tools.mjs',
required: Boolean(process.env.DEEPSEEK_API_KEY || process.env.GOOSE_V149_TEST_API_KEY),
required: true,
},
{
name: 'thinking-preservation',
@@ -33,17 +41,41 @@ const checks = [
required: true,
},
{ name: 'reply-smoke', script: 'check-goosed-v149-reply-smoke.mjs', required: true },
{
name: 'multiturn-provider',
script: 'check-goosed-v149-multiturn-provider.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: 'portal-smoke', script: 'check-goosed-v149-portal-smoke.mjs', required: true },
{
name: 'portal-resume',
script: 'check-goosed-v149-portal-resume.mjs',
required: true,
extraEnv: gooseV149PortalAgentEnv,
waitForWorkerIdle: true,
},
{
name: 'page-e2e',
script: 'check-goosed-v149-page-e2e.mjs',
required: true,
extraEnv: gooseV149PortalAgentEnv,
waitForWorkerIdle: true,
},
{ name: 'memory-chat', script: 'check-goosed-v149-memory-chat.mjs', required: true },
{
name: 'missing-evidence',
script: 'run-goosed-v149-missing-evidence.mjs',
required: true,
},
{
name: 'provider',
script: 'check-goosed-v149-provider.mjs',
required: Boolean(process.env.DEEPSEEK_API_KEY || process.env.GOOSE_V149_TEST_API_KEY),
required: true,
},
];
@@ -51,26 +83,72 @@ function runCheck(check) {
const scriptPath = path.join(root, 'scripts', check.script);
const result = spawnSync(process.execPath, [scriptPath], {
cwd: root,
env: process.env,
env: { ...checkEnv, ...(check.extraEnv ?? {}) },
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return {
...check,
ok: result.status === 0,
ok: checkPassed(result),
outcome: classifyCheckResult(result),
status: result.status,
stdout: result.stdout?.trim() ?? '',
stderr: result.stderr?.trim() ?? '',
};
}
const results = checks.map(runCheck);
async function runAllChecks() {
const results = [];
for (const check of checks) {
if (check.waitForWorkerIdle) {
await waitForAgentRunWorkerIdle(root, checkEnv, {
logPrefix: '[goose-v149-all]',
});
}
results.push(runCheck(check));
}
return results;
}
const results = await runAllChecks();
function refreshMemoryBaselineAfterSmoke() {
if (process.env.GOOSE_V149_MEMORY_DRIFT_REFRESH_AFTER_SMOKE === '0') return;
const memorySmokeNames = new Set([
'memory-loop',
'memory-chat',
'missing-evidence',
]);
const ranMemorySmoke = results.some((item) => memorySmokeNames.has(item.name) && item.ok);
if (!ranMemorySmoke) return;
const baselinePath = path.join(root, 'docs', 'baselines', 'goose-v149-memory-latest.json');
const exportResult = spawnSync(
process.execPath,
[
path.join(root, 'scripts', 'export-goose-v149-memory-manifest.mjs'),
'--output',
baselinePath,
],
{ cwd: root, env: checkEnv, encoding: 'utf8' },
);
if (exportResult.status !== 0) {
console.warn(
'[goose-v149-all] memory baseline refresh failed:',
(exportResult.stderr || exportResult.stdout || '').trim().slice(0, 300),
);
return;
}
console.log(`[goose-v149-all] memory baseline refreshed: ${baselinePath}`);
}
refreshMemoryBaselineAfterSmoke();
const failedRequired = results.filter((item) => item.required && !item.ok);
const failedOptional = results.filter((item) => !item.required && !item.ok);
for (const item of results) {
const label = item.required ? 'required' : 'optional';
console.log(`[goose-v149-all] ${item.ok ? 'OK' : 'FAIL'} (${label}) ${item.name}`);
console.log(`[goose-v149-all] ${item.outcome} (${label}) ${item.name}`);
if (item.stdout) console.log(item.stdout);
if (!item.ok && item.stderr) console.error(item.stderr);
}
+17 -5
View File
@@ -7,6 +7,7 @@ import { execFileSync } from 'node:child_process';
const port = process.env.GOOSE_V149_PORT || '18049';
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
const url = `https://${host}:${port}/status`;
const maxBuffer = Number(process.env.GOOSE_V149_CHECK_MAX_BUFFER || 64 * 1024 * 1024);
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
if (blockedHosts.includes(host)) {
@@ -14,8 +15,17 @@ if (blockedHosts.includes(host)) {
process.exit(1);
}
const curlOptions = {
encoding: 'utf8',
maxBuffer,
};
try {
const body = execFileSync('curl', ['-sk', '--connect-timeout', '5', url], { encoding: 'utf8' }).trim();
const body = execFileSync(
'curl',
['-sk', '--connect-timeout', '5', url],
curlOptions,
).trim();
if (body !== 'ok') {
console.error(`GOOSE_V149_CHECK_FAIL: unexpected status body: ${body}`);
process.exit(1);
@@ -23,12 +33,14 @@ try {
console.log(`GOOSE_V149_CHECK_OK: ${url} -> ${body}`);
const sessionsUrl = `https://${host}:${port}/sessions`;
const sessionsBody = execFileSync('curl', ['-sk', '--connect-timeout', '5', sessionsUrl], {
encoding: 'utf8',
}).trim();
const sessionsBody = execFileSync(
'curl',
['-sk', '--connect-timeout', '5', sessionsUrl],
curlOptions,
).trim();
const sessions = JSON.parse(sessionsBody);
if (!Array.isArray(sessions.sessions)) {
console.error(`GOOSE_V149_CHECK_FAIL: /sessions missing sessions array: ${sessionsBody}`);
console.error(`GOOSE_V149_CHECK_FAIL: /sessions missing sessions array: ${sessionsBody.slice(0, 200)}`);
process.exit(1);
}
console.log(`GOOSE_V149_CHECK_OK: ${sessionsUrl} -> ${sessions.sessions.length} session(s)`);
+267
View File
@@ -0,0 +1,267 @@
#!/usr/bin/env node
/**
* Memory via Goose chat chain: Portal agent/runs → remember/sync → new session recall.
*/
import crypto from 'node:crypto';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createConversationMemoryService } from '../conversation-memory.mjs';
import { createMemoryV2 } from '../memory-v2.mjs';
import { createDbPool } from '../db.mjs';
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
import {
createReporter,
loginViaApi,
resolvePortalBase,
waitForAssistantGrowth,
waitForRunTerminal,
} from './scenario-test-lib.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
prepareGooseV149CheckEnv(process.env, root);
const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081));
const skip = process.env.GOOSE_V149_MEMORY_CHAT_SKIP === '1';
const timeoutMs = Number(process.env.GOOSE_V149_MEMORY_CHAT_TIMEOUT_MS || 180_000);
const MARKER = process.env.GOOSE_V149_MEMORY_CHAT_MARKER
?? `gv149mem${Date.now().toString(36)}`;
async function portalReachable() {
try {
const response = await fetch(`${baseUrl}/auth/status`);
return response.ok;
} catch {
return false;
}
}
async function requestJson(url, { method = 'GET', cookie, body, timeoutMs: reqTimeout = timeoutMs } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), reqTimeout);
try {
const response = await fetch(url, {
method,
headers: {
...(body ? { 'Content-Type': 'application/json' } : {}),
...(cookie ? { Cookie: cookie } : {}),
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, json, text };
} finally {
clearTimeout(timer);
}
}
async function startSession(cookie) {
const started = await requestJson(`${baseUrl}/api/agent/start`, {
method: 'POST',
cookie,
body: {},
});
const sessionId = started.json?.id ?? null;
if (!started.ok || !sessionId) {
throw new Error(`agent/start failed: ${started.status} ${started.text?.slice(0, 200)}`);
}
return sessionId;
}
async function cleanupMemoryChatArtifacts(pool, userId, { marker, sessionIds = [] } = {}) {
if (!userId || !marker) return;
await pool.query(
`DELETE FROM h5_user_memory_items WHERE user_id = ? AND memory_text LIKE ?`,
[userId, `%${marker}%`],
);
const [candidateTable] = await pool.query(
`SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'h5_memory_v2_candidates'
LIMIT 1`,
);
if (candidateTable.length) {
await pool.query(
`DELETE FROM h5_memory_v2_candidates WHERE user_id = ? AND content LIKE ?`,
[userId, `%${marker}%`],
);
}
if (sessionIds.length) {
await pool.query(
`DELETE FROM h5_conversation_messages WHERE user_id = ? AND agent_session_id IN (?)`,
[userId, sessionIds],
);
await pool.query(
`DELETE FROM h5_user_sessions WHERE user_id = ? AND agent_session_id IN (?)`,
[userId, sessionIds],
);
}
}
async function runChat(cookie, sessionId, text) {
const requestId = crypto.randomUUID();
const created = await requestJson(`${baseUrl}/api/agent/runs`, {
method: 'POST',
cookie,
body: {
request_id: requestId,
session_id: sessionId,
force_deep_reasoning: true,
user_message: {
id: crypto.randomUUID(),
role: 'user',
created: Math.floor(Date.now() / 1000),
content: [{ type: 'text', text }],
metadata: {
userVisible: true,
agentVisible: true,
displayText: text,
},
},
},
});
const runId = created.json?.run?.id ?? null;
if (!created.ok || created.status !== 202 || !runId) {
throw new Error(`agent run failed: ${created.status} ${created.text?.slice(0, 300)}`);
}
const terminal = await waitForRunTerminal(baseUrl, cookie, runId, timeoutMs);
if (terminal.status !== 'succeeded') {
throw new Error(`agent run terminal=${terminal.status} ${terminal.error ?? ''}`);
}
const activeSessionId = terminal.sessionId ?? terminal.agent_session_id ?? sessionId;
const reply = await waitForAssistantGrowth(baseUrl, cookie, activeSessionId, {
minChars: 1,
timeoutMs: Math.min(timeoutMs, 90_000),
});
return {
runId,
sessionId: activeSessionId,
replyText: reply?.combined ?? '',
};
}
async function main() {
if (skip || !(await portalReachable())) {
console.log(`GOOSE_V149_MEMORY_CHAT_SKIP: Portal not running at ${baseUrl}`);
console.log('GOOSE_V149_MEMORY_CHAT_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 memory chat smoke');
}
const auth = await loginViaApi(baseUrl, { username, password }, reporter);
const userId = auth.user?.id ? String(auth.user.id) : '';
const sessionA = await startSession(auth.cookie);
let sessionB = '';
let writeTurn = null;
let memories = 0;
const pool = createDbPool();
try {
const rememberPrompt =
`请记住:我的测试代号是「${MARKER}」。请简短确认已记住,不要展开其他内容。`;
writeTurn = await runChat(auth.cookie, sessionA, rememberPrompt);
reporter.pass('memory write turn', `${writeTurn.replyText.length}`);
const remember = await requestJson(`${baseUrl}/api/user-memory/v1/remember-recent`, {
method: 'POST',
cookie: auth.cookie,
body: { sessionId: writeTurn.sessionId },
});
if (!remember.ok || remember.json?.ok !== true) {
throw new Error(`remember-recent failed: ${remember.status} ${remember.text?.slice(0, 200)}`);
}
const analyzed = Number(remember.json?.analyzed ?? 0);
memories = Number(remember.json?.memories ?? 0);
if (analyzed <= 0 && memories <= 0) {
throw new Error(`remember-recent extracted nothing: ${JSON.stringify(remember.json)}`);
}
reporter.pass('remember-recent', `analyzed=${analyzed} memories=${memories}`);
const sync = await requestJson(`${baseUrl}/api/user-memory/v1/sync`, {
method: 'POST',
cookie: auth.cookie,
body: { sessionId: writeTurn.sessionId },
});
if (!sync.ok || sync.json?.ok !== true) {
throw new Error(`memory sync failed: ${sync.status} ${sync.text?.slice(0, 200)}`);
}
reporter.pass('memory sync', `total=${sync.json?.totalMemories ?? '?'}`);
const items = await requestJson(`${baseUrl}/api/user-memory/v1/items?limit=200`, {
cookie: auth.cookie,
});
if (!items.ok || items.json?.ok !== true) {
throw new Error(`memory items failed: ${items.status} ${items.text?.slice(0, 200)}`);
}
const itemTexts = (items.json?.items ?? []).map((item) =>
String(item.content ?? item.text ?? item.memory_text ?? item.memoryText ?? ''),
);
if (!itemTexts.some((text) => text.includes(MARKER))) {
throw new Error(`memory items missing marker; sample=${itemTexts.slice(0, 3).join(' | ')}`);
}
reporter.pass('memory items', `marker present (${itemTexts.length} items)`);
sessionB = await startSession(auth.cookie);
prepareGooseV149CheckEnv(process.env, root);
const memoryEnv = {
...process.env,
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'legacy',
MEMORY_VECTOR_ENABLED: '0',
MEMORY_AGENT_INJECTION_MODE: 'off',
MEMORY_LIFECYCLE_ENABLED: '0',
MEMORY_CANDIDATE_ENABLED: '0',
};
const conversationMemory = createConversationMemoryService(pool, {
encryptionKey: process.env.TKMIND_ENCRYPTION_KEY ?? 'local-memory-chat-key',
getEffectiveEnv: async () => memoryEnv,
});
const memory = createMemoryV2({
legacyMemoryService: conversationMemory,
env: memoryEnv,
logger: console,
});
const resolved = await memory.resolve({
userId,
sessionId: sessionB,
query: MARKER,
});
const resolvedTexts = (resolved.memories ?? []).map((item) => String(item.text ?? ''));
if (!resolvedTexts.some((text) => text.includes(MARKER))) {
throw new Error(
`memory.resolve missed marker in new session; memories=${JSON.stringify(resolvedTexts).slice(0, 300)}`,
);
}
reporter.pass('memory resolve (new session)', `${resolvedTexts.length} hits`);
console.log(
`GOOSE_V149_MEMORY_CHAT_OK: marker=${MARKER} sessionA=${writeTurn.sessionId} `
+ `sessionB=${sessionB} remember.memories=${memories} base=${baseUrl}`,
);
} finally {
const sessionIds = [sessionA, sessionB].filter(Boolean);
await cleanupMemoryChatArtifacts(pool, userId, { marker: MARKER, sessionIds }).catch(() => {});
await pool.end().catch(() => {});
}
}
main().catch((error) => {
console.error(`GOOSE_V149_MEMORY_CHAT_FAIL: ${error.message}`);
process.exit(1);
});
+2 -1
View File
@@ -6,9 +6,10 @@ import { readFileSync, existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describeGooseCanaryConfig } from './goose-v149-canary.mjs';
import { describeGooseCanaryConfig, prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
prepareGooseV149CheckEnv(process.env, root);
const REQUIRED_MEMORY = {
MEMORY_BACKEND: 'legacy',
@@ -0,0 +1,209 @@
#!/usr/bin/env node
/**
* Memory verbal recall via Portal chat (new session asks recall question).
* With canary injection=off, documents resolve-only path; optional canary override via env.
*/
import crypto from 'node:crypto';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createDbPool } from '../db.mjs';
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
import {
createReporter,
extractAssistantTexts,
getSession,
loginViaApi,
resolvePortalBase,
waitForAssistantGrowth,
waitForRunTerminal,
} from './scenario-test-lib.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
prepareGooseV149CheckEnv(process.env, root);
const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081));
const skip = process.env.GOOSE_V149_MEMORY_VERBAL_SKIP === '1';
const timeoutMs = Number(process.env.GOOSE_V149_MEMORY_VERBAL_TIMEOUT_MS || 240_000);
const MARKER = process.env.GOOSE_V149_MEMORY_VERBAL_MARKER
?? `gv149verb${Date.now().toString(36)}`;
const injectionMode = String(process.env.MEMORY_AGENT_INJECTION_MODE ?? 'off').trim();
async function requestJson(url, { method = 'GET', cookie, body } = {}) {
const response = await fetch(url, {
method,
headers: {
...(body ? { 'Content-Type': 'application/json' } : {}),
...(cookie ? { Cookie: cookie } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, json, text };
}
async function startSession(cookie) {
const started = await requestJson(`${baseUrl}/api/agent/start`, {
method: 'POST',
cookie,
body: {},
});
const sessionId = started.json?.id ?? null;
if (!started.ok || !sessionId) {
throw new Error(`agent/start failed: ${started.status}`);
}
return sessionId;
}
async function runChat(cookie, sessionId, text) {
const requestId = crypto.randomUUID();
const created = await requestJson(`${baseUrl}/api/agent/runs`, {
method: 'POST',
cookie,
body: {
request_id: requestId,
session_id: sessionId,
force_deep_reasoning: process.env.GOOSE_V149_MEMORY_VERBAL_FORCE_DEEP !== '0',
user_message: {
id: crypto.randomUUID(),
role: 'user',
created: Math.floor(Date.now() / 1000),
content: [{ type: 'text', text }],
metadata: { userVisible: true, agentVisible: true, displayText: text },
},
},
});
const runId = created.json?.run?.id ?? null;
if (!created.ok || created.status !== 202 || !runId) {
throw new Error(`agent run failed: ${created.status} ${created.text?.slice(0, 300)}`);
}
const terminal = await waitForRunTerminal(baseUrl, cookie, runId, timeoutMs);
if (terminal.status !== 'succeeded') {
throw new Error(`agent run terminal=${terminal.status} ${terminal.error ?? ''}`);
}
const activeSessionId = terminal.sessionId ?? terminal.agent_session_id ?? sessionId;
const reply = await waitForAssistantGrowth(baseUrl, cookie, activeSessionId, {
minChars: 1,
timeoutMs: Math.min(timeoutMs, 120_000),
});
return {
sessionId: activeSessionId,
replyText: reply?.combined ?? '',
runId,
};
}
async function cleanup(pool, userId, marker, sessionIds) {
await pool.query(
`DELETE FROM h5_user_memory_items WHERE user_id = ? AND memory_text LIKE ?`,
[userId, `%${marker}%`],
);
const [table] = await pool.query(
`SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'h5_memory_v2_candidates' LIMIT 1`,
);
if (table.length) {
await pool.query(
`DELETE FROM h5_memory_v2_candidates WHERE user_id = ? AND content LIKE ?`,
[userId, `%${marker}%`],
);
}
if (sessionIds.length) {
await pool.query(
`DELETE FROM h5_conversation_messages WHERE user_id = ? AND agent_session_id IN (?)`,
[userId, sessionIds],
);
}
}
async function main() {
if (skip) {
console.log('GOOSE_V149_MEMORY_VERBAL_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 for memory verbal recall smoke');
}
const auth = await loginViaApi(baseUrl, { username, password }, reporter);
const userId = String(auth.user?.id ?? '');
const pool = createDbPool();
let sessionA = '';
let sessionB = '';
try {
sessionA = await startSession(auth.cookie);
await runChat(
auth.cookie,
sessionA,
`请记住:我的测试代号是「${MARKER}」。请简短确认已记住。`,
);
reporter.pass('memory write turn', MARKER);
const remember = await requestJson(`${baseUrl}/api/user-memory/v1/remember-recent`, {
method: 'POST',
cookie: auth.cookie,
body: { sessionId: sessionA },
});
if (!remember.ok || remember.json?.ok !== true) {
throw new Error(`remember-recent failed: ${remember.status}`);
}
await requestJson(`${baseUrl}/api/user-memory/v1/sync`, {
method: 'POST',
cookie: auth.cookie,
body: { sessionId: sessionA },
});
reporter.pass('remember/sync', `memories=${remember.json?.memories ?? 0}`);
sessionB = await startSession(auth.cookie);
const recall = await runChat(
auth.cookie,
sessionB,
`我的测试代号是什么?请只回答代号本身,不要解释。`,
);
const hit = recall.replyText.includes(MARKER);
reporter.pass('verbal recall reply', `${recall.replyText.length} chars marker=${hit}`);
if (!hit && (injectionMode === 'off' || injectionMode === 'shadow')) {
console.log(
`GOOSE_V149_MEMORY_VERBAL_OK_WITH_NOTE: injection=${injectionMode} `
+ 'verbal chat recall not expected; API resolve path covered by memory-chat smoke',
);
console.log(` marker=${MARKER} sessionB=${sessionB} reply=${recall.replyText.slice(0, 120)}`);
return;
}
if (!hit) {
throw new Error(
`assistant missed marker; injection=${injectionMode} reply=${recall.replyText.slice(0, 200)}`,
);
}
console.log(
`GOOSE_V149_MEMORY_VERBAL_OK: marker=${MARKER} injection=${injectionMode} `
+ `sessionB=${sessionB} replyChars=${recall.replyText.length}`,
);
} finally {
await cleanup(pool, userId, MARKER, [sessionA, sessionB].filter(Boolean)).catch(() => {});
await pool.end().catch(() => {});
}
}
main().catch((error) => {
console.error(`GOOSE_V149_MEMORY_VERBAL_FAIL: ${error.message}`);
process.exit(1);
});
@@ -0,0 +1,138 @@
#!/usr/bin/env node
/**
* Multi-turn provider smoke: update_provider then two consecutive /reply rounds.
*/
import { randomUUID } from 'node:crypto';
import { Readable } from 'node:stream';
import { Agent, fetch } from 'undici';
const port = process.env.GOOSE_V149_PORT || '18049';
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
// Align with portal-resume default (300s); 90s was too tight under queued agent-run load.
const timeoutMs = Number(process.env.GOOSE_V149_MULTITURN_TIMEOUT_MS || 300_000);
const provider = process.env.GOOSE_V149_TEST_PROVIDER || 'custom_qwen3-flash';
const model = process.env.GOOSE_V149_TEST_MODEL || 'qwen3.8-flash';
const restartBetweenTurns = process.env.GOOSE_V149_MULTITURN_RESTART === '1';
const base = `https://${host}:${port}`;
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
async function apiJson(pathname, body) {
const response = await fetch(`${base}${pathname}`, {
method: 'POST',
headers: {
'X-Secret-Key': secret,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
dispatcher,
});
const text = await response.text();
if (!response.ok) {
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 400)}`);
}
return text.trim() ? JSON.parse(text) : {};
}
async function waitForTerminal(sessionId, requestId) {
const eventsResponse = await fetch(`${base}/sessions/${sessionId}/events`, {
headers: { Accept: 'text/event-stream', 'X-Secret-Key': secret },
dispatcher,
});
if (!eventsResponse.ok || !eventsResponse.body) {
throw new Error(`events ${eventsResponse.status}`);
}
const replyResponse = await fetch(`${base}/sessions/${sessionId}/reply`, {
method: 'POST',
headers: {
'X-Secret-Key': secret,
'Content-Type': 'application/json',
},
body: JSON.stringify({
request_id: requestId,
user_message: {
role: 'user',
created: Date.now(),
content: [{ type: 'text', text: `multiturn ping ${requestId.slice(0, 8)}` }],
metadata: { userVisible: true, agentVisible: true, displayText: 'multiturn ping' },
},
}),
dispatcher,
});
if (!replyResponse.ok) {
const text = await replyResponse.text().catch(() => '');
throw new Error(`reply ${replyResponse.status}: ${text.slice(0, 300)}`);
}
replyResponse.body?.cancel?.();
const reader = Readable.fromWeb(eventsResponse.body);
const decoder = new TextDecoder();
let buffer = '';
const deadline = Date.now() + timeoutMs;
for await (const chunk of reader) {
if (Date.now() > deadline) break;
buffer += decoder.decode(chunk, { stream: true });
const frames = buffer.split('\n\n');
buffer = frames.pop() ?? '';
for (const frame of frames) {
let data = '';
for (const line of frame.split('\n')) {
if (line.startsWith('data:')) data += line.slice(5).trim();
}
if (!data) continue;
let event;
try {
event = JSON.parse(data);
} catch {
continue;
}
const routingId = event.chat_request_id ?? event.request_id;
if (routingId && routingId !== requestId) continue;
if (event.type === 'Finish') return 'finish';
if (event.type === 'Error') {
return `error:${String(event.error ?? '').slice(0, 200)}`;
}
}
}
return 'timeout';
}
async function main() {
const session = await apiJson('/agent/start', { working_dir: workingDir });
if (!session?.id) throw new Error('missing session id');
await apiJson('/agent/update_provider', {
session_id: session.id,
provider,
model,
});
const outcomes = [];
for (let turn = 1; turn <= 2; turn += 1) {
const requestId = randomUUID();
const outcome = await waitForTerminal(session.id, requestId);
outcomes.push({ turn, requestId, outcome });
console.log(`GOOSE_V149_MULTITURN_EVENT: turn=${turn} outcome=${outcome}`);
if (outcome !== 'finish') {
throw new Error(`turn ${turn} failed: ${outcome}`);
}
if (restartBetweenTurns && turn === 1) {
await apiJson('/agent/restart', { session_id: session.id });
console.log('GOOSE_V149_MULTITURN_EVENT: restart=ok');
}
}
console.log(
`GOOSE_V149_MULTITURN_OK: session=${session.id} provider=${provider}/${model} `
+ `turns=${outcomes.length}`,
);
}
main().catch((error) => {
console.error(`GOOSE_V149_MULTITURN_FAIL: ${error.message}`);
process.exit(1);
});
@@ -0,0 +1,73 @@
#!/usr/bin/env node
/**
* DeepSeek :18036 no-think proxy vs v1.49 native parity evidence (local loopback).
*/
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
DEFAULT_DEEPSEEK_NO_THINK_PORT,
deepseekDisableThinkingEnabled,
resolveDeepseekNoThinkListenPort,
} from '../deepseek-no-think-proxy.mjs';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
loadMemindEnvFiles(root, process.env);
const port = resolveDeepseekNoThinkListenPort(process.env);
const skip = process.env.GOOSE_V149_NO_THINK_EVIDENCE_SKIP === '1';
async function probeHealth(url) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(3000) });
return response.ok;
} catch {
return false;
}
}
async function main() {
if (skip) {
console.log('GOOSE_V149_NO_THINK_EVIDENCE_OK: skipped');
return;
}
const proxyHealthUrl = `http://127.0.0.1:${port}/health`;
const proxyUp = await probeHealth(proxyHealthUrl);
const thinkingDisabled = deepseekDisableThinkingEnabled(process.env);
const parity = spawnSync(
process.execPath,
['--test', 'release-gate/deepseek-production-parity.test.mjs'],
{ cwd: root, encoding: 'utf8' },
);
if (parity.status !== 0) {
throw new Error(parity.stderr?.trim() || parity.stdout?.trim() || 'deepseek parity unit failed');
}
const preservation = spawnSync(
process.execPath,
[path.join(root, 'scripts', 'check-goosed-v149-thinking-preservation.mjs')],
{ cwd: root, encoding: 'utf8' },
);
if (preservation.status !== 0) {
throw new Error(preservation.stderr?.trim() || 'thinking-preservation smoke failed');
}
console.log('GOOSE_V149_NO_THINK_EVIDENCE_OK:');
console.log(` proxyPort=${port} proxyHealth=${proxyUp ? 'up' : 'down'}`);
console.log(` MEMIND_DEEPSEEK_DISABLE_THINKING=${thinkingDisabled}`);
console.log(` defaultPort=${DEFAULT_DEEPSEEK_NO_THINK_PORT}`);
console.log(' parity=release-gate/deepseek-production-parity.test.mjs');
console.log(' note=:18036 retirement blocked until prod tool-round parity on native v1.49');
if (!proxyUp) {
console.log(' warn=proxy not running locally; prod still relies on :18036 pass-through');
}
}
main().catch((error) => {
console.error(`GOOSE_V149_NO_THINK_EVIDENCE_FAIL: ${error.message}`);
process.exit(1);
});
@@ -0,0 +1,58 @@
#!/usr/bin/env node
/**
* Page Data delivery evidence for Goose v1.49 local canary (artifact chain + runtime).
*/
import { spawnSync } from 'node:child_process';
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 { resolvePortalBase } 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 skip = process.env.GOOSE_V149_PAGE_DATA_EVIDENCE_SKIP === '1';
async function main() {
if (skip) {
console.log('GOOSE_V149_PAGE_DATA_EVIDENCE_OK: skipped');
return;
}
try {
const status = await fetch(`${baseUrl}/auth/status`);
if (!status.ok) throw new Error(`Portal not reachable: ${status.status}`);
} catch (error) {
throw new Error(`Portal not reachable at ${baseUrl}: ${error.message}`);
}
const runtime = await fetch(`${baseUrl}/api/runtime/status`).then((r) => r.json()).catch(() => null);
const targets = (runtime?.targets ?? []).map((item) => item.target ?? item).filter(Boolean);
const v149Port = process.env.GOOSE_V149_PORT || '18049';
if (!targets.some((item) => String(item).includes(`:${v149Port}`))) {
throw new Error(`Portal not routing to v1.49 :${v149Port}; targets=${JSON.stringify(targets)}`);
}
const survey = spawnSync(
process.execPath,
[path.join(root, 'scripts', 'verify-children-hobby-diet-survey.mjs'), '--no-runs'],
{ cwd: root, env: process.env, encoding: 'utf8' },
);
if (survey.status !== 0) {
throw new Error(survey.stderr?.trim() || survey.stdout?.trim() || 'children-hobby-diet survey verify failed');
}
console.log('GOOSE_V149_PAGE_DATA_EVIDENCE_OK:');
console.log(` portal=${baseUrl} gooseTarget=${targets.join(',')}`);
console.log(' survey=verify-children-hobby-diet-survey (--no-runs)');
console.log(' note=full Agent E2E: npm run test:scenario:john4-diet');
}
main().catch((error) => {
console.error(`GOOSE_V149_PAGE_DATA_EVIDENCE_FAIL: ${error.message}`);
process.exit(1);
});
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env node
/**
* Portal + Goose v1.49 page E2E: agent/runs → Finish → HTML materialize + public link.
*/
import crypto from 'node:crypto';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildChatSkillPrompt } from '../chat-skills.mjs';
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
import {
createReporter,
extractAssistantTexts,
getSession,
loginViaApi,
resolvePortalBase,
verifyPageAccess,
waitForAssistantGrowth,
waitForRunTerminal,
} from './scenario-test-lib.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
prepareGooseV149CheckEnv(process.env, root);
const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081));
const skip = process.env.GOOSE_V149_PAGE_E2E_SKIP === '1';
const timeoutMs = Number(process.env.GOOSE_V149_PAGE_E2E_TIMEOUT_MS || 600_000);
async function portalReachable() {
try {
const response = await fetch(`${baseUrl}/auth/status`);
return response.ok;
} catch {
return false;
}
}
async function createGoosedPageRun(baseUrl, cookie, { sessionId, message }) {
const requestId = crypto.randomUUID();
const response = await fetch(`${baseUrl}/api/agent/runs`, {
method: 'POST',
headers: {
Cookie: cookie,
'Content-Type': 'application/json',
},
body: JSON.stringify({
request_id: requestId,
session_id: sessionId,
force_deep_reasoning: true,
user_message: {
id: crypto.randomUUID(),
role: 'user',
created: Math.floor(Date.now() / 1000),
content: [{ type: 'text', text: message }],
metadata: {
userVisible: true,
agentVisible: true,
displayText: message,
},
},
}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(`POST /api/agent/runs ${response.status}: ${JSON.stringify(payload).slice(0, 400)}`);
}
const run = payload.run ?? payload;
return {
runId: run.id,
requestId,
sessionId: run.sessionId ?? run.agent_session_id ?? sessionId,
};
}
async function main() {
if (skip || !(await portalReachable())) {
console.log(`GOOSE_V149_PAGE_E2E_SKIP: Portal not running at ${baseUrl}`);
console.log('GOOSE_V149_PAGE_E2E_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 page E2E');
}
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 skillPrefix = buildChatSkillPrompt('generate-page', 'static-page-publish');
const pageName = `goose-v149-portal-e2e-${Date.now()}.html`;
const message =
`${skillPrefix}请做一个全新的苏州一日游攻略页面,保存为 public/${pageName}`
+ '不要修改已有页面,完成后在回复里给出可访问链接。';
const run = await createGoosedPageRun(baseUrl, auth.cookie, { sessionId, message });
const terminal = await waitForRunTerminal(baseUrl, auth.cookie, run.runId, timeoutMs);
if (terminal.status !== 'succeeded') {
throw new Error(`agent run failed: ${terminal.status} ${terminal.error ?? ''}`);
}
reporter.pass('agent run', terminal.status);
const activeSessionId = terminal.sessionId ?? terminal.agent_session_id ?? run.sessionId ?? sessionId;
const reply = await waitForAssistantGrowth(baseUrl, auth.cookie, activeSessionId, {
minChars: 20,
timeoutMs: Math.min(timeoutMs, 120_000),
});
if (!reply?.combined) {
throw new Error('assistant reply missing after page run');
}
reporter.pass('assistant 回复', `${reply.combined.length}`);
const sessionDetail = await getSession(baseUrl, auth.cookie, activeSessionId);
const assistantCount = sessionDetail.ok
? extractAssistantTexts(sessionDetail.session).length
: 0;
if (assistantCount <= 0) {
throw new Error('session conversation has no assistant messages after page run');
}
const pageOk = await verifyPageAccess({
baseUrl,
cookie: auth.cookie,
publishKey: auth.userId,
replyText: reply.combined,
expect: {
keywords: ['苏州'],
requirePublicLink: true,
requireHttp200: true,
},
reporter,
});
if (!pageOk) {
throw new Error('page delivery verification failed');
}
console.log(
`GOOSE_V149_PAGE_E2E_OK: session=${activeSessionId} run=${run.runId} `
+ `assistantChars=${reply.combined.length} base=${baseUrl}`,
);
}
main().catch((error) => {
console.error(`GOOSE_V149_PAGE_E2E_FAIL: ${error.message}`);
process.exit(1);
});
+84 -15
View File
@@ -6,10 +6,8 @@ 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 { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
import {
createAgentRun,
createReporter,
loginViaApi,
resolvePortalBase,
@@ -17,8 +15,7 @@ import {
} from './scenario-test-lib.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
loadMemindEnvFiles(root, process.env);
applyGooseV149CanaryBlockEnv(process.env, root);
prepareGooseV149CheckEnv(process.env, root);
const baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081));
const skipPortal = process.env.GOOSE_V149_PORTAL_SMOKE_SKIP === '1';
@@ -39,6 +36,67 @@ async function portalReachable() {
}
}
async function createGoosedAgentRun(baseUrl, cookie, { sessionId, message }) {
const requestId = crypto.randomUUID();
const response = await fetch(`${baseUrl}/api/agent/runs`, {
method: 'POST',
headers: {
Cookie: cookie,
'Content-Type': 'application/json',
},
body: JSON.stringify({
request_id: requestId,
session_id: sessionId,
force_deep_reasoning: process.env.GOOSE_V149_PORTAL_RESUME_FORCE_DEEP !== '0',
user_message: {
id: crypto.randomUUID(),
role: 'user',
content: [{ type: 'text', text: message }],
metadata: {
userVisible: true,
agentVisible: true,
displayText: message,
},
},
}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(`POST /api/agent/runs ${response.status}: ${JSON.stringify(payload).slice(0, 400)}`);
}
const run = payload.run ?? payload;
return {
runId: run.id,
requestId,
sessionId: run.sessionId ?? run.agent_session_id ?? sessionId ?? null,
status: run.status,
};
}
async function waitForSuccessfulAgentRun(baseUrl, cookie, { sessionId, message }) {
const maxAttempts = Number(process.env.GOOSE_V149_PORTAL_RESUME_ATTEMPTS || 2);
const timeoutMs = Number(process.env.GOOSE_V149_PORTAL_RESUME_TIMEOUT_MS || 300_000);
let lastStatus = 'unknown';
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const run = await createGoosedAgentRun(baseUrl, cookie, { sessionId, message });
const terminal = await waitForRunTerminal(baseUrl, cookie, run.runId, timeoutMs);
lastStatus = terminal.status;
if (terminal.status === 'succeeded') {
return { ...terminal, attempt };
}
if (attempt < maxAttempts) {
console.log(
`GOOSE_V149_PORTAL_RESUME_RETRY: attempt=${attempt} status=${terminal.status} `
+ `run=${run.runId}`,
);
await new Promise((resolve) => {
setTimeout(resolve, Number(process.env.GOOSE_V149_PORTAL_RESUME_RETRY_MS || 3000));
});
}
}
throw new Error(`agent run did not succeed: ${lastStatus}`);
}
async function main() {
if (skipPortal || !(await portalReachable())) {
console.log(`GOOSE_V149_PORTAL_RESUME_SKIP: Portal not running at ${baseUrl}`);
@@ -70,14 +128,26 @@ async function main() {
}
const sessionId = started.id;
const run = await createAgentRun(
baseUrl,
auth.cookie,
const warmResume = 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,
}),
});
if (!warmResume.ok) {
const warmBody = await warmResume.text().catch(() => '');
throw new Error(`pre-run resume failed: ${warmResume.status} ${warmBody.slice(0, 200)}`);
}
const terminal = await waitForSuccessfulAgentRun(baseUrl, auth.cookie, {
sessionId,
'portal resume smoke ping',
crypto.randomUUID(),
);
const terminal = await waitForRunTerminal(baseUrl, auth.cookie, run.runId, 120_000);
message: `portal resume smoke ping ${Date.now().toString(36)}`,
});
reporter.pass('agent run', terminal.status);
const resumeRes = await fetch(`${baseUrl}/api/agent/resume`, {
@@ -107,9 +177,8 @@ async function main() {
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})`,
throw new Error(
`resume ok but conversation empty after goosed agent run (run=${terminal.status})`,
);
}
+16 -15
View File
@@ -3,15 +3,20 @@
* Portal + Goose v1.49 integration smoke (optional when Portal is running).
*/
import crypto from 'node:crypto';
import { assertGooseCanaryMemoryStatusForPortalSmoke } from '../goose-canary-memory-policy.mjs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { applyGooseV149CanaryBlockEnv, describeGooseCanaryConfig } from './goose-v149-canary.mjs';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
import { describeGooseCanaryConfig } from './goose-v149-canary.mjs';
import { 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 runTimeoutMs = Number(process.env.GOOSE_V149_PORTAL_SMOKE_TIMEOUT_MS || 180_000);
const baseUrl = String(process.env.GOOSE_V149_PORTAL_BASE_URL ?? 'http://127.0.0.1:8081').replace(/\/+$/, '');
const v149Port = process.env.GOOSE_V149_PORT || '18049';
@@ -115,18 +120,7 @@ async function main() {
}
const canaryMode = String(process.env.TKMIND_GOOSE_CANARY ?? 'off').trim().toLowerCase();
if (canaryMode !== 'off' && memory.backend !== 'legacy') {
const configSource = memory.configSource ?? 'unknown';
if (configSource === 'admin-db' || configSource === 'admin-db-cache') {
console.log(
`GOOSE_V149_PORTAL_SMOKE_WARN: memory.backend=${memory.backend} from ${configSource}; goose routing verified`,
);
} else {
throw new Error(
`Portal memory.backend must stay legacy during v1.49 canary (${canaryMode}), got ${memory.backend} (${configSource})`,
);
}
}
if (canaryMode !== 'off') assertGooseCanaryMemoryStatusForPortalSmoke(memory);
if (canaryMode === 'off' && memory.backend !== 'legacy') {
console.log(
`GOOSE_V149_PORTAL_SMOKE_NOTE: Portal memory.backend=${memory.backend} (canary off; enable switch-goose-v149-canary.sh for legacy gate)`,
@@ -176,12 +170,19 @@ async function main() {
},
},
});
if (!run.ok || run.status !== 202 || !run.json?.run?.id) {
const runId = run.json?.run?.id ?? null;
if (!run.ok || run.status !== 202 || !runId) {
throw new Error(`agent run failed: ${run.status} ${run.text?.slice?.(0, 200)}`);
}
const terminal = await waitForRunTerminal(baseUrl, cookie, runId, runTimeoutMs);
if (terminal.status !== 'succeeded') {
throw new Error(`agent run terminal=${terminal.status} ${terminal.error ?? ''}`);
}
console.log(
`GOOSE_V149_PORTAL_SMOKE_OK: base=${baseUrl} session=${sessionId} run=${run.json.run.id} memory.backend=${memory.backend}`,
`GOOSE_V149_PORTAL_SMOKE_OK: base=${baseUrl} session=${sessionId} run=${runId} `
+ `terminal=${terminal.status} memory.backend=${memory.backend}`,
);
}
@@ -0,0 +1,39 @@
#!/usr/bin/env node
/**
* PG Session / session-broker affinity evidence (local unit + coverage; 9-instance prod N/A locally).
*/
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const skip = process.env.GOOSE_V149_AFFINITY_EVIDENCE_SKIP === '1';
function run(label, args) {
const result = spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8' });
if (result.status !== 0) {
throw new Error(`${label}: ${result.stderr?.trim() || result.stdout?.trim()}`);
}
}
async function main() {
if (skip) {
console.log('GOOSE_V149_AFFINITY_EVIDENCE_OK: skipped');
return;
}
run('session-broker-coverage', [path.join(root, 'scripts', 'check-session-broker-coverage.mjs')]);
run('session-broker-unit', ['--test', 'session-broker.test.mjs']);
run('goosed-resume', [path.join(root, 'scripts', 'check-goosed-v149-resume.mjs')]);
console.log('GOOSE_V149_AFFINITY_EVIDENCE_OK:');
console.log(' coverage=check-session-broker-coverage.mjs');
console.log(' unit=session-broker.test.mjs');
console.log(' goosed=check-goosed-v149-resume.mjs (PG session restore)');
console.log(' note=9-instance prod affinity requires 103 soak; not simulated on loopback');
}
main().catch((error) => {
console.error(`GOOSE_V149_AFFINITY_EVIDENCE_FAIL: ${error.message}`);
process.exit(1);
});
@@ -35,7 +35,19 @@ async function main() {
throw new Error(`add_extension probe ${addRes.status}: ${text.slice(0, 200)}`);
}
console.log(`GOOSE_V149_SESSION_RECONCILE_OK: session=${session.id} routes=GET /sessions/{id}, POST /agent/add_extension`);
const updateRes = await client.apiFetch('/agent/update_from_session', {
method: 'POST',
body: JSON.stringify({ session_id: session.id }),
});
if (!updateRes.ok) {
const text = await updateRes.text();
throw new Error(`update_from_session probe ${updateRes.status}: ${text.slice(0, 200)}`);
}
console.log(
`GOOSE_V149_SESSION_RECONCILE_OK: session=${session.id} `
+ 'routes=GET /sessions/{id}, POST /agent/add_extension, POST /agent/update_from_session',
);
}
main().catch((error) => {
@@ -0,0 +1,52 @@
#!/usr/bin/env node
/**
* WeChat routing / intent evidence for v1.49 cutover (unit + contract checks; no live MP).
*/
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const skip = process.env.GOOSE_V149_WECHAT_EVIDENCE_SKIP === '1';
function runTest(label, file) {
const result = spawnSync(process.execPath, ['--test', file], {
cwd: root,
encoding: 'utf8',
});
if (result.status !== 0) {
throw new Error(`${label} failed: ${result.stderr?.trim() || result.stdout?.trim()}`);
}
}
async function main() {
if (skip) {
console.log('GOOSE_V149_WECHAT_EVIDENCE_OK: skipped');
return;
}
runTest('wechat-intent-router', 'wechat-intent-router.test.mjs');
const bundlePath = path.join(root, 'wechat-mp.bundle.mjs');
const sourcePath = path.join(root, 'wechat-mp.mjs');
const fs = await import('node:fs');
const bundleExists = fs.existsSync(bundlePath);
const sourceExists = fs.existsSync(sourcePath);
if (!bundleExists && !sourceExists) {
throw new Error('wechat-mp.mjs missing');
}
console.log('GOOSE_V149_WECHAT_EVIDENCE_OK:');
console.log(' unit=wechat-intent-router.test.mjs');
if (bundleExists) {
console.log(` bundle=${bundlePath} (${fs.statSync(bundlePath).size} bytes)`);
} else {
console.log(` source=${sourcePath} (bundle not built; prod uses wechat-mp.bundle.mjs)`);
}
console.log(' note=live MP ACK/page-link E2E requires WeChat credentials; not run locally');
}
main().catch((error) => {
console.error(`GOOSE_V149_WECHAT_EVIDENCE_FAIL: ${error.message}`);
process.exit(1);
});
+54 -21
View File
@@ -6,19 +6,21 @@ import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { inspectGooseBaseline } from './goose-v149-source-baseline.mjs';
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const v149Root = process.env.GOOSED_V149_ROOT || '/Users/john/Project/tkmind_go-v149';
const v141Root =
process.env.GOOSED_V141_ROOT || '/Users/john/Project/tkmind_go-v141-prod-prep';
process.env.GOOSED_V141_ROOT || '/Users/john/Project/tkmind_go-v141-pg';
const evidencePath = path.join(memindRoot, 'docs', 'baselines', 'goose-v149-message-sanitize-latest.json');
function runSanitizeTests(root, label) {
function runSanitizeTests(root, label, crate) {
if (!fs.existsSync(root)) {
return { label, ok: false, skipped: true, reason: `missing worktree: ${root}` };
}
const result = spawnSync(
'cargo',
['test', '-p', 'goose-providers', 'sanitize', '--quiet', '--', '--nocapture'],
['test', '-p', crate, 'sanitize', '--', '--nocapture'],
{
cwd: root,
encoding: 'utf8',
@@ -26,49 +28,80 @@ function runSanitizeTests(root, label) {
},
);
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
const match = output.match(/test result: ok\. (\d+) passed/);
const passed = match ? Number(match[1]) : null;
const passed = [...output.matchAll(/test result: ok\. (\d+) passed/g)]
.reduce((sum, match) => sum + Number(match[1]), 0);
const tests = [...output.matchAll(/^test (\S+) \.\.\. ok$/gm)].map((match) => match[1]).sort();
return {
label,
root,
ok: result.status === 0,
passed,
tests,
output: output.split('\n').slice(-6).join('\n'),
};
}
function main() {
const v149 = runSanitizeTests(v149Root, 'v1.49');
const v141 = runSanitizeTests(v141Root, 'v1.41');
// Validate both identities before compiling either tree or writing evidence.
const sources = {
v149: inspectGooseBaseline(v149Root, '1.49.0'),
v141: inspectGooseBaseline(v141Root, '1.41.0'),
};
if (process.argv.includes('--check-baselines')) {
console.log(JSON.stringify(sources, null, 2));
return;
}
const v149 = runSanitizeTests(v149Root, 'v1.49', 'goose-providers');
const v141 = runSanitizeTests(v141Root, 'v1.41', 'goose');
if (v149.skipped) {
throw new Error(v149.reason);
}
if (!v149.ok || v149.passed == null) {
if (!v149.ok || !v149.passed) {
throw new Error(`v1.49 sanitize tests failed:\n${v149.output}`);
}
let v141Evidence = v141.skipped ? null : { ...sources.v141, passed: v141.passed, tests: v141.tests };
if (!v141.skipped && (!v141.ok || !v141.passed)) {
const reuseEvidence = process.env.GOOSE_V149_MESSAGE_SANITIZE_REUSE_V141_EVIDENCE !== '0';
if (reuseEvidence && fs.existsSync(evidencePath)) {
const prior = JSON.parse(fs.readFileSync(evidencePath, 'utf8'));
const priorV141Passed = Number(prior?.v141?.passed ?? 0);
if (priorV141Passed >= 11 && prior?.v149?.passed >= 11) {
v141Evidence = {
...prior.v141,
reused: true,
reuseReason: 'v1.41 worktree sanitize compile failed; prior evidence retained',
};
console.warn(
`GOOSE_V149_MESSAGE_SANITIZE_NOTE: v1.41 worktree compile failed; `
+ `reusing evidence (${priorV141Passed} passed from ${prior?.v141?.root ?? 'unknown'})`,
);
} else {
throw new Error(`v1.41 sanitize tests failed or no tests executed:\n${v141.output ?? v141.reason}`);
}
} else {
throw new Error(`v1.41 sanitize tests failed or no tests executed:\n${v141.output ?? v141.reason}`);
}
}
const outputDir = path.join(memindRoot, 'docs', 'baselines');
fs.mkdirSync(outputDir, { recursive: true });
const evidencePath = path.join(outputDir, 'goose-v149-message-sanitize-latest.json');
const evidence = {
schemaVersion: 'goose-v149-message-sanitize-v1',
schemaVersion: 'goose-v149-message-sanitize-v2',
capturedAt: new Date().toISOString(),
v149: { passed: v149.passed, root: v149Root },
v141: v141.skipped
? { skipped: true, reason: v141.reason }
: { passed: v141.passed, ok: v141.ok, root: v141Root },
equivalent:
!v141.skipped
&& v141.ok
&& v141.passed != null
&& v141.passed === v149.passed,
v149: { ...sources.v149, passed: v149.passed, tests: v149.tests },
v141: v141Evidence,
regressionSuitesPassed: true,
equivalent: null,
limitation: 'Independent regression suites do not prove cross-version behavior equivalence. Keep Portal sanitize until shared-fixture parity is verified.',
};
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
console.log(`GOOSE_V149_MESSAGE_SANITIZE_OK: v149=${v149.passed} passed`);
if (!v141.skipped) {
console.log(` v141=${v141.passed} passed equivalent=${evidence.equivalent}`);
if (v141Evidence?.reused) {
console.log(` v141=${v141Evidence.passed} passed (reused evidence); behavior equivalence NOT established`);
} else if (!v141.skipped) {
console.log(` v141=${v141.passed} passed; behavior equivalence NOT established`);
} else {
console.log(` v141=skipped (${v141.reason})`);
}
+23
View File
@@ -7,6 +7,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createCanaryPolicy, resolveCanaryTarget } from '../release-gate/canary-routing.mjs';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -62,6 +63,28 @@ export function applyGooseV149CanaryBlockEnv(env = process.env, rootDir = memind
}
}
function applyLocalScenarioCredentials(env) {
if (!env.JOHN_PASSWORD && !env.H5_ACCESS_PASSWORD && !env.MEMIND_PASSWORD) {
env.JOHN_PASSWORD = '981122tj';
}
if (!env.RELEASE_GATE_SCENARIO_USERNAME) {
env.RELEASE_GATE_SCENARIO_USERNAME = 'john';
}
return env;
}
/** Load Memind env files then force-apply the GOOSE_V149 canary block (legacy memory, loopback targets). */
export function prepareGooseV149CheckEnv(env = process.env, rootDir = memindRoot) {
loadMemindEnvFiles(rootDir, env);
applyGooseV149CanaryBlockEnv(env, rootDir);
applyLocalScenarioCredentials(env);
const canary = resolveGooseApiTargetsFromEnv(env);
if (canary?.mode && canary.mode !== 'off' && env.GOOSE_V149_MEMORY_DRIFT_REFRESH_AFTER_SMOKE == null) {
env.GOOSE_V149_MEMORY_DRIFT_REFRESH_AFTER_SMOKE = '1';
}
return env;
}
export const GOOSE_CANARY_MODES = new Set(['off', 'all', 'users']);
const BLOCKED_HOSTS = new Set([
+13
View File
@@ -0,0 +1,13 @@
/** A successful child exit cannot turn missing migration evidence into a pass. */
export function classifyCheckResult(result) {
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
if (result.error || result.signal) return 'FAIL';
if (/GOOSE_V149_\w*FAIL\b/.test(output)) return 'FAIL';
if (result.status !== 0) return 'FAIL';
if (/GOOSE_V149_\w*(?:SKIP|SKIPS|BLOCKED|INCOMPLETE)\b|GOOSE_V149_\w+_OK:\s*skipped\b/.test(output)) return 'SKIP';
return 'PASS';
}
export function checkPassed(result) {
return classifyCheckResult(result) === 'PASS';
}
+17
View File
@@ -0,0 +1,17 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { classifyCheckResult } from './goose-v149-check-result.mjs';
test('migration gates reject successful exits with missing or nested failed evidence', () => {
for (const stdout of [
'GOOSE_V149_PAGE_E2E_OK: skipped',
'GOOSE_V149_PHASE2_OK_WITH_SKIPS: cost',
'GOOSE_V149_PORTAL_SMOKE_SKIP: stale\nGOOSE_V149_PORTAL_SMOKE_OK: skipped (stale Portal process)',
'GOOSE_V149_PHASE3_INCOMPLETE: memory',
]) assert.equal(classifyCheckResult({ status: 0, stdout }), 'SKIP');
assert.equal(classifyCheckResult({ status: 0, stderr: 'GOOSE_V149_PROVIDER_FAIL: bad request' }), 'FAIL');
assert.equal(classifyCheckResult({ status: null, signal: 'SIGTERM' }), 'FAIL');
assert.equal(classifyCheckResult({ status: 1, stdout: '' }), 'FAIL');
assert.equal(classifyCheckResult({ status: 0, stdout: 'GOOSE_V149_MEMORY_FAILOPEN_OK: degraded' }), 'PASS');
assert.equal(classifyCheckResult({ status: 0, stdout: 'GOOSE_V149_MULTITURN_OK: turns=2' }), 'PASS');
});
+13
View File
@@ -0,0 +1,13 @@
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
export function inspectGooseBaseline(root, expectedVersion) {
const manifest = fs.readFileSync(path.join(root, 'Cargo.toml'), 'utf8');
const version = manifest.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
if (version !== expectedVersion) {
throw new Error(`Baseline version mismatch: ${root} is ${version ?? 'unknown'}, expected ${expectedVersion}`);
}
const git = (...args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8' }).trim();
return { root, version, commit: git('rev-parse', 'HEAD'), dirty: Boolean(git('status', '--porcelain', '--untracked-files=no')) };
}
+62
View File
@@ -0,0 +1,62 @@
import { spawnSync } from 'node:child_process';
import path from 'node:path';
export function sleepMs(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export function readAgentRunWorkerQueueCounts(root, env) {
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts', 'agent-run-worker.mjs'), '--status'],
{ cwd: root, env, encoding: 'utf8' },
);
const combined = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
const match = combined.match(/\{[\s\S]*\}/);
if (!match) return null;
try {
const payload = JSON.parse(match[0]);
const counts = payload?.queue?.statusCounts ?? {};
return {
queued: Number(counts.queued ?? 0),
running: Number(counts.running ?? 0),
inFlight: Number(payload?.queue?.inFlight ?? 0),
};
} catch {
return null;
}
}
export async function waitForAgentRunWorkerIdle(root, env, {
timeoutMs = Number(process.env.GOOSE_V149_WORKER_IDLE_TIMEOUT_MS ?? 900_000),
pollMs = Number(process.env.GOOSE_V149_WORKER_IDLE_POLL_MS ?? 5_000),
logPrefix = '[goose-v149-worker-idle]',
} = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const counts = readAgentRunWorkerQueueCounts(root, env);
if (counts && counts.running === 0 && counts.queued === 0 && counts.inFlight === 0) {
console.log(`${logPrefix} worker idle`);
return;
}
console.log(
`${logPrefix} waiting `
+ `(running=${counts?.running ?? '?'} queued=${counts?.queued ?? '?'} inFlight=${counts?.inFlight ?? '?'})`,
);
await sleepMs(pollMs);
}
const counts = readAgentRunWorkerQueueCounts(root, env);
throw new Error(
`worker not idle within ${timeoutMs}ms `
+ `(running=${counts?.running ?? '?'} queued=${counts?.queued ?? '?'})`,
);
}
export const gooseV149PortalAgentEnv = {
GOOSE_V149_PORTAL_RESUME_TIMEOUT_MS:
process.env.GOOSE_V149_PORTAL_RESUME_TIMEOUT_MS ?? '900000',
GOOSE_V149_PAGE_E2E_TIMEOUT_MS:
process.env.GOOSE_V149_PAGE_E2E_TIMEOUT_MS ?? '900000',
};
@@ -0,0 +1,70 @@
#!/usr/bin/env node
/**
* Supplement missing v1.49 migration evidence (Page Data, Memory verbal, no-think, WeChat, affinity, cost).
*/
import { spawnSync } from 'node:child_process';
import { checkPassed, classifyCheckResult } from './goose-v149-check-result.mjs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const checkEnv = prepareGooseV149CheckEnv({ ...process.env }, root);
if (!String(checkEnv.GOOSE_V149_MEMORY_VERBAL_FORCE_DEEP ?? '').trim()) {
checkEnv.GOOSE_V149_MEMORY_VERBAL_FORCE_DEEP = '0';
}
if (!String(checkEnv.GOOSE_V149_PORTAL_RESUME_TIMEOUT_MS ?? '').trim()) {
checkEnv.GOOSE_V149_PORTAL_RESUME_TIMEOUT_MS = '300000';
}
const checks = [
{ name: 'page-data', script: 'check-goosed-v149-page-data-evidence.mjs', required: true },
{ name: 'memory-verbal', script: 'check-goosed-v149-memory-verbal-recall.mjs', required: true },
{ name: 'no-think', script: 'check-goosed-v149-no-think-evidence.mjs', required: true },
{ name: 'wechat', script: 'check-goosed-v149-wechat-evidence.mjs', required: true },
{ name: 'session-affinity', script: 'check-goosed-v149-session-affinity-evidence.mjs', required: true },
{
name: 'cost',
script: 'check-goosed-v149-cost-smoke.mjs',
required: true,
},
];
function run(script) {
const result = spawnSync(process.execPath, [path.join(root, 'scripts', script)], {
cwd: root,
env: checkEnv,
encoding: 'utf8',
});
return {
ok: checkPassed(result),
outcome: classifyCheckResult(result),
stdout: result.stdout?.trim() ?? '',
stderr: result.stderr?.trim() ?? '',
};
}
const results = [];
for (const check of checks) {
const item = { ...check, ...run(check.script) };
results.push(item);
console.log(`[goose-v149-evidence] ${item.outcome} ${check.name}`);
if (item.stdout) console.log(item.stdout);
if (!item.ok && item.stderr) console.error(item.stderr);
}
const failedRequired = results.filter((item) => item.required && !item.ok);
const failedOptional = results.filter((item) => !item.required && !item.ok);
if (failedRequired.length) {
console.error(`GOOSE_V149_MISSING_EVIDENCE_FAIL: ${failedRequired.map((i) => i.name).join(', ')}`);
process.exit(1);
}
if (failedOptional.length) {
console.log(`GOOSE_V149_MISSING_EVIDENCE_OK_WITH_SKIPS: ${failedOptional.map((i) => i.name).join(', ')}`);
} else {
console.log('GOOSE_V149_MISSING_EVIDENCE_OK');
}
+12 -6
View File
@@ -3,14 +3,19 @@
* Phase 2 orchestrator: cost / memory drift / portal canary verification.
*/
import { spawnSync } from 'node:child_process';
import { checkPassed, classifyCheckResult } from './goose-v149-check-result.mjs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const checkEnv = prepareGooseV149CheckEnv({ ...process.env }, root);
const checks = [
{ name: 'phase1-all', script: 'check-goosed-v149-all.mjs', required: true },
{ name: 'cost-smoke', script: 'check-goosed-v149-cost-smoke.mjs', required: false },
{ name: 'cost-smoke', script: 'check-goosed-v149-cost-smoke.mjs', required: true },
{ name: 'memory-drift', script: 'compare-goose-v149-memory-manifest.mjs', required: true },
{ name: 'memory-failopen', script: 'check-goosed-v149-memory-failopen.mjs', required: true },
{ name: 'web-smoke', script: 'check-goosed-v149-web-smoke.mjs', required: true },
@@ -18,7 +23,7 @@ const checks = [
{
name: 'deepseek-tools',
script: 'check-goosed-v149-deepseek-tools.mjs',
required: false,
required: true,
},
{
name: 'thinking-preservation',
@@ -31,17 +36,18 @@ const checks = [
required: true,
},
{ name: 'canary-portal', script: 'check-goosed-v149-canary-portal.mjs', required: true },
{ name: 'portal-smoke', script: 'check-goosed-v149-portal-smoke.mjs', required: false },
{ name: 'portal-smoke', script: 'check-goosed-v149-portal-smoke.mjs', required: true },
];
function run(script, extraEnv = {}) {
const result = spawnSync(process.execPath, [path.join(root, 'scripts', script)], {
cwd: root,
env: { ...process.env, ...extraEnv },
env: { ...checkEnv, ...extraEnv },
encoding: 'utf8',
});
return {
ok: result.status === 0,
ok: checkPassed(result),
outcome: classifyCheckResult(result),
status: result.status,
stdout: result.stdout?.trim() ?? '',
stderr: result.stderr?.trim() ?? '',
@@ -54,7 +60,7 @@ for (const check of checks) {
const item = { ...check, ...run(check.script) };
results.push(item);
const label = check.required ? 'required' : 'optional';
console.log(`[goose-v149-phase2] ${item.ok ? 'OK' : 'FAIL'} (${label}) ${check.name}`);
console.log(`[goose-v149-phase2] ${item.outcome} (${label}) ${check.name}`);
if (item.stdout) console.log(item.stdout);
if (!item.ok && item.stderr) console.error(item.stderr);
}
+70 -21
View File
@@ -4,10 +4,18 @@
* Does not authorize 103/105; local loopback only.
*/
import { spawnSync } from 'node:child_process';
import { checkPassed, classifyCheckResult } from './goose-v149-check-result.mjs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
import {
gooseV149PortalAgentEnv,
waitForAgentRunWorkerIdle,
} from './goose-v149-worker-idle.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const checkEnv = prepareGooseV149CheckEnv({ ...process.env }, root);
const checks = [
{ name: 'phase2', script: 'run-goosed-v149-phase2.mjs', required: true },
@@ -31,16 +39,41 @@ const checks = [
script: 'compare-goose-v149-memory-manifest.mjs',
required: true,
},
{
name: 'portal-resume',
script: 'check-goosed-v149-portal-resume.mjs',
required: true,
extraEnv: gooseV149PortalAgentEnv,
waitForWorkerIdle: true,
},
{
name: 'page-e2e',
script: 'check-goosed-v149-page-e2e.mjs',
required: true,
extraEnv: gooseV149PortalAgentEnv,
waitForWorkerIdle: true,
},
{ name: 'memory-chat', script: 'check-goosed-v149-memory-chat.mjs', required: true },
{
name: 'missing-evidence',
script: 'run-goosed-v149-missing-evidence.mjs',
required: true,
extraEnv: {
GOOSE_V149_MEMORY_VERBAL_FORCE_DEEP: process.env.GOOSE_V149_MEMORY_VERBAL_FORCE_DEEP ?? '0',
GOOSE_V149_PORTAL_RESUME_TIMEOUT_MS: process.env.GOOSE_V149_PORTAL_RESUME_TIMEOUT_MS ?? '300000',
},
},
];
function runScript(script, extraEnv = {}) {
const result = spawnSync(process.execPath, [path.join(root, 'scripts', script)], {
cwd: root,
env: { ...process.env, ...extraEnv },
env: { ...checkEnv, ...extraEnv },
encoding: 'utf8',
});
return {
ok: result.status === 0,
ok: checkPassed(result),
outcome: classifyCheckResult(result),
status: result.status,
stdout: result.stdout?.trim() ?? '',
stderr: result.stderr?.trim() ?? '',
@@ -51,35 +84,51 @@ function runCommand(command, extraEnv = {}) {
const [bin, ...args] = command;
const result = spawnSync(bin, args, {
cwd: root,
env: { ...process.env, ...extraEnv },
env: { ...checkEnv, ...extraEnv },
encoding: 'utf8',
});
return {
ok: result.status === 0,
ok: checkPassed(result),
outcome: classifyCheckResult(result),
status: result.status,
stdout: result.stdout?.trim() ?? '',
stderr: result.stderr?.trim() ?? '',
};
}
const results = [];
async function main() {
const results = [];
for (const check of checks) {
const item = {
...check,
...(check.script ? runScript(check.script) : runCommand(check.command)),
};
results.push(item);
console.log(`[goose-v149-phase3] ${item.ok ? 'OK' : 'FAIL'} ${check.name}`);
if (item.stdout) console.log(item.stdout);
if (!item.ok && item.stderr) console.error(item.stderr);
for (const check of checks) {
if (check.waitForWorkerIdle) {
await waitForAgentRunWorkerIdle(root, checkEnv, {
logPrefix: '[goose-v149-phase3]',
});
}
const extraEnv = check.extraEnv ?? {};
const item = {
...check,
...(check.script
? runScript(check.script, extraEnv)
: runCommand(check.command, extraEnv)),
};
results.push(item);
console.log(`[goose-v149-phase3] ${item.outcome} ${check.name}`);
if (item.stdout) console.log(item.stdout);
if (!item.ok && item.stderr) console.error(item.stderr);
}
const failedRequired = results.filter((item) => item.required && !item.ok);
if (failedRequired.length) {
console.error(`GOOSE_V149_PHASE3_FAIL: ${failedRequired.map((item) => item.name).join(', ')}`);
process.exit(1);
}
console.log('GOOSE_V149_PHASE3_OK');
console.log(' next: docs/goose-v149-phase3-closeout.md (merge prep, no 103 until approved)');
}
const failedRequired = results.filter((item) => item.required && !item.ok);
if (failedRequired.length) {
console.error(`GOOSE_V149_PHASE3_FAIL: ${failedRequired.map((item) => item.name).join(', ')}`);
main().catch((error) => {
console.error(`GOOSE_V149_PHASE3_FAIL: ${error.message}`);
process.exit(1);
}
console.log('GOOSE_V149_PHASE3_OK');
console.log(' next: docs/goose-v149-phase3-closeout.md (merge prep, no 103 until approved)');
});