refactor(artwork): export the trace outcome vocabulary

The CLI hardcoded the outcome literals and the "external:" prefix, so renaming a
constant's value in core/artwork left cmd compiling and the suite green while
`artwork explain` silently degraded its verdict.

Renaming a value now fails the golden vocabulary test in core/artwork and the
explainResult tests in cmd.
This commit is contained in:
Deluan 2026-08-14 14:47:21 -04:00
commit a1b9976521
7 changed files with 92 additions and 72 deletions

View file

@ -517,10 +517,10 @@ func walksPriorityChain(kind model.Kind) bool {
func explainResult(source string, steps []artwork.TraceStep) string {
if source != "" {
for _, s := range steps {
if s.Outcome == "hit" {
if s.Outcome == artwork.OutcomeHit {
break
}
if s.Outcome == "would-try" {
if s.Outcome == artwork.OutcomeWouldTry {
return "resolved from " + source +
" (offline: a higher-priority external candidate was not tried; re-run with --live)"
}
@ -529,9 +529,9 @@ func explainResult(source string, steps []artwork.TraceStep) string {
}
for _, s := range steps {
switch {
case s.Outcome == "would-try":
case s.Outcome == artwork.OutcomeWouldTry:
return "indeterminate (external agents not called; re-run with --live)"
case s.Outcome == "error" && strings.HasPrefix(s.Candidate, "external:"):
case s.Outcome == artwork.OutcomeError && strings.HasPrefix(s.Candidate, artwork.ExternalPrefix):
return "indeterminate (an external lookup failed; the item may resolve on a later attempt)"
}
}

View file

@ -47,13 +47,13 @@ func fetchArtistImage(ctx context.Context, ag *agents.Agents, gate gateFunc, ar
// Synthetic artists would otherwise get an unrelated agent result assigned to them.
switch ar.ID {
case consts.UnknownArtistID, consts.VariousArtistsID:
traceFrom(ctx).add(traceStep{Candidate: externalCandidate, Outcome: outcomeSkipped, Detail: "synthetic artist"})
traceFrom(ctx).add(traceStep{Candidate: ExternalCandidate, Outcome: OutcomeSkipped, Detail: "synthetic artist"})
return nil, "", false
}
name := externalName(ar.Name)
imageAgents := ag.ArtistImageAgents()
if len(imageAgents) == 0 {
traceFrom(ctx).add(traceStep{Candidate: externalCandidate, Outcome: outcomeSkipped,
traceFrom(ctx).add(traceStep{Candidate: ExternalCandidate, Outcome: OutcomeSkipped,
Detail: "no enabled agent provides artist images"})
return nil, "", false
}
@ -85,7 +85,7 @@ func fetchAlbumImage(ctx context.Context, ag *agents.Agents, gate gateFunc, al m
name, artist := externalName(al.Name), externalName(al.AlbumArtist)
imageAgents := ag.AlbumImageAgents()
if len(imageAgents) == 0 {
traceFrom(ctx).add(traceStep{Candidate: externalCandidate, Outcome: outcomeSkipped,
traceFrom(ctx).add(traceStep{Candidate: ExternalCandidate, Outcome: OutcomeSkipped,
Detail: "no enabled agent provides album images"})
return nil, "", false
}

View file

@ -180,7 +180,7 @@ var _ = Describe("agent images", func() {
r, _, extErr := fetchArtistImage(withTrace(ctx, t), ag, passthroughGate, model.Artist{ID: "ar1"})
Expect(r).To(BeNil())
Expect(extErr).To(BeFalse())
Expect(t.Steps()).To(Equal([]traceStep{{Candidate: "external", Outcome: outcomeSkipped,
Expect(t.Steps()).To(Equal([]traceStep{{Candidate: "external", Outcome: OutcomeSkipped,
Detail: "no enabled agent provides artist images"}}),
"a configured external token must never be silently absent from the chain")
})
@ -193,7 +193,7 @@ var _ = Describe("agent images", func() {
_, _, _ = fetchArtistImage(withTrace(ctx, t), ag, passthroughGate,
model.Artist{ID: consts.VariousArtistsID, Name: "Various Artists"})
Expect(t.Steps()).To(HaveLen(1))
Expect(t.Steps()[0].Outcome).To(Equal(outcomeSkipped))
Expect(t.Steps()[0].Outcome).To(Equal(OutcomeSkipped))
Expect(t.Steps()[0].Detail).To(ContainSubstring("synthetic"))
})
@ -262,7 +262,7 @@ var _ = Describe("agent images", func() {
r, _, extErr := fetchAlbumImage(withTrace(ctx, t), ag, passthroughGate, model.Album{Name: "Album"})
Expect(r).To(BeNil())
Expect(extErr).To(BeFalse())
Expect(t.Steps()).To(Equal([]traceStep{{Candidate: "external", Outcome: outcomeSkipped,
Expect(t.Steps()).To(Equal([]traceStep{{Candidate: "external", Outcome: OutcomeSkipped,
Detail: "no enabled agent provides album images"}}),
"a configured external token must never be silently absent from the chain")
})

View file

@ -123,7 +123,7 @@ func (d *discArtworkReader) fromDiscArtPriority(ctx context.Context, ffmpeg ffmp
fromTag(ctx, d.lib.FS, d.firstTrackRel),
fromFFmpegTag(ctx, ffmpeg, d.lib.Abs(d.firstTrackRel)),
)
case pattern == "external":
case pattern == ExternalCandidate:
// Not supported for disc art, silently ignore
case pattern == "discsubtitle":
if subtitle := strings.TrimSpace(d.album.Discs[d.discNumber]); subtitle != "" {

View file

@ -43,21 +43,21 @@ type chainState struct {
func (c *chainState) try(candidate string, res resolution, ok bool) (resolution, bool) {
if ok {
res.extError = c.extErr
c.record(candidate, outcomeHit, res.sourcePath)
c.record(candidate, OutcomeHit, res.sourcePath)
return res, true
}
c.localErr = c.localErr || res.localError
if res.localError {
c.record(candidate, outcomeUnreadable, "")
c.record(candidate, OutcomeUnreadable, "")
} else {
c.record(candidate, outcomeMiss, "")
c.record(candidate, OutcomeMiss, "")
}
return resolution{}, false
}
func (c *chainState) record(candidate, outcome, detail string) {
func (c *chainState) record(candidate string, out Outcome, detail string) {
if c.trace != nil {
c.trace.add(traceStep{Candidate: candidate, Outcome: outcome, Detail: detail})
c.trace.add(traceStep{Candidate: candidate, Outcome: out, Detail: detail})
}
}
@ -153,9 +153,9 @@ func (r *resolver) resolveAlbum(ctx context.Context, albumID string) (resolution
if res, ok = chain.try(pattern, res, ok); ok {
return res, nil
}
case pattern == "external":
case pattern == ExternalCandidate:
if rd, name, isErr := r.fetchExternalAlbum(ctx, *al); rd != nil {
return resolution{reader: rd, source: "external:" + name}, nil
return resolution{reader: rd, source: ExternalPrefix + name}, nil
} else if isErr {
chain.extErr = true
}
@ -165,7 +165,7 @@ func (r *resolver) resolveAlbum(ctx context.Context, albumID string) (resolution
return res, nil
}
default:
chain.record(pattern, outcomeSkipped, "no images in album folder")
chain.record(pattern, OutcomeSkipped, "no images in album folder")
}
}
return chain.exhausted(), nil
@ -220,9 +220,9 @@ func (r *resolver) resolveArtist(ctx context.Context, artistID string) (resoluti
continue
}
switch {
case pattern == "external":
case pattern == ExternalCandidate:
if rd, name, isErr := r.fetchExternalArtist(ctx, *ar); rd != nil {
return resolution{reader: rd, source: "external:" + name}, nil
return resolution{reader: rd, source: ExternalPrefix + name}, nil
} else if isErr {
chain.extErr = true
}
@ -233,7 +233,7 @@ func (r *resolver) resolveArtist(ctx context.Context, artistID string) (resoluti
}
case strings.HasPrefix(pattern, "album/"):
if lib.FS == nil {
chain.record(pattern, outcomeSkipped, "artist has no albums")
chain.record(pattern, OutcomeSkipped, "artist has no albums")
continue
}
res, ok := resolveFolderFile(ctx, lib, imgFiles, strings.TrimPrefix(pattern, "album/"))
@ -242,11 +242,11 @@ func (r *resolver) resolveArtist(ctx context.Context, artistID string) (resoluti
}
default:
if lib.FS == nil {
chain.record(pattern, outcomeSkipped, "artist has no albums")
chain.record(pattern, OutcomeSkipped, "artist has no albums")
continue
}
if artistFolder == "" {
chain.record(pattern, outcomeSkipped, "no artist folder")
chain.record(pattern, OutcomeSkipped, "no artist folder")
continue
}
res, ok := resolveArtistFolderPattern(ctx, lib, artistFolder, pattern)
@ -387,7 +387,7 @@ func (r *resolver) resolveMediaFile(ctx context.Context, id string) (resolution,
func resolveExternalStep(gate gateFunc, name string, sf sourceFunc) (res resolution, ok bool, extErr bool) {
r, path, err := gate(name, sf)
if r != nil {
return resolution{reader: r, source: "external", sourcePath: path}, true, false
return resolution{reader: r, source: ExternalCandidate, sourcePath: path}, true, false
}
return resolution{}, false, err != nil && !errors.Is(err, model.ErrNotFound)
}

View file

@ -8,23 +8,31 @@ import (
"sync"
)
// Outcome is what the priority chain observed for one candidate. The CLI branches on these,
// so they are part of the package's API, not private labels.
type Outcome string
const (
outcomeHit = "hit"
outcomeMiss = "miss"
outcomeUnreadable = "unreadable"
outcomeSkipped = "skipped"
outcomeWouldTry = "would-try"
outcomeError = "error"
outcomeNotReached = "not-reached"
OutcomeHit Outcome = "hit"
OutcomeMiss Outcome = "miss"
OutcomeUnreadable Outcome = "unreadable"
OutcomeSkipped Outcome = "skipped"
OutcomeWouldTry Outcome = "would-try"
OutcomeError Outcome = "error"
outcomeNotReached Outcome = "not-reached"
)
// externalCandidate labels the external tier itself, for the cases that never reach an agent.
const externalCandidate = "external"
const (
// ExternalCandidate labels the external tier itself, for the cases that never reach an agent.
ExternalCandidate = "external"
// ExternalPrefix qualifies a candidate or a stored source with the agent that produced it.
ExternalPrefix = ExternalCandidate + ":"
)
// traceStep is one candidate the priority chain considered.
type traceStep struct {
Candidate string
Outcome string
Outcome Outcome
Detail string
}
@ -67,16 +75,16 @@ var errOfflineSkipped = errors.New("artwork: external lookup skipped (offline)")
func tracingGate(t *chainTrace, inner gateFunc) gateFunc {
return func(name string, f func() (io.ReadCloser, string, error)) (io.ReadCloser, string, error) {
r, path, err := inner(name, f)
candidate := "external:" + name
candidate := ExternalPrefix + name
switch {
case r != nil:
t.add(traceStep{Candidate: candidate, Outcome: outcomeHit, Detail: path})
t.add(traceStep{Candidate: candidate, Outcome: OutcomeHit, Detail: path})
case errors.Is(err, errBreakerOpen):
t.add(traceStep{Candidate: candidate, Outcome: outcomeSkipped, Detail: "circuit breaker open"})
t.add(traceStep{Candidate: candidate, Outcome: OutcomeSkipped, Detail: "circuit breaker open"})
case isTransientExternal(err):
t.add(traceStep{Candidate: candidate, Outcome: outcomeError, Detail: err.Error()})
t.add(traceStep{Candidate: candidate, Outcome: OutcomeError, Detail: err.Error()})
default:
t.add(traceStep{Candidate: candidate, Outcome: outcomeMiss})
t.add(traceStep{Candidate: candidate, Outcome: OutcomeMiss})
}
return r, path, err
}
@ -86,7 +94,7 @@ func tracingGate(t *chainTrace, inner gateFunc) gateFunc {
// command cannot add load to a provider that is already rate-limiting us.
func offlineGate(t *chainTrace) gateFunc {
return func(name string, _ func() (io.ReadCloser, string, error)) (io.ReadCloser, string, error) {
t.add(traceStep{Candidate: "external:" + name, Outcome: outcomeWouldTry})
t.add(traceStep{Candidate: ExternalPrefix + name, Outcome: OutcomeWouldTry})
return nil, "", errOfflineSkipped
}
}

View file

@ -19,6 +19,18 @@ import (
. "github.com/onsi/gomega"
)
var _ = Describe("trace vocabulary", func() {
// The CLI renders these verbatim and branches on them; a value change is a change to
// what `artwork explain` tells an operator, so it must be made deliberately.
It("pins the wire values the CLI reads", func() {
Expect([]Outcome{
OutcomeHit, OutcomeMiss, OutcomeUnreadable, OutcomeSkipped, OutcomeWouldTry, OutcomeError,
}).To(Equal([]Outcome{"hit", "miss", "unreadable", "skipped", "would-try", "error"}))
Expect(ExternalCandidate).To(Equal("external"))
Expect(ExternalPrefix).To(Equal("external:"))
})
})
var _ = Describe("chainTrace", func() {
It("returns nil when no trace is attached", func() {
Expect(traceFrom(context.Background())).To(BeNil())
@ -28,12 +40,12 @@ var _ = Describe("chainTrace", func() {
t := &chainTrace{}
ctx := withTrace(context.Background(), t)
traceFrom(ctx).add(traceStep{Candidate: "cover.*", Outcome: outcomeMiss})
traceFrom(ctx).add(traceStep{Candidate: "embedded", Outcome: outcomeHit, Detail: "/music/a.flac"})
traceFrom(ctx).add(traceStep{Candidate: "cover.*", Outcome: OutcomeMiss})
traceFrom(ctx).add(traceStep{Candidate: "embedded", Outcome: OutcomeHit, Detail: "/music/a.flac"})
Expect(t.Steps()).To(Equal([]traceStep{
{Candidate: "cover.*", Outcome: outcomeMiss},
{Candidate: "embedded", Outcome: outcomeHit, Detail: "/music/a.flac"},
{Candidate: "cover.*", Outcome: OutcomeMiss},
{Candidate: "embedded", Outcome: OutcomeHit, Detail: "/music/a.flac"},
}))
s := t.Steps()
@ -43,7 +55,7 @@ var _ = Describe("chainTrace", func() {
It("does not panic when the trace is nil", func() {
var t *chainTrace
Expect(func() { t.add(traceStep{Candidate: "cover.*", Outcome: outcomeMiss}) }).ToNot(Panic())
Expect(func() { t.add(traceStep{Candidate: "cover.*", Outcome: OutcomeMiss}) }).ToNot(Panic())
})
It("is safe to use concurrently", func() {
@ -52,7 +64,7 @@ var _ = Describe("chainTrace", func() {
for range 10 {
go func() {
defer GinkgoRecover()
t.add(traceStep{Candidate: "x", Outcome: outcomeMiss})
t.add(traceStep{Candidate: "x", Outcome: OutcomeMiss})
done <- struct{}{}
}()
}
@ -71,7 +83,7 @@ var _ = Describe("chainState tracing", func() {
_, ok := c.try("cover.*", resolution{}, false)
Expect(ok).To(BeFalse())
Expect(t.Steps()).To(Equal([]traceStep{{Candidate: "cover.*", Outcome: outcomeMiss}}))
Expect(t.Steps()).To(Equal([]traceStep{{Candidate: "cover.*", Outcome: OutcomeMiss}}))
})
It("records unreadable when the candidate existed but could not be read", func() {
@ -82,7 +94,7 @@ var _ = Describe("chainState tracing", func() {
Expect(ok).To(BeFalse())
Expect(t.Steps()).To(HaveLen(1))
Expect(t.Steps()[0].Outcome).To(Equal(outcomeUnreadable),
Expect(t.Steps()[0].Outcome).To(Equal(OutcomeUnreadable),
"a candidate that existed and failed to decode must be distinguishable from one that was absent")
})
@ -95,7 +107,7 @@ var _ = Describe("chainState tracing", func() {
Expect(ok).To(BeTrue())
Expect(res.source).To(Equal("embedded"))
Expect(t.Steps()).To(Equal([]traceStep{
{Candidate: "embedded", Outcome: outcomeHit, Detail: "/music/a.flac"},
{Candidate: "embedded", Outcome: OutcomeHit, Detail: "/music/a.flac"},
}))
})
@ -121,28 +133,28 @@ var _ = Describe("external gate tracing", func() {
Expect(err).ToNot(HaveOccurred())
Expect(r).ToNot(BeNil())
Expect(t.Steps()).To(Equal([]traceStep{
{Candidate: "external:deezer", Outcome: outcomeHit, Detail: "http://img"},
{Candidate: "external:deezer", Outcome: OutcomeHit, Detail: "http://img"},
}))
})
It("records a miss for a not-found", func() {
t := &chainTrace{}
_, _, _ = tracingGate(t, passthroughGate)("deezer", miss)
Expect(t.Steps()[0].Outcome).To(Equal(outcomeMiss))
Expect(t.Steps()[0].Outcome).To(Equal(OutcomeMiss))
})
It("records a miss for a model not-found", func() {
t := &chainTrace{}
notFound := func() (io.ReadCloser, string, error) { return nil, "", model.ErrNotFound }
_, _, _ = tracingGate(t, passthroughGate)("deezer", notFound)
Expect(t.Steps()[0].Outcome).To(Equal(outcomeMiss),
Expect(t.Steps()[0].Outcome).To(Equal(OutcomeMiss),
"both not-found flavours are definitive answers, not faults")
})
It("records an error with its reason", func() {
t := &chainTrace{}
_, _, _ = tracingGate(t, passthroughGate)("apple-music", boom)
Expect(t.Steps()[0].Outcome).To(Equal(outcomeError))
Expect(t.Steps()[0].Outcome).To(Equal(OutcomeError))
Expect(t.Steps()[0].Detail).To(ContainSubstring("429"))
})
@ -152,7 +164,7 @@ var _ = Describe("external gate tracing", func() {
return nil, "", errBreakerOpen
}
_, _, _ = tracingGate(t, open)("apple-music", hit)
Expect(t.Steps()[0].Outcome).To(Equal(outcomeSkipped))
Expect(t.Steps()[0].Outcome).To(Equal(OutcomeSkipped))
Expect(t.Steps()[0].Detail).To(ContainSubstring("circuit breaker"))
})
@ -169,7 +181,7 @@ var _ = Describe("external gate tracing", func() {
Expect(called).To(BeFalse(), "offline mode must not perform external requests")
Expect(err).To(MatchError(errOfflineSkipped))
Expect(t.Steps()).To(Equal([]traceStep{
{Candidate: "external:deezer", Outcome: outcomeWouldTry},
{Candidate: "external:deezer", Outcome: OutcomeWouldTry},
}))
})
})
@ -216,10 +228,10 @@ var _ = Describe("resolveAlbum tracing", func() {
defer res.reader.Close()
Expect(t.Steps()).To(HaveLen(2), "a configured pattern must appear even when the chain never evaluated it")
Expect(t.Steps()[0]).To(Equal(traceStep{
Candidate: "cover.jpg", Outcome: outcomeSkipped, Detail: "no images in album folder",
Candidate: "cover.jpg", Outcome: OutcomeSkipped, Detail: "no images in album folder",
}))
Expect(t.Steps()[1].Candidate).To(Equal("embedded"))
Expect(t.Steps()[1].Outcome).To(Equal(outcomeHit))
Expect(t.Steps()[1].Outcome).To(Equal(OutcomeHit))
})
It("records an evaluated pattern that matched nothing as a miss, not a skip", func() {
@ -235,7 +247,7 @@ var _ = Describe("resolveAlbum tracing", func() {
Expect(err).ToNot(HaveOccurred())
Expect(res.reader).ToNot(BeNil())
defer res.reader.Close()
Expect(t.Steps()[0]).To(Equal(traceStep{Candidate: "cover.jpg", Outcome: outcomeMiss}),
Expect(t.Steps()[0]).To(Equal(traceStep{Candidate: "cover.jpg", Outcome: OutcomeMiss}),
"the folder was searched and held no cover.jpg, which is not the same as never looking")
})
@ -246,7 +258,7 @@ var _ = Describe("resolveAlbum tracing", func() {
_, err := newResolver(ds, ag, ffm, nil).resolve(ctx, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al2"})
Expect(err).ToNot(HaveOccurred())
Expect(t.Steps()).To(Equal([]traceStep{
{Candidate: "cover.jpg", Outcome: outcomeSkipped, Detail: "no images in album folder"},
{Candidate: "cover.jpg", Outcome: OutcomeSkipped, Detail: "no images in album folder"},
}))
})
})
@ -303,7 +315,7 @@ var _ = Describe("resolveArtist tracing", func() {
Expect(err).ToNot(HaveOccurred())
Expect(res.reader).ToNot(BeNil())
defer res.reader.Close()
Expect(t.Steps()).To(Equal([]traceStep{{Candidate: "upload", Outcome: outcomeHit, Detail: path}}))
Expect(t.Steps()).To(Equal([]traceStep{{Candidate: "upload", Outcome: OutcomeHit, Detail: path}}))
})
It("records an upload miss before walking the chain", func() {
@ -311,7 +323,7 @@ var _ = Describe("resolveArtist tracing", func() {
_, err := newResolver(ds, ag, ffm, nil).resolve(ctx, model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar2"})
Expect(err).ToNot(HaveOccurred())
Expect(t.Steps()[0]).To(Equal(traceStep{Candidate: "upload", Outcome: outcomeMiss}))
Expect(t.Steps()[0]).To(Equal(traceStep{Candidate: "upload", Outcome: OutcomeMiss}))
})
It("labels each step with the configured priority token", func() {
@ -330,7 +342,7 @@ var _ = Describe("resolveArtist tracing", func() {
Expect(t.Steps()).To(HaveLen(2))
Expect(t.Steps()[1].Candidate).To(Equal("album/artist.*"),
"the step must be labelled with the priority token, not the pattern it was rewritten into")
Expect(t.Steps()[1].Outcome).To(Equal(outcomeHit))
Expect(t.Steps()[1].Outcome).To(Equal(OutcomeHit))
Expect(filepath.ToSlash(t.Steps()[1].Detail)).To(HaveSuffix("tests/fixtures/artist/an-album/artist.png"))
})
@ -340,8 +352,8 @@ var _ = Describe("resolveArtist tracing", func() {
_, err := newResolver(ds, ag, ffm, nil).resolve(ctx, model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar5"})
Expect(err).ToNot(HaveOccurred())
Expect(t.Steps()).To(Equal([]traceStep{
{Candidate: "upload", Outcome: outcomeMiss},
{Candidate: "album/artist.*", Outcome: outcomeSkipped, Detail: "artist has no albums"},
{Candidate: "upload", Outcome: OutcomeMiss},
{Candidate: "album/artist.*", Outcome: OutcomeSkipped, Detail: "artist has no albums"},
}), "a configured pattern that was never evaluated must still appear, and say why")
})
@ -353,8 +365,8 @@ var _ = Describe("resolveArtist tracing", func() {
_, err := newResolver(ds, ag, ffm, nil).resolve(ctx, model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar6"})
Expect(err).ToNot(HaveOccurred())
Expect(t.Steps()).To(Equal([]traceStep{
{Candidate: "upload", Outcome: outcomeMiss},
{Candidate: "artist.*", Outcome: outcomeSkipped, Detail: "no artist folder"},
{Candidate: "upload", Outcome: OutcomeMiss},
{Candidate: "artist.*", Outcome: OutcomeSkipped, Detail: "no artist folder"},
}))
})
@ -370,7 +382,7 @@ var _ = Describe("resolveArtist tracing", func() {
_, err := newResolver(ds, ag, ffm, nil).resolve(ctx, model.ArtworkQueueItem{ItemKind: "ar", ItemID: "ar3"})
Expect(err).ToNot(HaveOccurred())
Expect(t.Steps()).To(HaveLen(1))
Expect(t.Steps()[0].Outcome).To(Equal(outcomeUnreadable),
Expect(t.Steps()[0].Outcome).To(Equal(OutcomeUnreadable),
"an upload that exists and will not open must not look like an absent upload")
})
})
@ -433,7 +445,7 @@ var _ = Describe("NewTracingResolver", func() {
Expect(err).ToNot(HaveOccurred())
Expect(source).To(Equal("embedded"))
Expect(fake.albumCalls).To(BeZero(), "offline mode must not add load to an external provider")
Expect(t.Steps()).To(ContainElement(TraceStep{Candidate: "external:offline-probe", Outcome: outcomeWouldTry}))
Expect(t.Steps()).To(ContainElement(TraceStep{Candidate: "external:offline-probe", Outcome: OutcomeWouldTry}))
})
It("records the local chain steps too", func() {
@ -442,7 +454,7 @@ var _ = Describe("NewTracingResolver", func() {
Expect(err).ToNot(HaveOccurred())
last := t.Steps()[len(t.Steps())-1]
Expect(last.Candidate).To(Equal("embedded"), "the local chain must be traced, not just the external gate")
Expect(last.Outcome).To(Equal(outcomeHit))
Expect(last.Outcome).To(Equal(OutcomeHit))
})
It("never persists artwork state", func() {
@ -460,7 +472,7 @@ var _ = Describe("NewTracingResolver", func() {
Expect(err).ToNot(HaveOccurred())
Expect(source).To(BeEmpty())
Expect(fake.artistCalls).To(BeZero())
Expect(t.Steps()).To(ContainElement(TraceStep{Candidate: "external:offline-probe", Outcome: outcomeWouldTry}))
Expect(t.Steps()).To(ContainElement(TraceStep{Candidate: "external:offline-probe", Outcome: OutcomeWouldTry}))
Expect(artworkRepo.ItemData).To(BeEmpty())
Expect(queueRepo.Data).To(BeEmpty())
})
@ -495,6 +507,6 @@ var _ = Describe("NewTracingResolver", func() {
Expect(err).ToNot(HaveOccurred())
Expect(fake.albumCalls).To(Equal(1))
Expect(t.Steps()).To(ContainElement(TraceStep{Candidate: "external:live-probe", Outcome: outcomeMiss}))
Expect(t.Steps()).To(ContainElement(TraceStep{Candidate: "external:live-probe", Outcome: OutcomeMiss}))
})
})