fix: resolve all integration test failures against live PVE

- BaseUrl trailing slash and resource path cleanup: PveSession.BaseUrl now
  ends with '/', all cmdlet resource strings stripped of leading slashes and
  /api2/json/ prefixes so URLs compose correctly
- PveNodeStatus: rename Name -> Node to match JSON field and test expectations
- NewPveVmCmdlet: auto-allocate vmid via GET cluster/nextid when -VmId omitted
- UploadFileAsync (BZ 7389 workarounds):
  * Unquoted multipart boundary in Content-Type header
  * Quoted name= values in Content-Disposition (embedded double-quotes)
  * Content-Disposition set before Content-Type on file part — PVE's parser
    closes the connection if Content-Type appears first
- SendPveIsoCmdlet: run upload in Task.Run, track progress atomically with
  Interlocked, poll and call WriteProgress only from pipeline thread to avoid
  InvalidOperationException from PSCmdlet thread-affinity requirements
- Add debug/Capture-UploadDiff.ps1: mitmproxy capture script used to isolate
  the header-ordering bug by diffing raw multipart bytes from PS vs C# module

Integration test result: 11 passed, 0 failed, 4 skipped (expected — no
template/stopped VMs on bare nested PVE test node).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-18 12:57:21 -05:00
parent 94b0fcdad2
commit 8ea983c0fc
36 changed files with 384 additions and 181 deletions
@@ -15,7 +15,7 @@ public class PveNodeStatus
/// </summary>
[JsonPropertyName("node")]
[JsonProperty("node")]
public string Name { get; set; } = string.Empty;
public string Node { get; set; } = string.Empty;
/// <summary>
/// The current status of the node (e.g., "online" or "offline").
@@ -141,7 +141,7 @@ public class PveNodeStatus
var diskUsedGb = DiskUsed.HasValue ? $"{DiskUsed.Value / 1024 / 1024 / 1024} GB" : "N/A";
var diskTotalGb = DiskTotal.HasValue ? $"{DiskTotal.Value / 1024 / 1024 / 1024} GB" : "N/A";
var uptimeStr = Uptime.HasValue ? TimeSpan.FromSeconds(Uptime.Value).ToString(@"d\.hh\:mm\:ss") : "N/A";
return $"Node: {Name} | Status: {Status} | CPU: {cpuPct} | "
return $"Node: {Node} | Status: {Status} | CPU: {cpuPct} | "
+ $"Mem: {memUsedMb}/{memTotalMb} ({MemoryUsage:F1}%) | "
+ $"Disk: {diskUsedGb}/{diskTotalGb} | Uptime: {uptimeStr}";
}