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,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"current_session": {
|
|
4
|
+
"session_id": "snake-current-1",
|
|
5
|
+
"agent_id": "agent-snake",
|
|
6
|
+
"model": "gpt-5.3-codex",
|
|
7
|
+
"total_tokens": 8888,
|
|
8
|
+
"updated_at": 1771299000,
|
|
9
|
+
"ts": 1771299012345
|
|
10
|
+
},
|
|
11
|
+
"default_model": "gpt-5.3-codex"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"stats": {
|
|
4
|
+
"current": {
|
|
5
|
+
"sessions": [
|
|
6
|
+
{
|
|
7
|
+
"sessionId": "legacy-1",
|
|
8
|
+
"agentId": "agent-legacy-1",
|
|
9
|
+
"model": "gpt-4.1",
|
|
10
|
+
"total_tokens": "111",
|
|
11
|
+
"updatedAt": 1771261000000
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"session_id": "active-string-tokens",
|
|
15
|
+
"agent_id": "agent-legacy-2",
|
|
16
|
+
"defaultModel": "gpt-5.3-codex",
|
|
17
|
+
"total_tokens": "4321",
|
|
18
|
+
"updatedAt": 1771279100000,
|
|
19
|
+
"tokensPerMinute": 42.0
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"stats": {
|
|
4
|
+
"current": {
|
|
5
|
+
"sessions": [
|
|
6
|
+
{
|
|
7
|
+
"sessionId": "old-session",
|
|
8
|
+
"agentId": "agent-old",
|
|
9
|
+
"model": "gpt-4.1",
|
|
10
|
+
"totalTokens": 100,
|
|
11
|
+
"updatedAt": 1771270000000
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"sessionId": "active-session",
|
|
15
|
+
"agentId": "agent-active",
|
|
16
|
+
"model": "gpt-5.3-codex",
|
|
17
|
+
"totalTokens": 4321,
|
|
18
|
+
"updatedAt": 1771279100000,
|
|
19
|
+
"tokensPerMinute": 66.6
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"totals": {
|
|
23
|
+
"totalTokens": 4421
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"current": {
|
|
4
|
+
"stats": {
|
|
5
|
+
"current": {
|
|
6
|
+
"session": {
|
|
7
|
+
"sessionId": "camelcase-session",
|
|
8
|
+
"agentId": "agent-camel",
|
|
9
|
+
"model": "gpt-5.3-codex",
|
|
10
|
+
"totalTokens": 555,
|
|
11
|
+
"tokensPerMinute": 22,
|
|
12
|
+
"usageTime": "2026-02-27T11:30:00.000Z"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"ts": 1771323000000
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"stats": {
|
|
4
|
+
"current": {
|
|
5
|
+
"sessions": [
|
|
6
|
+
{
|
|
7
|
+
"sessionId": "legacy-model",
|
|
8
|
+
"defaultModel": "claude-mini-3.7",
|
|
9
|
+
"agentId": "agent-default-model",
|
|
10
|
+
"totalTokens": "777",
|
|
11
|
+
"updatedAt": 1771274000000,
|
|
12
|
+
"tokensPerMinute": "12.34"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"sessionId": "better-total",
|
|
16
|
+
"default_model": "gpt-4.1-mini",
|
|
17
|
+
"defaultModel": "should-not-win",
|
|
18
|
+
"agentId": "agent-legacy",
|
|
19
|
+
"total_tokens": "999",
|
|
20
|
+
"updatedAt": 1771264000000,
|
|
21
|
+
"tokens_per_minute": 9.87
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"current": {
|
|
4
|
+
"model": "gpt-5.3-codex-spark",
|
|
5
|
+
"totalTokens": 4321,
|
|
6
|
+
"sessionId": "status-wrap-session",
|
|
7
|
+
"agent_id": "agent-status-wrap",
|
|
8
|
+
"updatedAt": 1771300100,
|
|
9
|
+
"updatedAtMs": 1771300100
|
|
10
|
+
},
|
|
11
|
+
"defaultModel": "gpt-5.3-codex-spark"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sessions": {
|
|
3
|
+
"defaults": {
|
|
4
|
+
"model": "gpt-5.3-codex",
|
|
5
|
+
"defaultModel": "gpt-5.3-codex-spark"
|
|
6
|
+
},
|
|
7
|
+
"recent": [
|
|
8
|
+
{
|
|
9
|
+
"agentId": "agent-1",
|
|
10
|
+
"sessionId": "a9f2d3",
|
|
11
|
+
"updatedAt": "1771278800000",
|
|
12
|
+
"age": "600000",
|
|
13
|
+
"total_tokens": "120",
|
|
14
|
+
"totalTokensFresh": "false",
|
|
15
|
+
"model": "gpt-5.3-codex",
|
|
16
|
+
"tokens_per_minute": "12.75"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"agentId": "agent-2",
|
|
20
|
+
"sessionId": "b7e1f8",
|
|
21
|
+
"updatedAt": 1771278820000,
|
|
22
|
+
"ageMs": "30000",
|
|
23
|
+
"totalTokens": "450",
|
|
24
|
+
"totalTokensFresh": "true",
|
|
25
|
+
"model": "claude-opus-4-6",
|
|
26
|
+
"tokensPerMinute": "45"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"agentId": "agent-3",
|
|
30
|
+
"sessionId": "f2c9aa",
|
|
31
|
+
"updatedAt": 1771278830000,
|
|
32
|
+
"age": 15000,
|
|
33
|
+
"totalTokens": null,
|
|
34
|
+
"model": "gpt-5.3-codex"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"current": {
|
|
4
|
+
"session": {
|
|
5
|
+
"sessionId": "ts-ms-updated-at-session",
|
|
6
|
+
"agentId": "agent-ts-ms-updated-at",
|
|
7
|
+
"model": "gpt-5.3-codex-spark",
|
|
8
|
+
"totalTokens": 2500,
|
|
9
|
+
"tokensPerMinute": 31.2,
|
|
10
|
+
"updated_at_ms": 1771325000000
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"current": {
|
|
4
|
+
"session": {
|
|
5
|
+
"sessionId": "ts-ms-underscore-session",
|
|
6
|
+
"agentId": "agent-ts-ms-underscore",
|
|
7
|
+
"model": "qwen3",
|
|
8
|
+
"totalTokens": 1717,
|
|
9
|
+
"tokensPerMinute": 22,
|
|
10
|
+
"usage_timestamp_ms": 1771321200000
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"result": {
|
|
3
|
+
"defaultModel": "gpt-4.1",
|
|
4
|
+
"defaults": {
|
|
5
|
+
"defaultModel": "gpt-4.1"
|
|
6
|
+
},
|
|
7
|
+
"sessions": [
|
|
8
|
+
{
|
|
9
|
+
"id": "wrapped-session-a",
|
|
10
|
+
"agentId": "agent-wrap",
|
|
11
|
+
"usage": {
|
|
12
|
+
"model": "claude-opus-4.6",
|
|
13
|
+
"totals": {
|
|
14
|
+
"totalTokens": "789"
|
|
15
|
+
},
|
|
16
|
+
"updatedAt": "2026-02-17T09:00:00Z",
|
|
17
|
+
"tokensPerMinute": "12.5",
|
|
18
|
+
"sessionId": "wrapped-session-a"
|
|
19
|
+
},
|
|
20
|
+
"age": 120000
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sessions": {
|
|
3
|
+
"defaults": {
|
|
4
|
+
"model": "gpt-5.3-codex",
|
|
5
|
+
"contextTokens": 272000
|
|
6
|
+
},
|
|
7
|
+
"recent": [
|
|
8
|
+
{
|
|
9
|
+
"agentId": "main",
|
|
10
|
+
"sessionId": "781280ba-01a6-49d9-9c75-09519f28f709",
|
|
11
|
+
"updatedAt": 1771278795632,
|
|
12
|
+
"age": 29308,
|
|
13
|
+
"totalTokens": null,
|
|
14
|
+
"totalTokensFresh": false,
|
|
15
|
+
"model": "gpt-5.3-codex"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"agentId": "main",
|
|
19
|
+
"sessionId": "45f23527-d596-469a-882c-9bba6dc8b5a0",
|
|
20
|
+
"updatedAt": 1771278693678,
|
|
21
|
+
"age": 131262,
|
|
22
|
+
"inputTokens": 1036,
|
|
23
|
+
"outputTokens": 96,
|
|
24
|
+
"totalTokens": 67980,
|
|
25
|
+
"totalTokensFresh": true,
|
|
26
|
+
"model": "gpt-5.3-codex"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"agentId": "main",
|
|
30
|
+
"sessionId": "90d2a820-6d77-42f0-8db4-12b90f9f7203",
|
|
31
|
+
"updatedAt": 1771278893678,
|
|
32
|
+
"age": 11000,
|
|
33
|
+
"inputTokens": 1200,
|
|
34
|
+
"outputTokens": 210,
|
|
35
|
+
"totalTokens": 70500,
|
|
36
|
+
"totalTokensFresh": true,
|
|
37
|
+
"model": "gpt-5.3-codex"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { gpuSampleDarwin, __gpuTestUtils } from '../src/gpu.js'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = path.dirname(__filename)
|
|
10
|
+
const fixture = (name) => fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8').trim()
|
|
11
|
+
|
|
12
|
+
const { parseIoregGpuUtilization, parseGpuPercent } = __gpuTestUtils
|
|
13
|
+
|
|
14
|
+
test('parses ioreg AGX performance statistics device utilization', () => {
|
|
15
|
+
const sample = fixture('gpu-agx.txt')
|
|
16
|
+
assert.equal(parseIoregGpuUtilization(sample), 34)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('parses ioreg IOGPU performance statistics when AGX is unavailable', () => {
|
|
20
|
+
const sample = fixture('gpu-iogpu.txt')
|
|
21
|
+
assert.equal(parseIoregGpuUtilization(sample), 1.2)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('parses powermetrics GPU residency percent', () => {
|
|
25
|
+
const sample = 'GPU HW active residency: 18.25%\nGPU SW requested state: 5'
|
|
26
|
+
assert.equal(parseGpuPercent(sample, 'powermetrics'), 18.25)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('parses top grep fallback output', () => {
|
|
30
|
+
const sample = fixture('gpu-top-grep.txt')
|
|
31
|
+
assert.equal(parseGpuPercent(sample, 'top-grep'), 7.8)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('gpu sampler captures three distinct macOS telemetry sources deterministically', () => {
|
|
35
|
+
const fakeExec = (cmd) => {
|
|
36
|
+
if (cmd.includes('AGXAccelerator')) {
|
|
37
|
+
return fixture('gpu-agx.txt')
|
|
38
|
+
}
|
|
39
|
+
if (cmd.includes('IOGPU')) {
|
|
40
|
+
return fixture('gpu-iogpu.txt')
|
|
41
|
+
}
|
|
42
|
+
if (cmd.includes('powermetrics')) {
|
|
43
|
+
return 'GPU HW active residency: 11.11%\nGPU SW requested state: 5'
|
|
44
|
+
}
|
|
45
|
+
if (cmd.includes('top -l 1 | grep -i \'GPU\'')) {
|
|
46
|
+
return fixture('gpu-top-grep.txt')
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`unexpected command in test: ${cmd}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const gpu = gpuSampleDarwin(fakeExec)
|
|
52
|
+
assert.deepEqual(gpu, {
|
|
53
|
+
pct: 34,
|
|
54
|
+
source: 'ioreg-agx',
|
|
55
|
+
confidence: 'high',
|
|
56
|
+
sampleWindowMs: null
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { parseMemoryPressurePct, memoryPressureDarwin } from '../src/memory.js'
|
|
4
|
+
|
|
5
|
+
test('parseMemoryPressurePct derives pressure from free percentage output', () => {
|
|
6
|
+
const raw = 'System-wide memory free percentage: 24%\nPages free: 12345.'
|
|
7
|
+
assert.equal(parseMemoryPressurePct(raw), 76)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('parseMemoryPressurePct supports explicit pressure output', () => {
|
|
11
|
+
const raw = 'System-wide memory pressure: 67.5%\n'
|
|
12
|
+
assert.equal(parseMemoryPressurePct(raw), 67.5)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('memoryPressureDarwin returns unavailable when command fails', () => {
|
|
16
|
+
const sample = memoryPressureDarwin(() => {
|
|
17
|
+
throw new Error('boom')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(sample, {
|
|
21
|
+
pct: null,
|
|
22
|
+
cls: 'unavailable',
|
|
23
|
+
source: 'unavailable'
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('memoryPressureDarwin returns classified value when parse succeeds', () => {
|
|
28
|
+
const sample = memoryPressureDarwin(() => 'System-wide memory free percentage: 12%')
|
|
29
|
+
|
|
30
|
+
assert.deepEqual(sample, {
|
|
31
|
+
pct: 88,
|
|
32
|
+
cls: 'warning',
|
|
33
|
+
source: 'memory_pressure'
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { loadLastGoodUsageSnapshot, persistLastGoodUsageSnapshot } from '../src/openclaw-cache.js'
|
|
7
|
+
|
|
8
|
+
test('persists and reloads last-good usage snapshot', () => {
|
|
9
|
+
const dir = mkdtempSync(path.join(tmpdir(), 'idlewatch-cache-test-'))
|
|
10
|
+
const cachePath = path.join(dir, 'last-good.json')
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
|
|
13
|
+
const saved = persistLastGoodUsageSnapshot(cachePath, {
|
|
14
|
+
at: now - 500,
|
|
15
|
+
usage: { model: 'gpt-5.3-codex', totalTokens: 12345 }
|
|
16
|
+
})
|
|
17
|
+
assert.equal(saved, true)
|
|
18
|
+
|
|
19
|
+
const loaded = loadLastGoodUsageSnapshot(cachePath, now)
|
|
20
|
+
assert.ok(loaded)
|
|
21
|
+
assert.equal(loaded.usage.model, 'gpt-5.3-codex')
|
|
22
|
+
assert.equal(loaded.usage.totalTokens, 12345)
|
|
23
|
+
assert.ok(Number.isFinite(loaded.ageMs) && loaded.ageMs >= 0)
|
|
24
|
+
|
|
25
|
+
const files = readdirSync(dir)
|
|
26
|
+
assert.ok(files.includes('last-good.json'))
|
|
27
|
+
assert.equal(files.some((name) => name.startsWith('last-good.json.tmp-')), false)
|
|
28
|
+
|
|
29
|
+
rmSync(dir, { recursive: true, force: true })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('returns null on corrupt cache payload', () => {
|
|
33
|
+
const dir = mkdtempSync(path.join(tmpdir(), 'idlewatch-cache-test-'))
|
|
34
|
+
const cachePath = path.join(dir, 'last-good.json')
|
|
35
|
+
|
|
36
|
+
persistLastGoodUsageSnapshot(cachePath, {
|
|
37
|
+
at: Date.now(),
|
|
38
|
+
usage: { model: 'ok' }
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// corrupt file after write
|
|
42
|
+
writeFileSync(cachePath, '{nope', 'utf8')
|
|
43
|
+
|
|
44
|
+
const loaded = loadLastGoodUsageSnapshot(cachePath)
|
|
45
|
+
assert.equal(loaded, null)
|
|
46
|
+
|
|
47
|
+
rmSync(dir, { recursive: true, force: true })
|
|
48
|
+
})
|