Skip to content

fix: Delete terms by their remote id and surface deletion failures - #23294

Open
dcalhoun wants to merge 8 commits into
trunkfrom
fix/term-deletion-remote-id
Open

fix: Delete terms by their remote id and surface deletion failures#23294
dcalhoun wants to merge 8 commits into
trunkfrom
fix/term-deletion-remote-id

Conversation

@dcalhoun

@dcalhoun dcalhoun commented Sep 3, 2026

Copy link
Copy Markdown
Member

Description

Fixes #23293. Fix CMM-2401.

Deleting a category over the wordpress-rs path failed with a 404 and left the confirmation dialog spinning on "Deleting category" forever. Two independent defects, both from #22252:

deleteTerm identified the term by its local database id. TermModel carries mId, the local @PrimaryKey row id, and mRemoteTermId, the id the API knows. deleteTerm sent term.id, so the API was asked to delete a term id belonging to something else and answered 404 rest_term_invalid. Every sibling already uses the remote identity — TaxonomyXMLRPCClient sends getRemoteTermId(), TaxonomyRestClient uses the slug, and updateTerm in the same class sends term.remoteTermId. Creating works because it needs no id. The success path had the mirror image of the slip, passing the local id as the confirmation model's remoteTermId.

A failed deletion was reported under a cause nothing subscribes to. A successful deletion emits OnTaxonomyChanged(causeOfChange = REMOVE_TERM); a failed one emitted DELETE_TERM. CategoryDetailViewModel, CategoriesListViewModel and SiteSettingsTagListActivity all match on REMOVE_TERM only, so the failure reached nobody and the InProgress state was never cleared. Both outcomes now emit REMOVE_TERM, and CategoryDetailViewModel already passes event.isError through to the message. This half affects every client — any failed term deletion left the dialog spinning, including on WordPress.com and XML-RPC sites.

Moving failures onto the cause every screen already listens to means the other two subscribers now receive an event they never saw before, so both had to be taught what to do with it:

  • CategoriesListViewModel matched on the cause alone, so a rejected deletion triggered a refetch — the list posted Loading and, when the network was unavailable (a likely reason the deletion failed in the first place), replaced itself with the no-connection screen. It now refetches only on success.
  • SiteSettingsTagListActivity shared one switch branch between REMOVE_TERM and UPDATE_TERM, which hides the progress dialog and pops the detail fragment unconditionally. A rejected tag deletion therefore closed the dialog and popped the screen with nothing but an AppLog line, indistinguishable from a success while the stale tag stayed in the list. It now shows a "Deleting tag failed" toast, matching the message the categories screen shows.

The existing delete tests stub WpApiClient.request wholesale, so the id sent was never observable and two assertions had been written against the wrong value. The new test runs the request against a stubbed executor and asserts the id itself. Every new test here was confirmed to fail without its fix.

Testing instructions

Note

The category deletion fix requires a site whose taxonomy requests go through wordpress-rs: added with an application password, with taxonomies_rest_api_migration enabled in the dev menu. The failure-reporting fixes apply to every site type.

Deleting a category:

  1. Go to Site Settings → Categories and create a category.
  2. Open the new category and delete it.
  • Verify the category is deleted and disappears from the list
  • Verify the DELETE request uses the same id the POST response returned (on trunk it uses a different one and returns 404)

