Skip to content

Commit 98e09e8

Browse files
committed
fix(test): harden Docker resource ceilings
1 parent e8643f0 commit 98e09e8

3 files changed

Lines changed: 118 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
1111
- iMessage: mark authorized slash-command turns as text-sourced commands so `/status`, `/new`, and `/restart` acknowledgements return to the source conversation. (#82642) thanks @homer-byte.
1212
- Crabbox: install Corepack shims into the writable hydration `PNPM_HOME` so local AWS runner hydration no longer tries to overwrite `/usr/local/bin/pnpm`.
1313
- Live tests: fail Gateway live model sweeps when selected coverage is lost to timeouts or stale high-signal filters instead of reporting false missing-profile coverage, and pin Docker OpenAI gateway coverage to the current `gpt-5.5` lane.
14+
- Tests: fail Docker resource-ceiling checks when stats samples or configured limits are invalid instead of silently reporting zero peaks.
1415

1516

1617
## 2026.5.24

scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,29 @@ const [statsFile, maxMemoryRaw, maxCpuRaw, label = "docker"] = process.argv.slic
44
const maxMemoryMiB = Number(maxMemoryRaw);
55
const maxCpuPercent = Number(maxCpuRaw);
66

7+
function assertFiniteLimit(value, raw, name) {
8+
if (!Number.isFinite(value) || value < 0) {
9+
throw new Error(`${name} must be a finite non-negative number. Got: ${JSON.stringify(raw)}`);
10+
}
11+
}
12+
713
function parseMemoryMiB(raw) {
814
const value =
915
String(raw || "")
1016
.split("/")[0]
1117
?.trim() || "";
1218
const match = /^([0-9.]+)\s*([KMGT]?i?B)$/iu.exec(value);
1319
if (!match) {
14-
return 0;
20+
return undefined;
1521
}
1622
const amount = Number(match[1]);
23+
if (!Number.isFinite(amount)) {
24+
return undefined;
25+
}
1726
const unit = match[2].toLowerCase();
27+
if (unit === "b") {
28+
return amount / 1024 / 1024;
29+
}
1830
if (unit === "kb" || unit === "kib") {
1931
return amount / 1024;
2032
}
@@ -27,33 +39,52 @@ function parseMemoryMiB(raw) {
2739
if (unit === "tb" || unit === "tib") {
2840
return amount * 1024 * 1024;
2941
}
30-
return 0;
42+
return undefined;
43+
}
44+
45+
function parseCpuPercent(raw) {
46+
const parsed = Number(String(raw || "").replace(/%$/u, ""));
47+
return Number.isFinite(parsed) ? parsed : undefined;
48+
}
49+
50+
function assertSampleValue(value, raw, name, label) {
51+
if (value === undefined) {
52+
throw new Error(
53+
`docker stats sample for ${label} had invalid ${name}: ${JSON.stringify(raw)}`,
54+
);
55+
}
3156
}
3257

3358
const lines = fs.existsSync(statsFile)
3459
? fs.readFileSync(statsFile, "utf8").split(/\r?\n/u).filter(Boolean)
3560
: [];
3661
let maxObservedMemoryMiB = 0;
3762
let maxObservedCpuPercent = 0;
63+
let parsedSamples = 0;
64+
65+
assertFiniteLimit(maxMemoryMiB, maxMemoryRaw, "max memory MiB");
66+
assertFiniteLimit(maxCpuPercent, maxCpuRaw, "max CPU percent");
3867

3968
for (const line of lines) {
4069
let parsed;
4170
try {
4271
parsed = JSON.parse(line);
4372
} catch {
44-
continue;
73+
throw new Error(`docker stats sample for ${label} was not valid JSON`);
4574
}
46-
maxObservedMemoryMiB = Math.max(maxObservedMemoryMiB, parseMemoryMiB(parsed.MemUsage));
47-
maxObservedCpuPercent = Math.max(
48-
maxObservedCpuPercent,
49-
Number(String(parsed.CPUPerc || "0").replace(/%$/u, "")) || 0,
50-
);
75+
const observedMemoryMiB = parseMemoryMiB(parsed.MemUsage);
76+
const observedCpuPercent = parseCpuPercent(parsed.CPUPerc);
77+
assertSampleValue(observedMemoryMiB, parsed.MemUsage, "MemUsage", label);
78+
assertSampleValue(observedCpuPercent, parsed.CPUPerc, "CPUPerc", label);
79+
parsedSamples += 1;
80+
maxObservedMemoryMiB = Math.max(maxObservedMemoryMiB, observedMemoryMiB);
81+
maxObservedCpuPercent = Math.max(maxObservedCpuPercent, observedCpuPercent);
5182
}
5283

5384
console.log(
54-
`${label} resource peak: memory=${maxObservedMemoryMiB.toFixed(1)}MiB cpu=${maxObservedCpuPercent.toFixed(1)}% samples=${lines.length}`,
85+
`${label} resource peak: memory=${maxObservedMemoryMiB.toFixed(1)}MiB cpu=${maxObservedCpuPercent.toFixed(1)}% samples=${parsedSamples}`,
5586
);
56-
if (lines.length === 0) {
87+
if (parsedSamples === 0) {
5788
throw new Error(`no docker stats samples captured for ${label}`);
5889
}
5990
if (maxObservedMemoryMiB > maxMemoryMiB) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { spawnSync } from "node:child_process";
2+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { afterEach, describe, expect, it } from "vitest";
6+
7+
const SCRIPT_PATH = "scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs";
8+
const tempRoots: string[] = [];
9+
10+
function writeStats(contents: string): string {
11+
const root = mkdtempSync(join(tmpdir(), "openclaw-docker-stats-"));
12+
tempRoots.push(root);
13+
const file = join(root, "stats.jsonl");
14+
writeFileSync(file, contents);
15+
return file;
16+
}
17+
18+
function runAssert(statsFile: string, maxMemoryMiB = "512", maxCpuPercent = "100") {
19+
return spawnSync(process.execPath, [SCRIPT_PATH, statsFile, maxMemoryMiB, maxCpuPercent, "test"], {
20+
encoding: "utf8",
21+
});
22+
}
23+
24+
afterEach(() => {
25+
for (const root of tempRoots.splice(0)) {
26+
rmSync(root, { force: true, recursive: true });
27+
}
28+
});
29+
30+
describe("scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs", () => {
31+
it("fails when the stats log contains no parseable samples", () => {
32+
const result = runAssert(writeStats("not-json\n"));
33+
34+
expect(result.status).not.toBe(0);
35+
expect(result.stderr).toContain("was not valid JSON");
36+
});
37+
38+
it("rejects invalid resource limits instead of disabling the ceiling", () => {
39+
const result = runAssert(writeStats('{"MemUsage":"128MiB / 2GiB","CPUPerc":"25.0%"}\n'), "nope");
40+
41+
expect(result.status).not.toBe(0);
42+
expect(result.stderr).toContain("max memory MiB must be a finite non-negative number");
43+
});
44+
45+
it("rejects JSON samples without parseable Docker resource fields", () => {
46+
const missing = runAssert(writeStats("{}\n"));
47+
48+
expect(missing.status).not.toBe(0);
49+
expect(missing.stderr).toContain("had invalid MemUsage");
50+
51+
const malformed = runAssert(writeStats('{"MemUsage":"bad","CPUPerc":"bad"}\n'));
52+
53+
expect(malformed.status).not.toBe(0);
54+
expect(malformed.stderr).toContain("had invalid MemUsage");
55+
});
56+
57+
it("reports and enforces parsed Docker resource peaks", () => {
58+
const result = runAssert(
59+
writeStats('{"MemUsage":"128MiB / 2GiB","CPUPerc":"25.0%"}\n'),
60+
"256",
61+
"50",
62+
);
63+
64+
expect(result.status).toBe(0);
65+
expect(result.stdout).toContain("memory=128.0MiB");
66+
expect(result.stdout).toContain("cpu=25.0%");
67+
expect(result.stdout).toContain("samples=1");
68+
});
69+
70+
it("accepts byte-unit Docker memory samples", () => {
71+
const result = runAssert(writeStats('{"MemUsage":"512B / 2GiB","CPUPerc":"0.5%"}\n'));
72+
73+
expect(result.status).toBe(0);
74+
expect(result.stdout).toContain("samples=1");
75+
});
76+
});

0 commit comments

Comments
 (0)