From 91b14fd3dd44c65d9b15fae8dbd0e6dc44d2f888 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sat, 20 Jun 2026 22:24:54 -0500 Subject: [PATCH] Fix msstore search by id fallback Add a REST search fallback that retries single-ID substring misses through the optimized exact-ID path so msstore IDs resolve without requiring --exact while preserving partial-ID behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/ReleaseNotes.md | 2 +- .../RestInterface_1_0.cpp | 88 +++++++++++++++++++ .../Rest/Schema/1_0/RestInterface_1_0.cpp | 23 ++++- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 57c970aeb6..e2e2a9c01d 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -4,4 +4,4 @@ Nothing yet. ## Bug Fixes -* None yet +* Fixed an issue where `winget search --id ` could fail to return a Microsoft Store package unless `--exact` was also provided. diff --git a/src/AppInstallerCLITests/RestInterface_1_0.cpp b/src/AppInstallerCLITests/RestInterface_1_0.cpp index bb8ef2526a..374f61e568 100644 --- a/src/AppInstallerCLITests/RestInterface_1_0.cpp +++ b/src/AppInstallerCLITests/RestInterface_1_0.cpp @@ -514,6 +514,94 @@ TEST_CASE("Search_Optimized_NoResponse_NotFoundCode", "[RestSource][Interface_1_ REQUIRE_THROWS_HR(v1.Search(request), APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND); } +TEST_CASE("Search_SubstringIdFallback_ManifestResponse", "[RestSource][Interface_1_0]") +{ + utility::string_t emptySearchResponse = _XPLATSTR(R"delimiter({ "Data" : [] })delimiter"); + + auto handler = std::make_shared( + [emptySearchResponse](web::http::http_request request) -> pplx::task + { + web::http::http_response response; + response.headers().set_content_type(web::http::details::mime_types::application_json); + response.headers().set_cache_control(L"no-store"); + + if (request.method() == web::http::methods::POST) + { + response.set_status_code(web::http::status_codes::OK); + response.set_body(web::json::value::parse(emptySearchResponse)); + } + else if (request.method() == web::http::methods::GET) + { + response.set_status_code(web::http::status_codes::OK); + response.set_body(web::json::value::parse(GetGoodManifest_RequiredFields())); + } + else + { + response.set_status_code(web::http::status_codes::BadRequest); + } + + return pplx::task_from_result(response); + }); + + HttpClientHelper helper{ std::move(handler) }; + AppInstaller::Repository::SearchRequest request; + request.Filters.emplace_back(PackageMatchFilter{ PackageMatchField::Id, MatchType::Substring, "Foo.Bar" }); + Interface v1{ TestRestUriString, std::move(helper) }; + Schema::IRestClient::SearchResult result = v1.Search(request); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].PackageInformation.PackageIdentifier == "Foo.Bar"); + REQUIRE(result.Matches[0].Versions.size() == 1); + REQUIRE(result.Matches[0].Versions[0].VersionAndChannel.GetVersion().ToString() == "5.0.0"); + REQUIRE(result.Matches[0].Versions[0].Manifest.has_value()); +} + +TEST_CASE("Search_SubstringId_NoFallbackWhenSearchMatches", "[RestSource][Interface_1_0]") +{ + utility::string_t searchResponse = _XPLATSTR( + R"delimiter({ + "Data" : [ + { + "PackageIdentifier": "Foo.Bar.Baz", + "PackageName": "Foo Package", + "Publisher": "Foo", + "Versions": [ + { "PackageVersion": "1.0.0" } + ] + } + ] + })delimiter"); + + auto handler = std::make_shared( + [searchResponse](web::http::http_request request) -> pplx::task + { + web::http::http_response response; + response.headers().set_content_type(web::http::details::mime_types::application_json); + response.headers().set_cache_control(L"no-store"); + + if (request.method() == web::http::methods::POST) + { + response.set_status_code(web::http::status_codes::OK); + response.set_body(web::json::value::parse(searchResponse)); + } + else + { + response.set_status_code(web::http::status_codes::NotFound); + } + + return pplx::task_from_result(response); + }); + + HttpClientHelper helper{ std::move(handler) }; + AppInstaller::Repository::SearchRequest request; + request.Filters.emplace_back(PackageMatchFilter{ PackageMatchField::Id, MatchType::Substring, "Foo.Bar" }); + Interface v1{ TestRestUriString, std::move(helper) }; + Schema::IRestClient::SearchResult result = v1.Search(request); + + REQUIRE(result.Matches.size() == 1); + REQUIRE(result.Matches[0].PackageInformation.PackageIdentifier == "Foo.Bar.Baz"); +} + TEST_CASE("GetManifests_GoodResponse", "[RestSource][Interface_1_0]") { GoodManifest_AllFields sampleManifest; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp index 689ad7bf33..ed180d19da 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp @@ -70,6 +70,14 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 return result; } + + bool MeetsOptimizedSearchFallbackCriteria(const SearchRequest& request) + { + return !request.Query && request.Inclusions.empty() && + request.Filters.size() == 1 && + request.Filters[0].Field == PackageMatchField::Id && + request.Filters[0].Type == MatchType::Substring; + } } Interface::Interface(const std::string& restApi, const Http::HttpClientHelper& httpClientHelper) : m_restApiUri(restApi), m_httpClientHelper(httpClientHelper) @@ -98,7 +106,20 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 return OptimizedSearch(request); } - return SearchInternal(request); + SearchResult result = SearchInternal(request); + + // Some sources (including msstore) may not return exact package identifier matches for substring ID requests. + // Preserve substring semantics, but if no match was found for a single-ID request, retry through optimized exact lookup. + if (result.Matches.empty() && MeetsOptimizedSearchFallbackCriteria(request)) + { + SearchRequest optimizedRequest = request; + optimizedRequest.Filters[0].Type = MatchType::CaseInsensitive; + + AICLI_LOG(Repo, Verbose, << "No search results for ID substring request; retrying with optimized exact ID lookup."); + return OptimizedSearch(optimizedRequest); + } + + return result; } IRestClient::SearchResult Interface::SearchInternal(const SearchRequest& request) const