67220a14ea
Prevent phase2/phase3/all reruns from burning DashScope tokens unless GOOSE_V149_ALLOW_REAL_LLM=1 is explicitly set with human approval. Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
6.8 KiB
JavaScript
212 lines
6.8 KiB
JavaScript
#!/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);
|
|
});
|