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.

Document applyWrites reference divergence

alice 5cc091df 6f1e646f

+117
+1
docs/TEST_AUDIT.md
··· 63 63 - `com.atproto.sync.getBlob` should ship the same download-hardening headers as the reference PDS (`X-Content-Type-Options`, `Content-Disposition`, `Content-Security-Policy`). 64 64 - `com.atproto.sync.getBlob` should not add an extra `Cross-Origin-Resource-Policy` header beyond the reference PDS surface; the executable differential now pins the exact hardening-header set instead of a stricter local variant. 65 65 - `com.atproto.sync.listReposByCollection` is present in the published lexicon but not exposed by the current official runtime, so it remains locally regression-tested rather than executable-reference-differenced. 66 + - `com.atproto.repo.applyWrites` now has executable-reference happy-path coverage, but the official runtime still returns a `500 InternalServerError` when a delete write targets a missing record. Perlsky intentionally keeps the cleaner `400 InvalidRequest` there and documents the difference instead of copying the upstream quirk. 66 67 67 68 ## Known Intentional Divergences 68 69
+116
script/differential-validate
··· 1082 1082 'putRecord create-or-update semantics match the official reference PDS', 1083 1083 ); 1084 1084 1085 + note('Comparing applyWrites'); 1086 + for my $name (sort keys %server) { 1087 + my $seed_update = post_json($server{$name}{origin}, 'com.atproto.repo.createRecord', { 1088 + repo => $server{$name}{did}, 1089 + collection => 'app.bsky.feed.post', 1090 + rkey => 'apply-update-target', 1091 + record => { 1092 + %{$record}, 1093 + text => "applyWrites seed update for $name", 1094 + }, 1095 + }, auth_header($server{$name}{access})); 1096 + check($seed_update->is_success, "$name applyWrites seed update record succeeds"); 1097 + 1098 + my $seed_delete = post_json($server{$name}{origin}, 'com.atproto.repo.createRecord', { 1099 + repo => $server{$name}{did}, 1100 + collection => 'app.bsky.feed.post', 1101 + rkey => 'apply-delete-target', 1102 + record => { 1103 + %{$record}, 1104 + text => "applyWrites seed delete for $name", 1105 + }, 1106 + }, auth_header($server{$name}{access})); 1107 + check($seed_delete->is_success, "$name applyWrites seed delete record succeeds"); 1108 + next unless $seed_update->is_success && $seed_delete->is_success; 1109 + 1110 + my $apply = post_json($server{$name}{origin}, 'com.atproto.repo.applyWrites', { 1111 + repo => $server{$name}{did}, 1112 + writes => [ 1113 + { 1114 + '$type' => 'com.atproto.repo.applyWrites#create', 1115 + collection => 'app.bsky.feed.post', 1116 + rkey => 'apply-created', 1117 + value => { 1118 + '$type' => 'app.bsky.feed.post', 1119 + text => "applyWrites create for $name", 1120 + createdAt => '2026-03-12T00:00:01Z', 1121 + }, 1122 + }, 1123 + { 1124 + '$type' => 'com.atproto.repo.applyWrites#update', 1125 + collection => 'app.bsky.feed.post', 1126 + rkey => 'apply-update-target', 1127 + value => { 1128 + '$type' => 'app.bsky.feed.post', 1129 + text => "applyWrites update for $name", 1130 + createdAt => '2026-03-12T00:00:02Z', 1131 + }, 1132 + }, 1133 + { 1134 + '$type' => 'com.atproto.repo.applyWrites#delete', 1135 + collection => 'app.bsky.feed.post', 1136 + rkey => 'apply-delete-target', 1137 + }, 1138 + ], 1139 + }, auth_header($server{$name}{access})); 1140 + check($apply->is_success, "$name applyWrites succeeds"); 1141 + 1142 + my $created = get_form($server{$name}{origin}, 'com.atproto.repo.getRecord', { 1143 + repo => $server{$name}{did}, 1144 + collection => 'app.bsky.feed.post', 1145 + rkey => 'apply-created', 1146 + }); 1147 + my $updated = get_form($server{$name}{origin}, 'com.atproto.repo.getRecord', { 1148 + repo => $server{$name}{did}, 1149 + collection => 'app.bsky.feed.post', 1150 + rkey => 'apply-update-target', 1151 + }); 1152 + my $deleted = get_form($server{$name}{origin}, 'com.atproto.repo.getRecord', { 1153 + repo => $server{$name}{did}, 1154 + collection => 'app.bsky.feed.post', 1155 + rkey => 'apply-delete-target', 1156 + }); 1157 + my $missing_delete = post_json($server{$name}{origin}, 'com.atproto.repo.applyWrites', { 1158 + repo => $server{$name}{did}, 1159 + writes => [ 1160 + { 1161 + '$type' => 'com.atproto.repo.applyWrites#delete', 1162 + collection => 'app.bsky.feed.post', 1163 + rkey => 'apply-missing-target', 1164 + }, 1165 + ], 1166 + }, auth_header($server{$name}{access})); 1167 + 1168 + my $apply_json = $apply->json || {}; 1169 + my $results = $apply_json->{results} || []; 1170 + $server{$name}{apply_writes} = { 1171 + ok => $apply->is_success ? 1 : 0, 1172 + has_commit => ref($apply_json->{commit}) eq 'HASH' ? 1 : 0, 1173 + results_count => 0 + @$results, 1174 + create_result_type => ($results->[0]{'$type'} // q()), 1175 + update_result_type => ($results->[1]{'$type'} // q()), 1176 + delete_result_type => ($results->[2]{'$type'} // q()), 1177 + created_text_ok => (($created->json || {})->{value}{text} // q()) =~ /\AapplyWrites create / ? 1 : 0, 1178 + updated_text_ok => (($updated->json || {})->{value}{text} // q()) =~ /\AapplyWrites update / ? 1 : 0, 1179 + deleted_missing => $deleted->is_success ? 0 : 1, 1180 + deleted_error => ($deleted->json || {})->{error} // q(), 1181 + }; 1182 + $server{$name}{apply_writes_missing_delete_error} = normalize_xrpc_error($missing_delete); 1183 + } 1184 + 1185 + if (!same_hash($server{reference}{apply_writes}, $server{perlsky}{apply_writes})) { 1186 + note('reference applyWrites: ' . encode_json($server{reference}{apply_writes})); 1187 + note('perlsky applyWrites: ' . encode_json($server{perlsky}{apply_writes})); 1188 + fail_check('applyWrites matches the official reference PDS semantics'); 1189 + } else { 1190 + pass('applyWrites matches the official reference PDS semantics'); 1191 + } 1192 + 1193 + if (!same_hash($server{reference}{apply_writes_missing_delete_error}, $server{perlsky}{apply_writes_missing_delete_error})) { 1194 + note('reference applyWrites missing-delete error: ' . encode_json($server{reference}{apply_writes_missing_delete_error})); 1195 + note('perlsky applyWrites missing-delete error: ' . encode_json($server{perlsky}{apply_writes_missing_delete_error})); 1196 + pass('applyWrites missing-delete error remains a documented intentional divergence from the official runtime'); 1197 + } else { 1198 + pass('applyWrites missing-delete error matches the official reference PDS semantics'); 1199 + } 1200 + 1085 1201 note('Comparing repo write swap preconditions'); 1086 1202 for my $name (sort keys %server) { 1087 1203 my $put_swap_commit = post_json($server{$name}{origin}, 'com.atproto.repo.putRecord', {