Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .agents/skills/code-style/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,31 @@ Handle the full PKCE + DPoP + PAR native OAuth flow.
\apply_filters( 'atmosphere_client_metadata', $metadata );
\apply_filters( 'atmosphere_syncable_post_types', array( 'post' ) );
```

## Cron Lifecycle — three-way symmetry

Every plugin-owned `wp_schedule_*` hook MUST also be in `Atmosphere\get_cron_hooks()` (`includes/functions.php`). That single list drives:

- `Atmosphere\deactivate()` (`atmosphere.php`)
- `Atmosphere\OAuth\Client::disconnect()` (`includes/oauth/class-client.php`)
- `uninstall.php`

When adding a new cron hook:

1. Add the hook name to `get_cron_hooks()` — do not duplicate the literal in deactivate / disconnect / uninstall.
2. If the hook handler issues PDS writes without re-checking `is_connected()` (e.g. `atmosphere_delete_records`, `atmosphere_delete_comment_record`), the symmetry is load-bearing: a queued event from a previous connection would otherwise fire against a different repo on reconnect.
3. If the handler stores or sweeps commentmeta / postmeta keys, mirror those keys in `uninstall.php`.

This pattern was extracted in PR #32; see review by @kraftbj for the cross-install risk that motivated it.

## Cron Handler Errors — never swallow `WP_Error`

Cron handlers in `register_async_hooks()` MUST surface `Publisher::*` errors via `error_log()` (typically through `log_cron_error()`). `wp_schedule_single_event` does not retry, so a silent drop loses the only signal operators have for transient PDS failures, expired refresh tokens, or DPoP nonce drift.

When the handler operates on records the caller has already lost local state for (e.g. `atmosphere_delete_comment_record` after the WP comment row is gone), include the TID/identifier in the log line so the orphan is recoverable manually.

## Inflight-state Races

When a cron handler writes meta both *before* an `apply_writes` call (e.g. `Comment::get_rkey()` persists META_TID) and *after* (e.g. `store_comment_result()` writes META_URI), and a concurrent state change can short-circuit the cleanup gates that key off the *post-call* meta, the handler MUST re-check eligibility after the call returns and roll back if needed.

Concrete pattern: `atmosphere_publish_comment` → `reconcile_comment_after_publish()`. Re-fetch the WP object, re-run the eligibility gate, schedule the orphan-cleanup cron (not direct delete) so transient PDS failures retry through the standard channel.
39 changes: 39 additions & 0 deletions .agents/skills/test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,42 @@ npm run env-test -- --stop-on-failure
# Run single test method.
npm run env-test -- --filter=test_specific_method
```

## Stubbing `applyWrites` calls

The Publisher test fixture (`Test_Publisher`) exposes `register_capture()` plus `$captured_calls` / `$fail_call_indexes` for asserting on the `writes` batch and forcing per-call failures:

```php
$this->fail_call_indexes = array(
2 => new \WP_Error( 'atmosphere_pds_500', 'PDS rejected.' ),
);
$this->register_capture( $post_id );
// ...exercise...
$this->assertCount( 3, $this->captured_calls );
```

Outside the Publisher test, hook the `atmosphere_pre_apply_writes` filter directly (see Publisher::apply_writes — short-circuits before the HTTP layer, so DPoP-less test environments work).

## Simulating in-flight races

To reproduce a "state changed during the API call" race in tests, mutate the WP state from inside the `atmosphere_pre_apply_writes` filter callback and return a synthetic 2xx response. The plugin's hooks fire synchronously in the test process — the filter callback is the analogue of "the API call took long enough for another request to land".

```php
\add_filter(
'atmosphere_pre_apply_writes',
static function ( $short, $writes ) use ( $comment_id ) {
\wp_set_comment_status( $comment_id, 'hold' );
return array( 'results' => array( /* synthetic */ ) );
},
10,
2
);
```

Note: `wp_delete_comment( $id, true )` removes commentmeta synchronously, which can erase TIDs the reconcile path needs. Prefer status transitions (`hold`, `spam`) when possible.

## Cron handlers in tests

The plugin's `register_async_hooks()` runs at `plugins_loaded` (via the bootstrap), so cron handlers ARE registered before tests execute. Use `\do_action( 'atmosphere_publish_comment', $comment_id )` to fire a handler synchronously; assert on `\wp_next_scheduled()` for follow-up scheduling.

Always clean up scheduled hooks in `tear_down()` — leftover events from one test become flaky preconditions for the next.
4 changes: 4 additions & 0 deletions .github/changelog/fix-comment-publish-race
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Remove a comment reply from Bluesky if the comment was deleted or unapproved while it was being published, instead of leaving an orphan reply behind.
4 changes: 4 additions & 0 deletions .github/changelog/fix-delete-batch-chunking
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Break up large cleanup batches when removing a post and its replies so deletion still completes on threads with many comments.
4 changes: 4 additions & 0 deletions .github/changelog/fix-disconnect-cron-cleanup
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Clear queued sync events on disconnect, deactivation, and uninstall so leftover jobs cannot fire against a different connected account.
4 changes: 4 additions & 0 deletions .github/changelog/publish-comments
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Publish replies from registered WordPress users to Bluesky as native replies, with edit and unapprove/delete synced back to the AT Protocol record.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ composer.lock
package-lock.json
*.zip
.phpunit.result.cache
/docs/superpowers/
7 changes: 1 addition & 6 deletions atmosphere.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,7 @@ function activate() {
* Deactivation hook.
*/
function deactivate() {
\wp_clear_scheduled_hook( 'atmosphere_refresh_token' );
\wp_clear_scheduled_hook( 'atmosphere_sync_reactions' );
\wp_clear_scheduled_hook( 'atmosphere_sync_publication' );
\wp_clear_scheduled_hook( 'atmosphere_delete_records' );
// Clear the legacy hook name in case an earlier PR-6 build scheduled it.
\wp_clear_scheduled_hook( 'atmosphere_sync_comments' );
clear_scheduled_hooks();
\flush_rewrite_rules();
Comment thread
kraftbj marked this conversation as resolved.
}
\register_deactivation_hook( __FILE__, __NAMESPACE__ . '\deactivate' );
Loading