fix(wfengine): wire real metadata enrichment refetch (was silent no-op in all deployments) (bookshelf-eapl) #355
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-eapl"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
build_extended_deps.go: replacedext.Enrich = nilwith a newbuildEnrichDeps()that constructsbooks.RefetchMetadatawith all four real providers (Google Books, Open Library, Hardcover, ComicVine), their rate limiters, persist deps, and rating setters — mirroring exactly howbooks.WirewiresrefetchMeta. This fixes BOTH the embedded-app path AND the standalone worker binary path (both callbuildExtendedDeps). Removed the stale "wired per-deployment in worker binary" comment.engine.go: addedlogger.Warn(...)at the three no-op registration sites (scan without cover pipeline, bulk_covers without cover pipeline, bulk_enrich without enrich pipeline) so a missing real dep produces a loud startup warning instead of silently eating work forever.e2e/api/enrich_workflow_test.go: regression proof — starts a WFEngine-enabled app with a mock Google Books server, triggersEnrichWorkflowfor a seeded book, and assertsbook_metadata.titleis updated to the mock response value. This test times out (fails) on origin/main wherenoopRefetchis registered.Test plan
go build ./...— compiles cleanmake test— all unit tests passmake coverage— coverage gate at 100% (check-coverage: OK)golangci-lint run ./internal/app/... ./internal/wfengine/...— 0 issues in changed filesenrich_workflow_test.goexercises EnrichWorkflow end-to-end with a mocked providerCloses bead bookshelf-eapl on merge.
Security Review — PR #355 (bd-bookshelf-eapl)
Scope: wires real metadata enrichment (Google Books, Open Library, Hardcover, ComicVine) into the wfengine
EnrichWorkflowactivity viabuildEnrichDepsininternal/app/build_extended_deps.go.Findings
No blockers or majors found. Three minors:
[MINOR] internal/app/build_extended_deps.go —
buildEnrichDepscreates separate rate-limiter instances from the HTTP-path (books.Wire)buildEnrichDepscallsratelimiter.New(cfg.MetadataGoogleBooksRateLimit)etc., which are independent token buckets from those created inbooks.Wire. When both the HTTP refetch path (triggered by a user pressing "Refetch" in the UI) and the worker EnrichWorkflow activity run concurrently, they each hold their own limiter — the combined outbound rate can double up to 2×cfg rate before the providers see a 429. This is not a DoS vector (providers will 429 and the existing retry+backoff handles it), but it is a mild correctness issue with the rate-limiting intent. No fix required to merge; worth a follow-up bead to share a single application-scoped limiter per provider if provider bans become a practical concern.[MINOR] internal/wfengine/engine.go:358 —
logger.Warnat scan registration usesloggernote.loggerThe three new Warn lines at the no-op registration guards are
logger.Warn(...)(line 358) vse.logger.Warn(...)(lines 390, 416). Both refer to the same*slog.Loggerpassed in and stored one— they behave identically at runtime. But the asymmetry is a readability nit: callers reading the code would expect the engine to use its own logger field after it has been assigned. No security impact.[MINOR] e2e/api/enrich_workflow_test.go — polling loop uses bare
time.Sleepinstead ofEventuallyThe 60-second DB-poll loop at the end of the e2e test is hand-rolled (
for time.Now().Before(deadline)). Existing browser tests in this repo use GomegaEventuallyfor similar polling, which gives better failure messages and integrates with Go testing context cancellation. No security impact; test-hygiene nit.Checklist per security focus areas
ratelimiter.New(cfg.Metadata*RateLimit)called for all 4 providers; same defaults (1s GB, 350ms OL, 1s HC, 1s CV) as the HTTP path&http.Client{Timeout: 30 * time.Second}— identical tobooks.Wirekey=fromurl.ErrorviaredactKey(). Hardcover never logs the Authorization header. ComicVine redactsapi_key=fromurl.ErrorviaredactURLErrors(). New Warn lines inengine.gocontain only static strings.q.UpsertBookMetadata, etc.) — the samebuildPersistMetaDepsForWFused by the HTTP refetch path. No raw SQL construction from provider strings.BulkEnrichWorkflowpassesfanOutConcurrency(fromext.FanOutConcurrency, default 4) intoBoundedFanOutEnrichInput, which implements a proper sliding-window limiter. No unbounded scheduling.REVIEW VERDICT: 0 blocker, 0 major, 3 minor
CODE REVIEW: PR #355 — Wire real enrich deps into wfengine
Phase 0: DEMO Verification
No DEMO block in this bead — the bead description specifies an e2e regression proof test rather than a CLI command. The regression test (
enrich_workflow_test.go) is the proof artifact. CI is green per the implementer's completion comment, and the review-standard permits this for a test-driven fix. Proceeding.Phase 1: Spec Compliance
All requirements from the bead met:
ext.Enrich = buildEnrichDeps(...)replaces the hardcodednilatinternal/app/build_extended_deps.go:201buildEnrichDepswiresbooks.RefetchMetadatawith all 4 real providerslogger.Warnadditions at the no-op registration sites inengine.goPhase 2: Code Quality
Provider parity check (the key focus of this review)
buildEnrichDepsvsbooks.Wire(internal/books/wire.go:83-247):[google-books, open-library, hardcover, comicvine]— matches exactlyMetadataGoogleBooksRateLimit, etc.)comicvine.NewHourlyLimiter()for ComicVinegetAPIKey:settings.GetAPIKey(q.GetAppSetting)— identical tod.GetProviderAPIKeywiring inapp.go:198isProviderEnabled:settings.IsProviderEnabled(q.GetAppSetting)— identical tod.IsProviderEnabledwiring inapp.go:199resolveRating/getRatingsForResolution: inlined vs named-var in wire.go but functionally identicalNo behavioral divergence found between the WF path and the HTTP refetch path.
PersistComicomission inbuildPersistMetaDepsForWF: the HTTP path setsPersistComicviabuildPersistComicForQueries. The WF path omits it. This is safe:PersistMetadata(used byRefetchMetadata) always callspersistInTx(comicReq=nil), soPersistComicis never invoked in the refetch code path — only inSaveMetadata(manual save).metadata_store.go:542guards withif comicReq != nil && deps.PersistComic != nil. Not a defect.Reachability: both embedded app and worker binary
internal/app/app.go:234→buildExtendedDeps(...)→ext.Enrich = buildEnrichDeps(...)cmd/pergamum/worker.go:130→app.BuildExtendedDepsForWorker(...)→buildExtendedDeps(...)→ext.Enrich = buildEnrichDeps(...)Both paths now get the real enrich pipeline.
logger.Warnplacement: all three warnings are guarded by their respective nil-depifbranches (if generateCover == nil,if e.startCoverWorkflow == nil,if ext.Enrich == nil). They fire only when a dep is genuinely missing, not on the normal wired path. The first useslogger(function param innewWithFactory) and the latter two usee.logger(inregisterExtendedWorkflows) — both correct for their call context.Findings
[MINOR] internal/app/build_extended_deps.go:242 —
buildEnrichDepsexceeds function-length conventionbuildEnrichDepsis ~104 lines. Project convention is < 30 lines. This is a wiring function (likebuildBulkMetaDepsin the same file at ~40 lines) so the impact is cognitive rather than correctness-related. Could be split intobuildProviders(...)andbuildRatingSetters(...)helpers. Not blocking.[MINOR] e2e/api/enrich_workflow_test.go:133-150 —
Itblock contains twoExpectcalls + raw polling loopThe
Itblock asserts bothresp.StatusCode(line 135) andgot(line 150) — twoExpectcalls where convention requires one perIt. The async assertion uses a manualfor time.Now().Before(deadline)+time.Sleeploop rather than Gomega'sEventually(func() string {...}).Within(60*time.Second).Should(Equal("Enriched By Workflow")). The poll loop pattern is functional (and there are no other async e2e API tests to establish a preferred pattern) but diverges from the Gomega idiom and frome2e/browser/tests that useEventually. Not blocking for a regression proof test.REVIEW VERDICT: 0 blocker, 0 major, 2 minor
60e7d8f1e4466c00aad1