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.
Files changed (110) hide show
  1. package/.env.example +73 -0
  2. package/.github/workflows/ci.yml +99 -0
  3. package/.github/workflows/release-macos-trusted.yml +103 -0
  4. package/README.md +336 -0
  5. package/bin/idlewatch-agent.js +1053 -0
  6. package/docs/onboarding-external.md +58 -0
  7. package/docs/packaging/macos-dmg.md +199 -0
  8. package/docs/packaging/macos-launch-agent.md +70 -0
  9. package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
  10. package/docs/qa/mac-qa-log.md +2864 -0
  11. package/docs/telemetry/idle-stale-policy.md +57 -0
  12. package/docs/telemetry/openclaw-mapping.md +80 -0
  13. package/package.json +76 -0
  14. package/scripts/build-dmg.sh +65 -0
  15. package/scripts/install-macos-launch-agent.sh +78 -0
  16. package/scripts/lib/telemetry-row-parser.mjs +100 -0
  17. package/scripts/package-macos.sh +228 -0
  18. package/scripts/uninstall-macos-launch-agent.sh +30 -0
  19. package/scripts/validate-all.sh +142 -0
  20. package/scripts/validate-bin.mjs +25 -0
  21. package/scripts/validate-dmg-checksum.sh +37 -0
  22. package/scripts/validate-dmg-install.sh +155 -0
  23. package/scripts/validate-dry-run-schema.mjs +257 -0
  24. package/scripts/validate-onboarding.mjs +63 -0
  25. package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
  26. package/scripts/validate-openclaw-release-gates.mjs +51 -0
  27. package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
  28. package/scripts/validate-openclaw-usage-health.mjs +95 -0
  29. package/scripts/validate-packaged-artifact.mjs +233 -0
  30. package/scripts/validate-packaged-bundled-runtime.sh +191 -0
  31. package/scripts/validate-packaged-metadata.sh +43 -0
  32. package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
  33. package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
  34. package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
  35. package/scripts/validate-packaged-sourcemaps.mjs +82 -0
  36. package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
  37. package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
  38. package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
  39. package/scripts/validate-trusted-prereqs.sh +44 -0
  40. package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
  41. package/scripts/validate-usage-freshness-e2e.mjs +81 -0
  42. package/skill/SKILL.md +43 -0
  43. package/src/config.js +100 -0
  44. package/src/enrollment.js +176 -0
  45. package/src/gpu.js +115 -0
  46. package/src/memory.js +67 -0
  47. package/src/openclaw-cache.js +51 -0
  48. package/src/openclaw-usage.js +1020 -0
  49. package/src/telemetry-mapping.js +54 -0
  50. package/src/usage-alert.js +41 -0
  51. package/src/usage-freshness.js +31 -0
  52. package/test/config.test.mjs +112 -0
  53. package/test/fixtures/gpu-agx.txt +2 -0
  54. package/test/fixtures/gpu-iogpu.txt +2 -0
  55. package/test/fixtures/gpu-top-grep.txt +2 -0
  56. package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
  57. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
  58. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
  59. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
  60. package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
  61. package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
  62. package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
  63. package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
  64. package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
  65. package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
  66. package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
  67. package/test/fixtures/openclaw-stats.json +17 -0
  68. package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
  69. package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
  70. package/test/fixtures/openclaw-status-control-noise.txt +1 -0
  71. package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
  72. package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
  73. package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
  74. package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
  75. package/test/fixtures/openclaw-status-multi-json.txt +3 -0
  76. package/test/fixtures/openclaw-status-nested-recent.json +19 -0
  77. package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
  78. package/test/fixtures/openclaw-status-noisy.txt +3 -0
  79. package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
  80. package/test/fixtures/openclaw-status-result-session.json +15 -0
  81. package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
  82. package/test/fixtures/openclaw-status-session-map.json +28 -0
  83. package/test/fixtures/openclaw-status-session-model-name.json +18 -0
  84. package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
  85. package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
  86. package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
  87. package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
  88. package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
  89. package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
  90. package/test/fixtures/openclaw-status-strings.json +38 -0
  91. package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
  92. package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
  93. package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
  94. package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
  95. package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
  96. package/test/fixtures/openclaw-status.json +41 -0
  97. package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
  98. package/test/gpu.test.mjs +58 -0
  99. package/test/memory.test.mjs +35 -0
  100. package/test/openclaw-cache.test.mjs +48 -0
  101. package/test/openclaw-env.test.mjs +365 -0
  102. package/test/openclaw-usage.test.mjs +555 -0
  103. package/test/telemetry-mapping.test.mjs +69 -0
  104. package/test/telemetry-row-parser.test.mjs +44 -0
  105. package/test/usage-alert.test.mjs +73 -0
  106. package/test/usage-freshness.test.mjs +63 -0
  107. package/test/validate-dry-run-schema.test.mjs +146 -0
  108. package/tui/Cargo.lock +801 -0
  109. package/tui/Cargo.toml +11 -0
  110. package/tui/src/main.rs +368 -0
