diff --git a/fp-plugins/mastodon/developer-docs/05-Regression-Test-Matrix.md b/fp-plugins/mastodon/developer-docs/05-Regression-Test-Matrix.md index c6c6948f..e1abb242 100644 --- a/fp-plugins/mastodon/developer-docs/05-Regression-Test-Matrix.md +++ b/fp-plugins/mastodon/developer-docs/05-Regression-Test-Matrix.md @@ -1,7 +1,7 @@ # 05 - Regression Matrix ## Harness model -`simulate_mastodon_plugin.php` loads the real Mastodon plugin and simulates the FlatPress and Mastodon boundaries. The plugin code itself is not replaced. The HTTP layer, fixture content, media files and some FlatPress services are controlled by the harness so behavior is deterministic. +`simulate_mastodon_plugin.php` loads the real Mastodon plugin and simulates the FlatPress and Mastodon boundaries. The plugin code itself is not replaced. The HTTP layer, fixture content, media files and some FlatPress services are controlled by the harness so behavior is deterministic. The root script and the content-identical `fp-plugins/mastodon/regression-test/simulate_mastodon_plugin.ph_` copy both resolve the FlatPress blog root before the sandbox is built, so either documented entry point is executable. Normal run checked for this documentation set after the exported-comment deletion invariant update: ```text php simulate_mastodon_plugin.php --summary @@ -60,6 +60,14 @@ The simulation verifies that `state.json` is written as compact JSON, then runs The dirty-comment scheduled-run regression clears the `content` sync guard immediately before calling `plugin_mastodon_run_sync(false)`. This keeps the fixture independent from earlier cooldown tests on both file-backed and APCu-backed shared hosts; otherwise an APCu guard can survive sandbox directory cleanup and produce `sync_cooldown` before `dirty_comments` is exercised. +## Date-window fixture isolation policy + +Date-window-sensitive comment-export regressions use the plugin's existing `$GLOBALS['plugin_mastodon_test_now']` clock hook with a fixed UTC timestamp. The harness freezes the clock for the complete related fixture group and unsets the hook afterwards. Fixed entry/comment dates, FlatPress IDs, filenames, request payloads and diagnostics therefore remain reproducible without widening the configured synchronization window or replacing production logic with forced synchronization. + +## Mutable option fixture isolation policy + +The admin-assignment regression snapshots the normalized options returned by `plugin_mastodon_get_options()`, saves explicit values for `sync_scheduled_window_days`, `old_thread_reply_check` and `delete_sync_enabled`, asserts the assigned values against that local fixture, and restores the previous options afterwards. This prevents a preceding regression scenario from changing the test result while still exercising the real option persistence and admin-assignment paths. + ## Locally deleted imported remote replies When a FlatPress admin deletes a comment that originally came from a Mastodon reply (`source=remote`), the harness now treats that deletion as a local ignore decision. The comment-deleted hook must set `comment_tombstones`, remove the reverse `comments_remote` mapping and avoid queuing a Mastodon `DELETE` for a status that may belong to another account. Regression coverage verifies both the unchanged reply and the edited-remote-reply case so a later `/api/v1/statuses/:id/context` pass cannot re-create the local comment, and it preserves `source=local` ownership for already exported FlatPress comments seen again in the context response. @@ -94,276 +102,276 @@ The media-family policy is protected by dedicated tests. These tests call the re | Line | Static test name | | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 3201 | Simulation summary mode shortens verbose JSON details | -| 3213 | Simulation final summary reports exit code and status counters | -| 3234 | Simulation sandbox excludes live fp-content/content by default | -| 3245 | Simulation sandbox can include live fp-content/content only when explicitly requested | -| 3269 | Entry file scanner skips comment directories while keeping real FlatPress entries | -| 3328 | Scheduled direct YY/MM scanner keeps active-window entries and dirty-comment parents | -| 3377 | Scheduled direct scanner honors the 7-day admin window at parse stage | -| 3412 | Scheduled direct scanner honors the 14-day admin window across a month boundary | -| 3447 | Scheduled direct scanner honors the 30-day admin window across a year boundary | -| 3481 | Scheduled direct scanner treats more than three dirty entries as mandatory candidates | -| 3521 | Scheduled direct scanner treats more than three dirty-comment parents as mandatory candidates | -| 3548 | Manual force sync keeps the direct all-YY/MM scanner repair path | -| 3572 | Mastodon companion-plugin detection uses FlatPress central enabled-plugin state | -| 3581 | Mastodon companion-plugin detection respects FlatPress plugin_exists() for missing configured plugins | -| 3594 | Mastodon admin companion-plugin diagnostics cover BBCode, PhotoSwipe, AudioVideo, Tag and Emoticons | -| 3624 | One-way companion-plugin diagnostics hide import-only helpers and describe export helpers | -| 3643 | FlatPress locale is normalized to a Mastodon language code | -| 3661 | Mastodon HTML -> FlatPress BBCode | -| 3682 | Mastodon mention noise is cleaned for FlatPress | -| 3687 | Imported Mastodon entry keeps the Mastodon status link on a new line with target blank | -| 3694 | Imported Mastodon status footer BBCode opens the single toot in a new tab | -| 3700 | Imported Mastodon status footer attributes are stripped for outbound Mastodon text | -| 3708 | Imported Mastodon status footer renders target blank HTML for the single toot | -| 3718 | Mastodon emojis become FlatPress emoticons | -| 3726 | FlatPress BBCode -> Mastodon text | -| 3734 | FlatPress URLs lose HTML-like attributes and omit localhost links | -| 3812 | Comment author target blank is limited to external URLs | -| 3827 | FlatPress entry tags become a Mastodon hashtag footer | -| 3852 | Mastodon tags become FlatPress tag BBCode without a visible hashtag footer | -| 3875 | Removing Mastodon tags removes FlatPress tag BBCode on update | -| 3888 | FlatPress entry without tag BBCode has no Mastodon hashtag footer | -| 3901 | Mastodon head metadata uses the configured instance URL and username | -| 3914 | Mastodon head metadata tolerates usernames stored with a leading at-sign | -| 3927 | Mastodon head metadata stays silent when no username is configured | -| 3941 | Actual FlatPress comment exports as readable Mastodon reply | -| 3951 | FlatPress comment export appends the public comment link on a new line | -| 3962 | FlatPress entry export converts Emoticons plugin shortcodes in titles and bodies to Unicode emoji for Mastodon | -| 3981 | FlatPress comment export converts Emoticons plugin shortcodes in author lines and bodies to Unicode emoji for Mastodon | -| 3999 | Long local entry export is limited even without a public permalink | -| 4008 | FlatPress single-image entry exposes one uploadable media item | -| 4018 | FlatPress gallery entry exposes uploadable media items and keeps raw gallery BBCode out of Mastodon text | -| 4071 | FlatPress AudioVideo entry exposes described audio/video media items and strips raw player BBCode from Mastodon text | -| 4091 | AudioVideo BBCode parser renders optional description endtags as media attributes and keeps legacy single tags working | -| 4127 | Mastodon media upload sends AudioVideo files with descriptions and video thumbnail | -| 4152 | Mastodon media planner exports only images when an entry mixes image, audio and video media | -| 4163 | Mastodon media planner exports exactly one audio attachment when an entry mixes audio and video media | -| 4177 | Mastodon media planner exports one video and keeps its poster as an upload thumbnail only | -| 4190 | Mastodon media planner keeps multiple images up to the Mastodon media limit | -| 4219 | Mastodon export uploads only the selected audio item from an audio/video FlatPress entry | -| 4249 | Mastodon export sends a video poster as upload thumbnail instead of a second media ID | -| 4281 | Mastodon single image imports into a FlatPress image tag and stored file | -| 4305 | Mastodon import preserves long remote media descriptions without corrupting FlatPress image markup | -| 4349 | Mastodon multiple images import into a FlatPress gallery with captions | -| 4398 | Mastodon audio and video attachments import into FlatPress AudioVideo BBCode and stored files | -| 4437 | Mastodon audio/video import retries alternate direct media URLs when the first download candidate fails | -| 4451 | First sync is immediately due | -| 4454 | Next day after 23:00 is due | -| 4455 | Before configured time is not due | -| 4476 | Scheduled synchronization due checks use stored UTC time regardless of PHP default timezone | -| 4498 | Pending deletion synchronization due checks use UTC cooldowns regardless of PHP default timezone | -| 4529 | Admin state timestamps are formatted with FlatPress timeoffset regardless of PHP default timezone | -| 4544 | Scheduled content synchronization uses a short file/APCu cooldown guard | -| 4561 | Full Mastodon state fallback is not stored in APCu | -| 4593 | Central Mastodon rate-limit guard stops requests after the per-run request budget | -| 4627 | Central Mastodon rate-limit guard stops media uploads after the per-run media budget | -| 4651 | Central Mastodon rate-limit guard stops status deletions after the per-run delete budget | -| 4677 | Cached Mastodon versions before 4.4 delete statuses without the delete_media query parameter | -| 4701 | Status deletion retries without delete_media when an older Mastodon server rejects the query parameter | -| 4731 | Persistent Mastodon media-upload window budget is shared across forced runs | -| 4761 | Persistent media-upload window budget is written to the synchronization last error | -| 4788 | Persistent Mastodon delete window budget is shared across forced runs | -| 4812 | Persistent delete window budget is written to the synchronization last error | -| 4839 | Persistent Mastodon account-status paging window budget is shared across forced runs | -| 4862 | Persistent account-status paging window budget is written to the synchronization last error | -| 4885 | Admin synchronization time converts stored UTC to FlatPress local time and back | -| 4908 | Admin synchronization time conversion supports fractional FlatPress offsets | -| 4924 | Sync start date normalization accepts valid ISO dates | -| 4930 | Local and remote date helpers derive stable date keys | -| 4939 | Remote-to-local update toggle normalizes safely | -| 4950 | Comment-as-entry import toggle normalizes safely | -| 4972 | Comment/reply synchronization disable toggle normalizes and saves safely | -| 5003 | Mastodon comment opt-in is required only when local comments may be exported | -| 5029 | Mastodon comment opt-in markup is shown only while comment/reply sync is active | -| 5046 | State entry mapping created | -| 5047 | State comment mapping created | -| 5049 | State comment-to-reply opt-in marker created | -| 5050 | Local visitor comment without Mastodon opt-in is not eligible for Mastodon export | -| 5101 | Mastodon runtime files in fp-content/plugin_mastodon use FlatPress FILE_PERMISSIONS | -| 5140 | Initial local thread comment/reply sync is enabled | -| 5299 | OAuth code exchange | -| 5307 | State cache stays fresh after writing state.json | -| 5328 | Pretty-printed legacy state.json remains readable after compact-state optimization | -| 5376 | State write persists compact split state.json while preserving round-trip mappings | -| 5441 | Split comment shards support bounded per-entry state reads | -| 5465 | Partial split-state writes preserve unloaded comment shards | -| 5509 | Legacy inline comment state migrates to per-entry comment shards on write | -| 5584 | Legacy inline migration preserves non-contiguous comments and creates a migration backup | -| 5643 | Comment-shard diagnostics detect stale metadata and repair the reverse index from shards | -| 5664 | CLI comment-shard diagnostics entry point returns a machine-checkable status | -| 5699 | Shard-write failures leave the main state unchanged and return failure | -| 5738 | Main-state write failures after shard writes remain repairable from shard files | -| 5775 | Legacy combined stats are migrated into separate content and deletion counters | -| 5799 | Instance configuration cache avoids repeated /api/v2/instance requests | -| 5826 | Nightly Mastodon versions use cached api_versions for status edit and delete capabilities | -| 5849 | Machine-readable api_versions are preferred over complex human-readable Mastodon version strings | -| 5878 | api_versions below delete_media support suppress the delete_media query even with a nightly version string | -| 5904 | api_versions below unattached media delete support skip uploaded media cleanup DELETE requests | -| 5927 | Cached Mastodon versions before 4.4 skip unattached media DELETE cleanup requests | -| 5961 | Unknown unattached media delete capability stays best-effort without an instance lookup | -| 6000 | Mastodon API version 4 deletes unattached uploaded media during cleanup | -| 6031 | Mastodon 4.6 configuration.accounts limits survive compact instance snapshots | -| 6063 | Mastodon 4.6.0-nightly account-status import sends exclude_direct for privacy | -| 6095 | Mastodon API version 10 account-status import sends exclude_direct even on forked version strings | -| 6131 | Account-status import retries without exclude_direct when a compatible server rejects the parameter | -| 6166 | Cached Mastodon versions before 4.6 keep account-status import compatible without exclude_direct | -| 6195 | Failed instance-information lookups are negatively cached per request and fall back to defaults | -| 6232 | Mastodon URL budgeting respects characters_reserved_per_url when limiting plain-text exports | -| 6257 | Configured credentials verify (read-only smoke test) | -| 6269 | Entry export uses clean Mastodon text and suppresses localhost permalinks | -| 6277 | Comment export is reply-like, localized and emoji-aware | -| 6314 | Remote entry imported through sync with Mastodon media visible in FlatPress | -| 6320 | Deletion sync is queued for a follow-up request instead of running inside the content sync request | -| 6326 | Content sync updates the synchronization timestamp and counters while leaving the deletion timestamp for the follow-up request | -| 6345 | Private mention status is skipped during Mastodon import | -| 6351 | Remote imported entry keeps a stable FlatPress date | -| 6357 | Remote comment imported through sync | -| 6363 | Nested remote comment keeps parent mapping metadata | -| 6369 | Private mention reply is skipped during Mastodon import | -| 6407 | Entry export creates a top-level Mastodon status | -| 6413 | Comment to entry exports with in_reply_to_id on the entry status | -| 6419 | Comment to comment exports with in_reply_to_id on the parent reply | -| 6425 | Entry and comment exports include the configured Mastodon language code | -| 6444 | Deletion sync waits at least five minutes after a completed content sync | -| 6496 | Create and update status requests include the configured Mastodon language code | -| 6539 | Initial Mastodon media uploads include the attachment description in POST /api/v2/media | -| 6571 | Unchanged entry media is reused without a new upload when only the post text changes | -| 6644 | Mastodon 4.1+ updates changed image descriptions through status media_attributes without re-uploading media | -| 6681 | Older Mastodon versions fall back to a fresh upload when only the media description changes | -| 6768 | Full local-to-remote sync reuses stored media IDs when the attachments did not change | -| 6868 | Full sync updates changed media descriptions through status media_attributes on Mastodon 4.1+ | -| 6991 | Batch entry export keeps older FlatPress entries below newer ones on Mastodon | -| 7094 | New local comment on an already synchronized older entry is exported to Mastodon | -| 7173 | Disabled comment/reply sync keeps local comments off Mastodon | -| 7252 | Disabled comment/reply sync skips remote reply deletion follow-up | -| 7348 | Known synchronized entry mappings older than the sync start date do not trigger context refreshes | -| 7422 | Scheduled content synchronization respects the automatic recent-content window | -| 7448 | Productive content-sync workset loads only active scheduled comment shards | -| 7507 | Normal manual synchronization bypasses the daily due check but still respects the automatic window | -| 7569 | Explicit full manual synchronization bypasses the automatic window while keeping normal limits | -| 7648 | Older changed mapped FlatPress entries are synchronized through the dirty queue | -| 7700 | Post-success comment hook queues older changed mapped comments | -| 7730 | Scheduled sync updates older dirty comments through direct YY/MM dirty-parent candidates | -| 7777 | Remote-write guard suppresses dirty tracking for Mastodon-owned entry_save calls | -| 7866 | Large scheduled dirty-tracking sync parses only active-window and dirty entries | -| 7949 | Optional old-thread reply checks rotate through known synchronized threads | -| 7990 | Disabled old-thread reply checks do not refresh known synchronized entry contexts | -| 8071 | Disabled comment/reply sync imports Mastodon entries without fetching reply contexts or notifications | -| 8150 | Automatic scheduled synchronization rotates known synchronized threads through the full sync path | -| 8236 | Notification hints import a new reply on an old mapped Mastodon entry without context rotation | -| 8252 | Notification fallback payloads are ignored when a normal mention status is available | -| 8321 | Notification hints import a new reply on an old mapped Mastodon reply with reply metadata | -| 8426 | Notification context hints use the old-thread budget before normal rotation | -| 8477 | Reply-notification polling is skipped until read:notifications is authorized | -| 8600 | Remote updates do not overwrite existing local content when the toggle is disabled | -| 8620 | Remote updates overwrite existing local content when the toggle is enabled | -| 8645 | FlatPress single-image entry exports Mastodon media_ids and strips raw image BBCode | -| 8656 | FlatPress gallery entry exports Mastodon media_ids up to the instance limit and strips raw gallery BBCode | -| 8794 | Sync start date filters local exports by entry and comment date | -| 8880 | Sync start date filters remote imports by status and reply date | -| 8972 | Remote Mastodon imports honor the FlatPress timeoffset for stored entry/comment dates and ordering keys | -| 9043 | Remote sync start filtering respects the FlatPress timeoffset near midnight | -| 9108 | Sync reports local export failures instead of silently succeeding | -| 9126 | Shared-request synchronization refreshes the PHP execution budget for long Mastodon work | -| 9209 | Synchronized local comments are not imported as duplicate entries from Mastodon by default | -| 9301 | Synchronized local comments may be imported as entries when the toggle is enabled | -| 9436 | Local FlatPress comments on imported Mastodon entries sync back to Mastodon, including nested replies | -| 9512 | Asynchronous Mastodon media uploads are polled until the attachment is ready | -| 9536 | Long-running Mastodon media uploads refresh the PHP execution budget for upload and polling | -| 9608 | Slow asynchronous Mastodon video uploads keep polling beyond the old short retry window | -| 9674 | Mastodon audio uploads keep polling even when pending responses have no preview_url | -| 9739 | Follow-up deletion sync removes remote Mastodon content after local FlatPress deletion | -| 9764 | Follow-up deletion sync updates last_deletion_run without overwriting the last synchronization timestamp or content counters | -| 9820 | Deletion sync falls back to DELETE without delete_media for older Mastodon servers | -| 9883 | Follow-up deletion sync removes mirrored FlatPress content after remote Mastodon deletion | -| 9938 | Deletion sync skips old locally deleted mappings outside the sync start date window | -| 9988 | Deletion sync skips old remotely mirrored mappings outside the sync start date window | -| 10045 | Scheduled deletion sync skips remote lookups outside the automatic scheduled window | -| 10118 | Scheduled deletion sync still propagates old local deletions outside the automatic window | -| 10179 | Large scheduled deletion syncs resume from the saved entry cursor | -| 10263 | Large scheduled deletion syncs resume from the saved comment cursor inside one entry shard | -| 10308 | Disabling deletion synchronization clears pending delete work without issuing deletion requests | -| 10328 | Content sync before deletion sync protects a locally deleted exported FlatPress comment from stale re-import | -| 10347 | Deletion sync after the protected content sync reattaches the imported descendant reply to the synchronized entry status and keeps a later verification pending | -| 10390 | Normal manual partial content sync keeps an old locally deleted exported comment tombstoned without stale re-import | -| 10429 | Automatic scheduled partial content sync keeps an old locally deleted exported comment tombstoned without stale re-import | -| 10466 | Remote import defensively tombstones a missing source=local comment when the deletion hook was bypassed | -| 10493 | Comment mapping rejects duplicate remote ids and removes the previous reverse id on a legitimate remap | -| 10520 | Deletion sync keeps imported descendants for a later follow-up verification when Mastodon still returns the child reply once | -| 10537 | Tombstones prevent re-importing the deleted exported parent comment while descendant rechecks are still pending | -| 10550 | A later follow-up deletion sync removes the imported descendant once Mastodon reports the child reply missing | -| 10581 | Content sync before deletion sync also protects a locally deleted exported FlatPress comment when the imported descendant reply already has its own child | -| 10601 | Deletion sync after the protected content sync keeps the descendant child below the reattached reply and verifies both remote replies in the same pass | -| 10641 | Deletion sync removes imported descendants immediately when Mastodon already reports the child reply missing in the first follow-up pass | -| 10672 | Hybrid descendant rechecks keep only the direct child queued after the first stale follow-up pass of a deeper reply chain | -| 10691 | Hybrid descendant rechecks remove the direct child and its direct child within the same targeted follow-up request | -| 10716 | Locally deleted imported external Mastodon reply is tombstoned immediately and not re-imported unchanged | -| 10741 | Locally deleted imported external Mastodon reply is not re-imported after the remote author edits it | -| 10764 | Deletion sync treats locally deleted imported remote replies as local ignore decisions without remote DELETE | -| 10846 | Remote Mastodon comment updates increment the local-comment update counter instead of the entry counter | -| 10878 | Admin assignment exposes split sync counters, local admin timestamps, and the deletion-sync option | -| 10968 | Admin maintenance actions are separated into a dedicated template reached from the main plugin page | -| 11059 | Media upload cleanup deletes already uploaded attachments when a later media upload fails | -| 11126 | Media upload cleanup deletes uploaded attachments when final status creation fails | -| 11184 | OAuth scope discovery prefers the profile scope on current Mastodon instances | -| 11230 | OAuth scope discovery falls back to read:accounts plus notifications on older Mastodon instances | -| 11282 | Existing registered apps keep the legacy read:accounts scope until they are re-registered | -| 11373 | Instance information refresh persists a compact snapshot including the exact Mastodon version | -| 11419 | Admin assignment exposes cached instance-information rows without triggering another live instance request | -| 11436 | Changing the configured instance URL invalidates the saved instance-information snapshot | -| 11474 | Sync-related instance limit helpers reuse the stored instance snapshot without another /api/v2/instance request | -| 11496 | Manual sync exports FlatPress comments on remote-sourced entries to Mastodon replies | -| 11514 | Replies to FlatPress comments on remote-sourced entries are exported as replies to the parent Mastodon reply | -| 11544 | Unsynchronized local parent comment is exported before local child reply | -| 11564 | Non-forced sync exports FlatPress comments on remote-sourced entries to Mastodon replies | -| 11587 | Manual sync exports Emoticons plugin shortcodes from FlatPress entries to Unicode emoji in Mastodon status requests | -| 11603 | Manual sync exports Emoticons plugin shortcodes from FlatPress comments to Unicode emoji in Mastodon reply requests | -| 11624 | Manual sync imports another Mastodon member reply to an exported FlatPress comment only as a FlatPress comment when comment-as-entry import is disabled | -| 11650 | Non-forced sync imports another Mastodon member reply to an exported FlatPress comment only as a FlatPress comment when comment-as-entry import is disabled | -| 11673 | Another Mastodon member reply to an exported FlatPress comment is still imported as a FlatPress comment when comment-as-entry import is enabled | -| 11690 | Disabling the quote option imports a Mastodon reply to an exported FlatPress comment without a leading quote block | -| 11704 | Imported Mastodon replies to another Mastodon reply quote the replied-to Mastodon user and text by default | -| 11723 | Disabling the quote option imports Mastodon reply-to-reply comments without a leading quote block | -| 11737 | Manual sync imports a Mastodon self-reply to an exported FlatPress comment only as a FlatPress comment | -| 11761 | Non-forced sync imports a Mastodon self-reply to an exported FlatPress comment only as a FlatPress comment | -| 11814 | Scheduler state is written as a compact summary without full mapping arrays | -| 11833 | Fresh scheduler-state read uses the APCu-capable FlatPress I/O path and does not load full state.json | -| 11862 | Stale scheduler-state falls back to the full state and rebuilds the summary | -| 11883 | Manual admin synchronization still loads the full state before reporting configuration errors | -| 11902 | sync.log uses append-only writes with size-based rotation | -| 11928 | Large skip volumes are logged as aggregate summaries | -| 11941 | Small 300x10 state keeps scheduler-state compact and disables full APCu fallback | -| 11963 | Fresh small scheduler-state read avoids full state.json and uses APCu-capable file I/O | -| 11986 | Large 3000x10 state keeps scheduler-state compact and disables full APCu fallback | -| 12009 | Fresh large scheduler-state read avoids full state.json and uses APCu-capable file I/O | -| 12125 | Automatic scheduled sync exports a new current comment on an old mapped entry through dirty_comments | -| 12223 | Authenticated FlatPress comments export after a stored LOGGEDIN marker without visitor opt-in | -| 12334 | Admin session does not grant Mastodon export to visitor comments without opt-in | -| 12400 | Visitor Mastodon opt-in checkbox preserves POST state and hook priorities/signatures are explicit | -| 12502 | Visitor opt-in persists through the direct FlatPress save path when CommentCenter is disabled | -| 12681 | CommentCenter preserves visitor Mastodon opt-in until admin approval and export | -| 12819 | CommentCenter rejection and bounded pruning remove orphaned pending opt-in grants | -| 12925 | Visitor opt-in is stored without complete credentials and later permits comment export | -| 13030 | Opted-in visitor replies export even when their local parent has no Mastodon opt-in | -| 13089 | One-way mode option is disabled by default, normalized, assigned to admin UI, templated and translated | -| 13129 | One-way admin save preserves hidden import options while keeping one-way mode enabled | -| 13146 | One-way admin save preserves hidden import options when one-way mode is disabled in the same submit | -| 13166 | Bidirectional admin save still stores visible unchecked import options as disabled | -| 13198 | One-way admin template hides import controls, notification hints and import-only counters | -| 13224 | One-way admin template keeps export, OAuth, instance, token, state and deletion outputs visible | -| 13240 | One-way admin assignment hides import-only companion plugins and uses one-way companion intro | -| 13346 | One-way mode blocks Mastodon-to-FlatPress imports while FlatPress-to-Mastodon export still runs | -| 13433 | Automatic one-way content sync refreshes the Mastodon widget profile cache without remote import reads | -| 13508 | One-way deletion sync keeps local content, removes stale remote mappings and queues re-export | -| 13583 | One-way content sync re-exports local objects whose remote mappings were unlinked after remote deletion | -| 13652 | One-way pending descendant rechecks keep local comments and queue them for re-export instead of deleting them | -| 13673 | Mastodon widget remains hidden and performs no HTTP requests while its local profile cache is missing | -| 13716 | Mastodon widget profile cache is refreshed from verify_credentials and stores only public profile data plus a local avatar | -| 13741 | Mastodon widget renders compact local-cache markup without remote API calls or inline CSS | -| 13765 | Mastodon widget stylesheet is loaded by plugin_mastodon_head as a versioned CSS asset | -| 13780 | Mastodon widget profile refresh reuses the cached avatar when the Mastodon avatar URL is unchanged | -| 13807 | Mastodon widget uses a localized fallback avatar alt text for Mastodon 4.0-4.5 account payloads without avatar_description | -| 13826 | Mastodon widget hides incomplete local profile caches instead of falling back to remote avatar URLs | -| 13851 | Mastodon widget is registered and translated in all FlatPress plugin language files | +| 3207 | Simulation summary mode shortens verbose JSON details | +| 3219 | Simulation final summary reports exit code and status counters | +| 3240 | Simulation sandbox excludes live fp-content/content by default | +| 3251 | Simulation sandbox can include live fp-content/content only when explicitly requested | +| 3275 | Entry file scanner skips comment directories while keeping real FlatPress entries | +| 3334 | Scheduled direct YY/MM scanner keeps active-window entries and dirty-comment parents | +| 3383 | Scheduled direct scanner honors the 7-day admin window at parse stage | +| 3418 | Scheduled direct scanner honors the 14-day admin window across a month boundary | +| 3453 | Scheduled direct scanner honors the 30-day admin window across a year boundary | +| 3487 | Scheduled direct scanner treats more than three dirty entries as mandatory candidates | +| 3527 | Scheduled direct scanner treats more than three dirty-comment parents as mandatory candidates | +| 3554 | Manual force sync keeps the direct all-YY/MM scanner repair path | +| 3578 | Mastodon companion-plugin detection uses FlatPress central enabled-plugin state | +| 3587 | Mastodon companion-plugin detection respects FlatPress plugin_exists() for missing configured plugins | +| 3600 | Mastodon admin companion-plugin diagnostics cover BBCode, PhotoSwipe, AudioVideo, Tag and Emoticons | +| 3630 | One-way companion-plugin diagnostics hide import-only helpers and describe export helpers | +| 3649 | FlatPress locale is normalized to a Mastodon language code | +| 3667 | Mastodon HTML -> FlatPress BBCode | +| 3688 | Mastodon mention noise is cleaned for FlatPress | +| 3693 | Imported Mastodon entry keeps the Mastodon status link on a new line with target blank | +| 3700 | Imported Mastodon status footer BBCode opens the single toot in a new tab | +| 3706 | Imported Mastodon status footer attributes are stripped for outbound Mastodon text | +| 3714 | Imported Mastodon status footer renders target blank HTML for the single toot | +| 3724 | Mastodon emojis become FlatPress emoticons | +| 3732 | FlatPress BBCode -> Mastodon text | +| 3740 | FlatPress URLs lose HTML-like attributes and omit localhost links | +| 3818 | Comment author target blank is limited to external URLs | +| 3833 | FlatPress entry tags become a Mastodon hashtag footer | +| 3858 | Mastodon tags become FlatPress tag BBCode without a visible hashtag footer | +| 3881 | Removing Mastodon tags removes FlatPress tag BBCode on update | +| 3894 | FlatPress entry without tag BBCode has no Mastodon hashtag footer | +| 3907 | Mastodon head metadata uses the configured instance URL and username | +| 3920 | Mastodon head metadata tolerates usernames stored with a leading at-sign | +| 3933 | Mastodon head metadata stays silent when no username is configured | +| 3947 | Actual FlatPress comment exports as readable Mastodon reply | +| 3957 | FlatPress comment export appends the public comment link on a new line | +| 3968 | FlatPress entry export converts Emoticons plugin shortcodes in titles and bodies to Unicode emoji for Mastodon | +| 3987 | FlatPress comment export converts Emoticons plugin shortcodes in author lines and bodies to Unicode emoji for Mastodon | +| 4005 | Long local entry export is limited even without a public permalink | +| 4014 | FlatPress single-image entry exposes one uploadable media item | +| 4024 | FlatPress gallery entry exposes uploadable media items and keeps raw gallery BBCode out of Mastodon text | +| 4077 | FlatPress AudioVideo entry exposes described audio/video media items and strips raw player BBCode from Mastodon text | +| 4097 | AudioVideo BBCode parser renders optional description endtags as media attributes and keeps legacy single tags working | +| 4133 | Mastodon media upload sends AudioVideo files with descriptions and video thumbnail | +| 4158 | Mastodon media planner exports only images when an entry mixes image, audio and video media | +| 4169 | Mastodon media planner exports exactly one audio attachment when an entry mixes audio and video media | +| 4183 | Mastodon media planner exports one video and keeps its poster as an upload thumbnail only | +| 4196 | Mastodon media planner keeps multiple images up to the Mastodon media limit | +| 4225 | Mastodon export uploads only the selected audio item from an audio/video FlatPress entry | +| 4255 | Mastodon export sends a video poster as upload thumbnail instead of a second media ID | +| 4287 | Mastodon single image imports into a FlatPress image tag and stored file | +| 4311 | Mastodon import preserves long remote media descriptions without corrupting FlatPress image markup | +| 4355 | Mastodon multiple images import into a FlatPress gallery with captions | +| 4404 | Mastodon audio and video attachments import into FlatPress AudioVideo BBCode and stored files | +| 4443 | Mastodon audio/video import retries alternate direct media URLs when the first download candidate fails | +| 4457 | First sync is immediately due | +| 4460 | Next day after 23:00 is due | +| 4461 | Before configured time is not due | +| 4482 | Scheduled synchronization due checks use stored UTC time regardless of PHP default timezone | +| 4504 | Pending deletion synchronization due checks use UTC cooldowns regardless of PHP default timezone | +| 4535 | Admin state timestamps are formatted with FlatPress timeoffset regardless of PHP default timezone | +| 4550 | Scheduled content synchronization uses a short file/APCu cooldown guard | +| 4567 | Full Mastodon state fallback is not stored in APCu | +| 4599 | Central Mastodon rate-limit guard stops requests after the per-run request budget | +| 4633 | Central Mastodon rate-limit guard stops media uploads after the per-run media budget | +| 4657 | Central Mastodon rate-limit guard stops status deletions after the per-run delete budget | +| 4683 | Cached Mastodon versions before 4.4 delete statuses without the delete_media query parameter | +| 4707 | Status deletion retries without delete_media when an older Mastodon server rejects the query parameter | +| 4737 | Persistent Mastodon media-upload window budget is shared across forced runs | +| 4767 | Persistent media-upload window budget is written to the synchronization last error | +| 4794 | Persistent Mastodon delete window budget is shared across forced runs | +| 4818 | Persistent delete window budget is written to the synchronization last error | +| 4845 | Persistent Mastodon account-status paging window budget is shared across forced runs | +| 4868 | Persistent account-status paging window budget is written to the synchronization last error | +| 4891 | Admin synchronization time converts stored UTC to FlatPress local time and back | +| 4914 | Admin synchronization time conversion supports fractional FlatPress offsets | +| 4930 | Sync start date normalization accepts valid ISO dates | +| 4936 | Local and remote date helpers derive stable date keys | +| 4945 | Remote-to-local update toggle normalizes safely | +| 4956 | Comment-as-entry import toggle normalizes safely | +| 4978 | Comment/reply synchronization disable toggle normalizes and saves safely | +| 5009 | Mastodon comment opt-in is required only when local comments may be exported | +| 5035 | Mastodon comment opt-in markup is shown only while comment/reply sync is active | +| 5052 | State entry mapping created | +| 5053 | State comment mapping created | +| 5055 | State comment-to-reply opt-in marker created | +| 5056 | Local visitor comment without Mastodon opt-in is not eligible for Mastodon export | +| 5107 | Mastodon runtime files in fp-content/plugin_mastodon use FlatPress FILE_PERMISSIONS | +| 5146 | Initial local thread comment/reply sync is enabled | +| 5305 | OAuth code exchange | +| 5313 | State cache stays fresh after writing state.json | +| 5334 | Pretty-printed legacy state.json remains readable after compact-state optimization | +| 5382 | State write persists compact split state.json while preserving round-trip mappings | +| 5447 | Split comment shards support bounded per-entry state reads | +| 5471 | Partial split-state writes preserve unloaded comment shards | +| 5515 | Legacy inline comment state migrates to per-entry comment shards on write | +| 5590 | Legacy inline migration preserves non-contiguous comments and creates a migration backup | +| 5649 | Comment-shard diagnostics detect stale metadata and repair the reverse index from shards | +| 5670 | CLI comment-shard diagnostics entry point returns a machine-checkable status | +| 5705 | Shard-write failures leave the main state unchanged and return failure | +| 5744 | Main-state write failures after shard writes remain repairable from shard files | +| 5781 | Legacy combined stats are migrated into separate content and deletion counters | +| 5805 | Instance configuration cache avoids repeated /api/v2/instance requests | +| 5832 | Nightly Mastodon versions use cached api_versions for status edit and delete capabilities | +| 5855 | Machine-readable api_versions are preferred over complex human-readable Mastodon version strings | +| 5884 | api_versions below delete_media support suppress the delete_media query even with a nightly version string | +| 5910 | api_versions below unattached media delete support skip uploaded media cleanup DELETE requests | +| 5933 | Cached Mastodon versions before 4.4 skip unattached media DELETE cleanup requests | +| 5967 | Unknown unattached media delete capability stays best-effort without an instance lookup | +| 6006 | Mastodon API version 4 deletes unattached uploaded media during cleanup | +| 6037 | Mastodon 4.6 configuration.accounts limits survive compact instance snapshots | +| 6069 | Mastodon 4.6.0-nightly account-status import sends exclude_direct for privacy | +| 6101 | Mastodon API version 10 account-status import sends exclude_direct even on forked version strings | +| 6137 | Account-status import retries without exclude_direct when a compatible server rejects the parameter | +| 6172 | Cached Mastodon versions before 4.6 keep account-status import compatible without exclude_direct | +| 6201 | Failed instance-information lookups are negatively cached per request and fall back to defaults | +| 6238 | Mastodon URL budgeting respects characters_reserved_per_url when limiting plain-text exports | +| 6263 | Configured credentials verify (read-only smoke test) | +| 6275 | Entry export uses clean Mastodon text and suppresses localhost permalinks | +| 6283 | Comment export is reply-like, localized and emoji-aware | +| 6320 | Remote entry imported through sync with Mastodon media visible in FlatPress | +| 6326 | Deletion sync is queued for a follow-up request instead of running inside the content sync request | +| 6332 | Content sync updates the synchronization timestamp and counters while leaving the deletion timestamp for the follow-up request | +| 6351 | Private mention status is skipped during Mastodon import | +| 6357 | Remote imported entry keeps a stable FlatPress date | +| 6363 | Remote comment imported through sync | +| 6369 | Nested remote comment keeps parent mapping metadata | +| 6375 | Private mention reply is skipped during Mastodon import | +| 6413 | Entry export creates a top-level Mastodon status | +| 6419 | Comment to entry exports with in_reply_to_id on the entry status | +| 6425 | Comment to comment exports with in_reply_to_id on the parent reply | +| 6431 | Entry and comment exports include the configured Mastodon language code | +| 6450 | Deletion sync waits at least five minutes after a completed content sync | +| 6502 | Create and update status requests include the configured Mastodon language code | +| 6545 | Initial Mastodon media uploads include the attachment description in POST /api/v2/media | +| 6577 | Unchanged entry media is reused without a new upload when only the post text changes | +| 6650 | Mastodon 4.1+ updates changed image descriptions through status media_attributes without re-uploading media | +| 6687 | Older Mastodon versions fall back to a fresh upload when only the media description changes | +| 6774 | Full local-to-remote sync reuses stored media IDs when the attachments did not change | +| 6874 | Full sync updates changed media descriptions through status media_attributes on Mastodon 4.1+ | +| 6997 | Batch entry export keeps older FlatPress entries below newer ones on Mastodon | +| 7100 | New local comment on an already synchronized older entry is exported to Mastodon | +| 7179 | Disabled comment/reply sync keeps local comments off Mastodon | +| 7258 | Disabled comment/reply sync skips remote reply deletion follow-up | +| 7354 | Known synchronized entry mappings older than the sync start date do not trigger context refreshes | +| 7428 | Scheduled content synchronization respects the automatic recent-content window | +| 7454 | Productive content-sync workset loads only active scheduled comment shards | +| 7513 | Normal manual synchronization bypasses the daily due check but still respects the automatic window | +| 7575 | Explicit full manual synchronization bypasses the automatic window while keeping normal limits | +| 7654 | Older changed mapped FlatPress entries are synchronized through the dirty queue | +| 7706 | Post-success comment hook queues older changed mapped comments | +| 7736 | Scheduled sync updates older dirty comments through direct YY/MM dirty-parent candidates | +| 7783 | Remote-write guard suppresses dirty tracking for Mastodon-owned entry_save calls | +| 7872 | Large scheduled dirty-tracking sync parses only active-window and dirty entries | +| 7955 | Optional old-thread reply checks rotate through known synchronized threads | +| 7996 | Disabled old-thread reply checks do not refresh known synchronized entry contexts | +| 8077 | Disabled comment/reply sync imports Mastodon entries without fetching reply contexts or notifications | +| 8156 | Automatic scheduled synchronization rotates known synchronized threads through the full sync path | +| 8242 | Notification hints import a new reply on an old mapped Mastodon entry without context rotation | +| 8258 | Notification fallback payloads are ignored when a normal mention status is available | +| 8327 | Notification hints import a new reply on an old mapped Mastodon reply with reply metadata | +| 8432 | Notification context hints use the old-thread budget before normal rotation | +| 8483 | Reply-notification polling is skipped until read:notifications is authorized | +| 8606 | Remote updates do not overwrite existing local content when the toggle is disabled | +| 8626 | Remote updates overwrite existing local content when the toggle is enabled | +| 8651 | FlatPress single-image entry exports Mastodon media_ids and strips raw image BBCode | +| 8662 | FlatPress gallery entry exports Mastodon media_ids up to the instance limit and strips raw gallery BBCode | +| 8800 | Sync start date filters local exports by entry and comment date | +| 8886 | Sync start date filters remote imports by status and reply date | +| 8978 | Remote Mastodon imports honor the FlatPress timeoffset for stored entry/comment dates and ordering keys | +| 9049 | Remote sync start filtering respects the FlatPress timeoffset near midnight | +| 9114 | Sync reports local export failures instead of silently succeeding | +| 9132 | Shared-request synchronization refreshes the PHP execution budget for long Mastodon work | +| 9215 | Synchronized local comments are not imported as duplicate entries from Mastodon by default | +| 9307 | Synchronized local comments may be imported as entries when the toggle is enabled | +| 9442 | Local FlatPress comments on imported Mastodon entries sync back to Mastodon, including nested replies | +| 9518 | Asynchronous Mastodon media uploads are polled until the attachment is ready | +| 9542 | Long-running Mastodon media uploads refresh the PHP execution budget for upload and polling | +| 9614 | Slow asynchronous Mastodon video uploads keep polling beyond the old short retry window | +| 9680 | Mastodon audio uploads keep polling even when pending responses have no preview_url | +| 9745 | Follow-up deletion sync removes remote Mastodon content after local FlatPress deletion | +| 9770 | Follow-up deletion sync updates last_deletion_run without overwriting the last synchronization timestamp or content counters | +| 9826 | Deletion sync falls back to DELETE without delete_media for older Mastodon servers | +| 9889 | Follow-up deletion sync removes mirrored FlatPress content after remote Mastodon deletion | +| 9944 | Deletion sync skips old locally deleted mappings outside the sync start date window | +| 9994 | Deletion sync skips old remotely mirrored mappings outside the sync start date window | +| 10051 | Scheduled deletion sync skips remote lookups outside the automatic scheduled window | +| 10124 | Scheduled deletion sync still propagates old local deletions outside the automatic window | +| 10185 | Large scheduled deletion syncs resume from the saved entry cursor | +| 10269 | Large scheduled deletion syncs resume from the saved comment cursor inside one entry shard | +| 10314 | Disabling deletion synchronization clears pending delete work without issuing deletion requests | +| 10334 | Content sync before deletion sync protects a locally deleted exported FlatPress comment from stale re-import | +| 10353 | Deletion sync after the protected content sync reattaches the imported descendant reply to the synchronized entry status and keeps a later verification pending | +| 10396 | Normal manual partial content sync keeps an old locally deleted exported comment tombstoned without stale re-import | +| 10435 | Automatic scheduled partial content sync keeps an old locally deleted exported comment tombstoned without stale re-import | +| 10472 | Remote import defensively tombstones a missing source=local comment when the deletion hook was bypassed | +| 10499 | Comment mapping rejects duplicate remote ids and removes the previous reverse id on a legitimate remap | +| 10526 | Deletion sync keeps imported descendants for a later follow-up verification when Mastodon still returns the child reply once | +| 10543 | Tombstones prevent re-importing the deleted exported parent comment while descendant rechecks are still pending | +| 10556 | A later follow-up deletion sync removes the imported descendant once Mastodon reports the child reply missing | +| 10587 | Content sync before deletion sync also protects a locally deleted exported FlatPress comment when the imported descendant reply already has its own child | +| 10607 | Deletion sync after the protected content sync keeps the descendant child below the reattached reply and verifies both remote replies in the same pass | +| 10647 | Deletion sync removes imported descendants immediately when Mastodon already reports the child reply missing in the first follow-up pass | +| 10678 | Hybrid descendant rechecks keep only the direct child queued after the first stale follow-up pass of a deeper reply chain | +| 10697 | Hybrid descendant rechecks remove the direct child and its direct child within the same targeted follow-up request | +| 10722 | Locally deleted imported external Mastodon reply is tombstoned immediately and not re-imported unchanged | +| 10747 | Locally deleted imported external Mastodon reply is not re-imported after the remote author edits it | +| 10770 | Deletion sync treats locally deleted imported remote replies as local ignore decisions without remote DELETE | +| 10852 | Remote Mastodon comment updates increment the local-comment update counter instead of the entry counter | +| 10893 | Admin assignment exposes split sync counters, local timestamps, old-thread and deletion-sync options | +| 10986 | Admin maintenance actions are separated into a dedicated template reached from the main plugin page | +| 11077 | Media upload cleanup deletes already uploaded attachments when a later media upload fails | +| 11144 | Media upload cleanup deletes uploaded attachments when final status creation fails | +| 11202 | OAuth scope discovery prefers the profile scope on current Mastodon instances | +| 11248 | OAuth scope discovery falls back to read:accounts plus notifications on older Mastodon instances | +| 11300 | Existing registered apps keep the legacy read:accounts scope until they are re-registered | +| 11391 | Instance information refresh persists a compact snapshot including the exact Mastodon version | +| 11437 | Admin assignment exposes cached instance-information rows without triggering another live instance request | +| 11454 | Changing the configured instance URL invalidates the saved instance-information snapshot | +| 11492 | Sync-related instance limit helpers reuse the stored instance snapshot without another /api/v2/instance request | +| 11514 | Manual sync exports FlatPress comments on remote-sourced entries to Mastodon replies | +| 11532 | Replies to FlatPress comments on remote-sourced entries are exported as replies to the parent Mastodon reply | +| 11562 | Unsynchronized local parent comment is exported before local child reply | +| 11582 | Non-forced sync exports FlatPress comments on remote-sourced entries to Mastodon replies | +| 11605 | Manual sync exports Emoticons plugin shortcodes from FlatPress entries to Unicode emoji in Mastodon status requests | +| 11621 | Manual sync exports Emoticons plugin shortcodes from FlatPress comments to Unicode emoji in Mastodon reply requests | +| 11642 | Manual sync imports another Mastodon member reply to an exported FlatPress comment only as a FlatPress comment when comment-as-entry import is disabled | +| 11668 | Non-forced sync imports another Mastodon member reply to an exported FlatPress comment only as a FlatPress comment when comment-as-entry import is disabled | +| 11691 | Another Mastodon member reply to an exported FlatPress comment is still imported as a FlatPress comment when comment-as-entry import is enabled | +| 11708 | Disabling the quote option imports a Mastodon reply to an exported FlatPress comment without a leading quote block | +| 11722 | Imported Mastodon replies to another Mastodon reply quote the replied-to Mastodon user and text by default | +| 11741 | Disabling the quote option imports Mastodon reply-to-reply comments without a leading quote block | +| 11755 | Manual sync imports a Mastodon self-reply to an exported FlatPress comment only as a FlatPress comment | +| 11779 | Non-forced sync imports a Mastodon self-reply to an exported FlatPress comment only as a FlatPress comment | +| 11832 | Scheduler state is written as a compact summary without full mapping arrays | +| 11851 | Fresh scheduler-state read uses the APCu-capable FlatPress I/O path and does not load full state.json | +| 11880 | Stale scheduler-state falls back to the full state and rebuilds the summary | +| 11901 | Manual admin synchronization still loads the full state before reporting configuration errors | +| 11920 | sync.log uses append-only writes with size-based rotation | +| 11946 | Large skip volumes are logged as aggregate summaries | +| 11959 | Small 300x10 state keeps scheduler-state compact and disables full APCu fallback | +| 11981 | Fresh small scheduler-state read avoids full state.json and uses APCu-capable file I/O | +| 12004 | Large 3000x10 state keeps scheduler-state compact and disables full APCu fallback | +| 12027 | Fresh large scheduler-state read avoids full state.json and uses APCu-capable file I/O | +| 12143 | Automatic scheduled sync exports a new current comment on an old mapped entry through dirty_comments | +| 12246 | Authenticated FlatPress comments export after a stored LOGGEDIN marker without visitor opt-in | +| 12357 | Admin session does not grant Mastodon export to visitor comments without opt-in | +| 12423 | Visitor Mastodon opt-in checkbox preserves POST state and hook priorities/signatures are explicit | +| 12525 | Visitor opt-in persists through the direct FlatPress save path when CommentCenter is disabled | +| 12704 | CommentCenter preserves visitor Mastodon opt-in until admin approval and export | +| 12842 | CommentCenter rejection and bounded pruning remove orphaned pending opt-in grants | +| 12948 | Visitor opt-in is stored without complete credentials and later permits comment export | +| 13053 | Opted-in visitor replies export even when their local parent has no Mastodon opt-in | +| 13114 | One-way mode option is disabled by default, normalized, assigned to admin UI, templated and translated | +| 13154 | One-way admin save preserves hidden import options while keeping one-way mode enabled | +| 13171 | One-way admin save preserves hidden import options when one-way mode is disabled in the same submit | +| 13191 | Bidirectional admin save still stores visible unchecked import options as disabled | +| 13223 | One-way admin template hides import controls, notification hints and import-only counters | +| 13249 | One-way admin template keeps export, OAuth, instance, token, state and deletion outputs visible | +| 13265 | One-way admin assignment hides import-only companion plugins and uses one-way companion intro | +| 13371 | One-way mode blocks Mastodon-to-FlatPress imports while FlatPress-to-Mastodon export still runs | +| 13458 | Automatic one-way content sync refreshes the Mastodon widget profile cache without remote import reads | +| 13533 | One-way deletion sync keeps local content, removes stale remote mappings and queues re-export | +| 13608 | One-way content sync re-exports local objects whose remote mappings were unlinked after remote deletion | +| 13677 | One-way pending descendant rechecks keep local comments and queue them for re-export instead of deleting them | +| 13698 | Mastodon widget remains hidden and performs no HTTP requests while its local profile cache is missing | +| 13741 | Mastodon widget profile cache is refreshed from verify_credentials and stores only public profile data plus a local avatar | +| 13766 | Mastodon widget renders compact local-cache markup without remote API calls or inline CSS | +| 13790 | Mastodon widget stylesheet is loaded by plugin_mastodon_head as a versioned CSS asset | +| 13805 | Mastodon widget profile refresh reuses the cached avatar when the Mastodon avatar URL is unchanged | +| 13832 | Mastodon widget uses a localized fallback avatar alt text for Mastodon 4.0-4.5 account payloads without avatar_description | +| 13851 | Mastodon widget hides incomplete local profile caches instead of falling back to remote avatar URLs | +| 13876 | Mastodon widget is registered and translated in all FlatPress plugin language files | diff --git a/fp-plugins/mastodon/developer-docs/06-Process-Flow.md b/fp-plugins/mastodon/developer-docs/06-Process-Flow.md index a62f7b30..1a16e0d4 100644 --- a/fp-plugins/mastodon/developer-docs/06-Process-Flow.md +++ b/fp-plugins/mastodon/developer-docs/06-Process-Flow.md @@ -1476,14 +1476,16 @@ flowchart TD ## 7. Simulation and regression-test architecture -`simulate_mastodon_plugin.php` loads the real plugin code from the checked-out tree. It replaces +Both the root `simulate_mastodon_plugin.php` and its content-identical `fp-plugins/mastodon/regression-test/simulate_mastodon_plugin.ph_` copy resolve the FlatPress blog root before creating the sandbox. The nested copy falls back three directories only when `defaults.php` is not beside the script. The harness then loads the real plugin code from the checked-out tree. It replaces the external Mastodon side with deterministic fixtures and mock HTTP responses so the regression suite can verify state transitions, API requests, media handling, and edge-case conversion logic. The `fp-plugins/mastodon/regression-test/simulate_mastodon_plugin.php` regression-test simulator copy must stay content-identical to the root harness after CRLF/LF line-ending normalization; `check-consistency.php` verifies this so UI, one-way-mode, and synchronization assertions cannot drift between the two locations. ```mermaid flowchart TD - Script["simulate_mastodon_plugin.php"] + Script["Root or regression-test simulator entry point"] + SourceRoot{"defaults.php beside script?"} + RootFallback["Resolve FlatPress root three directories above regression-test copy"] OutputMode{"--summary or SIMULATE_MASTODON_SUMMARY=1?"} VerboseOutput["Verbose per-test details"] SummaryOutput["Compact per-test details"] @@ -1494,6 +1496,8 @@ flowchart TD CoreStubs["Load FlatPress includes and test helpers"] Plugin["require fp-plugins/mastodon/plugin.mastodon.php"] Fixtures["Create deterministic entries, comments, media files, options, and state fixtures"] + FixedClock["Freeze plugin_mastodon_test_now for date-window-sensitive fixture groups"] + ScopedOptions["Snapshot, override, and restore mutable admin option fixtures"] HTTPQueue["Mock Mastodon HTTP response queue"] GuardIsolation["Dirty-comment scheduled fixture clears content guard"] RunSync["Call real plugin sync/deletion functions"] @@ -1510,13 +1514,16 @@ flowchart TD OptionalLive["Optional --live-auth credentials smoke test"] Cleanup["Delete temporary sandbox"] - Script --> OutputMode + Script --> SourceRoot + SourceRoot -- "Yes" --> OutputMode + SourceRoot -- "No" --> RootFallback --> OutputMode OutputMode -- "No" --> VerboseOutput --> SandboxPolicy OutputMode -- "Yes" --> SummaryOutput --> SandboxPolicy SandboxPolicy -- "default" --> MinimalSandbox --> EmptyContent --> CoreStubs SandboxPolicy -- "--include-live-content or env flag" --> LiveSandbox --> CoreStubs CoreStubs --> Plugin --> Fixtures - Fixtures --> HTTPQueue --> RunSync --> Assertions + Fixtures --> FixedClock --> HTTPQueue --> RunSync --> Assertions + Fixtures --> ScopedOptions --> Assertions Fixtures -. "APCu/file cooldown isolation" .-> GuardIsolation --> RunSync Assertions --> CompactWrite --> SmallState --> MemoryGuard MemoryGuard -- "enough memory" --> LargeState --> InspectState @@ -1532,7 +1539,7 @@ The simulation sandbox deliberately excludes live `fp-content/content` in the de The output mode is independent of the assertions. Normal output keeps full details for one-file diagnostics. Summary output can be requested with `--summary` or `SIMULATE_MASTODON_SUMMARY=1`; it keeps the status and test name, but collapses verbose details to short text or JSON key summaries. Both output modes always end with the same counter block: `Exit-code`, `[OK]`, `[FAIL]`, `[WARN]`, and `[SKIP]`. -The APCu cooldown regression verifies that a marked content guard is no longer active after `plugin_mastodon_sync_guard_clear()`; this catches mismatched FlatPress wrapper key usage on APCu-enabled shared hosts. The dirty-comment scheduled-run regression also clears the `content` guard immediately before its own non-forced `plugin_mastodon_run_sync(false)` call, so earlier cooldown assertions cannot survive through APCu and mask the `dirty_comments` export path as `sync_cooldown`. The compact-state regression first verifies that legacy pretty-printed `state.json` files remain readable and that new `state.json` writes are compact JSON while preserving all mapping queues. The large `3000x10` scheduler-state regression is intentionally guarded before its synthetic state is built. On 128 MiB shared-hosting-style PHP limits, the large PHP array plus full JSON string can exhaust memory before the compact scheduler-state behavior is reached. The smaller `300x10` state test always runs; the heavy test runs when memory is sufficient, warns in low-memory non-CI environments, and is skipped in CI only after an attempted memory-limit raise fails. The CI skip branch can be tested deterministically with `SIMULATE_MASTODON_DISABLE_MEMORY_RAISE=1`. +The APCu cooldown regression verifies that a marked content guard is no longer active after `plugin_mastodon_sync_guard_clear()`; this catches mismatched FlatPress wrapper key usage on APCu-enabled shared hosts. The dirty-comment scheduled-run regression also clears the `content` guard immediately before its own non-forced `plugin_mastodon_run_sync(false)` call, so earlier cooldown assertions cannot survive through APCu and mask the `dirty_comments` export path as `sync_cooldown`. Date-window-sensitive comment-export fixtures freeze `$GLOBALS['plugin_mastodon_test_now']` to a fixed UTC instant and unset the hook after the related group, which keeps fixed FlatPress IDs and filenames reproducible without bypassing the scheduled-window code. The admin-assignment fixture similarly snapshots, overrides and restores the normalized plugin options so assertions for `old_thread_reply_check` and `delete_sync_enabled` cannot inherit state from earlier scenarios. The compact-state regression first verifies that legacy pretty-printed `state.json` files remain readable and that new `state.json` writes are compact JSON while preserving all mapping queues. The large `3000x10` scheduler-state regression is intentionally guarded before its synthetic state is built. On 128 MiB shared-hosting-style PHP limits, the large PHP array plus full JSON string can exhaust memory before the compact scheduler-state behavior is reached. The smaller `300x10` state test always runs; the heavy test runs when memory is sufficient, warns in low-memory non-CI environments, and is skipped in CI only after an attempted memory-limit raise fails. The CI skip branch can be tested deterministically with `SIMULATE_MASTODON_DISABLE_MEMORY_RAISE=1`. The locally deleted imported remote reply tombstone regressions exercise both a normal context replay and an edited remote reply replay. They ensure `comment_tombstones` block re-import before hash comparison and that deletion sync treats legacy missing `source=remote` comments as local ignore decisions without outbound Mastodon `DELETE` requests. diff --git a/fp-plugins/mastodon/developer-docs/README.md b/fp-plugins/mastodon/developer-docs/README.md index a7cf6c4d..ada4db2f 100644 --- a/fp-plugins/mastodon/developer-docs/README.md +++ b/fp-plugins/mastodon/developer-docs/README.md @@ -54,13 +54,14 @@ The distributed configuration checks the declared PHP 7.2 through 8.5 range and ## Simulation harness quick reference -Run the full deterministic regression harness from the FlatPress root: +Run the full deterministic regression harness from the FlatPress root. The content-identical copy under `fp-plugins/mastodon/regression-test/` can also resolve the blog root when invoked directly: ```bash php simulate_mastodon_plugin.php +php fp-plugins/mastodon/regression-test/simulate_mastodon_plugin.ph_ ``` -The harness copies the current FlatPress tree into an isolated temporary sandbox, excludes live `fp-content/content` by default, seeds deterministic fixtures, mocks Mastodon HTTP calls unless live auth is explicitly requested, and ends with a counter block containing `Exit-code`, `[OK]`, `[FAIL]`, `[WARN]`, and `[SKIP]`. +The harness copies the current FlatPress tree into an isolated temporary sandbox, excludes live `fp-content/content` by default, seeds deterministic fixtures, freezes the plugin clock around date-window-sensitive fixture groups, snapshots and restores mutable option fixtures, mocks Mastodon HTTP calls unless live auth is explicitly requested, and ends with a counter block containing `Exit-code`, `[OK]`, `[FAIL]`, `[WARN]`, and `[SKIP]`. ### Simulation parameters diff --git a/fp-plugins/mastodon/developer-docs/check-consistency.php b/fp-plugins/mastodon/developer-docs/check-consistency.php index 3592916c..4fe81d56 100644 --- a/fp-plugins/mastodon/developer-docs/check-consistency.php +++ b/fp-plugins/mastodon/developer-docs/check-consistency.php @@ -626,6 +626,54 @@ function mastodon_docs_extract_backticked_function_references($content) { } } +$fixtureIsolationRequiredImplementation = array( + "\$GLOBALS ['plugin_mastodon_test_now'] = strtotime('2026-06-16 12:00:00 UTC');", + "unset(\$GLOBALS ['plugin_mastodon_test_now']);", + 'plugin_mastodon_save_options($adminPreviousOptions);', + '$regressionCopySourceRoot = dirname(__DIR__, 3);' +); +foreach ($fixtureIsolationRequiredImplementation as $requiredSnippet) { + if ($simulationContent !== '' && strpos($simulationContent, $requiredSnippet) === false) { + $errors [] = 'Simulation harness missing deterministic fixture-isolation snippet: ' . $requiredSnippet; + } +} + +$fixtureIsolationRequiredDocs = array( + 'README.md' => array( + mastodon_docs_read_file(__DIR__ . '/README.md', $errors), + array( + 'freezes the plugin clock around date-window-sensitive fixture groups', + 'snapshots and restores mutable option fixtures', + 'regression-test/simulate_mastodon_plugin.ph_' + ) + ), + '05-Regression-Test-Matrix.md' => array( + $regressionDocContent, + array( + 'Date-window fixture isolation policy', + 'Mutable option fixture isolation policy', + ) + ), + '06-Process-Flow.md' => array( + $flowDocContent, + array( + 'Freeze plugin_mastodon_test_now for date-window-sensitive fixture groups', + 'Snapshot, override, and restore mutable admin option fixtures', + 'cannot inherit state from earlier scenarios', + 'Resolve FlatPress root three directories above regression-test copy' + ) + ) +); +foreach ($fixtureIsolationRequiredDocs as $docName => $docData) { + $docContent = (string) $docData [0]; + $requiredSnippets = $docData [1]; + foreach ($requiredSnippets as $requiredSnippet) { + if ($docContent !== '' && strpos($docContent, (string) $requiredSnippet) === false) { + $errors [] = $docName . ' missing deterministic fixture-isolation documentation snippet: ' . (string) $requiredSnippet; + } + } +} + $importedRemoteReplyDeleteRequiredDocs = array( '02-State-Model.md' => array( $stateModelDocContent, diff --git a/fp-plugins/mastodon/regression-test/simulate_mastodon_plugin.ph_ b/fp-plugins/mastodon/regression-test/simulate_mastodon_plugin.ph_ index 26ce9862..b560be11 100644 --- a/fp-plugins/mastodon/regression-test/simulate_mastodon_plugin.ph_ +++ b/fp-plugins/mastodon/regression-test/simulate_mastodon_plugin.ph_ @@ -1111,6 +1111,12 @@ function simulate_seed_regression_fixtures() { } $sourceRoot = __DIR__; +if (!is_file($sourceRoot . '/defaults.php')) { + $regressionCopySourceRoot = dirname(__DIR__, 3); + if (is_file($regressionCopySourceRoot . '/defaults.php')) { + $sourceRoot = $regressionCopySourceRoot; + } +} $sandboxRoot = simulate_prepare_sandbox($sourceRoot); register_shutdown_function(function () use ($sandboxRoot) { simulate_delete_recursive($sandboxRoot); @@ -10857,7 +10863,16 @@ $allOk = test_result( ) && $allOk; plugin_mastodon_save_options($options); -// Admin assignment must expose deletion sync status counters for the admin template. +// Admin assignment must expose deletion sync status counters and explicitly +// configured boolean options for the admin template without inheriting state +// from an earlier regression scenario. +$adminPreviousOptions = plugin_mastodon_get_options(); +$adminOptions = $adminPreviousOptions; +$adminOptions ['sync_scheduled_window_days'] = '14'; +$adminOptions ['old_thread_reply_check'] = '0'; +$adminOptions ['delete_sync_enabled'] = '1'; +plugin_mastodon_save_options($adminOptions); + $adminState = plugin_mastodon_default_state(); $adminState ['last_run'] = '2026-04-01 10:00:00'; $adminState ['last_deletion_run'] = '2026-04-01 10:05:00'; @@ -10876,7 +10891,7 @@ $assignedCfg = isset($adminSmarty->assigned ['mastodon_cfg']) && is_array($admin $expectedLastRunLocal = plugin_mastodon_format_admin_datetime($adminState ['last_run']); $expectedLastDeletionRunLocal = plugin_mastodon_format_admin_datetime($adminState ['last_deletion_run']); $allOk = test_result( - 'Admin assignment exposes split sync counters, local admin timestamps, and the deletion-sync option', + 'Admin assignment exposes split sync counters, local timestamps, old-thread and deletion-sync options', !empty($assignedState) && isset($assignedState ['last_deletion_run']) && $assignedState ['last_deletion_run'] === '2026-04-01 10:05:00' @@ -10895,14 +10910,17 @@ $allOk = test_result( && isset($adminSmarty->assigned ['mastodon_scheduled_window_choices']) && is_array($adminSmarty->assigned ['mastodon_scheduled_window_choices']) && count($adminSmarty->assigned ['mastodon_scheduled_window_choices']) === 3 && isset($adminSmarty->assigned ['mastodon_state_maintenance_result']) && is_array($adminSmarty->assigned ['mastodon_state_maintenance_result']) && empty($adminSmarty->assigned ['mastodon_state_maintenance_result'] ['available']) - && isset($assignedCfg ['old_thread_reply_check']) && (string) $assignedCfg ['old_thread_reply_check'] === '0' - && isset($assignedCfg ['delete_sync_enabled']) && (string) $assignedCfg ['delete_sync_enabled'] === '1', + && isset($assignedCfg ['old_thread_reply_check']) + && (string) $assignedCfg ['old_thread_reply_check'] === $adminOptions ['old_thread_reply_check'] + && isset($assignedCfg ['delete_sync_enabled']) + && (string) $assignedCfg ['delete_sync_enabled'] === $adminOptions ['delete_sync_enabled'], json_encode(array( 'assigned_keys' => array_keys($adminSmarty->assigned), 'cfg' => $assignedCfg, 'state' => $assignedState )) ) && $allOk; +plugin_mastodon_save_options($adminPreviousOptions); $mainAdminTemplate = @file_get_contents(PLUGIN_MASTODON_DIR . 'tpls/admin.plugin.mastodon.tpl'); $maintenanceAdminTemplate = @file_get_contents(PLUGIN_MASTODON_DIR . 'tpls/admin.plugin.mastodon.maintenance.tpl'); @@ -12143,6 +12161,11 @@ $allOk = test_result( entry_delete($automaticOldCommentEntryId); unset($GLOBALS ['plugin_mastodon_test_now']); +// Freeze the scheduled synchronization window for the following comment-export +// regression fixtures. The fixed clock keeps entry IDs, comment IDs, filenames, +// request payloads, and diagnostics reproducible regardless of the wall clock. +$GLOBALS ['plugin_mastodon_test_now'] = strtotime('2026-06-16 12:00:00 UTC'); + // Regression test: authenticated FlatPress comments carry a stored LOGGEDIN marker; // those comments must still be eligible for Mastodon reply export without a public visitor opt-in. $adminOptinOptions = simulate_seed_options_from_config(plugin_mastodon_get_options()); @@ -13051,6 +13074,8 @@ $allOk = test_result( ) && $allOk; entry_delete($visitorOptinEntryId); +unset($GLOBALS ['plugin_mastodon_test_now']); + // Regression test: the explicit one-way mode must be disabled by default, saved from the admin form, and translated everywhere. $oneWayDefaultOptions = plugin_mastodon_default_options(); $oneWayAdminLanguageOk = true;