Skip to content

feat: progress display redesign#28

Merged
woutervanranst merged 4 commits into
masterfrom
branch-jaw
Mar 28, 2026
Merged

feat: progress display redesign#28
woutervanranst merged 4 commits into
masterfrom
branch-jaw

Conversation

@woutervanranst
Copy link
Copy Markdown
Owner

@woutervanranst woutervanranst commented Mar 28, 2026

Summary

  • Eliminate stuck states: replaced Hashing 100% / Queued in TAR display dead-ends — files only appear while actively hashing or uploading, then disappear
  • TAR bundles as first-class entities: TAR archives are now tracked and displayed as their own progress lines with state (Accumulating → Sealing → Uploading) and byte-level upload progress
  • Live scanning counter: the scanning header ticks up per-file during enumeration and flips to (green) on completion
  • Queue depth: hashing and uploading headers show [N pending] when the pipeline has backpressure
  • Dedup counter: unique files/chunks are counted and shown in the hashing header and final summary
  • All three headers now go green: scanning, hashing, and uploading headers all flip from (yellow) to (green) when their stage completes

Changes

Core (Arius.Core)

  • FileScannedEvent changed from (long TotalFiles) to (string RelativePath, long FileSize) — per-file event instead of a running total
  • Added ScanCompleteEvent(long TotalFiles, long TotalBytes) — fires once enumeration finishes
  • Added TarBundleStartedEvent() — fired by tar builder when a new tar is initialized
  • Added OnHashQueueReady / OnUploadQueueReady callbacks to ArchiveOptions for live queue depth reporting

CLI (Arius.Cli)

  • ProgressState: simplified FileState enum (removed QueuedInTar/UploadingTar, added Hashed); new TrackedTar / TarState; new aggregate fields (FilesScanned, FilesUnique, HashQueueDepth, UploadQueueDepth)
  • All notification handlers rewritten to match new model
  • BuildArchiveDisplay redesigned with new layout; Spectre bracket escaping fixed ([[N pending]])
  • ProgressTests fully rewritten — 105 tests, all passing

Summary by CodeRabbit

  • New Features

    • Redesigned archive progress display with live per-file scanning ticks, a separate TAR-bundle section, and byte-level upload progress for TARs.
    • Added deduplication effectiveness counter and queue-depth indicators for hashing and uploading stages.
  • Documentation

    • Updated specs and proposal for new progress events, TAR lifecycle, and display requirements.
  • Tests

    • Updated tests to validate the new progress model, TAR lifecycle, and rendering behavior.

- Replace FileScannedEvent(long TotalFiles) with per-file (RelativePath, FileSize); add ScanCompleteEvent and TarBundleStartedEvent
- Add OnHashQueueReady/OnUploadQueueReady callbacks to ArchiveOptions; wire queue depth in CliBuilder
- Redesign ProgressState: simplified FileState enum, TrackedTar/TarState, new aggregate fields (FilesScanned, FilesUnique, queue depths)
- Rewrite all notification handlers: FileScanned, ScanComplete, TarBundleStarted, TarEntryAdded, ChunkUploading (dual-lookup), TarBundleSealing/Uploaded
- Redesign BuildArchiveDisplay: live scanning counter, hashing/uploading queue depth, TAR bundle lines, filter files to Hashing/Uploading only
- Fix Spectre markup: escape queue-depth brackets as [[N pending]] to avoid style-tag parse errors
- Fully rewrite ProgressTests (103 tests, all passing)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

This pull request redesigns the archive progress display pipeline: it changes scanning from a batch total to per-file FileScannedEvent(path, size) + ScanCompleteEvent, adds TAR bundle tracking (TrackedTar) with its own lifecycle and upload byte progress, introduces queue-depth callbacks on ArchiveOptions, and simplifies file lifecycle with a Hashed state hiding files until upload.

Changes

