feat(worker): add OAuth usage client + poller

Adds Usage/ with ClaudeOAuthUsageClient (reads the access token Claude
Code keeps fresh at ~/.claude/.credentials.json, calls the Anthropic
OAuth usage endpoint, defensively parses buckets/limits), UsageState
(threadsafe last-good-snapshot holder that never regresses on
failure), and UsageMonitorService (BackgroundService polling on the
new usage_poll_interval_seconds config, min 15s, one poll at startup,
warns at most once per distinct error).
This commit is contained in:
mika kuns
2026-08-05 10:00:25 +02:00
parent 8d7ba1e314
commit 20d17c6887
11 changed files with 619 additions and 0 deletions
@@ -0,0 +1,48 @@
using ClaudeDo.Worker.Usage;
namespace ClaudeDo.Worker.Tests.Usage;
public sealed class UsageStateTests
{
private static UsageSnapshot MakeSnapshot(double fiveHourUtilization) => new(
new UsageBucket(fiveHourUtilization, null),
null,
[],
DateTime.UtcNow);
[Fact]
public void ReportSuccess_SetsSnapshotAndClearsError()
{
var state = new UsageState();
state.ReportFailure("boom", DateTime.UtcNow);
state.ReportSuccess(MakeSnapshot(42));
Assert.NotNull(state.Snapshot);
Assert.Equal(42, state.Snapshot!.FiveHour!.Utilization);
Assert.Null(state.LastError);
}
[Fact]
public void ReportFailure_AfterSuccess_KeepsOldSnapshotButSetsError()
{
var state = new UsageState();
state.ReportSuccess(MakeSnapshot(10));
state.ReportFailure("network down", DateTime.UtcNow);
Assert.NotNull(state.Snapshot);
Assert.Equal(10, state.Snapshot!.FiveHour!.Utilization);
Assert.Equal("network down", state.LastError);
}
[Fact]
public void NoAttemptYet_SnapshotAndErrorAreNull()
{
var state = new UsageState();
Assert.Null(state.Snapshot);
Assert.Null(state.LastError);
Assert.Null(state.LastAttemptUtc);
}
}