Skip to content
This repository was archived by the owner on May 24, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions PolyPilot.IntegrationTests/QuotaDisplayTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using PolyPilot.IntegrationTests.Fixtures;

namespace PolyPilot.IntegrationTests;

/// <summary>
/// Integration tests for the quota display feature.
/// Creates a session, sends a message to trigger AssistantUsageEvent with quota data,
/// then verifies the quota indicator appears in the Dashboard header.
/// </summary>
[Collection("PolyPilot")]
[Trait("Category", "QuotaDisplay")]
public class QuotaDisplayTests : IntegrationTestBase
{
public QuotaDisplayTests(AppFixture app, ITestOutputHelper output)
: base(app, output) { }

[Fact]
public async Task QuotaIndicator_AppearsAfterSendingMessage()
{
await WaitForCdpReadyAsync();
await ScreenshotAsync("quota-01-before");

// Step 1: Create a new session by clicking "+" → "Session"
await ClickAsync("[title='Create new session'], button.new-session-btn");
await Task.Delay(1000);

var menuItem = await CdpEvalAsync(
"const items = [...document.querySelectorAll('.sidebar-new-menu-item, .popover-item, button')]; " +
"const btn = items.find(b => b.textContent?.includes('Session') || b.textContent?.includes('Empty')); " +
"btn?.click(); btn ? 'clicked: ' + btn.textContent?.trim()?.substring(0,20) : 'no session btn'");
Output.WriteLine($"Create session: {menuItem}");
await Task.Delay(2000);

// Step 2: Find the input area and type a message
var fillResult = await CdpEvalAsync(
"const sel = '.card-input input, .card-input textarea, .input-row textarea, textarea'; " +
"const input = [...document.querySelectorAll(sel)].find(i => i.offsetParent !== null); " +
"if (input) { input.value = 'Say hello'; " +
"input.dispatchEvent(new Event('input', {bubbles:true})); " +
"input.dispatchEvent(new Event('change', {bubbles:true})); 'filled'; } else { 'no input'; }");
Output.WriteLine($"Fill input: {fillResult}");

// Step 3: Click send
var sendResult = await CdpEvalAsync(
"const sel = '.card-input input, .card-input textarea, .input-row textarea, textarea'; " +
"const input = [...document.querySelectorAll(sel)].find(i => i.offsetParent !== null); " +
"if (input) { const container = input.closest('.card-input') || input.closest('.input-row'); " +
"const sendBtn = container?.querySelector('.send-btn:not(.stop-btn)') || " +
"container?.querySelectorAll('button')?.[container?.querySelectorAll('button')?.length - 1]; " +
"if (sendBtn) { sendBtn.click(); 'sent: ' + sendBtn.className; } " +
"else { input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); 'enter'; } " +
"} else { 'no input'; }");
Output.WriteLine($"Send: {sendResult}");

// Step 4: Wait for response + quota event (up to 60 seconds)
Output.WriteLine("Waiting for Copilot response + quota data...");
var hasQuota = false;
for (var i = 0; i < 30; i++)
{
var quotaExists = await ExistsAsync("#quota-indicator, .quota-indicator, .quota-pill");
if (quotaExists)
{
hasQuota = true;
Output.WriteLine($"Quota indicator appeared after {i * 2}s");
break;
}
if (i % 5 == 0)
{
var pageState = await CdpEvalAsync(
"JSON.stringify({quota: !!document.querySelector('#quota-indicator, .quota-indicator'), " +
"messages: document.querySelectorAll('.message-content, .markdown-body').length, " +
"processing: !!document.querySelector('.thinking, .processing, .spinner')})");
Output.WriteLine($"Poll {i * 2}s: {pageState}");
}
await Task.Delay(2000);
}

await ScreenshotAsync("quota-02-after-message");

if (hasQuota)
{
// Verify quota indicator content
var quotaText = await GetTextAsync("#quota-indicator, .quota-indicator, .quota-pill");
Output.WriteLine($"Quota indicator text: '{quotaText}'");
Assert.False(string.IsNullOrWhiteSpace(quotaText), "Quota indicator should show percentage");

await ScreenshotAsync("quota-03-indicator-visible");
}
else
{
Output.WriteLine("Quota indicator did not appear — may need more messages or token may not have quota data");
// Don't fail — the test documents the behavior. Quota depends on the account's plan.
}
}

[Fact]
public async Task Dashboard_LoadsWithoutErrors()
{
await WaitForCdpReadyAsync();

var dashboardLoaded = await ExistsAsync(".dashboard, #scheduled-tasks-page, .session-item");
Assert.True(dashboardLoaded, "Dashboard should render without errors");

await ScreenshotAsync("quota-dashboard");
}

[Fact]
public async Task UsageCommand_ShowsQuotaInfo()
{
await WaitForCdpReadyAsync();

// Navigate to a session if not already in one
var hasSession = await ExistsAsync(".session-item, .session-list-item");
if (hasSession)
{
await ClickAsync(".session-item, .session-list-item");
await Task.Delay(2000);
}

// Type /usage command
var fillResult = await CdpEvalAsync(
"const sel = '.card-input input, .card-input textarea, .input-row textarea, textarea'; " +
"const input = [...document.querySelectorAll(sel)].find(i => i.offsetParent !== null); " +
"if (input) { input.value = '/usage'; " +
"input.dispatchEvent(new Event('input', {bubbles:true})); " +
"input.dispatchEvent(new Event('change', {bubbles:true})); 'filled'; } else { 'no input'; }");
Output.WriteLine($"Fill /usage: {fillResult}");

if (fillResult == "filled")
{
// Send it
await CdpEvalAsync(
"const sel = '.card-input input, .card-input textarea, .input-row textarea, textarea'; " +
"const input = [...document.querySelectorAll(sel)].find(i => i.offsetParent !== null); " +
"if (input) { input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true})); }");
await Task.Delay(2000);

await ScreenshotAsync("quota-usage-command");
}
}
}
1 change: 1 addition & 0 deletions PolyPilot.Tests/PolyPilot.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Compile Include="../PolyPilot/Models/ChatMessage.cs" Link="Shared/ChatMessage.cs" />
<Compile Include="../PolyPilot/Models/ChatCopyPayloads.cs" Link="Shared/ChatCopyPayloads.cs" />
<Compile Include="../PolyPilot/Models/AgentSessionInfo.cs" Link="Shared/AgentSessionInfo.cs" />
<Compile Include="../PolyPilot/Models/QuotaDisplayHelper.cs" Link="Shared/QuotaDisplayHelper.cs" />
<Compile Include="../PolyPilot/Models/SynchronizedMessageQueue.cs" Link="Shared/SynchronizedMessageQueue.cs" />
<Compile Include="../PolyPilot/Models/AuditLogEntry.cs" Link="Shared/AuditLogEntry.cs" />
<Compile Include="../PolyPilot/Models/BridgeMessages.cs" Link="Shared/BridgeMessages.cs" />
Expand Down
138 changes: 138 additions & 0 deletions PolyPilot.Tests/QuotaDisplayTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using PolyPilot.Models;
using PolyPilot.Services;

