mirror of
https://github.com/zgs225/cliproxy-plugin-commandcode.git
synced 2026-09-26 03:32:50 +08:00
feat: add subscription plan inference and Management Center theme sync
This commit is contained in:
+149
-1
@@ -37,8 +37,9 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
--radius-lg: 16px;
|
||||
}
|
||||
|
||||
/* Auto mode fallback: when no explicit data-theme is set, follow OS dark preference */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
:root:not([data-theme="light"]):not([data-theme="white"]) {
|
||||
--bg-page: #0b0f19;
|
||||
--bg-card: #151d30;
|
||||
--bg-subtle: #1e293b;
|
||||
@@ -61,6 +62,53 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
}
|
||||
}
|
||||
|
||||
/* Explicit dark theme from Management Center */
|
||||
:root[data-theme="dark"] {
|
||||
--bg-page: #0b0f19;
|
||||
--bg-card: #151d30;
|
||||
--bg-subtle: #1e293b;
|
||||
--border-color: #334155;
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--text-dim: #64748b;
|
||||
--primary: #60a5fa;
|
||||
--primary-hover: #3b82f6;
|
||||
--primary-subtle: rgba(59, 130, 246, 0.12);
|
||||
--success: #34d399;
|
||||
--success-subtle: rgba(16, 185, 129, 0.12);
|
||||
--warning: #fbbf24;
|
||||
--warning-subtle: rgba(245, 158, 11, 0.12);
|
||||
--danger: #f87171;
|
||||
--danger-subtle: rgba(239, 68, 68, 0.12);
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
||||
}
|
||||
|
||||
/* Explicit white / light theme from Management Center: overrides OS dark preference */
|
||||
:root[data-theme="white"],
|
||||
:root[data-theme="light"] {
|
||||
--bg-page: #f8fafc;
|
||||
--bg-card: #ffffff;
|
||||
--bg-subtle: #f1f5f9;
|
||||
--border-color: #e2e8f0;
|
||||
--text-main: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--text-dim: #94a3b8;
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--primary-subtle: #eff6ff;
|
||||
--success: #10b981;
|
||||
--success-subtle: #ecfdf5;
|
||||
--warning: #f59e0b;
|
||||
--warning-subtle: #fffbeb;
|
||||
--danger: #ef4444;
|
||||
--danger-subtle: #fef2f2;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -141,6 +189,29 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.plan-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 10px;
|
||||
border-radius: 9999px;
|
||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(59, 130, 246, 0.15));
|
||||
color: #8b5cf6;
|
||||
border: 1px solid rgba(139, 92, 246, 0.35);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .plan-tag,
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]):not([data-theme="white"]) .plan-tag {
|
||||
background: linear-gradient(135deg, rgba(167, 139, 250, 0.2), rgba(96, 165, 250, 0.2));
|
||||
color: #c084fc;
|
||||
border-color: rgba(167, 139, 250, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
@@ -588,6 +659,7 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
<div class="brand-title">
|
||||
Command Code 配额
|
||||
<span class="version-tag">v0.1.0</span>
|
||||
<span id="planBadge" class="plan-tag" style="display:none;">Plan: -</span>
|
||||
</div>
|
||||
<div class="brand-subtitle">CLIProxyAPI 实时限额与 Credits 用量监控</div>
|
||||
</div>
|
||||
@@ -796,6 +868,7 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
const alertMsg = document.getElementById("alertMsg");
|
||||
const statusBadge = document.getElementById("statusBadge");
|
||||
const statusText = document.getElementById("statusText");
|
||||
const planBadge = document.getElementById("planBadge");
|
||||
|
||||
const valMonthlyCredits = document.getElementById("valMonthlyCredits");
|
||||
const valOpensourceCredits = document.getElementById("valOpensourceCredits");
|
||||
@@ -898,6 +971,16 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
const credits = data.credits || (data.data && data.data.credits) || {};
|
||||
const limits = data.window_limits || (data.data && data.data.window_limits) || {};
|
||||
|
||||
// Plan
|
||||
const plan = data.plan || (data.data && data.data.plan);
|
||||
if (plan) {
|
||||
const planName = plan.name || (typeof plan === "string" ? plan : "Unknown");
|
||||
planBadge.textContent = "Plan: " + planName;
|
||||
planBadge.style.display = "inline-flex";
|
||||
} else {
|
||||
planBadge.style.display = "none";
|
||||
}
|
||||
|
||||
// Credits
|
||||
valMonthlyCredits.textContent = formatNumber(credits.monthly_credits);
|
||||
valOpensourceCredits.textContent = formatNumber(credits.opensource_monthly_credits);
|
||||
@@ -1062,6 +1145,71 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
inputMgmtKey.value = initKey;
|
||||
}
|
||||
|
||||
function syncTheme() {
|
||||
let themeSetting = null;
|
||||
|
||||
// 1. Try reading from parent window (if same-origin iframe)
|
||||
try {
|
||||
if (window.parent && window.parent !== window && window.parent.document) {
|
||||
const parentTheme = window.parent.document.documentElement.getAttribute("data-theme");
|
||||
if (parentTheme) {
|
||||
themeSetting = parentTheme;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Try reading from localStorage['cli-proxy-theme']
|
||||
if (!themeSetting) {
|
||||
try {
|
||||
const stored = localStorage.getItem("cli-proxy-theme");
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (parsed && parsed.state) {
|
||||
if (parsed.state.theme) {
|
||||
themeSetting = parsed.state.theme;
|
||||
}
|
||||
if (parsed.state.resolvedTheme && themeSetting === "auto") {
|
||||
themeSetting = parsed.state.resolvedTheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 3. Fallback to own html attribute
|
||||
if (!themeSetting) {
|
||||
themeSetting = document.documentElement.getAttribute("data-theme");
|
||||
}
|
||||
|
||||
// Apply theme to documentElement
|
||||
const docEl = document.documentElement;
|
||||
if (themeSetting === "dark") {
|
||||
docEl.setAttribute("data-theme", "dark");
|
||||
} else if (themeSetting === "white" || themeSetting === "light") {
|
||||
docEl.setAttribute("data-theme", themeSetting);
|
||||
} else {
|
||||
// auto or unset: check system preference
|
||||
const isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
if (isDark) {
|
||||
docEl.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
docEl.setAttribute("data-theme", "light");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme and sync listeners
|
||||
syncTheme();
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key === "cli-proxy-theme") {
|
||||
syncTheme();
|
||||
}
|
||||
});
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", syncTheme);
|
||||
}
|
||||
setInterval(syncTheme, 2000);
|
||||
|
||||
if (!timerInterval) {
|
||||
timerInterval = setInterval(updateTimers, 1000);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ThemeStorageState represents the zustand-persisted theme state from CLIProxyAPI Management Center.
|
||||
type ThemeStorageState struct {
|
||||
State struct {
|
||||
Theme string `json:"theme"`
|
||||
ResolvedTheme string `json:"resolvedTheme"`
|
||||
} `json:"state"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
// ResolveThemeFromStorage extracts theme configuration from the 'cli-proxy-theme' localStorage JSON.
|
||||
// Returns the user-selected theme setting ("auto", "white", "light", "dark") and the resolved theme ("dark" or "light"/"white").
|
||||
func ResolveThemeFromStorage(storageJSON string) (theme string, resolved string) {
|
||||
theme = "auto"
|
||||
resolved = "light"
|
||||
|
||||
s := strings.TrimSpace(storageJSON)
|
||||
if s == "" {
|
||||
return theme, resolved
|
||||
}
|
||||
|
||||
// 1. Try zustand persist format
|
||||
var zustandState ThemeStorageState
|
||||
if err := json.Unmarshal([]byte(s), &zustandState); err == nil && (zustandState.State.Theme != "" || zustandState.State.ResolvedTheme != "") {
|
||||
if zustandState.State.Theme != "" {
|
||||
theme = zustandState.State.Theme
|
||||
}
|
||||
if zustandState.State.ResolvedTheme != "" {
|
||||
resolved = zustandState.State.ResolvedTheme
|
||||
} else {
|
||||
switch theme {
|
||||
case "dark":
|
||||
resolved = "dark"
|
||||
case "white", "light":
|
||||
resolved = "light"
|
||||
default:
|
||||
resolved = "light"
|
||||
}
|
||||
}
|
||||
return theme, resolved
|
||||
}
|
||||
|
||||
// 2. Try simple map {"theme": "..."}
|
||||
var simpleMap map[string]any
|
||||
if err := json.Unmarshal([]byte(s), &simpleMap); err == nil {
|
||||
if t, ok := simpleMap["theme"].(string); ok && t != "" {
|
||||
theme = t
|
||||
if r, ok2 := simpleMap["resolvedTheme"].(string); ok2 && r != "" {
|
||||
resolved = r
|
||||
} else if theme == "dark" {
|
||||
resolved = "dark"
|
||||
} else {
|
||||
resolved = "light"
|
||||
}
|
||||
return theme, resolved
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback for raw string literals like `"dark"` or `dark`
|
||||
clean := strings.ToLower(strings.Trim(s, "\" \t\r\n"))
|
||||
switch clean {
|
||||
case "dark":
|
||||
return "dark", "dark"
|
||||
case "white":
|
||||
return "white", "white"
|
||||
case "light":
|
||||
return "light", "light"
|
||||
case "auto":
|
||||
return "auto", "light"
|
||||
}
|
||||
|
||||
return theme, resolved
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveThemeFromStorage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
storageJSON string
|
||||
wantTheme string
|
||||
wantResolved string
|
||||
}{
|
||||
{
|
||||
name: "zustand dark theme",
|
||||
storageJSON: `{"state":{"theme":"dark","resolvedTheme":"dark"},"version":0}`,
|
||||
wantTheme: "dark",
|
||||
wantResolved: "dark",
|
||||
},
|
||||
{
|
||||
name: "zustand white theme",
|
||||
storageJSON: `{"state":{"theme":"white","resolvedTheme":"light"},"version":0}`,
|
||||
wantTheme: "white",
|
||||
wantResolved: "light",
|
||||
},
|
||||
{
|
||||
name: "zustand light theme",
|
||||
storageJSON: `{"state":{"theme":"light","resolvedTheme":"light"},"version":0}`,
|
||||
wantTheme: "light",
|
||||
wantResolved: "light",
|
||||
},
|
||||
{
|
||||
name: "zustand auto theme with resolved dark",
|
||||
storageJSON: `{"state":{"theme":"auto","resolvedTheme":"dark"},"version":0}`,
|
||||
wantTheme: "auto",
|
||||
wantResolved: "dark",
|
||||
},
|
||||
{
|
||||
name: "zustand auto theme with resolved light",
|
||||
storageJSON: `{"state":{"theme":"auto","resolvedTheme":"light"},"version":0}`,
|
||||
wantTheme: "auto",
|
||||
wantResolved: "light",
|
||||
},
|
||||
{
|
||||
name: "simple json dark",
|
||||
storageJSON: `{"theme":"dark"}`,
|
||||
wantTheme: "dark",
|
||||
wantResolved: "dark",
|
||||
},
|
||||
{
|
||||
name: "simple json white",
|
||||
storageJSON: `{"theme":"white"}`,
|
||||
wantTheme: "white",
|
||||
wantResolved: "light",
|
||||
},
|
||||
{
|
||||
name: "raw string dark",
|
||||
storageJSON: `"dark"`,
|
||||
wantTheme: "dark",
|
||||
wantResolved: "dark",
|
||||
},
|
||||
{
|
||||
name: "raw string white",
|
||||
storageJSON: `white`,
|
||||
wantTheme: "white",
|
||||
wantResolved: "white",
|
||||
},
|
||||
{
|
||||
name: "empty string defaults to auto/light",
|
||||
storageJSON: "",
|
||||
wantTheme: "auto",
|
||||
wantResolved: "light",
|
||||
},
|
||||
{
|
||||
name: "invalid json defaults to auto/light",
|
||||
storageJSON: `{not-valid-json`,
|
||||
wantTheme: "auto",
|
||||
wantResolved: "light",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotTheme, gotResolved := ResolveThemeFromStorage(tt.storageJSON)
|
||||
if gotTheme != tt.wantTheme {
|
||||
t.Errorf("theme = %q, want %q", gotTheme, tt.wantTheme)
|
||||
}
|
||||
if gotResolved != tt.wantResolved {
|
||||
t.Errorf("resolved = %q, want %q", gotResolved, tt.wantResolved)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,7 @@ type UpstreamCreditsResponse struct {
|
||||
|
||||
// UpstreamWindowLimits carries fiveHour and weekly window metrics.
|
||||
type UpstreamWindowLimits struct {
|
||||
Limited *bool `json:"limited,omitempty"`
|
||||
FiveHour UpstreamWindowLimit `json:"fiveHour"`
|
||||
Weekly UpstreamWindowLimit `json:"weekly"`
|
||||
}
|
||||
@@ -274,8 +275,15 @@ type UsageWindowLimitsData struct {
|
||||
Weekly UsageWindowLimitData `json:"weekly"`
|
||||
}
|
||||
|
||||
// PlanInfo represents inferred Command Code subscription plan details.
|
||||
type PlanInfo struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// FormattedUsageData is the complete formatted usage payload.
|
||||
type FormattedUsageData struct {
|
||||
Plan PlanInfo `json:"plan"`
|
||||
Credits UsageCreditsData `json:"credits"`
|
||||
WindowLimits UsageWindowLimitsData `json:"window_limits"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
@@ -284,6 +292,7 @@ type FormattedUsageData struct {
|
||||
// FormattedUsageResponse is returned by GET /plugins/commandcode/usage and POST /plugins/commandcode/usage.
|
||||
type FormattedUsageResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Plan PlanInfo `json:"plan"`
|
||||
Data FormattedUsageData `json:"data"`
|
||||
Credits UsageCreditsData `json:"credits"`
|
||||
WindowLimits UsageWindowLimitsData `json:"window_limits"`
|
||||
|
||||
@@ -163,8 +163,12 @@ func ParseAndFormatUsage(raw []byte, summary *UpstreamUsageSummaryResponse, now
|
||||
windowLimitsData := formatWindowLimits(upstream.WindowLimits, now)
|
||||
windowLimitsData.Monthly = formatMonthlyWindow(upstream.Credits, summary, now)
|
||||
|
||||
// Inferred subscription plan
|
||||
planInfo := PlanFromWindowLimits(upstream.WindowLimits.FiveHour.Cap, upstream.WindowLimits.Weekly.Cap, upstream.WindowLimits.Limited)
|
||||
|
||||
nowRFC := now.Format(time.RFC3339)
|
||||
data := FormattedUsageData{
|
||||
Plan: planInfo,
|
||||
Credits: creditsData,
|
||||
WindowLimits: windowLimitsData,
|
||||
UpdatedAt: nowRFC,
|
||||
@@ -172,6 +176,7 @@ func ParseAndFormatUsage(raw []byte, summary *UpstreamUsageSummaryResponse, now
|
||||
|
||||
return &FormattedUsageResponse{
|
||||
OK: true,
|
||||
Plan: planInfo,
|
||||
Data: data,
|
||||
Credits: creditsData,
|
||||
WindowLimits: windowLimitsData,
|
||||
@@ -179,6 +184,69 @@ func ParseAndFormatUsage(raw []byte, summary *UpstreamUsageSummaryResponse, now
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PlanFromWindowLimits infers the Command Code subscription plan based on window limit caps.
|
||||
//
|
||||
// Rules:
|
||||
// - windowLimits.limited == false -> Provider plan (pay-as-you-go)
|
||||
// - 5h cap=14 && weekly cap=35 -> GOAT plan
|
||||
// - 5h cap=16 && weekly cap=40 -> Pro plan
|
||||
// - 5h cap=90 && weekly cap=180 -> Max 20× plan
|
||||
// - 5h cap=45 && weekly cap=90 -> Max 10× plan
|
||||
// - 5h cap=3 && weekly cap=6 -> Go plan
|
||||
// - Otherwise -> Unknown
|
||||
func PlanFromWindowLimits(fiveHourCap, weeklyCap float64, limited *bool) PlanInfo {
|
||||
if limited != nil && !*limited {
|
||||
return PlanInfo{
|
||||
Name: "Provider",
|
||||
Code: "provider",
|
||||
}
|
||||
}
|
||||
|
||||
match := func(capVal, target float64) bool {
|
||||
return math.Abs(capVal-target) < 0.01
|
||||
}
|
||||
|
||||
if match(fiveHourCap, 14) && match(weeklyCap, 35) {
|
||||
return PlanInfo{
|
||||
Name: "GOAT",
|
||||
Code: "goat",
|
||||
}
|
||||
}
|
||||
|
||||
if match(fiveHourCap, 16) && match(weeklyCap, 40) {
|
||||
return PlanInfo{
|
||||
Name: "Pro",
|
||||
Code: "pro",
|
||||
}
|
||||
}
|
||||
|
||||
if match(fiveHourCap, 90) && match(weeklyCap, 180) {
|
||||
return PlanInfo{
|
||||
Name: "Max 20×",
|
||||
Code: "max_20x",
|
||||
}
|
||||
}
|
||||
|
||||
if match(fiveHourCap, 45) && match(weeklyCap, 90) {
|
||||
return PlanInfo{
|
||||
Name: "Max 10×",
|
||||
Code: "max_10x",
|
||||
}
|
||||
}
|
||||
|
||||
if match(fiveHourCap, 3) && match(weeklyCap, 6) {
|
||||
return PlanInfo{
|
||||
Name: "Go",
|
||||
Code: "go",
|
||||
}
|
||||
}
|
||||
|
||||
return PlanInfo{
|
||||
Name: "Unknown",
|
||||
Code: "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
func formatCredits(credits map[string]any) UsageCreditsData {
|
||||
data := UsageCreditsData{
|
||||
Details: credits,
|
||||
|
||||
@@ -174,6 +174,27 @@ func TestParseAndFormatUsage(t *testing.T) {
|
||||
if weekly.ResetAt != "2025-03-10T12:00:00Z" {
|
||||
t.Errorf("Weekly ResetAt = %v, want 2025-03-10T12:00:00Z", weekly.ResetAt)
|
||||
}
|
||||
|
||||
// Verify inferred plan (cap 25 / 100 is unknown)
|
||||
if usage.Plan.Name != "Unknown" || usage.Plan.Code != "unknown" {
|
||||
t.Errorf("Plan = %+v, want Unknown", usage.Plan)
|
||||
}
|
||||
|
||||
// Verify plan inference for GOAT
|
||||
rawGOAT := []byte(`{
|
||||
"credits": {"monthlyCredits": 100},
|
||||
"windowLimits": {
|
||||
"fiveHour": {"used": 2, "cap": 14},
|
||||
"weekly": {"used": 10, "cap": 35}
|
||||
}
|
||||
}`)
|
||||
usageGOAT, errGOAT := ParseAndFormatUsage(rawGOAT, nil, now)
|
||||
if errGOAT != nil {
|
||||
t.Fatalf("ParseAndFormatUsage GOAT error: %v", errGOAT)
|
||||
}
|
||||
if usageGOAT.Plan.Name != "GOAT" || usageGOAT.Plan.Code != "goat" {
|
||||
t.Errorf("GOAT Plan = %+v, want name=GOAT code=goat", usageGOAT.Plan)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchCreditsRaw_FallbackHTTP(t *testing.T) {
|
||||
@@ -258,3 +279,100 @@ func TestFetchCreditsRaw_HostCaller(t *testing.T) {
|
||||
t.Errorf("body = %s, want %s", string(body), string(mockResponsePayload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanFromWindowLimits(t *testing.T) {
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fiveHourCap float64
|
||||
weeklyCap float64
|
||||
limited *bool
|
||||
wantName string
|
||||
wantCode string
|
||||
}{
|
||||
{
|
||||
name: "GOAT plan",
|
||||
fiveHourCap: 14,
|
||||
weeklyCap: 35,
|
||||
limited: &trueVal,
|
||||
wantName: "GOAT",
|
||||
wantCode: "goat",
|
||||
},
|
||||
{
|
||||
name: "Pro plan",
|
||||
fiveHourCap: 16,
|
||||
weeklyCap: 40,
|
||||
limited: nil,
|
||||
wantName: "Pro",
|
||||
wantCode: "pro",
|
||||
},
|
||||
{
|
||||
name: "Max 20x plan",
|
||||
fiveHourCap: 90,
|
||||
weeklyCap: 180,
|
||||
limited: &trueVal,
|
||||
wantName: "Max 20×",
|
||||
wantCode: "max_20x",
|
||||
},
|
||||
{
|
||||
name: "Max 10x plan",
|
||||
fiveHourCap: 45,
|
||||
weeklyCap: 90,
|
||||
limited: nil,
|
||||
wantName: "Max 10×",
|
||||
wantCode: "max_10x",
|
||||
},
|
||||
{
|
||||
name: "Go plan",
|
||||
fiveHourCap: 3,
|
||||
weeklyCap: 6,
|
||||
limited: nil,
|
||||
wantName: "Go",
|
||||
wantCode: "go",
|
||||
},
|
||||
{
|
||||
name: "Provider pay-as-you-go plan",
|
||||
fiveHourCap: 0,
|
||||
weeklyCap: 0,
|
||||
limited: &falseVal,
|
||||
wantName: "Provider",
|
||||
wantCode: "provider",
|
||||
},
|
||||
{
|
||||
name: "Provider plan with caps set but limited=false",
|
||||
fiveHourCap: 14,
|
||||
weeklyCap: 35,
|
||||
limited: &falseVal,
|
||||
wantName: "Provider",
|
||||
wantCode: "provider",
|
||||
},
|
||||
{
|
||||
name: "Float tolerance test",
|
||||
fiveHourCap: 13.999,
|
||||
weeklyCap: 35.001,
|
||||
limited: nil,
|
||||
wantName: "GOAT",
|
||||
wantCode: "goat",
|
||||
},
|
||||
{
|
||||
name: "Unknown caps",
|
||||
fiveHourCap: 10,
|
||||
weeklyCap: 20,
|
||||
limited: nil,
|
||||
wantName: "Unknown",
|
||||
wantCode: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := PlanFromWindowLimits(tt.fiveHourCap, tt.weeklyCap, tt.limited)
|
||||
if got.Name != tt.wantName || got.Code != tt.wantCode {
|
||||
t.Errorf("PlanFromWindowLimits(%v, %v, %v) = %+v, want name=%q code=%q",
|
||||
tt.fiveHourCap, tt.weeklyCap, tt.limited, got, tt.wantName, tt.wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user