1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it as effectIt } from "@effect/vitest";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Fiber from "effect/Fiber";
8import * as Path from "effect/Path";
9import * as Schema from "effect/Schema";
10import * as TestClock from "effect/testing/TestClock";
11import { HttpClient, HttpClientResponse, UrlParams } from "effect/http";
12import { describe, expect, it } from "vite-plus/test";
13
14import * as ClaudeResetCredits from "./claudeResetCredits.ts";
15
16const NOW = Date.parse("2026-09-22T12:00:00.000Z");
17const grant = (overrides: Record<string, unknown>) => ({
18 id: "grant_a",
19 resets_left: 1,
20 usable_now: true,
21 ...overrides,
22});
23
24const writeLogin = Effect.gen(function* () {
25 const fs = yield* FileSystem.FileSystem;
26 const path = yield* Path.Path;
27 const directory = yield* fs.makeTempDirectoryScoped();
28 yield* fs.writeFileString(
29 path.join(directory, ".credentials.json"),
30 '{"claudeAiOauth":{"accessToken":"oauth-token"}}',
31 );
32 const accountConfigPath = path.join(directory, ".claude.json");
33 yield* fs.writeFileString(accountConfigPath, '{"oauthAccount":{"organizationUuid":"org-1"}}');
34 return { configDir: directory, accountConfigPath };
35});
36
37const respond = (status: number, body: unknown) =>
38 HttpClient.make((request) =>
39 Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(body, { status }))),
40 );
41const refuseRequests = HttpClient.make(() => Effect.die("must not send a request"));
42
43describe("claudeResetCreditsToContract", () => {
44 it("counts live grants and pins the next usable one", () => {
45 expect(
46 ClaudeResetCredits.claudeResetCreditsToContract(
47 {
48 eligible: true,
49 next_grant_id: "grant_a",
50 grants: [
51 grant({ resets_left: 2, ends_at: "2026-10-01T00:00:00Z" }),
52 grant({ id: "paused", paused: true }),
53 grant({ id: "expired", ends_at: "2026-09-01T00:00:00Z" }),
54 grant({ id: "garbled", ends_at: "not a date" }),
55 grant({ id: "date_only", ends_at: "2026-10-01" }),
56 grant({ id: "impossible", ends_at: "2027-02-30T00:00:00Z" }),
57 grant({ id: "empty", ends_at: "" }),
58 grant({ id: "Not Valid" }),
59 grant({ id: "grant_b", resets_left: 3, usable_now: false }),
60 ],
61 },
62 NOW,
63 ),
64 ).toEqual({
65 availableCount: 2,
66 nextCreditId: "grant_a",
67 nextExpiresAt: "2026-10-01T00:00:00.000Z",
68 });
69 });
70
71 it("offers nothing to redeem without a usable next grant or an eligible account", () => {
72 expect(
73 ClaudeResetCredits.claudeResetCreditsToContract(
74 { eligible: true, next_grant_id: "grant_a", grants: [grant({ usable_now: false })] },
75 NOW,
76 ),
77 ).toEqual({ availableCount: 0 });
78 expect(
79 ClaudeResetCredits.claudeResetCreditsToContract({ eligible: true, grants: [grant({})] }, NOW),
80 ).toEqual({
81 availableCount: 0,
82 });
83 expect(
84 ClaudeResetCredits.claudeResetCreditsToContract(
85 { eligible: false, grants: [grant({})] },
86 NOW,
87 ),
88 ).toBeUndefined();
89 expect(ClaudeResetCredits.claudeResetCreditsToContract(undefined, NOW)).toBeUndefined();
90 });
91});
92
93effectIt.layer(NodeServices.layer)("readClaudeResetCredits", (it) => {
94 it.effect("reads the grants with the CLI's request", () =>
95 Effect.gen(function* () {
96 const { configDir } = yield* writeLogin;
97 const client = HttpClient.make((request) => {
98 expect(request.method).toBe("GET");
99 expect(request.url).toBe("https://api.anthropic.com/api/oauth/usage");
100 expect(UrlParams.toString(request.urlParams)).toBe("cedar_ember=1&skip_spend=1");
101 expect(request.headers.authorization).toBe("Bearer oauth-token");
102 expect(request.headers["anthropic-beta"]).toBe("oauth-2025-04-20");
103 expect(request.headers["user-agent"]).toBe("claude-cli/2.1.0 (external, cli)");
104 return Effect.succeed(
105 HttpClientResponse.fromWeb(
106 request,
107 Response.json({
108 cedar_ember: { eligible: true, next_grant_id: "grant_a", grants: [grant({})] },
109 }),
110 ),
111 );
112 });
113 const credits = yield* ClaudeResetCredits.readClaudeResetCredits(configDir, "2.1.0").pipe(
114 Effect.provideService(HostProcess.Platform, "linux"),
115 Effect.provideService(HttpClient.HttpClient, client),
116 );
117 expect(credits).toEqual({ availableCount: 1, nextCreditId: "grant_a" });
118 }),
119 );
120
121 it.effect("reads nothing from keychain logins or failed requests", () =>
122 Effect.gen(function* () {
123 const { configDir } = yield* writeLogin;
124 const darwin = yield* ClaudeResetCredits.readClaudeResetCredits(configDir, "2.1.0").pipe(
125 Effect.provideService(HostProcess.Platform, "darwin"),
126 Effect.provideService(HttpClient.HttpClient, refuseRequests),
127 );
128 const limited = yield* ClaudeResetCredits.readClaudeResetCredits(configDir, "2.1.0").pipe(
129 Effect.provideService(HostProcess.Platform, "linux"),
130 Effect.provideService(HttpClient.HttpClient, respond(429, {})),
131 );
132 expect([darwin, limited]).toEqual([undefined, undefined]);
133 }),
134 );
135});
136
137const ClaimBody = Schema.fromJsonString(
138 Schema.Struct({ program: Schema.String, grant_id: Schema.String, request_id: Schema.String }),
139);
140const decodeClaimBody = Schema.decodeEffect(ClaimBody);
141
142const consume = (client: HttpClient.HttpClient, ids = { grantId: "grant_a", requestId: "r-1" }) =>
143 Effect.gen(function* () {
144 const login = yield* writeLogin;
145 return yield* ClaudeResetCredits.consumeClaudeResetCredit({
146 ...login,
147 version: "2.1.0",
148 ...ids,
149 }).pipe(
150 Effect.provideService(HostProcess.Platform, "linux"),
151 Effect.provideService(HttpClient.HttpClient, client),
152 Effect.result,
153 );
154 });
155
156effectIt.layer(NodeServices.layer)("consumeClaudeResetCredit", (it) => {
157 it.effect("claims the grant for the organization", () =>
158 Effect.gen(function* () {
159 const client = HttpClient.make((request) =>
160 Effect.gen(function* () {
161 expect(request.method).toBe("POST");
162 expect(request.url).toBe(
163 "https://api.anthropic.com/api/organizations/org-1/reset_rate_limits",
164 );
165 expect(request.headers.authorization).toBe("Bearer oauth-token");
166 const body =
167 request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "";
168 expect(yield* decodeClaimBody(body)).toEqual({
169 program: "cedar_ember",
170 grant_id: "grant_a",
171 request_id: "r-1",
172 });
173 return HttpClientResponse.fromWeb(request, Response.json({ result: "reset" }));
174 }).pipe(Effect.orDie),
175 );
176 expect(yield* consume(client)).toMatchObject({ _tag: "Success", success: "reset" });
177 }),
178 );
179
180 it.effect("maps each answer to an outcome or a failure", () =>
181 Effect.gen(function* () {
182 for (const [result, outcome] of [
183 ["not_limited", "nothingToReset"],
184 ["already_used", "alreadyRedeemed"],
185 ["ineligible", "noCredit"],
186 ] as const) {
187 expect(yield* consume(respond(200, { result }))).toMatchObject({ success: outcome });
188 }
189 for (const client of [
190 respond(200, { result: "cooldown" }),
191 respond(429, {}),
192 respond(401, {}),
193 ]) {
194 const result = yield* consume(client);
195 expect(result).toMatchObject({ _tag: "Failure" });
196 // Claude answered, so a retry must be a new claim.
197 if (result._tag === "Failure") {
198 expect(ClaudeResetCredits.isSettledClaudeResetCreditFailure(result.failure)).toBe(true);
199 }
200 }
201 // No answer, or Claude could not confirm the claim: a retry is the same claim.
202 for (const client of [respond(500, {}), respond(200, { result: "unavailable" })]) {
203 const unanswered = yield* consume(client);
204 expect(unanswered).toMatchObject({ _tag: "Failure" });
205 if (unanswered._tag === "Failure") {
206 expect(ClaudeResetCredits.isSettledClaudeResetCreditFailure(unanswered.failure)).toBe(
207 false,
208 );
209 }
210 }
211 }),
212 );
213
214 it.effect("times out a stalled claim body", () =>
215 Effect.gen(function* () {
216 const login = yield* writeLogin;
217 const readingBody = yield* Deferred.make<void>();
218 const client = HttpClient.make((request) => {
219 const response = HttpClientResponse.fromWeb(request, Response.json({ result: "reset" }));
220 Object.defineProperty(response, "json", {
221 value: Deferred.succeed(readingBody, undefined).pipe(Effect.andThen(Effect.never)),
222 });
223 return Effect.succeed(response);
224 });
225 const claim = yield* ClaudeResetCredits.consumeClaudeResetCredit({
226 ...login,
227 version: "2.1.0",
228 grantId: "grant_a",
229 requestId: "r-1",
230 }).pipe(
231 Effect.provideService(HostProcess.Platform, "linux"),
232 Effect.provideService(HttpClient.HttpClient, client),
233 Effect.result,
234 Effect.forkChild,
235 );
236 yield* Deferred.await(readingBody);
237 yield* TestClock.adjust("26 seconds");
238 expect(yield* Fiber.join(claim)).toMatchObject({
239 _tag: "Failure",
240 failure: {
241 _tag: "ClaudeResetCreditError",
242 reason: "requestFailed",
243 cause: { _tag: "TimeoutError" },
244 },
245 });
246 }).pipe(Effect.provide(TestClock.layer())),
247 );
248
249 it.effect("refuses malformed ids without sending anything", () =>
250 Effect.gen(function* () {
251 for (const ids of [
252 { grantId: "Bad Grant", requestId: "r-1" },
253 { grantId: "grant_a", requestId: "has space" },
254 ]) {
255 expect(yield* consume(refuseRequests, ids)).toMatchObject({ _tag: "Failure" });
256 }
257 }),
258 );
259});