| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { it as effectIt } from "@effect/vitest"; |
| 3 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 4 | import * as Deferred from "effect/Deferred"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | import * as Fiber from "effect/Fiber"; |
| 8 | import * as Path from "effect/Path"; |
| 9 | import * as Schema from "effect/Schema"; |
| 10 | import * as TestClock from "effect/testing/TestClock"; |
| 11 | import { HttpClient, HttpClientResponse, UrlParams } from "effect/http"; |
| 12 | import { describe, expect, it } from "vite-plus/test"; |
| 13 | |
| 14 | import * as ClaudeResetCredits from "./claudeResetCredits.ts"; |
| 15 | |
| 16 | const NOW = Date.parse("2026-09-22T12:00:00.000Z"); |
| 17 | const grant = (overrides: Record<string, unknown>) => ({ |
| 18 | id: "grant_a", |
| 19 | resets_left: 1, |
| 20 | usable_now: true, |
| 21 | ...overrides, |
| 22 | }); |
| 23 | |
| 24 | const 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 | |
| 37 | const respond = (status: number, body: unknown) => |
| 38 | HttpClient.make((request) => |
| 39 | Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(body, { status }))), |
| 40 | ); |
| 41 | const refuseRequests = HttpClient.make(() => Effect.die("must not send a request")); |
| 42 | |
| 43 | describe("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 | |
| 93 | effectIt.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 | |
| 137 | const ClaimBody = Schema.fromJsonString( |
| 138 | Schema.Struct({ program: Schema.String, grant_id: Schema.String, request_id: Schema.String }), |
| 139 | ); |
| 140 | const decodeClaimBody = Schema.decodeEffect(ClaimBody); |
| 141 | |
| 142 | const 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 | |
| 156 | effectIt.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 | }); |