Skip to content

Profile CLI Switch#199

Open
FrankRay78 wants to merge 1 commit into
mainfrom
003-profile-cli-switch
Open

Profile CLI Switch#199
FrankRay78 wants to merge 1 commit into
mainfrom
003-profile-cli-switch

Conversation

@FrankRay78
Copy link
Copy Markdown
Owner

Summary

Introduces a public, provider-agnostic Profile enum (Tiny, Small, Medium, Large, Mega) on NetPace.Core, surfaced as a single --profile CLI switch and as two new public constructors on OoklaSpeedtestSettings. The profile bundles per-request shape (DownloadSizes, iterations, parallel tasks) and a total-byte cap into one knob, shifting the default-run traffic from ~370 MiB to ~125 MiB (Medium = the new default, ≥ 65 % reduction) so users on constrained data plans can stay within budget without further tuning. Breaking (pre-1.0): the four int sizeMb per-call overloads on ISpeedTestService are deleted; the cap now lives on DownloadTestSettings.DownloadSizeMb / UploadTestSettings.UploadSizeMb.

Refs #174

Spec

specs/003-profile-cli-switch

Changed Files

  • .claude/settings.json
  • .gitignore
  • .specify/feature.json
  • README.md
  • USER_GUIDE.md
  • docs/architecture/download-upload-size-controls.md
  • docs/change-intent-records/2026-05-15-profile-cli-switch.md
  • specs/003-profile-cli-switch/checklists/requirements.md
  • specs/003-profile-cli-switch/contracts/cli-flag.md
  • specs/003-profile-cli-switch/contracts/ooklasettings-ctors.md
  • specs/003-profile-cli-switch/contracts/profile-enum.md
  • specs/003-profile-cli-switch/contracts/speedtestservice-surface.md
  • specs/003-profile-cli-switch/data-model.md
  • specs/003-profile-cli-switch/plan.md
  • specs/003-profile-cli-switch/quickstart.md
  • specs/003-profile-cli-switch/research.md
  • specs/003-profile-cli-switch/spec.md
  • specs/003-profile-cli-switch/tasks.md
  • specs/003-profile-cli-switch/test-plan.md
  • src/NetPace.Console.Tests/CommandLineTestHost.cs
  • src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Display_Help.verified.txt
  • src/NetPace.Console.Tests/NetPace.Console.Tests.csproj
  • src/NetPace.Console.Tests/NetPaceConsoleTests.Profile.cs
  • src/NetPace.Console.Tests/NetPaceConsoleTests.cs
  • src/NetPace.Console.Tests/VerifyConfiguration.cs
  • src/NetPace.Console/Commands/SpeedTestCommandSettings.cs
  • src/NetPace.Console/CompositeAnsiConsole.cs
  • src/NetPace.Console/ConsoleWriters/CSVConsoleWriter.cs
  • src/NetPace.Console/ConsoleWriters/DefaultConsoleWriter.cs
  • src/NetPace.Console/ConsoleWriters/JsonConsoleWriter.cs
  • src/NetPace.Console/ConsoleWriters/MinimalConsoleWriter.cs
  • src/NetPace.Console/FileConsole.cs
  • src/NetPace.Console/OoklaSpeedtestSettingsAccessor.cs
  • src/NetPace.Console/Program.cs
  • src/NetPace.Console/Properties/Usings.cs
  • src/NetPace.Core.Tests/OoklaSpeedtestSettingsTests.Profiles.cs
  • src/NetPace.Core.Tests/OoklaSpeedtestSettingsTests.cs
  • src/NetPace.Core.Tests/OoklaSpeedtestTests.Guards.cs
  • src/NetPace.Core.Tests/OoklaSpeedtestTests.cs
  • src/NetPace.Core.Tests/ProfileTests.cs
  • src/NetPace.Core.Tests/ProfileXmlDocTests.cs
  • src/NetPace.Core/Clients/Ookla/OoklaSpeedtest.cs
  • src/NetPace.Core/Clients/Ookla/OoklaSpeedtestSettings.cs
  • src/NetPace.Core/Clients/Ookla/Settings/DownloadTestSettings.cs
  • src/NetPace.Core/Clients/Ookla/Settings/UploadTestSettings.cs
  • src/NetPace.Core/Clients/Testing/FaultySpeedTester.cs
  • src/NetPace.Core/Clients/Testing/SpeedTestMock.cs
  • src/NetPace.Core/Clients/Testing/SpeedTestStub.cs
  • src/NetPace.Core/Clients/Testing/VariableSpeedTester.cs
  • src/NetPace.Core/ISpeedTestService.cs
  • src/NetPace.Core/Profile.cs

