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,402 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, writeFileSync, chmodSync, rmSync } from 'node:fs'
|
|
4
|
+
import { readTelemetryJsonRow } from './lib/telemetry-row-parser.mjs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
8
|
+
|
|
9
|
+
const repoRoot = process.cwd()
|
|
10
|
+
const rootDir = process.cwd()
|
|
11
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'idlewatch-openclaw-stats-pkg-egress-'))
|
|
12
|
+
const mockBinPath = join(tempDir, 'openclaw-mock.sh')
|
|
13
|
+
|
|
14
|
+
function writeMockOpenClaw(pathToScript, shape) {
|
|
15
|
+
const resultCurrent = `{
|
|
16
|
+
"status": {
|
|
17
|
+
"result": {
|
|
18
|
+
"stats": {
|
|
19
|
+
"current": {
|
|
20
|
+
"session": {
|
|
21
|
+
"sessionId": "packaged-stats-session",
|
|
22
|
+
"agentId": "agent-stats-packaged",
|
|
23
|
+
"model": "gpt-5.3-codex",
|
|
24
|
+
"totalTokens": 4242,
|
|
25
|
+
"tokensPerMinute": 12.5,
|
|
26
|
+
"updatedAt": 1771311100000
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"ts": 1771311110000
|
|
33
|
+
}`
|
|
34
|
+
|
|
35
|
+
const statusCurrent = `{
|
|
36
|
+
"status": {
|
|
37
|
+
"current": {
|
|
38
|
+
"stats": {
|
|
39
|
+
"current": {
|
|
40
|
+
"session": {
|
|
41
|
+
"sessionId": "packaged-status-current-session",
|
|
42
|
+
"agentId": "agent-stats-packaged-current",
|
|
43
|
+
"model": "gpt-5.3-codex-pro",
|
|
44
|
+
"totalTokens": 4096,
|
|
45
|
+
"tokensPerMinute": 15,
|
|
46
|
+
"updatedAt": 1771311900000
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"ts": 1771311910000
|
|
53
|
+
}`
|
|
54
|
+
|
|
55
|
+
const statusCurrentTsMs = `{
|
|
56
|
+
"status": {
|
|
57
|
+
"current": {
|
|
58
|
+
"stats": {
|
|
59
|
+
"current": {
|
|
60
|
+
"session": {
|
|
61
|
+
"sessionId": "packaged-status-current-tsms-session",
|
|
62
|
+
"agentId": "agent-stats-packaged-tsms",
|
|
63
|
+
"model": "gpt-5.3-codex-pro",
|
|
64
|
+
"totalTokens": 3210,
|
|
65
|
+
"tokensPerMinute": 18,
|
|
66
|
+
"usage_ts_ms": 1771312500000,
|
|
67
|
+
"ts_ms": 1771312501000
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"ts": 1771312510000
|
|
73
|
+
}`
|
|
74
|
+
|
|
75
|
+
const statusCurrentUsageTs = `{
|
|
76
|
+
"status": {
|
|
77
|
+
"current": {
|
|
78
|
+
"stats": {
|
|
79
|
+
"current": {
|
|
80
|
+
"session": {
|
|
81
|
+
"sessionId": "packaged-status-current-usage-ts",
|
|
82
|
+
"agentId": "agent-stats-packaged-usage-ts",
|
|
83
|
+
"model": "gpt-5.3-codex-pro",
|
|
84
|
+
"totalTokens": 2900,
|
|
85
|
+
"tokensPerMinute": 19,
|
|
86
|
+
"usage_ts": 1771312600000,
|
|
87
|
+
"ts_ms": 1771312601000
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"ts": 1771312610000
|
|
93
|
+
}`
|
|
94
|
+
|
|
95
|
+
const statusCurrentUsageTimestampMs = `{
|
|
96
|
+
"status": {
|
|
97
|
+
"current": {
|
|
98
|
+
"stats": {
|
|
99
|
+
"current": {
|
|
100
|
+
"session": {
|
|
101
|
+
"sessionId": "packaged-status-current-usage-timestamp-ms",
|
|
102
|
+
"agentId": "agent-stats-packaged-usage-timestamp-ms",
|
|
103
|
+
"model": "qwen-lite-packaged",
|
|
104
|
+
"totalTokens": 2121,
|
|
105
|
+
"tokensPerMinute": 22,
|
|
106
|
+
"usage_timestamp_ms": 1771317000000,
|
|
107
|
+
"ts_ms": 1771317001000
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"ts": 1771317010000
|
|
113
|
+
}`
|
|
114
|
+
|
|
115
|
+
const statusCurrentUpdatedAtMs = `{
|
|
116
|
+
"status": {
|
|
117
|
+
"current": {
|
|
118
|
+
"stats": {
|
|
119
|
+
"current": {
|
|
120
|
+
"session": {
|
|
121
|
+
"sessionId": "packaged-status-current-updated-at-ms",
|
|
122
|
+
"agentId": "agent-stats-packaged-updated-at-ms",
|
|
123
|
+
"model": "claude-mini-3.8",
|
|
124
|
+
"totalTokens": 777,
|
|
125
|
+
"tokensPerMinute": 21,
|
|
126
|
+
"updated_at_ms": 1771315000000,
|
|
127
|
+
"ts_ms": 1771315001000
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"ts": 1771315010000
|
|
133
|
+
}`
|
|
134
|
+
|
|
135
|
+
const statusCurrentUsageTimestamp = `{
|
|
136
|
+
"status": {
|
|
137
|
+
"current": {
|
|
138
|
+
"stats": {
|
|
139
|
+
"current": {
|
|
140
|
+
"session": {
|
|
141
|
+
"sessionId": "packaged-status-current-usage-timestamp",
|
|
142
|
+
"agentId": "agent-stats-packaged-usage-timestamp",
|
|
143
|
+
"model": "qwen-lite-packaged",
|
|
144
|
+
"totalTokens": 1919,
|
|
145
|
+
"tokensPerMinute": 12,
|
|
146
|
+
"usage_timestamp": "2026-02-27T09:10:00.000Z",
|
|
147
|
+
"ts_ms": 1771316000000
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"ts": 1771316010000
|
|
153
|
+
}`
|
|
154
|
+
|
|
155
|
+
const statusCurrentUsageTime = `{
|
|
156
|
+
"status": {
|
|
157
|
+
"current": {
|
|
158
|
+
"stats": {
|
|
159
|
+
"current": {
|
|
160
|
+
"session": {
|
|
161
|
+
"sessionId": "packaged-status-current-usage-time",
|
|
162
|
+
"agentId": "agent-stats-packaged-usage-time",
|
|
163
|
+
"model": "claude-mini-3.8-packaged",
|
|
164
|
+
"totalTokens": 1555,
|
|
165
|
+
"tokensPerMinute": 14,
|
|
166
|
+
"usage_time": "2026-02-27T09:20:00.000Z",
|
|
167
|
+
"ts_ms": 1771317000000
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"ts": 1771317010000
|
|
174
|
+
}`
|
|
175
|
+
|
|
176
|
+
const statusCurrentUsageTimeCamel = `{
|
|
177
|
+
"status": {
|
|
178
|
+
"current": {
|
|
179
|
+
"stats": {
|
|
180
|
+
"current": {
|
|
181
|
+
"session": {
|
|
182
|
+
"sessionId": "packaged-status-current-usage-time-camel",
|
|
183
|
+
"agentId": "agent-stats-packaged-usage-time-camel",
|
|
184
|
+
"model": "qwen-camel-packaged",
|
|
185
|
+
"totalTokens": 2333,
|
|
186
|
+
"tokensPerMinute": 17,
|
|
187
|
+
"usageTime": "2026-02-27T09:50:00.000Z",
|
|
188
|
+
"ts_ms": 1771319000000
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"ts": 1771319010000
|
|
195
|
+
}`
|
|
196
|
+
|
|
197
|
+
const script = `#!/usr/bin/env bash
|
|
198
|
+
set -euo pipefail
|
|
199
|
+
|
|
200
|
+
SCENARIO="${shape}"
|
|
201
|
+
if [[ "$1" == "stats" && "$2" == "--json" ]]; then
|
|
202
|
+
if [[ "$SCENARIO" == "statusCurrent" ]]; then
|
|
203
|
+
cat <<JSON
|
|
204
|
+
${statusCurrent}
|
|
205
|
+
JSON
|
|
206
|
+
elif [[ "$SCENARIO" == "statusCurrentTsMs" ]]; then
|
|
207
|
+
cat <<JSON
|
|
208
|
+
${statusCurrentTsMs}
|
|
209
|
+
JSON
|
|
210
|
+
elif [[ "$SCENARIO" == "statusCurrentUsageTs" ]]; then
|
|
211
|
+
cat <<JSON
|
|
212
|
+
${statusCurrentUsageTs}
|
|
213
|
+
JSON
|
|
214
|
+
elif [[ "$SCENARIO" == "statusCurrentUsageTimestampMs" ]]; then
|
|
215
|
+
cat <<JSON
|
|
216
|
+
${statusCurrentUsageTimestampMs}
|
|
217
|
+
JSON
|
|
218
|
+
elif [[ "$SCENARIO" == "statusCurrentUpdatedAtMs" ]]; then
|
|
219
|
+
cat <<JSON
|
|
220
|
+
${statusCurrentUpdatedAtMs}
|
|
221
|
+
JSON
|
|
222
|
+
elif [[ "$SCENARIO" == "statusCurrentUsageTimestamp" ]]; then
|
|
223
|
+
cat <<JSON
|
|
224
|
+
${statusCurrentUsageTimestamp}
|
|
225
|
+
JSON
|
|
226
|
+
elif [[ "$SCENARIO" == "statusCurrentUsageTime" ]]; then
|
|
227
|
+
cat <<JSON
|
|
228
|
+
${statusCurrentUsageTime}
|
|
229
|
+
JSON
|
|
230
|
+
elif [[ "$SCENARIO" == "statusCurrentUsageTimeCamel" ]]; then
|
|
231
|
+
cat <<JSON
|
|
232
|
+
${statusCurrentUsageTimeCamel}
|
|
233
|
+
JSON
|
|
234
|
+
else
|
|
235
|
+
cat <<JSON
|
|
236
|
+
${resultCurrent}
|
|
237
|
+
JSON
|
|
238
|
+
fi
|
|
239
|
+
exit 0
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
if [[ "$1" == "status" || "$1" == "usage" || "$1" == "session" || "$1" == "session_status" ]]; then
|
|
243
|
+
echo '{"message":"legacy status output unavailable"}\n'
|
|
244
|
+
exit 0
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
echo '{"error":"unsupported command"}'
|
|
248
|
+
exit 2
|
|
249
|
+
`
|
|
250
|
+
writeFileSync(pathToScript, script, 'utf8')
|
|
251
|
+
chmodSync(pathToScript, 0o755)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function readRow(output) {
|
|
255
|
+
return readTelemetryJsonRow(output)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function collectRow(env) {
|
|
259
|
+
const launcher = join(rootDir, 'dist', 'IdleWatch.app', 'Contents', 'MacOS', 'IdleWatch')
|
|
260
|
+
const out = spawnSync(launcher, ['--dry-run'], {
|
|
261
|
+
cwd: repoRoot,
|
|
262
|
+
encoding: 'utf8',
|
|
263
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
264
|
+
timeout: 20000,
|
|
265
|
+
env,
|
|
266
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
267
|
+
killSignal: 'SIGINT'
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const output = String(out.stdout || '') + String(out.stderr || '')
|
|
271
|
+
assert.ok(!out.error || out.error.code === 'ETIMEDOUT', `packaged dry-run command failed unexpectedly: ${out.error?.message || `exit ${out.status}`}`)
|
|
272
|
+
assert.equal(out.status, 0, `packaged dry-run exited with ${out.status}`)
|
|
273
|
+
assert.ok(output.trim(), 'packaged dry-run produced no output')
|
|
274
|
+
return readRow(output)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function run(shape, expectations) {
|
|
278
|
+
writeMockOpenClaw(mockBinPath, shape)
|
|
279
|
+
|
|
280
|
+
const env = {
|
|
281
|
+
...process.env,
|
|
282
|
+
HOME: process.env.HOME || '',
|
|
283
|
+
IDLEWATCH_OPENCLAW_BIN: mockBinPath,
|
|
284
|
+
IDLEWATCH_OPENCLAW_BIN_STRICT: '1',
|
|
285
|
+
IDLEWATCH_OPENCLAW_USAGE: 'auto',
|
|
286
|
+
IDLEWATCH_OPENCLAW_PROBE_RETRIES: '0',
|
|
287
|
+
IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS: '2000',
|
|
288
|
+
IDLEWATCH_USAGE_STALE_MS: '5000',
|
|
289
|
+
IDLEWATCH_USAGE_NEAR_STALE_MS: '3000',
|
|
290
|
+
IDLEWATCH_USAGE_STALE_GRACE_MS: '1000',
|
|
291
|
+
IDLEWATCH_USAGE_REFRESH_REPROBES: '0',
|
|
292
|
+
IDLEWATCH_USAGE_REFRESH_DELAY_MS: '0',
|
|
293
|
+
IDLEWATCH_USAGE_REFRESH_ON_NEAR_STALE: '0',
|
|
294
|
+
IDLEWATCH_REQUIRE_OPENCLAW_USAGE: '1',
|
|
295
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS: '5000',
|
|
296
|
+
IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH: join(tempDir, 'openclaw-last-good.json')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const row = collectRow(env)
|
|
300
|
+
|
|
301
|
+
assert.equal(row?.source?.usage, 'openclaw', 'packaged OpenClaw path should emit usage=openclaw')
|
|
302
|
+
assert.equal(row?.source?.usageIngestionStatus, 'ok', 'packaged OpenClaw path should report ingestion ok')
|
|
303
|
+
assert.equal(row?.source?.usageProbeResult, 'ok', 'stats --json fallback path should be accepted as successful probe')
|
|
304
|
+
assert.equal(row?.openclawSessionId, expectations.sessionId)
|
|
305
|
+
assert.equal(row?.openclawAgentId, expectations.agentId)
|
|
306
|
+
assert.equal(row?.openclawTotalTokens, expectations.totalTokens)
|
|
307
|
+
assert.equal(row?.tokensPerMin, expectations.tokensPerMin)
|
|
308
|
+
assert.equal(row?.openclawModel, expectations.model)
|
|
309
|
+
assert.equal(row?.source?.usageCommand?.includes('stats --json'), true, 'OpenClaw command used in packaged run should include stats --json when stats fallback is needed')
|
|
310
|
+
assert.equal(row?.source?.usageProbeAttempts >= 3, true, 'stats fallback should require attempts through multiple commands before success')
|
|
311
|
+
assert.equal(row?.usageProbeError || null, null, 'successful packaged stats fallback should not leak probe error')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function runAllShapes() {
|
|
315
|
+
if (process.env.IDLEWATCH_SKIP_PACKAGE_MACOS !== '1') {
|
|
316
|
+
execFileSync('npm', ['run', 'package:macos', '--silent'], {
|
|
317
|
+
cwd: repoRoot,
|
|
318
|
+
encoding: 'utf8',
|
|
319
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
run('resultCurrent', {
|
|
324
|
+
sessionId: 'packaged-stats-session',
|
|
325
|
+
agentId: 'agent-stats-packaged',
|
|
326
|
+
totalTokens: 4242,
|
|
327
|
+
model: 'gpt-5.3-codex',
|
|
328
|
+
tokensPerMin: 12.5
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
run('statusCurrent', {
|
|
332
|
+
sessionId: 'packaged-status-current-session',
|
|
333
|
+
agentId: 'agent-stats-packaged-current',
|
|
334
|
+
totalTokens: 4096,
|
|
335
|
+
model: 'gpt-5.3-codex-pro',
|
|
336
|
+
tokensPerMin: 15
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
run('statusCurrentTsMs', {
|
|
340
|
+
sessionId: 'packaged-status-current-tsms-session',
|
|
341
|
+
agentId: 'agent-stats-packaged-tsms',
|
|
342
|
+
totalTokens: 3210,
|
|
343
|
+
model: 'gpt-5.3-codex-pro',
|
|
344
|
+
tokensPerMin: 18
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
run('statusCurrentUsageTs', {
|
|
348
|
+
sessionId: 'packaged-status-current-usage-ts',
|
|
349
|
+
agentId: 'agent-stats-packaged-usage-ts',
|
|
350
|
+
totalTokens: 2900,
|
|
351
|
+
model: 'gpt-5.3-codex-pro',
|
|
352
|
+
tokensPerMin: 19
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
run('statusCurrentUsageTimestampMs', {
|
|
356
|
+
sessionId: 'packaged-status-current-usage-timestamp-ms',
|
|
357
|
+
agentId: 'agent-stats-packaged-usage-timestamp-ms',
|
|
358
|
+
totalTokens: 2121,
|
|
359
|
+
model: 'qwen-lite-packaged',
|
|
360
|
+
tokensPerMin: 22
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
run('statusCurrentUsageTimestamp', {
|
|
364
|
+
sessionId: 'packaged-status-current-usage-timestamp',
|
|
365
|
+
agentId: 'agent-stats-packaged-usage-timestamp',
|
|
366
|
+
totalTokens: 1919,
|
|
367
|
+
model: 'qwen-lite-packaged',
|
|
368
|
+
tokensPerMin: 12
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
run('statusCurrentUsageTime', {
|
|
372
|
+
sessionId: 'packaged-status-current-usage-time',
|
|
373
|
+
agentId: 'agent-stats-packaged-usage-time',
|
|
374
|
+
totalTokens: 1555,
|
|
375
|
+
model: 'claude-mini-3.8-packaged',
|
|
376
|
+
tokensPerMin: 14
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
run('statusCurrentUsageTimeCamel', {
|
|
380
|
+
sessionId: 'packaged-status-current-usage-time-camel',
|
|
381
|
+
agentId: 'agent-stats-packaged-usage-time-camel',
|
|
382
|
+
totalTokens: 2333,
|
|
383
|
+
model: 'qwen-camel-packaged',
|
|
384
|
+
tokensPerMin: 17
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
run('statusCurrentUpdatedAtMs', {
|
|
388
|
+
sessionId: 'packaged-status-current-updated-at-ms',
|
|
389
|
+
agentId: 'agent-stats-packaged-updated-at-ms',
|
|
390
|
+
totalTokens: 777,
|
|
391
|
+
model: 'claude-mini-3.8',
|
|
392
|
+
tokensPerMin: 21
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
console.log('validate-packaged-openclaw-stats-ingestion: ok (packaged app parses stats fallback across payload shapes)')
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
runAllShapes()
|
|
400
|
+
} finally {
|
|
401
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
402
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const [, , targetRoot] = process.argv
|
|
6
|
+
const ROOT = targetRoot || path.join(process.cwd(), 'dist', 'IdleWatch.app', 'Contents', 'Resources', 'payload', 'package')
|
|
7
|
+
|
|
8
|
+
function* walkJsFiles(startDir) {
|
|
9
|
+
const entries = fs.readdirSync(startDir, { withFileTypes: true })
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const fullPath = path.join(startDir, entry.name)
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
yield* walkJsFiles(fullPath)
|
|
14
|
+
continue
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!entry.isFile()) continue
|
|
18
|
+
if (!entry.name.endsWith('.js')) continue
|
|
19
|
+
yield fullPath
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractSourceMapRef(contents) {
|
|
24
|
+
const match = String(contents).match(/\/\/#[\s]*sourceMappingURL=([^\n\r]+)\s*$/m)
|
|
25
|
+
return match ? match[1].trim() : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateSourceMaps(rootDir) {
|
|
29
|
+
const missing = []
|
|
30
|
+
let nodeModuleMissing = 0
|
|
31
|
+
|
|
32
|
+
for (const jsPath of walkJsFiles(rootDir)) {
|
|
33
|
+
const contents = fs.readFileSync(jsPath, 'utf8')
|
|
34
|
+
const ref = extractSourceMapRef(contents)
|
|
35
|
+
if (!ref) continue
|
|
36
|
+
|
|
37
|
+
if (ref.startsWith('data:')) continue
|
|
38
|
+
if (ref.startsWith('http:') || ref.startsWith('https:')) continue
|
|
39
|
+
|
|
40
|
+
const candidate = path.resolve(path.dirname(jsPath), ref)
|
|
41
|
+
const isNodeModule = jsPath.includes(`${path.sep}node_modules${path.sep}`)
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(candidate)) {
|
|
44
|
+
if (isNodeModule) {
|
|
45
|
+
nodeModuleMissing += 1
|
|
46
|
+
} else {
|
|
47
|
+
missing.push({ jsPath, ref })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (nodeModuleMissing > 0) {
|
|
53
|
+
console.warn(`packaged sourcemap check skipped ${nodeModuleMissing} external-map references in node_modules (not validated by design).`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (missing.length > 0) {
|
|
57
|
+
console.error('packaged source maps validation failed: missing external map files')
|
|
58
|
+
for (const item of missing.slice(0, 40)) {
|
|
59
|
+
console.error(`- ${item.jsPath} -> ${item.ref}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (missing.length > 40) {
|
|
63
|
+
console.error(`...and ${missing.length - 40} more`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('packaged sourcemaps validation passed')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(ROOT)) {
|
|
73
|
+
console.error(`packaged payload directory not found: ${ROOT}`)
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
validateSourceMaps(ROOT)
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(`packaged sourcemap validation error: ${error.message}`)
|
|
81
|
+
process.exit(1)
|
|
82
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { readTelemetryJsonRow } from './lib/telemetry-row-parser.mjs'
|
|
5
|
+
import { execFileSync } from 'node:child_process'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
const repoRoot = process.cwd()
|
|
10
|
+
const appBin = './dist/IdleWatch.app/Contents/MacOS/IdleWatch'
|
|
11
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'idlewatch-packaged-alert-rate-'))
|
|
12
|
+
const mockBinPath = join(tempDir, 'openclaw-mock.sh')
|
|
13
|
+
|
|
14
|
+
function writeMockOpenClaw(pathToFile) {
|
|
15
|
+
const script = `#!/usr/bin/env bash
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
AGE_MS="\${MOCK_OPENCLAW_AGE_MS:-500}"
|
|
18
|
+
now_ms=$(node -p 'Date.now()')
|
|
19
|
+
updated_at=$((now_ms - AGE_MS))
|
|
20
|
+
cat <<JSON
|
|
21
|
+
{"sessions":{"defaults":{"model":"gpt-5.3-codex"},"recent":[{"sessionId":"sess-packaged-alert","agentId":"main","model":"gpt-5.3-codex","totalTokens":12345,"updatedAt":\${updated_at},"totalTokensFresh":true}]},"ts":\${now_ms}}
|
|
22
|
+
JSON
|
|
23
|
+
`
|
|
24
|
+
writeFileSync(pathToFile, script, 'utf8')
|
|
25
|
+
chmodSync(pathToFile, 0o755)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readRow(output) {
|
|
29
|
+
return readTelemetryJsonRow(output)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runSample(ageMs, overrides = {}) {
|
|
33
|
+
const env = {
|
|
34
|
+
...process.env,
|
|
35
|
+
IDLEWATCH_OPENCLAW_BIN: mockBinPath,
|
|
36
|
+
IDLEWATCH_USAGE_STALE_MS: '60000',
|
|
37
|
+
IDLEWATCH_USAGE_STALE_GRACE_MS: '10000',
|
|
38
|
+
IDLEWATCH_INTERVAL_MS: '1000',
|
|
39
|
+
MOCK_OPENCLAW_AGE_MS: String(ageMs),
|
|
40
|
+
...overrides
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const out = execFileSync(appBin, ['--dry-run'], {
|
|
44
|
+
cwd: repoRoot,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
env,
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return readRow(out)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectAlertLevels(ageSeries, overrides = {}) {
|
|
54
|
+
return ageSeries.map((ageMs) => {
|
|
55
|
+
const row = runSample(ageMs, overrides)
|
|
56
|
+
return {
|
|
57
|
+
ageMs,
|
|
58
|
+
nearMs: row?.source?.usageNearStaleMsThreshold,
|
|
59
|
+
level: row?.source?.usageAlertLevel,
|
|
60
|
+
reason: row?.source?.usageAlertReason,
|
|
61
|
+
freshness: row?.source?.usageFreshnessState
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
writeMockOpenClaw(mockBinPath)
|
|
68
|
+
execFileSync('npm', ['run', 'package:macos', '--silent'], {
|
|
69
|
+
cwd: repoRoot,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const typicalAges = [28000, 33000, 39000, 45000, 50000, 54000]
|
|
75
|
+
const typical = collectAlertLevels(typicalAges)
|
|
76
|
+
|
|
77
|
+
const nonOkTypical = typical.filter((sample) => sample.level !== 'ok')
|
|
78
|
+
assert.equal(nonOkTypical.length, 0, `expected no non-ok alerts for typical low-traffic ages, got: ${JSON.stringify(nonOkTypical)}`)
|
|
79
|
+
assert.equal(typical[0].nearMs, 59500, 'expected default near-stale threshold to include stale+grace derivation (59500ms)')
|
|
80
|
+
|
|
81
|
+
const boundaryAges = [1800, 3600, 5600]
|
|
82
|
+
const boundary = collectAlertLevels(boundaryAges, {
|
|
83
|
+
IDLEWATCH_USAGE_STALE_MS: '3000',
|
|
84
|
+
IDLEWATCH_USAGE_NEAR_STALE_MS: '1000',
|
|
85
|
+
IDLEWATCH_USAGE_STALE_GRACE_MS: '1000'
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
assert.equal(boundary[0].level, 'notice', 'expected boundary sample 0 to emit notice for near-stale')
|
|
89
|
+
assert.equal(boundary[0].reason, 'activity-near-stale', 'expected boundary sample 0 reason to be activity-near-stale')
|
|
90
|
+
assert.equal(boundary[1].level, 'warning', 'expected boundary sample 1 to emit warning after stale threshold')
|
|
91
|
+
assert.equal(boundary[1].reason, 'activity-past-threshold', 'expected boundary sample 1 reason to be activity-past-threshold')
|
|
92
|
+
assert.equal(boundary[2].level, 'warning', 'expected boundary sample 2 to remain warning')
|
|
93
|
+
assert.equal(boundary[2].reason, 'activity-past-threshold', 'expected boundary sample 2 reason to remain activity-past-threshold')
|
|
94
|
+
|
|
95
|
+
console.log('validate-packaged-usage-alert-rate-e2e: ok (packaged launcher alert-level transitions remain aligned)')
|
|
96
|
+
} finally {
|
|
97
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
98
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, writeFileSync, chmodSync, rmSync } from 'node:fs'
|
|
4
|
+
import { readTelemetryJsonRow } from './lib/telemetry-row-parser.mjs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { execFileSync } from 'node:child_process'
|
|
8
|
+
|
|
9
|
+
const repoRoot = process.cwd()
|
|
10
|
+
const appBin = './dist/IdleWatch.app/Contents/MacOS/IdleWatch'
|
|
11
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'idlewatch-packaged-probe-noise-'))
|
|
12
|
+
const mockBinPath = join(tempDir, 'openclaw-mock.sh')
|
|
13
|
+
|
|
14
|
+
function writeMockOpenClaw(binPath) {
|
|
15
|
+
const script = `#!/usr/bin/env bash
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
age_ms="\${MOCK_OPENCLAW_AGE_MS:-500}"
|
|
18
|
+
exit_code="\${MOCK_OPENCLAW_EXIT_CODE:-3}"
|
|
19
|
+
|
|
20
|
+
now_ms=$(node -p 'Date.now()')
|
|
21
|
+
updated_at=$((now_ms - age_ms))
|
|
22
|
+
|
|
23
|
+
cat <<JSON
|
|
24
|
+
{"sessions":{"defaults":{"model":"gpt-5.3-codex"},"recent":[{"sessionId":"sess-packaged-noise","agentId":"main","model":"gpt-5.3-codex","totalTokens":54321,"updatedAt":\${updated_at},"totalTokensFresh":true}]},"ts":\${now_ms}}
|
|
25
|
+
JSON
|
|
26
|
+
|
|
27
|
+
echo "openclaw status probe complete (simulated warning)." >&2
|
|
28
|
+
exit "\${exit_code}"
|
|
29
|
+
`
|
|
30
|
+
writeFileSync(binPath, script, 'utf8')
|
|
31
|
+
chmodSync(binPath, 0o755)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readRow(output) {
|
|
35
|
+
return readTelemetryJsonRow(output)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
writeMockOpenClaw(mockBinPath)
|
|
40
|
+
|
|
41
|
+
execFileSync('npm', ['run', 'package:macos', '--silent'], {
|
|
42
|
+
cwd: repoRoot,
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const env = {
|
|
48
|
+
...process.env,
|
|
49
|
+
IDLEWATCH_OPENCLAW_BIN: mockBinPath,
|
|
50
|
+
IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS: '2500',
|
|
51
|
+
IDLEWATCH_OPENCLAW_PROBE_RETRIES: '0',
|
|
52
|
+
IDLEWATCH_USAGE_STALE_MS: '60000',
|
|
53
|
+
IDLEWATCH_USAGE_STALE_GRACE_MS: '10000',
|
|
54
|
+
IDLEWATCH_USAGE_REFRESH_REPROBES: '0',
|
|
55
|
+
IDLEWATCH_OPENCLAW_USAGE: 'auto',
|
|
56
|
+
MOCK_OPENCLAW_AGE_MS: '500',
|
|
57
|
+
MOCK_OPENCLAW_EXIT_CODE: '3'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const out = execFileSync(appBin, ['--dry-run'], {
|
|
61
|
+
cwd: repoRoot,
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
env,
|
|
64
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const row = readRow(out)
|
|
68
|
+
const source = row.source
|
|
69
|
+
|
|
70
|
+
assert.equal(source.usage, 'openclaw')
|
|
71
|
+
assert.equal(source.usageIngestionStatus, 'ok')
|
|
72
|
+
assert.equal(source.usageProbeResult, 'ok')
|
|
73
|
+
assert.equal(
|
|
74
|
+
typeof source.usageProbeError === 'string' &&
|
|
75
|
+
(source.usageProbeError.includes('non-zero') || source.usageProbeError.includes('command-exited')),
|
|
76
|
+
true
|
|
77
|
+
)
|
|
78
|
+
assert.ok(source.usageCommand.includes('openclaw-mock.sh status --json'))
|
|
79
|
+
assert.equal(source.usageIntegrationStatus, 'ok')
|
|
80
|
+
assert.equal(source.usageAlertReason, 'healthy')
|
|
81
|
+
assert.equal(row.openclawSessionId, 'sess-packaged-noise')
|
|
82
|
+
assert.equal(row.openclawTotalTokens, 54321)
|
|
83
|
+
|
|
84
|
+
console.log('packaged-usage-probe-noise-e2e: ok (non-zero exit with valid JSON still ingested)')
|
|
85
|
+
} finally {
|
|
86
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
87
|
+
}
|