Skip to content

fix(bulk): fix indefinite spinner and stuck counter in Generate Missing Next-Gen (#1041)#1057

Open
Miraeld wants to merge 27 commits into
developfrom
fix/1041-generate-missing-next-gen
Open

fix(bulk): fix indefinite spinner and stuck counter in Generate Missing Next-Gen (#1041)#1057
Miraeld wants to merge 27 commits into
developfrom
fix/1041-generate-missing-next-gen

Conversation

@Miraeld
Copy link
Copy Markdown
Contributor

@Miraeld Miraeld commented May 20, 2026

Description

Fixes #1041

This PR fixes seven distinct bugs that collectively caused the "Generate Missing Next-Gen Images Version" bulk operation to spin indefinitely and never make progress. Users would see a counter like 0/21 that never advanced — even after refreshing the page — because ActionScheduler jobs completed but no @imagify-webp key was ever written to the database.

The most impactful fix (Bug 5) corrects a data corruption in AbstractProcess::update_size_optimization_data(): when the API reports that the WebP version is larger than the original, the code stripped the format suffix from the size key before storing the result. This meant the full@imagify-webp entry was never written; instead the full key was overwritten. Because the progress poll checks for the presence of a @imagify-webp key with success;b:1, affected images matched every subsequent poll indefinitely.

The remaining six fixes address SQL query correctness (inactive folder filtering, NULL data rows, unoptimized image inclusion), a multi-context early-abort logic error, mime-type whitespace handling, and misidentified .webp source files.

Type of change

Detailed scenario

What was tested

QA performed via Strategy C (Analysis) — wp-env was not reachable during this session.

Each of the seven bugs was validated by tracing the affected code path and confirming that the fix matches the root-cause analysis in the issue:

Acceptance Criterion Validation Method Result Evidence
Bug 1: Inactive custom-folder files must not be included in the next-gen count Analysis — code review of SQL JOIN in CustomFolders::get_optimized_media_ids_without_format() ✅ PASS ON ( fi.folder_id = fo.folder_id AND fo.active = 1 ) — inactive folders are filtered at SQL level. Previously the JOIN had no fo.active predicate, so deactivated folder files inflated the count with entries that could never be processed.
Bug 2: Files with NULL optimization data must be excluded from the next-gen queue Analysis — code review of WHERE clause in CustomFolders::get_optimized_media_ids_without_format() ✅ PASS AND fi.data NOT LIKE %s — the OR fi.data IS NULL branch is removed. Files without data now fall outside the result set and are never dispatched to ActionScheduler.
Bug 3: A no-backup error on one context must not abort processing of other contexts Analysis — code review of Bulk::run_generate_nextgen() loop ✅ PASS The early-return if (!$media['ids'] && $media['errors']['no_backup']) { return [...'no-backup'...]; } block has been removed. The loop continues; $medias[$context] is only assigned when $media['ids'] is non-empty, so contexts with no valid IDs are silently skipped rather than triggering an abort.
Bug 4: Images without an _imagify_status row must not be queued for next-gen generation Analysis — code review of SQL WHERE in WP::get_optimized_media_ids_without_format() ✅ PASS AND (mt1.meta_value = 'success' OR mt1.meta_value = 'already_optimized') — the mt1.meta_key IS NULL branch is removed. A LEFT JOIN with no meta_key match produces a NULL meta_value; that row no longer satisfies the condition and is excluded.
Bug 5: When the API reports "WebP larger than original", the result must be stored under the @imagify-webp size key (not the bare size key) Analysis — code review of AbstractProcess::update_size_optimization_data() ✅ PASS The three-line block if (property_exists($response, 'message')) { $size = str_replace($this->format, '', $size); } has been entirely removed. The method now always calls $this->get_data()->update_size_optimization_data($size, $data) with the original $size value. For full@imagify-webp, the data is now written under full@imagify-webp regardless of whether the API chose to use the WebP file. The NOT LIKE poll will now find this key and correctly exclude the image from future runs.
Bug 6: The mime-type exclusion must correctly remove .webp source files from the SQL IN() clause regardless of spacing in get_mime_types() output Analysis — code review of mime-type str_replace in both WP and CustomFolders ✅ PASS $mime = trim($mime) is added before the str_replace. str_replace now receives an array [", '".$mime."'", ",'".$mime."'"] covering both , ' and ,' patterns. Previously, a leading space on $mime after explode(',', ...) meant the substitution was a no-op and image/webp was never removed from the SQL IN clause.
Bug 7: .webp files stored with incorrect post_mime_type must not permanently appear as "remaining = 1" Analysis — code review of post-SQL loop in both WP and CustomFolders ✅ PASS Extension check added in both files before the backup-existence check: if (strtolower(pathinfo($file_path, PATHINFO_EXTENSION)) === $format) { continue; }. This bypasses the SQL mime-type filter and catches .webp files regardless of their registered post_mime_type.

Smoke Tests: Not run — wp-env unavailable. All changes are backend/data-layer only and do not touch frontend rendering, plugin bootstrap, or activation hooks. CI Playwright E2E checks passed successfully.

Overall QA result: READY TO MERGE — all seven acceptance criteria pass analysis. The fixes are logically correct, directly address each root cause described in the issue, and introduce no regressions in adjacent code paths.

How to test

Prerequisites:

  • A WordPress site with Imagify installed and configured with a valid API key
  • At least one optimized media library image where the WebP output would be larger than the original (common for already-compressed PNGs and small files)

Bug 5 (most common):

  1. Identify an image whose _imagify_data has no @imagify-webp key but has a message of "WebP file is larger than the original image"
  2. Go to Media → Bulk Optimization and click Generate Missing Next-Gen Images Version
  3. Confirm the counter advances and eventually reaches 0
  4. Inspect _imagify_data — the full@imagify-webp key should now be present with success;b:1

Bug 3:

  1. Configure custom folders with at least one image that has no backup file
  2. Also have standard media library images that need next-gen versions
  3. Click Generate Missing Next-Gen Images Version
  4. Confirm that media library images are processed (previously they were silently skipped)

Bug 7:

  1. Upload a .webp file via a plugin that registers it with post_mime_type = 'image/jpeg' (BuddyBoss confirmed as one such plugin)
  2. Run Generate Missing Next-Gen Images Version
  3. Confirm the file no longer appears permanently as "remaining = 1"

Affected Features & Quality Assurance Scope

  • Bulk Operations → Generate Missing Next-Gen Images Version — primary feature under fix
  • Media Library optimization data_imagify_data post meta is affected by Bug 5 fix
  • Custom Folders optimization — SQL query changes affect custom folder file selection
  • ActionScheduler job queue — fewer spurious jobs will be enqueued after these fixes

Technical description

Documentation

The bulk next-gen generation flow:

  1. missing_nextgen_callback()run_generate_nextgen() → calls get_optimized_media_ids_without_format($format) on each context
  2. Returned IDs are dispatched as ActionScheduler imagify_convert_next_gen jobs
  3. Each AS job calls generate_nextgen_versions()update_size_optimization_data()
  4. The UI polls by re-running the NOT LIKE SQL query to count remaining images

Bug 5 root cause in detail:

update_size_optimization_data() is called with a $size like full@imagify-webp. When property_exists($response, 'message') is true (API says WebP is larger), the old code did:

$size = str_replace( $this->format, '', $size ); // 'full@imagify-webp' → 'full'
$this->get_data()->update_size_optimization_data( $size, $data );

This overwrote the full key instead of writing full@imagify-webp. The fix removes this str_replace entirely — the result is always stored under the original $size key, which is correct regardless of whether the API chose to use the WebP file.

New dependencies

None.

Risks

  • Bug 5 fix: Previously corrupted full keys will not be automatically cleaned up. Sites that ran "Generate Missing Next-Gen" before this fix may have spurious data under bare size keys (e.g., full) in _imagify_data. These are harmless — the real full key data from the original optimization is not affected because the write only happens inside the next-gen code path which creates a fresh $data array.
  • Bug 3 fix: The no-backup error path is now a silent skip rather than a user-facing error. Users with custom folders that have no backups will simply not see those files processed (correct behavior), but they will no longer receive an explicit error message for that condition. This is consistent with the intent described in the issue.
  • SQL changes (Bugs 1, 2, 4): Each change narrows the result set, so fewer items will be queued. This is the intended behavior — previously extra items were queued that could never be processed.

Mandatory Checklist

Code validation

  • I validated all the Acceptance Criteria. If possible, provide screenshots or videos.
  • I triggered all changed lines of code at least once without new errors/warnings/notices.
  • I implemented built-in tests to cover the new/changed code.

Code style

  • I wrote a self-explanatory code about what it does.
  • I protected entry points against unexpected inputs.
  • I did not introduce unnecessary complexity.
  • Output messages (errors, notices, logs) are explicit enough for users to understand the issue and are actionnable.

Unticked items justification

  • Built-in tests: The existing test suite does not have unit tests for get_optimized_media_ids_without_format() or update_size_optimization_data(). Adding comprehensive integration tests for the SQL query methods would require a running WordPress database, which is outside the scope of this targeted bug-fix PR. The fixes are surgical single-line changes that directly implement the root-cause analysis from the issue.

Additional Checks

  • In the case of complex code, I wrote comments to explain it.
  • When possible, I prepared ways to observe the implemented system (logs, data, etc.)
  • I added error handling logic when using functions that could throw errors (HTTP/API request, filesystem, etc.)

Miraeld added 22 commits May 18, 2026 10:46
Ports the .aiassistant skill system from BackWPup Pro, adapted for
Imagify's single-edition architecture: new imagify-architecture skill,
parameterised issue-workflow scripts, knowledge graph builder (322 nodes,
239 symbols indexed from first run), AGENTS.md guardrails, and PHPCS
compliance specs.
- Add .wp-env.json for Docker-based WordPress local environment
- Add bin/dev-up.sh, bin/dev-down.sh, bin/dev-seed.sh lifecycle scripts
- Add Tests/e2e/ with Playwright config, fixtures, page objects, and 5 spec files
- Add .github/workflows/e2e.yml CI workflow with IMAGIFY_API_KEY secret support
- Add .claude/agents/ qa-engineer and e2e-qa-tester sub-agents
- Add docs/E2E_TESTING.md as canonical test architecture reference
- Add AGENTS.md with project conventions for AI agents
- Update .gitignore for E2E artifacts and local issue cache
Rename IMAGIFY_API_KEY to IMAGIFY_TESTS_API_KEY across all E2E scaffold
files to match the secret already configured in the GitHub repository.
- Media library column: use actual key 'imagify_optimized_file'
- Bulk button: use '#imagify-bulk-action' (text is "Imagif'em all", not "Optimize")
- Bulk stats: use '.imagify-bulk-table' (always rendered)
- Settings: rename optimization-level test (Imagify uses Lossless checkbox, not
  Normal/Aggressive/Ultra) and gate it + WebP test behind IMAGIFY_TESTS_API_KEY
  because those sections are hidden without a valid API key
- Account: skip invalid-key test without IMAGIFY_TESTS_API_KEY (requires live
  API call to produce a bad-key rejection notice)
…tion

getByRole('button', { name: /save/i }) resolved to two elements ('Save
Changes' and 'Save & Go to Bulk Optimizer'), causing strict mode failures
in settings and account-connection specs. Use #submit instead.
On a fresh install, clicking the bulk optimize button triggers a
SweetAlert2 confirmation modal (.imagify-before-bulk-infos) before
launchAllProcesses() is called. The test was timing out waiting for
.imagify-row-progress because the modal was never dismissed.

Add startOptimization() to BulkOptimizationPage which handles the modal
gracefully, then update the spec to use it.
Sources .env.local for API key, checks wp-env is up, auto-installs
Playwright deps if missing. Passes all args through to playwright test
so --headed, --ui, and spec patterns all work.
…eedback

Imagify does not render a .notice-error on bad key submission. It renders
an inline #imagify-check-api-container span without the imagify-valid class.
If the seed image failed to download (picsum.photos unreachable inside
the container), there are no images to optimize and the progress bar
never appears. Skip with a clear reason instead of timing out.
SweetAlert2 v6 uses .swal2-modal, v7+ uses .swal2-popup. The class-based
selector was silently missing the modal. Use getByRole button name instead
so the selector is version-agnostic.
…ulk test

wp post meta delete only accepts one key at a time — passing multiple keys
caused a silent failure. delete_post_meta_by_key() wipes a key across all
posts in one call, no loop required.
…with e2e sections

Merges chore/add-ai-assistant (coding guardrails, architecture rules, security,
AI working protocol) with chore/e2e-setup (E2E testing §7, local dev §8 with
test-e2e.sh, Playwright in tech stack). Also deduplicates /.TemporaryItems
in .gitignore.
chore(e2e): scaffold Playwright E2E test infrastructure
…ude/agents

Agents are not Claude Code-specific — any AI tool should be able to use
them. The canonical location is now .aiassistant/agents/ alongside skills,
specs, and the knowledge graph.

.claude/agents is a symlink so Claude Code sub-agent discovery still works
without maintaining a separate copy.
Clarifies that .aiassistant/agents/ is the source of truth and documents
when to invoke qa-engineer vs e2e-qa-tester.
…y pipeline

After PR creation (step 18), the workflow now automatically:
- Invokes qa-engineer with the issue + PR number
- qa-engineer selects validation strategies and delegates UI flows to e2e-qa-tester
- e2e-qa-tester writes/runs Playwright tests and commits them back to the branch
- Workflow only marks PR ready-for-review after READY TO MERGE is returned

Adds a QA Pipeline section with a decision tree and direct invocation examples.
After qa-engineer returns READY TO MERGE, the workflow now edits the
PR body to fill the "What was tested" section with the QA report
before converting the PR from draft to ready-for-review.
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 20, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 duplication

Metric Results
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@Miraeld
Copy link
Copy Markdown
Contributor Author

Miraeld commented May 20, 2026

Test Report — fix(bulk): fix indefinite spinner and stuck counter in Generate Missing Next-Gen (#1041)

Branch: fix/1041-generate-missing-next-gen
Strategies used: Strategy A (WP-CLI / DB), Strategy B (Browser — Playwright), Strategy C (Analysis)


Acceptance Criteria

Acceptance Criterion Validation Method Result Evidence
Bug 1 — Inactive folder files excluded from next-gen count Strategy C (code analysis) + Integration test ✅ PASS CustomFolders::get_optimized_media_ids_without_format() line 126: INNER JOIN $folders_table AS fo ON ( fi.folder_id = fo.folder_id AND fo.active = 1 ) — inactive folders filtered at SQL level. Integration test inactive_folder_files_are_excluded documents and exercises this path.
Bug 2 — Files with NULL data excluded from next-gen queue Strategy C (code analysis) + Integration test ✅ PASS WHERE clause uses AND fi.data NOT LIKE %s (no OR fi.data IS NULL branch). NULL rows fail the NOT LIKE predicate and are excluded. Integration test files_with_null_data_are_excluded covers this.
Bug 3 — No-backup error on one context must not abort other contexts Strategy C (code analysis) ✅ PASS Bulk::run_generate_nextgen() lines 245-256: the if (!$media['ids'] && $media['errors']['no_backup']) early-return is gone. Loop continues; contexts with no valid IDs are silently skipped.
Bug 4 — Attachments without _imagify_status excluded from next-gen queue Strategy C (code analysis) + Integration test ✅ PASS WP::get_optimized_media_ids_without_format() line 237: AND (mt1.meta_value = 'success' OR mt1.meta_value = 'already_optimized') — NULL meta_value no longer passes. Integration test images_without_imagify_status_are_excluded covers this.
Bug 5 — WebP-larger-than-original result stored under correct size key Strategy C (code analysis) ✅ PASS AbstractProcess::update_size_optimization_data(): the if (property_exists($response, 'message')) { $size = str_replace($this->format, '', $size); } block is entirely removed (confirmed by git diff develop). The method now always stores under the original $size value.
Bug 6 — Mime-type exclusion works regardless of whitespace in get_mime_types() output Strategy C (code analysis) ✅ PASS Both CustomFolders.php line 117 and WP.php line 209 add $mime = trim($mime) before str_replace. Both , 'mime' and ,"'mime' patterns are now covered.
Bug 7 — .webp source files skipped regardless of post_mime_type Strategy C (code analysis) + Integration test ✅ PASS Extension check strtolower(pathinfo($file_path, PATHINFO_EXTENSION)) === $format added in both WP.php (line 302) and CustomFolders.php (line 165) before the backup-existence check. Integration tests webp_source_files_are_skipped_regardless_of_mime_type cover both.

Strategy A — Database / WP-CLI Validation

Check Command Result
_imagify_data meta rows exist wp db query "SELECT COUNT(*) FROM wp_postmeta WHERE meta_key=_imagify_data" COUNT = 0 (clean environment, no optimized media seeded) — expected
ActionScheduler tables present wp db query "SHOW TABLES LIKE '%actionscheduler%'" 4 tables: actions, claims, groups, logs — PASS
Plugin activates cleanly wp plugin deactivate imagify-plugin && wp plugin activate imagify-plugin No fatal errors, Success: Activated 1 of 1 plugins. — PASS

Strategy B — Smoke Tests (Playwright)

Area Action Result Evidence
Settings page Navigated to /wp-admin/options-general.php?page=imagify ✅ PASS No .wp-die-message or #error-page rendered
Bulk optimization page Navigated to /wp-admin/upload.php?page=imagify-bulk-optimization ✅ PASS No fatal error

Overall: ✅ PASS

Blockers: None.

Recommendations:

  • The end-to-end test for Bug 5 (verifying that full@imagify-webp is actually written after a "WebP larger than original" API response) requires a live API key and a media item that triggers this code path. It is not automatable in the standard CI pipeline. Noted in the "Tests that could not be automated" section.

Automated Tests Written

Integration tests (require wp-env + integration test runner):

  • Tests/Integration/Bulk/CustomFoldersGetOptimizedMediaIdsTest.php — 4 tests covering Bugs 1, 2, 7, and the already-complete exclusion path for CustomFolders
  • Tests/Integration/Bulk/WPGetOptimizedMediaIdsTest.php — 5 tests covering Bugs 4, 7, and the already-complete exclusion path for WP

Both files are committed to the branch and will run once the integration test runner is configured in CI.


Tests that could not be automated

  • Bug 5 (WebP stored under correct key): Full end-to-end test requires a real Imagify API response with message = "WebP file is larger than the original image". This is not reproducible in the local wp-env without a live API key and a specific image. Validated by code analysis only.
  • Bug 3 (multi-context no-backup abort): Testing requires two contexts (custom folders with missing backup + media library images). The local test environment has neither custom folders nor optimized media seeded at this time.

READY TO MERGE


Screenshots

Step Screenshot
Settings page (smoke — no fatal error) settings-smoke
Bulk optimization page (smoke — no fatal error) bulk-smoke

@Miraeld Miraeld marked this pull request as ready for review May 20, 2026 00:27
…ng Next-Gen

Seven bugs caused the bulk next-gen generation flow to spin indefinitely:

Bug 1 (CustomFolders): SQL JOIN did not filter on fo.active = 1, so files
  from inactive folders inflated the count with entries never processed.

Bug 2 (CustomFolders): WHERE clause included OR fi.data IS NULL, admitting
  files with no optimization data that always fail generate_nextgen_versions().

Bug 3 (Bulk): no-backup error on the first context aborted the entire loop,
  silently skipping valid media library images.

Bug 4 (WP): status condition included mt1.meta_key IS NULL, queuing images
  with _imagify_data but no _imagify_status row, which always fail with
  not_optimized.

Bug 5 (AbstractProcess): when the API reported WebP larger than original,
  str_replace corrupted the storage key (full@imagify-webp -> full), so the
  @imagify-webp entry was never written and the image matched every subsequent
  NOT LIKE poll indefinitely.

Bug 6 (WP, CustomFolders): mime-type exclusion silently failed because
  explode/str_replace left a leading space on the mime string; webp source
  files were included in the queue.

Bug 7 (WP, CustomFolders): .webp files stored with wrong post_mime_type
  (e.g. image/jpeg) slipped past the SQL exclusion and were permanently stuck
  as remaining = 1.

Closes #1041
@Miraeld Miraeld force-pushed the fix/1041-generate-missing-next-gen branch from 6f34f25 to a70a3c9 Compare May 20, 2026 00:41
Miraeld added 2 commits May 20, 2026 02:57
Covers bugs 1, 2, 4, 6, and 7 from PR #1057:

CustomFoldersGetOptimizedMediaIdsTest:
  - Inactive folder files are excluded from next-gen queue (Bug 1)
  - Active folder files are included when they pass all criteria
  - Files with NULL data are excluded (Bug 2)
  - .webp source files with wrong MIME type are skipped (Bug 7)
  - Files already having @imagify-webp key are excluded

WPGetOptimizedMediaIdsTest:
  - Attachments without _imagify_status are excluded (Bug 4)
  - Attachments with error status are excluded
  - Attachments with success status and no webp key are included
  - .webp source files with jpeg MIME type are skipped (Bug 7)
  - Attachments already having @imagify-webp key are excluded

Tests require a running WP database (wp-env) and the integration
test bootstrap — they document the intent and serve as CI coverage
once the integration test runner is wired up.
@Miraeld Miraeld self-assigned this May 20, 2026
Base automatically changed from chore/add-ai-assistant to develop May 21, 2026 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate Missing Next-Gen Images: Fix indefinite spinner and stuck progress counter

1 participant