From f53288cea68cd94802cfbfc8aa3dab9db901885b Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:22:53 +0700 Subject: [PATCH] Match Amp card to the Mac: Amp Free label, no free-tier pace, monthly allowance rows --- rust/src/providers/amp/display.rs | 56 +++++++++- rust/src/providers/amp/mod.rs | 147 ++++++++++++++++++++++++- rust/src/providers/amp/subscription.rs | 38 +------ 3 files changed, 198 insertions(+), 43 deletions(-) diff --git a/rust/src/providers/amp/display.rs b/rust/src/providers/amp/display.rs index 07b16fd1b2..87bef5d856 100644 --- a/rust/src/providers/amp/display.rs +++ b/rust/src/providers/amp/display.rs @@ -10,12 +10,13 @@ use crate::core::{ use crate::providers::format::usd; use super::subscription::{ - next_free_tier_reset, parse_amp_number, parse_amp_subscription_usage, + AmpSubscriptionUsage, next_free_tier_reset, parse_amp_number, parse_amp_subscription_usage, usage_snapshot_from_subscription, }; const AMOUNT: &str = r"([0-9][0-9,]*(?:\.[0-9]+)?)"; const CREDITS_SECTION: &str = "Credits"; +const ALLOWANCES_SECTION: &str = "Monthly allowances"; /// Parsed Amp usage: quota windows plus the credit balances shown as rows. pub(super) struct AmpDisplayUsage { @@ -25,10 +26,28 @@ pub(super) struct AmpDisplayUsage { impl AmpDisplayUsage { pub fn into_fetch_result(self, source_label: &str) -> ProviderFetchResult { - ProviderFetchResult::new(self.usage, source_label).with_display_details(self.details) + let allows_pace = window_allows_pace(&self.usage.primary); + let result = + ProviderFetchResult::new(self.usage, source_label).with_display_details(self.details); + if allows_pace { + result + } else { + result.with_non_authoritative_pace() + } } } +/// Upstream `AmpProviderDescriptor` pace rule: only a subscription window +/// (a known length and a "renews in ..." reset) has a pace. The Amp Free +/// allowance refills hourly or daily, so a pace line under it is noise. +pub(super) fn window_allows_pace(window: &RateWindow) -> bool { + window.window_minutes.is_some() + && window + .reset_description + .as_deref() + .is_some_and(|text| text.starts_with("renews in ")) +} + /// Amp Free allowance, from either the dollar or the percentage line. #[derive(Debug, Clone, PartialEq)] pub(super) struct AmpFreeTierUsage { @@ -67,6 +86,10 @@ pub(super) fn parse_amp_display_text( } let free_window = free.as_ref().map(|free| free_tier_window(free, now)); + let mut details = subscription + .as_ref() + .map(allowance_details) + .unwrap_or_default(); let mut usage = match (subscription, free_window) { (Some(subscription), free_window) => { let usage = usage_snapshot_from_subscription(subscription); @@ -89,10 +112,31 @@ pub(super) fn parse_amp_display_text( } } - Ok(AmpDisplayUsage { - usage, - details: credit_details(individual_credits, &workspaces), - }) + details.extend(credit_details(individual_credits, &workspaces)); + Ok(AmpDisplayUsage { usage, details }) +} + +/// Upstream "Monthly allowances" section: the Tier agent balance in dollars +/// and the orb balance in whole hours. +fn allowance_details(subscription: &AmpSubscriptionUsage) -> Vec { + let agent = subscription.agent_remaining().and_then(|remaining| { + ProviderDisplayDetail::new("amp-allowance-agent", "Agent", usd(remaining))? + .with_section_title(ALLOWANCES_SECTION) + }); + let orb = subscription.orb_hours_remaining().and_then(|remaining| { + ProviderDisplayDetail::new("amp-allowance-orb", "Orb", orb_hours_text(remaining))? + .with_section_title(ALLOWANCES_SECTION)? + .with_secondary_value("a1.small-equivalent hours") + }); + agent.into_iter().chain(orb).collect() +} + +fn orb_hours_text(remaining: f64) -> String { + if remaining > 0.0 && remaining < 1.0 { + "< 1h".to_string() + } else { + format!("{:.0}h", remaining.floor()) + } } /// Test helper: the quota snapshot only. diff --git a/rust/src/providers/amp/mod.rs b/rust/src/providers/amp/mod.rs index aaa9b99707..07e9aba75d 100755 --- a/rust/src/providers/amp/mod.rs +++ b/rust/src/providers/amp/mod.rs @@ -30,14 +30,14 @@ impl AmpProvider { metadata: ProviderMetadata { id: ProviderId::Amp, display_name: "Amp", - session_label: "Usage", - weekly_label: "Monthly", + session_label: "Amp Free", + weekly_label: "Balance", supports_opus: false, supports_credits: true, default_enabled: false, is_primary: false, dashboard_url: Some("https://ampcode.com/settings/usage"), - status_page_url: Some("https://sourcegraphstatus.com"), + status_page_url: None, tertiary_label_key: None, }, } @@ -367,6 +367,9 @@ Subscription Megawatt: 42% other usage and 88% orb usage remaining - resets upon .or(Some(AMP_MONTHLY_WINDOW_MINUTES)) ); assert_eq!(snapshot.login_method.as_deref(), Some("Megawatt")); + // Upstream primaryLabel: "Other usage" when there is an orb lane. + assert_eq!(snapshot.primary_label.as_deref(), Some("Other usage")); + assert_eq!(snapshot.secondary_label.as_deref(), Some("Orb usage")); } #[test] @@ -511,7 +514,7 @@ period 2026-02-30 to 2026-03-30, resets upon renewal in 27 days"; assert!(snapshot.primary.resets_at.is_none()); assert_eq!( snapshot.primary.reset_description.as_deref(), - Some("renews in 27 days · $18.00/$20.00 remaining") + Some("renews in 27 days") ); } @@ -543,3 +546,139 @@ period 2026-09-13 to 2026-10-13, resets upon renewal in 999999999999999999999999 assert_eq!(snapshot.primary.used_percent, 50.0); } } + +/// Upstream 0.70 `AmpProviderDescriptor` / `AmpUsageSnapshot` parity: labels, +/// status page, pace rule and the "Monthly allowances" rows. +#[cfg(test)] +mod mac_parity_tests { + use super::display::{parse_amp_display_text, window_allows_pace}; + use super::*; + use chrono::TimeZone; + + const TIER_TEXT: &str = "Amp Megawatt Tier: agent usage $18.57 of $20 remaining (93%), \ +orb usage 732.8h of 750h a1.small orb hours remaining (98%) - \ +period 2026-09-13 to 2026-10-13, resets upon renewal in 27 days"; + + fn now() -> chrono::DateTime { + Utc.with_ymd_and_hms(2026, 9, 16, 12, 0, 0).unwrap() + } + + #[test] + fn metadata_matches_upstream_descriptor() { + let provider = AmpProvider::new(); + let metadata = provider.metadata(); + assert_eq!(metadata.session_label, "Amp Free"); + assert_eq!(metadata.weekly_label, "Balance"); + assert_eq!(metadata.status_page_url, None); + } + + #[test] + fn free_tier_has_no_pace() { + // The parity pack's dollar line (hourly refill) and the daily percent line. + for text in [ + "Amp Free: $12.50 / $20 remaining (replenishes +$0.42 / hour)", + "Amp Free: 72% remaining (resets daily)", + ] { + let result = parse_amp_display_text(text, now()) + .expect("free tier") + .into_fetch_result("cli"); + assert!(!result.pace_authoritative, "{text}"); + } + } + + #[test] + fn subscription_windows_keep_pace() { + let legacy = "Subscription Megawatt: 42% other usage and 88% orb usage remaining - resets upon renewal in 12 days"; + for text in [TIER_TEXT, legacy] { + let result = parse_amp_display_text(text, now()) + .expect("subscription") + .into_fetch_result("cli"); + assert!(result.pace_authoritative, "{text}"); + } + // A subscription with a free allowance still paces on the subscription. + let both = format!("Amp Free: 72% remaining (resets daily)\n{TIER_TEXT}"); + let result = parse_amp_display_text(&both, now()) + .expect("both") + .into_fetch_result("cli"); + assert!(result.pace_authoritative); + } + + #[test] + fn pace_rule_needs_window_length_and_renewal_text() { + let window = |minutes: Option, text: Option<&str>| { + RateWindow::with_details(10.0, minutes, None, text.map(str::to_string)) + }; + assert!(window_allows_pace(&window( + Some(43_200), + Some("renews in 27 days") + ))); + assert!(!window_allows_pace(&window( + None, + Some("renews in 27 days") + ))); + assert!(!window_allows_pace(&window( + Some(1_440), + Some("resets daily") + ))); + assert!(!window_allows_pace(&window(Some(2_880), None))); + } + + #[test] + fn tier_reports_monthly_allowance_rows() { + let parsed = parse_amp_display_text(TIER_TEXT, now()).expect("tier"); + let rows: Vec<_> = parsed + .details + .iter() + .map(|row| { + ( + row.section_title().map(str::to_string), + row.title().to_string(), + row.value().to_string(), + row.secondary_value().map(str::to_string), + ) + }) + .collect(); + assert_eq!( + rows, + vec![ + ( + Some("Monthly allowances".to_string()), + "Agent".to_string(), + "$18.57".to_string(), + None, + ), + ( + Some("Monthly allowances".to_string()), + "Orb".to_string(), + "732h".to_string(), + Some("a1.small-equivalent hours".to_string()), + ), + ] + ); + assert_eq!( + parsed.usage.primary.reset_description.as_deref(), + Some("renews in 27 days") + ); + } + + #[test] + fn orb_under_one_hour_reads_less_than_one() { + let text = "Amp Example Tier: agent usage $3 of $20 remaining, \ +orb usage 0.4h of 750h a1.small orb hours remaining - \ +period 2026-09-13 to 2026-10-13, resets upon renewal in 27 days"; + let parsed = parse_amp_display_text(text, now()).expect("tier"); + let orb = parsed + .details + .iter() + .find(|row| row.title() == "Orb") + .expect("orb row"); + assert_eq!(orb.value(), "< 1h"); + } + + #[test] + fn legacy_subscription_has_no_allowance_rows() { + let text = "Subscription Megawatt: 42% other usage and 88% orb usage remaining - resets upon renewal in 12 days"; + let parsed = parse_amp_display_text(text, now()).expect("legacy"); + assert!(parsed.details.is_empty()); + } +} diff --git a/rust/src/providers/amp/subscription.rs b/rust/src/providers/amp/subscription.rs index 34a9039797..0a170413a2 100644 --- a/rust/src/providers/amp/subscription.rs +++ b/rust/src/providers/amp/subscription.rs @@ -328,9 +328,12 @@ pub(super) fn usage_snapshot_from_subscription(sub: AmpSubscriptionUsage) -> Usa Some(resets_at), Some(reset_description), ); + // Upstream `AmpProviderDescriptor.primaryLabel` / `secondaryLabel`. UsageSnapshot::new(primary) .with_secondary(secondary) .with_login_method(plan) + .with_primary_label("Other usage") + .with_secondary_label("Orb usage") } AmpSubscriptionKind::Tier { agent, @@ -351,11 +354,7 @@ pub(super) fn usage_snapshot_from_subscription(sub: AmpSubscriptionUsage) -> Usa agent.used_percent(), window_minutes, resets_at, - Some(tier_allowance_description( - &reset_description, - &agent, - AmpAllowanceUnit::Dollars, - )), + Some(reset_description.clone()), ) }; let mut usage = UsageSnapshot::new(primary) @@ -366,11 +365,7 @@ pub(super) fn usage_snapshot_from_subscription(sub: AmpSubscriptionUsage) -> Usa orb.used_percent(), window_minutes, resets_at, - Some(tier_allowance_description( - &reset_description, - &orb, - AmpAllowanceUnit::Hours, - )), + Some(reset_description), ); usage = usage .with_secondary(secondary) @@ -381,29 +376,6 @@ pub(super) fn usage_snapshot_from_subscription(sub: AmpSubscriptionUsage) -> Usa } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum AmpAllowanceUnit { - Dollars, - Hours, -} - -fn tier_allowance_description( - reset_description: &str, - allowance: &AmpAllowance, - unit: AmpAllowanceUnit, -) -> String { - match unit { - AmpAllowanceUnit::Hours => format!( - "{reset_description} · {:.2}h/{:.2}h remaining", - allowance.remaining, allowance.limit - ), - AmpAllowanceUnit::Dollars => format!( - "{reset_description} · ${:.2}/${:.2} remaining", - allowance.remaining, allowance.limit - ), - } -} - /// Next 8:00 PM America/New_York boundary strictly after `now`. pub(super) fn next_free_tier_reset( now: chrono::DateTime,