agent-dealer 1.2.1 → 1.2.3
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/bundle/server/dist/adapters/linear-inbox.js +9 -5
- package/bundle/server/dist/capacity/adapter.js +2 -0
- package/bundle/server/dist/capacity/claude-events.js +45 -10
- package/bundle/server/dist/capacity/claude-events.test.js +7 -0
- package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
- package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
- package/bundle/server/dist/capacity/codex-app-server.js +100 -10
- package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
- package/bundle/server/dist/capacity/cursor-individual-credentials.js +425 -0
- package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +369 -0
- package/bundle/server/dist/capacity/cursor-individual.js +1037 -0
- package/bundle/server/dist/capacity/cursor-individual.test.js +1081 -0
- package/bundle/server/dist/capacity/muse-host.js +748 -0
- package/bundle/server/dist/capacity/muse-host.test.js +480 -0
- package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
- package/bundle/server/dist/capacity/muse.js +389 -91
- package/bundle/server/dist/capacity/muse.test.js +100 -61
- package/bundle/server/dist/capacity/service.js +1 -0
- package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
- package/bundle/server/dist/db/index.js +30 -0
- package/bundle/server/dist/db/schema.sql +27 -0
- package/bundle/server/dist/index.js +17 -2
- package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
- package/bundle/server/dist/repository/repository-mappings.js +46 -0
- package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
- package/bundle/server/dist/repository/runtime-capacity.js +8 -3
- package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
- package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
- package/bundle/server/dist/routes/index.js +58 -9
- package/bundle/server/dist/routes/intake-linear.test.js +55 -1
- package/bundle/server/dist/routes/repository-mappings.js +22 -0
- package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
- package/bundle/server/dist/routes/runtime-capacity.test.js +84 -8
- package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
- package/bundle/server/dist/runners/muse-serve-session.js +313 -0
- package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
- package/bundle/server/static-ui/assets/index-CYqRh_-S.css +1 -0
- package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
- package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/index.d.ts +6 -6
- package/bundle/shared/dist/linear-intake.d.ts +71 -0
- package/bundle/shared/dist/linear-intake.js +90 -0
- package/bundle/shared/dist/linear-intake.test.js +80 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
- package/bundle/shared/dist/runtime-capacity.js +60 -0
- package/bundle/shared/dist/runtime-capacity.test.js +1 -0
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +57 -0
- package/dist/doctor.js +211 -0
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +246 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-ChnI0F02.css +0 -1
- package/bundle/server/static-ui/assets/index-CyB1573Q.js +0 -60
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
// packages/server/src/capacity/claude-local-cache.test.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-268: Claude capacity local-first ladder — local cache 5H/1W plus a
|
|
4
|
+
// one-hour paid fallback. No test here performs a live provider request:
|
|
5
|
+
// every probe spawn goes through an injected fake runner.
|
|
6
|
+
import { test, before, beforeEach, afterEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-claude-cache-"));
|
|
12
|
+
const { migrate } = await import("../db/index.js");
|
|
13
|
+
const { createAgent } = await import("../repository/agents.js");
|
|
14
|
+
const { listCapacitySnapshots, clearAllCapacitySnapshots, } = await import("../repository/runtime-capacity.js");
|
|
15
|
+
const { parseNdjson } = await import("../runners/stream-json.js");
|
|
16
|
+
const { getRuntimeCapacitySnapshot } = await import("./service.js");
|
|
17
|
+
const { recordClaudeCapacityFromEvents } = await import("./claude-events.js");
|
|
18
|
+
const { CLAUDE_CACHE_FILE_ENV, CLAUDE_CAPACITY_REFRESH_ENV, buildClaudeProbeArgv, claudeCacheFilePath, extractClaudeCacheSubtree, ingestClaudeLocalCache, isClaudePaidFallbackEnabled, maybeProbeClaudeCapacity, newestValidClaudeObservationMs, parseClaudeCachedUtilization, probeDiagnosticLogPath, readClaudeLocalCache, refreshClaudeCapacityIfStale, resetClaudeCapacityRefreshState, runClaudeCapacityProbe, } = await import("./claude-local-cache.js");
|
|
19
|
+
const NOW_MS = Date.parse("2026-09-20T12:00:00.000Z");
|
|
20
|
+
const FIVE_HOUR_RESET_SEC = Math.floor(NOW_MS / 1000) + 2 * 3600;
|
|
21
|
+
const SEVEN_DAY_RESET_SEC = Math.floor(NOW_MS / 1000) + 3 * 24 * 3600;
|
|
22
|
+
let cacheDir;
|
|
23
|
+
let cacheFile;
|
|
24
|
+
const savedEnv = {};
|
|
25
|
+
function fullCacheFixture(fetchedAtMs) {
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
fetchedAtMs,
|
|
28
|
+
accountUuid: "acct-must-never-persist",
|
|
29
|
+
email: "someone@example.com",
|
|
30
|
+
credentials: { token: "secret-must-never-persist" },
|
|
31
|
+
extraUsage: { spendUsd: 12.34 },
|
|
32
|
+
experiments: ["exp-a"],
|
|
33
|
+
utilization: {
|
|
34
|
+
five_hour: { utilization: 0.17, resets_at: FIVE_HOUR_RESET_SEC },
|
|
35
|
+
seven_day: { utilization: 0.42, resets_at: SEVEN_DAY_RESET_SEC },
|
|
36
|
+
limits: [
|
|
37
|
+
{ name: "session", utilization: 0.17, resets_at: FIVE_HOUR_RESET_SEC },
|
|
38
|
+
{ name: "weekly_all", utilization: 0.42, resets_at: SEVEN_DAY_RESET_SEC },
|
|
39
|
+
{ name: "sonnet", utilization: 0.9, resets_at: SEVEN_DAY_RESET_SEC },
|
|
40
|
+
{ name: "extra_usage", utilization: 0.1, resets_at: SEVEN_DAY_RESET_SEC },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function probeStreamFixture(observedIso, costUsd = 0.0003) {
|
|
46
|
+
return [
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
type: "rate_limit_event",
|
|
49
|
+
timestamp: observedIso,
|
|
50
|
+
rate_limit_info: {
|
|
51
|
+
status: "allowed",
|
|
52
|
+
unifiedWindows: [
|
|
53
|
+
{ window: "five_hour", utilization: 0.2, resetsAt: FIVE_HOUR_RESET_SEC },
|
|
54
|
+
{ window: "seven_day", utilization: 0.4, resetsAt: SEVEN_DAY_RESET_SEC },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
type: "result",
|
|
60
|
+
is_error: false,
|
|
61
|
+
result: "ok",
|
|
62
|
+
total_cost_usd: costUsd,
|
|
63
|
+
}),
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
const throwingRunner = () => {
|
|
67
|
+
throw new Error("probe runner must not be called while disabled/fresh");
|
|
68
|
+
};
|
|
69
|
+
before(() => {
|
|
70
|
+
migrate();
|
|
71
|
+
createAgent({
|
|
72
|
+
name: "claude-cap-cache",
|
|
73
|
+
runtime: "claude_code",
|
|
74
|
+
deckId: "44444444-4444-4444-8444-444444444444",
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
clearAllCapacitySnapshots();
|
|
79
|
+
resetClaudeCapacityRefreshState();
|
|
80
|
+
cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-claude-cache-file-"));
|
|
81
|
+
cacheFile = path.join(cacheDir, "cachedUsageUtilization.json");
|
|
82
|
+
for (const key of [CLAUDE_CACHE_FILE_ENV, CLAUDE_CAPACITY_REFRESH_ENV]) {
|
|
83
|
+
savedEnv[key] = process.env[key];
|
|
84
|
+
}
|
|
85
|
+
process.env[CLAUDE_CACHE_FILE_ENV] = cacheFile;
|
|
86
|
+
delete process.env[CLAUDE_CAPACITY_REFRESH_ENV];
|
|
87
|
+
});
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
delete process.env[key];
|
|
92
|
+
else
|
|
93
|
+
process.env[key] = value;
|
|
94
|
+
}
|
|
95
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
96
|
+
});
|
|
97
|
+
test("fixture yields exactly Claude 5H/1W and drops account/extra fields", () => {
|
|
98
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 5 * 60_000));
|
|
99
|
+
const result = readClaudeLocalCache(NOW_MS);
|
|
100
|
+
assert.ok(result);
|
|
101
|
+
assert.equal(result.runtime, "claude_code");
|
|
102
|
+
assert.equal(result.unavailable.length, 0);
|
|
103
|
+
assert.equal(result.windows.length, 2);
|
|
104
|
+
const fiveHour = result.windows.find((w) => w.criticalRole === "five_hour");
|
|
105
|
+
const weekly = result.windows.find((w) => w.criticalRole === "weekly");
|
|
106
|
+
assert.ok(fiveHour && weekly);
|
|
107
|
+
assert.equal(fiveHour.windowKey, "claude_unified_five_hour");
|
|
108
|
+
assert.equal(fiveHour.providerBucket, "five_hour");
|
|
109
|
+
assert.equal(fiveHour.durationMinutes, 300);
|
|
110
|
+
assert.equal(fiveHour.usedFraction, 0.17);
|
|
111
|
+
assert.equal(weekly.windowKey, "claude_unified_seven_day");
|
|
112
|
+
assert.equal(weekly.providerBucket, "seven_day");
|
|
113
|
+
assert.equal(weekly.durationMinutes, 10080);
|
|
114
|
+
assert.equal(weekly.usedFraction, 0.42);
|
|
115
|
+
for (const w of result.windows) {
|
|
116
|
+
assert.equal(w.source, "observed_event");
|
|
117
|
+
assert.equal(w.evidenceRef, "claude-cache:cachedUsageUtilization");
|
|
118
|
+
// fetchedAtMs is the observed time.
|
|
119
|
+
assert.equal(w.observedAt, new Date(NOW_MS - 5 * 60_000).toISOString());
|
|
120
|
+
assert.ok(!JSON.stringify(w).includes("acct-must-never-persist"));
|
|
121
|
+
assert.ok(!JSON.stringify(w).includes("someone@example.com"));
|
|
122
|
+
assert.ok(!JSON.stringify(w).includes("secret-must-never-persist"));
|
|
123
|
+
}
|
|
124
|
+
// Model-specific/overage limits never become rows.
|
|
125
|
+
assert.equal(result.windows.some((w) => w.providerBucket === "sonnet" || w.providerBucket === "extra_usage"), false);
|
|
126
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
127
|
+
const rows = listCapacitySnapshots("claude_code");
|
|
128
|
+
assert.equal(rows.length, 2);
|
|
129
|
+
assert.ok(rows.every((r) => !JSON.stringify(r).includes("acct-must-never-persist")));
|
|
130
|
+
const snap = getRuntimeCapacitySnapshot(NOW_MS);
|
|
131
|
+
const claude = snap.runtimes.find((r) => r.runtime === "claude_code");
|
|
132
|
+
assert.equal(claude.windows.find((w) => w.displayLabel === "5H").remainingPercent, 83);
|
|
133
|
+
assert.equal(claude.windows.find((w) => w.displayLabel === "1W").remainingPercent, 58);
|
|
134
|
+
});
|
|
135
|
+
test("limits[] wins per role; named fields are the compatibility path", () => {
|
|
136
|
+
const raw = {
|
|
137
|
+
fetchedAtMs: NOW_MS - 60_000,
|
|
138
|
+
utilization: {
|
|
139
|
+
five_hour: { utilization: 0.99, resets_at: FIVE_HOUR_RESET_SEC },
|
|
140
|
+
seven_day: { utilization: 0.42, resets_at: SEVEN_DAY_RESET_SEC },
|
|
141
|
+
limits: [{ name: "session", utilization: 0.1, resets_at: FIVE_HOUR_RESET_SEC }],
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const readings = parseClaudeCachedUtilization(raw, NOW_MS);
|
|
145
|
+
assert.ok(readings);
|
|
146
|
+
// limits[] session (0.1) beats named five_hour (0.99); seven_day has no
|
|
147
|
+
// limits entry so the named field fills it.
|
|
148
|
+
assert.equal(readings.find((w) => w.criticalRole === "five_hour").usedFraction, 0.1);
|
|
149
|
+
assert.equal(readings.find((w) => w.criticalRole === "weekly").usedFraction, 0.42);
|
|
150
|
+
const namedOnly = parseClaudeCachedUtilization({
|
|
151
|
+
fetchedAtMs: NOW_MS - 60_000,
|
|
152
|
+
utilization: {
|
|
153
|
+
five_hour: { utilization: 0.2, resets_at: FIVE_HOUR_RESET_SEC },
|
|
154
|
+
seven_day: { utilization: 0.3, resets_at: SEVEN_DAY_RESET_SEC },
|
|
155
|
+
},
|
|
156
|
+
}, NOW_MS);
|
|
157
|
+
assert.ok(namedOnly);
|
|
158
|
+
assert.equal(namedOnly.length, 2);
|
|
159
|
+
});
|
|
160
|
+
test("limit role names are exact and case-insensitive; extras are dropped", () => {
|
|
161
|
+
const readings = parseClaudeCachedUtilization({
|
|
162
|
+
fetchedAtMs: NOW_MS - 60_000,
|
|
163
|
+
utilization: {
|
|
164
|
+
limits: [
|
|
165
|
+
{ name: "Session", utilization: 0.1, resets_at: FIVE_HOUR_RESET_SEC },
|
|
166
|
+
{ name: "WEEKLY_ALL", utilization: 0.2, resets_at: SEVEN_DAY_RESET_SEC },
|
|
167
|
+
{ name: "seven_day_sonnet", utilization: 0.9, resets_at: SEVEN_DAY_RESET_SEC },
|
|
168
|
+
{ name: "session_extra", utilization: 0.9, resets_at: FIVE_HOUR_RESET_SEC },
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
}, NOW_MS);
|
|
172
|
+
assert.ok(readings);
|
|
173
|
+
assert.equal(readings.length, 2);
|
|
174
|
+
// Near-miss names never claim the account-wide identity.
|
|
175
|
+
assert.ok(readings.every((w) => w.providerBucket === "five_hour" || w.providerBucket === "seven_day"));
|
|
176
|
+
});
|
|
177
|
+
test("future/missing fetchedAtMs rejects the whole observation", () => {
|
|
178
|
+
assert.equal(parseClaudeCachedUtilization({ fetchedAtMs: NOW_MS + 60_000, utilization: {} }, NOW_MS), null);
|
|
179
|
+
assert.equal(parseClaudeCachedUtilization({ utilization: {} }, NOW_MS), null);
|
|
180
|
+
assert.equal(parseClaudeCachedUtilization({ fetchedAtMs: "soon", utilization: {} }, NOW_MS), null);
|
|
181
|
+
assert.equal(parseClaudeCachedUtilization(null, NOW_MS), null);
|
|
182
|
+
assert.equal(parseClaudeCachedUtilization({ fetchedAtMs: NOW_MS, utilization: null }, NOW_MS), null);
|
|
183
|
+
// File-level: missing file, garbage JSON, and future cache all read null.
|
|
184
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS + 60_000));
|
|
185
|
+
assert.equal(readClaudeLocalCache(NOW_MS), null);
|
|
186
|
+
fs.writeFileSync(cacheFile, "{not json");
|
|
187
|
+
assert.equal(readClaudeLocalCache(NOW_MS), null);
|
|
188
|
+
fs.rmSync(cacheFile);
|
|
189
|
+
assert.equal(readClaudeLocalCache(NOW_MS), null);
|
|
190
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), null);
|
|
191
|
+
});
|
|
192
|
+
test("malformed scales and expired resets are rejected, never fabricated", () => {
|
|
193
|
+
// Bare `utilization` is dual-scale (≤ 1 fraction, above percent to 100 —
|
|
194
|
+
// the real cache carries percent values like 9 there), so 1.5 is a valid
|
|
195
|
+
// 1.5%, not malformed. Out-of-range on both scales stays rejected, as do
|
|
196
|
+
// explicit fraction spellings above 1.
|
|
197
|
+
const bad = [
|
|
198
|
+
{ utilization: 101, resets_at: FIVE_HOUR_RESET_SEC },
|
|
199
|
+
{ utilization: -0.1, resets_at: FIVE_HOUR_RESET_SEC },
|
|
200
|
+
{ utilization: "high", resets_at: FIVE_HOUR_RESET_SEC },
|
|
201
|
+
{ utilization: null, resets_at: FIVE_HOUR_RESET_SEC },
|
|
202
|
+
{ usedPercent: 101, resets_at: FIVE_HOUR_RESET_SEC },
|
|
203
|
+
{ usedFraction: 1.5, resets_at: FIVE_HOUR_RESET_SEC },
|
|
204
|
+
{ percent: -1, resets_at: FIVE_HOUR_RESET_SEC },
|
|
205
|
+
];
|
|
206
|
+
for (const entry of bad) {
|
|
207
|
+
assert.equal(parseClaudeCachedUtilization({ fetchedAtMs: NOW_MS - 60_000, utilization: { five_hour: entry } }, NOW_MS), null);
|
|
208
|
+
}
|
|
209
|
+
// Expired reset (at or before observed time) drops the window; the fresh
|
|
210
|
+
// sibling still normalizes.
|
|
211
|
+
const partial = parseClaudeCachedUtilization({
|
|
212
|
+
fetchedAtMs: NOW_MS - 60_000,
|
|
213
|
+
utilization: {
|
|
214
|
+
five_hour: { utilization: 0.2, resets_at: Math.floor(NOW_MS / 1000) - 3600 },
|
|
215
|
+
seven_day: { utilization: 0.3, resets_at: SEVEN_DAY_RESET_SEC },
|
|
216
|
+
},
|
|
217
|
+
}, NOW_MS);
|
|
218
|
+
assert.ok(partial);
|
|
219
|
+
assert.equal(partial.length, 1);
|
|
220
|
+
assert.equal(partial[0].criticalRole, "weekly");
|
|
221
|
+
});
|
|
222
|
+
test("stale cache renders N/A with honest age; older cache never overwrites a newer event", () => {
|
|
223
|
+
// A Dealer event observed 10 minutes ago.
|
|
224
|
+
const eventAt = NOW_MS - 10 * 60_000;
|
|
225
|
+
const eventNdjson = JSON.stringify({
|
|
226
|
+
type: "rate_limit_event",
|
|
227
|
+
timestamp: new Date(eventAt).toISOString(),
|
|
228
|
+
rate_limit_info: {
|
|
229
|
+
status: "allowed",
|
|
230
|
+
unifiedWindows: [{ window: "five_hour", utilization: 0.5, resetsAt: FIVE_HOUR_RESET_SEC }],
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
assert.equal(recordClaudeCapacityFromEvents(parseNdjson(eventNdjson), "claude_code", NOW_MS, eventAt), 1);
|
|
234
|
+
// An older cache (20 minutes ago) cannot clobber it.
|
|
235
|
+
fs.writeFileSync(cacheFile, JSON.stringify({
|
|
236
|
+
fetchedAtMs: NOW_MS - 20 * 60_000,
|
|
237
|
+
utilization: {
|
|
238
|
+
five_hour: { utilization: 0.1, resets_at: FIVE_HOUR_RESET_SEC },
|
|
239
|
+
seven_day: { utilization: 0.3, resets_at: SEVEN_DAY_RESET_SEC },
|
|
240
|
+
},
|
|
241
|
+
}));
|
|
242
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 1);
|
|
243
|
+
const rows = listCapacitySnapshots("claude_code");
|
|
244
|
+
// five_hour keeps the newer event reading (50% remaining); seven_day is
|
|
245
|
+
// new from the cache.
|
|
246
|
+
assert.equal(rows.find((r) => r.providerBucket === "five_hour").remainingPercent, 50);
|
|
247
|
+
assert.equal(rows.find((r) => r.providerBucket === "five_hour").observedAt, new Date(eventAt).toISOString());
|
|
248
|
+
assert.equal(rows.find((r) => r.providerBucket === "seven_day").remainingPercent, 70);
|
|
249
|
+
// A newer cache does win its own window.
|
|
250
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 5 * 60_000));
|
|
251
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
252
|
+
assert.equal(listCapacitySnapshots("claude_code").find((r) => r.providerBucket === "five_hour")
|
|
253
|
+
.remainingPercent, 83);
|
|
254
|
+
});
|
|
255
|
+
test("a 2-hour-old cache ingests with its true age and reads expired, never live", () => {
|
|
256
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 2 * 3600_000));
|
|
257
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
258
|
+
const snap = getRuntimeCapacitySnapshot(NOW_MS);
|
|
259
|
+
const claude = snap.runtimes.find((r) => r.runtime === "claude_code");
|
|
260
|
+
assert.ok(claude.windows.length > 0);
|
|
261
|
+
for (const w of claude.windows) {
|
|
262
|
+
assert.equal(w.remainingPercent, null);
|
|
263
|
+
assert.equal(w.unavailableReason, "expired");
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
test("disabled fallback never spawns: every non-opt-in value is a strict no-op", async () => {
|
|
267
|
+
for (const value of [undefined, "", "off", "auto", "paid-after-1h "]) {
|
|
268
|
+
if (value === undefined)
|
|
269
|
+
delete process.env[CLAUDE_CAPACITY_REFRESH_ENV];
|
|
270
|
+
else
|
|
271
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = value;
|
|
272
|
+
assert.equal(isClaudePaidFallbackEnabled(), false);
|
|
273
|
+
const outcome = await maybeProbeClaudeCapacity(NOW_MS, { runner: throwingRunner });
|
|
274
|
+
assert.deepEqual(outcome, { probed: false, reason: "disabled" });
|
|
275
|
+
}
|
|
276
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
277
|
+
assert.equal(isClaudePaidFallbackEnabled(), true);
|
|
278
|
+
});
|
|
279
|
+
test("fresh sample suppresses the probe; stale data triggers exactly one", async () => {
|
|
280
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
281
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 10 * 60_000));
|
|
282
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
283
|
+
assert.ok((newestValidClaudeObservationMs(NOW_MS) ?? 0) > NOW_MS - 60 * 60_000);
|
|
284
|
+
const fresh = await maybeProbeClaudeCapacity(NOW_MS, { runner: throwingRunner });
|
|
285
|
+
assert.deepEqual(fresh, { probed: false, reason: "fresh" });
|
|
286
|
+
// All samples ≥60 minutes old: five concurrent readers share one probe.
|
|
287
|
+
clearAllCapacitySnapshots();
|
|
288
|
+
resetClaudeCapacityRefreshState();
|
|
289
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 61 * 60_000));
|
|
290
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
291
|
+
let calls = 0;
|
|
292
|
+
const runner = async () => {
|
|
293
|
+
calls++;
|
|
294
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
295
|
+
return { stdout: probeStreamFixture(new Date(NOW_MS).toISOString()), exitCode: 0, timedOut: false, spawnError: null };
|
|
296
|
+
};
|
|
297
|
+
const outcomes = await Promise.all(Array.from({ length: 5 }, () => maybeProbeClaudeCapacity(NOW_MS, { runner })));
|
|
298
|
+
assert.equal(calls, 1);
|
|
299
|
+
assert.equal(outcomes.filter((o) => o.reason === "completed").length, 1);
|
|
300
|
+
assert.equal(outcomes.filter((o) => o.reason === "shared").length, 4);
|
|
301
|
+
assert.ok(outcomes.every((o) => o.probed && o.ok));
|
|
302
|
+
});
|
|
303
|
+
test("probe argv pins the fixed prompt, cheapest model, no tools/MCP, one turn, ≤$0.01", async () => {
|
|
304
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
305
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 61 * 60_000));
|
|
306
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
307
|
+
let seenBin = "";
|
|
308
|
+
let seenArgv = [];
|
|
309
|
+
let seenCwd = "";
|
|
310
|
+
const runner = async (bin, argv, opts) => {
|
|
311
|
+
seenBin = bin;
|
|
312
|
+
seenArgv = argv;
|
|
313
|
+
seenCwd = opts.cwd;
|
|
314
|
+
return { stdout: probeStreamFixture(new Date(NOW_MS).toISOString()), exitCode: 0, timedOut: false, spawnError: null };
|
|
315
|
+
};
|
|
316
|
+
const outcome = await maybeProbeClaudeCapacity(NOW_MS, { runner, bin: "/fake/claude" });
|
|
317
|
+
assert.equal(outcome.probed, true);
|
|
318
|
+
assert.equal(seenBin, "/fake/claude");
|
|
319
|
+
assert.deepEqual(seenArgv, [
|
|
320
|
+
"-p",
|
|
321
|
+
"Reply with exactly: ok",
|
|
322
|
+
"--model",
|
|
323
|
+
"haiku",
|
|
324
|
+
"--max-turns",
|
|
325
|
+
"1",
|
|
326
|
+
"--tools",
|
|
327
|
+
"",
|
|
328
|
+
"--strict-mcp-config",
|
|
329
|
+
"--no-session-persistence",
|
|
330
|
+
"--output-format",
|
|
331
|
+
"stream-json",
|
|
332
|
+
"--verbose",
|
|
333
|
+
"--max-budget-usd",
|
|
334
|
+
"0.01",
|
|
335
|
+
]);
|
|
336
|
+
// Semantic pins behind the exact match: cheapest model, hard one-turn
|
|
337
|
+
// bound, tools fully off, MCP restricted to none passed, stream JSON,
|
|
338
|
+
// hard budget cap.
|
|
339
|
+
assert.equal(seenArgv[seenArgv.indexOf("--model") + 1], "haiku");
|
|
340
|
+
assert.equal(seenArgv[seenArgv.indexOf("--max-turns") + 1], "1");
|
|
341
|
+
assert.equal(seenArgv[seenArgv.indexOf("--tools") + 1], "");
|
|
342
|
+
assert.ok(!seenArgv.includes("--mcp-config"));
|
|
343
|
+
const budget = Number(seenArgv[seenArgv.indexOf("--max-budget-usd") + 1]);
|
|
344
|
+
assert.ok(Number.isFinite(budget) && budget <= 0.01);
|
|
345
|
+
assert.equal(seenArgv.filter((a) => a === "-p").length, 1);
|
|
346
|
+
// No worktree, no repo checkout — the probe runs in the OS temp dir.
|
|
347
|
+
assert.equal(seenCwd, os.tmpdir());
|
|
348
|
+
assert.deepEqual(buildClaudeProbeArgv(), seenArgv);
|
|
349
|
+
});
|
|
350
|
+
test("probe success updates both 5H and 1W and records cost without prompt/output", async () => {
|
|
351
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
352
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 61 * 60_000));
|
|
353
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
354
|
+
const marker = "probe-stream-marker-must-never-reach-log";
|
|
355
|
+
const runner = async () => ({
|
|
356
|
+
stdout: `${probeStreamFixture(new Date(NOW_MS).toISOString(), 0.0007)}\n${JSON.stringify({ type: "assistant", text: marker })}`,
|
|
357
|
+
exitCode: 0,
|
|
358
|
+
timedOut: false,
|
|
359
|
+
spawnError: null,
|
|
360
|
+
});
|
|
361
|
+
const result = await runClaudeCapacityProbe(NOW_MS, { runner });
|
|
362
|
+
assert.equal(result.ok, true);
|
|
363
|
+
assert.equal(result.failureKind, null);
|
|
364
|
+
assert.equal(result.costUsd, 0.0007);
|
|
365
|
+
assert.deepEqual(result.windowsUpdated, ["claude_unified_five_hour", "claude_unified_seven_day"]);
|
|
366
|
+
const rows = listCapacitySnapshots("claude_code");
|
|
367
|
+
assert.equal(rows.find((r) => r.providerBucket === "five_hour").remainingPercent, 80);
|
|
368
|
+
assert.equal(rows.find((r) => r.providerBucket === "seven_day").remainingPercent, 60);
|
|
369
|
+
assert.equal(rows.find((r) => r.providerBucket === "five_hour").observedAt, new Date(NOW_MS).toISOString());
|
|
370
|
+
const logText = fs.readFileSync(probeDiagnosticLogPath(), "utf8");
|
|
371
|
+
assert.ok(logText.includes('"costUsd":0.0007'));
|
|
372
|
+
assert.ok(!logText.includes("Reply with exactly"));
|
|
373
|
+
assert.ok(!logText.includes(marker));
|
|
374
|
+
});
|
|
375
|
+
test("probe success via the cache side effect when the stream carries no windows", async () => {
|
|
376
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
377
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 61 * 60_000));
|
|
378
|
+
// The stream is empty but the probe run itself refreshes Claude's own
|
|
379
|
+
// cache file — the re-read must count as success.
|
|
380
|
+
const runner = async () => {
|
|
381
|
+
// A real probe refreshes Claude's own cache file while it runs, so the
|
|
382
|
+
// new `fetchedAtMs` is later than the pre-spawn `NOW_MS` stamp — the
|
|
383
|
+
// post-spawn re-read must accept it, not reject it as future.
|
|
384
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS + 5_000));
|
|
385
|
+
return { stdout: "", exitCode: 0, timedOut: false, spawnError: null };
|
|
386
|
+
};
|
|
387
|
+
const result = await runClaudeCapacityProbe(NOW_MS, { runner });
|
|
388
|
+
assert.equal(result.ok, true);
|
|
389
|
+
assert.equal(result.failureKind, null);
|
|
390
|
+
assert.deepEqual(result.windowsUpdated, ["claude_unified_five_hour", "claude_unified_seven_day"]);
|
|
391
|
+
const rows = listCapacitySnapshots("claude_code");
|
|
392
|
+
assert.equal(rows.find((r) => r.providerBucket === "five_hour").remainingPercent, 83);
|
|
393
|
+
assert.equal(rows.find((r) => r.providerBucket === "five_hour").observedAt, new Date(NOW_MS + 5_000).toISOString());
|
|
394
|
+
});
|
|
395
|
+
test("probe failure preserves last-good rows and enters bounded backoff", async () => {
|
|
396
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
397
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 61 * 60_000));
|
|
398
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
399
|
+
let calls = 0;
|
|
400
|
+
const failing = async () => {
|
|
401
|
+
calls++;
|
|
402
|
+
return { stdout: "not-json\n", exitCode: 1, timedOut: false, spawnError: null };
|
|
403
|
+
};
|
|
404
|
+
const first = await maybeProbeClaudeCapacity(NOW_MS, { runner: failing });
|
|
405
|
+
assert.equal(first.probed, true);
|
|
406
|
+
assert.equal(first.ok, false);
|
|
407
|
+
assert.equal(first.failureKind, "nonzero_exit");
|
|
408
|
+
// Last-good rows are untouched by the failed probe (0.17 used → 83 left).
|
|
409
|
+
const rows = listCapacitySnapshots("claude_code");
|
|
410
|
+
assert.equal(rows.find((r) => r.providerBucket === "five_hour").remainingPercent, 83);
|
|
411
|
+
// Never retry per UI poll: the next read backs off.
|
|
412
|
+
const second = await maybeProbeClaudeCapacity(NOW_MS + 60_000, { runner: failing });
|
|
413
|
+
assert.deepEqual(second, { probed: false, reason: "backoff" });
|
|
414
|
+
assert.equal(calls, 1);
|
|
415
|
+
// Exponential backoff: one failure doubles the 60-minute cooldown.
|
|
416
|
+
const during = await maybeProbeClaudeCapacity(NOW_MS + 61 * 60_000, { runner: failing });
|
|
417
|
+
assert.deepEqual(during, { probed: false, reason: "backoff" });
|
|
418
|
+
const after = await maybeProbeClaudeCapacity(NOW_MS + 121 * 60_000, { runner: failing });
|
|
419
|
+
assert.equal(after.probed, true);
|
|
420
|
+
assert.equal(calls, 2);
|
|
421
|
+
});
|
|
422
|
+
test("spawn failure is a bounded backoff, not a throw", async () => {
|
|
423
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
424
|
+
const outcome = await maybeProbeClaudeCapacity(NOW_MS, {
|
|
425
|
+
runner: async () => ({ stdout: "", exitCode: null, timedOut: false, spawnError: "ENOENT" }),
|
|
426
|
+
});
|
|
427
|
+
assert.equal(outcome.probed, true);
|
|
428
|
+
assert.equal(outcome.ok, false);
|
|
429
|
+
assert.equal(outcome.failureKind, "spawn");
|
|
430
|
+
});
|
|
431
|
+
test("full refresh ingests free cache first and skips the probe when fresh", async () => {
|
|
432
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
433
|
+
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 10 * 60_000));
|
|
434
|
+
const outcome = await refreshClaudeCapacityIfStale(NOW_MS, { runner: throwingRunner });
|
|
435
|
+
assert.deepEqual(outcome, { probed: false, reason: "fresh" });
|
|
436
|
+
assert.equal(listCapacitySnapshots("claude_code").length, 2);
|
|
437
|
+
});
|
|
438
|
+
test("cache file override resolves from env, defaulting to ~/.claude.json", () => {
|
|
439
|
+
assert.equal(claudeCacheFilePath(), cacheFile);
|
|
440
|
+
delete process.env[CLAUDE_CACHE_FILE_ENV];
|
|
441
|
+
assert.equal(claudeCacheFilePath(), path.join(process.env.HOME ?? os.homedir(), ".claude.json"));
|
|
442
|
+
});
|
|
443
|
+
test("a full ~/.claude.json-shaped file yields 5H/1W from the subtree only", () => {
|
|
444
|
+
// Mirrors the observed real config at 2.1.283: percent-scale named
|
|
445
|
+
// fields, kind/group/percent limits[], ISO resets, and sensitive
|
|
446
|
+
// top-level + extra-usage siblings that must never persist.
|
|
447
|
+
const fetchedAtMs = NOW_MS - 5 * 60_000;
|
|
448
|
+
const observedIso = new Date(fetchedAtMs).toISOString();
|
|
449
|
+
fs.writeFileSync(cacheFile, JSON.stringify({
|
|
450
|
+
userID: "user-secret-must-never-persist",
|
|
451
|
+
email: "someone@example.com",
|
|
452
|
+
projects: { "/tmp/x": { history: ["sensitive"] } },
|
|
453
|
+
cachedUsageUtilization: {
|
|
454
|
+
fetchedAtMs,
|
|
455
|
+
accountUuid: "acct-must-never-persist",
|
|
456
|
+
utilization: {
|
|
457
|
+
five_hour: {
|
|
458
|
+
utilization: 99,
|
|
459
|
+
resets_at: new Date(FIVE_HOUR_RESET_SEC * 1000).toISOString(),
|
|
460
|
+
},
|
|
461
|
+
seven_day: {
|
|
462
|
+
utilization: 88,
|
|
463
|
+
resets_at: new Date(SEVEN_DAY_RESET_SEC * 1000).toISOString(),
|
|
464
|
+
},
|
|
465
|
+
seven_day_sonnet: {
|
|
466
|
+
utilization: 5,
|
|
467
|
+
resets_at: new Date(SEVEN_DAY_RESET_SEC * 1000).toISOString(),
|
|
468
|
+
},
|
|
469
|
+
extra_usage: { is_enabled: true, used_credits: 4878, utilization: 69.68 },
|
|
470
|
+
limits: [
|
|
471
|
+
{
|
|
472
|
+
kind: "session",
|
|
473
|
+
group: "session",
|
|
474
|
+
percent: 9,
|
|
475
|
+
severity: "normal",
|
|
476
|
+
resets_at: new Date(FIVE_HOUR_RESET_SEC * 1000).toISOString(),
|
|
477
|
+
scope: null,
|
|
478
|
+
is_active: false,
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
kind: "weekly_all",
|
|
482
|
+
group: "weekly",
|
|
483
|
+
percent: 23,
|
|
484
|
+
severity: "normal",
|
|
485
|
+
resets_at: new Date(SEVEN_DAY_RESET_SEC * 1000).toISOString(),
|
|
486
|
+
scope: null,
|
|
487
|
+
is_active: true,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
}));
|
|
493
|
+
// The subtree extractor drops the envelope before parsing.
|
|
494
|
+
const extracted = extractClaudeCacheSubtree(JSON.parse(fs.readFileSync(cacheFile, "utf8")));
|
|
495
|
+
assert.equal(typeof extracted.fetchedAtMs, "number");
|
|
496
|
+
assert.ok(!JSON.stringify(extracted).includes("user-secret-must-never-persist"));
|
|
497
|
+
const result = readClaudeLocalCache(NOW_MS);
|
|
498
|
+
assert.ok(result);
|
|
499
|
+
assert.equal(result.windows.length, 2);
|
|
500
|
+
// limits[] (kind/group/percent) wins over the named fields per role.
|
|
501
|
+
const fiveHour = result.windows.find((w) => w.criticalRole === "five_hour");
|
|
502
|
+
const weekly = result.windows.find((w) => w.criticalRole === "weekly");
|
|
503
|
+
assert.equal(fiveHour.usedPercent, 9);
|
|
504
|
+
assert.equal(fiveHour.usedUnit, "percent");
|
|
505
|
+
assert.equal(fiveHour.observedAt, observedIso);
|
|
506
|
+
assert.equal(weekly.usedPercent, 23);
|
|
507
|
+
assert.equal(weekly.observedAt, observedIso);
|
|
508
|
+
for (const w of result.windows) {
|
|
509
|
+
const text = JSON.stringify(w);
|
|
510
|
+
assert.ok(!text.includes("acct-must-never-persist"));
|
|
511
|
+
assert.ok(!text.includes("user-secret-must-never-persist"));
|
|
512
|
+
assert.ok(!text.includes("someone@example.com"));
|
|
513
|
+
assert.ok(!text.includes("4878"));
|
|
514
|
+
}
|
|
515
|
+
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
516
|
+
const snap = getRuntimeCapacitySnapshot(NOW_MS);
|
|
517
|
+
const claude = snap.runtimes.find((r) => r.runtime === "claude_code");
|
|
518
|
+
assert.equal(claude.windows.find((w) => w.displayLabel === "5H").remainingPercent, 91);
|
|
519
|
+
assert.equal(claude.windows.find((w) => w.displayLabel === "1W").remainingPercent, 77);
|
|
520
|
+
});
|