Files
memind/scripts/check-memory-v2-app-canary.mjs
2026-07-03 09:46:03 +08:00

186 lines
6.1 KiB
JavaScript

#!/usr/bin/env node
function normalizeBaseUrl(value) {
return String(value ?? '').trim().replace(/\/+$/, '');
}
function requireValue(name, value) {
if (!value) throw new Error(`${name} requires a value`);
return value;
}
export function parseMemoryV2AppCanaryArgs(argv = [], env = process.env) {
const options = {
baseUrl: normalizeBaseUrl(env.MEMORY_V2_APP_BASE_URL || 'http://127.0.0.1:8081'),
requireEnabled: false,
requireTargetHealthy: false,
expectedBackend: null,
expectedSelectedBackend: null,
timeoutMs: Number(env.MEMORY_V2_APP_CANARY_TIMEOUT_MS || 8000),
help: false,
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--base-url') {
options.baseUrl = normalizeBaseUrl(requireValue(arg, argv[++i]));
} else if (arg === '--require-enabled') {
options.requireEnabled = true;
} else if (arg === '--require-target-healthy') {
options.requireTargetHealthy = true;
} else if (arg === '--expect-backend') {
options.expectedBackend = requireValue(arg, argv[++i]);
} else if (arg === '--expect-selected-backend') {
options.expectedSelectedBackend = requireValue(arg, argv[++i]);
} else if (arg === '--timeout-ms') {
options.timeoutMs = Number(requireValue(arg, argv[++i]));
} else if (arg === '-h' || arg === '--help') {
options.help = true;
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}
if (!options.baseUrl) throw new Error('--base-url is required');
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
throw new Error('--timeout-ms must be a positive number');
}
return options;
}
function usage() {
return `Usage:
node scripts/check-memory-v2-app-canary.mjs [options]
Options:
--base-url <url> Portal base URL. Defaults to MEMORY_V2_APP_BASE_URL or http://127.0.0.1:8081.
--require-enabled Fail unless /api/runtime/status reports memory.enabled=true.
--expect-backend <name> Fail unless memory.backend equals this value.
--expect-selected-backend <name> Fail unless memory.selectedBackend equals this value.
--require-target-healthy Fail unless at least one runtime target is healthy.
--timeout-ms <ms> Request timeout. Defaults to 8000.
`;
}
async function fetchJson(fetchImpl, url, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchImpl(url, { 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,
text,
json,
};
} finally {
clearTimeout(timer);
}
}
function check(name, ok, details = {}) {
return { name, ok: Boolean(ok), ...details };
}
export async function runMemoryV2AppCanaryCli({
argv = process.argv.slice(2),
env = process.env,
stdout = process.stdout,
fetchImpl = globalThis.fetch,
} = {}) {
const options = parseMemoryV2AppCanaryArgs(argv, env);
if (options.help) {
stdout.write(usage());
return 0;
}
if (typeof fetchImpl !== 'function') {
throw new Error('fetch is not available in this Node.js runtime');
}
const runtime = await fetchJson(fetchImpl, `${options.baseUrl}/api/runtime/status`, options.timeoutMs);
const apiStatus = await fetchJson(fetchImpl, `${options.baseUrl}/api/status`, options.timeoutMs);
const authStatus = await fetchJson(fetchImpl, `${options.baseUrl}/auth/status`, options.timeoutMs);
const memory = runtime.json?.memory ?? null;
const targets = Array.isArray(runtime.json?.targets) ? runtime.json.targets : [];
const healthyTargets = targets.filter((target) => target?.healthy);
const checks = [
check('runtime_status_http_ok', runtime.ok, { status: runtime.status }),
check('runtime_status_ok', runtime.json?.ok === true),
check('memory_status_present', Boolean(memory)),
check('api_status_http_ok', apiStatus.ok, { status: apiStatus.status }),
check('auth_status_http_ok', authStatus.ok, { status: authStatus.status }),
];
if (options.requireEnabled) {
checks.push(check('memory_enabled', memory?.enabled === true, { actual: memory?.enabled ?? null }));
}
if (options.expectedBackend) {
checks.push(check('memory_backend', memory?.backend === options.expectedBackend, {
expected: options.expectedBackend,
actual: memory?.backend ?? null,
}));
}
if (options.expectedSelectedBackend) {
checks.push(check('memory_selected_backend', memory?.selectedBackend === options.expectedSelectedBackend, {
expected: options.expectedSelectedBackend,
actual: memory?.selectedBackend ?? null,
}));
}
if (options.requireTargetHealthy) {
checks.push(check('runtime_target_healthy', healthyTargets.length > 0, {
healthyTargets: healthyTargets.length,
targets: targets.length,
}));
}
const report = {
ok: checks.every((item) => item.ok),
baseUrl: options.baseUrl,
summary: {
memory: memory
? {
enabled: memory.enabled,
backend: memory.backend,
selectedBackend: memory.selectedBackend,
failOpen: memory.failOpen,
vectorEnabled: memory.vectorEnabled,
}
: null,
targets: {
total: targets.length,
healthy: healthyTargets.length,
},
apiStatus: {
status: apiStatus.status,
body: apiStatus.text.slice(0, 80),
},
authStatus: {
status: authStatus.status,
authenticated: authStatus.json?.authenticated ?? null,
mode: authStatus.json?.mode ?? null,
},
},
checks,
};
stdout.write(`${JSON.stringify(report, null, 2)}\n`);
return report.ok ? 0 : 1;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runMemoryV2AppCanaryCli().then((code) => {
process.exitCode = code;
}).catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exitCode = 1;
});
}