Skip to content

Surface Transfer Failures - #222

Merged
FrankRay78 merged 26 commits into
mainfrom
feature/206-surface-transfer-failures
Sep 1, 2026
Merged

Surface Transfer Failures#222
FrankRay78 merged 26 commits into
mainfrom
feature/206-surface-transfer-failures

Conversation

@FrankRay78

Copy link
Copy Markdown
Owner

Closes #206.

Why

Certain upload endpoints (e.g. the Deutsche Telekom servers in #206) reported 0 bps while working on speedtest.net. The cause: the parallel test loop caught and discarded every per-request exception, counting failed requests as 0 bytes transferred. When every request failed, 0 bytes ÷ elapsed = 0 bps was reported as if it were a valid measurement — with no log, no message, and no non-zero exit code. This makes that failure visible.

Implements the approved specification in #206 (comment). Governing principle: Core reports the truth via request counts; the CLI applies policy. The exit code reflects only whether NetPace itself functioned — network conditions are data, not errors.

What changes

NetPace.Core (public API — MINOR bump)

  • SpeedTestResult gains RequestsAttempted / RequestsSucceeded / RequestsFailed. Per-request failures (transport errors, timeouts, non-success HTTP status) are aggregated into these counts instead of being swallowed; the method no longer throws for network outcomes (only caller cancellation and operational faults propagate).
  • Upload now validates the HTTP status — a rejected upload is a failed request, not throughput (mirrors the download path).
  • SpeedTestProgress gains FailedRequestReason to stream each failure live; nothing is retained on the result.
  • ISpeedTestService doc reversed to describe the counts-not-exceptions contract.

NetPace.Console

  • Exit code reflects only NetPace's health: total outage, 100% request failure, no servers found, and cancellation all exit 0; only operational failures (e.g. can't write --file) exit non-zero.
  • New --fail-on <total|partial|none> (default none) to opt in to a failure exit code; fail-fast and uniform across single / --count / --loop.
  • Counts appear in every output format: token annotation (Upload: 0 bps (32 of 32 requests failed)), CSV columns (DownloadSucceeded/DownloadFailed/…), and JSON integer fields. Human notices and operational errors now go to stderr; at --verbosity Debug each failure reason is streamed live.

Docs: README --help snapshot and a new USER_GUIDE "Detecting failed measurements" section (counts, exit-code model, --fail-on).

Non-obvious things a reviewer should know

  • This deliberately re-introduces a "return 0, don't throw" shape — the very shape that caused the bug — but only because two guarantees make a 0 bps un-ignorable: it always co-travels with the counts, and the CLI is obligated to surface them. Known accepted trade-off: netpace && next proceeds on a total outage by default; use --fail-on or inspect the counts to detect it.
  • JSON diverges slightly from the spec wording. The spec says an all-failed dimension's speed is "emitted as null"; I omit the speed field instead (System.Text.Json WhenWritingNull, consistent with how --no-download already drops fields), letting "UploadSucceeded": 0 carry validity. Per-instance explicit null isn't achievable without also emitting null for skipped dimensions. Happy to switch to explicit null if preferred.
  • Version: the MINOR bump is tag-driven in CI, so Directory.Build.props is intentionally untouched — tag v1.1.0 at release.
  • An earlier "continue multiple tests on exception" test was repurposed: a throwing service is now operational (exit 1), whereas an all-failed measurement is data (exit 0, loop continues).

How to verify

  • dotnet build src and dotnet test src are clean (633 tests, 0 warnings).
  • netpace --json against a server whose uploads all fail → JSON has "UploadSucceeded":0/"UploadFailed":N, no UploadSpeed, nothing on stderr, exit 0.
  • Same with --csv → the data row shows …,0,N,…; with default output → Upload: 0 bps (N of N requests failed) plus a stderr notice.
  • --fail-on total on an all-failed dimension → exit 1; default → exit 0.
  • --server <unreachable> (latency on) → exit 0 with the no-servers notice (regression: previously exit 1 only in this path).

Core reports the truth via per-request counts; the CLI applies policy.

- NetPace.Core: aggregate per-request failures into RequestsAttempted/
  Succeeded/Failed on SpeedTestResult instead of swallowing them; validate
  upload HTTP status (a rejected upload is a failed request, not throughput);
  never throw for network outcomes (only cancellation and operational faults);
  stream each failure reason live via SpeedTestProgress.FailedRequestReason;
  reverse the ISpeedTestService exception-propagation contract.
- NetPace.Console: route human/operational output to a stderr console; make the
  exit code reflect only NetPace's health (network conditions exit 0); add
  --fail-on total|partial|none (fail-fast); surface counts in every format
  (token annotation, CSV columns, JSON fields); stream failure reasons at Debug.
- Docs: README --help snapshot and a USER_GUIDE section on detecting failures.

Public NetPace.Core API change ships as a MINOR bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FrankRay78

