idlewatch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +73 -0
- package/.github/workflows/ci.yml +99 -0
- package/.github/workflows/release-macos-trusted.yml +103 -0
- package/README.md +336 -0
- package/bin/idlewatch-agent.js +1053 -0
- package/docs/onboarding-external.md +58 -0
- package/docs/packaging/macos-dmg.md +199 -0
- package/docs/packaging/macos-launch-agent.md +70 -0
- package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
- package/docs/qa/mac-qa-log.md +2864 -0
- package/docs/telemetry/idle-stale-policy.md +57 -0
- package/docs/telemetry/openclaw-mapping.md +80 -0
- package/package.json +76 -0
- package/scripts/build-dmg.sh +65 -0
- package/scripts/install-macos-launch-agent.sh +78 -0
- package/scripts/lib/telemetry-row-parser.mjs +100 -0
- package/scripts/package-macos.sh +228 -0
- package/scripts/uninstall-macos-launch-agent.sh +30 -0
- package/scripts/validate-all.sh +142 -0
- package/scripts/validate-bin.mjs +25 -0
- package/scripts/validate-dmg-checksum.sh +37 -0
- package/scripts/validate-dmg-install.sh +155 -0
- package/scripts/validate-dry-run-schema.mjs +257 -0
- package/scripts/validate-onboarding.mjs +63 -0
- package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
- package/scripts/validate-openclaw-release-gates.mjs +51 -0
- package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
- package/scripts/validate-openclaw-usage-health.mjs +95 -0
- package/scripts/validate-packaged-artifact.mjs +233 -0
- package/scripts/validate-packaged-bundled-runtime.sh +191 -0
- package/scripts/validate-packaged-metadata.sh +43 -0
- package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
- package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
- package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
- package/scripts/validate-packaged-sourcemaps.mjs +82 -0
- package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
- package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
- package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
- package/scripts/validate-trusted-prereqs.sh +44 -0
- package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
- package/scripts/validate-usage-freshness-e2e.mjs +81 -0
- package/skill/SKILL.md +43 -0
- package/src/config.js +100 -0
- package/src/enrollment.js +176 -0
- package/src/gpu.js +115 -0
- package/src/memory.js +67 -0
- package/src/openclaw-cache.js +51 -0
- package/src/openclaw-usage.js +1020 -0
- package/src/telemetry-mapping.js +54 -0
- package/src/usage-alert.js +41 -0
- package/src/usage-freshness.js +31 -0
- package/test/config.test.mjs +112 -0
- package/test/fixtures/gpu-agx.txt +2 -0
- package/test/fixtures/gpu-iogpu.txt +2 -0
- package/test/fixtures/gpu-top-grep.txt +2 -0
- package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
- package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
- package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
- package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
- package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
- package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
- package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
- package/test/fixtures/openclaw-stats.json +17 -0
- package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
- package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
- package/test/fixtures/openclaw-status-control-noise.txt +1 -0
- package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
- package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
- package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
- package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
- package/test/fixtures/openclaw-status-multi-json.txt +3 -0
- package/test/fixtures/openclaw-status-nested-recent.json +19 -0
- package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
- package/test/fixtures/openclaw-status-noisy.txt +3 -0
- package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
- package/test/fixtures/openclaw-status-result-session.json +15 -0
- package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
- package/test/fixtures/openclaw-status-session-map.json +28 -0
- package/test/fixtures/openclaw-status-session-model-name.json +18 -0
- package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
- package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
- package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
- package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
- package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-strings.json +38 -0
- package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
- package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
- package/test/fixtures/openclaw-status.json +41 -0
- package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
- package/test/gpu.test.mjs +58 -0
- package/test/memory.test.mjs +35 -0
- package/test/openclaw-cache.test.mjs +48 -0
- package/test/openclaw-env.test.mjs +365 -0
- package/test/openclaw-usage.test.mjs +555 -0
- package/test/telemetry-mapping.test.mjs +69 -0
- package/test/telemetry-row-parser.test.mjs +44 -0
- package/test/usage-alert.test.mjs +73 -0
- package/test/usage-freshness.test.mjs +63 -0
- package/test/validate-dry-run-schema.test.mjs +146 -0
- package/tui/Cargo.lock +801 -0
- package/tui/Cargo.toml +11 -0
- package/tui/src/main.rs +368 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import { mkdtempSync, writeFileSync, chmodSync, rmSync } from 'node:fs'
|
|
6
|
+
import os from 'node:os'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = path.dirname(__filename)
|
|
12
|
+
const BIN = path.resolve(__dirname, '../bin/idlewatch-agent.js')
|
|
13
|
+
|
|
14
|
+
test('accepts explicit IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS in dry-run', () => {
|
|
15
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
16
|
+
env: {
|
|
17
|
+
...process.env,
|
|
18
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
19
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS: '60000'
|
|
20
|
+
},
|
|
21
|
+
encoding: 'utf8'
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
assert.equal(run.status, 0, run.stderr)
|
|
25
|
+
assert.match(run.stdout, /idlewatch-agent dry-run/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('rejects invalid IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS', () => {
|
|
29
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
33
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS: '0'
|
|
34
|
+
},
|
|
35
|
+
encoding: 'utf8'
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
assert.notEqual(run.status, 0)
|
|
39
|
+
assert.match(run.stderr, /Invalid IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS/)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('accepts explicit IDLEWATCH_OPENCLAW_PROBE_RETRIES in dry-run', () => {
|
|
43
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
44
|
+
env: {
|
|
45
|
+
...process.env,
|
|
46
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
47
|
+
IDLEWATCH_OPENCLAW_PROBE_RETRIES: '2'
|
|
48
|
+
},
|
|
49
|
+
encoding: 'utf8'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
assert.equal(run.status, 0, run.stderr)
|
|
53
|
+
assert.match(run.stdout, /idlewatch-agent dry-run/)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('rejects invalid IDLEWATCH_OPENCLAW_PROBE_RETRIES', () => {
|
|
57
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
61
|
+
IDLEWATCH_OPENCLAW_PROBE_RETRIES: '-1'
|
|
62
|
+
},
|
|
63
|
+
encoding: 'utf8'
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
assert.notEqual(run.status, 0)
|
|
67
|
+
assert.match(run.stderr, /Invalid IDLEWATCH_OPENCLAW_PROBE_RETRIES/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('accepts explicit IDLEWATCH_USAGE_REFRESH_REPROBES in dry-run', () => {
|
|
71
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
72
|
+
env: {
|
|
73
|
+
...process.env,
|
|
74
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
75
|
+
IDLEWATCH_USAGE_REFRESH_REPROBES: '3'
|
|
76
|
+
},
|
|
77
|
+
encoding: 'utf8'
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
assert.equal(run.status, 0, run.stderr)
|
|
81
|
+
assert.match(run.stdout, /idlewatch-agent dry-run/)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('rejects invalid IDLEWATCH_USAGE_REFRESH_REPROBES', () => {
|
|
85
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
86
|
+
env: {
|
|
87
|
+
...process.env,
|
|
88
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
89
|
+
IDLEWATCH_USAGE_REFRESH_REPROBES: '-1'
|
|
90
|
+
},
|
|
91
|
+
encoding: 'utf8'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
assert.notEqual(run.status, 0)
|
|
95
|
+
assert.match(run.stderr, /Invalid IDLEWATCH_USAGE_REFRESH_REPROBES/)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('accepts explicit IDLEWATCH_USAGE_REFRESH_DELAY_MS in dry-run', () => {
|
|
99
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
103
|
+
IDLEWATCH_USAGE_REFRESH_DELAY_MS: '500'
|
|
104
|
+
},
|
|
105
|
+
encoding: 'utf8'
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
assert.equal(run.status, 0, run.stderr)
|
|
109
|
+
assert.match(run.stdout, /idlewatch-agent dry-run/)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('rejects invalid IDLEWATCH_USAGE_REFRESH_DELAY_MS', () => {
|
|
113
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
114
|
+
env: {
|
|
115
|
+
...process.env,
|
|
116
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
117
|
+
IDLEWATCH_USAGE_REFRESH_DELAY_MS: '-5'
|
|
118
|
+
},
|
|
119
|
+
encoding: 'utf8'
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
assert.notEqual(run.status, 0)
|
|
123
|
+
assert.match(run.stderr, /Invalid IDLEWATCH_USAGE_REFRESH_DELAY_MS/)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('accepts Firestore emulator mode without service account credentials', () => {
|
|
127
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
128
|
+
env: {
|
|
129
|
+
...process.env,
|
|
130
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
131
|
+
FIREBASE_PROJECT_ID: 'idlewatch-dev',
|
|
132
|
+
FIRESTORE_EMULATOR_HOST: '127.0.0.1:8080',
|
|
133
|
+
FIREBASE_SERVICE_ACCOUNT_JSON: '',
|
|
134
|
+
FIREBASE_SERVICE_ACCOUNT_B64: ''
|
|
135
|
+
},
|
|
136
|
+
encoding: 'utf8'
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
assert.equal(run.status, 0, run.stderr)
|
|
140
|
+
assert.match(run.stdout, /idlewatch-agent dry-run/)
|
|
141
|
+
assert.match(run.stdout, /firebase=true/)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('rejects emulator mode when FIREBASE_PROJECT_ID is missing', () => {
|
|
145
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
146
|
+
env: {
|
|
147
|
+
...process.env,
|
|
148
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
149
|
+
FIREBASE_PROJECT_ID: '',
|
|
150
|
+
FIRESTORE_EMULATOR_HOST: '127.0.0.1:8080'
|
|
151
|
+
},
|
|
152
|
+
encoding: 'utf8'
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
assert.notEqual(run.status, 0)
|
|
156
|
+
assert.match(run.stderr, /FIREBASE_PROJECT_ID is missing/)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('rejects required Firebase writes when Firebase is not configured', () => {
|
|
160
|
+
const run = spawnSync(process.execPath, [BIN, '--once'], {
|
|
161
|
+
env: {
|
|
162
|
+
...process.env,
|
|
163
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
164
|
+
IDLEWATCH_REQUIRE_FIREBASE_WRITES: '1',
|
|
165
|
+
FIREBASE_PROJECT_ID: '',
|
|
166
|
+
FIRESTORE_EMULATOR_HOST: '',
|
|
167
|
+
FIREBASE_SERVICE_ACCOUNT_JSON: '',
|
|
168
|
+
FIREBASE_SERVICE_ACCOUNT_B64: ''
|
|
169
|
+
},
|
|
170
|
+
encoding: 'utf8'
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
assert.notEqual(run.status, 0)
|
|
174
|
+
assert.match(run.stderr, /IDLEWATCH_REQUIRE_FIREBASE_WRITES=1 requires Firebase to be configured/)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('accepts required Firebase writes config in emulator mode (dry-run)', () => {
|
|
178
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
179
|
+
env: {
|
|
180
|
+
...process.env,
|
|
181
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
182
|
+
IDLEWATCH_REQUIRE_FIREBASE_WRITES: '1',
|
|
183
|
+
FIREBASE_PROJECT_ID: 'idlewatch-dev',
|
|
184
|
+
FIRESTORE_EMULATOR_HOST: '127.0.0.1:8080',
|
|
185
|
+
FIREBASE_SERVICE_ACCOUNT_JSON: '',
|
|
186
|
+
FIREBASE_SERVICE_ACCOUNT_B64: ''
|
|
187
|
+
},
|
|
188
|
+
encoding: 'utf8'
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
assert.equal(run.status, 0, run.stderr)
|
|
192
|
+
assert.match(run.stdout, /idlewatch-agent dry-run/)
|
|
193
|
+
assert.match(run.stdout, /firebase=true/)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('accepts FIREBASE_SERVICE_ACCOUNT_FILE credentials', () => {
|
|
197
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'idlewatch-creds-file-'))
|
|
198
|
+
const credsPath = path.join(tmpRoot, 'service-account.json')
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
fs.writeFileSync(credsPath, JSON.stringify({
|
|
202
|
+
type: 'service_account',
|
|
203
|
+
project_id: 'idlewatch-test',
|
|
204
|
+
private_key_id: 'abc123',
|
|
205
|
+
private_key: '-----BEGIN PRIVATE KEY-----\\nabc\\n-----END PRIVATE KEY-----\\n',
|
|
206
|
+
client_email: 'idlewatch@idlewatch-test.iam.gserviceaccount.com',
|
|
207
|
+
client_id: '123'
|
|
208
|
+
}))
|
|
209
|
+
|
|
210
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
211
|
+
env: {
|
|
212
|
+
...process.env,
|
|
213
|
+
IDLEWATCH_OPENCLAW_USAGE: 'off',
|
|
214
|
+
FIREBASE_PROJECT_ID: 'idlewatch-test',
|
|
215
|
+
FIRESTORE_EMULATOR_HOST: '127.0.0.1:8080',
|
|
216
|
+
FIREBASE_SERVICE_ACCOUNT_FILE: credsPath,
|
|
217
|
+
FIREBASE_SERVICE_ACCOUNT_JSON: '',
|
|
218
|
+
FIREBASE_SERVICE_ACCOUNT_B64: ''
|
|
219
|
+
},
|
|
220
|
+
encoding: 'utf8'
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
assert.equal(run.status, 0, run.stderr)
|
|
224
|
+
assert.match(run.stdout, /idlewatch-agent dry-run/)
|
|
225
|
+
assert.match(run.stdout, /firebase=true/)
|
|
226
|
+
} finally {
|
|
227
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true })
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
test('accepts OpenClaw JSON from stderr payload on non-zero-exit command', () => {
|
|
233
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), 'idlewatch-openclaw-stderr-'))
|
|
234
|
+
const mockBin = path.join(tempDir, 'openclaw-mock.sh')
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
writeFileSync(
|
|
238
|
+
mockBin,
|
|
239
|
+
`#!/usr/bin/env bash
|
|
240
|
+
set -euo pipefail
|
|
241
|
+
echo '{"sessions":{"defaults":{"model":"gpt-5.3-codex"},"recent":[{"sessionId":"stderr-session","agentId":"main","model":"gpt-5.3-codex","totalTokens":12345,"updatedAt":1771280000000,"totalTokensFresh":true}]},"ts":1771280001234}' >&2
|
|
242
|
+
exit 42\n`,
|
|
243
|
+
{ encoding: 'utf8' }
|
|
244
|
+
)
|
|
245
|
+
chmodSync(mockBin, 0o755)
|
|
246
|
+
|
|
247
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
248
|
+
env: {
|
|
249
|
+
...process.env,
|
|
250
|
+
IDLEWATCH_OPENCLAW_BIN: mockBin,
|
|
251
|
+
IDLEWATCH_OPENCLAW_USAGE: 'auto',
|
|
252
|
+
IDLEWATCH_OPENCLAW_PROBE_RETRIES: '0',
|
|
253
|
+
IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS: '500',
|
|
254
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS: '1000',
|
|
255
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH: path.join(tempDir, 'openclaw-last-good.json')
|
|
256
|
+
},
|
|
257
|
+
encoding: 'utf8'
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
assert.equal(run.status, 0, run.stderr)
|
|
261
|
+
const lines = run.stdout.trim().split('\n').filter(Boolean)
|
|
262
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{') && line.endsWith('}'))
|
|
263
|
+
assert.ok(jsonLine)
|
|
264
|
+
const payload = JSON.parse(jsonLine)
|
|
265
|
+
|
|
266
|
+
assert.equal(payload.source.usage, 'openclaw')
|
|
267
|
+
assert.equal(payload.source.usageProbeResult, 'ok')
|
|
268
|
+
assert.equal(payload.openclawSessionId, 'stderr-session')
|
|
269
|
+
assert.match(payload.source.usageProbeError, /command-exited/)
|
|
270
|
+
} finally {
|
|
271
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('accepts OpenClaw JSON from mixed stdout+stderr on non-zero-exit command', () => {
|
|
276
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), 'idlewatch-openclaw-mixed-'))
|
|
277
|
+
const mockBin = path.join(tempDir, 'openclaw-mock.sh')
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
writeFileSync(
|
|
281
|
+
mockBin,
|
|
282
|
+
`#!/usr/bin/env bash
|
|
283
|
+
set -euo pipefail
|
|
284
|
+
echo 'openclaw wrapper initializing...'
|
|
285
|
+
echo '{"sessions":{"defaults":{"model":"gpt-5.3-codex"},"recent":[{"sessionId":"mixed-session","agentId":"main","model":"gpt-5.3-codex","totalTokens":6789,"updatedAt":1771290000000,"totalTokensFresh":true}]},"ts":1771290001234}' >&2
|
|
286
|
+
exit 42\n`,
|
|
287
|
+
{ encoding: 'utf8' }
|
|
288
|
+
)
|
|
289
|
+
chmodSync(mockBin, 0o755)
|
|
290
|
+
|
|
291
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
292
|
+
env: {
|
|
293
|
+
...process.env,
|
|
294
|
+
IDLEWATCH_OPENCLAW_BIN: mockBin,
|
|
295
|
+
IDLEWATCH_OPENCLAW_USAGE: 'auto',
|
|
296
|
+
IDLEWATCH_OPENCLAW_PROBE_RETRIES: '0',
|
|
297
|
+
IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS: '500',
|
|
298
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS: '1000',
|
|
299
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH: path.join(tempDir, 'openclaw-last-good.json')
|
|
300
|
+
},
|
|
301
|
+
encoding: 'utf8'
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
assert.equal(run.status, 0, run.stderr)
|
|
305
|
+
const lines = run.stdout.trim().split('\n').filter(Boolean)
|
|
306
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{') && line.endsWith('}'))
|
|
307
|
+
assert.ok(jsonLine)
|
|
308
|
+
const payload = JSON.parse(jsonLine)
|
|
309
|
+
|
|
310
|
+
assert.equal(payload.source.usage, 'openclaw')
|
|
311
|
+
assert.equal(payload.source.usageProbeResult, 'ok')
|
|
312
|
+
assert.equal(payload.openclawSessionId, 'mixed-session')
|
|
313
|
+
assert.match(payload.source.usageProbeError, /command-exited/)
|
|
314
|
+
} finally {
|
|
315
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test('accepts legacy IDLEWATCH_OPENCLAW_BIN_HINT as explicit binary path in strict mode', () => {
|
|
320
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), 'idlewatch-openclaw-bin-hint-'))
|
|
321
|
+
const mockBin = path.join(tempDir, 'openclaw-hint-mock.sh')
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
writeFileSync(
|
|
325
|
+
mockBin,
|
|
326
|
+
`#!/usr/bin/env bash
|
|
327
|
+
set -euo pipefail
|
|
328
|
+
cat <<'JSON'
|
|
329
|
+
{"sessions":{"defaults":{"model":"gpt-5.3-codex"},"recent":[{"sessionId":"hint-session","agentId":"main","model":"gpt-5.3-codex","totalTokens":2222,"updatedAt":1771280100000,"totalTokensFresh":true}]},"ts":1771280100123}
|
|
330
|
+
JSON
|
|
331
|
+
`,
|
|
332
|
+
{ encoding: 'utf8' }
|
|
333
|
+
)
|
|
334
|
+
chmodSync(mockBin, 0o755)
|
|
335
|
+
|
|
336
|
+
const run = spawnSync(process.execPath, [BIN, '--dry-run'], {
|
|
337
|
+
env: {
|
|
338
|
+
...process.env,
|
|
339
|
+
IDLEWATCH_OPENCLAW_BIN: '',
|
|
340
|
+
IDLEWATCH_OPENCLAW_BIN_HINT: mockBin,
|
|
341
|
+
IDLEWATCH_OPENCLAW_BIN_STRICT: '1',
|
|
342
|
+
IDLEWATCH_OPENCLAW_USAGE: 'auto',
|
|
343
|
+
IDLEWATCH_OPENCLAW_PROBE_RETRIES: '0',
|
|
344
|
+
IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS: '500',
|
|
345
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS: '1000',
|
|
346
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH: path.join(tempDir, 'openclaw-last-good.json')
|
|
347
|
+
},
|
|
348
|
+
encoding: 'utf8'
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
assert.equal(run.status, 0, run.stderr)
|
|
352
|
+
const lines = run.stdout.trim().split('\n').filter(Boolean)
|
|
353
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{') && line.endsWith('}'))
|
|
354
|
+
assert.ok(jsonLine)
|
|
355
|
+
const payload = JSON.parse(jsonLine)
|
|
356
|
+
|
|
357
|
+
assert.equal(payload.source.usage, 'openclaw')
|
|
358
|
+
assert.equal(payload.source.usageProbeResult, 'ok')
|
|
359
|
+
assert.equal(payload.source.usageCommand, `${mockBin} status --json`)
|
|
360
|
+
assert.equal(payload.openclawSessionId, 'hint-session')
|
|
361
|
+
} finally {
|
|
362
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|