Skip to content
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
2 changes: 1 addition & 1 deletion .github/skills/publish-extension/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Use [DEVELOPMENT.md](../../../DEVELOPMENT.md) as the canonical source for comman

## Invariants and completion

- Microsoft Store is the production and update channel. Do not install unsigned Actions artifacts, introduce self-signing, or replace a production Store registration with a development build.
- Microsoft Store is the production and update channel. Do not install unsigned Actions artifacts or introduce self-signing. Replacing a Store registration for explicitly requested local testing uses the backed-up `scripts/test-local.ps1` workflow in `DEVELOPMENT.md`; it is not a production release.
- Preserve the assigned Store identity, COM registration, capabilities, self-contained x64/ARM64 layout, and older releases. A change to these needs explicit task scope.
- Prefer the existing GitHub CLI/workflow and package scripts for deterministic operations; use the supported browser for Partner Center when no suitable API is configured. Follow repository escalation instructions. Never store credentials in Git or release records.
- Distinguish **committed**, **pushed/merged**, **package validated**, **submitted**, **certified**, **published**, **installed**, and **functionally verified**. Claim each only with evidence. A successful build does not prove installation; an old public Store listing does not prove the new version is available.
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ jobs:
run: dotnet restore ./CodexUsageDock.Tests/CodexUsageDock.Tests.csproj -p:Platform=x64 -r win-x64 -p:SelfContained=true
- name: Test
run: dotnet test ./CodexUsageDock.Tests/CodexUsageDock.Tests.csproj -c Debug -p:Platform=x64 -r win-x64 -p:SelfContained=true --no-restore
- name: Test local installation workflow
shell: powershell
run: |
Import-Module Pester -RequiredVersion 3.4.0
Invoke-Pester ./scripts/tests/test-local.Tests.ps1 -EnableExit
- name: Build x64 Debug
run: dotnet build ./CodexUsageDock/CodexUsageDock.csproj -c Debug -p:Platform=x64 -r win-x64 --self-contained true
- name: Build ARM64 Debug
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Use the [setup instructions](DEVELOPMENT.md#prerequisites) and [verification com
For changes affecting registration or Command Palette behavior:

1. After building, run `scripts/test-integration.ps1` with the matching `-Architecture` and without `-Register`. This checks build outputs and existing registration without changing installation.
2. Registration is a separate installation action: use `-Register` only within authorized scope and in the isolated environment required by [DEVELOPMENT.md](DEVELOPMENT.md#integration-smoke-test). It can close the running extension; never replace a production Store installation with a development package. A missing registration is not permission to install.
2. Registration is a separate installation action: use `-Register` only within authorized scope. Prefer an isolated environment. When the user explicitly requests testing in their current account, use the backed-up `scripts/test-local.ps1` workflow in [DEVELOPMENT.md](DEVELOPMENT.md#test-in-your-current-windows-account); it may replace the Store registration and closes the running extension. A missing registration is not permission to install.
3. For authorized integration work, complete **Reload Command Palette Extension** and the applicable functional checks in DEVELOPMENT.md. Report preflight warnings and unavailable UI checks explicitly. A passing build or preflight does not establish successful COM activation or working UI behavior.

## Security and Privacy
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ the [changelog workflow](AGENTS.md#documentation-and-change-quality).

## [Unreleased]

### Changed

- Show remaining allowance in dashboard bars and the weekly chart, restore daily token bars and their right-hand axis by default, and stop forecasts at the estimated limit. Prioritize weekly status and the chart, use full-width single windows, improve column separation and chart labels, and replace the Details toggle with Usage information in the More menu. ([PR #29](https://github.com/TheBeems/CodexUsageDock/pull/29))
- Compact dashboard headings and spacing, match single-window bar thickness to paired windows, and shorten Usage information. Use readable text rows with shared quota names and consistent English local dates in the text alternative. ([PR #29](https://github.com/TheBeems/CodexUsageDock/pull/29))

### Fixed

- Require App Installer only when the local test workflow needs Store installation or recovery, and retain the latest observation and current-value label when weekly chart segments exceed the rendering budget. ([PR #29](https://github.com/TheBeems/CodexUsageDock/pull/29))
- Detect Smart App Control enforcement before unsigned local-test installation or registration, preserving the current installation and explaining signed Store recovery. ([PR #29](https://github.com/TheBeems/CodexUsageDock/pull/29))
- Deduplicate default quotas regardless of their API position, avoid narrow token-table cells that split full numbers, omit empty forecast sections, and use the same rounded forecast labels throughout the dashboard, chart, and information page. ([PR #29](https://github.com/TheBeems/CodexUsageDock/pull/29))

### Added

- Add a Debug-only navigation test page, automated action-contract checks, and a repeatable host UI procedure for disappearing footer actions after Back. ([PR #29](https://github.com/TheBeems/CodexUsageDock/pull/29))
- Add a one-command local test workflow for the current Windows account, with data backups, validated development registration, and a command to return to Microsoft Store. ([PR #29](https://github.com/TheBeems/CodexUsageDock/pull/29))

## [0.8.1] - 2026-09-15

### Fixed
Expand Down
42 changes: 42 additions & 0 deletions CodexUsageDock.Tests/CodexUsageTableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@ namespace CodexUsageDock.Tests;

public sealed class CodexUsageTableTests
{
[Fact]
public void RemappedWeeklyQuotaAppearsOnceAndOtherCategoriesStaySeparate()
{
var now = new DateTimeOffset(2026, 9, 16, 0, 0, 0, TimeSpan.Zero);
var weekly = new RateLimitWindow(49, 10080, now.AddDays(4));
var snapshot = CodexUsageSnapshot.Loading with
{
Primary = null, Secondary = weekly, UpdatedAt = now, Source = UsageDataSource.AppServer,
ResetCredits = new(0, null),
DefaultBucketId = "codex",
Buckets = [new("codex", "codex", weekly, null), new("spark", "Spark", weekly, null)],
};
var view = new UsagePresentation(snapshot, [], [], new([], null), LocalTokenUsageSnapshot.Unavailable, false);
var body = CodexUsageTablePage.Format(view, now, TimeSpan.FromMinutes(1));
var groups = UsageQuotaGroups.Create(snapshot);
Assert.Equal(2, groups.Count);
Assert.Single(groups[0].Windows, item => item.Window == weekly);
Assert.Single(groups[1].Windows);
Assert.Equal(2, body.Split("**Weekly:** 51% remaining", StringSplitOptions.None).Length - 1);
Assert.Equal(1, body.Split("Not reported", StringSplitOptions.None).Length - 1);
Assert.DoesNotContain("primary", body, StringComparison.Ordinal);
Assert.DoesNotContain("10080", body, StringComparison.Ordinal);
}

[Fact]
public void FullTokenTotalsAndLocalDatesUseUnconstrainedTextRows()
{
var now = new DateTimeOffset(2026, 9, 15, 22, 1, 0, TimeSpan.Zero);
var timeZone = TimeZoneInfo.CreateCustomTimeZone("Test local", TimeSpan.FromHours(2), "Test local", "Test local");
var snapshot = CodexUsageSnapshot.Loading with { UpdatedAt = now, Source = UsageDataSource.AppServer };
var tokens = new LocalTokenUsageSnapshot([new(new DateOnly(2026, 9, 13), 51_732_933),
new(new DateOnly(2026, 9, 15), 9_999_999_999)], now, LocalTokenUsageStatus.Complete);
var view = new UsagePresentation(snapshot, [new(now, 51)], [new(now, 51)], new([], null), tokens, false);
var body = CodexUsageTablePage.Format(view, now, TimeSpan.FromMinutes(1), timeZone);
Assert.Contains("Observed: Wed 16 Sep 2026 00:01", body, StringComparison.Ordinal);
Assert.Contains("Times are local (Test local)", body, StringComparison.Ordinal);
Assert.Contains("- Sun 13 Sep 2026: **51,732,933** tokens", body, StringComparison.Ordinal);
Assert.Contains("- Tue 15 Sep 2026: **9,999,999,999** tokens", body, StringComparison.Ordinal);
Assert.DoesNotContain("| ---", body, StringComparison.Ordinal);
Assert.DoesNotContain("UTC", body, StringComparison.Ordinal);
}

[Fact]
public void TextViewKeepsUnknownZeroExpiredAndUnconfirmedStatesDistinct()
{
Expand Down
93 changes: 93 additions & 0 deletions CodexUsageDock.Tests/NavigationContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Text.Json;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Xunit;

namespace CodexUsageDock.Tests;

// Contract checks only: the host Back stack and command bar require the manual UI test.
public sealed class NavigationContractTests
{
private static readonly string[] DashboardActionTitles = ["Refresh now", "Codex Usage settings", "Usage information", "Read usage in text"];
#if DEBUG
private static readonly string[] FixtureChildNames = ["Settings", "Usage information", "Read usage in text"];
#endif

[Theory]
[InlineData("nl.mathijs.codexusage.settings")]
[InlineData("nl.mathijs.codexusage.information")]
[InlineData("nl.mathijs.codexusage.table")]
public void DashboardActionsSurviveChildContentReadsAndPresentationRefreshes(string childId)
{
using var environment = new TestEnvironment();
using var service = environment.CreateService();
using var provider = new CodexUsageDockCommandsProvider(service, environment.CreateSettings(), _ => { });
var dashboard = Assert.IsType<CodexUsageDockPage>(provider.GetCommandItem("nl.mathijs.codexusage.details")!.Command);
var originalActions = dashboard.Commands.ToArray();
var originalTargets = originalActions.Cast<CommandContextItem>().Select(item => Assert.IsAssignableFrom<ICommand>(item.Command)).ToArray();
Assert.Equal(DashboardActionTitles,
originalActions.Cast<CommandContextItem>().Select(item => item.Title));
var child = Assert.IsAssignableFrom<ContentPage>(originalTargets.Single(command => command.Id == childId));

for (var cycle = 0; cycle < 3; cycle++)
{
Assert.NotEmpty(child.GetContent());
dashboard.Refresh();
Assert.NotEmpty(dashboard.GetContent());
Assert.Equal(originalActions.Length, dashboard.Commands.Length);
for (var index = 0; index < originalActions.Length; index++)
{
Assert.Same(originalActions[index], dashboard.Commands[index]);
Assert.Same(originalTargets[index], Assert.IsType<CommandContextItem>(dashboard.Commands[index]).Command);
}
}
}

[Fact]
public void NavigationFixtureIsAvailableOnlyInDebugBuilds()
{
using var environment = new TestEnvironment();
using var service = environment.CreateService();
using var provider = new CodexUsageDockCommandsProvider(service, environment.CreateSettings(), _ => { });
const string id = "nl.mathijs.codexusage.navigation-test";
#if DEBUG
var item = Assert.Single(provider.TopLevelCommands(), item => item.Command.Id == id);
Assert.Same(item, provider.GetCommandItem(id));
Assert.IsType<NavigationTestPage>(item.Command);
#else
Assert.Null(provider.GetCommandItem(id));
Assert.DoesNotContain(provider.TopLevelCommands(), item => item.Command.Id == id);
Assert.Null(typeof(CodexUsageDockCommandsProvider).Assembly.GetType("CodexUsageDock.NavigationTestPage"));
#endif
}

#if DEBUG
[Fact]
public void FixtureUsesProductionPageShapesAndRetainsActionsDuringRefresh()
{
var dashboard = new NavigationTestPage();
var form = Assert.IsType<FormContent>(Assert.Single(dashboard.GetContent()));
var original = dashboard.Commands.ToArray();
var children = original.Skip(1).Cast<CommandContextItem>()
.Select(item => Assert.IsType<NavigationTestChildPage>(item.Command)).ToArray();
Assert.Equal(FixtureChildNames, children.Select(child => child.Name));
Assert.IsType<FormContent>(Assert.Single(children[0].GetContent()));
Assert.Single(children[0].Commands);
Assert.IsType<MarkdownContent>(Assert.Single(children[1].GetContent()));
Assert.Empty(children[1].Commands);
Assert.IsType<MarkdownContent>(Assert.Single(children[2].GetContent()));
Assert.Equal(2, children[2].Commands.Length);

var refresh = Assert.IsType<AnonymousCommand>(Assert.IsType<CommandContextItem>(original[0]).Command);
for (var cycle = 1; cycle <= 3; cycle++)
{
refresh.Invoke();
using var data = JsonDocument.Parse(form.DataJson);
Assert.Equal(cycle, data.RootElement.GetProperty("refreshCount").GetInt32());
Assert.Same(form, Assert.Single(dashboard.GetContent()));
for (var index = 0; index < original.Length; index++)
Assert.Same(original[index], dashboard.Commands[index]);
}
}
#endif
}
Loading