Copy link
Copy Markdown
Owner Author

@claude Review this pull request. Analyse the code changes and provide feedback covering:

  • Bugs or correctness issues
  • Security concerns (including any flagged by static analysis)
  • Adherence to the project conventions in CLAUDE.md
  • Test coverage — do the tests adequately cover the new behaviour?
  • Any spec/test-plan mismatches

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FrankRay78's task in 8m 17s —— View job


Review: Surface Transfer Failures (#222)

Todo list

  • Gather context (diff, changed files, CLAUDE.md conventions)
  • Review NetPace.Core changes (OoklaSpeedtest, SpeedTestResult, SpeedTestProgress, ISpeedTestService)
  • Review NetPace.Console changes (SpeedTestCommand, writers, ErrorConsole, FailOn, exit-code logic)
  • Review test coverage (Core.Tests, Console.Tests, snapshot expectations)
  • Check docs (README, USER_GUIDE) for consistency with behaviour
  • Check for spec/test-plan mismatches vs issue some servers show an upload speed of 0 bps, unlike the speetest.net website #206
  • Post final review

Overall this is a well-scoped, thoroughly-tested implementation of the approved spec, and CI (Build and Test + CodeQL) is green. The core design — counts-not-exceptions in NetPace.Core, policy-in-the-CLI via --fail-on — is sound and consistently applied. Below are the issues I found, most-significant first.


1. --verbosity Debug live failure-reason streaming silently doesn't work with --csv / --json / --json-pretty

IConsoleWriter.PerformSpeedTestAsync now takes an errorConsole parameter for exactly this purpose, but only DefaultConsoleWriter (src/NetPace.Console/ConsoleWriters/DefaultConsoleWriter.cs:88-104) actually uses it. CSVConsoleWriter, JsonConsoleWriter, and MinimalConsoleWriter accept errorConsole but never reference it, and — more fundamentally — they all call the 2-arg GetDownloadSpeedAsync/GetUploadSpeedAsync overloads (no IProgress<SpeedTestProgress>), so FailedRequestReason progress updates are never even produced on those paths.

USER_GUIDE.md states unconditionally: "At --verbosity Debug, the raw reason for each failed request is streamed live to standard error." In practice netpace --csv --verbosity Debug (or --json/--json-pretty --verbosity Debug) prints nothing extra to stderr beyond the (also-suppressed, since ShouldEmitFailureNotice excludes CSV/JSON) failure notice — a real doc/behaviour mismatch.

This is also untested: the only debug-streaming test (Debug_Streams_Each_Failure_Reason_To_Stderr in NetPaceConsoleTests.Failures.cs) runs with no --csv/--json flag, so it only ever exercises DefaultConsoleWriter.

Suggest either scoping the USER_GUIDE line to interactive/Normal output, or wiring progress + errorConsole into the other three writers for consistency.

Fix this →


2. CSV header can be permanently skipped if the first --loop/--count iteration finds no server (pre-existing, but now much easier to hit)

SpeedTestCommand.ExecuteAsync drives the CSV header via initialSpeedTest: firstLoop (loop) / initialSpeedTest: (i == 0) (count) — both are keyed purely on iteration index, not on whether that first iteration actually produced a data row. Since finally { firstLoop = false; } runs even when PerformSpeedTestAsync returns SpeedTestOutcome.NoServers (no header/row written), a transient discovery failure on iteration 1 followed by success on iteration 2+ means no CSV header is ever printed, even though data rows are.

This flag/index pattern predates this PR, so it isn't a regression this PR introduces — but the PR's entire premise is that "no servers found" is now a routine, expected, non-throwing outcome in long-running --loop/--count sessions (see Should_Continue_Multiple_Speed_Tests_When_A_Measurement_All_Fails), which makes this latent edge case meaningfully more likely to actually occur in the field than it was when discovery failures usually threw. Worth a follow-up issue/test (--csv --loop where the first probe fails, later ones succeed) even if out of scope for this branch.


3. Minor / non-blocking observations

  • OoklaSpeedtest.cs:341-354 — the rewritten cancellation-vs-failure branch (e is OperationCanceledException && cancellationToken.IsCancellationRequested replacing the old !wasCancelledLocally check) is a genuine correctness fix beyond the stated scope: the old condition would propagate any OperationCanceledException not caused by the local byte-cap cancel — including, e.g., an HttpClient-internal per-request timeout (TaskCanceledException) — as if the user had cancelled, aborting the whole test. Good catch, worth calling out since it's not mentioned in the PR description.
  • GenericTestSpeedAsync's catch (Exception e) technically diverges from docs/conventions/csharp-style.md's "catch specific exception types" rule, but it's clearly the deliberate, well-commented mechanism for turning arbitrary per-request failures (transport, TLS, timeout, bad status) into counted data — the stated goal of the whole PR. Not a real concern, just flagging that a future style-linter pass shouldn't flag it blind.
  • JsonResult.csLatency/DownloadSpeed/UploadSpeed are required string (non-nullable) but assigned via latencyFormatted!/downloadFormatted!/uploadFormatted!, relying on DefaultIgnoreCondition = WhenWritingNull to drop them. This pattern predates this PR (already used for Latency) and is explicitly flagged by the author in "Non-obvious things," so no action needed — just noting it now covers two more fields.
  • No test exercises --quiet combined with an all-failed measurement to confirm the stderr notice still fires (existing quiet+file tests only cover the "no servers" case). Low priority, easy to add given ScriptedSpeedTester already supports it.

Adherence to CLAUDE.md / constitution

  • ✅ No new NetPace.Core dependencies (no .csproj changes).
  • ✅ XML docs present on all new/changed public NetPace.Core API surface (SpeedTestResult, SpeedTestProgress.FailedRequestReason, ISpeedTestService remarks).
  • ✅ README --help snapshot and USER_GUIDE both updated (per the "CLI feature issues must scope docs from the start" rule).
  • ✅ MINOR version bump correctly left tag-driven, Directory.Build.props untouched, as called out by the author.
  • Program.cs's top-level catch now writes usage/operational errors to errorConsole (stderr) instead of stdout — a nice incidental clig.dev-alignment fix.
  • No skipped tests, no build-warning risk introduced (CI confirms).

Test coverage

Coverage of the new behaviour is genuinely thorough: all-failed / partial-failure aggregation, status-code-as-failure for uploads, cancellation-still-propagates, and live-reason-streaming are all covered at the NetPace.Core level (OoklaSpeedtestTests.Failures.cs); exit-code policy, --fail-on (Total/Partial/None, fail-fast under --count), per-format notice suppression, and quiet/file interactions are covered at the NetPace.Console level via the new ScriptedSpeedTester (NetPaceConsoleTests.Failures.cs). The gaps I found are items #1 and #3's last bullet above.

Spec/test-plan mismatches

I couldn't fetch the linked issue #206 spec comment from this sandboxed environment (no network access) to do a line-by-line AC check, so I relied on the PR description + code/tests. The two divergences the author already flagged (JSON null-vs-omit, and the deliberate "return 0, don't throw" re-introduction) both read as reasonable, well-justified trade-offs. Test names reference AC5, AC8a/b, AC9–AC12 but not AC1–AC4/AC6/AC7 — likely because those map to the untagged NetPace.Core-level tests, but worth a quick self-check against the original comment before merge.
· Branch: feature/206-surface-transfer-failures

Comment thread src/NetPace.Console.Tests/CommandLineTestHost.cs Fixed
Comment thread src/NetPace.Console.Tests/ScriptedSpeedTester.cs Fixed
Comment thread src/NetPace.Console.Tests/ScriptedSpeedTester.cs Fixed
Comment thread src/NetPace.Console.Tests/ScriptedSpeedTester.cs Fixed
Comment thread src/NetPace.Console.Tests/ScriptedSpeedTester.cs Fixed
Comment thread src/NetPace.Console.Tests/ScriptedSpeedTester.cs Fixed
Comment thread src/NetPace.Console.Tests/ScriptedSpeedTester.cs Fixed
Comment thread src/NetPace.Console/Commands/SpeedTestCommand.cs Fixed
- CSV header no longer dropped when a leading --loop/--count iteration finds no
  server: key the header on the first actual data row, not the iteration index.
- Scope the USER_GUIDE Debug live-streaming claim to normal/interactive output
  (machine formats never write failure reasons to stderr, including at Debug).
- Rename the shadowing 'server' field in ScriptedSpeedTester (clears CodeQL).
- Add tests: CSV header survives a leading no-server iteration; JSON+Debug writes
  nothing to stderr; --quiet still emits the all-failed stderr notice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FrankRay78

Copy link
Copy Markdown
Owner Author

@claude Please re-review, identify only showstoppers or important issues (P1/P2).

@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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


Re-review: Surface Transfer Failures (#222)

Todo list

No P1/P2 (showstopper/important) issues found.

Commit 3d8eff6 addressed both substantive items from the prior review:

  • CSV header dropSpeedTestCommand.cs now keys the header on firstWrite (set false only once outcome.ServersFound), not iteration index, so a leading no-server iteration in --loop/--count no longer permanently suppresses the header. Verified in SpeedTestCommand.cs:51-92.
  • --verbosity Debug streaming doc mismatch — USER_GUIDE now scopes the live-streaming claim to normal/interactive output, matching the actual behaviour (CSV/JSON/Minimal writers never wire errorConsole/progress for this). Regression test added confirming JSON+Debug writes nothing extra to stderr.
  • CodeQL shadowing in ScriptedSpeedTester.cs (serverdefaultServer) is fixed.

The two remaining CodeQL notes (CommandLineTestHost.cs:38 missing-Dispose, SpeedTestCommand.cs:194 foreach-vs-Where) are both non-issues on inspection: testErrorConsole is already declared with using var, and the foreach/continue over a 2-element array is a style nit, not a correctness or security concern — neither rises to P1/P2.

Spot-re-checked the core failure-aggregation path (OoklaSpeedtest.cs GenericTestSpeedAsync), the CLI exit-code/--fail-on logic (SpeedTestCommand.cs), and the CSV/JSON writers for the IsAllFailed()/HasFailures() contract (SpeedTestResultReporting.cs:15-21, correctly guarding RequestsAttempted > 0 so a skipped dimension like --no-download isn't misreported as all-failed) — all consistent with the approved spec and no new correctness issues. CI (Build and Test) is green; CodeQL was still running at the time of this review.

Nothing blocking merge from this pass.
· Branch: feature/206-surface-transfer-failures

FrankRay78 and others added 7 commits July 15, 2026 09:05
Issue #206 is answered by the request counts, --fail-on and the exit code.
The standard-error channel and the live per-request reason stream were
implementation scope beyond that, and together they corrupted the progress
display: writes to a second IAnsiConsole landed inside Spectre's live
region, which repaints by moving the cursor, so each notice was partially
overwritten and pushed the bars down.

- Delete ErrorConsole and route notices back through the single console, as
  on main. The no-servers notice is now mode-gated like the failure notices
  so prose cannot corrupt CSV or JSON output.
- Remove SpeedTestProgress.FailedRequestReason and its plumbing;
  SpeedTestProgress is again identical to main. A total failure at
  --profile Mega would have emitted 456 near-identical lines, because the
  byte-budget cap only advances on success and so never trips.
- --quiet suppresses the notice along with the rest of the output (matching
  main); --fail-on is the detection path for quiet consumers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Discovery is not part of issue #206, which is answered by the request
counts, --fail-on and the exit code. The branch had rewritten it anyway:
ServerSelector swallowed every exception and returned null, collapsing DNS
errors, TLS errors and HTTP statuses into one fixed sentence. main reports
e.Message, so main was strictly more diagnosable on this path.

- Restore ServerSelector verbatim from main (discovery throws again).
- Restore WriteError and the per-iteration catch in SpeedTestCommand, and
  main's CSV header index; drop SpeedTestOutcome.ServersFound.
- Restore main's no-servers and network-exception snapshots.

One deliberate deviation: the per-iteration catch re-raises IOException and
UnauthorizedAccessException instead of swallowing them, so a fault in
NetPace itself still exits non-zero while network conditions exit 0. Covered
by Operational_Fault_During_A_Run_Exits_One.

Verified against origin/main by running both builds over the same
no-servers scenarios (default, --csv, --json, Minimal, --quiet): output and
exit codes are identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The class holds extension methods on SpeedTestResult, the same shape as
NetPace.Core's SpeedTestExtensions, but its name read like a service. Both
are now named for what they are, so a reader meeting GetSpeedString and
GetFailureAnnotation on the same line can see they are the same kind of
thing - Core stays policy-free, the CLI applies policy.

No behaviour change; call sites use extension syntax and are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread USER_GUIDE.md Outdated
Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname
```
- **JSON** — each active dimension gains integer `…Succeeded` / `…Failed` fields; on a total
failure the speed field is omitted (there is no valid measurement) while the counts remain:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Lets consider if we should include the 0 bps measurement field, so the JSON schema is always consistent - also it would align to the Normal and CSV behaviour. Please investigate why it's been implemented like this, and if there was an underlying good reason.

Comment thread USER_GUIDE.md Outdated
```
Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname
```
- **JSON** — each active dimension gains integer `…Succeeded` / `…Failed` fields; on a total

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Is the use of the word "dimension" obvious to the user - maybe there is a better term? Please investigate

Comment thread USER_GUIDE.md Outdated
operational failure (for example, being unable to write the `--file` output) exits non-zero. So a
`0 bps` measurement still exits `0` by default: inspect the counts (or use `--fail-on`) to detect it.

If you want a failed measurement to fail the process — for scripting or CI — opt in with

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Make: "If you want a failed measurement to fail the process, opt in with"

Comment thread USER_GUIDE.md Outdated
`1` at the first measurement that meets the threshold.

```bash
# In CI: treat a totally-failed dimension as a build failure

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Make: "# Treat a totally-failed dimension as a build failure"

Comment thread README.md Outdated
--file-mode Append Determines file output behavior. <Append, Overwrite>
-q, --quiet Suppress all normal console output (file output still works).
--fail-on None Exit with a non-zero code on a failed measurement. <None, Total, Partial>
None (default) never affects the exit code; Total triggers when a dimension is all-failed; Partial triggers on any failed request. Fail-fast across --count and --loop.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Make:
"None never affects the exit code; Total triggers when a dimension is all-failed; Partial triggers on any failed request."

Comment thread src/NetPace.Core/SpeedTestResult.cs Outdated
/// <see cref="RequestsSucceeded"/>; a value of zero with <see cref="RequestsAttempted"/> greater
/// than zero means every request failed and the reported speed is not a valid measurement.
/// </remarks>
public int RequestsAttempted { get; init; }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Why is this not computed from RequestsSucceeded/Failed - or left for the consumer to make this calc themself? I'm not sure I like this property. Please think and adivse.

Comment thread src/NetPace.Core/ISpeedTestService.cs Outdated
/// <param name="progress">A progress reporter that receives upload progress updates.</param>
/// <param name="cancellationToken">The token to allow the operation to be cancelled.</param>
/// <returns>The result including bytes processed and elapsed time in milliseconds.</returns>
/// <returns>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Revert this comment - I don't like including field level specifics here - the user should examine the return class for that. The existing comment was more robust.

Comment thread src/NetPace.Core/ISpeedTestService.cs Outdated
/// <param name="server">The server to measure upload speed from.</param>
/// <param name="cancellationToken">The token to allow the operation to be cancelled.</param>
/// <returns>The result including bytes processed and elapsed time in milliseconds.</returns>
/// <returns>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

As per my comment on line R120.

Comment thread src/NetPace.Core/ISpeedTestService.cs Outdated
/// <param name="progress">A progress reporter that receives download progress updates.</param>
/// <param name="cancellationToken">The token to allow the operation to be cancelled.</param>
/// <returns>The result including bytes processed and elapsed time in milliseconds.</returns>
/// <returns>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

As per my comment on line R120.

Comment thread src/NetPace.Core/ISpeedTestService.cs Outdated
/// <param name="server">The server to measure download speed from.</param>
/// <param name="cancellationToken">The token to allow the operation to be cancelled.</param>
/// <returns>The result including bytes processed and elapsed time in milliseconds.</returns>
/// <returns>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

As per my comment on line R120.

Comment thread src/NetPace.Core/ISpeedTestService.cs Outdated
/// when every request fails. Callers detect an unusable measurement by inspecting the counts
/// (for example, <see cref="SpeedTestResult.RequestsSucceeded"/> is zero). Exceptions are reserved
/// for caller-requested cancellation and for genuine operational failures; they do not signal
/// network conditions.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I hate this updated comment - you have put all sorts of implementation details on an interface. Favour the existing comment - but consider any material improvements, if you think there are some. Otherwise this is not the place to note all this. Let the code do the talking.

namespace NetPace.Core.Tests;

/// <summary>
/// Per-request failure aggregation for download and upload tests (issue #206): failed requests are

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I'm not a fan of including issue numbers in the code - please remove.

Comment thread src/NetPace.Console/Program.cs Outdated

var failOnOption = new Option<FailOn>("--fail-on")
{
Description = "Exit with a non-zero code on a failed measurement. <None, Total, Partial>\nNone (default) never affects the exit code; Total triggers when a dimension is all-failed; Partial triggers on any failed request. Fail-fast across --count and --loop.",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Align this with any changes made in the README.md changes I suggest below - make 100% sure they are aligned if either is updated

Comment thread src/NetPace.Console/JsonResult.cs


// Display speed test result.
// Display speed test result. The token carries the count annotation when requests failed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Revert this line please



// Display speed test result.
// Display speed test result. The token carries the count annotation when requests failed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Please revert this line.



if ((settings.Verbosity & Verbosity.Debug) != 0)
if (debug)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Please explain why this change.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

No good reason — that one's on me.

debug was declared at line 11 and used exactly once, here. It's a leftover from the stderr work reverted in fb0e632, where it had more than one call site; after the revert a single use doesn't earn a named alias.

Reverted the line and removed the local in 3fc7050.

FrankRay78 and others added 3 commits August 29, 2026 21:47
Revert the annotated "Display speed test result." comments in the default
and minimal writers, and drop the single-use `debug` local left behind by
the reverted stderr work - one call site does not earn a named alias.

Restore the original ISpeedTestService remarks and amend only the clause
the behaviour change made false, rather than replacing the contract note
with implementation detail.

Filter the --fail-on sequence explicitly with OfType instead of a
continue-guard, clearing the outstanding CodeQL finding, and drop the
ScriptedSpeedTester doc line describing per-request failure reasons on
the progress channel - that feature was reverted in fb0e632.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The writer-selection switch and ShouldEmitFailureNotice encoded the same
mapping over settings in two places, so a fifth writer meant editing both
and they could drift. Replace the settings sniffing with one property,
AcceptsProseNotices, answered by the writer that owns the output shape.

The predicate was never "is this machine readable" - MinimalConsoleWriter
is human prose and still excludes the notice. It is whether an extra line
of free text breaks what the writer promised: CSV promises delimited
records, JSON parseable documents, Minimal exactly one line per run.

Behaviour is unchanged; the existing failure tests cover both sides.

Make the console-output snapshot convention constitutional (1.6.0 ->
1.7.0) and reconcile it with the "do not test Spectre.Console" rule,
which read as a ban on the 125 snapshots the repo already relies on.
CLAUDE.md reviewed and updated in lockstep, per Governance point 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Restores ShouldEmitFailureNotice and the settings-based check in
SpeedTestCommand, and removes AcceptsProseNotices from IConsoleWriter and
its four implementations. The constitution and CLAUDE.md snapshot-testing
changes from 080b28d are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
{
if (result is not null && result.IsAllFailed())
{
console.WriteLine($"{testName} failed: all {result.RequestsFailed} requests to {serverUrl} failed.");

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Why should this not be moved out of SpeetTestCommand and put into DefaultConsoleWriter.PerformSpeedTestAsync ? The direct call to console here looks really really dirty.

Comment thread CLAUDE.md Outdated
The notice was emitted from SpeedTestCommand, which had to sniff the
output format to decide whether prose was safe - a condition that was
already an exact restatement of the writer-selection switch a few lines
above. DefaultConsoleWriter is the only writer that emits it, so the
notice moves there and ShouldEmitFailureNotice disappears rather than
being abstracted. The command no longer writes to the console directly,
and no longer depends on the composite console assigned into the primary
constructor parameter mid-method.

SpeedTestOutcome.ServerUrl existed only to carry the URL back for that
notice; the writer has it in hand, so the property and its four
assignments are removed. ProcessOutcome collapses into FailOnTriggered.

Output is unchanged - the notice still trails the help footer, which is
worth revisiting separately.

Snapshot the three shape-asserting failure tests, per the new Testing
Standards. The CSV one replaces a Split('\n')[1] plus Contains(",0,32,")
with the row itself.

Make JsonResult.Latency/DownloadSpeed/UploadSpeed `required string?`:
all three were assigned null with `!` when their test was skipped, so
the declaration now matches reality. No output change.

Split the snapshot guidance: the constitution carries the rule and the
rationale, CLAUDE.md the mechanics. CLAUDE.md includes the constitution,
so restating it there was guaranteed-redundant context.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FrankRay78 and others added 6 commits August 29, 2026 23:14
Pins the current output ahead of moving the notice, so the reorder shows
up as a reviewable diff rather than an invisible change. The snapshot
records the notice trailing the help footer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The notice was emitted after the "Try 'NetPace --help'" footer, leaving
it stranded below the sign-off and detached from the measurement it
explains. It now follows the result line directly, and the footer closes
the output as it does everywhere else.

The snapshot committed in the previous commit shows the move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The record spelled "may be absent" two ways: three conditional
properties were required string?, while their four count siblings were
plain int?. Now one rule holds throughout - required means the property
always appears, nullable means it is omitted when its test did not run -
and the class documents it.

Nothing is lost in practice: JsonResult has a single construction site,
and a property left unset fails the JSON snapshots immediately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The notice was specified for stderr, where a human-facing line sits on
its own channel and repeats nothing. That channel was reverted in
fb0e632; the notice stayed on stdout, where every fact in it is already
on screen - the URL four lines above in the server header, the count in
the token annotation immediately before it.

The counts were the point of this work. The annotation already spends
them; the notice spent them a second time, in longer form, on the same
stream.

Six of the sixteen failure tests were organised around its presence or
absence and are now simpler assertions about the annotation. The
USER_GUIDE paragraph and two stale doc comments go with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@FrankRay78

Copy link
Copy Markdown
Owner Author

@claude Review this pull request. Analyse the code changes and provide feedback covering:

  • Bugs or correctness issues
  • Security concerns (including any flagged by static analysis)
  • Adherence to the project conventions in CLAUDE.md
  • Test coverage — do the tests adequately cover the new behaviour?
  • Any spec/test-plan mismatches

@claude

claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FrankRay78's task in 4m 46s —— View job


Re-review: Surface Transfer Failures (#222)

Todo list

  • Gather context (diff vs origin/main, prior review threads, latest commits)
  • Review NetPace.Core changes (OoklaSpeedtest, SpeedTestResult, ISpeedTestService)
  • Review NetPace.Console changes (SpeedTestCommand, writers, FailOn, exit-code logic, JsonResult)
  • Review test coverage (Core.Tests, Console.Tests, snapshots)
  • Check docs (README, USER_GUIDE) for consistency
  • Check for spec/test-plan mismatches
  • Post final review

The branch has moved substantially since the last review pass (25 commits, several rounds of "address PR review feedback"). Almost everything raised in the two earlier reviews and Frank's inline thread comments is now resolved in the current HEAD (ef82170). One new, concrete issue found; everything else below is confirmation of what's already fixed.


Bugs / correctness

1. Misplaced XML doc comment in SpeedTestCommand.cs:8-23. The <remarks> block describing ExecuteAsync's exit-code policy is attached to the wrong member — it sits directly above WriteError, immediately followed by another <summary> for WriteError itself:

/// <summary>
/// Executes the speed test command using the provided settings.
/// </summary>
/// <remarks>
/// Network and discovery outcomes are data, not errors: ...
/// </remarks>
/// <summary>
/// Writes an error message to the console.
/// </summary>
private static void WriteError(IAnsiConsole console, string message)

Net effect: ExecuteAsync (line 33) now has no doc comment at all, and WriteError has two consecutive <summary> tags — malformed XML doc. GenerateDocumentationFile is false for NetPace.Console so this won't fail the build, but it's a genuine copy-paste defect that misattributes the exit-code-policy explanation. Move the <remarks> block back down to directly precede ExecuteAsync.

Fix this →

No other correctness issues found. Spot-checked:

  • RequestsAttempted was removed entirely from SpeedTestResult in favour of just RequestsSucceeded/RequestsFailed (addressing Frank's "why does this exist" comment) — IsAllFailed()/HasFailures() in SpeedTestResultExtensions.cs derive correctly from the two remaining counts, and every construction site (ScriptedSpeedTester, SpeedTestStub, VariableSpeedTester, OoklaSpeedtest) was updated consistently.
  • The VariableSpeedTester 0/0-counts bug Frank spotted in the CSV snapshot is fixed — all four call-count branches for both download and upload now populate real RequestsSucceeded values, and the corresponding snapshots were regenerated correctly.
  • OoklaSpeedtestSettingsAccessor registration was moved to always run (Program.cs:392, before the --test branch) — fixes a real DI gap (netpace --test previously threw "No service...registered"), and a new regression test (NetPaceConsoleTests.ServiceWiring.cs) covers it by driving Program.Main directly rather than through the test host (deliberately, so the test host's own default registrations can't mask the gap).
  • README's new --fail-on block (README.md:130-132) is byte-for-byte aligned with the actual --help snapshot and Program.cs's option description, as Frank asked to double-check.

Security / static analysis

All prior CodeQL findings are resolved:

  • The five ScriptedSpeedTester variable-shadowing warnings — renamed the field to defaultServer.
  • The TestConsole/missing-Dispose warning — moot now; the errorConsole/stderr-streaming feature that introduced the second TestConsole was fully reverted (commit fb0e632), so CommandLineTestHost.cs is back to a single using var testConsole.
  • The SpeedTestCommand.cs "missed opportunity to use Where" nit — FailOnTriggered now uses new[] { outcome.Download, outcome.Upload }.OfType<SpeedTestResult>() instead of a foreach/continue, which resolves it cleanly rather than suppressing it.

CLAUDE.md / constitution adherence

  • ✅ No new NetPace.Core dependencies.
  • ISpeedTestService's <returns> docs were reverted to the original terse form per Frank's request ("let the code do the talking"); only the interface-level <remarks> gained one sentence carving out the counts-not-exceptions exception.
  • JsonResult.DownloadSpeed/UploadSpeed are now string? (not required string assigned via !), matching the type to reality as Frank requested.
  • ✅ The snapshot-testing convention question is resolved by splitting it correctly: the constitution (Testing Standards) now carries the authoritative rule + rationale, CLAUDE.md carries a terse "how" pointer that cross-references it — consistent with CLAUDE.md's own stated split ("this guide covers HOW... the constitution covers WHAT and WHY").
  • ✅ Three of the sixteen NetPaceConsoleTests.Failures.cs tests were converted to Verify snapshots as agreed (the two JSON shape tests and the CSV all-failed row), the rest correctly stay as targeted asserts (exit code + notice presence).
  • ✅ Issue-number references (#206) removed from test file doc comments per Frank's request.
  • ✅ No skipped tests, no build-warning risk.

Test coverage

Coverage is thorough and now includes the gaps flagged in the first review: Quiet_All_Failed_Signals_Through_The_Exit_Code (the --quiet + all-failed gap), Unreachable_User_Specified_Server_Exits_Zero (the --server regression from the "How to verify" checklist), and Operational_Fault_During_A_Run_Exits_One (the one deliberate exit-1 case). Json_Debug_Reports_Only_The_Counts confirms the (now-simpler, since streaming was reverted) Debug-verbosity behaviour for machine formats.

Spec/test-plan mismatches

The one explicitly-flagged divergence from the PR description — JSON omitting the speed field on total failure instead of the spec's literal null — has actually been resolved in the other direction from what the PR description said: JSON, CSV, and Normal output all now report a literal "0 bps"/0 bps speed alongside the counts on a total failure, and only a genuinely skipped test (--no-download/--no-upload) omits fields. This is a more consistent design than either original option and directly answers Frank's "should the JSON schema always be consistent" comment. USER_GUIDE.md's "Detecting failed measurements" section was updated to match. No other mismatches found.


Nothing here blocks merge — item 1 is a doc-comment hygiene fix, not a behavioural bug.
· Branch: feature/206-surface-transfer-failures

@FrankRay78 FrankRay78 mentioned this pull request Aug 31, 2026
4 tasks
Comment thread src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs Dismissed
@FrankRay78

Copy link
Copy Markdown
Owner Author

@claude Please review the last commit, which addresses previous review findings. I'm looking for any remaining showstoppers or important items in the codebase, that would stop merging.

@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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


Re-review: Surface Transfer Failures (#222)

Todo list

  • Gather context (git log, last commit diff, prior review threads)
  • Check the last commit against previously raised findings (doc-comment fix, CodeQL Path.Combine, etc.)
  • Re-scan for new showstoppers/important issues introduced by the last commit
  • Post final review

No P1/P2 (showstopper/important) issues found. Commit fe74e4e ("Review feedback fixes") cleanly resolves the one item from the previous pass and fixes two more regressions along the way, all correctly.

What the last commit fixed

  • Misplaced XML doc comment (previously flagged) — the <remarks> block for ExecuteAsync's exit-code policy is now correctly positioned above ExecuteAsync itself; WriteError and IsOperationalFault moved back down to the bottom of the class with their own single <summary> each. Verified in SpeedTestCommand.cs:8-15 / 171-190.
  • HttpIOException misclassified as an operational fault (new regression fix)IsOperationalFault now reads e is (IOException and not HttpIOException) or UnauthorizedAccessException (SpeedTestCommand.cs:190), since HttpIOException derives from IOException and a reset connection is network data, not a NetPace fault. Covered by the new Network_IOException_Exits_Zero_By_Default regression test.
  • Markup injection via unescaped exception messages (new regression fix) — all three console.MarkupLine($"[red]Error:[/] {ex.Message}") call sites in Program.cs now call .EscapeMarkup(), so a --file path containing [/] (which Spectre.Console would otherwise try to parse as markup) renders literally instead of crashing. Covered by Unwritable_Output_File_Reports_The_Path_Literally.
  • CancelKeyPress handler leak (new regression fix)Program.Main previously subscribed a lambda to the process-wide Console.CancelKeyPress event and never unsubscribed it, leaving a handler closing over a disposed CancellationTokenSource reachable for the rest of the process's life (a real problem for any test host or long-lived process calling Main more than once). Now wrapped in try/finally with a named local function so it's always removed. Covered by Main_CanBeCalledTwiceInTheSameProcess.
  • --fail-on now also fires when a measurement never completes at all (discovery/latency throws) rather than only on all-failed results — a stricter, more correct interpretation ("didn't run" is worse than "ran and failed"), added consistently to all three loop shapes (single/--count/--loop) via the new FailOnRequested helper. Covered by FailOn_Exits_One_When_The_Measurement_Never_Ran (Theory over Total/Partial) and FailOn_Is_FailFast_When_The_Measurement_Never_Ran.
  • Doc prose in USER_GUIDE.md/README.md re-wrapped per the project's soft-wrap convention, and gained a line documenting the "never ran" --fail-on behavior above — consistent with the code change.
  • Should_Continue_Multiple_Speed_Tests_On_Exception (the original pre-PR "continue on exception" scenario, noted as repurposed in the PR description) is restored, now specifically exercising an operational exception path continuing the loop under default --fail-on None — distinct from, and complementary to, Should_Continue_Multiple_Speed_Tests_When_A_Measurement_All_Fails. Its pre-existing snapshot (NetPaceConsoleTests.Should_Continue_Multiple_Speed_Tests_On_Exception.verified.txt) is untouched and matches.

Checked and confirmed non-issues

  • CodeQL's "Path.Combine may silently drop its earlier arguments" flag on NetPaceConsoleTests.Failures.cs:92 (Path.Combine(Path.GetTempPath(), Path.GetRandomFileName(), "out[1].csv")) is a false positive here: neither Path.GetRandomFileName() nor the hardcoded literal "out[1].csv" can ever be rooted/absolute, which is the only condition under which Path.Combine drops earlier segments. Test-only code, not user input. Not blocking.
  • HttpIOException/HttpRequestException are used unqualified in SpeedTestCommand.cs and the test files with no explicit using System.Net.Http; in either the file or the project's Usings.cs — confirmed this already compiled cleanly pre-existing (HttpRequestException was used the same way before this commit, with CI green), so this isn't a new problem introduced here.
  • Spot-checked for stray duplicate test names / missing snapshots from the restored test — none found.

Nothing here blocks merge.
· Branch: feature/206-surface-transfer-failures

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.

some servers show an upload speed of 0 bps, unlike the speetest.net website

2 participants