feat(wfengine): bulk-ops by-IDs parent workflows (bookshelf-htpc.1) #828

Merged
zombor merged 3 commits from bd-bookshelf-htpc.1 into main 2026-06-29 11:24:09 +00:00
Owner

Summary

  • Fixes bounded fan-out HARD RULE violation: BulkEnrichHandler / BulkGenerateCoversHandler / BulkCustomFetchHandler were spawning one top-level workflow per selected book (N workflows for N books). At 4k selected books this floods the go-workflows queue.
  • Adds BulkEnrichByIDsWorkflow and BulkCoversGenerateByIDsWorkflow: single parent workflows that ContinueAsNew-paginate the ID list (batch 50) and fan out per-book sub-workflows under a bounded single-digit concurrency window.
  • Replaces the old per-book dispatcher functions in internal/books/trigger_dispatch.go with three new single-workflow dispatchers wired through WFTriggerDeps.

Test plan

  • Workflow tester unit tests: empty list, single batch, multi-epoch ContinueAsNew (10 IDs ÷ batch 3 = 4 epochs, asserts 10 sub-workflow fan-outs), cancel mid fan-out
  • Engine.Start* unit tests: not-registered error, success, error propagation
  • NewWithFactoryExt unit tests: exercises real closure bodies including Concurrency default path
  • Integration tests (real MySQL): no activity-not-found registration tests for both workflows
  • Integration test: >=3 real ContinueAsNew epochs, detail.Result == "null", Refetch called exactly 10 times
  • make test passes
  • make coverage passes (100% on internal/wfengine and internal/books)

Closes bead bookshelf-htpc.1 on merge.

## Summary - Fixes bounded fan-out HARD RULE violation: `BulkEnrichHandler` / `BulkGenerateCoversHandler` / `BulkCustomFetchHandler` were spawning one top-level workflow per selected book (N workflows for N books). At 4k selected books this floods the go-workflows queue. - Adds `BulkEnrichByIDsWorkflow` and `BulkCoversGenerateByIDsWorkflow`: single parent workflows that `ContinueAsNew`-paginate the ID list (batch 50) and fan out per-book sub-workflows under a bounded single-digit concurrency window. - Replaces the old per-book dispatcher functions in `internal/books/trigger_dispatch.go` with three new single-workflow dispatchers wired through `WFTriggerDeps`. ## Test plan - [x] Workflow tester unit tests: empty list, single batch, multi-epoch ContinueAsNew (10 IDs ÷ batch 3 = 4 epochs, asserts 10 sub-workflow fan-outs), cancel mid fan-out - [x] Engine.Start* unit tests: not-registered error, success, error propagation - [x] NewWithFactoryExt unit tests: exercises real closure bodies including Concurrency default path - [x] Integration tests (real MySQL): no activity-not-found registration tests for both workflows - [x] Integration test: >=3 real ContinueAsNew epochs, `detail.Result == "null"`, `Refetch` called exactly 10 times - [x] `make test` passes - [x] `make coverage` passes (100% on `internal/wfengine` and `internal/books`) Closes bead bookshelf-htpc.1 on merge.
feat(wfengine): bulk-ops by-IDs parent workflows to fix bounded fan-out violation (bookshelf-htpc.1)
Some checks failed
/ JS Unit Tests (pull_request) Successful in 1m3s
/ E2E API (pull_request) Successful in 1m35s
/ Lint (pull_request) Successful in 3m3s
/ Integration (pull_request) Failing after 3m43s
/ E2E Browser (pull_request) Successful in 3m32s
/ Test (pull_request) Successful in 4m48s
17d2a3b9ac
Replace the per-book workflow dispatch loop in BulkEnrichHandler /
BulkGenerateCoversHandler / BulkCustomFetchHandler with single parent
workflows that ContinueAsNew-paginate the ID list and fan out per-book
sub-workflows under a configurable single-digit concurrency window.

- Add BulkEnrichByIDsWorkflow and BulkCoversGenerateByIDsWorkflow with
  ContinueAsNew pagination (batch size 50, var overrideable for tests)
- Wire startBulkEnrichByIDsWorkflow / startBulkCoversGenerateByIDsWorkflow
  into Engine via registerExtendedWorkflows (metadata / covers queues)
- Add Engine.StartBulkEnrichByIDsWorkflow / StartBulkCoversGenerateByIDsWorkflow
  public methods
- Add StartBulkEnrichByIDs / StartBulkCoversGenerateByIDs to WFTriggerDeps
  and wire them in build_extended_deps.go
