#!/usr/bin/env node /** * 验证 john4 儿童爱好问卷「饮食偏好 / 每日几顿」增量改造链路(无需重新聊天)。 * * Usage: * node scripts/verify-children-hobby-diet-survey.mjs * node scripts/verify-children-hobby-diet-survey.mjs --no-insert * node scripts/verify-children-hobby-diet-survey.mjs --user-id --port 8081 */ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadH5Environment } from './load-env.mjs'; import { CHILDREN_HOBBY_DIET_SURVEY, createReporter, resolvePortalBase, verifyChildrenHobbyDietSurvey, } from './scenario-test-lib.mjs'; import { PUBLISH_ROOT_DIR } from '../user-publish.mjs'; import { createDbPool } from '../db.mjs'; const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); loadH5Environment(import.meta.dirname); function parseArgs(argv) { const options = { userId: CHILDREN_HOBBY_DIET_SURVEY.userId, port: Number(process.env.H5_PORT ?? 8081), testInsert: true, checkRuns: true, }; for (let i = 2; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--no-insert') options.testInsert = false; else if (arg === '--no-runs') options.checkRuns = false; else if (arg === '--user-id' && argv[i + 1]) options.userId = argv[++i]; else if (arg === '--port' && argv[i + 1]) options.port = Number(argv[++i]); else if (arg === '-h' || arg === '--help') { console.log(`Usage: node scripts/verify-children-hobby-diet-survey.mjs [--no-insert] [--no-runs] [--user-id ] [--port ]`); process.exit(0); } else { throw new Error(`未知参数: ${arg}`); } } return options; } async function ensurePortalReady(baseUrl) { const response = await fetch(`${baseUrl}/auth/status`); if (!response.ok) { throw new Error(`Portal 未就绪: ${baseUrl}/auth/status -> ${response.status}`); } } function verifySqliteColumn(sqlitePath, tableName, columnName, reporter) { try { const output = execSync( `sqlite3 ${JSON.stringify(sqlitePath)} "PRAGMA table_info(${tableName});"`, { encoding: 'utf8' }, ); if (!output.includes(`|${columnName}|`)) { reporter.fail('SQLite 列', `${tableName}.${columnName} 不存在`); return false; } reporter.pass('SQLite 列', `${tableName}.${columnName}`); const datasetJson = execSync( `sqlite3 ${JSON.stringify(sqlitePath)} "SELECT config_json FROM __page_data_datasets WHERE name='${CHILDREN_HOBBY_DIET_SURVEY.dataset}';"`, { encoding: 'utf8' }, ).trim(); const config = JSON.parse(datasetJson); const insertCols = config?.columns?.insert ?? []; if (!insertCols.includes(columnName)) { reporter.fail('dataset insert 列', `${columnName} 不在 ${insertCols.join(', ')}`); return false; } reporter.pass('dataset insert 列', insertCols.join(', ')); return true; } catch (error) { reporter.fail('SQLite 检查', error instanceof Error ? error.message : String(error)); return false; } } async function verifyAgentRunHealth(userId, reporter) { if (!process.env.DATABASE_URL && !fs.existsSync(path.join(repoRoot, '.env'))) { reporter.pass('Agent run 健康', '跳过(无 DATABASE_URL)'); return true; } try { for (const line of fs.readFileSync(path.join(repoRoot, '.env'), 'utf8').split('\n')) { const t = line.trim(); if (!t || t.startsWith('#')) continue; const eq = t.indexOf('='); if (eq < 0) continue; const k = t.slice(0, eq).trim(); const v = t.slice(eq + 1).trim(); if (!process.env[k]) process.env[k] = v; } const pool = createDbPool(); const [runningRows] = await pool.query( `SELECT id, agent_session_id, updated_at FROM h5_agent_runs WHERE user_id = ? AND status = 'running' ORDER BY updated_at DESC LIMIT 5`, [userId], ); await pool.end(); if (runningRows.length > 0) { reporter.fail('Agent run 健康', `${runningRows.length} 条 running(可能前端仍在 loading)`); for (const row of runningRows) { console.error(` - ${row.id} session=${row.agent_session_id}`); } return false; } reporter.pass('Agent run 健康', '无 orphan running'); return true; } catch (error) { reporter.fail('Agent run 健康', error instanceof Error ? error.message : String(error)); return false; } } async function main() { const options = parseArgs(process.argv); const reporter = createReporter(); const baseUrl = resolvePortalBase(options.port); const publishKey = options.userId; const sqlitePath = path.join( repoRoot, PUBLISH_ROOT_DIR, publishKey, '.mindspace', 'private-data.sqlite', ); console.log('==> 儿童爱好问卷 · 饮食偏好链路验证'); console.log(` Portal: ${baseUrl}`); console.log(` 用户: ${publishKey}\n`); await ensurePortalReady(baseUrl); let ok = true; ok = verifySqliteColumn( sqlitePath, CHILDREN_HOBBY_DIET_SURVEY.dataset, CHILDREN_HOBBY_DIET_SURVEY.dietField, reporter, ) && ok; ok = await verifyChildrenHobbyDietSurvey({ publishKey, baseUrl, reporter, testInsert: options.testInsert, }) && ok; if (options.checkRuns) { ok = await verifyAgentRunHealth(publishKey, reporter) && ok; } const code = reporter.summary(); process.exit(code); } main().catch((error) => { console.error(error instanceof Error ? error.stack ?? error.message : error); process.exit(1); });