New Artifacts

@FrankRay78 FrankRay78 linked an issue May 15, 2026 that may be closed by this pull request
31 tasks
@FrankRay78 FrankRay78 self-assigned this May 15, 2026
Comment thread src/NetPace.Core.Tests/ProfileTests.cs Fixed
Comment thread src/NetPace.Core.Tests/ProfileTests.cs Fixed
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 15, 2026

Code Review

Branch: 003-profile-cli-switchmain
Reviewer: Claude


Overview

This PR introduces a public, provider-agnostic Profile enum (Tiny/Small/Medium/Large/Mega) on NetPace.Core, surfaces it as a --profile CLI flag, and migrates the total-byte caps from per-call int sizeMb parameters into DownloadTestSettings/UploadTestSettings. The parameterless OoklaSpeedtestSettings() ctor now chains to Profile.Medium, dropping the default-run data cost from ~370 MiB to ~125 MiB (≥ 65 % reduction). The four int sizeMb overloads on ISpeedTestService are deleted as an accepted pre-1.0 breaking change.

The core implementation is excellent. One must-fix blocker needs to be addressed before merging.


Bugs / Correctness

No issues found.

  • Profile → Ookla field mapping is a pure-value switch expression; all five profiles' field values match the architecture doc tables exactly (verified by OoklaSpeedtestSettingsTests.Profiles).
  • Parameterless ctor correctly chains to Profile.Medium as the single source of truth.
  • Invalid profile values correctly throw ArgumentOutOfRangeException with ParamName == "profile".
  • with-expression cap overrides correctly preserve profile-supplied per-request shape while only mutating DownloadSizeMb/UploadSizeMb.
  • All four console writers (CSV, Default, JSON, Minimal) correctly removed the now-deleted sizeMb parameter from their GetDownloadSpeedAsync/GetUploadSpeedAsync calls.

Security

No issues found.

  • --profile is bound as an enum by System.CommandLine; no custom parsing, no user string eval.
  • Largest profile (Mega) is capped at 10 GiB down / 2 GiB up and requires explicit opt-in — not a default.
  • No reflection, no unbound resource loops.

Convention Adherence

MUST-FIX — .claude/settings.json contains ad-hoc development allowlist entries

The diff adds 12 new entries to the allow array in .claude/settings.json that were accumulated during development and must not be committed to the project-level settings file:

"Bash(Select-Object -Last 30)",
"Bash(pwsh -NoProfile -Command \"Set-Location 'd:/Source/Repos/NetPace'; dotnet build ...",  // hard-coded local path
"Bash(pwsh -NoProfile -Command \"dotnet build src/NetPace.sln ... | Select-Object -Last 20\")",
"Bash(pwsh -NoProfile -Command \"dotnet test ... | Select-String -Pattern '...' | Out-String\")",
// ... 8 more variants
  • One entry contains a developer-machine absolute path (d:/Source/Repos/NetPace) — will never match on any other environment and confuses readers about what is actually being permitted.
  • All 12 are session-scoped build/test command variants generated during development iteration. They shouldn't be in the shared project file.

Fix: Remove all 12 new allow entries added in this PR. If any of these are genuinely useful as project-wide permissions, rewrite them as portable relative-path patterns (e.g. "Bash(dotnet build:*)") and submit them separately for review.