- Replace old per-book dispatcher funcs with single-workflow dispatchers:
  EnqueueBulkEnrichByIDsDispatcher, BulkEnqueueEnrichByIDsWithOptionsDispatcher,
  BulkCoversGenerateByIDsDispatcher
- Update BulkEnrichHandler / BulkCustomFetchHandler / BulkGenerateCoversHandler
  to use new dispatchers via wire.go
- Unit tests: workflow tester (empty, single batch, multi-epoch ContinueAsNew,
  cancel) + Engine.Start* unit tests + NewWithFactoryExt closure coverage
- Integration tests: real-engine registration (no activity-not-found) +
  multi-epoch ContinueAsNew with >=3 epochs asserting null result

Closes bead bookshelf-htpc.1 on merge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)

recompute-match-score-kebab-open

**Recompute Match Score — kebab open screenshot** (recompute-match-score-kebab-open) ![recompute-match-score-kebab-open](/attachments/8dd39e17-524a-4ba0-899f-4696c9244072)

Workflow Detail page screenshot (wf-detail-older-execution)

Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.

wf-detail-older-execution

**Workflow Detail page screenshot** (wf-detail-older-execution) Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent. ![wf-detail-older-execution](/attachments/df40839c-9a96-488f-855c-6cf2afb8178b)
fix(wfengine): move Refetch stub to EnrichDeps in integration tests
All checks were successful
/ JS Unit Tests (pull_request) Successful in 45s
/ Lint (pull_request) Successful in 2m20s
/ E2E API (pull_request) Successful in 2m29s
/ E2E Browser (pull_request) Successful in 3m8s
/ Integration (pull_request) Successful in 3m28s
/ Test (pull_request) Successful in 3m59s
b36df71997
BulkEnrichDeps only has ListBooksPage and GetEnabledProviders; the
Refetch func lives in EnrichDeps. The integration tests were incorrectly
setting Refetch on BulkEnrichDeps causing a compile error in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Workflow Detail page screenshot (wf-detail-older-execution)

Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.

wf-detail-older-execution

**Workflow Detail page screenshot** (wf-detail-older-execution) Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent. ![wf-detail-older-execution](/attachments/a7733750-6aac-4f09-abe5-ff510b5fbc40)

Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)

recompute-match-score-kebab-open

**Recompute Match Score — kebab open screenshot** (recompute-match-score-kebab-open) ![recompute-match-score-kebab-open](/attachments/5370bf8b-3f12-4a11-a8ca-57d45016a4ab)
Author
Owner