Failed category deletion no longer hangs (regression check, reproducible on any site type):

  1. With a proxy, fail DELETE */categories/* with a 500.
  2. Delete a category.
  • Verify the "Deleting category" spinner is replaced by "Failed to delete category" instead of spinning indefinitely
  • Verify the category list behind it still shows the categories, rather than flashing a spinner or a "No connection" screen

Failed tag deletion reports the failure:

  1. With a proxy, fail DELETE */tags/* with a 500.
  2. Go to Site Settings → Tags, open a tag and delete it.
  • Verify a "Deleting tag failed" toast appears rather than the screen closing silently

Regressions:

  1. Create, rename and delete categories on a WordPress.com Simple site.
  • Verify all three still work
  1. Repeat on a self-hosted site with the feature flag disabled (XML-RPC path).
  • Verify all three still work
  1. Create, rename and delete tags.
  • Verify all three still work, and that the list refreshes after each

🤖 Generated with Claude Code

https://claude.ai/code/session_011pj4FNhM3TFx83R5uDSuYh

deleteTerm() identified the term with TermModel.id, the local database row id,
so the API was asked to delete an id belonging to another term or to nothing at
all and answered 404 rest_term_invalid. Creating works because it needs no id;
deleting a category the app had just created failed every time.

Every sibling uses the remote identity — TaxonomyXMLRPCClient sends
getRemoteTermId(), TaxonomyRestClient uses the slug, and updateTerm() in this
same class already sends term.remoteTermId. The success path had the mirror
image of the same slip, passing the local id as the confirmation model's
remoteTermId.

The existing delete tests stub WpApiClient.request wholesale, so the id sent
was never observed and two of them asserted the wrong value. Add a test that
runs the request against a stubbed executor and fails on the id itself.

Fixes #23293

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3eKPoHZwqK4AP3GQBSgEG
dcalhoun and others added 2 commits September 3, 2026 16:13
A successful deletion emits OnTaxonomyChanged with causeOfChange REMOVE_TERM
and a failed one emitted DELETE_TERM, which no screen subscribes to.
CategoryDetailViewModel, CategoriesListViewModel and SiteSettingsTagListActivity
all match on REMOVE_TERM only, so a failure reached nobody and the "Deleting
category" progress state stayed on screen indefinitely.

Emit both outcomes under REMOVE_TERM. CategoryDetailViewModel already passes
event.isError to showDeleteStatusMessage(), so the dialog now resolves to
"Failed to delete category" — its tests already build their error callback with
REMOVE_TERM, so they were written against this behaviour.

This affects every client, not just wp-rs: any failed term deletion left the
dialog spinning.

Fixes #23293

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3eKPoHZwqK4AP3GQBSgEG
Both halves reach users in 27.0. The spinner that never resolved was
client-agnostic, so every site type was affected regardless of feature flags,
and the wrong delete id affected sites on the wp-rs taxonomy path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3eKPoHZwqK4AP3GQBSgEG
@dcalhoun
dcalhoun force-pushed the fix/term-deletion-remote-id branch from 58ebae5 to 6b41648 Compare September 3, 2026 20:13
@wpmobilebot

wpmobilebot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr23294-b8b8bd2
Build Number1498
Application IDcom.jetpack.android.prealpha
Commitb8b8bd2
Installation URL4h6t0ajqa0ehg
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr23294-b8b8bd2
Build Number1498
Application IDorg.wordpress.android.prealpha
Commitb8b8bd2
Installation URL40pgvrilcn2io
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 37.95%. Comparing base (b8157ff) to head (b8b8bd2).

Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #23294      +/-   ##
==========================================
+ Coverage   37.93%   37.95%   +0.01%     
==========================================
  Files        2352     2352              
  Lines      128925   128925              
  Branches    17940    17940              
==========================================
+ Hits        48907    48928      +21     
+ Misses      76012    75994      -18     
+ Partials     4006     4003       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 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.

dcalhoun and others added 5 commits September 3, 2026 16:58
A failed deletion now arrives under REMOVE_TERM, the cause a successful one
already used, and this subscriber matched on the cause alone. A rejected
delete therefore triggered a refetch: the list posted Loading and, when the
network was unavailable — a likely reason the delete failed in the first
place — replaced itself with the no-connection screen.

Guard the branch with !event.isError, the guard SiteSettingsTagListActivity
already applies to loadTags(). The new tests pin both halves: a successful
removal still refetches, and a failed one leaves the list as it was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pj4FNhM3TFx83R5uDSuYh
REMOVE_TERM shared a switch branch with UPDATE_TERM, which hides the progress
dialog and pops the detail fragment unconditionally. Now that a failure
arrives under that cause, a rejected tag deletion closed the dialog and
popped the screen with nothing but an AppLog line — indistinguishable from
a success, while the stale tag stayed in the list.

Give REMOVE_TERM its own branch and a toast on failure, matching the message
the categories screen already shows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pj4FNhM3TFx83R5uDSuYh
The note claimed a term that can't be deleted says so instead of leaving the
screen loading, which held only for categories until the tags screen got its
own failure message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pj4FNhM3TFx83R5uDSuYh
The existing test covers only the error branch, though the contract it
protects is that both outcomes arrive under REMOVE_TERM — the subscribers
that tell success from failure would break silently if the success branch
changed cause or rowsAffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pj4FNhM3TFx83R5uDSuYh
Five delete tests each built the same TermsRequestDeleteResponse by hand, and
the two data helpers behind them were identical — the tag one returned
category-shaped data, which goes unnoticed because the delete path reads only
data.deleted.

The remote-id test also stated its response twice: it ran the request for its
side effect, discarded the result and returned a separately built Success.
doSuspendableAnswer lets the executor's own result flow through, dropping the
nested runBlocking and the unchecked cast with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pj4FNhM3TFx83R5uDSuYh
@dcalhoun
dcalhoun marked this pull request as ready for review September 3, 2026 21:14
@dcalhoun
dcalhoun requested a review from adalpari September 3, 2026 21:14
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.

Deleting a category fails with a 404 and leaves the dialog spinning

2 participants