test(qa): cover Prometheus install and runtime export (#118867)

This commit is contained in:
Vincent Koc
2026-08-04 05:38:23 +08:00
committed by GitHub
parent 7fd3363866
commit 7daf990688
3 changed files with 416 additions and 1 deletions
@@ -0,0 +1,374 @@
// Proves the external Prometheus plugin's managed install and trusted runtime boundary.
import { execFile, spawn, type ChildProcess } from "node:child_process";
import { once } from "node:events";
import fs from "node:fs/promises";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
const execFileAsync = promisify(execFile);
const packageName = "@openclaw/diagnostics-prometheus";
const pluginId = "diagnostics-prometheus";
const repoRoot = path.resolve(import.meta.dirname, "../../..");
const pluginRoot = path.resolve(import.meta.dirname, "..");
const tempDirs: string[] = [];
const children: ChildProcess[] = [];
async function stopChild(child: ChildProcess): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) {
return;
}
const exited = once(child, "exit").then(() => true);
child.kill("SIGTERM");
if (!(await Promise.race([exited, delay(5_000, false)]))) {
child.kill("SIGKILL");
await Promise.race([exited, delay(5_000)]);
}
}
afterEach(async () => {
await Promise.all(children.splice(0).map(stopChild));
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
});
async function makeTempDir(): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-prometheus-install-"));
tempDirs.push(dir);
return dir;
}
async function reservePort(): Promise<number> {
const server = net.createServer();
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
if (!address || typeof address === "string") {
server.close();
throw new Error("failed to reserve a loopback port");
}
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
return address.port;
}
function isolatedEnv(params: {
configPath: string;
home: string;
registry?: string;
stateDir: string;
}): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...process.env,
HOME: params.home,
USERPROFILE: params.home,
OPENCLAW_HOME: params.home,
OPENCLAW_STATE_DIR: params.stateDir,
OPENCLAW_CONFIG_PATH: params.configPath,
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
NODE_ENV: "production",
NO_COLOR: "1",
};
for (const key of [
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_PLUGIN_CATALOG_PATHS",
"OPENCLAW_PLUGINS_PATHS",
"OPENCLAW_TEST_FAST",
"OPENCLAW_TEST_HOME",
"OPENCLAW_TEST_MINIMAL_GATEWAY",
"OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR",
"VITEST",
"VITEST_POOL_ID",
"VITEST_WORKER_ID",
]) {
delete env[key];
}
if (params.registry) {
env.NPM_CONFIG_REGISTRY = params.registry;
env.npm_config_registry = params.registry;
}
return env;
}
async function runCli(args: string[], env: NodeJS.ProcessEnv, build = false): Promise<string> {
const entry = build ? "scripts/run-node.mjs" : "openclaw.mjs";
const result = await execFileAsync(process.execPath, [entry, ...args], {
cwd: repoRoot,
env,
maxBuffer: 4 * 1024 * 1024,
timeout: 180_000,
});
return result.stdout;
}
async function packPlugin(outputDir: string): Promise<{
files: string[];
tarballPath: string;
}> {
const stagingDir = path.join(outputDir, "package-source");
await fs.cp(pluginRoot, stagingDir, {
recursive: true,
filter: (source) => {
const relative = path.relative(pluginRoot, source);
const topLevel = relative.split(path.sep)[0];
return topLevel !== "dist" && topLevel !== "node_modules";
},
});
await execFileAsync(process.execPath, ["scripts/lib/plugin-npm-runtime-build.mjs", stagingDir], {
cwd: repoRoot,
maxBuffer: 2 * 1024 * 1024,
timeout: 60_000,
});
const result = await execFileAsync(
process.execPath,
[
"scripts/lib/plugin-npm-package-manifest.mjs",
"--run",
stagingDir,
"--",
"npm",
"pack",
"--json",
"--ignore-scripts",
"--pack-destination",
outputDir,
],
{
cwd: repoRoot,
env: {
...process.env,
OPENCLAW_PLUGIN_NPM_BUNDLE_DEPENDENCIES: "1",
},
maxBuffer: 2 * 1024 * 1024,
timeout: 60_000,
},
);
const entries = JSON.parse(result.stdout) as Array<{
filename?: string;
files?: Array<{ path?: string }>;
}>;
const entry = entries[0];
const filename = entry?.filename;
if (!filename) {
throw new Error("npm pack did not report the diagnostics-prometheus tarball");
}
return {
files: (entry.files ?? []).flatMap((file) =>
typeof file.path === "string" ? [file.path] : [],
),
tarballPath: path.join(outputDir, filename),
};
}
async function readPluginVersion(): Promise<string> {
const manifest = JSON.parse(await fs.readFile(path.join(pluginRoot, "package.json"), "utf8")) as {
version?: unknown;
};
if (typeof manifest.version !== "string" || !manifest.version.trim()) {
throw new Error("diagnostics-prometheus package version is missing");
}
return manifest.version.trim();
}
async function waitForFile(
filePath: string,
child: ChildProcess,
timeoutMs: number,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (child.exitCode !== null || child.signalCode !== null) {
throw new Error("local npm registry exited before publishing its port");
}
try {
if ((await fs.stat(filePath)).size > 0) {
return;
}
} catch {
// The registry writes the port file only after the listener is ready.
}
await delay(50);
}
throw new Error("timed out waiting for local npm registry");
}
async function startRegistry(params: {
root: string;
tarballPath: string;
version: string;
}): Promise<string> {
const portFile = path.join(params.root, "registry-port");
const logPath = path.join(params.root, "registry.log");
const logHandle = await fs.open(logPath, "a");
const child = spawn(
process.execPath,
[
"scripts/e2e/lib/plugins/npm-registry-server.mjs",
portFile,
packageName,
params.version,
params.tarballPath,
],
{
cwd: repoRoot,
env: isolatedEnv({
configPath: path.join(params.root, "unused.json"),
home: params.root,
stateDir: path.join(params.root, "unused-state"),
}),
stdio: ["ignore", logHandle.fd, logHandle.fd],
},
);
children.push(child);
await logHandle.close();
await waitForFile(portFile, child, 10_000);
const port = (await fs.readFile(portFile, "utf8")).trim();
return `http://127.0.0.1:${port}`;
}
async function waitForGateway(params: {
child: ChildProcess;
logPath: string;
port: number;
}): Promise<void> {
const deadline = Date.now() + 60_000;
while (Date.now() < deadline) {
if (params.child.exitCode !== null || params.child.signalCode !== null) {
const logs = await fs.readFile(params.logPath, "utf8").catch(() => "");
throw new Error(`Gateway exited before readiness:\n${logs.slice(-8_000)}`);
}
try {
const response = await fetch(`http://127.0.0.1:${params.port}/readyz`, {
signal: AbortSignal.timeout(1_000),
});
if (response.ok) {
return;
}
} catch {
// Readiness is authoritative only after the HTTP endpoint responds.
}
await delay(200);
}
const logs = await fs.readFile(params.logPath, "utf8").catch(() => "");
throw new Error(`Gateway did not become ready:\n${logs.slice(-8_000)}`);
}
describe("diagnostics-prometheus managed install runtime", () => {
it("installs the exact official package and exports metrics at Gateway startup", async () => {
const root = await makeTempDir();
const home = path.join(root, "home");
const stateDir = path.join(root, "state");
const configPath = path.join(stateDir, "openclaw.json");
const gatewayLog = path.join(root, "gateway.log");
const gatewayToken = "prometheus-managed-install-test-token";
const gatewayPort = await reservePort();
await fs.mkdir(home, { recursive: true });
await fs.mkdir(stateDir, { recursive: true });
await fs.writeFile(
configPath,
`${JSON.stringify(
{
diagnostics: { enabled: true },
gateway: {
mode: "local",
bind: "loopback",
port: gatewayPort,
auth: { mode: "token", token: gatewayToken },
},
},
null,
2,
)}\n`,
"utf8",
);
const pluginVersion = await readPluginVersion();
const packedPlugin = await packPlugin(root);
expect(packedPlugin.files.some((file) => /^dist\/index\.(?:js|mjs|cjs)$/u.test(file))).toBe(
true,
);
const registry = await startRegistry({
root,
tarballPath: packedPlugin.tarballPath,
version: pluginVersion,
});
const env = isolatedEnv({
configPath,
home,
registry,
stateDir,
});
await runCli(["plugins", "install", `npm:${packageName}@${pluginVersion}`], env, true);
const inspect = JSON.parse(
await runCli(["plugins", "inspect", pluginId, "--runtime", "--json"], env),
) as {
install?: {
artifactKind?: unknown;
installPath?: unknown;
resolvedName?: unknown;
resolvedVersion?: unknown;
source?: unknown;
sourcePath?: unknown;
};
plugin?: {
enabled?: unknown;
id?: unknown;
origin?: unknown;
status?: unknown;
trustedOfficialInstall?: unknown;
};
};
expect(inspect.install).toMatchObject({
source: "npm",
resolvedName: packageName,
resolvedVersion: pluginVersion,
});
expect(typeof inspect.install?.installPath).toBe("string");
expect(inspect.install?.artifactKind).toBeUndefined();
expect(inspect.install?.sourcePath).toBeUndefined();
expect(inspect.plugin).toMatchObject({
id: pluginId,
enabled: true,
status: "loaded",
trustedOfficialInstall: true,
});
expect(["config", "global"]).toContain(inspect.plugin?.origin);
const gatewayLogHandle = await fs.open(gatewayLog, "a");
const gateway = spawn(
process.execPath,
["openclaw.mjs", "gateway", "run", "--bind", "loopback", "--port", String(gatewayPort)],
{
cwd: repoRoot,
env,
stdio: ["ignore", gatewayLogHandle.fd, gatewayLogHandle.fd],
},
);
children.push(gateway);
await gatewayLogHandle.close();
await waitForGateway({ child: gateway, logPath: gatewayLog, port: gatewayPort });
const url = `http://127.0.0.1:${gatewayPort}/api/diagnostics/prometheus`;
const unauthenticated = await fetch(url);
expect([401, 403]).toContain(unauthenticated.status);
const authenticated = await fetch(url, {
headers: { authorization: `Bearer ${gatewayToken}` },
});
const body = await authenticated.text();
expect(authenticated.status).toBe(200);
expect(authenticated.headers.get("content-type")).toContain("text/plain");
expect(body).toContain(
'openclaw_telemetry_exporter_total{exporter="diagnostics-prometheus",reason="configured",signal="metrics",status="started"} 1',
);
const gatewayLogs = await fs.readFile(gatewayLog, "utf8");
expect(gatewayLogs).not.toContain(
"diagnostics-prometheus: internal diagnostics capability unavailable",
);
}, 300_000);
});
@@ -0,0 +1,27 @@
title: Diagnostics Prometheus managed install and runtime
scenario:
id: diagnostics-prometheus-plugin-install
surface: telemetry
coverage:
primary:
- observability.diagnostics-prometheus-plugin-install
objective: Verify the exact checkout plugin installs through the official npm path, loads as a trusted managed plugin, and exports runtime metrics after Gateway startup.
successCriteria:
- The exact checkout plugin package installs through the canonical npm plugin command.
- The managed npm install record identifies the official package and installed version.
- Runtime inspection reports the enabled plugin loaded from the trusted official install.
- An isolated Gateway exposes the protected Prometheus endpoint from the installed plugin.
- The authenticated scrape contains the diagnostics-prometheus exporter-start sample.
docsRefs:
- docs/plugins/reference/diagnostics-prometheus.md
- docs/gateway/prometheus.md
codeRefs:
- extensions/diagnostics-prometheus/src/install-runtime.e2e.test.ts
- src/plugins/official-external-install-trust.ts
- src/plugins/manifest-registry.ts
- src/plugins/services.ts
execution:
kind: vitest
path: extensions/diagnostics-prometheus/src/install-runtime.e2e.test.ts
summary: Install the exact checkout plugin from a local npm registry, start an isolated Gateway, and scrape its trusted runtime exporter.
@@ -7,8 +7,10 @@ scenario:
primary:
- observability.prometheus-api-auth
- observability.prometheus
secondary:
- observability.prometheus-qa-lab
- observability.prometheus-runtime-validation
- observability.telemetry-prometheus
secondary:
- containers.e2e-plan-runtime-validation
objective: Verify a QA-lab gateway run emits protected, bounded Prometheus diagnostics metrics through the diagnostics-prometheus plugin.
successCriteria:
@@ -128,6 +130,18 @@ flow:
- assert:
expr: "prometheusText.includes('# TYPE openclaw_harness_run_total counter')"
message: "missing harness run counter"
- assert:
expr: "/^openclaw_run_completed_total\\{[^}\\n]*outcome=\"completed\"[^}\\n]*\\}\\s+[1-9]\\d*(?:\\.\\d+)?$/m.test(prometheusText)"
message: "missing positive completed-run sample"
- assert:
expr: "/^openclaw_model_call_total\\{[^}\\n]*outcome=\"completed\"[^}\\n]*\\}\\s+[1-9]\\d*(?:\\.\\d+)?$/m.test(prometheusText)"
message: "missing positive completed model-call sample"
- assert:
expr: "/^openclaw_harness_run_total\\{[^}\\n]*outcome=\"completed\"[^}\\n]*\\}\\s+[1-9]\\d*(?:\\.\\d+)?$/m.test(prometheusText)"
message: "missing positive completed harness-run sample"
- assert:
expr: 'prometheusText.includes(''openclaw_telemetry_exporter_total{exporter="diagnostics-prometheus",reason="configured",signal="metrics",status="started"} 1'')'
message: "missing diagnostics-prometheus exporter-start sample"
- assert:
expr: "!prometheusText.includes(config.secretNeedle)"
message: "prometheus output leaked prompt sentinel"