GOOD — Everything else

  • Profile correctly lives at src/NetPace.Core/Profile.cs (top-level, not under Clients/). typeof(Profile).Namespace == "NetPace.Core" is reflection-verified by ProfileTests.
  • Dependency direction is correct: Profile → nothing; OoklaSpeedtestSettings(Profile)Profile; no reverse edge.
  • CompositeAnsiConsole.Profile is correctly qualified as Spectre.Console.Profile to resolve the name collision with the new enum — good catch.
  • All public additions in NetPace.Core carry XML docs. Profile.Mega's doc includes the bonus-payload caveat, payload IDs, and architecture doc cross-reference (enforced by ProfileXmlDocTests).
  • No column-aligned whitespace. No reflection or typeof/GetMethod in the profile dispatch path (trim-safe). No specs/ path references in committed source, tests, or docs.
  • Build produces zero warnings. The .specify/feature.json reference to specs/003-profile-cli-switch is internal tooling, not production code.

Test Coverage

Excellent. 23 new test cases across 5 files with complete coverage:

File What it covers
OoklaSpeedtestSettingsTests.Profiles.cs Field-for-field equality for all 5 profiles against architecture doc values
OoklaSpeedtestSettingsTests.cs Parameterless ctor chains to Medium; invalid profile throws; with preserves shape; no stored Profile property
NetPaceConsoleTests.Profile.cs CLI case-insensitivity; unknown value rejected; --profile is authoritative; default is Medium; override semantics (--downloadsize/--uploadsize mutate cap only); --no-download short-circuit
ProfileTests.cs Namespace, source file path, no extension methods, enum member definitions, default value is Medium
ProfileXmlDocTests.cs Mega's XML doc contains undocumented, 5000/6000/7000, and architecture doc reference

CIR behaviour scenarios map 1:1 to test methods. No gaps identified.


Spec / Test-Plan Alignment

No mismatches found.

All seven CIR behaviour scenarios have explicit test coverage. The data-model values in tests agree exactly with docs/architecture/download-upload-size-controls.md §5 tables. The architectural constraint (Profile knows no provider; one inline switch; no factory methods) is verified by reflection tests.


Other Observations

  • OoklaSpeedtestSettingsAccessor design: The singleton accessor pattern for the test seam is well-chosen — it decouples CLI parsing verification from writer internals and avoids coupling writers to Ookla-specific settings. The per-test instantiation in BuildServicesWithSettingsAccessor() prevents cross-test state leakage.
  • Mega caveat documentation: The three-way documentation (XML doc, USER_GUIDE warning box, architecture doc §5.3) for Mega's bonus-payload dependency is thorough. Users and NuGet consumers both see it at the right moment.
  • Help snapshot: The --profile entry in Expectations/NetPaceConsoleTests.Should_Display_Help.verified.txt correctly shows Medium as the default and the valid values list. Ordering is sensible (between --datetimeformat and --downloadsize).

Summary

Category Status Notes
Bugs ✅ None
Security ✅ None
Conventions ❌ 1 MUST-FIX Remove 12 ad-hoc entries from .claude/settings.json allow list
Test coverage ✅ Excellent 23 tests, full scenario coverage
Spec alignment ✅ Clean All CIR behaviours tested

Verdict: CONDITIONAL APPROVAL — ready to merge once the .claude/settings.json allow-list entries added during this PR are removed.

@FrankRay78
Copy link
Copy Markdown
Owner Author

@claude Can you investigate the build failure here please: https://github.com/FrankRay78/NetPace/actions/runs/25920768575/job/76188887015?pr=199

It runs fine on my local machine and the code review above didn't identify anything.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 15, 2026

Claude finished @FrankRay78's task in 3m 25s —— View job


Build Failure Investigation — Fixed

  • Fetch CI logs from the failed run
  • Identify the root cause of the failure
  • Fix FileConsole disposed race condition
  • Commit and push

