Files
memind/scripts/check-goosed-v149-memory-chat.mjs
T
john 1d165bc6e3 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>
2026-09-09 18:11:39 +08:00

268 lines
9.1 KiB
JavaScript

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