Cohort / File(s) Summary
OpenSpec Specification Documents
openspec/changes/progress-display-redesign/.openspec.yaml, openspec/changes/progress-display-redesign/design.md, openspec/changes/progress-display-redesign/proposal.md, openspec/changes/progress-display-redesign/specs/*
New and updated spec/design docs describing per-file scanning events, ScanComplete/TarBundleStarted notifications, TrackedTar lifecycle, simplified FileState (Hashed), queue-depth callbacks, dedup counting, and multi-section CLI display/render rules.
Core Archive Events & Options
src/Arius.Core/Archive/ArchiveModels.cs
Changed FileScannedEvent to (string RelativePath, long FileSize); added ScanCompleteEvent(long TotalFiles, long TotalBytes) and parameterless TarBundleStartedEvent(); added ArchiveOptions callbacks OnHashQueueReady and OnUploadQueueReady.
Core Archive Pipeline
src/Arius.Core/Archive/ArchivePipelineHandler.cs
Emit FileScannedEvent per discovered file and ScanCompleteEvent at end; invoke queue-depth callbacks after channel creation; publish TarBundleStartedEvent when creating a new tar; wrap sealed-tar upload streams in ProgressStream via CreateUploadProgress.
CLI Progress State Model
src/Arius.Cli/ProgressState.cs
Removed tar-specific file states (QueuedInTar, UploadingTar), added Hashed; removed TrackedFile.TarId; added TarState and new TrackedTar class; added TrackedTars, bundle numbering, tar target size, scanning counters (FilesScanned, BytesScanned, ScanComplete, TotalBytes), FilesUnique, and queue-depth getters.
CLI Progress Event Handlers
src/Arius.Cli/ProgressHandlers.cs
Added ScanCompleteHandler and TarBundleStartedHandler; refactored FileScannedHandler to increment per-file counters; converted tar handlers to maintain TrackedTar lifecycle (Accumulating → Sealing → Uploading) and perform hash-based removals/updates; ChunkUploadingHandler performs dual lookup (large file vs tar) and updates appropriate counters/states.
CLI Display Rendering & Progress Callbacks
src/Arius.Cli/CliBuilder.cs
Redesigned BuildArchiveDisplay to three-section layout (Scanning / Hashing / Uploading) with queue-depth and dedup indicators, per-file rows filtered to Hashing/Uploading, and a TAR-bundle section; CreateUploadProgress now supports dual lookup for large-file vs tar uploads and resets counters on transition.
Progress Tests
src/Arius.Cli.Tests/ProgressTests.cs
Extensively rewritten tests to validate new scan/tar/upload event ordering, Hashed semantics, TrackedTar lifecycle and byte progress, dual-lookup upload behavior, queue-depth indicators, and updated display expectations.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Archive Client
    participant Pipeline as Archive Pipeline
    participant Scanner as File Scanner
    participant Hash as Hash Stage
    participant TarBuilder as TAR Builder
    participant Upload as Upload Stage
    participant CLI as CLI Progress

    Client->>Pipeline: Start archive operation
    Pipeline->>Scanner: Begin enumeration

    loop per file discovered
        Scanner->>Pipeline: file discovered (path,size)
        Pipeline->>CLI: FileScannedEvent(path, size)
        CLI->>CLI: FilesScanned++, BytesScanned += size
    end

    Scanner->>Pipeline: enumeration complete
    Pipeline->>CLI: ScanCompleteEvent(totalFiles, totalBytes)
    CLI->>CLI: ScanComplete = true

    par hashing and TAR building
        loop per file
            Pipeline->>Hash: schedule hash
            Hash->>CLI: FileHashedEvent(contentHash)
            CLI->>CLI: mark TrackedFile state = Hashed
        end
    and
        loop per TAR bundle
            TarBuilder->>CLI: TarBundleStartedEvent()
            TarBuilder->>CLI: TarEntryAddedEvent(...)
            CLI->>CLI: remove small files from TrackedFiles, update TrackedTar.accumulated
            TarBuilder->>CLI: TarBundleSealingEvent(tarHash,totalBytes)
            CLI->>CLI: TrackedTar.state = Sealing, set TarHash
        end
    end

    par upload flows
        loop large-file uploads
            Upload->>CLI: ChunkUploadingEvent(contentHash,...)
            CLI->>CLI: find TrackedFile via contentHash → set Uploading, reset BytesProcessed
            Upload->>CLI: ProgressStream updates
            CLI->>CLI: TrackedFile.BytesProcessed += delta
        end
    and TAR uploads
        loop per TAR upload
            Upload->>CLI: ChunkUploadingEvent(tarHash,...)
            CLI->>CLI: find TrackedTar by TarHash → set Uploading
            Upload->>CLI: ProgressStream updates
            CLI->>CLI: TrackedTar.BytesUploaded += delta
            Upload->>CLI: TarBundleUploadedEvent(tarHash)
            CLI->>CLI: remove TrackedTar
        end
    end

    CLI->>Client: Render multi-section display (Scanning / Hashing / Uploading + per-file and TAR rows)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #9: Touches ArchiveModels and ArchivePipelineHandler with similar event and queue-callback changes that overlap core pipeline modifications here.
  • PR #20: Modifies archive pipeline implementation and notification emission logic; relevant to the Scan/FileScanned/ScanComplete changes in this PR.
  • PR #27: Adjusts CLI progress wiring, content-hash reverse mapping, and display/handler behavior that intersect the CLI-side restructuring in this PR.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: progress display redesign' accurately and concisely summarizes the main change: a redesign of the progress display feature affecting UI, Core events, and CLI state management.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch branch-jaw

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 28, 2026

Codecov Report

❌ Patch coverage is 89.79592% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.04%. Comparing base (d98eb8d) to head (b4ba47c).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
src/Arius.Cli/CliBuilder.cs 84.44% 6 Missing and 8 partials ⚠️
src/Arius.Core/Archive/ArchivePipelineHandler.cs 83.33% 0 Missing and 3 partials ⚠️
src/Arius.Cli/ProgressHandlers.cs 94.28% 0 Missing and 2 partials ⚠️
src/Arius.Cli/ProgressState.cs 98.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #28      +/-   ##
==========================================
+ Coverage   81.52%   82.04%   +0.52%     
==========================================
  Files          27       27              
  Lines        2890     3002     +112     
  Branches      330      354      +24     
==========================================
+ Hits         2356     2463     +107     
+ Misses        458      457       -1     
- Partials       76       82       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Arius.Core/Archive/ArchivePipelineHandler.cs (1)

173-212: ⚠️ Potential issue | 🔴 Critical

Propagate hash-stage exceptions to the channel instead of suppressing them with a normal EOF completion.

Parallel.ForEachAsync(...).ContinueWith(_ => hashedChannel.Writer.Complete(), CancellationToken.None) executes its continuation regardless of success or failure. When the parent task faults, Complete() is called without an exception argument, which signals normal EOF to downstream readers. This allows the dedup and upload stages to process incomplete data and return success, creating a silent data corruption path.

hashTask is never awaited in the pipeline completion (lines 571–576), so exceptions are never exposed.

The fix is to check the task status and propagate exceptions:

.ContinueWith(t => 
{
    if (t.IsFaulted)
        hashedChannel.Writer.Complete(t.Exception);
    else
        hashedChannel.Writer.Complete();
}, CancellationToken.None);

Also applies to: 570–576 (audit full pipeline await list).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Core/Archive/ArchivePipelineHandler.cs` around lines 173 - 212, The
Parallel.ForEachAsync continuation currently always calls
hashedChannel.Writer.Complete(), which suppresses exceptions and signals a
normal EOF; change the continuation on the Task returned by
Parallel.ForEachAsync (hashTask) to inspect the task status and call
hashedChannel.Writer.Complete(exception) when the task IsFaulted (propagating
t.Exception) otherwise call Complete() normally, and also ensure hashTask is
included/awaited in the pipeline completion await list so any exceptions bubble
up to the caller; look for Parallel.ForEachAsync, hashedChannel.Writer.Complete,
and the hashTask variable to make these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/progress-display-redesign/specs/archive-pipeline/spec.md`:
- Around line 58-68: The spec requires that when CreateHashProgress or
CreateUploadProgress are null the pipeline should NOT wrap streams in
ProgressStream; update ArchivePipelineHandler to follow that: in the code paths
that currently call new Progress<long>() and wrap the FileStream or TAR upload
stream with ProgressStream, change the logic to only construct and use
ProgressStream when CreateHashProgress/CreateUploadProgress returns a non-null
IProgress<long>, otherwise pass/use the original stream directly (no-op) so
there is no ProgressStream overhead; locate references to CreateHashProgress,
CreateUploadProgress, ProgressStream and the wrapping logic in
ArchivePipelineHandler and branch accordingly.

In `@src/Arius.Cli.Tests/ProgressTests.cs`:
- Around line 739-770: Add two unit tests to cover zero-queue upload states:
create one test that instantiates ProgressState, calls
IncrementChunksUploaded(1) (or leaves as-is if first upload is in flight), sets
UploadQueueDepth = () => 0, renders CliBuilder.BuildArchiveDisplay(state) via
RenderToString and asserts the header shows the zero-queue indication (e.g.,
contains "0" or the expected "pending"/queue text for zero); create another test
that sets UploadQueueDepth = () => 0, ensures SnapshotComplete is set via
SetSnapshotComplete() and ChunksUploaded is zero (no IncrementChunksUploaded),
renders with CliBuilder.BuildArchiveDisplay(state) and asserts the header shows
the completed/empty-run indicator (e.g., the filled/open circle expected). Use
the existing ProgressState, IncrementChunksUploaded, UploadQueueDepth,
SetSnapshotComplete, CliBuilder.BuildArchiveDisplay and RenderToString symbols
to locate where to add these tests.

In `@src/Arius.Cli/CliBuilder.cs`:
- Around line 893-910: The uploading header should be considered active as soon
as an upload is in-flight (even before any chunk/tar count or queue depth
appears) — update the condition that decides the active vs grey branch to treat
the header as active when SnapshotComplete is false or when ChunksUploaded,
TarsUploaded, or UploadQueueDepth indicate work; specifically, change the if
that currently checks (state.ChunksUploaded > 0 || state.TarsUploaded > 0 ||
queueDepth > 0) to also consider state.SnapshotComplete (e.g., if
(!state.SnapshotComplete || state.ChunksUploaded > 0 || state.TarsUploaded > 0
|| queueDepth > 0)) and ensure uploadSymbol still uses state.SnapshotComplete to
show completed vs in-progress.

In `@src/Arius.Cli/ProgressHandlers.cs`:
- Around line 65-68: The current code computes bundleNumber as
state.TrackedTars.Count + 1 and hard-codes 64MB, which can reuse IDs and ignore
overridden TarTargetSize; fix by adding a monotonic counter (e.g.,
ProgressState.NextBundleId or MonotonicBundleCounter) and an effective
TarTargetSize property on ProgressState, then obtain bundleNumber by
incrementing that counter (Interlocked.Increment) and construct the TrackedTar
with state.TarTargetSize (not 64L * 1024 * 1024) before calling
state.TrackedTars.TryAdd, ensuring each new TrackedTar uses a unique,
monotonically increasing ID.
- Around line 145-156: The current branch checks
ContentHashToPath.ContainsKey(...) which can be stale; change
ProgressState.SetFileUploading(string contentHash) to return a bool that
indicates whether it actually transitioned any TrackedFiles to
FileState.Uploading, then in ProgressHandlers.cs call that method and only call
state.IncrementFilesUnique() and return when SetFileUploading(...) returns true;
if it returns false, fall through to the TAR lookup (state.TrackedTars and
TarState.Uploading) so matching tar entries can still be promoted.

---

Outside diff comments:
In `@src/Arius.Core/Archive/ArchivePipelineHandler.cs`:
- Around line 173-212: The Parallel.ForEachAsync continuation currently always
calls hashedChannel.Writer.Complete(), which suppresses exceptions and signals a
normal EOF; change the continuation on the Task returned by
Parallel.ForEachAsync (hashTask) to inspect the task status and call
hashedChannel.Writer.Complete(exception) when the task IsFaulted (propagating
t.Exception) otherwise call Complete() normally, and also ensure hashTask is
included/awaited in the pipeline completion await list so any exceptions bubble
up to the caller; look for Parallel.ForEachAsync, hashedChannel.Writer.Complete,
and the hashTask variable to make these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 037a5d35-4765-4867-9182-ee536fbe2b97

📥 Commits

Reviewing files that changed from the base of the PR and between d98eb8d and c7e787c.

📒 Files selected for processing (13)
  • openspec/changes/progress-display-redesign/.openspec.yaml
  • openspec/changes/progress-display-redesign/design.md
  • openspec/changes/progress-display-redesign/proposal.md
  • openspec/changes/progress-display-redesign/specs/archive-pipeline/spec.md
  • openspec/changes/progress-display-redesign/specs/cli/spec.md
  • openspec/changes/progress-display-redesign/specs/progress-display/spec.md
  • openspec/changes/progress-display-redesign/tasks.md
  • src/Arius.Cli.Tests/ProgressTests.cs
  • src/Arius.Cli/CliBuilder.cs
  • src/Arius.Cli/ProgressHandlers.cs
  • src/Arius.Cli/ProgressState.cs
  • src/Arius.Core/Archive/ArchiveModels.cs
  • src/Arius.Core/Archive/ArchivePipelineHandler.cs

Comment on lines +58 to +68
#### Scenario: Core uses hash progress callback
- **WHEN** a file begins hashing and `CreateHashProgress` is not null
- **THEN** the pipeline SHALL call `CreateHashProgress(relativePath, fileSize)` and wrap the `FileStream` in a `ProgressStream` using the returned `IProgress<long>`
- **WHEN** `CreateHashProgress` is null
- **THEN** the pipeline SHALL hash the file stream directly without wrapping

#### Scenario: Core uses upload progress callback
- **WHEN** a chunk begins uploading and `CreateUploadProgress` is not null
- **THEN** the pipeline SHALL call `CreateUploadProgress(contentHash, size)` and use the returned `IProgress<long>` with `ProgressStream`
- **WHEN** `CreateUploadProgress` is null
- **THEN** the pipeline SHALL use a no-op `IProgress<long>` (current behavior)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

These SHALLs don't match the current null-callback path.

src/Arius.Core/Archive/ArchivePipelineHandler.cs still wraps both the hash stream and the TAR upload stream in ProgressStream with new Progress<long>() when the callback is null. As written, this spec promises a direct stream path and "no ProgressStream overhead", which the implementation in this PR does not do.

Also applies to: 176-178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/progress-display-redesign/specs/archive-pipeline/spec.md`
around lines 58 - 68, The spec requires that when CreateHashProgress or
CreateUploadProgress are null the pipeline should NOT wrap streams in
ProgressStream; update ArchivePipelineHandler to follow that: in the code paths
that currently call new Progress<long>() and wrap the FileStream or TAR upload
stream with ProgressStream, change the logic to only construct and use
ProgressStream when CreateHashProgress/CreateUploadProgress returns a non-null
IProgress<long>, otherwise pass/use the original stream directly (no-op) so
there is no ProgressStream overhead; locate references to CreateHashProgress,
CreateUploadProgress, ProgressStream and the wrapping logic in
ArchivePipelineHandler and branch accordingly.

Comment on lines +739 to +770
[Test]
public void BuildArchiveDisplay_UploadingHeader_ShowsQueueDepth_WhenNonZero()
{
var state = new ProgressState();
state.IncrementChunksUploaded(100);
state.UploadQueueDepth = () => 3;

var output = RenderToString(CliBuilder.BuildArchiveDisplay(state));
output.ShouldContain("3");
output.ShouldContain("pending");
}

[Test]
public void BuildArchiveDisplay_UploadingHeader_FilledCircle_WhenSnapshotComplete()
{
var state = new ProgressState();
state.IncrementChunksUploaded(100);
state.SetSnapshotComplete();

var output = RenderToString(CliBuilder.BuildArchiveDisplay(state));
output.ShouldContain("●");
}

[Test]
public void BuildArchiveDisplay_UploadingHeader_OpenCircle_WhenNotComplete()
{
var state = new ProgressState();
state.IncrementChunksUploaded(100);
// SnapshotComplete NOT set

var output = RenderToString(CliBuilder.BuildArchiveDisplay(state));
output.ShouldContain("○");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add regressions for the zero-queue upload states.

These assertions only cover the header after ChunksUploaded increments. They won't catch the grey header while the first upload is already in flight, or after a fully deduped/empty run completes with SnapshotComplete and zero uploaded chunks.

🧪 Suggested cases
+    [Test]
+    public void BuildArchiveDisplay_UploadingHeader_OpenCircle_WhenUploadIsInFlight()
+    {
+        var state = new ProgressState();
+        state.AddFile("large.bin", 10_000_000);
+        state.SetFileHashed("large.bin", "hash1");
+        state.SetFileUploading("hash1");
+
+        var output = RenderToString(CliBuilder.BuildArchiveDisplay(state));
+        output.ShouldContain("Uploading");
+        output.ShouldContain("○");
+    }
+
+    [Test]
+    public void BuildArchiveDisplay_UploadingHeader_FilledCircle_WhenSnapshotCompletesWithoutUploads()
+    {
+        var state = new ProgressState();
+        state.SetSnapshotComplete();
+
+        var output = RenderToString(CliBuilder.BuildArchiveDisplay(state));
+        output.ShouldContain("Uploading");
+        output.ShouldContain("●");
+    }
As per coding guidelines, `**/*.Tests/**`: Test projects. Focus on test coverage gaps and assertion quality rather than style.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli.Tests/ProgressTests.cs` around lines 739 - 770, Add two unit
tests to cover zero-queue upload states: create one test that instantiates
ProgressState, calls IncrementChunksUploaded(1) (or leaves as-is if first upload
is in flight), sets UploadQueueDepth = () => 0, renders
CliBuilder.BuildArchiveDisplay(state) via RenderToString and asserts the header
shows the zero-queue indication (e.g., contains "0" or the expected
"pending"/queue text for zero); create another test that sets UploadQueueDepth =
() => 0, ensures SnapshotComplete is set via SetSnapshotComplete() and
ChunksUploaded is zero (no IncrementChunksUploaded), renders with
CliBuilder.BuildArchiveDisplay(state) and asserts the header shows the
completed/empty-run indicator (e.g., the filled/open circle expected). Use the
existing ProgressState, IncrementChunksUploaded, UploadQueueDepth,
SetSnapshotComplete, CliBuilder.BuildArchiveDisplay and RenderToString symbols
to locate where to add these tests.

Comment on lines +893 to +910
// ── 6.3 Uploading header ──────────────────────────────────────────────
// Shows unique chunks uploaded, queue depth.
{
var chunksUploaded = state.ChunksUploaded;
var tarsUploaded = state.TarsUploaded;
var queueDepth = state.UploadQueueDepth?.Invoke() ?? 0;

// Hashing
if (totalFiles.HasValue && filesHashed >= totalFiles.Value)
lines.Add(new Markup($" [green]●[/] Hashing [dim]{filesHashed}/{totalFiles.Value}[/]"));
else if (totalFiles.HasValue)
lines.Add(new Markup($" [yellow]○[/] Hashing [dim]{filesHashed}/{totalFiles.Value}[/]"));
else
lines.Add(new Markup($" [yellow]○[/] Hashing [dim]{filesHashed}...[/]"));

// Uploading
var chunksUploaded = state.ChunksUploaded;
var totalChunks = state.TotalChunks;
var tarsUploaded = state.TarsUploaded;
if (totalChunks.HasValue && chunksUploaded >= totalChunks.Value && chunksUploaded > 0)
lines.Add(new Markup($" [green]●[/] Uploading [dim]{chunksUploaded}/{totalChunks.Value} chunks[/]"));
else if (totalChunks.HasValue)
lines.Add(new Markup($" [yellow]○[/] Uploading [dim]{chunksUploaded}/{totalChunks.Value} chunks[/]"));
else if (chunksUploaded > 0 || tarsUploaded > 0)
lines.Add(new Markup($" [yellow]○[/] Uploading [dim]{chunksUploaded} chunks...[/]"));
else
lines.Add(new Markup(" [grey] Uploading[/]"));
if (chunksUploaded > 0 || tarsUploaded > 0 || queueDepth > 0)
{
var queuePart = queueDepth > 0 ? $" [dim][[{queueDepth} pending]][/]" : string.Empty;
var uploadDone = state.SnapshotComplete;
var uploadSymbol = uploadDone ? "[green]●[/]" : "[yellow]○[/]";
lines.Add(new Markup($" {uploadSymbol} Uploading [dim]{chunksUploaded:N0} unique chunks[/]{queuePart}"));
}
else
{
lines.Add(new Markup(" [grey] Uploading[/]"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat the uploading header as active before the first completion.

This branch stays grey until a chunk has already finished or queue depth is non-zero. The first in-flight upload with queue depth 0, and fully deduped/empty runs where only SnapshotComplete flips, still render as "not started."

💡 Suggested fix
-            if (chunksUploaded > 0 || tarsUploaded > 0 || queueDepth > 0)
+            var hasActiveUploads =
+                state.SnapshotComplete ||
+                chunksUploaded > 0 ||
+                tarsUploaded > 0 ||
+                queueDepth > 0 ||
+                state.TrackedFiles.Values.Any(f => f.State == FileState.Uploading) ||
+                state.TrackedTars.Values.Any(t => t.State == TarState.Uploading);
+
+            if (hasActiveUploads)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/CliBuilder.cs` around lines 893 - 910, The uploading header
should be considered active as soon as an upload is in-flight (even before any
chunk/tar count or queue depth appears) — update the condition that decides the
active vs grey branch to treat the header as active when SnapshotComplete is
false or when ChunksUploaded, TarsUploaded, or UploadQueueDepth indicate work;
specifically, change the if that currently checks (state.ChunksUploaded > 0 ||
state.TarsUploaded > 0 || queueDepth > 0) to also consider
state.SnapshotComplete (e.g., if (!state.SnapshotComplete ||
state.ChunksUploaded > 0 || state.TarsUploaded > 0 || queueDepth > 0)) and
ensure uploadSymbol still uses state.SnapshotComplete to show completed vs
in-progress.

Comment thread src/Arius.Cli/ProgressHandlers.cs Outdated
Comment thread src/Arius.Cli/ProgressHandlers.cs Outdated
- Show uploading header as active (yellow ○) when scan is complete but
  snapshot is not yet done, even before first chunk finishes uploading
- Replace TrackedTars.Count+1 bundle numbering with a monotonic
  Interlocked counter (NextBundleNumber) on ProgressState to prevent
  ID reuse under concurrent TarBundleStartedEvents
- Expose TarTargetSize on ProgressState (mirrored from ArchiveOptions)
  so TarBundleStartedHandler no longer hard-codes 64 MB
- Make SetFileUploading return bool to eliminate ContainsKey TOCTOU
  race in ChunkUploadingHandler; fall through to TAR lookup on false
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Arius.Cli/ProgressHandlers.cs`:
- Around line 88-98: The code can throw InvalidOperationException between Count
check and Keys.Max() due to concurrent modification; wrap the Max/TryGetValue
sequence in a try/catch for InvalidOperationException (or alternatively take an
atomic snapshot under the same lock if you have one) so the handler safely bails
out when the dictionary empties; specifically, around
state.TrackedTars.Keys.Max() and state.TrackedTars.TryGetValue(maxKey, out var
tar) catch InvalidOperationException and return/skip updating tar.AddEntry using
notification.CurrentTarSize and tar.AccumulatedBytes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5aea4a4d-2b0a-4b87-a7b0-ee95b267ceb9

📥 Commits

Reviewing files that changed from the base of the PR and between c7e787c and b4ba47c.

📒 Files selected for processing (3)
  • src/Arius.Cli/CliBuilder.cs
  • src/Arius.Cli/ProgressHandlers.cs
  • src/Arius.Cli/ProgressState.cs
✅ Files skipped from review due to trivial changes (1)
  • src/Arius.Cli/CliBuilder.cs

Comment on lines +88 to +98
// Update the current tar (last in the dictionary by bundle number)
if (state.TrackedTars.Count > 0)
{
var maxKey = state.TrackedTars.Keys.Max();
if (state.TrackedTars.TryGetValue(maxKey, out var tar))
{
// Derive file size from the current tar size delta
var addedBytes = notification.CurrentTarSize - (tar.AccumulatedBytes);
tar.AddEntry(addedBytes > 0 ? addedBytes : 0);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TOCTOU race: Keys.Max() can throw if dictionary empties between checks.

Between checking Count > 0 and calling Keys.Max(), concurrent removal could leave the collection empty, causing InvalidOperationException. Use DefaultIfEmpty or a single atomic lookup pattern.

🛡️ Proposed fix
         // Update the current tar (last in the dictionary by bundle number)
-        if (state.TrackedTars.Count > 0)
-        {
-            var maxKey = state.TrackedTars.Keys.Max();
-            if (state.TrackedTars.TryGetValue(maxKey, out var tar))
-            {
-                // Derive file size from the current tar size delta
-                var addedBytes = notification.CurrentTarSize - (tar.AccumulatedBytes);
-                tar.AddEntry(addedBytes > 0 ? addedBytes : 0);
-            }
-        }
+        var maxKey = state.TrackedTars.Keys.DefaultIfEmpty(-1).Max();
+        if (maxKey >= 0 && state.TrackedTars.TryGetValue(maxKey, out var tar))
+        {
+            // Derive file size from the current tar size delta
+            var addedBytes = notification.CurrentTarSize - tar.AccumulatedBytes;
+            tar.AddEntry(addedBytes > 0 ? addedBytes : 0);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Update the current tar (last in the dictionary by bundle number)
if (state.TrackedTars.Count > 0)
{
var maxKey = state.TrackedTars.Keys.Max();
if (state.TrackedTars.TryGetValue(maxKey, out var tar))
{
// Derive file size from the current tar size delta
var addedBytes = notification.CurrentTarSize - (tar.AccumulatedBytes);
tar.AddEntry(addedBytes > 0 ? addedBytes : 0);
}
}
// Update the current tar (last in the dictionary by bundle number)
var maxKey = state.TrackedTars.Keys.DefaultIfEmpty(-1).Max();
if (maxKey >= 0 && state.TrackedTars.TryGetValue(maxKey, out var tar))
{
// Derive file size from the current tar size delta
var addedBytes = notification.CurrentTarSize - tar.AccumulatedBytes;
tar.AddEntry(addedBytes > 0 ? addedBytes : 0);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/ProgressHandlers.cs` around lines 88 - 98, The code can throw
InvalidOperationException between Count check and Keys.Max() due to concurrent
modification; wrap the Max/TryGetValue sequence in a try/catch for
InvalidOperationException (or alternatively take an atomic snapshot under the
same lock if you have one) so the handler safely bails out when the dictionary
empties; specifically, around state.TrackedTars.Keys.Max() and
state.TrackedTars.TryGetValue(maxKey, out var tar) catch
InvalidOperationException and return/skip updating tar.AddEntry using
notification.CurrentTarSize and tar.AccumulatedBytes.

@woutervanranst woutervanranst merged commit 6b83842 into master Mar 28, 2026
8 checks passed
@woutervanranst woutervanranst deleted the branch-jaw branch March 28, 2026 11:02
@coderabbitai coderabbitai Bot mentioned this pull request May 6, 2026
4 tasks
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.

1 participant