fix(plan): a Claude plan is the one Anthropic reports now, not the sign-in's
An account signed in on Pro and upgraded to Max 5x kept showing "Pro": the plan was read only from .credentials.json / .claude.json, whose subscriptionType and rate-limit tiers are written at sign-in. fetchOwner already asks GET /api/oauth/profile for a token's owner; it now also reads organization_type / rate_limit_tier / seat_tier into a plan (planFromProfile), remembered per account uuid (rememberLivePlan). The live row, saved profiles (identityOf) and pool accounts (poolStatus, whose quota refresh now asks a login's owner too) show that plan when one was seen (shownPlan), else planLabel of the files as before.
This commit is contained in:
@@ -448,8 +448,14 @@ describe("cc-profiles — identity and token disagree", () => {
|
||||
it("planLabel reads the plan from the rate-limit tier and seat, not subscriptionType alone", () => {
|
||||
const { planLabel } = profiles;
|
||||
// An upgrade keeps `subscriptionType: "pro"` from sign-in; the tier is the truth.
|
||||
assert.strictEqual(planLabel({ subscriptionType: "pro", rateLimitTier: "default_claude_max_5x" }, null), "Max 5x");
|
||||
assert.strictEqual(planLabel({ subscriptionType: "max", rateLimitTier: "default_claude_max_20x" }, null), "Max 20x");
|
||||
assert.strictEqual(
|
||||
planLabel({ subscriptionType: "pro", rateLimitTier: "default_claude_max_5x" }, null),
|
||||
"Max 5x"
|
||||
);
|
||||
assert.strictEqual(
|
||||
planLabel({ subscriptionType: "max", rateLimitTier: "default_claude_max_20x" }, null),
|
||||
"Max 20x"
|
||||
);
|
||||
assert.strictEqual(planLabel({ subscriptionType: "pro" }, null), "Pro");
|
||||
const team = { subscriptionType: "team", rateLimitTier: "default_raven" };
|
||||
assert.strictEqual(planLabel(team, { seatTier: "team_standard" }), "Team Standard");
|
||||
@@ -462,3 +468,31 @@ it("planLabel reads the plan from the rate-limit tier and seat, not subscription
|
||||
assert.strictEqual(planLabel(team, null), "Team");
|
||||
assert.strictEqual(planLabel(null, null), null);
|
||||
});
|
||||
|
||||
it("the profile endpoint's live plan wins over files written at sign-in", () => {
|
||||
const { planFromProfile, rememberLivePlan, shownPlan } = profiles;
|
||||
assert.strictEqual(
|
||||
planFromProfile({ organization_type: "claude_max", rate_limit_tier: "default_claude_max_5x" }),
|
||||
"Max 5x"
|
||||
);
|
||||
assert.strictEqual(
|
||||
planFromProfile({
|
||||
organization_type: "claude_team",
|
||||
rate_limit_tier: "default_raven",
|
||||
seat_tier: "team_standard",
|
||||
}),
|
||||
"Team Standard"
|
||||
);
|
||||
assert.strictEqual(planFromProfile({ organization_type: "claude_pro" }), "Pro");
|
||||
assert.strictEqual(planFromProfile(null), null);
|
||||
|
||||
// Signed in on Pro, upgraded to Max 5x since: the files still say Pro.
|
||||
const oauth = { subscriptionType: "pro", rateLimitTier: "default_claude_pro" };
|
||||
const account = { accountUuid: "acct-upgraded" };
|
||||
assert.strictEqual(shownPlan(oauth, account), "Pro");
|
||||
rememberLivePlan("acct-upgraded", "Max 5x");
|
||||
assert.strictEqual(shownPlan(oauth, account), "Max 5x");
|
||||
rememberLivePlan("acct-upgraded", null); // an unknown answer keeps the last one
|
||||
assert.strictEqual(shownPlan(oauth, account), "Max 5x");
|
||||
assert.strictEqual(shownPlan(oauth, { accountUuid: "other" }), "Pro");
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ const pty = require("../lib/pty-run");
|
||||
|
||||
const TOKEN = (c) => `sk-ant-oat01-${c.repeat(40)}`;
|
||||
const realFetchUsage = ccUsage.fetchUsage;
|
||||
const realFetchOwner = ccUsage.fetchOwner;
|
||||
|
||||
function run(id, account, { status = "completed", session = null, started, ended = null } = {}) {
|
||||
db.prepare(
|
||||
@@ -61,10 +62,12 @@ before(() => {
|
||||
ccUsage.fetchUsage = async () => {
|
||||
throw Object.assign(new Error("offline"), { code: "EUNAUTHORIZED" });
|
||||
};
|
||||
ccUsage.fetchOwner = async () => null;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
ccUsage.fetchUsage = realFetchUsage;
|
||||
ccUsage.fetchOwner = realFetchOwner;
|
||||
tmux.__setExecImpl(null);
|
||||
fs.rmSync(TMP, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ const claudePool = require("../lib/claude-pool");
|
||||
const pins = require("../lib/account-pins");
|
||||
|
||||
require("../lib/cc-usage").fetchUsage = async () => ({});
|
||||
require("../lib/cc-usage").fetchOwner = async () => null;
|
||||
|
||||
const KEY = (c) => `key_${c.repeat(40)}`;
|
||||
const TOKEN = (c) => `sk-ant-oat01-${c.repeat(40)}`;
|
||||
|
||||
@@ -30,6 +30,7 @@ const claudePool = require("../lib/claude-pool");
|
||||
const pins = require("../lib/account-pins");
|
||||
|
||||
require("../lib/cc-usage").fetchUsage = async () => ({});
|
||||
require("../lib/cc-usage").fetchOwner = async () => null;
|
||||
|
||||
const BOSS_PASS = "admin-pass-1";
|
||||
const KEY = (c) => `key_${c.repeat(40)}`;
|
||||
|
||||
@@ -170,6 +170,65 @@ test("a profile holding another account's token shows EWRONGACCOUNT, not that ac
|
||||
}
|
||||
});
|
||||
|
||||
test("the live plan is the profile endpoint's, not the one written at sign-in", async () => {
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { quotas, __resetCache } = require("../lib/quota");
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "quota-plan-"));
|
||||
const home = path.join(root, ".claude");
|
||||
fs.mkdirSync(home, { recursive: true });
|
||||
// Signed in on Pro; upgraded to Max 5x since — the files never learned.
|
||||
fs.writeFileSync(
|
||||
path.join(home, ".credentials.json"),
|
||||
JSON.stringify({
|
||||
claudeAiOauth: {
|
||||
accessToken: "live-upgraded",
|
||||
expiresAt: Date.now() + 3600_000,
|
||||
subscriptionType: "pro",
|
||||
rateLimitTier: "default_claude_pro",
|
||||
},
|
||||
})
|
||||
);
|
||||
fs.writeFileSync(
|
||||
`${home}.json`,
|
||||
JSON.stringify({ oauthAccount: { accountUuid: "uuid-up", emailAddress: "up@x.y" } })
|
||||
);
|
||||
const prev = { home: process.env.CLAUDE_HOME, fetch: global.fetch };
|
||||
process.env.CLAUDE_HOME = home;
|
||||
global.fetch = async (url) => ({
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: async () =>
|
||||
url.endsWith("/profile")
|
||||
? {
|
||||
account: { uuid: "uuid-up", email: "up@x.y" },
|
||||
organization: {
|
||||
organization_type: "claude_max",
|
||||
rate_limit_tier: "default_claude_max_5x",
|
||||
},
|
||||
}
|
||||
: { five_hour: { utilization: 10, resets_at: null } },
|
||||
});
|
||||
try {
|
||||
__resetCache();
|
||||
const live = (await quotas()).tools.find((t) => t.id === "claude" && t.active);
|
||||
assert.strictEqual(live.plan, "Max 5x");
|
||||
// Remembered: a later read from the files alone says the same.
|
||||
const { shownPlan } = require("../lib/cc-profiles");
|
||||
assert.strictEqual(
|
||||
shownPlan({ subscriptionType: "pro" }, { accountUuid: "uuid-up" }),
|
||||
"Max 5x"
|
||||
);
|
||||
} finally {
|
||||
if (prev.home === undefined) delete process.env.CLAUDE_HOME;
|
||||
else process.env.CLAUDE_HOME = prev.home;
|
||||
global.fetch = prev.fetch;
|
||||
__resetCache();
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("the live row is the account its token belongs to, not the one .claude.json names", async () => {
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
|
||||
@@ -29,6 +29,7 @@ const cursorPool = require("../lib/cursor-pool");
|
||||
|
||||
// The quota read would go to the network; the pool's list must not.
|
||||
require("../lib/cc-usage").fetchUsage = async () => ({});
|
||||
require("../lib/cc-usage").fetchOwner = async () => null;
|
||||
|
||||
let server;
|
||||
let BASE;
|
||||
|
||||
@@ -26,6 +26,7 @@ const auth = require("../lib/auth");
|
||||
|
||||
// The quota read would go to the network.
|
||||
require("../lib/cc-usage").fetchUsage = async () => ({});
|
||||
require("../lib/cc-usage").fetchOwner = async () => null;
|
||||
|
||||
let server;
|
||||
let BASE;
|
||||
|
||||
Reference in New Issue
Block a user