Fix: increment postsCount by local writes in read-after-write profile munge#175
Conversation
|
isn't this going to double count the posts? |
|
I think the count between the appview and what the PDS here is going to get confused |
|
Hey, |
- AppViewConfig + RocketConfig.app_view: inject a test AppView URL/DID into build_rocket() without touching env vars, enabling parallel tests - dev_mode auto-enabled when AppView is injected (bypasses is_safe_url localhost check in pipethrough) - MockAppView: minimal tokio HTTP/1.1 server bound to 127.0.0.1:0, returns fixed JSON with configurable atproto-repo-rev header - get_client_with_appview() + create_session() test helpers - get_profile_postscount_incremented_after_local_create_record: sentinel record → capture rev → write profile+post → assert postsCount = upstream(5) + local_posts(1) = 6 (tests PR blacksky-algorithms#175)
… munge (issue blacksky-algorithms#164) Pass local.posts.len() into update_profile_detailed and add it to the upstream posts_count via apply_local_posts_count(). Previously the munge only overlaid profile-record fields (displayName, description, avatar, banner) but preserved stale upstream counters unchanged, causing bsky.app to render a blank profile shell immediately after a post. Add mock AppView test harness + read-after-write integration test - AppViewConfig + RocketConfig.app_view: inject a test AppView URL/DID into build_rocket() without touching env vars, enabling parallel tests - dev_mode auto-enabled when AppView is injected (bypasses is_safe_url localhost check in pipethrough) - MockAppView: minimal tokio HTTP/1.1 server bound to 127.0.0.1:0, returns fixed JSON with configurable atproto-repo-rev header - get_client_with_appview() + create_session() test helpers - get_profile_postscount_incremented_after_local_create_record: sentinel record → capture rev → write profile+post → assert postsCount = upstream(5) + local_posts(1) = 6 (tests PR blacksky-algorithms#175) Fix read-after-write test: activate account after creation The create_account helper passes a hardcoded DID, which triggers the "import from another PDS" code path in validate_inputs_for_local_pds and sets deactivated = true. AccessStandardIncludeChecks (used by createRecord) rejects deactivated accounts with HTTP 400, causing the sentinel createRecord assertion to fail in CI. Add create_active_account helper that clears deactivatedAt via a direct SQL UPDATE after account creation. Existing tests are unaffected. Fix Sequencer DB connection in integration tests Sequencer::sequence_evt (and all other Sequencer DB methods) called establish_connection_for_sequencer() which reads DATABASE_URL from the environment. Integration tests inject the DB URL directly into Rocket's Figment and never set DATABASE_URL, causing every createRecord (and any other sequenced write) to fail with a connection error -> HTTP 500. Add db_url: String to Sequencer, set via new()'s third parameter. Falls back to DATABASE_URL env var when None is passed (preserves production behavior). build_rocket passes the already-known db_url; subscribe_repos passes None (env var fallback). Fix test assertion: getProfile munge wraps profile in HandlerResponse envelope The munged response serialises as {encoding, body: {...profile...}, headers}. Assertion was checking body["postsCount"] instead of body["body"]["postsCount"]. Add read-after-write guard path tests + pre-pull Postgres in CI Three new integration tests covering the paths that bypass the munge: - get_profile_unauthenticated_returns_raw_appview_response requester=None → HandlerPipeThrough immediately; raw AppView JSON, postsCount at top level equals upstream value. - get_profile_appview_current_skips_munge atproto-repo-rev = HEAD → local.count==0 → HandlerPipeThrough; confirms the fix does not spuriously increment when AppView is current. - get_profile_no_local_profile_record_postscount_unchanged local posts exist after sentinel rev but no "self" profile record → munge enters but exits early (local.profile=None) → HandlerResponse envelope with upstream postsCount unchanged. Also pre-pull postgres:11-alpine in CI before cargo test to eliminate the per-test Docker image pull from testcontainer startup time. Fix failing unauthenticated test: replace with no-rev-header path The unauthenticated path was unreachable — AccessStandard always returns Outcome::Error on missing auth, so requester = None is dead code. Replace with get_profile_no_rev_header_returns_raw_appview_response which exercises the real HandlerPipeThrough path: AppView response missing the atproto-repo-rev header → rev = None → read_after_write_internal returns HandlerPipeThrough immediately → raw JSON forwarded with no envelope. Also refactor MockAppView: add start_without_rev() constructor and extract shared start_inner() to avoid duplication.
… munge (issue blacksky-algorithms#164) Pass local.posts.len() into update_profile_detailed and add it to the upstream posts_count via apply_local_posts_count(). Previously the munge only overlaid profile-record fields (displayName, description, avatar, banner) but preserved stale upstream counters unchanged, causing bsky.app to render a blank profile shell immediately after a post. Add mock AppView test harness + read-after-write integration test - AppViewConfig + RocketConfig.app_view: inject a test AppView URL/DID into build_rocket() without touching env vars, enabling parallel tests - dev_mode auto-enabled when AppView is injected (bypasses is_safe_url localhost check in pipethrough) - MockAppView: minimal tokio HTTP/1.1 server bound to 127.0.0.1:0, returns fixed JSON with configurable atproto-repo-rev header - get_client_with_appview() + create_session() test helpers - get_profile_postscount_incremented_after_local_create_record: sentinel record → capture rev → write profile+post → assert postsCount = upstream(5) + local_posts(1) = 6 (tests PR blacksky-algorithms#175) Fix read-after-write test: activate account after creation The create_account helper passes a hardcoded DID, which triggers the "import from another PDS" code path in validate_inputs_for_local_pds and sets deactivated = true. AccessStandardIncludeChecks (used by createRecord) rejects deactivated accounts with HTTP 400, causing the sentinel createRecord assertion to fail in CI. Add create_active_account helper that clears deactivatedAt via a direct SQL UPDATE after account creation. Existing tests are unaffected. Fix Sequencer DB connection in integration tests Sequencer::sequence_evt (and all other Sequencer DB methods) called establish_connection_for_sequencer() which reads DATABASE_URL from the environment. Integration tests inject the DB URL directly into Rocket's Figment and never set DATABASE_URL, causing every createRecord (and any other sequenced write) to fail with a connection error -> HTTP 500. Add db_url: String to Sequencer, set via new()'s third parameter. Falls back to DATABASE_URL env var when None is passed (preserves production behavior). build_rocket passes the already-known db_url; subscribe_repos passes None (env var fallback). Fix test assertion: getProfile munge wraps profile in HandlerResponse envelope The munged response serialises as {encoding, body: {...profile...}, headers}. Assertion was checking body["postsCount"] instead of body["body"]["postsCount"]. Add read-after-write guard path tests + pre-pull Postgres in CI Three new integration tests covering the paths that bypass the munge: - get_profile_unauthenticated_returns_raw_appview_response requester=None → HandlerPipeThrough immediately; raw AppView JSON, postsCount at top level equals upstream value. - get_profile_appview_current_skips_munge atproto-repo-rev = HEAD → local.count==0 → HandlerPipeThrough; confirms the fix does not spuriously increment when AppView is current. - get_profile_no_local_profile_record_postscount_unchanged local posts exist after sentinel rev but no "self" profile record → munge enters but exits early (local.profile=None) → HandlerResponse envelope with upstream postsCount unchanged. Also pre-pull postgres:11-alpine in CI before cargo test to eliminate the per-test Docker image pull from testcontainer startup time. Fix failing unauthenticated test: replace with no-rev-header path The unauthenticated path was unreachable — AccessStandard always returns Outcome::Error on missing auth, so requester = None is dead code. Replace with get_profile_no_rev_header_returns_raw_appview_response which exercises the real HandlerPipeThrough path: AppView response missing the atproto-repo-rev header → rev = None → read_after_write_internal returns HandlerPipeThrough immediately → raw JSON forwarded with no envelope. Also refactor MockAppView: add start_without_rev() constructor and extract shared start_inner() to avoid duplication.
- AppViewConfig + RocketConfig.app_view: inject a test AppView URL/DID into build_rocket() without touching env vars, enabling parallel tests - dev_mode auto-enabled when AppView is injected (bypasses is_safe_url localhost check in pipethrough) - MockAppView: minimal tokio HTTP/1.1 server bound to 127.0.0.1:0, returns fixed JSON with configurable atproto-repo-rev header - get_client_with_appview() + create_session() test helpers - get_profile_postscount_incremented_after_local_create_record: sentinel record → capture rev → write profile+post → assert postsCount = upstream(5) + local_posts(1) = 6 (tests PR blacksky-algorithms#175)
- AppViewConfig + RocketConfig.app_view: inject a test AppView URL/DID into build_rocket() without touching env vars, enabling parallel tests - dev_mode auto-enabled when AppView is injected (bypasses is_safe_url localhost check in pipethrough) - MockAppView: minimal tokio HTTP/1.1 server bound to 127.0.0.1:0, returns fixed JSON with configurable atproto-repo-rev header - get_client_with_appview() + create_session() test helpers - get_profile_postscount_incremented_after_local_create_record: sentinel record → capture rev → write profile+post → assert postsCount = upstream(5) + local_posts(1) = 6 (tests PR blacksky-algorithms#175) Fix read-after-write test: activate account after creation The create_account helper passes a hardcoded DID, which triggers the "import from another PDS" code path in validate_inputs_for_local_pds and sets deactivated = true. AccessStandardIncludeChecks (used by createRecord) rejects deactivated accounts with HTTP 400, causing the sentinel createRecord assertion to fail in CI. Add create_active_account helper that clears deactivatedAt via a direct SQL UPDATE after account creation. Existing tests are unaffected. Fix Sequencer DB connection in integration tests Sequencer::sequence_evt (and all other Sequencer DB methods) called establish_connection_for_sequencer() which reads DATABASE_URL from the environment. Integration tests inject the DB URL directly into Rocket's Figment and never set DATABASE_URL, causing every createRecord (and any other sequenced write) to fail with a connection error -> HTTP 500. Add db_url: String to Sequencer, set via new()'s third parameter. Falls back to DATABASE_URL env var when None is passed (preserves production behavior). build_rocket passes the already-known db_url; subscribe_repos passes None (env var fallback). Fix test assertion: getProfile munge wraps profile in HandlerResponse envelope The munged response serialises as {encoding, body: {...profile...}, headers}. Assertion was checking body["postsCount"] instead of body["body"]["postsCount"]. Add read-after-write guard path tests + pre-pull Postgres in CI Three new integration tests covering the paths that bypass the munge: - get_profile_unauthenticated_returns_raw_appview_response requester=None → HandlerPipeThrough immediately; raw AppView JSON, postsCount at top level equals upstream value. - get_profile_appview_current_skips_munge atproto-repo-rev = HEAD → local.count==0 → HandlerPipeThrough; confirms the fix does not spuriously increment when AppView is current. - get_profile_no_local_profile_record_postscount_unchanged local posts exist after sentinel rev but no "self" profile record → munge enters but exits early (local.profile=None) → HandlerResponse envelope with upstream postsCount unchanged. Also pre-pull postgres:11-alpine in CI before cargo test to eliminate the per-test Docker image pull from testcontainer startup time. Fix failing unauthenticated test: replace with no-rev-header path The unauthenticated path was unreachable — AccessStandard always returns Outcome::Error on missing auth, so requester = None is dead code. Replace with get_profile_no_rev_header_returns_raw_appview_response which exercises the real HandlerPipeThrough path: AppView response missing the atproto-repo-rev header → rev = None → read_after_write_internal returns HandlerPipeThrough immediately → raw JSON forwarded with no envelope. Also refactor MockAppView: add start_without_rev() constructor and extract shared start_inner() to avoid duplication.
… munge (issue blacksky-algorithms#164) Pass local.posts.len() into update_profile_detailed and add it to the upstream posts_count via apply_local_posts_count(). Previously the munge only overlaid profile-record fields (displayName, description, avatar, banner) but preserved stale upstream counters unchanged, causing bsky.app to render a blank profile shell immediately after a post.
c636d55 to
58609a6
Compare
- AppViewConfig + RocketConfig.app_view: inject a test AppView URL/DID into build_rocket() without touching env vars, enabling parallel tests - dev_mode auto-enabled when AppView is injected (bypasses is_safe_url localhost check in pipethrough) - MockAppView: minimal tokio HTTP/1.1 server bound to 127.0.0.1:0, returns fixed JSON with configurable atproto-repo-rev header - get_client_with_appview() + create_session() test helpers - get_profile_postscount_incremented_after_local_create_record: sentinel record → capture rev → write profile+post → assert postsCount = upstream(5) + local_posts(1) = 6 (tests PR blacksky-algorithms#175) Fix read-after-write test: activate account after creation The create_account helper passes a hardcoded DID, which triggers the "import from another PDS" code path in validate_inputs_for_local_pds and sets deactivated = true. AccessStandardIncludeChecks (used by createRecord) rejects deactivated accounts with HTTP 400, causing the sentinel createRecord assertion to fail in CI. Add create_active_account helper that clears deactivatedAt via a direct SQL UPDATE after account creation. Existing tests are unaffected. Fix Sequencer DB connection in integration tests Sequencer::sequence_evt (and all other Sequencer DB methods) called establish_connection_for_sequencer() which reads DATABASE_URL from the environment. Integration tests inject the DB URL directly into Rocket's Figment and never set DATABASE_URL, causing every createRecord (and any other sequenced write) to fail with a connection error -> HTTP 500. Add db_url: String to Sequencer, set via new()'s third parameter. Falls back to DATABASE_URL env var when None is passed (preserves production behavior). build_rocket passes the already-known db_url; subscribe_repos passes None (env var fallback). Fix test assertion: getProfile munge wraps profile in HandlerResponse envelope The munged response serialises as {encoding, body: {...profile...}, headers}. Assertion was checking body["postsCount"] instead of body["body"]["postsCount"]. Add read-after-write guard path tests + pre-pull Postgres in CI Three new integration tests covering the paths that bypass the munge: - get_profile_unauthenticated_returns_raw_appview_response requester=None → HandlerPipeThrough immediately; raw AppView JSON, postsCount at top level equals upstream value. - get_profile_appview_current_skips_munge atproto-repo-rev = HEAD → local.count==0 → HandlerPipeThrough; confirms the fix does not spuriously increment when AppView is current. - get_profile_no_local_profile_record_postscount_unchanged local posts exist after sentinel rev but no "self" profile record → munge enters but exits early (local.profile=None) → HandlerResponse envelope with upstream postsCount unchanged. Also pre-pull postgres:11-alpine in CI before cargo test to eliminate the per-test Docker image pull from testcontainer startup time. Fix failing unauthenticated test: replace with no-rev-header path The unauthenticated path was unreachable — AccessStandard always returns Outcome::Error on missing auth, so requester = None is dead code. Replace with get_profile_no_rev_header_returns_raw_appview_response which exercises the real HandlerPipeThrough path: AppView response missing the atproto-repo-rev header → rev = None → read_after_write_internal returns HandlerPipeThrough immediately → raw JSON forwarded with no envelope. Also refactor MockAppView: add start_without_rev() constructor and extract shared start_inner() to avoid duplication.
|
Ok so, back with more tests! I was able to confirm the implementation won't double count the posts. To validate it I built a mock AppView test harness for rsky-pds. The test spins up a full Rocket + Postgres instance and replaces the AppView HTTP endpoint with a lightweight mock server that returns a controlled response (like our case here). The integration test then: 1. Creates an account and writes a post locally via the PDS The testing logic lives in a separate branch in my fork.. Happy to discuss the testing approach further in a separate discussion, and open a PR for this as well if this is something that would be welcome. |
|
I think the approach to consider is to follow the atproto reference pds read-after-write implementation that doesn't account for postCounts. An appview should eventually stabilize the post count. |
Fixes #164
What
update_profile_detailedinread_after_write/viewer.rsnow accepts alocal_posts_count: usizeparameter and adds it to the upstreamposts_countvia a newapply_local_posts_counthelper. Both call sites (get_profile_mungeandget_profiles_munge) passlocal.posts.len().Why
After
createRecord, the read-after-write munge was overlaying profile-record fields (displayName,description,avatar,banner) but passing upstreamfollowers_count,follows_count, andposts_countthrough unchanged. The stalepostsCountcausedbsky.appto render a blank profile shell immediately after a post, because the appview response was inconsistent with the local feed state.Testing
3 unit tests for
apply_local_posts_countinread_after_write::viewer::tests:Noneupstream count staysNone(unknown, not guessed)Full
LocalViewer-level integration testing is not feasible without a live database, which is noted as a known limitation.Checklist
cargo fmt)