Fix backup() silently truncating at 100 posts - #22
Merged
Conversation
`backup()` paginates through the posts endpoint by reading
`meta.next_page` off each response and feeding it back in as a
`page_handle` parameter. WP.com's v1.1 posts endpoint does not
return `meta.next_page` — the meta object is just
`{"links": {...}, "wpcom": true}` — so `page_handle` is always
None and the loop exits after the first page.
Result: on any site with more than 100 posts, the backup is
silently truncated to the first 100. The backup file reports
`total_posts: 100` regardless of the site's actual size, and
every post beyond the first page is absent from
`backup['post_categories']`. Verified empirically on a 766-post
site during the PR m#19 end-to-end test: the generated backup had
100 entries.
The log-mode restore path happens to mask this because it reads
the change log, not the backup. But snapshot-mode restore (the
fallback when logs are missing or corrupted) iterates
`backup['post_categories']`, so on a truncated backup it would
silently fail to restore 666 out of 766 posts on the same site
without raising an error. That's a data-loss hazard for the one
code path the backup exists to enable.
Switches to offset-based pagination, which v1.1 supports
reliably. Adds dedupe-by-ID because offset pagination can return
the same post twice if a post is added mid-backup and shifts the
underlying query. Loop exits on an empty batch or a batch that
contributes no new IDs (the second condition prevents infinite
spin if the query is stuck).
Three new tests cover the regression, the offset mechanism, and
the dedupe behavior. Updates `test_writes_backup_json` to mock
the empty second page that the offset loop now requires to exit.
3 tasks
…pagination # Conflicts: # tests/test_wpcom_adapter.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
backup()currently paginates through the posts endpoint by readingmeta.next_pageoff each response and feeding it back in as apage_handleparameter. WP.com's v1.1 posts endpoint does not returnmeta.next_pagethough — the meta object is just{"links": {...}, "wpcom": true}— sopage_handleis alwaysNoneand the loop exits after the first page.Result: on any site with more than 100 posts, the backup is silently truncated to the first 100. The file reports
total_posts: 100regardless of the site's actual size, and every post beyond the first page is absent frombackup['post_categories'].I ran into this during the end-to-end test of #19 on a 766-post site. The adapter happily wrote
backup-{timestamp}.jsonwithtotal_posts: 100; the other 666 posts had no backup representation at all. No errors, no warnings, nothing in logs. The backup file looked completely normal until I started comparing it against my independent export of the same site.Why this matters
Log-mode restore happens to mask this bug because it reads the change log, not the backup. That's why I didn't notice during the actual revert of #19 — the inverse replay had all the info it needed from the TSV logs.
Snapshot-mode restore is a different story. That's the fallback that runs when the change logs are missing, corrupted, or when the user explicitly asks for a full rewrite. It iterates
backup['post_categories']— so on a truncated backup, it would silently fail to restore 666 out of 766 posts on the same site without raising a single error. That's a data-loss hazard for the one code path the backup exists to enable.What changed
One method, one block.
backup()now:offset=0, 100, 200, ...), which v1.1 supports reliably. Same shape everything else on WP.com v1.1 uses.Nothing else in
backup()changes;list_categories, default category resolution, and the JSON shape are all untouched. Fix is purely in the pagination loop.Tests
Three new tests in
TestBackupplus one updated (the existingtest_writes_backup_jsonnow mocks the empty second page that the offset loop needs to exit). All 28 tests pass.test_paginates_through_multiple_pages— regression: mocks a 250-post response across 3 pages and asserts all 250 end up in the backup with their IDs intacttest_sends_offset_not_page_handle— verifies the query usesoffset=0and does not includepage_handle(so a future refactor that accidentally reverts the pagination mechanism gets caught)test_dedupes_across_page_boundaries— mocks two pages returning the same three posts and asserts each is captured exactly once, and the loop terminatesTest plan
python3 -m unittest tests.test_wpcom_adapterpasses all 28 testsmain; verified locally that the fix captures all 766 (though the unit tests cover the mechanism)Scope
Just
backup(). Nothing else in the adapter, no agent markdown, no tests for anything else. Can land independently of #19 and #21 — none of them touch this code path.That said, this one is arguably the most urgent of the adapter bugs I found during #19 testing, because a truncated backup is a silent snapshot-restore data loss waiting to happen. If #19 lands with snapshot-mode exercised, this bug becomes user-visible immediately.
Related