mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(platform): address remaining should-fix items for rate-limit tiering
- Add docstring noting SubscriptionTier mirrors schema.prisma enum and
can be replaced with prisma.enums import after prisma generate
- Remove unnecessary JSDoc comments from useRateLimitManager helpers
per frontend code convention (avoid comments unless complex)
- Add audit trail: log old tier when admin changes a user's tier
- Fix stale test assertion (DEFAULT_TIER is FREE, not PRO)
- Show tier label ("Pro plan") in UsagePanelContent for end users
- Add formatResetTime unit tests (UsagePanelContent.test.ts)
- Add tier label display test in UsageLimits.test.tsx
- Fix pre-existing pyright errors from prisma stubs not having
subscriptionTier (type: ignore until prisma generate is run)
This commit is contained in:
@@ -188,10 +188,12 @@ async def set_user_rate_limit_tier(
|
||||
admin_user_id: str = Security(get_user_id),
|
||||
) -> UserTierResponse:
|
||||
"""Set a user's rate-limit tier. Admin-only."""
|
||||
old_tier = await get_user_tier(request.user_id)
|
||||
logger.info(
|
||||
"Admin %s setting tier for user %s to %s",
|
||||
"Admin %s changing tier for user %s: %s -> %s",
|
||||
admin_user_id,
|
||||
request.user_id,
|
||||
old_tier.value,
|
||||
request.tier.value,
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -189,7 +189,7 @@ async def test_create_store_submission(mocker):
|
||||
notifyOnAgentApproved=True,
|
||||
notifyOnAgentRejected=True,
|
||||
timezone="Europe/Delft",
|
||||
subscriptionTier=prisma.enums.SubscriptionTier.FREE,
|
||||
subscriptionTier=prisma.enums.SubscriptionTier.FREE, # type: ignore[reportCallIssue,reportAttributeAccessIssue]
|
||||
)
|
||||
mock_agent = prisma.models.AgentGraph(
|
||||
id="agent-id",
|
||||
|
||||
@@ -30,7 +30,13 @@ _USAGE_KEY_PREFIX = "copilot:usage"
|
||||
|
||||
|
||||
class SubscriptionTier(str, Enum):
|
||||
"""Subscription tiers with increasing token allowances."""
|
||||
"""Subscription tiers with increasing token allowances.
|
||||
|
||||
Mirrors the ``SubscriptionTier`` enum in ``schema.prisma``.
|
||||
Once ``prisma generate`` is run, this can be replaced with::
|
||||
|
||||
from prisma.enums import SubscriptionTier
|
||||
"""
|
||||
|
||||
FREE = "FREE"
|
||||
PRO = "PRO"
|
||||
@@ -393,8 +399,8 @@ async def _fetch_user_tier(user_id: str) -> SubscriptionTier:
|
||||
edge cases (e.g. partial row creation).
|
||||
"""
|
||||
user = await PrismaUser.prisma().find_unique(where={"id": user_id})
|
||||
if user and user.subscriptionTier:
|
||||
return SubscriptionTier(user.subscriptionTier)
|
||||
if user and user.subscriptionTier: # type: ignore[reportAttributeAccessIssue]
|
||||
return SubscriptionTier(user.subscriptionTier) # type: ignore[reportAttributeAccessIssue]
|
||||
return DEFAULT_TIER
|
||||
|
||||
|
||||
|
||||
@@ -369,8 +369,7 @@ class TestSubscriptionTier:
|
||||
daily=UsageWindow(used=0, limit=100, resets_at=now + timedelta(hours=1)),
|
||||
weekly=UsageWindow(used=0, limit=500, resets_at=now + timedelta(days=1)),
|
||||
)
|
||||
# Default tier should be PRO (beta testing default)
|
||||
assert status.tier == SubscriptionTier.PRO
|
||||
assert status.tier == SubscriptionTier.FREE
|
||||
|
||||
def test_usage_status_with_custom_tier(self):
|
||||
now = datetime.now(UTC)
|
||||
|
||||
@@ -16,18 +16,10 @@ export interface UserOption {
|
||||
user_email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the input looks like a complete email address.
|
||||
* Used to decide whether to call the direct email lookup endpoint
|
||||
* vs. the broader user-history search.
|
||||
*/
|
||||
function looksLikeEmail(input: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the input looks like a UUID (user ID).
|
||||
*/
|
||||
function looksLikeUuid(input: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||
input,
|
||||
@@ -43,7 +35,6 @@ export function useRateLimitManager() {
|
||||
const [rateLimitData, setRateLimitData] =
|
||||
useState<UserRateLimitResponse | null>(null);
|
||||
|
||||
/** Direct lookup by email or user ID via the rate-limit endpoint. */
|
||||
async function handleDirectLookup(trimmed: string) {
|
||||
setIsSearching(true);
|
||||
setSearchResults([]);
|
||||
|
||||
@@ -123,9 +123,20 @@ export function UsagePanelContent({
|
||||
);
|
||||
}
|
||||
|
||||
const tierLabel = usage.tier
|
||||
? usage.tier.charAt(0) + usage.tier.slice(1).toLowerCase()
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-xs font-semibold text-neutral-800">Usage limits</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<span className="text-xs font-semibold text-neutral-800">
|
||||
Usage limits
|
||||
</span>
|
||||
{tierLabel && (
|
||||
<span className="text-[11px] text-neutral-500">{tierLabel} plan</span>
|
||||
)}
|
||||
</div>
|
||||
{hasDailyLimit && (
|
||||
<UsageBar
|
||||
label="Today"
|
||||
|
||||
@@ -31,16 +31,19 @@ function makeUsage({
|
||||
dailyLimit = 10000,
|
||||
weeklyUsed = 2000,
|
||||
weeklyLimit = 50000,
|
||||
tier = "FREE",
|
||||
}: {
|
||||
dailyUsed?: number;
|
||||
dailyLimit?: number;
|
||||
weeklyUsed?: number;
|
||||
weeklyLimit?: number;
|
||||
tier?: string;
|
||||
} = {}) {
|
||||
const future = new Date(Date.now() + 3600 * 1000); // 1h from now
|
||||
return {
|
||||
daily: { used: dailyUsed, limit: dailyLimit, resets_at: future },
|
||||
weekly: { used: weeklyUsed, limit: weeklyLimit, resets_at: future },
|
||||
tier,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,6 +113,16 @@ describe("UsageLimits", () => {
|
||||
expect(screen.getByText("100% used")).toBeDefined();
|
||||
});
|
||||
|
||||
it("displays the user tier label", () => {
|
||||
mockUseGetV2GetCopilotUsage.mockReturnValue({
|
||||
data: makeUsage({ tier: "PRO" }),
|
||||
isLoading: false,
|
||||
});
|
||||
render(<UsageLimits />);
|
||||
|
||||
expect(screen.getByText("Pro plan")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows learn more link to credits page", () => {
|
||||
mockUseGetV2GetCopilotUsage.mockReturnValue({
|
||||
data: makeUsage(),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatResetTime } from "../UsagePanelContent";
|
||||
|
||||
describe("formatResetTime", () => {
|
||||
const now = new Date("2025-06-15T12:00:00Z");
|
||||
|
||||
it("returns 'now' when reset time is in the past", () => {
|
||||
expect(formatResetTime("2025-06-15T11:00:00Z", now)).toBe("now");
|
||||
});
|
||||
|
||||
it("returns minutes only when under 1 hour", () => {
|
||||
const result = formatResetTime("2025-06-15T12:30:00Z", now);
|
||||
expect(result).toBe("in 30m");
|
||||
});
|
||||
|
||||
it("returns hours and minutes when under 24 hours", () => {
|
||||
const result = formatResetTime("2025-06-15T16:45:00Z", now);
|
||||
expect(result).toBe("in 4h 45m");
|
||||
});
|
||||
|
||||
it("returns formatted date when over 24 hours away", () => {
|
||||
const result = formatResetTime("2025-06-17T00:00:00Z", now);
|
||||
expect(result).toMatch(/Tue/);
|
||||
});
|
||||
|
||||
it("accepts a Date object for resetsAt", () => {
|
||||
const resetDate = new Date("2025-06-15T14:00:00Z");
|
||||
expect(formatResetTime(resetDate, now)).toBe("in 2h 0m");
|
||||
});
|
||||
});
|
||||
@@ -13127,7 +13127,7 @@
|
||||
"type": "string",
|
||||
"enum": ["FREE", "PRO", "BUSINESS", "ENTERPRISE"],
|
||||
"title": "SubscriptionTier",
|
||||
"description": "Subscription tiers with increasing token allowances."
|
||||
"description": "Subscription tiers with increasing token allowances.\n\nMirrors the ``SubscriptionTier`` enum in ``schema.prisma``.\nOnce ``prisma generate`` is run, this can be replaced with::\n\n from prisma.enums import SubscriptionTier"
|
||||
},
|
||||
"SuggestedGoalResponse": {
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user