fix(cli): never state an artwork outcome the walk did not observe

A transient external failure traced as 'error' fell through to 'not resolved', which
is the most common state behind a missing-artwork report. It is now indeterminate, and
an offline win that a skipped higher-priority external candidate could have taken says
so instead of naming a winner the live chain might not pick.
This commit is contained in:
Deluan 2026-08-14 13:25:14 -04:00
commit ab3e6fe949
2 changed files with 43 additions and 3 deletions

View file

@ -71,15 +71,27 @@ func walksPriorityChain(kind model.Kind) bool {
return kind == model.KindArtistArtwork || kind == model.KindAlbumArtwork
}
// explainResult states the verdict of the walk. Offline runs skip the external tier, so a
// local miss is unknown rather than unresolvable.
// explainResult states the verdict of the walk. An external tier that was skipped or that failed
// leaves the outcome unknown: nothing observed that the item has no artwork.
func explainResult(source string, steps []artwork.TraceStep) string {
if source != "" {
for _, s := range steps {
if s.Outcome == "hit" {
break
}
if s.Outcome == "would-try" {
return "resolved from " + source +
" (offline: a higher-priority external candidate was not tried; re-run with --live)"
}
}
return "resolved from " + source
}
for _, s := range steps {
if s.Outcome == "would-try" {
switch {
case s.Outcome == "would-try":
return "indeterminate (external agents not called; re-run with --live)"
case s.Outcome == "error" && strings.HasPrefix(s.Candidate, "external:"):
return "indeterminate (an external lookup failed; the item may resolve on a later attempt)"
}
}
return "not resolved"

View file

@ -44,6 +44,34 @@ var _ = Describe("explainResult", func() {
Expect(explainResult("", steps)).To(Equal("not resolved"))
})
It("reports indeterminate when an external lookup failed transiently", func() {
steps := []artwork.TraceStep{
{Candidate: "artist.*", Outcome: "miss"},
{Candidate: "external:deezer", Outcome: "error", Detail: "context deadline exceeded"},
}
Expect(explainResult("", steps)).To(ContainSubstring("indeterminate"),
"a failed network call is not evidence that the item has no artwork")
})
It("qualifies a win a skipped higher-priority external candidate could have taken", func() {
steps := []artwork.TraceStep{
{Candidate: "external:deezer", Outcome: "would-try"},
{Candidate: "artist.*", Outcome: "hit", Detail: "/music/artist.jpg"},
}
res := explainResult("artist.*", steps)
Expect(res).To(ContainSubstring("resolved from artist.*"))
Expect(res).To(ContainSubstring("--live"),
"offline, the winner is only the winner because the external tier was skipped")
})
It("does not qualify a win that no skipped candidate outranked", func() {
steps := []artwork.TraceStep{
{Candidate: "artist.*", Outcome: "hit"},
{Candidate: "external:deezer", Outcome: "would-try"},
}
Expect(explainResult("artist.*", steps)).To(Equal("resolved from artist.*"))
})
It("reports indeterminate when external agents were never called", func() {
steps := []artwork.TraceStep{
{Candidate: "artist.*", Outcome: "miss"},