@@ -0,0 +1,555 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { parseOpenClawUsage } from '../src/openclaw-usage.js'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+
11
+ function fixture(name) {
12
+ return fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8')
13
+ }
14
+
15
+ test('parses openclaw status --json output and chooses best recent session', () => {
16
+ const usage = parseOpenClawUsage(fixture('openclaw-status.json'))
17
+ assert.ok(usage)
18
+ assert.equal(usage.model, 'gpt-5.3-codex')
19
+ assert.equal(usage.totalTokens, 70500)
20
+ assert.equal(usage.sessionId, '90d2a820-6d77-42f0-8db4-12b90f9f7203')
21
+ assert.equal(usage.agentId, 'main')
22
+ assert.equal(usage.usageTimestampMs, 1771278893678)
23
+ assert.equal(usage.integrationStatus, 'ok')
24
+ assert.equal(usage.tokensPerMin, 384545.45)
25
+ })
26
+
27
+ test('parses noisy openclaw output and alternate sessions key names', () => {
28
+ const usage = parseOpenClawUsage(fixture('openclaw-status-noisy.txt'))
29
+ assert.ok(usage)
30
+ assert.equal(usage.model, 'gpt-5.3-codex')
31
+ assert.equal(usage.totalTokens, 222)
32
+ assert.equal(usage.sessionId, 'new')
33
+ assert.equal(usage.usageTimestampMs, 1771278999999)
34
+ assert.equal(usage.integrationStatus, 'ok')
35
+ })
36
+
37
+ test('ignores non-usage JSON noise and parses later status payload', () => {
38
+ const usage = parseOpenClawUsage(fixture('openclaw-status-multi-json.txt'))
39
+ assert.ok(usage)
40
+ assert.equal(usage.model, 'gpt-5.3-codex')
41
+ assert.equal(usage.totalTokens, 333)
42
+ assert.equal(usage.sessionId, 'new')
43
+ assert.equal(usage.usageTimestampMs, 1771279012345)
44
+ assert.equal(usage.integrationStatus, 'ok')
45
+ })
46
+
47
+ test('parses generic usage payloads', () => {
48
+ const usage = parseOpenClawUsage(JSON.stringify({
49
+ usage: {
50
+ model: 'gpt-5.3-codex',
51
+ totalTokens: 1234,
52
+ tokensPerMinute: 45.67
53
+ },
54
+ sessionId: 'abc',
55
+ agentId: 'main',
56
+ updatedAt: 123456
57
+ }))
58
+
59
+ assert.deepEqual(usage, {
60
+ model: 'gpt-5.3-codex',
61
+ totalTokens: 1234,
62
+ tokensPerMin: 45.67,
63
+ sessionId: 'abc',
64
+ agentId: 'main',
65
+ usageTimestampMs: 123456000,
66
+ integrationStatus: 'ok'
67
+ })
68
+ })
69
+
70
+
71
+
72
+ test('supports model_name in generic usage payloads', () => {
73
+ const usage = parseOpenClawUsage(fixture('openclaw-usage-model-name-generic.json'))
74
+ assert.ok(usage)
75
+ assert.equal(usage.model, 'llama-3.3')
76
+ assert.equal(usage.totalTokens, 135)
77
+ assert.equal(usage.tokensPerMin, 21.5)
78
+ assert.equal(usage.sessionId, 'generic-model-name')
79
+ assert.equal(usage.agentId, 'agent-generic')
80
+ assert.equal(usage.usageTimestampMs, 1771324000000)
81
+ assert.equal(usage.integrationStatus, 'ok')
82
+ })
83
+
84
+ test('prefers most recent candidate when scores tie', () => {
85
+ const usage = parseOpenClawUsage(fixture('openclaw-mixed-equal-score-status-vs-generic-newest.txt'))
86
+ assert.ok(usage)
87
+ assert.equal(usage.model, 'claude-3')
88
+ assert.equal(usage.totalTokens, 999)
89
+ assert.equal(usage.tokensPerMin, 4.4)
90
+ assert.equal(usage.sessionId, 'generic-newer')
91
+ assert.equal(usage.agentId, 'agent-generic')
92
+ assert.equal(usage.usageTimestampMs, 1771290000000)
93
+ assert.equal(usage.integrationStatus, 'ok')
94
+ })
95
+
96
+ test('prefers most recent candidate when timestamps are ISO strings', () => {
97
+ const usage = parseOpenClawUsage(fixture('openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt'))
98
+ assert.ok(usage)
99
+ assert.equal(usage.model, 'qwen-3')
100
+ assert.equal(usage.totalTokens, 100)
101
+ assert.equal(usage.tokensPerMin, 2.2)
102
+ assert.equal(usage.sessionId, 'generic-newer')
103
+ assert.equal(usage.agentId, 'agent-generic')
104
+ assert.equal(usage.integrationStatus, 'ok')
105
+ })
106
+
107
+ test('parses latest candidate when timestamp fields are strings', () => {
108
+ const usage = parseOpenClawUsage(fixture('openclaw-mixed-equal-score-status-vs-generic-string-ts.txt'))
109
+ assert.ok(usage)
110
+ assert.equal(usage.model, 'qwen-2.5')
111
+ assert.equal(usage.totalTokens, 100)
112
+ assert.equal(usage.tokensPerMin, 3.3)
113
+ assert.equal(usage.sessionId, 'generic-newer')
114
+ assert.equal(usage.agentId, 'agent-generic')
115
+ assert.equal(usage.usageTimestampMs, 1771290000000)
116
+ assert.equal(usage.integrationStatus, 'ok')
117
+ })
118
+
119
+ test('prefers richer generic usage candidate when status candidate is partial', () => {
120
+ const usage = parseOpenClawUsage(fixture('openclaw-mixed-status-then-generic-output.txt'))
121
+ assert.ok(usage)
122
+ assert.equal(usage.model, 'gpt-5.3-codex')
123
+ assert.equal(usage.totalTokens, 777)
124
+ assert.equal(usage.tokensPerMin, 11.1)
125
+ assert.equal(usage.sessionId, 'standalone')
126
+ assert.equal(usage.agentId, 'agent-stand')
127
+ assert.equal(usage.usageTimestampMs, 1771281000000)
128
+ assert.equal(usage.integrationStatus, 'ok')
129
+ })
130
+
131
+ test('parses status wrapper payloads under status.current', () => {
132
+ const usage = parseOpenClawUsage(fixture('openclaw-status-status-wrapper.json'))
133
+ assert.ok(usage)
134
+ assert.equal(usage.model, 'gpt-5.3-codex-spark')
135
+ assert.equal(usage.totalTokens, 4321)
136
+ assert.equal(usage.sessionId, 'status-wrap-session')
137
+ assert.equal(usage.agentId, 'agent-status-wrap')
138
+ assert.equal(usage.usageTimestampMs, 1771300100000)
139
+ assert.equal(usage.integrationStatus, 'ok')
140
+ })
141
+
142
+ test('returns null for invalid payload', () => {
143
+ assert.equal(parseOpenClawUsage('not-json'), null)
144
+ })
145
+
146
+ test('parses status payloads with stringified numeric fields and stale token marker fallback', () => {
147
+ const usage = parseOpenClawUsage(fixture('openclaw-status-strings.json'))
148
+ assert.ok(usage)
149
+ assert.equal(usage.model, 'claude-opus-4-6')
150
+ assert.equal(usage.totalTokens, 450)
151
+ assert.equal(usage.tokensPerMin, 45)
152
+ assert.equal(usage.sessionId, 'b7e1f8')
153
+ assert.equal(usage.agentId, 'agent-2')
154
+ assert.equal(usage.usageTimestampMs, 1771278820000)
155
+ assert.equal(usage.integrationStatus, 'ok')
156
+ })
157
+
158
+ test('converts epoch-seconds usage timestamps to milliseconds', () => {
159
+ const usage = parseOpenClawUsage(fixture('openclaw-status-epoch-seconds.json'))
160
+ assert.ok(usage)
161
+ assert.equal(usage.sessionId, 'sec-session')
162
+ assert.equal(usage.agentId, 'agent-sec')
163
+ assert.equal(usage.usageTimestampMs, 1771278800000)
164
+ assert.equal(usage.integrationStatus, 'ok')
165
+ })
166
+
167
+ test('parses stats payloads with nested usage totals', () => {
168
+ const usage = parseOpenClawUsage(fixture('openclaw-stats.json'))
169
+ assert.ok(usage)
170
+ assert.equal(usage.model, 'claude-opus-4-6')
171
+ assert.equal(usage.totalTokens, 4560)
172
+ assert.equal(usage.tokensPerMin, 123.45)
173
+ assert.equal(usage.sessionId, 'sess-stats-01')
174
+ assert.equal(usage.agentId, 'agent-stats')
175
+ assert.equal(usage.usageTimestampMs, 1771279012345)
176
+ assert.equal(usage.integrationStatus, 'ok')
177
+ })
178
+
179
+
180
+ test('parses stats payloads nested under data wrapper', () => {
181
+ const usage = parseOpenClawUsage(fixture('openclaw-stats-data-wrapper.json'))
182
+ assert.ok(usage)
183
+ assert.equal(usage.model, 'gpt-5.3-codex-spark')
184
+ assert.equal(usage.totalTokens, 999)
185
+ assert.equal(usage.tokensPerMin, 88.75)
186
+ assert.equal(usage.sessionId, 'data-wrap-session')
187
+ assert.equal(usage.agentId, 'agent-data-wrap')
188
+ assert.equal(usage.usageTimestampMs, 1771281010000)
189
+ assert.equal(usage.integrationStatus, 'ok')
190
+ })
191
+
192
+ test('parses stats payloads with camelCase usageTime', () => {
193
+ const usage = parseOpenClawUsage(fixture('openclaw-status-stats-current-usage-time-camelcase.json'))
194
+ assert.ok(usage)
195
+ assert.equal(usage.model, 'gpt-5.3-codex')
196
+ assert.equal(usage.totalTokens, 555)
197
+ assert.equal(usage.tokensPerMin, 22)
198
+ assert.equal(usage.sessionId, 'camelcase-session')
199
+ assert.equal(usage.agentId, 'agent-camel')
200
+ assert.equal(usage.usageTimestampMs, 1772191800000)
201
+ assert.equal(usage.integrationStatus, 'ok')
202
+ })
203
+
204
+ test('parses nested stats current.session wrapper payloads', () => {
205
+ const usage = parseOpenClawUsage(fixture('openclaw-stats-nested-session-wrapper.json'))
206
+ assert.ok(usage)
207
+ assert.equal(usage.model, 'claude-opus-4-6')
208
+ assert.equal(usage.totalTokens, 3141)
209
+ assert.equal(usage.tokensPerMin, 77.7)
210
+ assert.equal(usage.sessionId, 'stats-nested-session-1')
211
+ assert.equal(usage.agentId, 'agent-stats-nested')
212
+ assert.equal(usage.usageTimestampMs, 1771303100000)
213
+ assert.equal(usage.integrationStatus, 'ok')
214
+ })
215
+
216
+ test('parses status.stats.current.sessions as array', () => {
217
+ const usage = parseOpenClawUsage(fixture('openclaw-status-stats-current-sessions.json'))
218
+ assert.ok(usage)
219
+ assert.equal(usage.model, 'gpt-5.3-codex')
220
+ assert.equal(usage.totalTokens, 4321)
221
+ assert.equal(usage.tokensPerMin, 66.6)
222
+ assert.equal(usage.sessionId, 'active-session')
223
+ assert.equal(usage.agentId, 'agent-active')
224
+ assert.equal(usage.usageTimestampMs, 1771279100000)
225
+ assert.equal(usage.integrationStatus, 'ok')
226
+ })
227
+
228
+ test('parses session array payload with model in defaultModel field', () => {
229
+ const usage = parseOpenClawUsage(fixture('openclaw-status-stats-session-default-model.json'))
230
+ assert.ok(usage)
231
+ assert.equal(usage.model, 'claude-mini-3.7')
232
+ assert.equal(usage.totalTokens, 777)
233
+ assert.equal(usage.tokensPerMin, 12.34)
234
+ assert.equal(usage.sessionId, 'legacy-model')
235
+ assert.equal(usage.agentId, 'agent-default-model')
236
+ assert.equal(usage.usageTimestampMs, 1771274000000)
237
+ assert.equal(usage.integrationStatus, 'ok')
238
+ })
239
+
240
+ test('supports model_name alias in status sessions payload', () => {
241
+ const usage = parseOpenClawUsage(fixture('openclaw-status-session-model-name.json'))
242
+ assert.ok(usage)
243
+ assert.equal(usage.model, 'claude-3-opus')
244
+ assert.equal(usage.totalTokens, 888)
245
+ assert.equal(usage.tokensPerMin, 25.5)
246
+ assert.equal(usage.sessionId, 'name-case')
247
+ assert.equal(usage.agentId, 'agent-name')
248
+ assert.equal(usage.usageTimestampMs, 1771289000000)
249
+ assert.equal(usage.integrationStatus, 'ok')
250
+ })
251
+
252
+ test('supports string snake-case total_tokens in sessions array payloads', () => {
253
+ const usage = parseOpenClawUsage(fixture('openclaw-status-stats-current-sessions-snake-tokens.json'))
254
+ assert.ok(usage)
255
+ assert.equal(usage.model, 'gpt-5.3-codex')
256
+ assert.equal(usage.totalTokens, 4321)
257
+ assert.equal(usage.tokensPerMin, 42)
258
+ assert.equal(usage.sessionId, 'active-string-tokens')
259
+ assert.equal(usage.agentId, 'agent-legacy-2')
260
+ assert.equal(usage.usageTimestampMs, 1771279100000)
261
+ assert.equal(usage.integrationStatus, 'ok')
262
+ })
263
+
264
+ test('parses status payload with nested sessions object and totals in totals.nested field', () => {
265
+ const raw = fixture('openclaw-status-nested-recent.json')
266
+ const usage = parseOpenClawUsage(raw)
267
+ assert.ok(usage)
268
+ assert.equal(usage.model, 'gpt-5.3-codex-spark')
269
+ assert.equal(usage.totalTokens, 21737)
270
+ assert.equal(usage.tokensPerMin, 32502.31)
271
+ assert.equal(usage.sessionId, 'sess-1')
272
+ assert.equal(usage.agentId, 'agent-007')
273
+ assert.equal(usage.usageTimestampMs, 1739703000000)
274
+ assert.equal(usage.integrationStatus, 'ok')
275
+ })
276
+
277
+ test('parses status.current.stats.stats-in-legacy-shape payload', () => {
278
+ const usage = parseOpenClawUsage(fixture('openclaw-stats-status-current-wrapper.json'))
279
+ assert.ok(usage)
280
+ assert.equal(usage.model, 'gpt-5.3-codex-pro')
281
+ assert.equal(usage.totalTokens, 2048)
282
+ assert.equal(usage.tokensPerMin, 42)
283
+ assert.equal(usage.sessionId, 'status-current-stats-02')
284
+ assert.equal(usage.agentId, 'agent-status-current')
285
+ assert.equal(usage.usageTimestampMs, 1771304500000)
286
+ assert.equal(usage.integrationStatus, 'ok')
287
+ })
288
+
289
+ test('parses wrapped status payload with direct session array and nested usage totals', () => {
290
+ const usage = parseOpenClawUsage(fixture('openclaw-status-wrap-session-object.json'))
291
+ assert.ok(usage)
292
+ assert.equal(usage.model, 'claude-opus-4.6')
293
+ assert.equal(usage.totalTokens, 789)
294
+ assert.equal(usage.tokensPerMin, 12.5)
295
+ assert.equal(usage.sessionId, 'wrapped-session-a')
296
+ assert.equal(usage.agentId, 'agent-wrap')
297
+ assert.equal(usage.usageTimestampMs, Date.parse('2026-02-17T09:00:00Z'))
298
+ assert.equal(usage.integrationStatus, 'ok')
299
+ })
300
+
301
+ test('parses payloads wrapped under data.result with current session aliases', () => {
302
+ const usage = parseOpenClawUsage(fixture('openclaw-status-data-wrapper.json'))
303
+ assert.ok(usage)
304
+ assert.equal(usage.model, 'claude-opus-4-6')
305
+ assert.equal(usage.totalTokens, 3333)
306
+ assert.equal(usage.sessionId, 'data-wrapper-current')
307
+ assert.equal(usage.agentId, 'agent-wrapper')
308
+ assert.equal(usage.usageTimestampMs, 1771279300000)
309
+ assert.equal(usage.integrationStatus, 'ok')
310
+ })
311
+
312
+ test('parses status payload where sessions is an object map keyed by session id', () => {
313
+ const usage = parseOpenClawUsage(fixture('openclaw-status-session-map.json'))
314
+ assert.ok(usage)
315
+ assert.equal(usage.model, 'claude-opus-4-6')
316
+ assert.equal(usage.totalTokens, 1200)
317
+ assert.equal(usage.sessionId, 'map-backup-1')
318
+ assert.equal(usage.agentId, 'agent-map-backup')
319
+ assert.equal(usage.usageTimestampMs, 1771278950000)
320
+ assert.equal(usage.integrationStatus, 'ok')
321
+ })
322
+
323
+ test('selects active session from result.session payload shape', () => {
324
+ const usage = parseOpenClawUsage(fixture('openclaw-status-result-session.json'))
325
+ assert.ok(usage)
326
+ assert.equal(usage.model, 'gpt-5.3-codex')
327
+ assert.equal(usage.totalTokens, 1111)
328
+ assert.equal(usage.tokensPerMin, 1234.56)
329
+ assert.equal(usage.sessionId, 'result-session-1')
330
+ assert.equal(usage.agentId, 'agent-result')
331
+ assert.equal(usage.usageTimestampMs, 1771279000000)
332
+ assert.equal(usage.integrationStatus, 'ok')
333
+ })
334
+
335
+ test('ignores metadata defaults key in session maps and still selects most recent real session', () => {
336
+ const usage = parseOpenClawUsage(fixture('openclaw-status-session-map-with-defaults.json'))
337
+ assert.ok(usage)
338
+ assert.equal(usage.model, 'claude-opus-4-6')
339
+ assert.equal(usage.totalTokens, 2222)
340
+ assert.equal(usage.sessionId, 'map-main-2')
341
+ assert.equal(usage.agentId, 'agent-map-main-2')
342
+ assert.equal(usage.usageTimestampMs, 1771278960000)
343
+ assert.equal(usage.integrationStatus, 'ok')
344
+ })
345
+
346
+ test('parses data.current stats wrapper payloads', () => {
347
+ const usage = parseOpenClawUsage(fixture('openclaw-stats-current-wrapper.json'))
348
+ assert.ok(usage)
349
+ assert.equal(usage.model, 'claude-opus-4-6')
350
+ assert.equal(usage.totalTokens, 987)
351
+ assert.equal(usage.tokensPerMin, 12.34)
352
+ assert.equal(usage.sessionId, 'current-wrapper-session')
353
+ assert.equal(usage.agentId, 'agent-current')
354
+ assert.equal(usage.usageTimestampMs, 1771290000000)
355
+ assert.equal(usage.integrationStatus, 'ok')
356
+ })
357
+
358
+ test('parses payload wrapper stats payloads', () => {
359
+ const usage = parseOpenClawUsage(fixture('openclaw-stats-payload-wrapper.json'))
360
+ assert.ok(usage)
361
+ assert.equal(usage.model, 'gpt-5.3-codex-spark')
362
+ assert.equal(usage.totalTokens, 9876)
363
+ assert.equal(usage.tokensPerMin, null)
364
+ assert.equal(usage.sessionId, 'payload-wrap-session')
365
+ assert.equal(usage.agentId, 'agent-payload')
366
+ assert.equal(usage.usageTimestampMs, 1771302500000)
367
+ assert.equal(usage.integrationStatus, 'ok')
368
+ })
369
+
370
+ test('parses snake_case current_session payload aliases under status wrapper', () => {
371
+ const usage = parseOpenClawUsage(fixture('openclaw-status-snake-session-wrapper.json'))
372
+ assert.ok(usage)
373
+ assert.equal(usage.model, 'gpt-5.3-codex')
374
+ assert.equal(usage.totalTokens, 8888)
375
+ assert.equal(usage.sessionId, 'snake-current-1')
376
+ assert.equal(usage.agentId, 'agent-snake')
377
+ assert.equal(usage.usageTimestampMs, 1771299000000)
378
+ assert.equal(usage.integrationStatus, 'ok')
379
+ })
380
+
381
+ test('uses top-level default model when no sessions are available', () => {
382
+ const sample = '{"defaultModel":"claude-opus-4-6","sessions":{"recent":[]}}'
383
+ const usage = parseOpenClawUsage(sample)
384
+ assert.equal(usage.model, 'claude-opus-4-6')
385
+ assert.equal(usage.totalTokens, null)
386
+ assert.equal(usage.integrationStatus, 'partial')
387
+ })
388
+
389
+ test('parses stderr payload even when command exits non-zero', () => {
390
+ const sample = '{"not": "json"}\n{ "sessions": { "recent": [ { "model": "gpt", "totalTokens": 1 } ] } }'
391
+ const got = parseOpenClawUsage(sample)
392
+ assert.equal(got.model, 'gpt')
393
+ assert.equal(got.totalTokens, 1)
394
+ assert.equal(got.integrationStatus, 'ok')
395
+ })
396
+
397
+ test('ignores ANSI escape noise and parses JSON session payload', () => {
398
+ const got = parseOpenClawUsage(fixture('openclaw-status-ansi-noise.txt'))
399
+ assert.ok(got)
400
+ assert.equal(got.model, 'gpt-5.3-codex')
401
+ assert.equal(got.totalTokens, 4444)
402
+ assert.equal(got.sessionId, 'ansi1')
403
+ assert.equal(got.agentId, 'agent-ansi')
404
+ assert.equal(got.usageTimestampMs, 1771309900000)
405
+ assert.equal(got.integrationStatus, 'ok')
406
+ })
407
+
408
+ test('chooses strongest usage payload when earlier JSON is metadata-only', () => {
409
+ const got = parseOpenClawUsage(fixture('openclaw-status-noisy-default-then-usage.txt'))
410
+ assert.ok(got)
411
+ assert.equal(got.model, 'gpt-5.3-codex-pro')
412
+ assert.equal(got.totalTokens, 7777)
413
+ assert.equal(got.sessionId, 'main')
414
+ assert.equal(got.agentId, 'agent-priority')
415
+ assert.equal(got.usageTimestampMs, 1771312500000)
416
+ assert.equal(got.integrationStatus, 'ok')
417
+ })
418
+
419
+ test('strips complex ANSI cursor/control codes and parses JSON session payload', () => {
420
+ const got = parseOpenClawUsage(fixture('openclaw-status-ansi-complex-noise.txt'))
421
+ assert.ok(got)
422
+ assert.equal(got.model, 'claude-opus-4-6')
423
+ assert.equal(got.totalTokens, 9999)
424
+ assert.equal(got.sessionId, 'complex-ansi')
425
+ assert.equal(got.agentId, 'agent-ansi')
426
+ assert.equal(got.usageTimestampMs, 1771313300000)
427
+ assert.equal(got.integrationStatus, 'ok')
428
+ })
429
+
430
+
431
+
432
+ test('strips control characters (backspace/ctrl) before parsing JSON', () => {
433
+ const got = parseOpenClawUsage(fixture('openclaw-status-control-noise.txt'))
434
+ assert.ok(got)
435
+ assert.equal(got.model, 'gpt-4.1')
436
+ assert.equal(got.totalTokens, 3333)
437
+ assert.equal(got.sessionId, 'control-ansi')
438
+ assert.equal(got.agentId, 'agent-control')
439
+ assert.equal(got.usageTimestampMs, 1771314400000)
440
+ assert.equal(got.integrationStatus, 'ok')
441
+ })
442
+
443
+ test('strips OSC/title control sequences before parsing JSON', () => {
444
+ const got = parseOpenClawUsage(fixture('openclaw-status-osc-noise.txt'))
445
+ assert.ok(got)
446
+ assert.equal(got.model, 'qwen2.5-coder')
447
+ assert.equal(got.totalTokens, 5555)
448
+ assert.equal(got.sessionId, 'osc1')
449
+ assert.equal(got.agentId, 'agent-osc')
450
+ assert.equal(got.usageTimestampMs, 1771315500000)
451
+ assert.equal(got.integrationStatus, 'ok')
452
+ })
453
+
454
+ test('strips DCS control sequences before parsing JSON', () => {
455
+ const got = parseOpenClawUsage(fixture('openclaw-status-dcs-noise.txt'))
456
+ assert.ok(got)
457
+ assert.equal(got.model, 'deepseek-coder')
458
+ assert.equal(got.totalTokens, 6000)
459
+ assert.equal(got.sessionId, 'dcs1')
460
+ assert.equal(got.agentId, 'agent-dcs')
461
+ assert.equal(got.usageTimestampMs, 1771316600000)
462
+ assert.equal(got.integrationStatus, 'ok')
463
+ })
464
+
465
+
466
+ test('strips mixed terminal noise sequences together', () => {
467
+ const got = parseOpenClawUsage(fixture('openclaw-status-mixed-noise.txt'))
468
+ assert.ok(got)
469
+ assert.equal(got.model, 'gemini-pro')
470
+ assert.equal(got.totalTokens, 1010)
471
+ assert.equal(got.sessionId, 'mixed-ansi')
472
+ assert.equal(got.agentId, 'agent-mixed')
473
+ assert.equal(got.usageTimestampMs, 1771317600000)
474
+ assert.equal(got.integrationStatus, 'ok')
475
+ })
476
+
477
+ test('parses stats payload with nested current object under status.stats', () => {
478
+ const usage = parseOpenClawUsage(fixture('openclaw-stats-current-wrapper2.json'))
479
+ assert.ok(usage)
480
+ assert.equal(usage.model, 'gpt-5.3-codex-spark')
481
+ assert.equal(usage.totalTokens, 555)
482
+ assert.equal(usage.tokensPerMin, 21.5)
483
+ assert.equal(usage.sessionId, 'status-current-stats-01')
484
+ assert.equal(usage.agentId, 'agent-current-wrap')
485
+ assert.equal(usage.usageTimestampMs, 1771295000000)
486
+ assert.equal(usage.integrationStatus, 'ok')
487
+ })
488
+
489
+ test('parses usage timestamp aliases in snake_case ms fields', () => {
490
+ const usage = parseOpenClawUsage(fixture('openclaw-status-ts-ms-alias.json'))
491
+ assert.ok(usage)
492
+ assert.equal(usage.model, 'gpt-5.3-codex')
493
+ assert.equal(usage.totalTokens, 1500)
494
+ assert.equal(usage.tokensPerMin, 77)
495
+ assert.equal(usage.sessionId, 'ts-ms-session')
496
+ assert.equal(usage.agentId, 'agent-ts-ms')
497
+ assert.equal(usage.usageTimestampMs, 1771319900000)
498
+ assert.equal(usage.integrationStatus, 'ok')
499
+ })
500
+
501
+ test('parses usage timestamp aliases in usage_timestamp_ms fields', () => {
502
+ const usage = parseOpenClawUsage(fixture('openclaw-status-usage-timestamp-ms-alias.json'))
503
+ assert.ok(usage)
504
+ assert.equal(usage.model, 'qwen3')
505
+ assert.equal(usage.totalTokens, 1717)
506
+ assert.equal(usage.tokensPerMin, 22)
507
+ assert.equal(usage.sessionId, 'ts-ms-underscore-session')
508
+ assert.equal(usage.agentId, 'agent-ts-ms-underscore')
509
+ assert.equal(usage.usageTimestampMs, 1771321200000)
510
+ assert.equal(usage.integrationStatus, 'ok')
511
+ })
512
+
513
+ test('parses usage timestamp aliases in usage_ts fields', () => {
514
+ const usage = parseOpenClawUsage(fixture('openclaw-status-usage-ts-alias.json'))
515
+ assert.ok(usage)
516
+ assert.equal(usage.model, 'gpt-5.3-codex')
517
+ assert.equal(usage.totalTokens, 1900)
518
+ assert.equal(usage.tokensPerMin, 44)
519
+ assert.equal(usage.sessionId, 'usage-ts-session')
520
+ assert.equal(usage.agentId, 'agent-usage-ts')
521
+ assert.equal(usage.usageTimestampMs, 1771319000000)
522
+ assert.equal(usage.integrationStatus, 'ok')
523
+ })
524
+
525
+ test('parses usage_time timestamp alias in generic payloads', () => {
526
+ const usage = parseOpenClawUsage(JSON.stringify({
527
+ current: {
528
+ model: 'gpt-5.3-codex',
529
+ totalTokens: 1500,
530
+ sessionId: 'time-alias',
531
+ agentId: 'agent-time',
532
+ usage_time: '2026-02-28T00:00:00.000Z'
533
+ }
534
+ }))
535
+
536
+ assert.ok(usage)
537
+ assert.equal(usage.model, 'gpt-5.3-codex')
538
+ assert.equal(usage.totalTokens, 1500)
539
+ assert.equal(usage.sessionId, 'time-alias')
540
+ assert.equal(usage.agentId, 'agent-time')
541
+ assert.equal(usage.usageTimestampMs, Date.parse('2026-02-28T00:00:00.000Z'))
542
+ assert.equal(usage.integrationStatus, 'ok')
543
+ })
544
+
545
+ test('parses usage timestamp aliases in updated_at_ms fields', () => {
546
+ const usage = parseOpenClawUsage(fixture('openclaw-status-updated-at-ms-alias.json'))
547
+ assert.ok(usage)
548
+ assert.equal(usage.model, 'gpt-5.3-codex-spark')
549
+ assert.equal(usage.totalTokens, 2500)
550
+ assert.equal(usage.tokensPerMin, 31.2)
551
+ assert.equal(usage.sessionId, 'ts-ms-updated-at-session')
552
+ assert.equal(usage.agentId, 'agent-ts-ms-updated-at')
553
+ assert.equal(usage.usageTimestampMs, 1771325000000)
554
+ assert.equal(usage.integrationStatus, 'ok')
555
+ })
@@ -0,0 +1,69 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { OPENCLAW_FLEET_SCHEMA, enrichWithOpenClawFleetTelemetry } from '../src/telemetry-mapping.js'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+
11
+ function fixture(name) {
12
+ return JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8'))
13
+ }
14
+
15
+ test('enriches legacy sample with fleet schema envelope while preserving legacy fields', () => {
16
+ const legacy = {
17
+ host: 'mac-mini-1',
18
+ ts: 1771278899999,
19
+ cpuPct: 12.5,
20
+ memPct: 63.2,
21
+ memUsedPct: 63.2,
22
+ memPressurePct: 42,
23
+ memPressureClass: 'normal',
24
+ tokensPerMin: 128.4,
25
+ openclawModel: 'gpt-5.3-codex',
26
+ openclawTotalTokens: 70500,
27
+ openclawSessionId: '90d2a820-6d77-42f0-8db4-12b90f9f7203',
28
+ openclawAgentId: 'main',
29
+ openclawUsageTs: 1771278893678,
30
+ openclawUsageAgeMs: 6321,
31
+ source: {
32
+ usage: 'openclaw',
33
+ usageIntegrationStatus: 'ok',
34
+ usageIngestionStatus: 'ok',
35
+ usageActivityStatus: 'fresh',
36
+ usageFreshnessState: 'fresh',
37
+ usageAlertLevel: 'ok',
38
+ usageAlertReason: 'healthy',
39
+ usageCommand: 'openclaw status --json',
40
+ usageProbeResult: 'ok',
41
+ usageProbeAttempts: 1,
42
+ usageUsedFallbackCache: false,
43
+ usageFallbackCacheSource: null
44
+ }
45
+ }
46
+
47
+ const enriched = enrichWithOpenClawFleetTelemetry(legacy, {
48
+ collector: 'idlewatch-agent',
49
+ collectorVersion: '0.1.0'
50
+ })
51
+
52
+ assert.equal(enriched.host, legacy.host)
53
+ assert.equal(enriched.openclawSessionId, legacy.openclawSessionId)
54
+ assert.equal(enriched.schemaFamily, OPENCLAW_FLEET_SCHEMA.family)
55
+ assert.equal(enriched.schemaVersion, OPENCLAW_FLEET_SCHEMA.version)
56
+ assert.deepEqual(enriched.schemaCompat, OPENCLAW_FLEET_SCHEMA.backwardCompatibleWith)
57
+ assert.equal(enriched.fleet.usage.tokensPerMin, legacy.tokensPerMin)
58
+ assert.equal(enriched.fleet.usage.totalTokens, legacy.openclawTotalTokens)
59
+ assert.equal(enriched.fleet.provenance.collectorVersion, '0.1.0')
60
+ })
61
+
62
+ test('fixture sample remains aligned with schema metadata', () => {
63
+ const sample = fixture('openclaw-fleet-sample-v1.json')
64
+ assert.equal(sample.schemaFamily, OPENCLAW_FLEET_SCHEMA.family)
65
+ assert.equal(sample.schemaVersion, OPENCLAW_FLEET_SCHEMA.version)
66
+ assert.deepEqual(sample.schemaCompat, OPENCLAW_FLEET_SCHEMA.backwardCompatibleWith)
67
+ assert.equal(sample.fleet.usage.model, sample.openclawModel)
68
+ assert.equal(sample.fleet.usage.sessionId, sample.openclawSessionId)
69
+ })
@@ -0,0 +1,44 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ extractJsonCandidates,
6
+ readTelemetryJsonRow,
7
+ stripControlNoise
8
+ } from '../scripts/lib/telemetry-row-parser.mjs'
9
+
10
+ test('extractJsonCandidates finds last JSON object in noisy multiline output', () => {
11
+ const noisy = `\x1b[32m[boot]\x1b[0m starting\n` +
12
+ '{"step":"init"}\n' +
13
+ 'heartbeat\n' +
14
+ '{"host":"daemon","ts":123}'
15
+
16
+ const candidates = extractJsonCandidates(noisy)
17
+
18
+ assert.equal(candidates.length, 2)
19
+ assert.deepEqual(JSON.parse(candidates[1]), { host: 'daemon', ts: 123 })
20
+ })
21
+
22
+ test('extractJsonCandidates handles nested arrays and trailing text', () => {
23
+ const raw = '[{"a":1}]\nnoise\nother {"nested":{"k":2}} tail'
24
+ const candidates = extractJsonCandidates(raw)
25
+
26
+ assert.equal(candidates.length, 2)
27
+ assert.deepEqual(JSON.parse(candidates[0]), [{ a: 1 }])
28
+ assert.deepEqual(JSON.parse(candidates[1]), { nested: { k: 2 } })
29
+ })
30
+
31
+ test('readTelemetryJsonRow prefers the last valid JSON candidate', () => {
32
+ const output = `warn\n{"ts": 1,}` +
33
+ `\n{\"ts\": 2, \"host\": \"edge\"}\n`
34
+
35
+ const row = readTelemetryJsonRow(output)
36
+ assert.deepEqual(row, { ts: 2, host: 'edge' })
37
+ })
38
+
39
+ test('stripControlNoise removes ANSI control sequences that can break parser', () => {
40
+ const raw = '\x1b[33mwarn\x1b[0m\n{\"host\":\"x\",\"ts\":123}\x1b[?1049l'
41
+ const clean = stripControlNoise(raw)
42
+ assert.ok(clean.includes('{"host":"x","ts":123}'))
43
+ assert.ok(!clean.includes('\u001b['))
44
+ })