perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Split local label RPC coverage from broad suites

alice 18027938 1f8c68b5

+163 -114
+2 -2
docs/ENDPOINT_CONFORMANCE.md
··· 46 46 - `com.atproto.sync.listReposByCollection` 47 47 Present in the published lexicon and covered locally in `t/discovery-surfaces.t`, but not exposed by the current official runtime. 48 48 - `com.atproto.temp.*` 49 - `requestPhoneVerification`, `revokeAccountCredentials`, `checkSignupQueue`, `dereferenceScope`, and `fetchLabels` are locally covered in `t/temp-endpoints.t`, `t/extended-api.t`, and `t/external-surface.t`, but do not have a like-for-like official public comparison surface. 49 + `requestPhoneVerification`, `revokeAccountCredentials`, `checkSignupQueue`, and `dereferenceScope` are locally covered in `t/temp-endpoints.t`, while `fetchLabels` is covered in `t/label-rpc-surfaces.t`; these do not have a like-for-like official public comparison surface. 50 50 - local label RPCs 51 - `com.atproto.label.queryLabels`, `subscribeLabels`, and `com.atproto.temp.fetchLabels` are verified locally; the official runtime does not provide a directly comparable local-labeler implementation. 51 + `com.atproto.label.queryLabels`, `subscribeLabels`, and `com.atproto.temp.fetchLabels` are verified locally in `t/label-rpc-surfaces.t` and `t/labels.t`; the official runtime does not provide a directly comparable local-labeler implementation. 52 52 - local appview emulation 53 53 `app.bsky.actor.getPreferences`, `putPreferences`, `notification.getPreferences`, `notification.putPreferencesV2`, and the conservative local feed/thread/profile fallback behavior are locally regression-tested rather than compared against an official self-hosted AppView implementation. 54 54
+7 -6
docs/TEST_AUDIT.md
··· 13 13 The current baseline for saying "the audited suite is green" is: 14 14 15 15 - `prove -lr t` 16 - - latest full green result in the realigned Meridian worktree: `Files=54, Tests=3043` 16 + - latest full green result in the realigned Meridian worktree: `Files=55, Tests=3047` 17 17 - `prove -lv t/server-auth.t` 18 18 - `perl -c script/differential-validate` 19 19 - `PERLSKY_RUN_REFERENCE_DIFF=1 prove -lv t/reference-differential.t` ··· 116 116 Current suite counts by bucket: 117 117 118 118 - `direct reference differential`: `5` 119 - - `audited local regression`: `36` 119 + - `audited local regression`: `37` 120 120 - `local correctness/infrastructure`: `13` 121 121 122 122 | Test file | Bucket | Current note | ··· 138 138 | `t/email-confirmation.t` | audited local regression | intentionally testing-friendly email flow plus strict missing-email and invalid-email validation semantics | 139 139 | `t/email-update-helper.t` | audited local regression | shared email-update helper normalization, token revocation, and duplicate-email error semantics | 140 140 | `t/event-stream.t` | audited local regression | wire-format, malformed frame, and event decoding coverage | 141 - | `t/extended-api.t` | audited local regression | mixes reference-aligned repo/sync/moderation happy paths with still-local label fetch/query smoke and account/invite flows, but is cleaner after splitting reserved-handle and crawl-host checks out | 141 + | `t/extended-api.t` | audited local regression | focused mixed coverage for invite issuance, `applyWrites`, identity refresh/update, email flows, and blob/sync happy paths after the label RPCs were split out | 142 142 | `t/external-handle-update.t` | audited local regression | external-handle update semantics, including DID-resolution checks and empty-body success for external handle adoption | 143 - | `t/external-surface.t` | audited local regression | focused external-surface coverage for repo/blob/account-status and label behavior after splitting the local discovery surfaces out | 143 + | `t/external-surface.t` | audited local regression | focused external-surface coverage for repo/blob/account-status and missing-blob behavior after splitting discovery and label RPC checks into dedicated suites | 144 144 | `t/firehose.t` | audited local regression | repo subscription lifecycle, cursor, and CAR behavior | 145 145 | `t/identity.t` | local correctness/infrastructure | lower-level handle and DID helper coverage, including DNS-over-well-known preference and malformed-handle rejection | 146 146 | `t/import-repo.t` | audited local regression | focused `importRepo` snapshot-restore and rollback behavior, now cleaner after splitting the disabled-import policy gate into its own suite | ··· 149 149 | `t/invite-admin.t` | audited local regression | isolated invite-management coverage for admin listing/disabling and self-service invite-account gating | 150 150 | `t/ipld-canonical.t` | local correctness/infrastructure | canonical IPLD encoding invariants | 151 151 | `t/ipld-codecs.t` | local correctness/infrastructure | DAG-CBOR and codec coverage | 152 + | `t/label-rpc-surfaces.t` | audited local regression | isolated local label-RPC coverage for `queryLabels` and `temp.fetchLabels` account/record takedown visibility | 152 153 | `t/labels.t` | audited local regression | label persistence, replay, negation, and cursor behavior | 153 154 | `t/local-service-surfaces.t` | audited local regression | isolated local-only coverage for reserved handles and crawler host tracking surfaces | 154 155 | `t/metrics.t` | audited local regression | metrics endpoint, token-gating smoke, and instrumentation contract for local appview behavior | ··· 181 182 The broadest suites are green and audited, but they still mix several categories of behavior inside the same file: 182 183 183 184 - `t/extended-api.t` 184 - Carries real conformance value for `applyWrites`, blob/sync flows, and moderation/label visibility, but it still mixes those with some local product behavior such as label fetch/query smoke and invite/account flows. 185 + Carries real conformance value for `applyWrites`, blob/sync flows, and account/email identity lifecycle behavior, but it still mixes those with local product behavior such as self-service invite flows. 185 186 - `t/external-surface.t` 186 - Carries strong external-surface coverage for repo export, blob access, account-status behavior, and label visibility. It is cleaner after moving discovery-specific checks into `t/discovery-surfaces.t`, but still remains broader than a single-endpoint conformance file. 187 + Carries strong external-surface coverage for repo export, blob access, account-status behavior, and missing-blob listing. It is cleaner after moving discovery and label-RPC checks into dedicated suites, but still remains broader than a single-endpoint conformance file. 187 188 - `t/uncovered-endpoints.t` 188 189 Exists specifically to stop a few lesser-used local endpoints from falling out of coverage; it is much narrower now that the temp, invite, and admin-account blocks have moved into dedicated suites, but it should still be read as a pragmatic safety net, not as a pure reference-alignment suite. 189 190
+8 -75
t/extended-api.t
··· 19 19 } 20 20 21 21 use Test::Mojo; 22 - use Mojo::URL; 23 22 use ATProto::PDS; 24 23 25 24 my $root = File::Spec->rel2abs(File::Spec->catdir($Bin, '..')); ··· 237 236 }, 238 237 })->status_is(200); 239 238 240 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.sync.listBlobs')->query( 241 - did => $did, 242 - ))->status_is(200) 239 + $t->get_ok('/xrpc/com.atproto.sync.listBlobs?did=' . $did) 240 + ->status_is(200) 243 241 ->json_is('/cids/0', $blob_cid); 244 242 245 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.sync.getBlob')->query( 246 - did => $did, 247 - cid => $blob_cid, 248 - ))->status_is(200); 243 + $t->get_ok('/xrpc/com.atproto.sync.getBlob?did=' . $did . '&cid=' . $blob_cid) 244 + ->status_is(200); 249 245 is($t->tx->res->body, 'blob-bytes', 'blob bytes are served back'); 250 246 like($t->tx->res->headers->content_type // '', qr{image/png}, 'blob content type preserved'); 251 247 252 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.sync.getLatestCommit')->query( 253 - did => $did, 254 - ))->status_is(200) 248 + $t->get_ok('/xrpc/com.atproto.sync.getLatestCommit?did=' . $did) 249 + ->status_is(200) 255 250 ->json_has('/cid'); 256 251 257 252 my $commit_cid = $t->tx->res->json->{cid}; 258 253 259 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.sync.getBlocks')->query( 260 - did => $did, 261 - cids => $commit_cid, 262 - ))->status_is(200); 263 - like($t->tx->res->headers->content_type // '', qr{application/vnd\.ipld\.car}, 'block export is a CAR'); 264 - 265 - $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 266 - Authorization => $admin_auth, 267 - } => json => { 268 - subject => { did => $did }, 269 - takedown => { applied => JSON::PP::true, ref => 'unit-test' }, 270 - })->status_is(200); 271 - 272 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 273 - uriPatterns => "at://$did*", 274 - ))->status_is(200); 275 - ok( 276 - _find_label($t->tx->res->json->{labels}, val => '!hide'), 277 - 'queryLabels includes the takedown label', 278 - ); 279 - 280 - $t->get_ok('/xrpc/com.atproto.temp.fetchLabels?limit=10') 281 - ->status_is(200); 282 - ok( 283 - _find_label($t->tx->res->json->{labels}, val => '!hide'), 284 - 'fetchLabels includes the takedown label', 285 - ); 286 - 287 - $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 288 - Authorization => $admin_auth, 289 - } => json => { 290 - subject => { did => $did }, 291 - takedown => { applied => JSON::PP::false, ref => 'unit-test' }, 292 - })->status_is(200); 293 - 294 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 295 - uriPatterns => "at://$did*", 296 - ))->status_is(200); 297 - ok( 298 - _find_label($t->tx->res->json->{labels}, val => '!hide', neg => JSON::PP::true), 299 - 'queryLabels includes the negated takedown label', 300 - ); 301 - 302 - $t->get_ok('/xrpc/com.atproto.temp.fetchLabels?limit=10') 254 + $t->get_ok('/xrpc/com.atproto.sync.getBlocks?did=' . $did . '&cids=' . $commit_cid) 303 255 ->status_is(200); 304 - ok( 305 - _find_label($t->tx->res->json->{labels}, val => '!hide', neg => JSON::PP::true), 306 - 'fetchLabels includes the negated takedown label', 307 - ); 256 + like($t->tx->res->headers->content_type // '', qr{application/vnd\.ipld\.car}, 'block export is a CAR'); 308 257 309 258 done_testing; 310 - 311 - sub _find_label { 312 - my ($labels, %expected) = @_; 313 - return 0 unless ref($labels) eq 'ARRAY'; 314 - for my $label (@$labels) { 315 - next unless ref($label) eq 'HASH'; 316 - my $matches = 1; 317 - for my $key (keys %expected) { 318 - next if defined($label->{$key}) && "$label->{$key}" eq "$expected{$key}"; 319 - $matches = 0; 320 - last; 321 - } 322 - return 1 if $matches; 323 - } 324 - return 0; 325 - }
-31
t/external-surface.t
··· 278 278 })->status_is(200) 279 279 ->json_is('/handle' => 'alice.example.test'); 280 280 281 - $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 282 - Authorization => $admin_auth, 283 - } => json => { 284 - subject => { uri => $record_uri, cid => $record_cid }, 285 - takedown => { applied => JSON::PP::true }, 286 - })->status_is(200); 287 - 288 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 289 - uriPatterns => $record_uri, 290 - ))->status_is(200); 291 - ok( 292 - _find_label($t->tx->res->json->{labels}, val => '!hide', uri => $record_uri), 293 - 'queryLabels includes the record takedown label', 294 - ); 295 - 296 281 for my $cid ($blob_cid, $nested_blob_cid) { 297 282 $app->store->dbh->do( 298 283 q{DELETE FROM blob_owners WHERE cid = ?}, ··· 332 317 ->json_is('/cursor' => $missing_cids[1]); 333 318 334 319 done_testing; 335 - 336 - sub _find_label { 337 - my ($labels, %expected) = @_; 338 - return 0 unless ref($labels) eq 'ARRAY'; 339 - for my $label (@$labels) { 340 - next unless ref($label) eq 'HASH'; 341 - my $matches = 1; 342 - for my $key (keys %expected) { 343 - next if defined($label->{$key}) && "$label->{$key}" eq "$expected{$key}"; 344 - $matches = 0; 345 - last; 346 - } 347 - return 1 if $matches; 348 - } 349 - return 0; 350 - }
+146
t/label-rpc-surfaces.t
··· 1 + use v5.34; 2 + use warnings; 3 + 4 + use Config (); 5 + use File::Spec; 6 + use File::Temp qw(tempdir); 7 + use FindBin qw($Bin); 8 + use JSON::PP (); 9 + use Test::More; 10 + 11 + BEGIN { 12 + require lib; 13 + my $root = File::Spec->rel2abs(File::Spec->catdir($Bin, '..')); 14 + lib->import( 15 + File::Spec->catdir($root, 'lib'), 16 + File::Spec->catdir($root, 'local', 'lib', 'perl5'), 17 + File::Spec->catdir($root, 'local', 'lib', 'perl5', $Config::Config{archname}), 18 + ); 19 + } 20 + 21 + use Test::Mojo; 22 + use Mojo::URL; 23 + use ATProto::PDS; 24 + 25 + my $root = File::Spec->rel2abs(File::Spec->catdir($Bin, '..')); 26 + my $tmp = tempdir(CLEANUP => 1); 27 + 28 + my $app = ATProto::PDS->new( 29 + project_root => $root, 30 + settings => { 31 + base_url => 'http://127.0.0.1:7755', 32 + service_handle_domain => 'example.test', 33 + service_did_method => 'did:web', 34 + jwt_secret => 'label-surface-secret', 35 + admin_password => 'admin-secret', 36 + data_dir => File::Spec->catdir($tmp, 'data'), 37 + db_path => File::Spec->catfile($tmp, 'perlsky.sqlite'), 38 + }, 39 + ); 40 + 41 + my $t = Test::Mojo->new($app); 42 + my $admin_auth = 'Basic YWRtaW46YWRtaW4tc2VjcmV0'; 43 + 44 + $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 45 + handle => 'alice.example.test', 46 + email => 'alice@example.test', 47 + password => 'hunter22', 48 + })->status_is(200); 49 + 50 + my $session = $t->tx->res->json; 51 + my $did = $session->{did}; 52 + my $access = $session->{accessJwt}; 53 + 54 + $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 55 + Authorization => "Bearer $access", 56 + } => json => { 57 + repo => $did, 58 + collection => 'app.bsky.feed.post', 59 + rkey => 'label-target', 60 + record => { 61 + '$type' => 'app.bsky.feed.post', 62 + text => 'label target', 63 + createdAt => '2026-03-12T00:00:00Z', 64 + }, 65 + })->status_is(200); 66 + 67 + my $record = $t->tx->res->json; 68 + my $record_uri = $record->{uri}; 69 + my $record_cid = $record->{cid}; 70 + 71 + $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 72 + Authorization => $admin_auth, 73 + } => json => { 74 + subject => { did => $did }, 75 + takedown => { applied => JSON::PP::true, ref => 'unit-test' }, 76 + })->status_is(200); 77 + 78 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 79 + uriPatterns => "at://$did*", 80 + ))->status_is(200); 81 + ok( 82 + _find_label($t->tx->res->json->{labels}, val => '!hide'), 83 + 'queryLabels includes the account takedown label', 84 + ); 85 + 86 + $t->get_ok('/xrpc/com.atproto.temp.fetchLabels?limit=10') 87 + ->status_is(200); 88 + ok( 89 + _find_label($t->tx->res->json->{labels}, val => '!hide'), 90 + 'fetchLabels includes the account takedown label', 91 + ); 92 + 93 + $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 94 + Authorization => $admin_auth, 95 + } => json => { 96 + subject => { did => $did }, 97 + takedown => { applied => JSON::PP::false, ref => 'unit-test' }, 98 + })->status_is(200); 99 + 100 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 101 + uriPatterns => "at://$did*", 102 + ))->status_is(200); 103 + ok( 104 + _find_label($t->tx->res->json->{labels}, val => '!hide', neg => JSON::PP::true), 105 + 'queryLabels includes the negated account takedown label', 106 + ); 107 + 108 + $t->get_ok('/xrpc/com.atproto.temp.fetchLabels?limit=10') 109 + ->status_is(200); 110 + ok( 111 + _find_label($t->tx->res->json->{labels}, val => '!hide', neg => JSON::PP::true), 112 + 'fetchLabels includes the negated account takedown label', 113 + ); 114 + 115 + $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 116 + Authorization => $admin_auth, 117 + } => json => { 118 + subject => { uri => $record_uri, cid => $record_cid }, 119 + takedown => { applied => JSON::PP::true }, 120 + })->status_is(200); 121 + 122 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 123 + uriPatterns => $record_uri, 124 + ))->status_is(200); 125 + ok( 126 + _find_label($t->tx->res->json->{labels}, val => '!hide', uri => $record_uri), 127 + 'queryLabels includes the record takedown label', 128 + ); 129 + 130 + done_testing; 131 + 132 + sub _find_label { 133 + my ($labels, %expected) = @_; 134 + return 0 unless ref($labels) eq 'ARRAY'; 135 + for my $label (@$labels) { 136 + next unless ref($label) eq 'HASH'; 137 + my $matches = 1; 138 + for my $key (keys %expected) { 139 + next if defined($label->{$key}) && "$label->{$key}" eq "$expected{$key}"; 140 + $matches = 0; 141 + last; 142 + } 143 + return 1 if $matches; 144 + } 145 + return 0; 146 + }