···172172| `t/reference-differential-plc.t` | direct reference differential | official runtime comparison in PLC mode |
173173| `t/reference-differential.t` | direct reference differential | official runtime comparison in baseline mode |
174174| `t/remote-handle-resolution.t` | audited local regression | remote `did:web` DID docs, conservative remote identity handling, external-handle adoption, invalid-handle rejection, and missing remote DID/identity error semantics |
175175-| `t/repo-api.t` | audited local regression | record mutation and read semantics, but still lighter than ideal on some negative/reference edge cases |
175175+| `t/repo-api.t` | audited local regression | record mutation, stale-CID, swap, and missing-delete semantics |
176176| `t/repo-firehose-car.t` | audited local regression | repo commit CAR shape and firehose interactions |
177177| `t/repo_formats.t` | audited local regression | direct repo wire-format and CAR expectations |
178178| `t/self-service-invite-surfaces.t` | audited local regression | isolated self-service invite issuance coverage for invite minting, account scoping, and account invite-code listing |
+13-2
lib/ATProto/PDS/API/Repo.pm
···8686 return undef;
8787 }
8888 assert_repo_readable($c, $account);
8989+ my $cid = $c->param('cid');
8990 my $row = $c->store->get_record(
9091 $account->{did},
9192 $c->param('collection'),
9293 $c->param('rkey'),
9393- $c->param('cid'),
9494+ $cid,
9495 );
9595- xrpc_error(404, 'RecordNotFound', 'Record was not found') unless $row;
9696+ unless ($row) {
9797+ if (defined $cid && length $cid) {
9898+ my $current = $c->store->get_record(
9999+ $account->{did},
100100+ $c->param('collection'),
101101+ $c->param('rkey'),
102102+ );
103103+ xrpc_error(400, 'RecordNotFound', 'Record was not found') if $current;
104104+ }
105105+ xrpc_error(404, 'RecordNotFound', 'Record was not found');
106106+ }
96107 assert_record_readable($c, _record_uri($account->{did}, $row->{collection}, $row->{rkey}));
97108 return _record_view($account->{did}, $row);
98109 });
+50
script/differential-validate
···13371337 'putRecord create-or-update semantics match the official reference PDS',
13381338);
1339133913401340+note('Comparing repo stale-cid and missing-delete semantics');
13411341+for my $name (sort keys %server) {
13421342+ my $stale_get = get_form($server{$name}{origin}, 'com.atproto.repo.getRecord', {
13431343+ repo => $server{$name}{did},
13441344+ collection => 'app.bsky.feed.post',
13451345+ rkey => 'diffpost',
13461346+ cid => $server{$name}{record_cid},
13471347+ });
13481348+13491349+ my $latest_before_missing_delete = get_form(
13501350+ $server{$name}{origin},
13511351+ 'com.atproto.sync.getLatestCommit',
13521352+ { did => $server{$name}{did} },
13531353+ );
13541354+ check($latest_before_missing_delete->is_success, "$name getLatestCommit succeeds before missing delete");
13551355+13561356+ my $missing_delete = post_json($server{$name}{origin}, 'com.atproto.repo.deleteRecord', {
13571357+ repo => $server{$name}{did},
13581358+ collection => 'app.bsky.feed.post',
13591359+ rkey => 'missing-rkey-ok',
13601360+ }, auth_header($server{$name}{access}));
13611361+13621362+ my $latest_after_missing_delete = get_form(
13631363+ $server{$name}{origin},
13641364+ 'com.atproto.sync.getLatestCommit',
13651365+ { did => $server{$name}{did} },
13661366+ );
13671367+ check($latest_after_missing_delete->is_success, "$name getLatestCommit succeeds after missing delete");
13681368+13691369+ $server{$name}{repo_read_edges} = {
13701370+ stale_get => normalize_xrpc_error($stale_get),
13711371+ missing_delete => {
13721372+ status => $missing_delete->code // 0,
13731373+ body => $missing_delete->body,
13741374+ },
13751375+ latest_commit_unchanged => (
13761376+ (($latest_before_missing_delete->json || {})->{cid} // q()) eq (($latest_after_missing_delete->json || {})->{cid} // q())
13771377+ && (($latest_before_missing_delete->json || {})->{rev} // q()) eq (($latest_after_missing_delete->json || {})->{rev} // q())
13781378+ ) ? 1 : 0,
13791379+ };
13801380+}
13811381+13821382+if (!same_hash($server{reference}{repo_read_edges}, $server{perlsky}{repo_read_edges})) {
13831383+ note('reference repo read edges: ' . encode_json($server{reference}{repo_read_edges}));
13841384+ note('perlsky repo read edges: ' . encode_json($server{perlsky}{repo_read_edges}));
13851385+ fail_check('repo stale-cid reads and missing-delete no-ops match the official reference PDS');
13861386+} else {
13871387+ pass('repo stale-cid reads and missing-delete no-ops match the official reference PDS');
13881388+}
13891389+13401390note('Comparing applyWrites');
13411391for my $name (sort keys %server) {
13421392 my $seed_update = post_json($server{$name}{origin}, 'com.atproto.repo.createRecord', {