Root Cause

The test host was crashing with an unhandled ObjectDisposedException from a background thread — not a test assertion failure. The crash killed 180 already-passing tests along with it.

Call stack:

System.ObjectDisposedException: Cannot write to a closed TextWriter.
   at StreamWriter.Write(String value)
   at NetPace.Console.FileConsole.Write(...)        ← FileConsole.cs:69
   at NetPace.Console.CompositeAnsiConsole.Write(...)
   at Spectre.Console.ProgressContext.Refresh()
   at Spectre.Console.ProgressRefreshThread.Run()  ← background thread

What happens: Spectre.Console's ProgressRefreshThread runs on a background thread and calls FileConsole.Write() periodically. When a test ends and disposes the FileConsole (closing the StreamWriter), the refresh thread fires one more time against the now-closed writer and throws. Because it's unhandled on a background thread, it crashes the entire test host process.

Why it passes locally on Windows: Pure race condition. Windows thread scheduling and process-exit ordering happen to avoid the race; Linux CI runners expose it reliably.


Fix — FileConsole.cs

Added a _writeLock + _disposed guard to Write() and Dispose(). Background refresh-thread writes that arrive after disposal now silently no-op instead of throwing.

// Dispose — marks disposed inside the lock
lock (_writeLock)
{
    if (_disposed) return;
    _disposed = true;
    _fileWriter?.Flush();
    _fileWriter?.Dispose();
}

// Write — checks disposed inside the same lock
lock (_writeLock)
{
    if (_disposed) return;
    _fileWriter.Write(text);
}

The render step (GetSegments) is kept outside the lock since it has no side-effects on the writer.

Build: 0 Warning(s) 0 Error(s)

Commit: 0936fb0

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #199 Profile CLI Switch

Overview

Introduces a Profile enum (Tiny | Small | Medium | Large | Mega) on NetPace.Core, surfaced via a --profile CLI switch. Bundles per-request shape (download sizes, iterations, parallelism) and a total-byte cap into one knob. The new Medium default cuts traffic from ~370 MiB to ~125 MiB per run. As noted in the PR description, this is a pre-1.0 breaking change: four int sizeMb overloads are removed from ISpeedTestService.


Bugs / Correctness

No correctness issues found. Key logic is solid:

  • Profile → settings mapping uses an exhaustive switch expression with a _ => throw arm — no silent defaults.
  • int.MaxValue sentinel for "no override supplied" is consistently applied in BuildOoklaSettings.
  • with-expression on nested records creates copies, not mutations — correct.

Security

No vulnerabilities. System.CommandLine's enum parser rejects out-of-range values at parse time, and the OoklaSpeedtestSettings constructor provides a defensive second guard layer.


API Surface / Breaking Changes

Breaking change is intentional and documented. The removal of the four sizeMb overloads is flagged in the PR title, the CIR, and the spec contracts. The migration path (use DownloadTestSettings.DownloadSizeMb on the settings record) is clear. No inconsistencies detected between what the spec contracts describe and what was actually implemented.


C# Style / Conventions

MinorBuildOoklaSettings in Program.cs carries a <summary> tag but is missing <param> and <returns>:

/// <param name="settings">Parsed CLI settings including profile and override caps.</param>
/// <returns>OoklaSpeedtestSettings configured for the selected profile with any CLI overrides applied.</returns>

Everything else is clean: PascalCase throughout, nullable reference types enabled, async/await on all network paths, global alias using Profile = NetPace.Core.Profile correctly resolves the Spectre.Console name clash.


Test Coverage

Coverage is strong across four test files:

  • OoklaSpeedtestSettingsTests.Profiles.cs — five facts, one per profile, asserting every settings field.
  • NetPaceConsoleTests.Profile.cs — CLI binding, case-insensitive parsing, unknown value rejection, override logic.
  • ProfileTests.cs — namespace, enum membership, no stray extension methods.
  • ProfileXmlDocTests.cs — verifies Mega's caveat appears in published XML docs.

