From f2436b9df8528ad5342e69d91a7f2913891bdb10 Mon Sep 17 00:00:00 2001 From: dreamwing Date: Fri, 13 Mar 2026 22:08:04 +0800 Subject: [PATCH 1/9] fix(docker): implement IS_DOCKER detection and disable host executions - Detect Docker context via /.dockerenv - Skip openclaw CLI presence/version checks in Docker - Suppress gateway process scanning (ps/pgrep) in monitor to prevent log pollution - Gracefully disable /api/kill, /api/gateway/restart, and /api/run/:id endpoints with clear error responses (Fixes #24 host execution issue) --- src/config.js | 4 +++- src/routes/cron.js | 9 +++++++-- src/routes/process.js | 7 +++++++ src/services/monitor.js | 34 +++++++++++++++++++++------------- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/config.js b/src/config.js index 9b4f5db..c14f586 100644 --- a/src/config.js +++ b/src/config.js @@ -25,7 +25,8 @@ const TOKEN_FILE = path.join(APP_DIR, 'data/token_stats/latest.json'); const ID_FILE = path.join(APP_DIR, 'data/last_id.txt'); const ANALYZE_SCRIPT = path.join(APP_DIR, 'scripts/analyze.js'); -// --- Constants --- +// --- Constants & Environment Flags --- +const IS_DOCKER = fs.existsSync('/.dockerenv') || process.env.IS_DOCKER === 'true'; const CACHE_TTL_MS = 60000; // 60s cache module.exports = { @@ -40,4 +41,5 @@ module.exports = { ID_FILE, ANALYZE_SCRIPT, CACHE_TTL_MS, + IS_DOCKER, }; diff --git a/src/routes/cron.js b/src/routes/cron.js index 46e580c..36a6dbd 100644 --- a/src/routes/cron.js +++ b/src/routes/cron.js @@ -5,7 +5,7 @@ const router = require('express').Router(); const { exec, execFile } = require('child_process'); const path = require('path'); const fs = require('fs'); -const { HOME_DIR, STATE_DIR } = require('../config'); +const { HOME_DIR, STATE_DIR, IS_DOCKER } = require('../config'); const { getOpenClawCommand } = require('../services/openclaw'); router.get('/api/cron', (req, res) => { @@ -25,7 +25,9 @@ router.get('/api/cron', (req, res) => { // Silent fail, fallthrough to CLI } - // 2. SLOW PATH: CLI Fallback + // 2. SLOW PATH: CLI Fallback (n/a in Docker) + if (IS_DOCKER) return res.json([]); + const cmd = `${getOpenClawCommand()} cron list --json`; exec(cmd, { maxBuffer: 1024 * 1024 * 5 }, (err, stdout, stderr) => { try { @@ -43,6 +45,9 @@ router.post('/api/run/:id', (req, res) => { if (!/^[a-zA-Z0-9_-]+$/.test(id)) { return res.status(400).json({ error: 'Invalid job ID format' }); } + if (IS_DOCKER) { + return res.status(403).json({ error: 'Running cron jobs is not supported in Docker Mode. Please interact with the host CLI directly.' }); + } const openclawCmd = getOpenClawCommand(); execFile(openclawCmd, ['cron', 'run', id], (err, stdout, stderr) => { if (err) { diff --git a/src/routes/process.js b/src/routes/process.js index e47b3aa..dca48c5 100644 --- a/src/routes/process.js +++ b/src/routes/process.js @@ -6,6 +6,7 @@ const { exec } = require('child_process'); const path = require('path'); const { rateLimit } = require('../utils/rateLimit'); const { getOpenClawCommand, WORKSPACE_DIR } = require('../services/openclaw'); +const { IS_DOCKER } = require('../config'); router.post('/api/kill', (req, res) => { if (req.body?.confirm !== true) { @@ -14,6 +15,9 @@ router.post('/api/kill', (req, res) => { if (!rateLimit('kill', 5000)) { return res.status(429).json({ error: 'Please wait before retrying.' }); } + if (IS_DOCKER) { + return res.status(403).json({ error: 'Process termination is not supported in Docker Container Mode. Please restart the container or run host commands directly.' }); + } const openclawDir = path.resolve(WORKSPACE_DIR); const escapedDir = openclawDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -65,6 +69,9 @@ router.post('/api/gateway/restart', (req, res) => { if (!rateLimit('gateway_restart', 10000)) { return res.status(429).json({ error: 'Please wait at least 10 seconds before retrying.' }); } + if (IS_DOCKER) { + return res.status(403).json({ error: 'Gateway process restarts are not supported inside Docker containers. To restart the gateway, please restart the Docker container.' }); + } console.log(`[Gateway] Restart requested by ${req.ip} at ${new Date().toISOString()}`); exec("pkill -SIGTERM -f 'openclaw gateway' || true", (killErr, killStdout, killStderr) => { diff --git a/src/services/monitor.js b/src/services/monitor.js index e65f100..2ed4eb6 100644 --- a/src/services/monitor.js +++ b/src/services/monitor.js @@ -5,7 +5,7 @@ const { exec, execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); -const { ID_FILE, APP_DIR } = require('../config'); +const { ID_FILE, APP_DIR, IS_DOCKER } = require('../config'); const { getOpenClawCommand } = require('./openclaw'); const { getActiveContext } = require('./context'); const { logActivity, checkFileChanges } = require('./activity'); @@ -43,20 +43,25 @@ function getVersions() { console.warn('[Versions] Failed to read dashboard package.json:', e.message); } - try { - const cmd = `${getOpenClawCommand()} --version`; - core = execSync(cmd, { timeout: 5000 }).toString().trim(); - } catch (e) { + if (IS_DOCKER) { + core = 'Managed by Docker'; + } else { try { - const nodeBinDir = path.dirname(process.execPath); - const globalModulesPath = path.join(nodeBinDir, '../lib/node_modules/openclaw/package.json'); - const pkg = JSON.parse(fs.readFileSync(globalModulesPath, 'utf8')); - core = `v${pkg.version}`; - } catch (e2) { - console.warn('[Versions] OpenClaw version detection failed:', e2.message); - core = 'Unknown'; + const cmd = `${getOpenClawCommand()} --version`; + core = execSync(cmd, { timeout: 5000 }).toString().trim(); + } catch (e) { + try { + const nodeBinDir = path.dirname(process.execPath); + const globalModulesPath = path.join(nodeBinDir, '../lib/node_modules/openclaw/package.json'); + const pkg = JSON.parse(fs.readFileSync(globalModulesPath, 'utf8')); + core = `v${pkg.version}`; + } catch (e2) { + console.warn('[Versions] OpenClaw version detection failed:', e2.message); + core = 'Unknown'; + } } } + cachedVersions = { dashboard, core }; cachedVersionsTs = Date.now(); return cachedVersions; @@ -68,7 +73,10 @@ function checkSystemStatus(callback) { const osType = os.type(); let mergedCmd = ''; - if (osType === 'Darwin') { + if (IS_DOCKER) { + // In Docker, we can't reliably see host processes or the gateway PID. + mergedCmd = `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; echo ""; echo "===PS==="; echo ""`; + } else if (osType === 'Darwin') { mergedCmd = `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; pgrep -f '[o]penclaw.*gateway' | head -n 1 || true; echo "===PS==="; ps -Ao pid,pcpu,comm,args -r | head -n 21`; } else { mergedCmd = `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; pgrep -f '[o]penclaw.*gateway' | head -n 1 || true; echo "===PS==="; ps -eo pid,pcpu,comm,args --sort=-pcpu | head -n 20`; From bb996049ae8360bab5097fa16d17ba13381faa30 Mon Sep 17 00:00:00 2001 From: dreamwing Date: Fri, 13 Mar 2026 22:34:44 +0800 Subject: [PATCH 2/9] Update src/routes/process.js Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/process.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/process.js b/src/routes/process.js index dca48c5..64d5862 100644 --- a/src/routes/process.js +++ b/src/routes/process.js @@ -12,12 +12,12 @@ router.post('/api/kill', (req, res) => { if (req.body?.confirm !== true) { return res.status(400).json({ error: 'Confirmation required. Send { "confirm": true } in request body.' }); } - if (!rateLimit('kill', 5000)) { - return res.status(429).json({ error: 'Please wait before retrying.' }); - } if (IS_DOCKER) { return res.status(403).json({ error: 'Process termination is not supported in Docker Container Mode. Please restart the container or run host commands directly.' }); } + if (!rateLimit('kill', 5000)) { + return res.status(429).json({ error: 'Please wait before retrying.' }); + } const openclawDir = path.resolve(WORKSPACE_DIR); const escapedDir = openclawDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); From 489729950f17519943d7f9e85c608740fb422745 Mon Sep 17 00:00:00 2001 From: dreamwing Date: Fri, 13 Mar 2026 22:34:53 +0800 Subject: [PATCH 3/9] Update src/routes/process.js Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/process.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/process.js b/src/routes/process.js index 64d5862..08c3275 100644 --- a/src/routes/process.js +++ b/src/routes/process.js @@ -66,12 +66,12 @@ router.post('/api/kill', (req, res) => { }); router.post('/api/gateway/restart', (req, res) => { - if (!rateLimit('gateway_restart', 10000)) { - return res.status(429).json({ error: 'Please wait at least 10 seconds before retrying.' }); - } if (IS_DOCKER) { return res.status(403).json({ error: 'Gateway process restarts are not supported inside Docker containers. To restart the gateway, please restart the Docker container.' }); } + if (!rateLimit('gateway_restart', 10000)) { + return res.status(429).json({ error: 'Please wait at least 10 seconds before retrying.' }); + } console.log(`[Gateway] Restart requested by ${req.ip} at ${new Date().toISOString()}`); exec("pkill -SIGTERM -f 'openclaw gateway' || true", (killErr, killStdout, killStderr) => { From 1415bd7bba03603116ed65d4419d445ff043e67b Mon Sep 17 00:00:00 2001 From: dreamwing Date: Fri, 13 Mar 2026 23:16:33 +0800 Subject: [PATCH 4/9] fix(docker): clarify unsupported dashboard states --- public/index.html | 4 +- public/js/dashboard.js | 49 +++++++++-- src/services/monitor.js | 14 ++- tests/routes.docker-mode.test.js | 146 +++++++++++++++++++++++++++++++ tests/services.monitor.test.js | 94 ++++++++++++++++++++ 5 files changed, 293 insertions(+), 14 deletions(-) create mode 100644 tests/routes.docker-mode.test.js create mode 100644 tests/services.monitor.test.js diff --git a/public/index.html b/public/index.html index bdfdeb0..09e5490 100644 --- a/public/index.html +++ b/public/index.html @@ -378,7 +378,7 @@

About

Environment - Node.js (Systemd) + Node.js (Systemd)
Server Timezone @@ -451,4 +451,4 @@

About

- \ No newline at end of file + diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 2223904..0ebda58 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -34,6 +34,23 @@ return res; } + function isUnsupportedMetric(data, key) { + return Array.isArray(data.unsupportedMonitoring) && data.unsupportedMonitoring.includes(key); + } + + async function readErrorMessage(res, fallbackMessage) { + try { + const data = await res.json(); + if (data && typeof data.error === 'string' && data.error.trim()) return data.error; + if (data && typeof data.message === 'string' && data.message.trim()) return data.message; + } catch (e) { } + return fallbackMessage; + } + + function setMetricValue(id, value, fallback = 'N/A') { + document.getElementById(id).innerText = value == null ? fallback : value; + } + // --- TAB MANAGEMENT --- let currentTab = 'home'; let cronInterval = null; @@ -169,17 +186,22 @@ const res = await fetchAuth(API + '/status'); const data = await res.json(); - document.getElementById('cpu-val').innerText = data.cpu + '%'; - document.getElementById('mem-val').innerText = data.mem + '%'; + setMetricValue('cpu-val', isUnsupportedMetric(data, 'cpu') ? 'N/A' : (data.cpu == null ? '--%' : data.cpu + '%')); + setMetricValue('mem-val', isUnsupportedMetric(data, 'mem') ? 'N/A' : (data.mem == null ? '--%' : data.mem + '%')); if (data.disk) document.getElementById('disk-val').innerText = data.disk; if (data.timezone) document.getElementById('server-tz').innerText = data.timezone; + if (data.environment && typeof data.environment.isDocker === 'boolean') { + document.getElementById('runtime-env').innerText = data.environment.isDocker ? 'Docker Mode' : 'Node.js (Systemd)'; + } if (data.versions) { document.getElementById('ver-core').innerText = data.versions.core; document.getElementById('ver-num').innerText = 'v' + data.versions.dashboard; } // Update PID - if (data.gatewayPid) { + if (isUnsupportedMetric(data, 'gatewayPid')) { + document.getElementById('gateway-pid').innerText = 'N/A in Docker Mode'; + } else if (data.gatewayPid) { document.getElementById('gateway-pid').innerText = data.gatewayPid; } else { document.getElementById('gateway-pid').innerText = 'Stopped / Not Found'; @@ -187,7 +209,9 @@ // Update Scripts List const scriptList = document.getElementById('running-scripts-list'); - if (data.scripts && data.scripts.length > 0) { + if (isUnsupportedMetric(data, 'scripts')) { + scriptList.innerHTML = '
Unavailable in Docker Mode
'; + } else if (data.scripts && data.scripts.length > 0) { const items = data.scripts.map(s => `
${s.name} @@ -406,18 +430,28 @@ async function runJob(id) { if (!confirm('Execute task?')) return; - await fetchAuth(API + '/run/' + id, { method: 'POST' }); + const res = await fetchAuth(API + '/run/' + id, { method: 'POST' }); + if (!res.ok) { + alert(await readErrorMessage(res, 'Failed to run job.')); + return; + } setTimeout(fetchJobs, 2000); } async function killAll() { if (!confirm('⚠️ STOP ALL SCRIPTS?')) return; - await fetchAuth(API + '/kill', { method: 'POST' }); + const res = await fetchAuth(API + '/kill', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: true }) }); + if (!res.ok) { + alert(await readErrorMessage(res, 'Failed to stop scripts.')); + } } async function restartGateway() { if (!confirm('♻️ RESTART GATEWAY?')) return; - await fetchAuth(API + '/gateway/restart', { method: 'POST' }); + const res = await fetchAuth(API + '/gateway/restart', { method: 'POST' }); + if (!res.ok) { + alert(await readErrorMessage(res, 'Failed to restart gateway.')); + } } async function refreshTokenStats() { @@ -674,4 +708,3 @@ } return 0; } - diff --git a/src/services/monitor.js b/src/services/monitor.js index 2ed4eb6..3dfba13 100644 --- a/src/services/monitor.js +++ b/src/services/monitor.js @@ -27,6 +27,7 @@ try { let cachedVersions = null; let cachedVersionsTs = 0; const VERSION_CACHE_TTL = 5 * 60 * 1000; +const DOCKER_UNSUPPORTED_MONITORING = ['cpu', 'mem', 'gatewayPid', 'scripts']; function getVersions() { if (cachedVersions && Date.now() - cachedVersionsTs < VERSION_CACHE_TTL) { @@ -174,18 +175,23 @@ function checkSystemStatus(callback) { } const versions = getVersions(); + const unsupportedMonitoring = IS_DOCKER ? [...DOCKER_UNSUPPORTED_MONITORING] : []; callback({ status: status, task: taskText, - cpu: Math.round(totalCpu), - mem: Math.round((1 - os.freemem() / os.totalmem()) * 100), + cpu: IS_DOCKER ? null : Math.round(totalCpu), + mem: IS_DOCKER ? null : Math.round((1 - os.freemem() / os.totalmem()) * 100), disk: diskUsage, timezone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', lastHeartbeat: new Date().toISOString(), versions: versions, - gatewayPid: gatewayPid, - scripts: runningScripts, + gatewayPid: IS_DOCKER ? null : gatewayPid, + scripts: IS_DOCKER ? [] : runningScripts, + environment: { + isDocker: IS_DOCKER, + }, + unsupportedMonitoring, }); } } diff --git a/tests/routes.docker-mode.test.js b/tests/routes.docker-mode.test.js new file mode 100644 index 0000000..5939e86 --- /dev/null +++ b/tests/routes.docker-mode.test.js @@ -0,0 +1,146 @@ +/* eslint-env jest */ +'use strict'; + +function invokeRouteHandler(router, method, routePath, reqOverrides = {}) { + const layer = router.stack.find(entry => entry.route && entry.route.path === routePath); + if (!layer) throw new Error(`Route not found: ${routePath}`); + + const routeLayer = layer.route.stack.find(entry => entry.method === method); + if (!routeLayer) throw new Error(`Method not found: ${method.toUpperCase()} ${routePath}`); + + const req = { + body: {}, + params: {}, + ip: '127.0.0.1', + ...reqOverrides, + }; + const res = { + statusCode: 200, + body: undefined, + status(code) { + this.statusCode = code; + return this; + }, + json(payload) { + this.body = payload; + return this; + }, + }; + + routeLayer.handle(req, res); + return res; +} + +describe('Docker-mode route restrictions', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + test('GET /api/cron returns an empty list without CLI fallback in Docker mode', () => { + const exec = jest.fn(); + + jest.doMock('../src/config', () => ({ + HOME_DIR: '/tmp', + STATE_DIR: '/tmp/.openclaw', + IS_DOCKER: true, + })); + jest.doMock('fs', () => ({ + existsSync: jest.fn(() => false), + readFileSync: jest.fn(), + })); + jest.doMock('child_process', () => ({ + exec, + execFile: jest.fn(), + })); + jest.doMock('../src/services/openclaw', () => ({ + getOpenClawCommand: jest.fn(() => 'openclaw'), + })); + + const router = require('../src/routes/cron'); + const res = invokeRouteHandler(router, 'get', '/api/cron'); + + expect(res.statusCode).toBe(200); + expect(res.body).toEqual([]); + expect(exec).not.toHaveBeenCalled(); + }); + + test('POST /api/run/:id is blocked in Docker mode', () => { + jest.doMock('../src/config', () => ({ + HOME_DIR: '/tmp', + STATE_DIR: '/tmp/.openclaw', + IS_DOCKER: true, + })); + jest.doMock('fs', () => ({ + existsSync: jest.fn(() => false), + readFileSync: jest.fn(), + })); + jest.doMock('child_process', () => ({ + exec: jest.fn(), + execFile: jest.fn(), + })); + jest.doMock('../src/services/openclaw', () => ({ + getOpenClawCommand: jest.fn(() => 'openclaw'), + })); + + const router = require('../src/routes/cron'); + const res = invokeRouteHandler(router, 'post', '/api/run/:id', { + params: { id: 'daily-job' }, + }); + + expect(res.statusCode).toBe(403); + expect(res.body).toEqual({ + error: 'Running cron jobs is not supported in Docker Mode. Please interact with the host CLI directly.', + }); + }); + + test('POST /api/kill is blocked in Docker mode after confirmation passes', () => { + jest.doMock('../src/config', () => ({ + IS_DOCKER: true, + })); + jest.doMock('../src/utils/rateLimit', () => ({ + rateLimit: jest.fn(() => true), + })); + jest.doMock('../src/services/openclaw', () => ({ + getOpenClawCommand: jest.fn(() => 'openclaw'), + WORKSPACE_DIR: '/tmp/workspace', + })); + jest.doMock('child_process', () => ({ + exec: jest.fn(), + })); + + const router = require('../src/routes/process'); + const res = invokeRouteHandler(router, 'post', '/api/kill', { + body: { confirm: true }, + }); + + expect(res.statusCode).toBe(403); + expect(res.body).toEqual({ + error: 'Process termination is not supported in Docker Container Mode. Please restart the container or run host commands directly.', + }); + }); + + test('POST /api/gateway/restart is blocked in Docker mode', () => { + jest.doMock('../src/config', () => ({ + IS_DOCKER: true, + })); + jest.doMock('../src/utils/rateLimit', () => ({ + rateLimit: jest.fn(() => true), + })); + jest.doMock('../src/services/openclaw', () => ({ + getOpenClawCommand: jest.fn(() => 'openclaw'), + WORKSPACE_DIR: '/tmp/workspace', + })); + jest.doMock('child_process', () => ({ + exec: jest.fn(), + })); + + const router = require('../src/routes/process'); + const res = invokeRouteHandler(router, 'post', '/api/gateway/restart'); + + expect(res.statusCode).toBe(403); + expect(res.body).toEqual({ + error: 'Gateway process restarts are not supported inside Docker containers. To restart the gateway, please restart the Docker container.', + }); + }); +}); diff --git a/tests/services.monitor.test.js b/tests/services.monitor.test.js new file mode 100644 index 0000000..3a31947 --- /dev/null +++ b/tests/services.monitor.test.js @@ -0,0 +1,94 @@ +/* eslint-env jest */ +'use strict'; + +describe('monitor service in Docker mode', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + test('getVersions reports Docker-managed core without invoking openclaw CLI', () => { + const execSync = jest.fn(); + + jest.doMock('child_process', () => ({ + exec: jest.fn(), + execSync, + })); + jest.doMock('../src/config', () => ({ + ID_FILE: '/tmp/last_id.txt', + APP_DIR: '/app', + IS_DOCKER: true, + })); + jest.doMock('fs', () => ({ + existsSync: jest.fn(() => false), + readFileSync: jest.fn((targetPath) => { + if (targetPath === '/app/package.json') return JSON.stringify({ version: '1.2.3' }); + throw new Error(`Unexpected read: ${targetPath}`); + }), + writeFileSync: jest.fn(), + })); + jest.doMock('../src/services/openclaw', () => ({ + getOpenClawCommand: jest.fn(() => 'openclaw'), + })); + jest.doMock('../src/services/context', () => ({ + getActiveContext: jest.fn(() => null), + })); + jest.doMock('../src/services/activity', () => ({ + logActivity: jest.fn(), + checkFileChanges: jest.fn(), + })); + + const { getVersions } = require('../src/services/monitor'); + + expect(getVersions()).toEqual({ dashboard: '1.2.3', core: 'Managed by Docker' }); + expect(execSync).not.toHaveBeenCalled(); + }); + + test('checkSystemStatus marks host-only metrics as unsupported in Docker mode', done => { + const exec = jest.fn((cmd, callback) => { + callback(null, '===DISK===\n42%\n===GWPID===\n\n===PS===\n\n'); + }); + + jest.doMock('child_process', () => ({ + exec, + execSync: jest.fn(), + })); + jest.doMock('../src/config', () => ({ + ID_FILE: '/tmp/last_id.txt', + APP_DIR: '/app', + IS_DOCKER: true, + })); + jest.doMock('fs', () => ({ + existsSync: jest.fn(() => false), + readFileSync: jest.fn((targetPath) => { + if (targetPath === '/app/package.json') return JSON.stringify({ version: '1.2.3' }); + throw new Error(`Unexpected read: ${targetPath}`); + }), + writeFileSync: jest.fn(), + })); + jest.doMock('../src/services/openclaw', () => ({ + getOpenClawCommand: jest.fn(() => 'openclaw'), + })); + jest.doMock('../src/services/context', () => ({ + getActiveContext: jest.fn(() => null), + })); + jest.doMock('../src/services/activity', () => ({ + logActivity: jest.fn(), + checkFileChanges: jest.fn(), + })); + + const { checkSystemStatus } = require('../src/services/monitor'); + + checkSystemStatus(data => { + expect(exec).toHaveBeenCalled(); + expect(data.disk).toBe('42%'); + expect(data.cpu).toBeNull(); + expect(data.mem).toBeNull(); + expect(data.gatewayPid).toBeNull(); + expect(data.scripts).toEqual([]); + expect(data.environment).toEqual({ isDocker: true }); + expect(data.unsupportedMonitoring).toEqual(['cpu', 'mem', 'gatewayPid', 'scripts']); + done(); + }); + }); +}); From e3f721c293f2be971281663394a9e1c699e36c34 Mon Sep 17 00:00:00 2001 From: dreamwing Date: Sat, 14 Mar 2026 00:12:00 +0800 Subject: [PATCH 5/9] fix(docker): align unsupported monitor fields --- public/js/dashboard.js | 2 +- src/services/monitor.js | 6 +++--- tests/services.monitor.test.js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 0ebda58..e04314f 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -188,7 +188,7 @@ setMetricValue('cpu-val', isUnsupportedMetric(data, 'cpu') ? 'N/A' : (data.cpu == null ? '--%' : data.cpu + '%')); setMetricValue('mem-val', isUnsupportedMetric(data, 'mem') ? 'N/A' : (data.mem == null ? '--%' : data.mem + '%')); - if (data.disk) document.getElementById('disk-val').innerText = data.disk; + setMetricValue('disk-val', isUnsupportedMetric(data, 'disk') ? 'N/A' : (data.disk == null ? '--%' : data.disk)); if (data.timezone) document.getElementById('server-tz').innerText = data.timezone; if (data.environment && typeof data.environment.isDocker === 'boolean') { document.getElementById('runtime-env').innerText = data.environment.isDocker ? 'Docker Mode' : 'Node.js (Systemd)'; diff --git a/src/services/monitor.js b/src/services/monitor.js index 3dfba13..c622715 100644 --- a/src/services/monitor.js +++ b/src/services/monitor.js @@ -27,7 +27,7 @@ try { let cachedVersions = null; let cachedVersionsTs = 0; const VERSION_CACHE_TTL = 5 * 60 * 1000; -const DOCKER_UNSUPPORTED_MONITORING = ['cpu', 'mem', 'gatewayPid', 'scripts']; +const DOCKER_UNSUPPORTED_MONITORING = ['cpu', 'mem', 'disk', 'gatewayPid', 'scripts']; function getVersions() { if (cachedVersions && Date.now() - cachedVersionsTs < VERSION_CACHE_TTL) { @@ -182,12 +182,12 @@ function checkSystemStatus(callback) { task: taskText, cpu: IS_DOCKER ? null : Math.round(totalCpu), mem: IS_DOCKER ? null : Math.round((1 - os.freemem() / os.totalmem()) * 100), - disk: diskUsage, + disk: IS_DOCKER ? null : diskUsage, timezone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', lastHeartbeat: new Date().toISOString(), versions: versions, gatewayPid: IS_DOCKER ? null : gatewayPid, - scripts: IS_DOCKER ? [] : runningScripts, + scripts: IS_DOCKER ? null : runningScripts, environment: { isDocker: IS_DOCKER, }, diff --git a/tests/services.monitor.test.js b/tests/services.monitor.test.js index 3a31947..cba3b2b 100644 --- a/tests/services.monitor.test.js +++ b/tests/services.monitor.test.js @@ -81,13 +81,13 @@ describe('monitor service in Docker mode', () => { checkSystemStatus(data => { expect(exec).toHaveBeenCalled(); - expect(data.disk).toBe('42%'); + expect(data.disk).toBeNull(); expect(data.cpu).toBeNull(); expect(data.mem).toBeNull(); expect(data.gatewayPid).toBeNull(); - expect(data.scripts).toEqual([]); + expect(data.scripts).toBeNull(); expect(data.environment).toEqual({ isDocker: true }); - expect(data.unsupportedMonitoring).toEqual(['cpu', 'mem', 'gatewayPid', 'scripts']); + expect(data.unsupportedMonitoring).toEqual(['cpu', 'mem', 'disk', 'gatewayPid', 'scripts']); done(); }); }); From b73fcc6cda2516c59811929363a523d728fd484a Mon Sep 17 00:00:00 2001 From: dreamwing Date: Sat, 14 Mar 2026 00:36:13 +0800 Subject: [PATCH 6/9] fix(docker): tighten restricted endpoint handling --- src/routes/process.js | 6 +++--- src/services/monitor.js | 4 ++-- tests/routes.docker-mode.test.js | 6 ++---- tests/services.monitor.test.js | 1 + 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/routes/process.js b/src/routes/process.js index 08c3275..65028a6 100644 --- a/src/routes/process.js +++ b/src/routes/process.js @@ -9,12 +9,12 @@ const { getOpenClawCommand, WORKSPACE_DIR } = require('../services/openclaw'); const { IS_DOCKER } = require('../config'); router.post('/api/kill', (req, res) => { - if (req.body?.confirm !== true) { - return res.status(400).json({ error: 'Confirmation required. Send { "confirm": true } in request body.' }); - } if (IS_DOCKER) { return res.status(403).json({ error: 'Process termination is not supported in Docker Container Mode. Please restart the container or run host commands directly.' }); } + if (req.body?.confirm !== true) { + return res.status(400).json({ error: 'Confirmation required. Send { "confirm": true } in request body.' }); + } if (!rateLimit('kill', 5000)) { return res.status(429).json({ error: 'Please wait before retrying.' }); } diff --git a/src/services/monitor.js b/src/services/monitor.js index c622715..7af93ff 100644 --- a/src/services/monitor.js +++ b/src/services/monitor.js @@ -75,8 +75,8 @@ function checkSystemStatus(callback) { let mergedCmd = ''; if (IS_DOCKER) { - // In Docker, we can't reliably see host processes or the gateway PID. - mergedCmd = `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; echo ""; echo "===PS==="; echo ""`; + // In Docker, host-scoped process and disk metrics are unsupported. + mergedCmd = `echo "===DISK==="; echo ""; echo "===GWPID==="; echo ""; echo "===PS==="; echo ""`; } else if (osType === 'Darwin') { mergedCmd = `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; pgrep -f '[o]penclaw.*gateway' | head -n 1 || true; echo "===PS==="; ps -Ao pid,pcpu,comm,args -r | head -n 21`; } else { diff --git a/tests/routes.docker-mode.test.js b/tests/routes.docker-mode.test.js index 5939e86..6732ad3 100644 --- a/tests/routes.docker-mode.test.js +++ b/tests/routes.docker-mode.test.js @@ -94,7 +94,7 @@ describe('Docker-mode route restrictions', () => { }); }); - test('POST /api/kill is blocked in Docker mode after confirmation passes', () => { + test('POST /api/kill is blocked in Docker mode before confirmation validation', () => { jest.doMock('../src/config', () => ({ IS_DOCKER: true, })); @@ -110,9 +110,7 @@ describe('Docker-mode route restrictions', () => { })); const router = require('../src/routes/process'); - const res = invokeRouteHandler(router, 'post', '/api/kill', { - body: { confirm: true }, - }); + const res = invokeRouteHandler(router, 'post', '/api/kill'); expect(res.statusCode).toBe(403); expect(res.body).toEqual({ diff --git a/tests/services.monitor.test.js b/tests/services.monitor.test.js index cba3b2b..6263780 100644 --- a/tests/services.monitor.test.js +++ b/tests/services.monitor.test.js @@ -81,6 +81,7 @@ describe('monitor service in Docker mode', () => { checkSystemStatus(data => { expect(exec).toHaveBeenCalled(); + expect(exec.mock.calls[0][0]).not.toContain('df -h /'); expect(data.disk).toBeNull(); expect(data.cpu).toBeNull(); expect(data.mem).toBeNull(); From f30a44cd141c0050c55157eb6f0525f344c61299 Mon Sep 17 00:00:00 2001 From: dreamwing Date: Sat, 14 Mar 2026 00:45:45 +0800 Subject: [PATCH 7/9] Update src/config.js Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index c14f586..7523dfb 100644 --- a/src/config.js +++ b/src/config.js @@ -26,7 +26,7 @@ const ID_FILE = path.join(APP_DIR, 'data/last_id.txt'); const ANALYZE_SCRIPT = path.join(APP_DIR, 'scripts/analyze.js'); // --- Constants & Environment Flags --- -const IS_DOCKER = fs.existsSync('/.dockerenv') || process.env.IS_DOCKER === 'true'; +const IS_DOCKER = fs.existsSync('/.dockerenv') || ['true', '1', 'yes'].includes((process.env.IS_DOCKER || '').toLowerCase()); const CACHE_TTL_MS = 60000; // 60s cache module.exports = { From fd68d7bf89e74bb2fdbe0c9f46fac2b5c79dac5d Mon Sep 17 00:00:00 2001 From: dreamwing Date: Sat, 14 Mar 2026 00:53:10 +0800 Subject: [PATCH 8/9] fix(docker): expose cron unavailability explicitly --- public/js/dashboard.js | 8 +++++++- src/routes/cron.js | 4 ++-- tests/routes.docker-mode.test.js | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/public/js/dashboard.js b/public/js/dashboard.js index e04314f..4a1ea37 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -363,12 +363,18 @@ async function fetchJobs() { try { const res = await fetchAuth(API + '/cron'); - const jobs = await res.json(); + const data = await res.json(); + const jobs = Array.isArray(data) ? data : (Array.isArray(data.jobs) ? data.jobs : []); jobs.sort((a, b) => (b.state?.lastRunAtMs || 0) - (a.state?.lastRunAtMs || 0)); const container = document.getElementById('job-list'); container.innerHTML = ''; + if (data && data.dockerMode === true) { + container.innerHTML = '
Unavailable in Docker Mode
'; + return; + } + if (jobs.length === 0) { container.innerHTML = '
No jobs found
'; return; diff --git a/src/routes/cron.js b/src/routes/cron.js index 36a6dbd..8a4ec1c 100644 --- a/src/routes/cron.js +++ b/src/routes/cron.js @@ -26,10 +26,10 @@ router.get('/api/cron', (req, res) => { } // 2. SLOW PATH: CLI Fallback (n/a in Docker) - if (IS_DOCKER) return res.json([]); + if (IS_DOCKER) return res.json({ dockerMode: true, jobs: [] }); const cmd = `${getOpenClawCommand()} cron list --json`; - exec(cmd, { maxBuffer: 1024 * 1024 * 5 }, (err, stdout, stderr) => { + exec(cmd, { maxBuffer: 1024 * 1024 * 5 }, (err, stdout) => { try { const data = JSON.parse(stdout); if (data.jobs) return res.json(data.jobs); diff --git a/tests/routes.docker-mode.test.js b/tests/routes.docker-mode.test.js index 6732ad3..f4bd80d 100644 --- a/tests/routes.docker-mode.test.js +++ b/tests/routes.docker-mode.test.js @@ -37,7 +37,7 @@ describe('Docker-mode route restrictions', () => { jest.clearAllMocks(); }); - test('GET /api/cron returns an empty list without CLI fallback in Docker mode', () => { + test('GET /api/cron returns a Docker-mode marker without CLI fallback', () => { const exec = jest.fn(); jest.doMock('../src/config', () => ({ @@ -61,7 +61,7 @@ describe('Docker-mode route restrictions', () => { const res = invokeRouteHandler(router, 'get', '/api/cron'); expect(res.statusCode).toBe(200); - expect(res.body).toEqual([]); + expect(res.body).toEqual({ dockerMode: true, jobs: [] }); expect(exec).not.toHaveBeenCalled(); }); From db7c4d69c005d4f465acf5a0174e85147dd45983 Mon Sep 17 00:00:00 2001 From: dreamwing Date: Sat, 14 Mar 2026 01:12:55 +0800 Subject: [PATCH 9/9] fix(docker): short-circuit unsupported runtime paths --- src/routes/cron.js | 6 +- src/services/monitor.js | 232 +++++++++++++++---------------- tests/routes.docker-mode.test.js | 4 +- tests/services.monitor.test.js | 7 +- 4 files changed, 121 insertions(+), 128 deletions(-) diff --git a/src/routes/cron.js b/src/routes/cron.js index 8a4ec1c..2229fe1 100644 --- a/src/routes/cron.js +++ b/src/routes/cron.js @@ -42,12 +42,12 @@ router.get('/api/cron', (req, res) => { router.post('/api/run/:id', (req, res) => { const id = req.params.id; - if (!/^[a-zA-Z0-9_-]+$/.test(id)) { - return res.status(400).json({ error: 'Invalid job ID format' }); - } if (IS_DOCKER) { return res.status(403).json({ error: 'Running cron jobs is not supported in Docker Mode. Please interact with the host CLI directly.' }); } + if (!/^[a-zA-Z0-9_-]+$/.test(id)) { + return res.status(400).json({ error: 'Invalid job ID format' }); + } const openclawCmd = getOpenClawCommand(); execFile(openclawCmd, ['cron', 'run', id], (err, stdout, stderr) => { if (err) { diff --git a/src/services/monitor.js b/src/services/monitor.js index 7af93ff..bd1a3af 100644 --- a/src/services/monitor.js +++ b/src/services/monitor.js @@ -71,130 +71,126 @@ function getVersions() { function checkSystemStatus(callback) { checkFileChanges(); - const osType = os.type(); - let mergedCmd = ''; - - if (IS_DOCKER) { - // In Docker, host-scoped process and disk metrics are unsupported. - mergedCmd = `echo "===DISK==="; echo ""; echo "===GWPID==="; echo ""; echo "===PS==="; echo ""`; - } else if (osType === 'Darwin') { - mergedCmd = `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; pgrep -f '[o]penclaw.*gateway' | head -n 1 || true; echo "===PS==="; ps -Ao pid,pcpu,comm,args -r | head -n 21`; - } else { - mergedCmd = `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; pgrep -f '[o]penclaw.*gateway' | head -n 1 || true; echo "===PS==="; ps -eo pid,pcpu,comm,args --sort=-pcpu | head -n 20`; - } - - exec(mergedCmd, (err, stdout) => { - const sections = stdout ? stdout.split(/===\w+===\n?/) : []; - const diskUsage = sections[1] ? sections[1].trim() || '--%' : '--%'; - const gatewayPid = sections[2] ? sections[2].trim() || null : null; - const psOutput = sections[3] || ''; - { - { - if (err && !psOutput) return callback({ status: 'error', task: 'Monitor Error' }); - - const lines = psOutput.trim().split('\n').slice(1); - let activities = []; - let runningScripts = []; - let totalCpu = 0; - let topProc = null; - - lines.forEach((line, index) => { - const parts = line.trim().split(/\s+/); - const pid = parts[0]; - const cpu = parseFloat(parts[1]); - const comm = parts[2]; - const args = parts.slice(3).join(' '); - - if (!isNaN(cpu)) totalCpu += cpu; - if (index === 0) topProc = `${comm} (${Math.round(cpu)}%)`; - - if (comm === 'node' && args.includes('scripts/')) { - const script = args.match(/scripts\/([a-zA-Z0-9_.-]+)/)?.[1] || 'Script'; - activities.push(`📜 Running Script: ${script}`); - runningScripts.push({ pid, name: script }); - } - - if (['grep', 'find', 'curl', 'wget', 'git', 'tar', 'python', 'python3'].includes(comm)) { - let detail = args.split(' ').pop(); - if (comm === 'grep') detail = args.match(/"([^"]+)"/)?.[1] || detail; - if (detail && detail.length > 5000) detail = detail.substring(0, 5000) + '...'; - activities.push(`🔧 ${comm} ${detail}`); - } - }); - - activities = [...new Set(activities)]; - const ctx = getActiveContext(); - - let status = 'idle'; - let taskText = 'System Idle'; - - // 1. Agent Activity (Highest Priority) - if (ctx) { - status = 'busy'; - const currentId = String(ctx.id).trim(); - const lastId = String(lastProcessedId).trim(); - - if (currentId !== lastId) { - ctx.events.forEach(evt => logActivity(evt, currentId)); - lastProcessedId = currentId; - try { - fs.writeFileSync(ID_FILE, currentId, 'utf8'); - } catch (e) { - console.error('ID Save Failed:', e); - } - } - if (ctx.events && ctx.events.length > 0) { - taskText = ctx.events[ctx.events.length - 1]; - } - } + function finalizeStatus({ diskUsage = '--%', gatewayPid = null, psOutput = '', err = null }) { + if (err && !psOutput) return callback({ status: 'error', task: 'Monitor Error' }); + + const lines = psOutput.trim().split('\n').slice(1); + let activities = []; + let runningScripts = []; + let totalCpu = 0; + let topProc = null; + + lines.forEach((line, index) => { + const parts = line.trim().split(/\s+/); + const pid = parts[0]; + const cpu = parseFloat(parts[1]); + const comm = parts[2]; + const args = parts.slice(3).join(' '); + + if (!isNaN(cpu)) totalCpu += cpu; + if (index === 0) topProc = `${comm} (${Math.round(cpu)}%)`; + + if (comm === 'node' && args.includes('scripts/')) { + const script = args.match(/scripts\/([a-zA-Z0-9_.-]+)/)?.[1] || 'Script'; + activities.push(`📜 Running Script: ${script}`); + runningScripts.push({ pid, name: script }); + } - // 2. Background Scripts (Medium Priority) - if (activities.length > 0) { - status = 'busy'; - const activityText = activities.join(', '); - if (taskText === 'System Idle') taskText = activityText; - if (global.lastLoggedActivity !== activityText) { - logActivity(activityText); - global.lastLoggedActivity = activityText; - } - } else { - global.lastLoggedActivity = null; + if (['grep', 'find', 'curl', 'wget', 'git', 'tar', 'python', 'python3'].includes(comm)) { + let detail = args.split(' ').pop(); + if (comm === 'grep') detail = args.match(/"([^"]+)"/)?.[1] || detail; + if (detail && detail.length > 5000) detail = detail.substring(0, 5000) + '...'; + activities.push(`🔧 ${comm} ${detail}`); + } + }); + + activities = [...new Set(activities)]; + const ctx = getActiveContext(); + + let status = 'idle'; + let taskText = 'System Idle'; + + if (ctx) { + status = 'busy'; + const currentId = String(ctx.id).trim(); + const lastId = String(lastProcessedId).trim(); + + if (currentId !== lastId) { + ctx.events.forEach(evt => logActivity(evt, currentId)); + lastProcessedId = currentId; + try { + fs.writeFileSync(ID_FILE, currentId, 'utf8'); + } catch (e) { + console.error('ID Save Failed:', e); } + } + if (ctx.events && ctx.events.length > 0) { + taskText = ctx.events[ctx.events.length - 1]; + } + } - // 3. High CPU (Low Priority) - if (totalCpu > 70.0) { - status = 'busy'; - const cpuText = `⚡ High CPU: ${topProc || 'Unknown'}`; - if (taskText === 'System Idle') taskText = cpuText; - if (global.lastLoggedCpu !== cpuText) { - logActivity(cpuText); - global.lastLoggedCpu = cpuText; - } - } else { - global.lastLoggedCpu = null; - } + if (activities.length > 0) { + status = 'busy'; + const activityText = activities.join(', '); + if (taskText === 'System Idle') taskText = activityText; + if (global.lastLoggedActivity !== activityText) { + logActivity(activityText); + global.lastLoggedActivity = activityText; + } + } else { + global.lastLoggedActivity = null; + } - const versions = getVersions(); - const unsupportedMonitoring = IS_DOCKER ? [...DOCKER_UNSUPPORTED_MONITORING] : []; - - callback({ - status: status, - task: taskText, - cpu: IS_DOCKER ? null : Math.round(totalCpu), - mem: IS_DOCKER ? null : Math.round((1 - os.freemem() / os.totalmem()) * 100), - disk: IS_DOCKER ? null : diskUsage, - timezone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', - lastHeartbeat: new Date().toISOString(), - versions: versions, - gatewayPid: IS_DOCKER ? null : gatewayPid, - scripts: IS_DOCKER ? null : runningScripts, - environment: { - isDocker: IS_DOCKER, - }, - unsupportedMonitoring, - }); + if (totalCpu > 70.0) { + status = 'busy'; + const cpuText = `⚡ High CPU: ${topProc || 'Unknown'}`; + if (taskText === 'System Idle') taskText = cpuText; + if (global.lastLoggedCpu !== cpuText) { + logActivity(cpuText); + global.lastLoggedCpu = cpuText; } + } else { + global.lastLoggedCpu = null; } + + const versions = getVersions(); + const unsupportedMonitoring = IS_DOCKER ? [...DOCKER_UNSUPPORTED_MONITORING] : []; + + callback({ + status: status, + task: taskText, + cpu: IS_DOCKER ? null : Math.round(totalCpu), + mem: IS_DOCKER ? null : Math.round((1 - os.freemem() / os.totalmem()) * 100), + disk: IS_DOCKER ? null : diskUsage, + timezone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', + lastHeartbeat: new Date().toISOString(), + versions: versions, + gatewayPid: IS_DOCKER ? null : gatewayPid, + scripts: IS_DOCKER ? null : runningScripts, + environment: { + isDocker: IS_DOCKER, + }, + unsupportedMonitoring, + }); + } + + if (IS_DOCKER) { + return finalizeStatus({}); + } + + const osType = os.type(); + const mergedCmd = osType === 'Darwin' + ? `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; pgrep -f '[o]penclaw.*gateway' | head -n 1 || true; echo "===PS==="; ps -Ao pid,pcpu,comm,args -r | head -n 21` + : `echo "===DISK==="; df -h / | awk 'NR==2 {print $5}'; echo "===GWPID==="; pgrep -f '[o]penclaw.*gateway' | head -n 1 || true; echo "===PS==="; ps -eo pid,pcpu,comm,args --sort=-pcpu | head -n 20`; + + exec(mergedCmd, (err, stdout) => { + const sections = stdout ? stdout.split(/===\w+===\n?/) : []; + finalizeStatus({ + err, + diskUsage: sections[1] ? sections[1].trim() || '--%' : '--%', + gatewayPid: sections[2] ? sections[2].trim() || null : null, + psOutput: sections[3] || '', + }); }); } diff --git a/tests/routes.docker-mode.test.js b/tests/routes.docker-mode.test.js index f4bd80d..583a280 100644 --- a/tests/routes.docker-mode.test.js +++ b/tests/routes.docker-mode.test.js @@ -65,7 +65,7 @@ describe('Docker-mode route restrictions', () => { expect(exec).not.toHaveBeenCalled(); }); - test('POST /api/run/:id is blocked in Docker mode', () => { + test('POST /api/run/:id is blocked in Docker mode before ID validation', () => { jest.doMock('../src/config', () => ({ HOME_DIR: '/tmp', STATE_DIR: '/tmp/.openclaw', @@ -85,7 +85,7 @@ describe('Docker-mode route restrictions', () => { const router = require('../src/routes/cron'); const res = invokeRouteHandler(router, 'post', '/api/run/:id', { - params: { id: 'daily-job' }, + params: { id: 'bad!!id' }, }); expect(res.statusCode).toBe(403); diff --git a/tests/services.monitor.test.js b/tests/services.monitor.test.js index 6263780..77d210f 100644 --- a/tests/services.monitor.test.js +++ b/tests/services.monitor.test.js @@ -45,9 +45,7 @@ describe('monitor service in Docker mode', () => { }); test('checkSystemStatus marks host-only metrics as unsupported in Docker mode', done => { - const exec = jest.fn((cmd, callback) => { - callback(null, '===DISK===\n42%\n===GWPID===\n\n===PS===\n\n'); - }); + const exec = jest.fn(); jest.doMock('child_process', () => ({ exec, @@ -80,8 +78,7 @@ describe('monitor service in Docker mode', () => { const { checkSystemStatus } = require('../src/services/monitor'); checkSystemStatus(data => { - expect(exec).toHaveBeenCalled(); - expect(exec.mock.calls[0][0]).not.toContain('df -h /'); + expect(exec).not.toHaveBeenCalled(); expect(data.disk).toBeNull(); expect(data.cpu).toBeNull(); expect(data.mem).toBeNull();