namespace PolyPilot.Tests;

/// <summary>
/// Tests for QuotaDisplayHelper: level classification, reset countdown,
/// compact summary, and CSS class mapping.
/// </summary>
public class QuotaDisplayTests
{
// ─── GetQuotaLevel ───

[Theory]
[InlineData(100, QuotaLevel.Normal)]
[InlineData(80, QuotaLevel.Normal)]
[InlineData(51, QuotaLevel.Normal)]
[InlineData(50, QuotaLevel.Warning)]
[InlineData(35, QuotaLevel.Warning)]
[InlineData(21, QuotaLevel.Warning)]
[InlineData(20, QuotaLevel.Critical)]
[InlineData(10, QuotaLevel.Critical)]
[InlineData(1, QuotaLevel.Critical)]
[InlineData(0, QuotaLevel.Critical)]
public void GetQuotaLevel_ReturnsCorrectLevel(int remaining, QuotaLevel expected)
{
Assert.Equal(expected, QuotaDisplayHelper.GetQuotaLevel(remaining));
}

// ─── GetCssClass ───

[Fact]
public void GetCssClass_MapsLevelsToCssStrings()
{
Assert.Equal("normal", QuotaDisplayHelper.GetCssClass(QuotaLevel.Normal));
Assert.Equal("warning", QuotaDisplayHelper.GetCssClass(QuotaLevel.Warning));
Assert.Equal("critical", QuotaDisplayHelper.GetCssClass(QuotaLevel.Critical));
}

// ─── FormatResetCountdown ───

[Fact]
public void FormatResetCountdown_NullOrEmpty_ReturnsNull()
{
Assert.Null(QuotaDisplayHelper.FormatResetCountdown(null));
Assert.Null(QuotaDisplayHelper.FormatResetCountdown(""));
Assert.Null(QuotaDisplayHelper.FormatResetCountdown(" "));
}

[Fact]
public void FormatResetCountdown_InvalidDate_ReturnsNull()
{
Assert.Null(QuotaDisplayHelper.FormatResetCountdown("not-a-date"));
}

[Fact]
public void FormatResetCountdown_FutureDate_ShowsDays()
{
var futureDate = DateTime.UtcNow.AddDays(5).ToString("yyyy-MM-dd");
var result = QuotaDisplayHelper.FormatResetCountdown(futureDate);
Assert.NotNull(result);
Assert.Contains("Resets in", result);
Assert.Contains("days", result);
}

[Fact]
public void FormatResetCountdown_Tomorrow_ShowsTomorrow()
{
var tomorrow = DateTime.UtcNow.AddHours(25).ToString("yyyy-MM-dd");
var result = QuotaDisplayHelper.FormatResetCountdown(tomorrow);
Assert.NotNull(result);
// Could be "Resets tomorrow" or "Resets in 2 days" depending on time of day
Assert.StartsWith("Resets", result);
}

[Fact]
public void FormatResetCountdown_PastDate_ReturnsOverdue()
{
var pastDate = DateTime.UtcNow.AddDays(-3).ToString("yyyy-MM-dd");
var result = QuotaDisplayHelper.FormatResetCountdown(pastDate);
Assert.Equal("Reset overdue", result);
}

// ─── FormatCompactSummary ───

[Fact]
public void FormatCompactSummary_Unlimited_ReturnsUnlimited()
{
var quota = new QuotaInfo(true, 0, 0, 100, null);
Assert.Equal("Unlimited", QuotaDisplayHelper.FormatCompactSummary(quota));
}

[Fact]
public void FormatCompactSummary_Limited_ShowsRemainingAndPercent()
{
var quota = new QuotaInfo(false, 300, 42, 86, "2026-05-01");
var summary = QuotaDisplayHelper.FormatCompactSummary(quota);
Assert.Contains("258/300 remaining", summary);
Assert.Contains("86%", summary);
}

[Fact]
public void FormatCompactSummary_ZeroRemaining()
{
var quota = new QuotaInfo(false, 100, 100, 0, "2026-05-01");
var summary = QuotaDisplayHelper.FormatCompactSummary(quota);
Assert.Contains("0/100 remaining", summary);
Assert.Contains("0%", summary);
}

// ─── Threshold constants ───

[Fact]
public void ThresholdConstants_AreReasonable()
{
Assert.Equal(20, QuotaDisplayHelper.CriticalThresholdPercent);
Assert.Equal(50, QuotaDisplayHelper.WarningThresholdPercent);
Assert.True(QuotaDisplayHelper.CriticalThresholdPercent < QuotaDisplayHelper.WarningThresholdPercent);
}

// ─── QuotaInfo warning scenario ───

[Fact]
public void QuotaInfo_BelowCritical_TriggersWarning()
{
var quota = new QuotaInfo(false, 300, 255, 15, "2026-05-01");
var level = QuotaDisplayHelper.GetQuotaLevel(quota.RemainingPercentage);
Assert.Equal(QuotaLevel.Critical, level);
}

[Fact]
public void QuotaInfo_Unlimited_NeverTriggersWarning()
{
var quota = new QuotaInfo(true, 0, 0, 100, null);
var level = QuotaDisplayHelper.GetQuotaLevel(quota.RemainingPercentage);
Assert.Equal(QuotaLevel.Normal, level);
}
}
5 changes: 4 additions & 1 deletion PolyPilot.Tests/UsageCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,10 @@ private static string FormatUsageOutput(AgentSessionInfo session, SessionUsageIn
usageLines.AppendLine($"- **Remaining:** {quota.RemainingPercentage}%");
}
if (!string.IsNullOrEmpty(quota.ResetDate))
usageLines.AppendLine($"- **Resets:** {quota.ResetDate}");
{
var countdown = QuotaDisplayHelper.FormatResetCountdown(quota.ResetDate);
usageLines.AppendLine($"- **Resets:** {quota.ResetDate}{(countdown != null ? $" ({countdown})" : "")}");
}
}
}
return usageLines.ToString().TrimEnd();
Expand Down
14 changes: 13 additions & 1 deletion PolyPilot/Components/ExpandedSessionView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,19 @@
@if (UsageInfo?.PremiumQuota != null)
{
var q = UsageInfo.PremiumQuota;
<div class="info-row"><span class="info-label">Premium</span><span class="info-value">@(q.IsUnlimited ? "Unlimited" : $"{q.EntitlementRequests - q.UsedRequests}/{q.EntitlementRequests} remaining")</span></div>
var qLevel = QuotaDisplayHelper.GetQuotaLevel(q.RemainingPercentage);
var qCss = QuotaDisplayHelper.GetCssClass(qLevel);
var qReset = QuotaDisplayHelper.FormatResetCountdown(q.ResetDate);
<div class="info-row">
<span class="info-label">Premium</span>
<span class="info-value quota-info-@qCss">
@(q.IsUnlimited ? "Unlimited" : $"{q.EntitlementRequests - q.UsedRequests}/{q.EntitlementRequests} remaining ({q.RemainingPercentage}%)")
@if (qReset != null)
{
<span class="quota-reset-hint"> · @qReset</span>
}
</span>
</div>
}
@if (Session.SessionId != null && PlatformHelper.IsDesktop)
{
Expand Down
4 changes: 4 additions & 0 deletions PolyPilot/Components/ExpandedSessionView.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@
}
.info-label { color: var(--text-muted); white-space: nowrap; }
.info-value { color: var(--text-primary); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }
.quota-info-normal { color: #34d399; }
.quota-info-warning { color: #f59e0b; }
.quota-info-critical { color: #f87171; }
.quota-reset-hint { color: var(--text-dim); font-size: var(--type-caption2); }
.info-action {
display: block;
width: calc(100% - 1rem);
Expand Down
Loading
Loading