Security Review — bookshelf-htpc.1 (PR #828)

Reviewer: security-reviewer subagent (claude-sonnet-4-6)
Diff reviewed: origin/main...origin/bd-bookshelf-htpc.1
CI: green (not re-run — CI is source of behavioral truth)


Multi-user scoping

All three bulk handlers (BulkEnrichHandler, BulkCustomFetchHandler, BulkGenerateCoversHandler) delegate to bulkBookSetHandler which:

  1. Extracts userID via userIDFromRequest(r) — sourced from d.ExtractUser(r).ID which reads JWT claims set by AuthMiddleware, never from the request body.
  2. Calls getUserLibraryIDs(ctx, userID) scoped to that session user.
  3. Calls filterOwnedIDs(ctx, req.BookIDs, userLibraryIDs) before invoking enqueue.

Only the filtered, ownership-verified IDs are forwarded into the new single-parent workflow. The workflow itself never has access to unfiltered IDs. Scoping is correct and unchanged by this refactor.

MaxBulkBookIDs cap

MaxBulkBookIDs = 1000 is enforced in every handler before enqueue is called:

  • internal/books/bulk_handler.go:77 (enrich + covers)
  • internal/books/bulk_custom_fetch_handler.go:49 (custom-fetch)

The workflow boundary adds no second cap (none needed — the HTTP boundary is the validated entry point). DoS risk from unbounded input is mitigated.

optsJSON deserialization

MetadataRefreshOptions contains only string, bool, and composite struct fields. The round-trip json.Marshal(opts) → optsJSON in trigger_dispatch.go and json.Unmarshal(optsJSON, &o) in build_extended_deps.go:204 is typed and safe. No interface{} / any, no URL fields susceptible to SSRF, no callable fields. No injection or unsafe deserialization risk.

Architecture boundary

internal/wfengine/bulk_by_ids_workflow.go imports internal/books (for *books.MetadataRefreshOptions). The domain package internal/books does not import go-workflows or any workflow-engine symbol. Boundary rule is satisfied.

Secrets / PII logging

Both trigger_dispatch.go and engine.go log only instance_id and book_count. No book IDs, no options content, no tokens are emitted to the log. Clean.

SSRF

No new outbound URL fetching is introduced. The cover/enrich pipeline is unchanged; only the fan-out dispatch path is refactored. No new SSRF surface.

Concurrency cap

  • Covers path: clampCovers() enforces 1–9 range; falls back to defaultFanOutConcurrency (4) for zero/out-of-range values.
  • Enrich path: metadataWindow() falls back to defaultFanOutConcurrency (4) for zero. (Note: metadataWindow has no upper-bound clamp, but this is pre-existing behaviour shared with BulkEnrichWorkflow and is not introduced by this PR.)
  • Within the workflow Concurrency < 1 also falls back to defaultFanOutConcurrency. Fan-out HARD RULE is satisfied for this PR's additions.

No findings.

REVIEW VERDICT: 0 blocker, 0 major, 0 minor

## Security Review — bookshelf-htpc.1 (PR #828) Reviewer: security-reviewer subagent (claude-sonnet-4-6) Diff reviewed: `origin/main...origin/bd-bookshelf-htpc.1` CI: green (not re-run — CI is source of behavioral truth) --- ### Multi-user scoping All three bulk handlers (`BulkEnrichHandler`, `BulkCustomFetchHandler`, `BulkGenerateCoversHandler`) delegate to `bulkBookSetHandler` which: 1. Extracts `userID` via `userIDFromRequest(r)` — sourced from `d.ExtractUser(r).ID` which reads JWT claims set by `AuthMiddleware`, never from the request body. 2. Calls `getUserLibraryIDs(ctx, userID)` scoped to that session user. 3. Calls `filterOwnedIDs(ctx, req.BookIDs, userLibraryIDs)` before invoking `enqueue`. Only the filtered, ownership-verified IDs are forwarded into the new single-parent workflow. The workflow itself never has access to unfiltered IDs. **Scoping is correct and unchanged by this refactor.** ### MaxBulkBookIDs cap `MaxBulkBookIDs = 1000` is enforced in every handler before `enqueue` is called: - `internal/books/bulk_handler.go:77` (enrich + covers) - `internal/books/bulk_custom_fetch_handler.go:49` (custom-fetch) The workflow boundary adds no second cap (none needed — the HTTP boundary is the validated entry point). **DoS risk from unbounded input is mitigated.** ### optsJSON deserialization `MetadataRefreshOptions` contains only `string`, `bool`, and composite struct fields. The round-trip `json.Marshal(opts) → optsJSON` in `trigger_dispatch.go` and `json.Unmarshal(optsJSON, &o)` in `build_extended_deps.go:204` is typed and safe. No `interface{}` / `any`, no URL fields susceptible to SSRF, no callable fields. **No injection or unsafe deserialization risk.** ### Architecture boundary `internal/wfengine/bulk_by_ids_workflow.go` imports `internal/books` (for `*books.MetadataRefreshOptions`). The domain package `internal/books` does not import `go-workflows` or any workflow-engine symbol. **Boundary rule is satisfied.** ### Secrets / PII logging Both `trigger_dispatch.go` and `engine.go` log only `instance_id` and `book_count`. No book IDs, no options content, no tokens are emitted to the log. **Clean.** ### SSRF No new outbound URL fetching is introduced. The cover/enrich pipeline is unchanged; only the fan-out dispatch path is refactored. **No new SSRF surface.** ### Concurrency cap - Covers path: `clampCovers()` enforces 1–9 range; falls back to `defaultFanOutConcurrency` (4) for zero/out-of-range values. - Enrich path: `metadataWindow()` falls back to `defaultFanOutConcurrency` (4) for zero. (Note: `metadataWindow` has no upper-bound clamp, but this is pre-existing behaviour shared with `BulkEnrichWorkflow` and is not introduced by this PR.) - Within the workflow `Concurrency < 1` also falls back to `defaultFanOutConcurrency`. **Fan-out HARD RULE is satisfied for this PR's additions.** --- No findings. REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Author
Owner

Code Review — bookshelf-htpc.1 (PR #828)

Reviewer: code-reviewer subagent (claude-sonnet-4-6)
Diff reviewed: origin/main...origin/bd-bookshelf-htpc.1
CI: green (not re-run — CI is source of behavioral truth)


Phase 0: DEMO Verification

No explicit DEMO block in the bead or its comments. This is a pure internal workflow refactor with no user-visible endpoint output to demo. CI is green with comprehensive integration coverage. Proceeding on that basis.


Phase 1: Spec Compliance

All spec requirements met:

  • BulkEnrichHandlerBulkEnrichByIDsWorkflow (one parent instance) ✓
  • BulkCustomFetchHandlerBulkEnrichByIDsWorkflow with optsJSON
  • BulkGenerateCoversHandlerBulkCoversGenerateByIDsWorkflow (one parent instance) ✓
  • ContinueAsNew pagination with bulkByIDsBatchSize = 50, separate from concurrency ✓
  • Bounded single-digit fan-out (defaultFanOutConcurrency = 4, covers clamped ≤9, metadata configurable) ✓
  • User-ID scoping: filterOwnedBookIDs + getUserLibraryIDs + userIDFromRequest all preserved unchanged ✓
  • MaxBulkBookIDs cap: unchanged handler code, enforced before dispatch ✓
  • Architecture boundary: optsJSON []byte crosses the domain→adapter boundary (no wfengine type in internal/books) ✓

Phase 2: Code Quality

[MAJOR] internal/wfengine/engine_integration_test.goBulkCoversGenerateByIDsWorkflow has no multi-epoch real-engine integration test
The workflow-integration-test-must-assert-success rule requires ≥3 real ContinueAsNew epochs on the real engine, asserting the terminal Result value (not just .state == "completed"). BulkCoversGenerateByIDsWorkflow has only:

  • A single-book registration test (one epoch, no ContinueAsNew triggered, lines 1081–1128)
  • A multi-epoch unit test in the workflow tester (not the real MySQL engine)
    BulkEnrichByIDsWorkflow (same PR) correctly has a full multi-epoch real-engine test (10 IDs / batch 3 = 4 epochs, asserts detail.Result == "null" AND refetchCalls == numIDs). The same rapid-ContinueAsNew WorkflowExecutionStarted race (#583) that motivated that test applies equally to BulkCoversGenerateByIDsWorkflow. The fix is small: add a test mirroring the enrich multi-epoch test with WithBulkByIDsBatchSize(3), a generateCover call counter via sync/atomic, and assertions on detail.Result == "null" + coverCalls == numIDs.

Positive observations

  • Sliding-window limiter is correct: boundedFanOut seeds up to concurrency children, then awaits the oldest (FIFO) before scheduling the next — genuine schedule-N/await-one-before-next, not 50-at-once.
  • Batch size and concurrency are truly separate knobs: bulkByIDsBatchSize = 50 (epoch page size) vs defaultFanOutConcurrency = 4 / coversWindow / metadataWindow (per-epoch concurrent children). Clean separation.
  • Configurable concurrency path is complete: startBulkEnrichByIDsWorkflow closure sets input.Concurrency = metadataWindow before creating the workflow; startBulkCoversGenerateByIDsWorkflow sets input.Concurrency = coversWindow. Carried through ContinueAsNew. clampCovers() enforces ≤9 hard cap at the engine boundary.
  • Architecture boundary clean: internal/wfengine imports internal/books (adapter importing domain — allowed). internal/books/trigger_dispatch.go carries optsJSON []byte (not a wfengine type). Confirmed internal/books has no go-workflows import.
  • Determinism: no map-range over ID slice, no time.Now inside workflow coroutines.
  • Multi-epoch BulkEnrichByIDsWorkflow integration test is thorough: drives 4 real ContinueAsNew epochs, asserts detail.Result == "null" (the SUCCESS value — catches errored-but-marked-completed), asserts atomic.LoadInt64(&refetchCalls) == numIDs (all IDs processed across all epochs). Uses sync/atomic for the shared counter (race-safe). This is exactly what the rule requires.
  • Real-engine registration tests use wfengine.New() + StartWorker(): the production constructor + worker startup, catching queue/activity-registry mismatches invisible to the in-memory tester.
  • Multi-user scoping: all three handlers retain filterOwnedBookIDs + getUserLibraryIDs + userIDFromRequest. The workflow only ever receives ownership-verified IDs.

REVIEW VERDICT: 0 blocker, 1 major, 0 minor

## Code Review — bookshelf-htpc.1 (PR #828) Reviewer: code-reviewer subagent (claude-sonnet-4-6) Diff reviewed: `origin/main...origin/bd-bookshelf-htpc.1` CI: green (not re-run — CI is source of behavioral truth) --- ### Phase 0: DEMO Verification No explicit DEMO block in the bead or its comments. This is a pure internal workflow refactor with no user-visible endpoint output to demo. CI is green with comprehensive integration coverage. Proceeding on that basis. --- ### Phase 1: Spec Compliance All spec requirements met: - `BulkEnrichHandler` → `BulkEnrichByIDsWorkflow` (one parent instance) ✓ - `BulkCustomFetchHandler` → `BulkEnrichByIDsWorkflow` with `optsJSON` ✓ - `BulkGenerateCoversHandler` → `BulkCoversGenerateByIDsWorkflow` (one parent instance) ✓ - ContinueAsNew pagination with `bulkByIDsBatchSize = 50`, separate from concurrency ✓ - Bounded single-digit fan-out (`defaultFanOutConcurrency = 4`, covers clamped ≤9, metadata configurable) ✓ - User-ID scoping: `filterOwnedBookIDs` + `getUserLibraryIDs` + `userIDFromRequest` all preserved unchanged ✓ - `MaxBulkBookIDs` cap: unchanged handler code, enforced before dispatch ✓ - Architecture boundary: `optsJSON []byte` crosses the domain→adapter boundary (no wfengine type in `internal/books`) ✓ --- ### Phase 2: Code Quality [MAJOR] `internal/wfengine/engine_integration_test.go` — `BulkCoversGenerateByIDsWorkflow` has no multi-epoch real-engine integration test The `workflow-integration-test-must-assert-success` rule requires ≥3 real ContinueAsNew epochs on the real engine, asserting the terminal `Result` value (not just `.state == "completed"`). `BulkCoversGenerateByIDsWorkflow` has only: - A single-book registration test (one epoch, no ContinueAsNew triggered, lines 1081–1128) - A multi-epoch unit test in the workflow tester (not the real MySQL engine) `BulkEnrichByIDsWorkflow` (same PR) correctly has a full multi-epoch real-engine test (10 IDs / batch 3 = 4 epochs, asserts `detail.Result == "null"` AND `refetchCalls == numIDs`). The same rapid-ContinueAsNew `WorkflowExecutionStarted` race (#583) that motivated that test applies equally to `BulkCoversGenerateByIDsWorkflow`. The fix is small: add a test mirroring the enrich multi-epoch test with `WithBulkByIDsBatchSize(3)`, a `generateCover` call counter via `sync/atomic`, and assertions on `detail.Result == "null"` + `coverCalls == numIDs`. --- ### Positive observations - **Sliding-window limiter is correct**: `boundedFanOut` seeds up to `concurrency` children, then awaits the oldest (FIFO) before scheduling the next — genuine schedule-N/await-one-before-next, not 50-at-once. - **Batch size and concurrency are truly separate knobs**: `bulkByIDsBatchSize = 50` (epoch page size) vs `defaultFanOutConcurrency = 4` / `coversWindow` / `metadataWindow` (per-epoch concurrent children). Clean separation. - **Configurable concurrency path is complete**: `startBulkEnrichByIDsWorkflow` closure sets `input.Concurrency = metadataWindow` before creating the workflow; `startBulkCoversGenerateByIDsWorkflow` sets `input.Concurrency = coversWindow`. Carried through `ContinueAsNew`. `clampCovers()` enforces ≤9 hard cap at the engine boundary. - **Architecture boundary clean**: `internal/wfengine` imports `internal/books` (adapter importing domain — allowed). `internal/books/trigger_dispatch.go` carries `optsJSON []byte` (not a wfengine type). Confirmed `internal/books` has no `go-workflows` import. - **Determinism**: no `map`-range over ID slice, no `time.Now` inside workflow coroutines. - **Multi-epoch BulkEnrichByIDsWorkflow integration test is thorough**: drives 4 real ContinueAsNew epochs, asserts `detail.Result == "null"` (the SUCCESS value — catches errored-but-marked-completed), asserts `atomic.LoadInt64(&refetchCalls) == numIDs` (all IDs processed across all epochs). Uses `sync/atomic` for the shared counter (race-safe). This is exactly what the rule requires. - **Real-engine registration tests use `wfengine.New()` + `StartWorker()`**: the production constructor + worker startup, catching queue/activity-registry mismatches invisible to the in-memory tester. - **Multi-user scoping**: all three handlers retain `filterOwnedBookIDs` + `getUserLibraryIDs` + `userIDFromRequest`. The workflow only ever receives ownership-verified IDs. --- REVIEW VERDICT: 0 blocker, 1 major, 0 minor
test(wfengine): add multi-epoch real-engine integration test for BulkCoversGenerateByIDsWorkflow
All checks were successful
/ JS Unit Tests (pull_request) Successful in 1m22s
/ E2E API (pull_request) Successful in 2m29s
/ Lint (pull_request) Successful in 3m23s
/ Integration (pull_request) Successful in 3m31s
/ Test (pull_request) Successful in 4m3s
/ E2E Browser (pull_request) Successful in 4m24s
ee19244e73
Adds a multi-epoch ContinueAsNew integration test for
BulkCoversGenerateByIDsWorkflow mirroring the existing
BulkEnrichByIDsWorkflow test (bookshelf-htpc.1 review fix).

Uses WithBulkByIDsBatchSize(3) with 10 IDs → 4 epochs on the real
go-workflows engine (MySQL backend + real workers). Asserts:
- terminal detail.Result == "null" (true SUCCESS, guards the
  diag-tiebreaker fix from bookshelf-r6lx.2)
- atomic generateCover call counter == numIDs (all IDs processed
  across all 4 epochs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)

recompute-match-score-kebab-open

**Recompute Match Score — kebab open screenshot** (recompute-match-score-kebab-open) ![recompute-match-score-kebab-open](/attachments/c2026e1e-7461-4744-a896-8c77f8666048)

Workflow Detail page screenshot (wf-detail-older-execution)

Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.

wf-detail-older-execution

**Workflow Detail page screenshot** (wf-detail-older-execution) Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent. ![wf-detail-older-execution](/attachments/5d63bfdc-4911-4f00-b83b-b6d1d6055c63)
Author
Owner

MAJOR addressed: Added a real-engine multi-epoch ContinueAsNew integration test for BulkCoversGenerateByIDsWorkflow (commit ee19244e).

The new test mirrors the existing BulkEnrichByIDsWorkflow multi-epoch test exactly:

  • Uses wfengine.New() + StartWorker() on the production constructor path (real MySQL engine)
  • WithBulkByIDsBatchSize(3) with 10 IDs → 4 ContinueAsNew epochs
  • sync/atomic counter on the generateCover stub; asserts == numIDs at the end
  • Asserts detail.Result == "null" (JSON null = true SUCCESS), not merely absence of an error string

CI green. PR is mergeable.

MAJOR addressed: Added a real-engine multi-epoch ContinueAsNew integration test for `BulkCoversGenerateByIDsWorkflow` (commit ee19244e). The new test mirrors the existing `BulkEnrichByIDsWorkflow` multi-epoch test exactly: - Uses `wfengine.New()` + `StartWorker()` on the production constructor path (real MySQL engine) - `WithBulkByIDsBatchSize(3)` with 10 IDs → 4 ContinueAsNew epochs - `sync/atomic` counter on the `generateCover` stub; asserts `== numIDs` at the end - Asserts `detail.Result == "null"` (JSON null = true SUCCESS), not merely absence of an error string CI green. PR is mergeable.
zombor force-pushed bd-bookshelf-htpc.1 from ee19244e73
All checks were successful
/ JS Unit Tests (pull_request) Successful in 1m22s
/ E2E API (pull_request) Successful in 2m29s
/ Lint (pull_request) Successful in 3m23s
/ Integration (pull_request) Successful in 3m31s
/ Test (pull_request) Successful in 4m3s
/ E2E Browser (pull_request) Successful in 4m24s
to fba1cda8dc
All checks were successful
/ JS Unit Tests (pull_request) Successful in 1m20s
/ Lint (pull_request) Successful in 1m35s
/ E2E API (pull_request) Successful in 2m0s
/ Test (pull_request) Successful in 2m19s
/ Integration (pull_request) Successful in 2m40s
/ E2E Browser (pull_request) Successful in 3m27s
2026-06-29 11:20:12 +00:00
Compare

Workflow Detail page screenshot (wf-detail-older-execution)

Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.

wf-detail-older-execution

**Workflow Detail page screenshot** (wf-detail-older-execution) Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent. ![wf-detail-older-execution](/attachments/dbb60ae6-6987-4d69-a79a-6a6c21bd69cf)

Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)

recompute-match-score-kebab-open

**Recompute Match Score — kebab open screenshot** (recompute-match-score-kebab-open) ![recompute-match-score-kebab-open](/attachments/05e12ed9-af3b-444b-9a86-fd28ee720fa5)
zombor merged commit 47eb0872c1 into main 2026-06-29 11:24:09 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
zombor/pergamum!828
No description provided.