Files
openclaw/extensions/diagnostics-prometheus/src/install-runtime.e2e.test.ts
Peter Steinberger 8616c0c374 refactor: finish shared test helper migrations (#120996)
* test: finish shared helper migrations

* test: fix helper migration CI

* style: fix test import ordering

* test(acpx): restore deferred void types

* test: fix helper migrations after rebase
2026-08-09 06:00:06 -07:00

378 lines
12 KiB
TypeScript

// 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 path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { promisify } from "node:util";
import {
resolvePreferredOpenClawTmpDir,
tempWorkspace,
type TempWorkspace,
} from "openclaw/plugin-sdk/temp-path";
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 tempWorkspaces: TempWorkspace[] = [];
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(tempWorkspaces.splice(0).map((workspace) => workspace.cleanup()));
});
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 workspace = await tempWorkspace({
rootDir: resolvePreferredOpenClawTmpDir(),
prefix: "openclaw-prometheus-install-",
});
tempWorkspaces.push(workspace);
const root = workspace.dir;
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);
});