mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 16:21:07 +00:00
146 lines
5.1 KiB
JavaScript
146 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const ROOT = process.cwd();
|
|
|
|
function usage() {
|
|
return 'Usage: node scripts/check-domain-e2e-coverage.mjs';
|
|
}
|
|
|
|
for (const arg of process.argv.slice(2)) {
|
|
if (arg === '--help' || arg === '-h') {
|
|
console.log(usage());
|
|
process.exit(0);
|
|
}
|
|
console.error(`check-domain-e2e-coverage: unknown argument: ${arg}`);
|
|
console.error(usage());
|
|
process.exit(2);
|
|
}
|
|
|
|
const rawThreshold = process.env.DOMAIN_E2E_COVERAGE_THRESHOLD ?? '90';
|
|
const THRESHOLD = Number(rawThreshold);
|
|
if (!Number.isFinite(THRESHOLD) || THRESHOLD < 0 || THRESHOLD > 100) {
|
|
// A non-numeric value would make THRESHOLD NaN, turning every `percent <
|
|
// THRESHOLD` comparison false and silently disabling the gate. Fail loudly.
|
|
console.error(
|
|
`Invalid DOMAIN_E2E_COVERAGE_THRESHOLD="${rawThreshold}". Expected a number between 0 and 100.`,
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
const MODULES = [
|
|
{ label: 'config', namespaces: ['config'] },
|
|
{ label: 'credentials', namespaces: ['auth'] },
|
|
{ label: 'app_state', namespaces: ['app_state'] },
|
|
{ label: 'connectivity', namespaces: ['connectivity'] },
|
|
{ label: 'inference', namespaces: ['inference'] },
|
|
{ label: 'agent', namespaces: ['agent'] },
|
|
{ label: 'tools', namespaces: ['tools'] },
|
|
{ label: 'tool_registry', namespaces: ['tool_registry'] },
|
|
{ label: 'approval', namespaces: ['approval'] },
|
|
{ label: 'memory', namespaces: ['memory'] },
|
|
{ label: 'memory_tree', namespaces: ['memory_tree'] },
|
|
{ label: 'memory_sync', namespaces: ['memory_sync'] },
|
|
{ label: 'memory_sources', namespaces: ['memory_sources'] },
|
|
{ label: 'embeddings', namespaces: ['embeddings'] },
|
|
{ label: 'channels', namespaces: ['channels'] },
|
|
{ label: 'composio', namespaces: ['composio'] },
|
|
{ label: 'threads', namespaces: ['threads'] },
|
|
];
|
|
|
|
const TARGET_NAMESPACES = new Set(MODULES.flatMap((module) => module.namespaces));
|
|
|
|
function walk(dir, predicate, out = []) {
|
|
if (!fs.existsSync(dir)) return out;
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
walk(full, predicate, out);
|
|
} else if (predicate(full)) {
|
|
out.push(full);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function read(file) {
|
|
return fs.readFileSync(file, 'utf8');
|
|
}
|
|
|
|
function collectInvokedMethods() {
|
|
const methods = new Set();
|
|
const testsDir = path.join(ROOT, 'tests');
|
|
const files = walk(testsDir, (file) => file.endsWith('_e2e.rs'));
|
|
|
|
for (const file of files) {
|
|
const text = read(file);
|
|
for (const match of text.matchAll(/"((?:openhuman)\.[A-Za-z0-9_]+)"/g)) {
|
|
methods.add(match[1]);
|
|
}
|
|
}
|
|
|
|
return methods;
|
|
}
|
|
|
|
function collectSchemaMethods() {
|
|
const methodsByNamespace = new Map([...TARGET_NAMESPACES].map((namespace) => [namespace, new Set()]));
|
|
const files = walk(path.join(ROOT, 'src', 'openhuman'), (file) => {
|
|
const normalized = file.split(path.sep).join('/');
|
|
return file.endsWith('.rs') && /(^|\/)schemas?(\.rs|\/)/.test(normalized);
|
|
});
|
|
|
|
for (const file of files) {
|
|
const text = read(file);
|
|
const constNamespace = text.match(/const\s+NAMESPACE:\s*&str\s*=\s*"([a-z_]+)"/)?.[1];
|
|
for (const match of text.matchAll(/ControllerSchema\s*\{([\s\S]*?)\n\s*\}/g)) {
|
|
const block = match[1];
|
|
const namespaceToken = block.match(/namespace:\s*(?:NAMESPACE|"([a-z_]+)")/);
|
|
const functionName = block.match(/function:\s*"([A-Za-z0-9_]+)"/)?.[1];
|
|
const namespace = namespaceToken?.[1] ?? (namespaceToken ? constNamespace : undefined);
|
|
if (!namespace || !functionName || functionName === 'unknown') continue;
|
|
if (!TARGET_NAMESPACES.has(namespace)) continue;
|
|
methodsByNamespace.get(namespace).add(`openhuman.${namespace}_${functionName}`);
|
|
}
|
|
}
|
|
|
|
return methodsByNamespace;
|
|
}
|
|
|
|
const invoked = collectInvokedMethods();
|
|
const schemas = collectSchemaMethods();
|
|
let failed = false;
|
|
|
|
console.log(`Domain Rust E2E controller coverage threshold: ${THRESHOLD}%`);
|
|
console.log('');
|
|
console.log('| Module | Namespace(s) | Covered | Percent | Missing |');
|
|
console.log('| --- | --- | ---: | ---: | --- |');
|
|
|
|
for (const module of MODULES) {
|
|
const expected = new Set();
|
|
const covered = new Set();
|
|
for (const namespace of module.namespaces) {
|
|
for (const method of schemas.get(namespace) ?? []) expected.add(method);
|
|
}
|
|
for (const method of expected) {
|
|
if (invoked.has(method)) covered.add(method);
|
|
}
|
|
|
|
const missing = [...expected].filter((method) => !covered.has(method)).sort();
|
|
const percent = expected.size === 0 ? 100 : (covered.size / expected.size) * 100;
|
|
const missingText = missing.length === 0 ? '-' : missing.join('<br>');
|
|
|
|
console.log(
|
|
`| ${module.label} | ${module.namespaces.join(', ')} | ${covered.size}/${expected.size} | ${percent.toFixed(1)}% | ${missingText} |`,
|
|
);
|
|
|
|
if (expected.size > 0 && percent < THRESHOLD) failed = true;
|
|
}
|
|
|
|
if (failed) {
|
|
console.error(`\nDomain Rust E2E controller coverage is below ${THRESHOLD}% for one or more modules.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\nAll named modules meet the ${THRESHOLD}% Rust E2E controller coverage threshold.`);
|