From ae7c730489daef273dcdb52e6abb2dbbbbe6a9d5 Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Wed, 26 Aug 2026 14:40:17 -0700 Subject: [PATCH 1/9] wslc: match docker prune semantics - confirmation prompt, -f aliases --force Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 36 ++++++ .../wslc/arguments/ArgumentDefinitions.h | 2 + .../wslc/arguments/ArgumentValidation.cpp | 5 + src/windows/wslc/arguments/SpecParsing.cpp | 11 ++ src/windows/wslc/arguments/SpecParsing.h | 4 + .../wslc/commands/ContainerPruneCommand.cpp | 5 +- .../wslc/commands/ImagePruneCommand.cpp | 3 +- .../wslc/commands/NetworkPruneCommand.cpp | 3 +- .../wslc/commands/VolumePruneCommand.cpp | 3 +- src/windows/wslc/core/CLIExecutionContext.cpp | 10 ++ src/windows/wslc/core/CLIExecutionContext.h | 5 + src/windows/wslc/core/Terminal.cpp | 7 ++ src/windows/wslc/core/Terminal.h | 6 + .../wslc/services/ContainerService.cpp | 12 +- src/windows/wslc/services/ContainerService.h | 2 +- src/windows/wslc/tasks/ContainerTasks.cpp | 10 +- src/windows/wslc/tasks/ImageTasks.cpp | 7 +- src/windows/wslc/tasks/NetworkTasks.cpp | 7 +- src/windows/wslc/tasks/VolumeTasks.cpp | 7 +- test/windows/wslc/WSLCCLICommandUnitTests.cpp | 52 +++++++++ .../windows/wslc/WSLCCLITerminalUnitTests.cpp | 51 ++++++++ .../wslc/e2e/WSLCE2EContainerPruneTests.cpp | 110 ++++++++++++++++-- .../wslc/e2e/WSLCE2EImagePruneTests.cpp | 19 +-- .../wslc/e2e/WSLCE2ENetworkPruneTests.cpp | 20 ++-- .../wslc/e2e/WSLCE2EVolumePruneTests.cpp | 22 ++-- 25 files changed, 370 insertions(+), 49 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 7649618eb2..e011ac6252 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -3040,6 +3040,42 @@ On first run, creates the file with all settings commented out at their defaults Invalid value "{}" for the '-f, --filter' option: bad format of filter (expected name=value) {FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated + + Invalid value "{}" for the '--filter' option: bad format of filter (expected name=value) + {FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated + + + Provide filter values (e.g. "until=<timestamp>") + {Locked="until"}Command line arguments, file names and string inserts should not be translated + + + Provide filter values (e.g. "label=<label>") + {Locked="label"}Command line arguments, file names and string inserts should not be translated + + + Do not prompt for confirmation + + + Are you sure you want to continue? + + + WARNING! This will remove all stopped containers. + + + WARNING! This will remove all dangling images. + + + WARNING! This will remove all images without at least one container associated to them. + + + WARNING! This will remove anonymous local volumes not used by at least one container. + + + WARNING! This will remove all local volumes not used by at least one container. + + + WARNING! This will remove all custom networks not used by at least one container. + Invalid argument "{}" for '--mount' option: {} {FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--mount'"}Command line arguments, file names and string inserts should not be translated diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index 4c153722f5..3b9c7da8bb 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -111,6 +111,8 @@ _(Password, "password", L"p", Kind::Value, _(PasswordStdin, "password-stdin", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_LoginPasswordStdinArgDescription()) \ _(Path, "path", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_PathArgDescription()) \ _(Progress, "progress", NO_ALIAS, Kind::Value, ProgressMode, Localization::WSLCCLI_ProgressArgDescription()) \ +/* Prune commands take --filter without the 'f' alias, which they reserve for --force. */ \ +_(PruneFilter, "filter", NO_ALIAS, Kind::Value, KeyValuePair, Localization::WSLCCLI_PruneFilterArgDescription()) \ _(Publish, "publish", L"p", Kind::Value, NoConversion, Localization::WSLCCLI_PublishArgDescription()) \ _(PublishAll, "publish-all", L"P", Kind::Flag, NoConversion, Localization::WSLCCLI_PublishAllArgDescription()) \ _(Pull, "pull", NO_ALIAS, Kind::Value, PullPolicy, Localization::WSLCCLI_PullArgDescription()) \ diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index ca6580878b..d3c15ef794 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -212,6 +212,11 @@ void Argument::Validate(ArgMap& execArgs) const execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseFilter(value); }); break; + case ArgType::PruneFilter: + CacheConverted( + execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParsePruneFilter(value); }); + break; + case ArgType::Label: CacheConverted( execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseLabel(value); }); diff --git a/src/windows/wslc/arguments/SpecParsing.cpp b/src/windows/wslc/arguments/SpecParsing.cpp index 3f88cbacab..83fcdaa784 100644 --- a/src/windows/wslc/arguments/SpecParsing.cpp +++ b/src/windows/wslc/arguments/SpecParsing.cpp @@ -457,6 +457,17 @@ std::pair ParseFilter(const std::wstring& value) return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)}; } +std::pair ParsePruneFilter(const std::wstring& value) +{ + const auto kv = SplitKeyValue(value); + if (!kv.HadSeparator) + { + throw ArgumentException(Localization::WSLCCLI_InvalidPruneFilterError(value)); + } + + return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)}; +} + ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value, const std::wstring& argName) { ParsedNetworkArgument result; diff --git a/src/windows/wslc/arguments/SpecParsing.h b/src/windows/wslc/arguments/SpecParsing.h index 9570427662..2fde6307f1 100644 --- a/src/windows/wslc/arguments/SpecParsing.h +++ b/src/windows/wslc/arguments/SpecParsing.h @@ -74,6 +74,10 @@ std::pair ParseDriverOption(const std::wstring& value) // Parses a --filter spec ("key=value"); the separator is required. std::pair ParseFilter(const std::wstring& value); +// Parses a prune --filter spec ("key=value"); the separator is required. Reports the option as +// '--filter' only, since prune commands reserve the 'f' alias for --force. +std::pair ParsePruneFilter(const std::wstring& value); + struct ParsedNetworkArgument { std::string Name; diff --git a/src/windows/wslc/commands/ContainerPruneCommand.cpp b/src/windows/wslc/commands/ContainerPruneCommand.cpp index a68f77d47d..32ec80cbde 100644 --- a/src/windows/wslc/commands/ContainerPruneCommand.cpp +++ b/src/windows/wslc/commands/ContainerPruneCommand.cpp @@ -26,7 +26,10 @@ namespace wsl::windows::wslc { // Container Prune Command std::vector ContainerPruneCommand::GetArguments() const { - return {}; + return { + Argument::Create(ArgType::PruneFilter, false, Limit::Unlimited), + Argument::Create(ArgType::Force, std::nullopt, std::nullopt, Localization::WSLCCLI_PruneForceArgDescription()), + }; } std::wstring ContainerPruneCommand::ShortDescription() const diff --git a/src/windows/wslc/commands/ImagePruneCommand.cpp b/src/windows/wslc/commands/ImagePruneCommand.cpp index bd6e6a6c7e..63afc7514b 100644 --- a/src/windows/wslc/commands/ImagePruneCommand.cpp +++ b/src/windows/wslc/commands/ImagePruneCommand.cpp @@ -29,7 +29,8 @@ std::vector ImagePruneCommand::GetArguments() const { return { Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_ImagePruneAllArgDescription()), - Argument::Create(ArgType::Filter, false, Limit::Unlimited), + Argument::Create(ArgType::PruneFilter, false, Limit::Unlimited), + Argument::Create(ArgType::Force, std::nullopt, std::nullopt, Localization::WSLCCLI_PruneForceArgDescription()), }; } diff --git a/src/windows/wslc/commands/NetworkPruneCommand.cpp b/src/windows/wslc/commands/NetworkPruneCommand.cpp index 1b034f81df..a37bb21245 100644 --- a/src/windows/wslc/commands/NetworkPruneCommand.cpp +++ b/src/windows/wslc/commands/NetworkPruneCommand.cpp @@ -27,7 +27,8 @@ namespace wsl::windows::wslc { std::vector NetworkPruneCommand::GetArguments() const { return { - Argument::Create(ArgType::Filter, false, Limit::Unlimited), + Argument::Create(ArgType::PruneFilter, false, Limit::Unlimited), + Argument::Create(ArgType::Force, std::nullopt, std::nullopt, Localization::WSLCCLI_PruneForceArgDescription()), }; } diff --git a/src/windows/wslc/commands/VolumePruneCommand.cpp b/src/windows/wslc/commands/VolumePruneCommand.cpp index e5b33fe4d8..312e80918f 100644 --- a/src/windows/wslc/commands/VolumePruneCommand.cpp +++ b/src/windows/wslc/commands/VolumePruneCommand.cpp @@ -28,7 +28,8 @@ std::vector VolumePruneCommand::GetArguments() const { return { Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumePruneAllArgDescription()), - Argument::Create(ArgType::Filter, false, Limit::Unlimited), + Argument::Create(ArgType::PruneFilter, false, Limit::Unlimited, Localization::WSLCCLI_VolumePruneFilterArgDescription()), + Argument::Create(ArgType::Force, std::nullopt, std::nullopt, Localization::WSLCCLI_PruneForceArgDescription()), }; } diff --git a/src/windows/wslc/core/CLIExecutionContext.cpp b/src/windows/wslc/core/CLIExecutionContext.cpp index 0b804eb78a..ea45606fee 100644 --- a/src/windows/wslc/core/CLIExecutionContext.cpp +++ b/src/windows/wslc/core/CLIExecutionContext.cpp @@ -16,6 +16,16 @@ HANDLE CLIExecutionContext::CreateCancelEvent() return CancelEvent.get(); } +bool CLIExecutionContext::ConfirmPrune(std::wstring_view warning) +{ + if (Args.GetValue()) + { + return true; + } + + return Terminal.Confirm(std::format(L"{}\n{}", warning, wsl::shared::Localization::WSLCCLI_PruneConfirmPrompt())); +} + void CLIExecutionContext::ApplyGlobalEnvironmentOptions() { // NoColor is environment-only and resolved before any output. Freezing it keeps the terminal diff --git a/src/windows/wslc/core/CLIExecutionContext.h b/src/windows/wslc/core/CLIExecutionContext.h index 4a59537e0c..b647f5531e 100644 --- a/src/windows/wslc/core/CLIExecutionContext.h +++ b/src/windows/wslc/core/CLIExecutionContext.h @@ -50,6 +50,11 @@ struct CLIExecutionContext : public wsl::windows::common::ExecutionContext HANDLE CreateCancelEvent(); + // Returns true when a destructive prune may proceed, either because --force was passed or + // because the user accepted the confirmation prompt. End of input declines, so a prune with no + // interactive input aborts rather than blocking. + bool ConfirmPrune(std::wstring_view warning); + // Applies and freezes environment-only global options before command-line parsing reports errors. void ApplyGlobalEnvironmentOptions(); }; diff --git a/src/windows/wslc/core/Terminal.cpp b/src/windows/wslc/core/Terminal.cpp index ef76d0eabb..8ab7873c9e 100644 --- a/src/windows/wslc/core/Terminal.cpp +++ b/src/windows/wslc/core/Terminal.cpp @@ -80,4 +80,11 @@ std::wstring Terminal::PromptForLine(Level level, std::wstring_view label, bool return line.value_or(std::wstring{}); } +bool Terminal::Confirm(std::wstring_view message) +{ + const auto answer = PromptForLine(std::format(L"{} [y/N] ", message)); + + return wsl::shared::string::IsEqual(wsl::shared::string::TrimAscii(std::wstring_view{answer}), L"y", true); +} + } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/core/Terminal.h b/src/windows/wslc/core/Terminal.h index 8e1e742ba7..7a540c48a5 100644 --- a/src/windows/wslc/core/Terminal.h +++ b/src/windows/wslc/core/Terminal.h @@ -139,6 +139,12 @@ struct Terminal return PromptForLine(Level::Output, label, mask); } + // Prompts for confirmation by writing message followed by " [y/N] ", then reading one line. + // Returns true only when the answer is "y" (case-insensitive, surrounding ASCII whitespace + // ignored); every other answer, including end of input, declines. Declining at end of input is + // what lets a non-interactive prune abort instead of blocking. + bool Confirm(std::wstring_view message); + bool IsVTEnabled(Level level) const noexcept; bool IsColorEnabled(Level level) const noexcept; diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 1c598094d2..95b355fd83 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -692,10 +692,18 @@ wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Sess return wsl::shared::FromJson(output.get()); } -PruneContainersResult ContainerService::Prune(Session& session) +PruneContainersResult ContainerService::Prune(Session& session, const std::vector>& filters) { + std::vector filterEntries; + filterEntries.reserve(filters.size()); + for (const auto& [key, value] : filters) + { + filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()}); + } + PruneResult result; - THROW_IF_FAILED(session.Get()->PruneContainers(nullptr, 0, &result.result)); + THROW_IF_FAILED(session.Get()->PruneContainers( + filterEntries.empty() ? nullptr : filterEntries.data(), static_cast(filterEntries.size()), &result.result)); PruneContainersResult pruneResult; pruneResult.SpaceReclaimed = result.result.SpaceReclaimed; diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h index 4e0e5bbbb0..b164945466 100644 --- a/src/windows/wslc/services/ContainerService.h +++ b/src/windows/wslc/services/ContainerService.h @@ -42,6 +42,6 @@ struct ContainerService static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id); static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, LONGLONG since, LONGLONG until, ULONGLONG tail = 0); static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id); - static models::PruneContainersResult Prune(models::Session& session); + static models::PruneContainersResult Prune(models::Session& session, const std::vector>& filters); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 3e057b5a03..001ba30db1 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -1071,7 +1071,15 @@ void PruneContainers(CLIExecutionContext& context) WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); - auto result = ContainerService::Prune(session); + if (!context.ConfirmPrune(Localization::WSLCCLI_ContainerPruneConfirm())) + { + return; + } + + // Filter values are parsed and cached during argument validation. + auto filters = context.Args.GetAllValues(); + + auto result = ContainerService::Prune(session, filters); if (!result.PrunedContainers.empty()) { diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index 6bb80092fc..25a1de0448 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -430,8 +430,13 @@ void PruneImages(CLIExecutionContext& context) bool all = context.Args.GetValue(); + if (!context.ConfirmPrune(all ? Localization::WSLCCLI_ImagePruneAllConfirm() : Localization::WSLCCLI_ImagePruneConfirm())) + { + return; + } + // Filter values are parsed and cached during argument validation. - auto filters = context.Args.GetAllValues(); + auto filters = context.Args.GetAllValues(); auto result = ImageService::Prune(session, all, filters); diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp index c59357409f..40c81c37eb 100644 --- a/src/windows/wslc/tasks/NetworkTasks.cpp +++ b/src/windows/wslc/tasks/NetworkTasks.cpp @@ -265,8 +265,13 @@ void PruneNetworks(CLIExecutionContext& context) WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); + if (!context.ConfirmPrune(Localization::WSLCCLI_NetworkPruneConfirm())) + { + return; + } + // Filter values are parsed and cached during argument validation. - auto filters = context.Args.GetAllValues(); + auto filters = context.Args.GetAllValues(); auto result = NetworkService::Prune(session, filters); diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp index fc548def69..8c17e0b2fd 100644 --- a/src/windows/wslc/tasks/VolumeTasks.cpp +++ b/src/windows/wslc/tasks/VolumeTasks.cpp @@ -243,8 +243,13 @@ void PruneVolumes(CLIExecutionContext& context) const bool all = context.Args.GetValue(); + if (!context.ConfirmPrune(all ? Localization::WSLCCLI_VolumePruneAllConfirm() : Localization::WSLCCLI_VolumePruneConfirm())) + { + return; + } + // Filter values are parsed and cached during argument validation. - auto filters = context.Args.GetAllValues(); + auto filters = context.Args.GetAllValues(); auto result = VolumeService::Prune(context.Terminal, session, all, filters); diff --git a/test/windows/wslc/WSLCCLICommandUnitTests.cpp b/test/windows/wslc/WSLCCLICommandUnitTests.cpp index 59b8c1a3d4..b7a2633632 100644 --- a/test/windows/wslc/WSLCCLICommandUnitTests.cpp +++ b/test/windows/wslc/WSLCCLICommandUnitTests.cpp @@ -21,9 +21,12 @@ Module Name: #include "Command.h" #include "RootCommand.h" #include "ContainerCommand.h" +#include "ImageCommand.h" +#include "NetworkCommand.h" #include "SessionCommand.h" #include "SystemCommand.h" #include "VersionCommand.h" +#include "VolumeCommand.h" #include "EnvironmentOptions.h" using namespace wsl::windows::wslc; @@ -100,6 +103,55 @@ class WSLCCLICommandUnitTests } } + // Test: Every prune command binds -f to --force and leaves --filter unaliased. Aliases resolve + // by first match with no collision detection, so an aliased --filter here would shadow -f. + TEST_METHOD(PruneCommands_BindShortFToForce) + { + const auto verifyPruneArguments = [](const std::vector& args, const std::wstring& command) { + LogComment(L"Verifying prune argument aliases for: " + command); + + const auto find = [&args](ArgType type) -> const Argument* { + const auto itr = std::find_if(args.begin(), args.end(), [type](const auto& arg) { return arg.Type() == type; }); + return itr == args.end() ? nullptr : &*itr; + }; + + const auto* force = find(ArgType::Force); + VERIFY_IS_NOT_NULL(force); + VERIFY_ARE_EQUAL(std::wstring{L"force"}, force->Name()); + VERIFY_ARE_EQUAL(std::wstring{L"f"}, force->Alias()); + + const auto* filter = find(ArgType::PruneFilter); + VERIFY_IS_NOT_NULL(filter); + VERIFY_ARE_EQUAL(std::wstring{L"filter"}, filter->Name()); + VERIFY_ARE_EQUAL(std::wstring{L""}, filter->Alias()); + + // The aliased list-command filter must never be registered alongside --force. + VERIFY_IS_NULL(find(ArgType::Filter)); + }; + + verifyPruneArguments(ContainerPruneCommand(L"container").GetArguments(), L"container prune"); + verifyPruneArguments(ImagePruneCommand(L"image").GetArguments(), L"image prune"); + verifyPruneArguments(VolumePruneCommand(L"volume").GetArguments(), L"volume prune"); + verifyPruneArguments(NetworkPruneCommand(L"network").GetArguments(), L"network prune"); + } + + // Test: List commands keep -f bound to --filter, which is why only prune commands were realigned. + TEST_METHOD(ListCommands_KeepShortFOnFilter) + { + const auto verifyFilterAlias = [](const std::vector& args, const std::wstring& command) { + LogComment(L"Verifying filter alias for: " + command); + + const auto itr = std::find_if(args.begin(), args.end(), [](const auto& arg) { return arg.Type() == ArgType::Filter; }); + VERIFY_IS_TRUE(itr != args.end()); + VERIFY_ARE_EQUAL(std::wstring{L"f"}, itr->Alias()); + }; + + verifyFilterAlias(ContainerListCommand(L"container").GetArguments(), L"container list"); + verifyFilterAlias(ImageListCommand(L"image").GetArguments(), L"image list"); + verifyFilterAlias(VolumeListCommand(L"volume").GetArguments(), L"volume list"); + verifyFilterAlias(NetworkListCommand(L"network").GetArguments(), L"network list"); + } + // Test: Verify SessionEnterCommand has the expected arguments TEST_METHOD(SessionEnterCommand_HasExpectedArguments) { diff --git a/test/windows/wslc/WSLCCLITerminalUnitTests.cpp b/test/windows/wslc/WSLCCLITerminalUnitTests.cpp index 555dbf41e9..b6ef6d0f3b 100644 --- a/test/windows/wslc/WSLCCLITerminalUnitTests.cpp +++ b/test/windows/wslc/WSLCCLITerminalUnitTests.cpp @@ -552,6 +552,57 @@ class WSLCCLITerminalUnitTests VERIFY_ARE_EQUAL(expected, result); } + TEST_METHOD(Terminal_ConfirmAcceptsLowercaseY) + { + InputCaptureTerminal cap{L"y\n"}; + VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?")); + + // The prompt is written inline on stdout with the standard suffix appended. + VERIFY_ARE_EQUAL(std::wstring{L"Remove everything? [y/N] "}, cap.outPipe.captured()); + VERIFY_ARE_EQUAL(std::wstring{L""}, cap.errPipe.captured()); + } + + TEST_METHOD(Terminal_ConfirmAcceptsUppercaseY) + { + InputCaptureTerminal cap{L"Y\n"}; + VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?")); + } + + TEST_METHOD(Terminal_ConfirmTrimsSurroundingWhitespace) + { + InputCaptureTerminal cap{L" y \n"}; + VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?")); + } + + TEST_METHOD(Terminal_ConfirmRejectsAnswersOtherThanY) + { + // Only a bare y accepts; a spelled-out yes declines, matching the container CLI ecosystem. + for (const auto* answer : {L"yes\n", L"n\n", L"N\n", L"no\n", L"\n", L"maybe\n"}) + { + InputCaptureTerminal cap{answer}; + VERIFY_IS_FALSE(cap.terminal.Confirm(L"Remove everything?")); + } + } + + TEST_METHOD(Terminal_ConfirmDeclinesAtEndOfInput) + { + // A prune with no input attached must abort rather than block, so end of input declines. + InputCaptureTerminal cap{L""}; + VERIFY_IS_FALSE(cap.terminal.Confirm(L"Remove everything?")); + VERIFY_ARE_EQUAL(std::wstring{L"Remove everything? [y/N] "}, cap.outPipe.captured()); + } + + TEST_METHOD(Terminal_ConfirmEmitsMessageVerbatimWithFormatCharacters) + { + // The message is a formatting argument, not a format string, so braces must not be + // interpreted. + InputCaptureTerminal cap{L"y\n"}; + const std::wstring message = L"Remove {} {0} {name} 100%?"; + + VERIFY_IS_TRUE(cap.terminal.Confirm(message)); + VERIFY_ARE_EQUAL(message + L" [y/N] ", cap.outPipe.captured()); + } + TEST_METHOD(Terminal_ReadLineMaskDefaultsToUnmasked) { // ReadLine(bool mask = false): the default reads without masking and returns diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp index d07d2481df..f6b822c1fc 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp @@ -33,13 +33,14 @@ class WSLCE2EContainerPruneTests EnsureContainerDoesNotExist(L"prune-running-test"); EnsureContainerDoesNotExist(L"prune-multi-1"); EnsureContainerDoesNotExist(L"prune-multi-2"); + EnsureContainerDoesNotExist(L"prune-confirm-test"); return true; } TEST_CLASS_CLEANUP(ClassCleanup) { // Clean up any leftover containers - RunWslc(L"container prune"); + RunWslc(L"container prune --force"); return true; } @@ -53,9 +54,9 @@ class WSLCE2EContainerPruneTests WSLC_TEST_METHOD(WSLCE2E_Container_Prune_NoStoppedContainers) { // Establish a clean baseline first so this does not depend on what other tests left behind. - RunWslc(L"container prune").Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(L"container prune --force").Verify({.Stderr = L"", .ExitCode = 0}); - const auto result = RunWslc(L"container prune"); + const auto result = RunWslc(L"container prune --force"); // With nothing pruned docker emits only the reclaimed-space line, with no header or leading blank line. result.Verify({.Stdout = L"Total reclaimed space: 0B\r\n", .Stderr = L"", .ExitCode = 0}); @@ -68,10 +69,10 @@ class WSLCE2EContainerPruneTests createResult.Verify({.Stderr = L"", .ExitCode = 0}); auto containerId = createResult.GetStdoutOneLine(); - auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); }); + auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune --force"); }); // The created container is in stopped state, so prune should remove it - const auto result = RunWslc(L"container prune"); + const auto result = RunWslc(L"container prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); // Verify pruned container ID is in output @@ -94,7 +95,7 @@ class WSLCE2EContainerPruneTests }); // Prune should not remove a running container - const auto pruneResult = RunWslc(L"container prune"); + const auto pruneResult = RunWslc(L"container prune --force"); pruneResult.Verify({.Stderr = L"", .ExitCode = 0}); // Verify the running container is still present @@ -112,9 +113,9 @@ class WSLCE2EContainerPruneTests create2.Verify({.Stderr = L"", .ExitCode = 0}); auto containerId2 = create2.GetStdoutOneLine(); - auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); }); + auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune --force"); }); - const auto result = RunWslc(L"container prune"); + const auto result = RunWslc(L"container prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); // Verify pruned container IDs are in output @@ -126,7 +127,100 @@ class WSLCE2EContainerPruneTests VerifyContainerIsNotListed(L"prune-multi-2"); } + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_WithoutForceDeclinesWithoutInput) + { + // Without --force the prune asks for confirmation. No input is attached here, so the read hits + // end of input and declines: the container survives and the command still succeeds. + RunWslc(std::format(L"container create --name {} {}", ConfirmContainerName, DebianImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0}); + + auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); }); + + const auto result = RunWslc(L"container prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // The warning and the question are both written to stdout. + VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneConfirm())); + VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_PruneConfirmPrompt())); + + // Declining prunes nothing, so no deleted header is emitted and the container is still listed. + VERIFY_IS_FALSE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneDeletedHeader())); + VerifyContainerIsListed(ConfirmContainerName, L"created"); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_AcceptingPromptRemovesContainer) + { + // Answering the prompt with y proceeds with the prune. + auto createResult = RunWslc(std::format(L"container create --name {} {}", ConfirmContainerName, DebianImage.NameAndTag())); + createResult.Verify({.Stderr = L"", .ExitCode = 0}); + const auto containerId = createResult.GetStdoutOneLine(); + + auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); }); + + const auto answerPath = std::filesystem::current_path() / L"wslc-prune-confirm.txt"; + { + std::ofstream answer(answerPath, std::ios::binary | std::ios::trunc); + answer << "y\n"; + } + + auto removeAnswer = wil::scope_exit([&]() { DeleteFileW(answerPath.c_str()); }); + + const auto result = RunWslcWithStdinFile(L"container prune", answerPath); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneConfirm())); + VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId)); + VerifyContainerIsNotListed(ConfirmContainerName); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_ShortForceFlagSkipsPrompt) + { + // -f resolves to --force on prune commands, so no warning is printed and the prune proceeds. + auto createResult = RunWslc(std::format(L"container create --name {} {}", ConfirmContainerName, DebianImage.NameAndTag())); + createResult.Verify({.Stderr = L"", .ExitCode = 0}); + const auto containerId = createResult.GetStdoutOneLine(); + + auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); }); + + const auto result = RunWslc(L"container prune -f"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VERIFY_IS_FALSE(result.StdoutContainsSubstring(Localization::WSLCCLI_PruneConfirmPrompt())); + VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId)); + VerifyContainerIsNotListed(ConfirmContainerName); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_FilterExcludesNonMatchingContainer) + { + // container prune gained --filter here, so a label filter that matches nothing must leave the + // stopped container in place, and an unfiltered prune must still remove it. + auto createResult = RunWslc(std::format(L"container create --name {} {}", ConfirmContainerName, DebianImage.NameAndTag())); + createResult.Verify({.Stderr = L"", .ExitCode = 0}); + const auto containerId = createResult.GetStdoutOneLine(); + + auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); }); + + const auto filtered = RunWslc(L"container prune --force --filter label=wslc.test.never=present"); + filtered.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_FALSE(filtered.StdoutContainsSubstring(containerId)); + VerifyContainerIsListed(ConfirmContainerName, L"created"); + + const auto unfiltered = RunWslc(L"container prune --force"); + unfiltered.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_TRUE(unfiltered.StdoutContainsSubstring(containerId)); + VerifyContainerIsNotListed(ConfirmContainerName); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_MalformedFilterIsRejectedBeforePrompting) + { + // Argument validation runs before the confirmation prompt, so a bad filter fails with empty + // stdout rather than printing the warning first. + const auto result = RunWslc(L"container prune --filter label"); + result.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidPruneFilterError(L"label"))); + } + private: + const std::wstring ConfirmContainerName = L"prune-confirm-test"; const TestImage& DebianImage = DebianTestImage(); }; } // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp index 024c62cb61..b6e93cb0bf 100644 --- a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp @@ -46,7 +46,7 @@ class WSLCE2EImagePruneTests WSLC_TEST_METHOD(WSLCE2E_Image_Prune_NoDanglingImages) { // Prune when no dangling images exist should succeed with zero reclaimed space - const auto result = RunWslc(L"image prune"); + const auto result = RunWslc(L"image prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); VerifyStdoutContains(result, L"Total reclaimed space:"); @@ -60,7 +60,7 @@ class WSLCE2EImagePruneTests // 3. Tag alpine as prune-target:v1, overwriting it — debian image is now dangling TestImageRegistry::Instance().EnsureLoaded(AlpineImage); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - RunWslc(L"image prune"); + RunWslc(L"image prune --force"); RunWslc(L"image delete prune-target:v1"); TestImageRegistry::Instance().Delete(AlpineImage); TestImageRegistry::Instance().Restore(DebianImage); @@ -71,7 +71,7 @@ class WSLCE2EImagePruneTests RunWslc(std::format(L"image tag {} prune-target:v1", AlpineImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0}); // Now prune should remove the dangling (original debian) image - const auto result = RunWslc(L"image prune"); + const auto result = RunWslc(L"image prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); bool foundDeleted = false; @@ -99,7 +99,7 @@ class WSLCE2EImagePruneTests }); // --all should prune unused images (not just dangling) - const auto result = RunWslc(L"image prune --all"); + const auto result = RunWslc(L"image prune --force --all"); result.Verify({.Stderr = L"", .ExitCode = 0}); VerifyStdoutContains(result, L"Total reclaimed space:"); @@ -120,13 +120,13 @@ class WSLCE2EImagePruneTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"image prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidPruneFilterError(L"label"))); } WSLC_TEST_METHOD(WSLCE2E_Image_Prune_Filter_InvalidKey) { // Filter keys are validated by the Docker daemon, which rejects unknown keys. - const auto result = RunWslc(L"image prune --filter color=blue"); + const auto result = RunWslc(L"image prune --force --filter color=blue"); VERIFY_ARE_EQUAL(1, result.ExitCode); VERIFY_IS_TRUE(result.Stderr.has_value()); VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter")); @@ -137,7 +137,7 @@ class WSLCE2EImagePruneTests // Create a dangling debian image (same trick as WSLCE2E_Image_Prune_DanglingImage). TestImageRegistry::Instance().EnsureLoaded(AlpineImage); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - RunWslc(L"image prune"); + RunWslc(L"image prune --force"); RunWslc(L"image delete prune-target:v1"); TestImageRegistry::Instance().Delete(AlpineImage); TestImageRegistry::Instance().Restore(DebianImage); @@ -150,7 +150,8 @@ class WSLCE2EImagePruneTests // Prune only dangling images carrying a label the dangling image does NOT have. // Multiple --filter label= values are AND'd by the daemon; the dangling image // matches neither, so it must survive this prune. - auto filteredPrune = RunWslc(L"image prune --filter label=wslc.test.never=present --filter label=wslc.test.also=missing"); + auto filteredPrune = + RunWslc(L"image prune --force --filter label=wslc.test.never=present --filter label=wslc.test.also=missing"); filteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); for (const auto& line : filteredPrune.GetStdoutLines()) { @@ -163,7 +164,7 @@ class WSLCE2EImagePruneTests // A subsequent unfiltered prune should still find and remove the dangling image, // proving the filter — not the absence of dangling images — was the reason nothing // was pruned above. - auto unfilteredPrune = RunWslc(L"image prune"); + auto unfilteredPrune = RunWslc(L"image prune --force"); unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); bool foundDeleted = false; for (const auto& line : unfilteredPrune.GetStdoutLines()) diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp index 966e92adf3..e2459aa209 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp @@ -53,7 +53,7 @@ class WSLCE2ENetworkPruneTests WSLC_TEST_METHOD(WSLCE2E_Network_Prune_NoNetworks) { // Prune when no unused networks exist should succeed without reporting any of our test networks. - const auto result = RunWslc(L"network prune"); + const auto result = RunWslc(L"network prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE(result.StdoutContainsLine(TestNetworkName)); @@ -67,7 +67,7 @@ class WSLCE2ENetworkPruneTests auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(TestNetworkName); }); - const auto result = RunWslc(L"network prune"); + const auto result = RunWslc(L"network prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); auto output = result.GetStdoutLines(); @@ -91,7 +91,7 @@ class WSLCE2ENetworkPruneTests EnsureNetworkDoesNotExist(TestNetworkName2); }); - const auto result = RunWslc(L"network prune"); + const auto result = RunWslc(L"network prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName)); @@ -116,7 +116,7 @@ class WSLCE2ENetworkPruneTests EnsureNetworkDoesNotExist(TestNetworkName); }); - const auto result = RunWslc(L"network prune"); + const auto result = RunWslc(L"network prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE(result.StdoutContainsLine(TestNetworkName), L"Network in use by a running container must not be pruned"); @@ -132,7 +132,7 @@ class WSLCE2ENetworkPruneTests auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(TestNetworkName); }); // A label filter that does not match the network should preserve it. - const auto filteredPrune = RunWslc(L"network prune --filter label=wslc.test.never=present"); + const auto filteredPrune = RunWslc(L"network prune --force --filter label=wslc.test.never=present"); filteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE( filteredPrune.StdoutContainsLine(TestNetworkName), L"Filtered prune should not have deleted the non-matching network"); @@ -140,7 +140,7 @@ class WSLCE2ENetworkPruneTests // A subsequent unfiltered prune should still remove it, proving the filter // was the reason it survived. - const auto unfilteredPrune = RunWslc(L"network prune"); + const auto unfilteredPrune = RunWslc(L"network prune --force"); unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(TestNetworkName)); VerifyNetworkIsNotListed(TestNetworkName); @@ -158,7 +158,7 @@ class WSLCE2ENetworkPruneTests EnsureNetworkDoesNotExist(TestNetworkName2); }); - const auto result = RunWslc(L"network prune --filter label=wslc.test.prune=keep"); + const auto result = RunWslc(L"network prune --force --filter label=wslc.test.prune=keep"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName)); @@ -180,7 +180,7 @@ class WSLCE2ENetworkPruneTests EnsureNetworkDoesNotExist(TestNetworkName2); }); - const auto result = RunWslc(L"network prune --filter label!=wslc.test.keep"); + const auto result = RunWslc(L"network prune --force --filter label!=wslc.test.keep"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(result.StdoutContainsLine(TestNetworkName2)); @@ -195,12 +195,12 @@ class WSLCE2ENetworkPruneTests { const auto result = RunWslc(L"network prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidPruneFilterError(L"label"))); } WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_InvalidKey) { - const auto result = RunWslc(L"network prune --filter color=red"); + const auto result = RunWslc(L"network prune --force --filter color=red"); result.Verify({.Stdout = L"", .ExitCode = 1}); VERIFY_IS_TRUE(result.StderrContainsSubstring(L"invalid filter 'color'\r\nError code: E_INVALIDARG")); } diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp index 7dc2bee337..ea816d08f3 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -53,7 +53,7 @@ class WSLCE2EVolumePruneTests WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_NoVolumes) { // Prune when no volumes exist should succeed and report a reclaimed-space line. - const auto result = RunWslc(L"volume prune"); + const auto result = RunWslc(L"volume prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); // The deleted-volume block, and the blank line that follows it, are only written when @@ -68,7 +68,7 @@ class WSLCE2EVolumePruneTests auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); }); - const auto result = RunWslc(L"volume prune"); + const auto result = RunWslc(L"volume prune --force"); result.Verify({.Stderr = L"", .ExitCode = 0}); auto output = result.GetStdoutLines(); @@ -85,7 +85,7 @@ class WSLCE2EVolumePruneTests auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); }); - const auto result = RunWslc(L"volume prune --all"); + const auto result = RunWslc(L"volume prune --force --all"); result.Verify({.Stderr = L"", .ExitCode = 0}); auto output = result.GetStdoutLines(); @@ -110,7 +110,7 @@ class WSLCE2EVolumePruneTests EnsureVolumeDoesNotExist(TestVolumeName2); }); - const auto result = RunWslc(L"volume prune --all"); + const auto result = RunWslc(L"volume prune --force --all"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(result.StdoutContainsLine(L"Deleted Volumes:")); @@ -136,7 +136,7 @@ class WSLCE2EVolumePruneTests EnsureVolumeDoesNotExist(TestVolumeName); }); - const auto result = RunWslc(L"volume prune --all"); + const auto result = RunWslc(L"volume prune --force --all"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE(result.StdoutContainsLine(TestVolumeName), L"Volume in use by a running container must not be pruned"); @@ -152,7 +152,7 @@ class WSLCE2EVolumePruneTests auto cleanup = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(TestVolumeName); }); // A label filter that does not match the volume should preserve it - const auto filteredPrune = RunWslc(L"volume prune --all --filter label=wslc.test.never=present"); + const auto filteredPrune = RunWslc(L"volume prune --force --all --filter label=wslc.test.never=present"); filteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_FALSE( filteredPrune.StdoutContainsLine(TestVolumeName), L"Filtered prune should not have deleted the non-matching volume"); @@ -160,7 +160,7 @@ class WSLCE2EVolumePruneTests // Subsequent unfiltered prune --all should still remove it, proving // the filter was the reason it survived. - const auto unfilteredPrune = RunWslc(L"volume prune --all"); + const auto unfilteredPrune = RunWslc(L"volume prune --force --all"); unfilteredPrune.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(unfilteredPrune.StdoutContainsLine(TestVolumeName)); VerifyVolumeIsNotListed(TestVolumeName); @@ -178,7 +178,7 @@ class WSLCE2EVolumePruneTests EnsureVolumeDoesNotExist(TestVolumeName2); }); - const auto result = RunWslc(L"volume prune --all --filter label=wslc.test.prune=keep"); + const auto result = RunWslc(L"volume prune --force --all --filter label=wslc.test.prune=keep"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(result.StdoutContainsLine(TestVolumeName)); @@ -200,7 +200,7 @@ class WSLCE2EVolumePruneTests EnsureVolumeDoesNotExist(TestVolumeName2); }); - const auto result = RunWslc(L"volume prune --all --filter label!=wslc.test.keep"); + const auto result = RunWslc(L"volume prune --force --all --filter label!=wslc.test.keep"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(result.StdoutContainsLine(TestVolumeName2)); @@ -215,12 +215,12 @@ class WSLCE2EVolumePruneTests { const auto result = RunWslc(L"volume prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidPruneFilterError(L"label"))); } WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_InvalidKey) { - const auto result = RunWslc(L"volume prune --filter color=red"); + const auto result = RunWslc(L"volume prune --force --filter color=red"); result.Verify({.Stdout = L"", .ExitCode = 1}); VERIFY_IS_TRUE(result.StderrContainsSubstring(L"invalid filter 'color'\r\nError code: E_INVALIDARG")); } From 9a43f09752a5bc81c6f047c1168c6e1227566148 Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Wed, 26 Aug 2026 14:52:46 -0700 Subject: [PATCH 2/9] wslc: drop redundant comments on prune filter argument Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslc/arguments/ArgumentDefinitions.h | 1 - src/windows/wslc/arguments/SpecParsing.h | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index 3b9c7da8bb..6ed3a6020c 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -111,7 +111,6 @@ _(Password, "password", L"p", Kind::Value, _(PasswordStdin, "password-stdin", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_LoginPasswordStdinArgDescription()) \ _(Path, "path", NO_ALIAS, Kind::Positional, NoConversion, Localization::WSLCCLI_PathArgDescription()) \ _(Progress, "progress", NO_ALIAS, Kind::Value, ProgressMode, Localization::WSLCCLI_ProgressArgDescription()) \ -/* Prune commands take --filter without the 'f' alias, which they reserve for --force. */ \ _(PruneFilter, "filter", NO_ALIAS, Kind::Value, KeyValuePair, Localization::WSLCCLI_PruneFilterArgDescription()) \ _(Publish, "publish", L"p", Kind::Value, NoConversion, Localization::WSLCCLI_PublishArgDescription()) \ _(PublishAll, "publish-all", L"P", Kind::Flag, NoConversion, Localization::WSLCCLI_PublishAllArgDescription()) \ diff --git a/src/windows/wslc/arguments/SpecParsing.h b/src/windows/wslc/arguments/SpecParsing.h index 2fde6307f1..89f8beff5d 100644 --- a/src/windows/wslc/arguments/SpecParsing.h +++ b/src/windows/wslc/arguments/SpecParsing.h @@ -74,8 +74,7 @@ std::pair ParseDriverOption(const std::wstring& value) // Parses a --filter spec ("key=value"); the separator is required. std::pair ParseFilter(const std::wstring& value); -// Parses a prune --filter spec ("key=value"); the separator is required. Reports the option as -// '--filter' only, since prune commands reserve the 'f' alias for --force. +// Parses a prune --filter spec ("key=value"); the separator is required. std::pair ParsePruneFilter(const std::wstring& value); struct ParsedNetworkArgument From aab8d8b3ba9dbef9ee68a443c06ee93fbb01eb52 Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Wed, 26 Aug 2026 15:54:08 -0700 Subject: [PATCH 3/9] Emit the prune warning on stderr instead of embedding it in the stdout prompt Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslc/core/CLIExecutionContext.cpp | 4 +++- test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/windows/wslc/core/CLIExecutionContext.cpp b/src/windows/wslc/core/CLIExecutionContext.cpp index ea45606fee..75538bf636 100644 --- a/src/windows/wslc/core/CLIExecutionContext.cpp +++ b/src/windows/wslc/core/CLIExecutionContext.cpp @@ -23,7 +23,9 @@ bool CLIExecutionContext::ConfirmPrune(std::wstring_view warning) return true; } - return Terminal.Confirm(std::format(L"{}\n{}", warning, wsl::shared::Localization::WSLCCLI_PruneConfirmPrompt())); + Terminal.Warn(L"{}\n", warning); + + return Terminal.Confirm(wsl::shared::Localization::WSLCCLI_PruneConfirmPrompt()); } void CLIExecutionContext::ApplyGlobalEnvironmentOptions() diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp index f6b822c1fc..0dc4537986 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp @@ -136,10 +136,10 @@ class WSLCE2EContainerPruneTests auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); }); const auto result = RunWslc(L"container prune"); - result.Verify({.Stderr = L"", .ExitCode = 0}); + result.Verify({.ExitCode = 0}); - // The warning and the question are both written to stdout. - VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneConfirm())); + // The warning goes to stderr; the question goes to stdout. + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_ContainerPruneConfirm())); VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_PruneConfirmPrompt())); // Declining prunes nothing, so no deleted header is emitted and the container is still listed. @@ -165,9 +165,9 @@ class WSLCE2EContainerPruneTests auto removeAnswer = wil::scope_exit([&]() { DeleteFileW(answerPath.c_str()); }); const auto result = RunWslcWithStdinFile(L"container prune", answerPath); - result.Verify({.Stderr = L"", .ExitCode = 0}); + result.Verify({.ExitCode = 0}); - VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneConfirm())); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_ContainerPruneConfirm())); VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId)); VerifyContainerIsNotListed(ConfirmContainerName); } From 662944e234a987f82a5a06023dbebfb77e89cb3e Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Thu, 27 Aug 2026 13:25:15 -0700 Subject: [PATCH 4/9] wslc: move prune confirmation into a shared ConfirmAction task Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 8 +--- .../wslc/arguments/ArgumentValidation.cpp | 16 +------ .../wslc/arguments/ArgumentValidation.h | 2 - src/windows/wslc/arguments/SpecParsing.cpp | 15 +----- src/windows/wslc/arguments/SpecParsing.h | 5 +- .../wslc/commands/ContainerPruneCommand.cpp | 5 ++ .../wslc/commands/ImagePruneCommand.cpp | 6 +++ .../wslc/commands/NetworkPruneCommand.cpp | 5 ++ .../wslc/commands/VolumePruneCommand.cpp | 6 +++ src/windows/wslc/core/CLIExecutionContext.cpp | 12 ----- src/windows/wslc/core/CLIExecutionContext.h | 5 -- src/windows/wslc/core/Exceptions.h | 9 ++++ src/windows/wslc/core/ExecutionContextData.h | 4 ++ src/windows/wslc/core/Main.cpp | 5 ++ src/windows/wslc/tasks/CommonTasks.cpp | 47 +++++++++++++++++++ src/windows/wslc/tasks/CommonTasks.h | 21 +++++++++ src/windows/wslc/tasks/ContainerTasks.cpp | 5 -- src/windows/wslc/tasks/ImageTasks.cpp | 5 -- src/windows/wslc/tasks/NetworkTasks.cpp | 5 -- src/windows/wslc/tasks/VolumeTasks.cpp | 5 -- .../wslc/e2e/WSLCE2EContainerListTests.cpp | 2 +- .../wslc/e2e/WSLCE2EContainerPruneTests.cpp | 44 +---------------- .../wslc/e2e/WSLCE2EImageListTests.cpp | 2 +- .../wslc/e2e/WSLCE2EImagePruneTests.cpp | 2 +- .../wslc/e2e/WSLCE2ENetworkListTests.cpp | 2 +- .../wslc/e2e/WSLCE2ENetworkPruneTests.cpp | 2 +- .../wslc/e2e/WSLCE2EVolumeListTests.cpp | 2 +- .../wslc/e2e/WSLCE2EVolumePruneTests.cpp | 2 +- 28 files changed, 123 insertions(+), 126 deletions(-) create mode 100644 src/windows/wslc/tasks/CommonTasks.cpp create mode 100644 src/windows/wslc/tasks/CommonTasks.h diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index e011ac6252..007a00a03a 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -3038,12 +3038,8 @@ On first run, creates the file with all settings commented out at their defaults Filter output based on conditions provided - Invalid value "{}" for the '-f, --filter' option: bad format of filter (expected name=value) - {FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated - - Invalid value "{}" for the '--filter' option: bad format of filter (expected name=value) - {FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated - + Invalid value "{}" for the '--{}' option: bad format of filter (expected name=value) + {FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--"}Command line arguments, file names and string inserts should not be translated Provide filter values (e.g. "until=<timestamp>") {Locked="until"}Command line arguments, file names and string inserts should not be translated diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index d3c15ef794..6c6290d282 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -208,13 +208,11 @@ void Argument::Validate(ArgMap& execArgs) const break; case ArgType::Filter: - CacheConverted( - execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseFilter(value); }); + CacheConverted(execArgs, m_name, validation::ParseFilter); break; case ArgType::PruneFilter: - CacheConverted( - execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParsePruneFilter(value); }); + CacheConverted(execArgs, m_name, validation::ParseFilter); break; case ArgType::Label: @@ -357,16 +355,6 @@ void ValidateWSLCSignalFromString(const std::vector& values, const } } -// Validates that each --filter argument is in the form "key=value". Rejects entries without an '='; -// the runtime validates the key and value for specific objects. -void ValidateFilter(const std::vector& values) -{ - for (const auto& value : values) - { - std::ignore = ParseFilter(value); - } -} - void ValidateTimestamp(const std::vector& values, const std::wstring& argName) { for (const auto& value : values) diff --git a/src/windows/wslc/arguments/ArgumentValidation.h b/src/windows/wslc/arguments/ArgumentValidation.h index f0b84f6262..4d999b67b1 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.h +++ b/src/windows/wslc/arguments/ArgumentValidation.h @@ -78,6 +78,4 @@ void ValidateFormatTypeFromString(const std::vector& values, const void ValidateGpus(const std::vector& values, const std::wstring& argName); -void ValidateFilter(const std::vector& values); - } // namespace wsl::windows::wslc::validation diff --git a/src/windows/wslc/arguments/SpecParsing.cpp b/src/windows/wslc/arguments/SpecParsing.cpp index 83fcdaa784..8c53542602 100644 --- a/src/windows/wslc/arguments/SpecParsing.cpp +++ b/src/windows/wslc/arguments/SpecParsing.cpp @@ -446,23 +446,12 @@ std::pair ParseDriverOption(const std::wstring& value) return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)}; } -std::pair ParseFilter(const std::wstring& value) +std::pair ParseFilter(const std::wstring& value, const std::wstring& argName) { const auto kv = SplitKeyValue(value); if (!kv.HadSeparator) { - throw ArgumentException(Localization::WSLCCLI_InvalidFilterError(value)); - } - - return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)}; -} - -std::pair ParsePruneFilter(const std::wstring& value) -{ - const auto kv = SplitKeyValue(value); - if (!kv.HadSeparator) - { - throw ArgumentException(Localization::WSLCCLI_InvalidPruneFilterError(value)); + throw ArgumentException(Localization::WSLCCLI_InvalidFilterError(value, argName)); } return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)}; diff --git a/src/windows/wslc/arguments/SpecParsing.h b/src/windows/wslc/arguments/SpecParsing.h index 89f8beff5d..f06f8c3bfe 100644 --- a/src/windows/wslc/arguments/SpecParsing.h +++ b/src/windows/wslc/arguments/SpecParsing.h @@ -72,10 +72,7 @@ std::pair ParseLabel(const std::wstring& value); std::pair ParseDriverOption(const std::wstring& value); // Parses a --filter spec ("key=value"); the separator is required. -std::pair ParseFilter(const std::wstring& value); - -// Parses a prune --filter spec ("key=value"); the separator is required. -std::pair ParsePruneFilter(const std::wstring& value); +std::pair ParseFilter(const std::wstring& value, const std::wstring& argName); struct ParsedNetworkArgument { diff --git a/src/windows/wslc/commands/ContainerPruneCommand.cpp b/src/windows/wslc/commands/ContainerPruneCommand.cpp index 32ec80cbde..96fe5cc626 100644 --- a/src/windows/wslc/commands/ContainerPruneCommand.cpp +++ b/src/windows/wslc/commands/ContainerPruneCommand.cpp @@ -14,6 +14,7 @@ Module Name: #include "ContainerCommand.h" #include "CLIExecutionContext.h" +#include "CommonTasks.h" #include "ContainerTasks.h" #include "SessionTasks.h" #include "Task.h" @@ -44,8 +45,12 @@ std::wstring ContainerPruneCommand::LongDescription() const void ContainerPruneCommand::ExecuteInternal(CLIExecutionContext& context) const { + context.Data.Add(Localization::WSLCCLI_ContainerPruneConfirm()); + context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); + context // << ResolveSession // + << ConfirmAction // << PruneContainers; } } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/ImagePruneCommand.cpp b/src/windows/wslc/commands/ImagePruneCommand.cpp index 63afc7514b..1ea02fc8fc 100644 --- a/src/windows/wslc/commands/ImagePruneCommand.cpp +++ b/src/windows/wslc/commands/ImagePruneCommand.cpp @@ -14,6 +14,7 @@ Module Name: #include "ImageCommand.h" #include "CLIExecutionContext.h" +#include "CommonTasks.h" #include "ImageTasks.h" #include "SessionTasks.h" #include "Task.h" @@ -46,8 +47,13 @@ std::wstring ImagePruneCommand::LongDescription() const void ImagePruneCommand::ExecuteInternal(CLIExecutionContext& context) const { + context.Data.Add( + context.Args.GetValue() ? Localization::WSLCCLI_ImagePruneAllConfirm() : Localization::WSLCCLI_ImagePruneConfirm()); + context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); + context // << ResolveSession // + << ConfirmAction // << PruneImages; } } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/NetworkPruneCommand.cpp b/src/windows/wslc/commands/NetworkPruneCommand.cpp index a37bb21245..a7eab15966 100644 --- a/src/windows/wslc/commands/NetworkPruneCommand.cpp +++ b/src/windows/wslc/commands/NetworkPruneCommand.cpp @@ -14,6 +14,7 @@ Module Name: #include "NetworkCommand.h" #include "CLIExecutionContext.h" +#include "CommonTasks.h" #include "SessionTasks.h" #include "NetworkTasks.h" #include "Task.h" @@ -44,8 +45,12 @@ std::wstring NetworkPruneCommand::LongDescription() const void NetworkPruneCommand::ExecuteInternal(CLIExecutionContext& context) const { + context.Data.Add(Localization::WSLCCLI_NetworkPruneConfirm()); + context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); + context // << ResolveSession // + << ConfirmAction // << PruneNetworks; } } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/VolumePruneCommand.cpp b/src/windows/wslc/commands/VolumePruneCommand.cpp index 312e80918f..af9eb557ba 100644 --- a/src/windows/wslc/commands/VolumePruneCommand.cpp +++ b/src/windows/wslc/commands/VolumePruneCommand.cpp @@ -14,6 +14,7 @@ Module Name: #include "VolumeCommand.h" #include "CLIExecutionContext.h" +#include "CommonTasks.h" #include "SessionTasks.h" #include "VolumeTasks.h" #include "Task.h" @@ -45,8 +46,13 @@ std::wstring VolumePruneCommand::LongDescription() const void VolumePruneCommand::ExecuteInternal(CLIExecutionContext& context) const { + context.Data.Add( + context.Args.GetValue() ? Localization::WSLCCLI_VolumePruneAllConfirm() : Localization::WSLCCLI_VolumePruneConfirm()); + context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); + context // << ResolveSession // + << ConfirmAction // << PruneVolumes; } } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/core/CLIExecutionContext.cpp b/src/windows/wslc/core/CLIExecutionContext.cpp index 75538bf636..0b804eb78a 100644 --- a/src/windows/wslc/core/CLIExecutionContext.cpp +++ b/src/windows/wslc/core/CLIExecutionContext.cpp @@ -16,18 +16,6 @@ HANDLE CLIExecutionContext::CreateCancelEvent() return CancelEvent.get(); } -bool CLIExecutionContext::ConfirmPrune(std::wstring_view warning) -{ - if (Args.GetValue()) - { - return true; - } - - Terminal.Warn(L"{}\n", warning); - - return Terminal.Confirm(wsl::shared::Localization::WSLCCLI_PruneConfirmPrompt()); -} - void CLIExecutionContext::ApplyGlobalEnvironmentOptions() { // NoColor is environment-only and resolved before any output. Freezing it keeps the terminal diff --git a/src/windows/wslc/core/CLIExecutionContext.h b/src/windows/wslc/core/CLIExecutionContext.h index b647f5531e..4a59537e0c 100644 --- a/src/windows/wslc/core/CLIExecutionContext.h +++ b/src/windows/wslc/core/CLIExecutionContext.h @@ -50,11 +50,6 @@ struct CLIExecutionContext : public wsl::windows::common::ExecutionContext HANDLE CreateCancelEvent(); - // Returns true when a destructive prune may proceed, either because --force was passed or - // because the user accepted the confirmation prompt. End of input declines, so a prune with no - // interactive input aborts rather than blocking. - bool ConfirmPrune(std::wstring_view warning); - // Applies and freezes environment-only global options before command-line parsing reports errors. void ApplyGlobalEnvironmentOptions(); }; diff --git a/src/windows/wslc/core/Exceptions.h b/src/windows/wslc/core/Exceptions.h index 1b341474c7..5bf100fbf9 100644 --- a/src/windows/wslc/core/Exceptions.h +++ b/src/windows/wslc/core/Exceptions.h @@ -76,4 +76,13 @@ struct ExecutionException : CLIException { } }; + +// Specific exception for a user declining a confirmation prompt. The requested action is abandoned +// and the program exits successfully. +struct TerminateException : CLIException +{ + TerminateException() : CLIException(std::wstring_view{}) + { + } +}; } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/core/ExecutionContextData.h b/src/windows/wslc/core/ExecutionContextData.h index 09d9529298..ea9f37f095 100644 --- a/src/windows/wslc/core/ExecutionContextData.h +++ b/src/windows/wslc/core/ExecutionContextData.h @@ -42,6 +42,8 @@ enum class Data : size_t Volumes, Networks, NetworkEndpointOptions, + ConfirmWarning, + ConfirmMessage, Max }; @@ -59,6 +61,8 @@ namespace details { DEFINE_DATA_MAPPING(Volumes, std::vector); DEFINE_DATA_MAPPING(Networks, std::vector); DEFINE_DATA_MAPPING(NetworkEndpointOptions, wsl::windows::wslc::models::NetworkEndpointOptions); + DEFINE_DATA_MAPPING(ConfirmWarning, std::wstring); + DEFINE_DATA_MAPPING(ConfirmMessage, std::wstring); } // namespace details struct DataMap : wsl::windows::wslc::EnumBasedVariantMap diff --git a/src/windows/wslc/core/Main.cpp b/src/windows/wslc/core/Main.cpp index d2cac7e69a..4f4c745a0a 100644 --- a/src/windows/wslc/core/Main.cpp +++ b/src/windows/wslc/core/Main.cpp @@ -149,6 +149,11 @@ try context.Terminal.Error(L"{}\n", ee.Message()); return 1; } + catch (const TerminateException&) + { + // The user declined a confirmation prompt, so the requested action is not performed. + return 0; + } catch (...) { LOG_CAUGHT_EXCEPTION(); diff --git a/src/windows/wslc/tasks/CommonTasks.cpp b/src/windows/wslc/tasks/CommonTasks.cpp new file mode 100644 index 0000000000..17cd04c3f8 --- /dev/null +++ b/src/windows/wslc/tasks/CommonTasks.cpp @@ -0,0 +1,47 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + CommonTasks.cpp + +Abstract: + + Implementation of execution logic shared by multiple commands. + +--*/ +#include "Argument.h" +#include "CLIExecutionContext.h" +#include "CommonTasks.h" +#include "Exceptions.h" + +using namespace wsl::shared; +using namespace wsl::windows::wslc::execution; + +namespace wsl::windows::wslc::task { + +// Prompts the user to confirm the action described by Data::ConfirmMessage, preceded by the +// Data::ConfirmWarning explaining what the action affects. The prompt is skipped when --force is +// passed, and declining terminates the invocation without running the remaining tasks. +void ConfirmAction(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::ConfirmMessage)); + + if (context.Args.GetValue()) + { + return; + } + + if (context.Data.Contains(Data::ConfirmWarning)) + { + context.Terminal.Warn(L"{}\n", context.Data.Get()); + } + + if (!context.Terminal.Confirm(context.Data.Get())) + { + throw TerminateException{}; + } +} + +} // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/CommonTasks.h b/src/windows/wslc/tasks/CommonTasks.h new file mode 100644 index 0000000000..b232404312 --- /dev/null +++ b/src/windows/wslc/tasks/CommonTasks.h @@ -0,0 +1,21 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + CommonTasks.h + +Abstract: + + Declaration of execution tasks shared by multiple commands. + +--*/ +#pragma once +#include "CLIExecutionContext.h" + +using wsl::windows::wslc::execution::CLIExecutionContext; + +namespace wsl::windows::wslc::task { +void ConfirmAction(CLIExecutionContext& context); +} // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 001ba30db1..8829e73297 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -1071,11 +1071,6 @@ void PruneContainers(CLIExecutionContext& context) WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); - if (!context.ConfirmPrune(Localization::WSLCCLI_ContainerPruneConfirm())) - { - return; - } - // Filter values are parsed and cached during argument validation. auto filters = context.Args.GetAllValues(); diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index 25a1de0448..d2cbf08133 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -430,11 +430,6 @@ void PruneImages(CLIExecutionContext& context) bool all = context.Args.GetValue(); - if (!context.ConfirmPrune(all ? Localization::WSLCCLI_ImagePruneAllConfirm() : Localization::WSLCCLI_ImagePruneConfirm())) - { - return; - } - // Filter values are parsed and cached during argument validation. auto filters = context.Args.GetAllValues(); diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp index 40c81c37eb..a6f26c6e0b 100644 --- a/src/windows/wslc/tasks/NetworkTasks.cpp +++ b/src/windows/wslc/tasks/NetworkTasks.cpp @@ -265,11 +265,6 @@ void PruneNetworks(CLIExecutionContext& context) WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); - if (!context.ConfirmPrune(Localization::WSLCCLI_NetworkPruneConfirm())) - { - return; - } - // Filter values are parsed and cached during argument validation. auto filters = context.Args.GetAllValues(); diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp index 8c17e0b2fd..e39614a460 100644 --- a/src/windows/wslc/tasks/VolumeTasks.cpp +++ b/src/windows/wslc/tasks/VolumeTasks.cpp @@ -243,11 +243,6 @@ void PruneVolumes(CLIExecutionContext& context) const bool all = context.Args.GetValue(); - if (!context.ConfirmPrune(all ? Localization::WSLCCLI_VolumePruneAllConfirm() : Localization::WSLCCLI_VolumePruneConfirm())) - { - return; - } - // Filter values are parsed and cached during argument validation. auto filters = context.Args.GetAllValues(); diff --git a/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp index 6e9c7c8807..f469bf1f05 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp @@ -218,7 +218,7 @@ class WSLCE2EContainerListTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"container list --filter status"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"status"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"status", L"filter"))); } WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_InvalidStatusValue) diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp index 0dc4537986..f3473e6736 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp @@ -147,48 +147,6 @@ class WSLCE2EContainerPruneTests VerifyContainerIsListed(ConfirmContainerName, L"created"); } - WSLC_TEST_METHOD(WSLCE2E_Container_Prune_AcceptingPromptRemovesContainer) - { - // Answering the prompt with y proceeds with the prune. - auto createResult = RunWslc(std::format(L"container create --name {} {}", ConfirmContainerName, DebianImage.NameAndTag())); - createResult.Verify({.Stderr = L"", .ExitCode = 0}); - const auto containerId = createResult.GetStdoutOneLine(); - - auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); }); - - const auto answerPath = std::filesystem::current_path() / L"wslc-prune-confirm.txt"; - { - std::ofstream answer(answerPath, std::ios::binary | std::ios::trunc); - answer << "y\n"; - } - - auto removeAnswer = wil::scope_exit([&]() { DeleteFileW(answerPath.c_str()); }); - - const auto result = RunWslcWithStdinFile(L"container prune", answerPath); - result.Verify({.ExitCode = 0}); - - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_ContainerPruneConfirm())); - VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId)); - VerifyContainerIsNotListed(ConfirmContainerName); - } - - WSLC_TEST_METHOD(WSLCE2E_Container_Prune_ShortForceFlagSkipsPrompt) - { - // -f resolves to --force on prune commands, so no warning is printed and the prune proceeds. - auto createResult = RunWslc(std::format(L"container create --name {} {}", ConfirmContainerName, DebianImage.NameAndTag())); - createResult.Verify({.Stderr = L"", .ExitCode = 0}); - const auto containerId = createResult.GetStdoutOneLine(); - - auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); }); - - const auto result = RunWslc(L"container prune -f"); - result.Verify({.Stderr = L"", .ExitCode = 0}); - - VERIFY_IS_FALSE(result.StdoutContainsSubstring(Localization::WSLCCLI_PruneConfirmPrompt())); - VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId)); - VerifyContainerIsNotListed(ConfirmContainerName); - } - WSLC_TEST_METHOD(WSLCE2E_Container_Prune_FilterExcludesNonMatchingContainer) { // container prune gained --filter here, so a label filter that matches nothing must leave the @@ -216,7 +174,7 @@ class WSLCE2EContainerPruneTests // stdout rather than printing the warning first. const auto result = RunWslc(L"container prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidPruneFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); } private: diff --git a/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp index ed162f61ca..c4f3324992 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp @@ -326,7 +326,7 @@ class WSLCE2EImageListTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"image list --filter dangling"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"dangling"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"dangling", L"filter"))); } WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp index b6e93cb0bf..42e00bc47e 100644 --- a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp @@ -120,7 +120,7 @@ class WSLCE2EImagePruneTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"image prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidPruneFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); } WSLC_TEST_METHOD(WSLCE2E_Image_Prune_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp index b382a784c8..309d6d50ac 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp @@ -197,7 +197,7 @@ class WSLCE2ENetworkListTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"network list --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); } WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp index e2459aa209..5f6c791f20 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp @@ -195,7 +195,7 @@ class WSLCE2ENetworkPruneTests { const auto result = RunWslc(L"network prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidPruneFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); } WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp index 918195d06d..368fa90d75 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp @@ -131,7 +131,7 @@ class WSLCE2EVolumeListTests { const auto result = RunWslc(L"volume list --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); } WSLC_TEST_METHOD(WSLCE2E_Volume_List_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp index ea816d08f3..bcd916641b 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -215,7 +215,7 @@ class WSLCE2EVolumePruneTests { const auto result = RunWslc(L"volume prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidPruneFilterError(L"label"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); } WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_InvalidKey) From cc61a2d0d4ad3e2f45223eb927c393cde66ccdec Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Thu, 27 Aug 2026 13:38:39 -0700 Subject: [PATCH 5/9] wslc: call ConfirmAction directly from the prune tasks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslc/commands/ContainerPruneCommand.cpp | 2 -- src/windows/wslc/commands/ImagePruneCommand.cpp | 2 -- src/windows/wslc/commands/NetworkPruneCommand.cpp | 2 -- src/windows/wslc/commands/VolumePruneCommand.cpp | 2 -- src/windows/wslc/tasks/ContainerTasks.cpp | 3 +++ src/windows/wslc/tasks/ImageTasks.cpp | 3 +++ src/windows/wslc/tasks/NetworkTasks.cpp | 3 +++ src/windows/wslc/tasks/VolumeTasks.cpp | 3 +++ 8 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/windows/wslc/commands/ContainerPruneCommand.cpp b/src/windows/wslc/commands/ContainerPruneCommand.cpp index 96fe5cc626..5ad8a5f4b2 100644 --- a/src/windows/wslc/commands/ContainerPruneCommand.cpp +++ b/src/windows/wslc/commands/ContainerPruneCommand.cpp @@ -14,7 +14,6 @@ Module Name: #include "ContainerCommand.h" #include "CLIExecutionContext.h" -#include "CommonTasks.h" #include "ContainerTasks.h" #include "SessionTasks.h" #include "Task.h" @@ -50,7 +49,6 @@ void ContainerPruneCommand::ExecuteInternal(CLIExecutionContext& context) const context // << ResolveSession // - << ConfirmAction // << PruneContainers; } } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/ImagePruneCommand.cpp b/src/windows/wslc/commands/ImagePruneCommand.cpp index 1ea02fc8fc..abb3c26aa3 100644 --- a/src/windows/wslc/commands/ImagePruneCommand.cpp +++ b/src/windows/wslc/commands/ImagePruneCommand.cpp @@ -14,7 +14,6 @@ Module Name: #include "ImageCommand.h" #include "CLIExecutionContext.h" -#include "CommonTasks.h" #include "ImageTasks.h" #include "SessionTasks.h" #include "Task.h" @@ -53,7 +52,6 @@ void ImagePruneCommand::ExecuteInternal(CLIExecutionContext& context) const context // << ResolveSession // - << ConfirmAction // << PruneImages; } } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/NetworkPruneCommand.cpp b/src/windows/wslc/commands/NetworkPruneCommand.cpp index a7eab15966..7d951ffd27 100644 --- a/src/windows/wslc/commands/NetworkPruneCommand.cpp +++ b/src/windows/wslc/commands/NetworkPruneCommand.cpp @@ -14,7 +14,6 @@ Module Name: #include "NetworkCommand.h" #include "CLIExecutionContext.h" -#include "CommonTasks.h" #include "SessionTasks.h" #include "NetworkTasks.h" #include "Task.h" @@ -50,7 +49,6 @@ void NetworkPruneCommand::ExecuteInternal(CLIExecutionContext& context) const context // << ResolveSession // - << ConfirmAction // << PruneNetworks; } } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/VolumePruneCommand.cpp b/src/windows/wslc/commands/VolumePruneCommand.cpp index af9eb557ba..c18e7a9837 100644 --- a/src/windows/wslc/commands/VolumePruneCommand.cpp +++ b/src/windows/wslc/commands/VolumePruneCommand.cpp @@ -14,7 +14,6 @@ Module Name: #include "VolumeCommand.h" #include "CLIExecutionContext.h" -#include "CommonTasks.h" #include "SessionTasks.h" #include "VolumeTasks.h" #include "Task.h" @@ -52,7 +51,6 @@ void VolumePruneCommand::ExecuteInternal(CLIExecutionContext& context) const context // << ResolveSession // - << ConfirmAction // << PruneVolumes; } } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 8829e73297..6925222b5c 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -15,6 +15,7 @@ Module Name: #include "ArgumentConvertedTypes.h" #include "AsyncExecution.h" #include "CLIExecutionContext.h" +#include "CommonTasks.h" #include "ContainerModel.h" #include "ContainerService.h" #include "ContainerTasks.h" @@ -1068,6 +1069,8 @@ void ViewContainerLogs(CLIExecutionContext& context) void PruneContainers(CLIExecutionContext& context) { + ConfirmAction(context); + WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index d2cbf08133..62723281c0 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -15,6 +15,7 @@ Module Name: #include "ArgumentConvertedTypes.h" #include "BuildImageCallback.h" #include "CLIExecutionContext.h" +#include "CommonTasks.h" #include "ContainerService.h" #include "ImageModel.h" #include "ImageService.h" @@ -425,6 +426,8 @@ void TagImage(CLIExecutionContext& context) void PruneImages(CLIExecutionContext& context) { + ConfirmAction(context); + WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp index a6f26c6e0b..38c33c044f 100644 --- a/src/windows/wslc/tasks/NetworkTasks.cpp +++ b/src/windows/wslc/tasks/NetworkTasks.cpp @@ -14,6 +14,7 @@ Module Name: #include "Argument.h" #include "ArgumentConvertedTypes.h" #include "CLIExecutionContext.h" +#include "CommonTasks.h" #include "NetworkModel.h" #include "NetworkService.h" #include "NetworkTasks.h" @@ -262,6 +263,8 @@ void ListNetworks(CLIExecutionContext& context) void PruneNetworks(CLIExecutionContext& context) { + ConfirmAction(context); + WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp index e39614a460..957ac8c978 100644 --- a/src/windows/wslc/tasks/VolumeTasks.cpp +++ b/src/windows/wslc/tasks/VolumeTasks.cpp @@ -14,6 +14,7 @@ Module Name: #include "Argument.h" #include "ArgumentConvertedTypes.h" #include "CLIExecutionContext.h" +#include "CommonTasks.h" #include "VolumeModel.h" #include "VolumeService.h" #include "VolumeTasks.h" @@ -238,6 +239,8 @@ void ListVolumes(CLIExecutionContext& context) void PruneVolumes(CLIExecutionContext& context) { + ConfirmAction(context); + WI_ASSERT(context.Data.Contains(Data::Session)); auto& session = context.Data.Get(); From 1889ac0f8b5d19c5265353d2b7c76e376492cd55 Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Thu, 27 Aug 2026 13:42:58 -0700 Subject: [PATCH 6/9] wslc: set confirm prompt data inside the prune tasks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslc/commands/ContainerPruneCommand.cpp | 3 --- src/windows/wslc/commands/ImagePruneCommand.cpp | 4 ---- src/windows/wslc/commands/NetworkPruneCommand.cpp | 3 --- src/windows/wslc/commands/VolumePruneCommand.cpp | 4 ---- src/windows/wslc/tasks/ContainerTasks.cpp | 2 ++ src/windows/wslc/tasks/ImageTasks.cpp | 3 +++ src/windows/wslc/tasks/NetworkTasks.cpp | 2 ++ src/windows/wslc/tasks/VolumeTasks.cpp | 3 +++ 8 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/windows/wslc/commands/ContainerPruneCommand.cpp b/src/windows/wslc/commands/ContainerPruneCommand.cpp index 5ad8a5f4b2..32ec80cbde 100644 --- a/src/windows/wslc/commands/ContainerPruneCommand.cpp +++ b/src/windows/wslc/commands/ContainerPruneCommand.cpp @@ -44,9 +44,6 @@ std::wstring ContainerPruneCommand::LongDescription() const void ContainerPruneCommand::ExecuteInternal(CLIExecutionContext& context) const { - context.Data.Add(Localization::WSLCCLI_ContainerPruneConfirm()); - context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); - context // << ResolveSession // << PruneContainers; diff --git a/src/windows/wslc/commands/ImagePruneCommand.cpp b/src/windows/wslc/commands/ImagePruneCommand.cpp index abb3c26aa3..63afc7514b 100644 --- a/src/windows/wslc/commands/ImagePruneCommand.cpp +++ b/src/windows/wslc/commands/ImagePruneCommand.cpp @@ -46,10 +46,6 @@ std::wstring ImagePruneCommand::LongDescription() const void ImagePruneCommand::ExecuteInternal(CLIExecutionContext& context) const { - context.Data.Add( - context.Args.GetValue() ? Localization::WSLCCLI_ImagePruneAllConfirm() : Localization::WSLCCLI_ImagePruneConfirm()); - context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); - context // << ResolveSession // << PruneImages; diff --git a/src/windows/wslc/commands/NetworkPruneCommand.cpp b/src/windows/wslc/commands/NetworkPruneCommand.cpp index 7d951ffd27..a37bb21245 100644 --- a/src/windows/wslc/commands/NetworkPruneCommand.cpp +++ b/src/windows/wslc/commands/NetworkPruneCommand.cpp @@ -44,9 +44,6 @@ std::wstring NetworkPruneCommand::LongDescription() const void NetworkPruneCommand::ExecuteInternal(CLIExecutionContext& context) const { - context.Data.Add(Localization::WSLCCLI_NetworkPruneConfirm()); - context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); - context // << ResolveSession // << PruneNetworks; diff --git a/src/windows/wslc/commands/VolumePruneCommand.cpp b/src/windows/wslc/commands/VolumePruneCommand.cpp index c18e7a9837..312e80918f 100644 --- a/src/windows/wslc/commands/VolumePruneCommand.cpp +++ b/src/windows/wslc/commands/VolumePruneCommand.cpp @@ -45,10 +45,6 @@ std::wstring VolumePruneCommand::LongDescription() const void VolumePruneCommand::ExecuteInternal(CLIExecutionContext& context) const { - context.Data.Add( - context.Args.GetValue() ? Localization::WSLCCLI_VolumePruneAllConfirm() : Localization::WSLCCLI_VolumePruneConfirm()); - context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); - context // << ResolveSession // << PruneVolumes; diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 6925222b5c..d4896f971e 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -1069,6 +1069,8 @@ void ViewContainerLogs(CLIExecutionContext& context) void PruneContainers(CLIExecutionContext& context) { + context.Data.Add(Localization::WSLCCLI_ContainerPruneConfirm()); + context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); ConfirmAction(context); WI_ASSERT(context.Data.Contains(Data::Session)); diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index 62723281c0..2ad29112d2 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -426,6 +426,9 @@ void TagImage(CLIExecutionContext& context) void PruneImages(CLIExecutionContext& context) { + context.Data.Add( + context.Args.GetValue() ? Localization::WSLCCLI_ImagePruneAllConfirm() : Localization::WSLCCLI_ImagePruneConfirm()); + context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); ConfirmAction(context); WI_ASSERT(context.Data.Contains(Data::Session)); diff --git a/src/windows/wslc/tasks/NetworkTasks.cpp b/src/windows/wslc/tasks/NetworkTasks.cpp index 38c33c044f..6cc9136a81 100644 --- a/src/windows/wslc/tasks/NetworkTasks.cpp +++ b/src/windows/wslc/tasks/NetworkTasks.cpp @@ -263,6 +263,8 @@ void ListNetworks(CLIExecutionContext& context) void PruneNetworks(CLIExecutionContext& context) { + context.Data.Add(Localization::WSLCCLI_NetworkPruneConfirm()); + context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); ConfirmAction(context); WI_ASSERT(context.Data.Contains(Data::Session)); diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp index 957ac8c978..7d75190214 100644 --- a/src/windows/wslc/tasks/VolumeTasks.cpp +++ b/src/windows/wslc/tasks/VolumeTasks.cpp @@ -239,6 +239,9 @@ void ListVolumes(CLIExecutionContext& context) void PruneVolumes(CLIExecutionContext& context) { + context.Data.Add( + context.Args.GetValue() ? Localization::WSLCCLI_VolumePruneAllConfirm() : Localization::WSLCCLI_VolumePruneConfirm()); + context.Data.Add(Localization::WSLCCLI_PruneConfirmPrompt()); ConfirmAction(context); WI_ASSERT(context.Data.Contains(Data::Session)); From af01e83e5580fdf465cb53c218f7e51940f0ef7b Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Thu, 27 Aug 2026 18:27:27 -0700 Subject: [PATCH 7/9] wslc: fix filter error comment and consolidate confirmation tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 3 +- .../wslc/WSLCCLIExecutionUnitTests.cpp | 13 ++++ .../windows/wslc/WSLCCLITerminalUnitTests.cpp | 63 ++++++++----------- .../wslc/e2e/WSLCE2EContainerPruneTests.cpp | 17 +++-- 4 files changed, 47 insertions(+), 49 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 007a00a03a..43935cbc83 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -3039,7 +3039,8 @@ On first run, creates the file with all settings commented out at their defaults Invalid value "{}" for the '--{}' option: bad format of filter (expected name=value) - {FixedPlaceholder="{}"}{FixedPlaceholder="{}"}{Locked="--"}Command line arguments, file names and string inserts should not be translated + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Provide filter values (e.g. "until=<timestamp>") {Locked="until"}Command line arguments, file names and string inserts should not be translated diff --git a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp index 2ba79c0076..74bf13f1f7 100644 --- a/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIExecutionUnitTests.cpp @@ -160,6 +160,16 @@ class WSLCCLIExecutionUnitTests dataMap.Add(std::move(endpointOptions)); handled = true; } + else if (dataType == Data::ConfirmWarning) + { + dataMap.Add(std::wstring{L"warning"}); + handled = true; + } + else if (dataType == Data::ConfirmMessage) + { + dataMap.Add(std::wstring{L"message"}); + handled = true; + } if (!handled) { @@ -177,6 +187,9 @@ class WSLCCLIExecutionUnitTests auto& containers = dataMap.Get(); VERIFY_ARE_EQUAL(0u, containers.size()); + auto& confirmMessage = dataMap.Get(); + VERIFY_ARE_EQUAL(std::wstring{L"message"}, confirmMessage); + // Other more complex EnumVariantMap tests are in the Args unit tests. // This one will just verify all the data types in the Data Map work as expected. } diff --git a/test/windows/wslc/WSLCCLITerminalUnitTests.cpp b/test/windows/wslc/WSLCCLITerminalUnitTests.cpp index b6ef6d0f3b..5b35321c38 100644 --- a/test/windows/wslc/WSLCCLITerminalUnitTests.cpp +++ b/test/windows/wslc/WSLCCLITerminalUnitTests.cpp @@ -552,55 +552,42 @@ class WSLCCLITerminalUnitTests VERIFY_ARE_EQUAL(expected, result); } - TEST_METHOD(Terminal_ConfirmAcceptsLowercaseY) + TEST_METHOD(Terminal_Confirm) { - InputCaptureTerminal cap{L"y\n"}; - VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?")); - - // The prompt is written inline on stdout with the standard suffix appended. - VERIFY_ARE_EQUAL(std::wstring{L"Remove everything? [y/N] "}, cap.outPipe.captured()); - VERIFY_ARE_EQUAL(std::wstring{L""}, cap.errPipe.captured()); - } - - TEST_METHOD(Terminal_ConfirmAcceptsUppercaseY) - { - InputCaptureTerminal cap{L"Y\n"}; - VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?")); - } + // The prompt is written inline on stdout with the standard suffix appended, and nothing + // goes to stderr. + { + InputCaptureTerminal cap{L"y\n"}; + VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?")); + VERIFY_ARE_EQUAL(std::wstring{L"Remove everything? [y/N] "}, cap.outPipe.captured()); + VERIFY_ARE_EQUAL(std::wstring{L""}, cap.errPipe.captured()); + } - TEST_METHOD(Terminal_ConfirmTrimsSurroundingWhitespace) - { - InputCaptureTerminal cap{L" y \n"}; - VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?")); - } + // Only a bare y accepts, in either case and with surrounding whitespace trimmed. + for (const auto* answer : {L"y\n", L"Y\n", L" y \n"}) + { + InputCaptureTerminal cap{answer}; + VERIFY_IS_TRUE(cap.terminal.Confirm(L"Remove everything?")); + } - TEST_METHOD(Terminal_ConfirmRejectsAnswersOtherThanY) - { - // Only a bare y accepts; a spelled-out yes declines, matching the container CLI ecosystem. - for (const auto* answer : {L"yes\n", L"n\n", L"N\n", L"no\n", L"\n", L"maybe\n"}) + // Anything else declines, including a spelled-out yes, matching the container CLI ecosystem. + // A prune with no input attached must abort rather than block, so end of input declines too. + for (const auto* answer : {L"yes\n", L"n\n", L"N\n", L"no\n", L"\n", L"maybe\n", L""}) { InputCaptureTerminal cap{answer}; VERIFY_IS_FALSE(cap.terminal.Confirm(L"Remove everything?")); + VERIFY_ARE_EQUAL(std::wstring{L"Remove everything? [y/N] "}, cap.outPipe.captured()); } - } - TEST_METHOD(Terminal_ConfirmDeclinesAtEndOfInput) - { - // A prune with no input attached must abort rather than block, so end of input declines. - InputCaptureTerminal cap{L""}; - VERIFY_IS_FALSE(cap.terminal.Confirm(L"Remove everything?")); - VERIFY_ARE_EQUAL(std::wstring{L"Remove everything? [y/N] "}, cap.outPipe.captured()); - } - - TEST_METHOD(Terminal_ConfirmEmitsMessageVerbatimWithFormatCharacters) - { // The message is a formatting argument, not a format string, so braces must not be // interpreted. - InputCaptureTerminal cap{L"y\n"}; - const std::wstring message = L"Remove {} {0} {name} 100%?"; + { + InputCaptureTerminal cap{L"y\n"}; + const std::wstring message = L"Remove {} {0} {name} 100%?"; - VERIFY_IS_TRUE(cap.terminal.Confirm(message)); - VERIFY_ARE_EQUAL(message + L" [y/N] ", cap.outPipe.captured()); + VERIFY_IS_TRUE(cap.terminal.Confirm(message)); + VERIFY_ARE_EQUAL(message + L" [y/N] ", cap.outPipe.captured()); + } } TEST_METHOD(Terminal_ReadLineMaskDefaultsToUnmasked) diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp index f3473e6736..0c0f576cba 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp @@ -127,7 +127,7 @@ class WSLCE2EContainerPruneTests VerifyContainerIsNotListed(L"prune-multi-2"); } - WSLC_TEST_METHOD(WSLCE2E_Container_Prune_WithoutForceDeclinesWithoutInput) + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_WithoutForcePromptsAndDeclines) { // Without --force the prune asks for confirmation. No input is attached here, so the read hits // end of input and declines: the container survives and the command still succeeds. @@ -135,6 +135,12 @@ class WSLCE2EContainerPruneTests auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(ConfirmContainerName); }); + // Argument validation runs before the confirmation prompt, so a bad filter fails with empty + // stdout rather than printing the warning first. + const auto malformed = RunWslc(L"container prune --filter label"); + malformed.Verify({.Stdout = L"", .ExitCode = 1}); + VERIFY_IS_TRUE(malformed.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); + const auto result = RunWslc(L"container prune"); result.Verify({.ExitCode = 0}); @@ -168,15 +174,6 @@ class WSLCE2EContainerPruneTests VerifyContainerIsNotListed(ConfirmContainerName); } - WSLC_TEST_METHOD(WSLCE2E_Container_Prune_MalformedFilterIsRejectedBeforePrompting) - { - // Argument validation runs before the confirmation prompt, so a bad filter fails with empty - // stdout rather than printing the warning first. - const auto result = RunWslc(L"container prune --filter label"); - result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); - } - private: const std::wstring ConfirmContainerName = L"prune-confirm-test"; const TestImage& DebianImage = DebianTestImage(); From 524f8d38d46429afed0ac88cc8cac79547b42598 Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Fri, 28 Aug 2026 11:11:16 -0700 Subject: [PATCH 8/9] wslc: hardcode --filter in the invalid filter error to keep one insert Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- localization/strings/en-US/Resources.resw | 4 ++-- src/windows/wslc/arguments/ArgumentValidation.cpp | 6 ++++-- src/windows/wslc/arguments/SpecParsing.cpp | 4 ++-- src/windows/wslc/arguments/SpecParsing.h | 2 +- test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2EImageListTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp | 2 +- 12 files changed, 17 insertions(+), 15 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 43935cbc83..65cc51506a 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -3038,8 +3038,8 @@ On first run, creates the file with all settings commented out at their defaults Filter output based on conditions provided - Invalid value "{}" for the '--{}' option: bad format of filter (expected name=value) - {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Invalid value "{}" for the '--filter' option: bad format of filter (expected name=value) + {FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated Provide filter values (e.g. "until=<timestamp>") diff --git a/src/windows/wslc/arguments/ArgumentValidation.cpp b/src/windows/wslc/arguments/ArgumentValidation.cpp index 6c6290d282..bd14b20e7a 100644 --- a/src/windows/wslc/arguments/ArgumentValidation.cpp +++ b/src/windows/wslc/arguments/ArgumentValidation.cpp @@ -208,11 +208,13 @@ void Argument::Validate(ArgMap& execArgs) const break; case ArgType::Filter: - CacheConverted(execArgs, m_name, validation::ParseFilter); + CacheConverted( + execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseFilter(value); }); break; case ArgType::PruneFilter: - CacheConverted(execArgs, m_name, validation::ParseFilter); + CacheConverted( + execArgs, m_name, [](const std::wstring& value, const std::wstring&) { return validation::ParseFilter(value); }); break; case ArgType::Label: diff --git a/src/windows/wslc/arguments/SpecParsing.cpp b/src/windows/wslc/arguments/SpecParsing.cpp index 8c53542602..3f88cbacab 100644 --- a/src/windows/wslc/arguments/SpecParsing.cpp +++ b/src/windows/wslc/arguments/SpecParsing.cpp @@ -446,12 +446,12 @@ std::pair ParseDriverOption(const std::wstring& value) return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)}; } -std::pair ParseFilter(const std::wstring& value, const std::wstring& argName) +std::pair ParseFilter(const std::wstring& value) { const auto kv = SplitKeyValue(value); if (!kv.HadSeparator) { - throw ArgumentException(Localization::WSLCCLI_InvalidFilterError(value, argName)); + throw ArgumentException(Localization::WSLCCLI_InvalidFilterError(value)); } return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)}; diff --git a/src/windows/wslc/arguments/SpecParsing.h b/src/windows/wslc/arguments/SpecParsing.h index f06f8c3bfe..9570427662 100644 --- a/src/windows/wslc/arguments/SpecParsing.h +++ b/src/windows/wslc/arguments/SpecParsing.h @@ -72,7 +72,7 @@ std::pair ParseLabel(const std::wstring& value); std::pair ParseDriverOption(const std::wstring& value); // Parses a --filter spec ("key=value"); the separator is required. -std::pair ParseFilter(const std::wstring& value, const std::wstring& argName); +std::pair ParseFilter(const std::wstring& value); struct ParsedNetworkArgument { diff --git a/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp index f469bf1f05..6e9c7c8807 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp @@ -218,7 +218,7 @@ class WSLCE2EContainerListTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"container list --filter status"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"status", L"filter"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"status"))); } WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_InvalidStatusValue) diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp index 0c0f576cba..ce89023357 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp @@ -139,7 +139,7 @@ class WSLCE2EContainerPruneTests // stdout rather than printing the warning first. const auto malformed = RunWslc(L"container prune --filter label"); malformed.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(malformed.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); + VERIFY_IS_TRUE(malformed.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); const auto result = RunWslc(L"container prune"); result.Verify({.ExitCode = 0}); diff --git a/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp index c4f3324992..ed162f61ca 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageListTests.cpp @@ -326,7 +326,7 @@ class WSLCE2EImageListTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"image list --filter dangling"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"dangling", L"filter"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"dangling"))); } WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp index 42e00bc47e..eb4d9b3642 100644 --- a/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImagePruneTests.cpp @@ -120,7 +120,7 @@ class WSLCE2EImagePruneTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"image prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); } WSLC_TEST_METHOD(WSLCE2E_Image_Prune_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp index 309d6d50ac..b382a784c8 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkListTests.cpp @@ -197,7 +197,7 @@ class WSLCE2ENetworkListTests // Filter values must be of the form key=value; bare keys are rejected by the CLI. const auto result = RunWslc(L"network list --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); } WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp index 5f6c791f20..bf49eab972 100644 --- a/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ENetworkPruneTests.cpp @@ -195,7 +195,7 @@ class WSLCE2ENetworkPruneTests { const auto result = RunWslc(L"network prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); } WSLC_TEST_METHOD(WSLCE2E_Network_Prune_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp index 368fa90d75..918195d06d 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeListTests.cpp @@ -131,7 +131,7 @@ class WSLCE2EVolumeListTests { const auto result = RunWslc(L"volume list --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); } WSLC_TEST_METHOD(WSLCE2E_Volume_List_Filter_InvalidKey) diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp index bcd916641b..612172e7d6 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -215,7 +215,7 @@ class WSLCE2EVolumePruneTests { const auto result = RunWslc(L"volume prune --filter label"); result.Verify({.Stdout = L"", .ExitCode = 1}); - VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label", L"filter"))); + VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); } WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_Filter_InvalidKey) From cb582761381d584c7f5cae7707cd6bdadd2f2434 Mon Sep 17 00:00:00 2001 From: Gavin Garzia Date: Wed, 2 Sep 2026 09:25:52 -0700 Subject: [PATCH 9/9] remove fix-and-build.ps1 helper script Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fix-and-build.ps1 | 80 ----------------------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 fix-and-build.ps1 diff --git a/fix-and-build.ps1 b/fix-and-build.ps1 deleted file mode 100644 index d23030d8af..0000000000 --- a/fix-and-build.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -$ErrorActionPreference = 'Continue' -$repo = 'C:\Users\gavingarzia\repos\WSL' -$logDir = 'C:\Users\gavingarzia\.copilot\session-state\5e1713ed-368a-4854-8fd8-38a8fa7216bf\files\buildlogs2' -New-Item -ItemType Directory -Force -Path $logDir | Out-Null -Set-Location $repo - -$branches = @( - 'user/ggarzia/wslc-prune-docker-parity', - 'user/ggarzia/wslc-logs-details', - 'user/ggarzia/wslc-quiet-parity', - 'user/ggarzia/wslc-inspect-size', - 'user/ggarzia/wslc-ps-size', - 'user/ggarzia/wslc-pull-all-tags', - 'user/ggarzia/wslc-images-digests', - 'user/ggarzia/wslc-images-all', - 'user/ggarzia/unity-build' -) - -$target = 'test/windows/wslc/WSLCCLIParserUnitTests.cpp' -$old = 'Argument::Create(ArgType::ObjectId, false, Limit::Unlimited)' -$new = 'Argument::Create(ArgType::ObjectId, {.Required = false, .Limit = Limit::Unlimited})' - -$summary = @() -foreach ($b in $branches) { - $short = $b -replace '^user/ggarzia/', '' - $log = Join-Path $logDir "$short.log" - Write-Host "=== $short : checkout ===" - git checkout $b 2>&1 | Out-Null - - # Apply the master-inherited build fix. - $path = Join-Path $repo $target - $content = Get-Content $path -Raw - $patched = $false - if ($content.Contains($old)) { - $content = $content.Replace($old, $new) - Set-Content -Path $path -Value $content -NoNewline - $patched = $true - } - - if ($patched) { - # Format the whole branch diff, per repo convention. - $base = git merge-base origin/master HEAD - $files = git diff --name-only $base HEAD | Where-Object { $_ -match '\.(h|cpp|hpp|c|hxx)$' } - $files += $target - $files | Sort-Object -Unique | Out-File -Encoding ascii "$env:TEMP\fmt_$short.txt" - powershell -ExecutionPolicy Bypass -File .\FormatSource.ps1 -ChangesFile "$env:TEMP\fmt_$short.txt" 2>&1 | Out-Null - git add -A - git commit --amend --no-edit 2>&1 | Out-Null - } - - # Build, retrying once to clear the documented stale-PCH (C4651/C2220) storm. - $code = 1 - $mins = 0 - for ($attempt = 1; $attempt -le 2; $attempt++) { - Write-Host "=== $short : build attempt $attempt ===" - $start = Get-Date - cmake --build . -- -m 2>&1 | Tee-Object -FilePath $log | Out-Null - $code = $LASTEXITCODE - $mins = [math]::Round(((Get-Date) - $start).TotalMinutes, 1) - if ($code -eq 0) { break } - } - - $real = Select-String -Path $log -Pattern 'error [A-Z]+[0-9]+' -ErrorAction SilentlyContinue | - Where-Object { $_.Line -notmatch 'C2220' } | - ForEach-Object { $_.Line.Trim() } | Sort-Object -Unique - - $summary += [pscustomobject]@{ - Branch = $short - Patched = $patched - Result = $(if ($code -eq 0) { 'OK' } else { 'FAILED' }) - Minutes = $mins - RealErrors = @($real).Count - } - Write-Host "=== $short : exit=$code ${mins}min realerrors=$(@($real).Count) ===" - if ($code -ne 0) { $real | Select-Object -First 5 | ForEach-Object { Write-Host " $_" } } -} - -Write-Host "" -Write-Host "############ SUMMARY ############" -$summary | Format-Table -AutoSize | Out-String -Width 220 | Write-Host