Skip to content

fix: remove stale posts when publish status changes - #305

Open
Kallyan01 wants to merge 4 commits into
mainfrom
fix/stale-posts
Open

fix: remove stale posts when publish status changes#305
Kallyan01 wants to merge 4 commits into
mainfrom
fix/stale-posts

Conversation

@Kallyan01

@Kallyan01 Kallyan01 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes the de-indexing path so unpublished, trashed and permanently deleted posts stop appearing in search results without a full re-sync.

Why

The delete filter and the stored record disagreed on the key, so deleteBy matched nothing and Algolia reported success:

records store:  site_post_id = "httplocalhost8889_4"     ← sanitize_key( normalize_url( site_url ) ) . "_" . ID
filter asked:   site_post_id = "http://localhost:8889/_4" ← normalize_url( site_url ) . "_" . ID

sanitize_key() lowercases and strips : and /, so the two can never be equal. Algolia matches facet filters on exact attribute values, and deleting zero records is a legitimate outcome, not an error — so is_wp_error() passed and the failure was completely silent.

Publishing kept working because saveObjects upserts by objectID and never consults the filter, which is why the incremental indexing path looked healthy while only the delete half was broken.

Related Issue(s):

AI Disclosure

Written with Claude Code (Opus 5), used for diagnosis, implementation, and tests.

Testing Instructions

  1. Configure OneSearch with valid Algolia credentials and make post an indexable entity.
  2. Publish a post titled "Hello Indexing" and confirm it appears in front-end search.
  3. Move it to the trash. Search again — it should be gone immediately, with no re-sync.
  4. Restore it, confirm it returns to results, then switch it to Draft. Gone again.
  5. Restore and publish once more, then Delete permanently from the trash. Gone, and this time via the new deleted_post hook.

Screenshots

No UI changes.

  • I have read the Contribution Guidelines.
  • I have read the Development Guidelines.
  • I have added necessary tests to cover my changes.
  • I have updated the project documentation as needed.
  • My code has detailed inline documentation.
  • My code is tested to the best of my abilities.
  • My code passes all lints, tests, and checks.
Open WordPress Playground Preview

Copilot AI balanced review requested due to automatic review settings September 3, 2026 20:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new permanent-deletion path lacks direct integration coverage, and test credential setup bypasses the production storage API.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes incremental de-indexing when posts become unpublished, trashed, or permanently deleted.

Changes:

  • Centralizes sanitized site/post identifiers.
  • Adds permanent-deletion cleanup.
  • Extends Algolia request capture and regression tests.
File summaries
File Description
inc/Modules/Search/Watcher.php Uses consistent deletion filters and handles permanent deletion.
inc/Modules/Search/Post_Record.php Centralizes site key and post identifier generation.
tests/phpunit/TestCase.php Captures Algolia request payloads.
tests/phpunit/Integration/Modules/Search/WatcherTest.php Tests transition-based record deletion.
tests/phpunit/Integration/Modules/Search/PostRecordTest.php Tests identifier consistency and sanitization.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/phpunit/Integration/Modules/Search/WatcherTest.php Outdated
Comment thread tests/phpunit/Integration/Modules/Search/WatcherTest.php
@codecov-commenter

codecov-commenter commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 85.99%. Comparing base (30af2f0) to head (bbcc753).

Files with missing lines Patch % Lines
inc/Modules/Search/Watcher.php 92.30% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #305      +/-   ##
============================================
+ Coverage     85.95%   85.99%   +0.03%     
- Complexity      613      621       +8     
============================================
  Files            22       22              
  Lines          2158     2170      +12     
============================================
+ Hits           1855     1866      +11     
- Misses          303      304       +1     
Flag Coverage Δ
unit 85.99% <94.73%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
inc/Modules/Search/Post_Record.php 91.37% <100.00%> (+0.17%) ⬆️
inc/Modules/Search/Watcher.php 97.05% <92.30%> (-2.95%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 3, 2026 20:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The newly added permanent-deletion hook lacks integration coverage.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

inc/Modules/Search/Watcher.php:27

  • The new permanent-deletion path is not exercised by WatcherTest; the added regression test only transitions a post to draft. Please add an integration test that registers the hooks, calls wp_delete_post( $post_id, true ), and verifies the emitted deleteByQuery filter so the hook registration and callback arguments are covered.
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 3, 2026 20:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The pre-deletion hook can remove search records even when WordPress subsequently fails to delete the post.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread inc/Modules/Search/Watcher.php Outdated
Copilot AI review requested due to automatic review settings September 3, 2026 21:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation and regression coverage are sound; only the PR description’s hook name needs correction.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread inc/Modules/Search/Watcher.php
@Kallyan01
Kallyan01 requested a review from justlevine September 4, 2026 05:52
Comment on lines +142 to +150
/**
* Gets the key used to namespace this site's records.
*
* Sanitized because it is embedded in the `objectID`, which Algolia addresses
* as a URL path segment.
*/
public static function get_site_key(): string {
return sanitize_key( Utils::normalize_url( get_site_url() ) );
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not using this anywhere else (yet at least). At minimum, lets make this private static and put it with the other private methods in this class (although if we decide that we don't need public static get_site_post_id(), then just private)

* as a URL path segment.
*/
public static function get_site_key(): string {
return sanitize_key( Utils::normalize_url( get_site_url() ) );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The internals also don't seem to make sense here. Note how they duplicate $this->site_url(). Usually a sign to rethink our approach and desired end result.

Comment on lines +67 to +68
* Runs on `deleted_post` rather than `before_delete_post`: deletion can still fail
* after the earlier hook, and attachments never fire it at all.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a decision log about how it's used, if you think it's worth retaining, then move it as a comment above add_action( 'deleted_post', ... ) not here.

]
);

// A site without credentials is not a failure worth reporting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a glance, fixing this is out of scope of this PR, but we should address the underlying issue long term, not just assume the tech debt.

(If algolia is configured correctly, but we still get those errors, we def want to log them)

Suggested change
// A site without credentials is not a failure worth reporting.
// @todo this class shouldn't run if the Algolia config isn't good.

Comment on lines +46 to 49
// First delete the old records, so a post that is no longer indexable leaves nothing behind.
if ( is_wp_error( $this->delete_post_records( $indexer, (int) $post->ID ) ) ) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we always be deleting this? I havn't tried to replicate myself, but I'd assume we only want to delete if the old record if the currently status is NOT indexable and the previous status was indexable. (I'd also assume that the $indexer->save_records() cleans up old records for the same id when resaving). Otherwise, why slow the user down with the external API hit?

So something like:

// Check if the new status is allowed before reindexing.
if ( ! in_array( $new_status, Post_Record::get_allowed_statuses( [ $post->post_type ] ), true ) ) {
	// Cleanup old indexed post.
	if ( in_array ( $old_status, Post_Record::get_allowed_statuses( [ $post->post_type ] ) ) ) {
		$this->delete_post_records( $indexer, (int) $post->ID )
	}

	return;
}

$records = ( new Post_Record() )->to_records( $post );
...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants