#!/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 { enforceRealLlmGate } from './goose-v149-real-llm-gate.mjs'; import { createReporter, extractAssistantTexts, getSession, loginViaApi, resolvePortalBase, waitForAssistantGrowth, waitForRunTerminal, } from './scenario-test-lib.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); enforceRealLlmGate('check-goosed-v149-memory-verbal-recall.mjs'); 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); });