Minor gap — Contract C-OS-3 specifies that new OoklaSpeedtestSettings((Profile)999) throws ArgumentOutOfRangeException, but there is no [Fact] for it. The System.CommandLine parser prevents this at the CLI layer, so runtime risk is negligible, but the contract is untested. Suggested addition:

[Fact]
public void Constructor_InvalidProfileValue_ThrowsArgumentOutOfRangeException()
{
    // Given / When
    Action act = () => new OoklaSpeedtestSettings((Profile)999);

    // Then
    var ex = Assert.Throws<ArgumentOutOfRangeException>(act);
    Assert.Equal("profile", ex.ParamName);
}

Spec / Test-Plan Traceability

Traceability is excellent. Test method names map cleanly to spec user stories and contract identifiers:

Test name Covers
NoProfileFlag_DefaultsToMedium US2 — omitted --profile defaults to Medium
Profile_CaseInsensitiveEnumParsing_BindsToTiny US4 — case-insensitive parsing
DownloadSizeOverride_PreservesProfileShape_OnTiny US5 — override only the byte cap
Mega_DownloadSizes_ContainsBonusPayloads FR-021 — Mega includes 5000/6000/7000 MiB sizes

Other

DI Singleton noteOoklaSpeedtestSettingsAccessor is registered as Singleton and mutated during the command action. This is safe for the current single-invocation-per-process model, but a brief inline comment would protect the next person from making the container reusable without accounting for this:

// Singleton is safe because each process invocation runs one command action and exits.
// If this container is ever reused across invocations, convert to Scoped.

Summary

Category Severity Count
Correctness bugs 0
Security issues 0
Style / conventions Nit 1 (missing XML param/returns tags)
Test gaps Minor 1 (ArgumentOutOfRangeException for invalid Profile int cast)
Design notes Nit 1 (DI Singleton comment)

Recommendation: approve. The feature is well-specified, cleanly implemented, and comprehensively tested. The two nits and one minor test gap are non-blocking.

@FrankRay78
Copy link
Copy Markdown
Owner Author

@claude Something has gone wrong with my rebase, and it's causing the build to fail. It's like I'm missing the fix to the Spectre.Console breaking change - but I don't see how that's happened.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 22, 2026

Claude finished @FrankRay78's task in 1m 41s —— View job


Rebase Build Failure — Fixed

  • Check current CI status
  • Examine build logs for the failure
  • Identify what's missing/wrong after the rebase
  • Apply fix and verify build

Root Cause

CompositeAnsiConsoleTests.cs:49 — the SpyConsole.Profile property return type was left as the bare Profile, which became ambiguous after the rebase:

error CS0104: 'Profile' is an ambiguous reference between 'Spectre.Console.Profile' and 'NetPace.Core.Profile'
error CS0738: 'SpyConsole' does not implement interface member 'IAnsiConsole.Profile'

The test project's global usings (Properties/Usings.cs) bring NetPace.Core into scope, and CompositeAnsiConsoleTests.cs has using Spectre.Console — so Profile resolves to both. The exact same collision was already fixed in CompositeAnsiConsole.cs (the production file) by using Spectre.Console.Profile as the fully-qualified return type. The rebase dropped the equivalent fix from the test file.

Fix

One-line change in CompositeAnsiConsoleTests.cs to match the pattern already in CompositeAnsiConsole.cs:

// Before (ambiguous after rebase):
public Profile Profile => AnsiConsole.Console.Profile;

// After (matches CompositeAnsiConsole.cs pattern):
public Spectre.Console.Profile Profile => AnsiConsole.Console.Profile;

Build result: 0 Warning(s) 0 Error(s)

Commit: a450fb2
| Branch

@FrankRay78 FrankRay78 force-pushed the 003-profile-cli-switch branch from a450fb2 to 578fdad Compare May 22, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add --profile CLI switch (Tiny/Small/Medium/Large/Mega)

2 participants