@@ -30,6 +30,20 @@ function execute(sql: string): Array<Record<string, unknown>> {
3030 return parsed . flatMap ( ( item ) => item . results ?? [ ] ) ;
3131}
3232
33+ /* Sunucu aynı ajandan iki KAYIT OLUŞTURMA arasında en az 15 saniye
34+ * istiyor (publication.create.minimum_interval, migration 0017). Kural
35+ * bu betikten sonra girdi ve betik onu hiç görmüyordu; staging gerçek
36+ * saatle çalıştığı için her ardışık yazım 429 alıyordu.
37+ *
38+ * Revision'lar (PATCH) tetikleyiciyi güncellemiyor, o yüzden yalnız yeni
39+ * kayıtlardan önce bekliyoruz. Bekleme kuralın kendisinden bir saniye
40+ * fazla: sınırda beklemek saat kaymasına bağımlı bir test demek. */
41+ const PUBLICATION_BURST_INTERVAL_MS = 16_000 ;
42+
43+ async function settleBurstWindow ( ) : Promise < void > {
44+ await new Promise ( ( resolve ) => setTimeout ( resolve , PUBLICATION_BURST_INTERVAL_MS ) ) ;
45+ }
46+
3347async function waitReady ( ) : Promise < void > {
3448 for ( let attempt = 0 ; attempt < 60 ; attempt += 1 ) {
3549 const response = await fetch ( `${ ORIGIN } /v1/records` , { method : 'POST' , headers : { 'content-type' : 'application/json' } , body : '{}' } ) ;
@@ -39,6 +53,12 @@ async function waitReady(): Promise<void> {
3953 assert . fail ( 'Slice 4 deployment readiness timeout.' ) ;
4054}
4155
56+ /* Seedlenen her ajan buraya yazılıyor. Kanıt sorgusu ve temizlik eskiden
57+ * ajanları elle sayıyordu; kota kuralları yüzünden ajan eklemek zorunda
58+ * kaldığımızda o listeler sessizce eksik kalırdı — sonuç, temizlenmeyen
59+ * canlı kimlik bilgisi olurdu. */
60+ const seededAgents : Array < { id : string ; token : string ; credentialId : string } > = [ ] ;
61+
4262async function seedAgent (
4363 ownerId : string ,
4464 handle : string ,
@@ -72,7 +92,9 @@ async function seedAgent(
7292 ${ credential . hashVersion } , 'feed:read records:write', ${ quote ( ownerId ) } , ${ now }
7393 )
7494 ` ) ;
75- return { id : agentId , token : credential . token , credentialId : credential . selector } ;
95+ const seeded = { id : agentId , token : credential . token , credentialId : credential . selector } ;
96+ seededAgents . push ( seeded ) ;
97+ return seeded ;
7698}
7799
78100async function ownerSession ( ownerId : string , sessionPepper : string , csrfPepper : string , now : number ) {
@@ -141,7 +163,18 @@ const direct = await seedAgent(ownerId, `slice4-direct-${suffix}`, 'direct_publi
141163const approval = await seedAgent ( ownerId , `slice4-review-${ suffix } ` , 'approval_required' , agentPepper , now + 1 ) ;
142164const readonly = await seedAgent ( ownerId , `slice4-readonly-${ suffix } ` , 'read_only' , agentPepper , now + 2 ) ;
143165const concurrentDirect = await seedAgent ( ownerId , `slice4-concurrent-${ suffix } ` , 'direct_publish' , agentPepper , now + 3 ) ;
144- const concurrentReview = await seedAgent ( ownerId , `slice4-concurrent-review-${ suffix } ` , 'approval_required' , agentPepper , now + 4 ) ;
166+ /* Saatlik gönderi kotası ajan başına 2 (migration 0017). Bu betik tek
167+ * ajana beş gönderi yazdırıyordu ve kural girdiğinden beri geçemiyordu.
168+ * Test ettiğimiz şeyler kotayla ilgili değil, o yüzden tek kullanımlık
169+ * ajan seedliyoruz: kotayı gevşetmek yerine kotanın altında kalıyoruz. */
170+ let disposableCount = 0 ;
171+ const disposableAgent = async ( mode : 'direct_publish' | 'approval_required' = 'direct_publish' ) => {
172+ disposableCount += 1 ;
173+ return await seedAgent (
174+ ownerId , `slice4-tek-${ suffix } -${ disposableCount } ` , mode , agentPepper , now + 10 + disposableCount ,
175+ ) ;
176+ } ;
177+
145178const session = await ownerSession ( ownerId , sessionPepper , csrfPepper , now + 3 ) ;
146179const createdRecordIds : string [ ] = [ ] ;
147180const cleanupRecordIds : string [ ] = [ ] ;
@@ -163,6 +196,7 @@ const replay = await agentWrite(direct.token, '/v1/records', {
163196assert . equal ( replay . status , 201 ) ;
164197assert . equal ( replay . headers . get ( 'idempotency-replayed' ) , 'true' ) ;
165198
199+ await settleBurstWindow ( ) ;
166200const reply = await agentWrite ( direct . token , `/v1/records/${ directBody . record . id } /replies` , {
167201 bodyMarkdown : 'Staging reply kökünü sunucudan alıyor.' ,
168202} , `slice4-${ suffix } -reply` ) ;
@@ -224,6 +258,7 @@ const concurrentPost = await pair(() => agentWrite(concurrentDirect.token, '/v1/
224258const concurrentPostId = concurrentPost . record ! . id ;
225259createdRecordIds . push ( concurrentPostId ) ;
226260cleanupRecordIds . push ( concurrentPostId ) ;
261+ await settleBurstWindow ( ) ;
227262const concurrentReply = await pair ( ( ) => agentWrite (
228263 concurrentDirect . token ,
229264 `/v1/records/${ concurrentPostId } /replies` ,
@@ -236,11 +271,16 @@ await pair(() => agentWrite(concurrentDirect.token, `/v1/records/${concurrentPos
236271 bodyMarkdown : `Staging paralel idempotency revision ${ suffix } .` ,
237272} , `slice4-${ suffix } -concurrent-revision` , 'PATCH' ) , 200 ) ;
238273
274+ /* Slug çakışması global bir sorun, ajana bağlı değil — o yüzden yarışı
275+ * iki ayrı ajana yaptırmak testin ölçtüğü şeyi değiştirmiyor. Tek ajanla
276+ * yapılamaz: aynı anda iki kayıt, 15 saniye kuralının tam olarak
277+ * yasakladığı şey. */
278+ const [ slugAgentA , slugAgentB ] = [ await disposableAgent ( ) , await disposableAgent ( ) ] ;
239279const slugRace = await Promise . all ( [
240- agentWrite ( concurrentDirect . token , '/v1/records' , {
280+ agentWrite ( slugAgentA . token , '/v1/records' , {
241281 bodyMarkdown : `Staging aynı anda üretilen slug ${ suffix } .` ,
242282 } , `slice4-${ suffix } -slug-a` ) ,
243- agentWrite ( concurrentDirect . token , '/v1/records' , {
283+ agentWrite ( slugAgentB . token , '/v1/records' , {
244284 bodyMarkdown : `Staging aynı anda üretilen slug ${ suffix } .` ,
245285 } , `slice4-${ suffix } -slug-b` ) ,
246286] ) ;
@@ -253,12 +293,17 @@ assert.ok(slugBodies.some((item) => item.record.slug.endsWith(item.record.id.rep
253293createdRecordIds . push ( ...slugBodies . map ( ( item ) => item . record . id ) ) ;
254294cleanupRecordIds . push ( ...slugBodies . map ( ( item ) => item . record . id ) ) ;
255295
296+ /* Her bekleyen kayıt kendi ajanıyla üretiliyor. Üç kayıt tek ajandan
297+ gelirse saatlik gönderi kotasına takılıyor; testin konusu onay akışı,
298+ kota değil. Ajanı geri döndürüyoruz çünkü geri çekme adımı aynı
299+ kimlikle imzalanmak zorunda. */
256300const createPending = async ( bodyMarkdown : string , key : string ) => {
257- const response = await agentWrite ( concurrentReview . token , '/v1/records' , { bodyMarkdown } , key ) ;
258- assert . equal ( response . status , 202 ) ;
301+ const agent = await disposableAgent ( 'approval_required' ) ;
302+ const response = await agentWrite ( agent . token , '/v1/records' , { bodyMarkdown } , key ) ;
303+ assert . equal ( response . status , 202 , await response . clone ( ) . text ( ) ) ;
259304 const body = await response . json ( ) as { record : { id : string } } ;
260305 createdRecordIds . push ( body . record . id ) ;
261- return body . record . id ;
306+ return { id : body . record . id , agent } ;
262307} ;
263308const reviewFor = async ( recordId : string ) => {
264309 const reviews = await ownerRequest ( session , '/v1/approvals' ) . then ( ( response ) => response . json ( ) ) as {
@@ -268,39 +313,40 @@ const reviewFor = async (recordId: string) => {
268313 assert . ok ( match ) ;
269314 return match . id ;
270315} ;
271- const approveId = await createPending ( `Staging paralel onay ${ suffix } .` , `slice4-${ suffix } -approve-create` ) ;
316+ const { id : approveId } = await createPending ( `Staging paralel onay ${ suffix } .` , `slice4-${ suffix } -approve-create` ) ;
272317const approveReview = await reviewFor ( approveId ) ;
273318await pair ( ( ) => ownerRequest ( session , `/v1/approvals/${ approveReview } /approve` , 'POST' , {
274319 note : 'parallel staging approval' ,
275320} , `slice4-${ suffix } -concurrent-approve` ) , 200 ) ;
276321cleanupRecordIds . push ( approveId ) ;
277322
278- const rejectId = await createPending ( `Staging paralel ret ${ suffix } .` , `slice4-${ suffix } -reject-create` ) ;
323+ const { id : rejectId } = await createPending ( `Staging paralel ret ${ suffix } .` , `slice4-${ suffix } -reject-create` ) ;
279324const rejectReview = await reviewFor ( rejectId ) ;
280325await pair ( ( ) => ownerRequest ( session , `/v1/approvals/${ rejectReview } /reject` , 'POST' , {
281326 note : 'parallel staging rejection' ,
282327} , `slice4-${ suffix } -concurrent-reject` ) , 200 ) ;
283328
284- const withdrawId = await createPending ( `Staging paralel geri çekme ${ suffix } .` , `slice4-${ suffix } -withdraw-create` ) ;
329+ const { id : withdrawId , agent : withdrawAgent } = await createPending ( `Staging paralel geri çekme ${ suffix } .` , `slice4-${ suffix } -withdraw-create` ) ;
285330await pair ( ( ) => agentWrite (
286- concurrentReview . token ,
331+ withdrawAgent . token ,
287332 `/v1/records/${ withdrawId } /withdraw` ,
288333 { } ,
289334 `slice4-${ suffix } -concurrent-withdraw` ,
290335) , 200 ) ;
291336
292- const agentDeleteRecord = await agentWrite ( concurrentDirect . token , '/v1/records' , {
337+ const agentDeleteAgent = await disposableAgent ( ) ;
338+ const agentDeleteRecord = await agentWrite ( agentDeleteAgent . token , '/v1/records' , {
293339 bodyMarkdown : `Staging paralel ajan silme ${ suffix } .` ,
294340} , `slice4-${ suffix } -agent-delete-create` ) . then ( ( response ) => response . json ( ) ) as { record : { id : string } } ;
295341createdRecordIds . push ( agentDeleteRecord . record . id ) ;
296342await pair ( ( ) => agentWrite (
297- concurrentDirect . token ,
343+ agentDeleteAgent . token ,
298344 `/v1/records/${ agentDeleteRecord . record . id } /delete` ,
299345 { reason : 'parallel staging agent delete' } ,
300346 `slice4-${ suffix } -concurrent-agent-delete` ,
301347) , 200 ) ;
302348
303- const sponsorDeleteRecord = await agentWrite ( concurrentDirect . token , '/v1/records' , {
349+ const sponsorDeleteRecord = await agentWrite ( ( await disposableAgent ( ) ) . token , '/v1/records' , {
304350 bodyMarkdown : `Staging paralel sponsor silme ${ suffix } .` ,
305351} , `slice4-${ suffix } -sponsor-delete-create` ) . then ( ( response ) => response . json ( ) ) as { record : { id : string } } ;
306352createdRecordIds . push ( sponsorDeleteRecord . record . id ) ;
@@ -317,27 +363,40 @@ const evidence = execute(`
317363 (SELECT COUNT(*) FROM records WHERE id IN (${ createdRecordIds . map ( quote ) . join ( ',' ) } )) AS records,
318364 (SELECT COUNT(*) FROM audit_events WHERE subject_type = 'record' AND subject_id IN (${ createdRecordIds . map ( quote ) . join ( ',' ) } )) AS audits,
319365 (SELECT COUNT(*) FROM idempotency_keys WHERE
320- (principal_type = 'agent' AND principal_id IN (${ quote ( direct . id ) } , ${ quote ( approval . id ) } , ${ quote ( concurrentDirect . id ) } , ${ quote ( concurrentReview . id ) } ))
366+ (principal_type = 'agent' AND principal_id IN (${ seededAgents . map ( ( agent ) => quote ( agent . id ) ) . join ( ',' ) } ))
321367 OR (principal_type = 'account' AND principal_id = ${ quote ( ownerId ) } )
322368 ) AS idempotency_rows
323369` ) [ 0 ] as { records : number ; audits : number ; idempotency_rows : number } ;
324370assert . equal ( Number ( evidence . records ) , createdRecordIds . length ) ;
325371assert . ok ( Number ( evidence . audits ) >= 15 ) ;
326372assert . ok ( Number ( evidence . idempotency_rows ) >= 15 ) ;
327373
374+ /* Kök gönderiyi silmek yanıt ağacını da siliyor — bu, ayrı bir testin
375+ * doğruladığı kasıtlı davranış. Dolayısıyla listede önce kök, sonra onun
376+ * yanıtı geldiğinde ikincisi 404 döner ve bu bir arıza değil, istediğimiz
377+ * son durumun ta kendisi. Temizliğin ölçütü "her çağrı 200 döndü mü"
378+ * değil, "geride yayımlanmış kayıt kaldı mı". */
328379for ( const recordId of cleanupRecordIds ) {
329380 const ownerDelete = await ownerRequest ( session , `/v1/manage/records/${ recordId } /delete` , 'POST' , {
330381 reason : 'Slice 4 staging cleanup.' ,
331382 } , `slice4-${ suffix } -delete-${ recordId } ` ) ;
332- assert . equal ( ownerDelete . status , 200 ) ;
383+ assert . ok (
384+ ownerDelete . status === 200 || ownerDelete . status === 404 ,
385+ `Cleanup delete failed for ${ recordId } : ${ ownerDelete . status } ` ,
386+ ) ;
387+ }
388+ for ( const recordId of cleanupRecordIds ) {
389+ assert . equal (
390+ ( await fetch ( `${ ORIGIN } /v1/records/${ recordId } ` ) ) . status ,
391+ 404 ,
392+ `Cleanup left ${ recordId } publicly readable.` ,
393+ ) ;
333394}
334395execute ( `
335396 UPDATE agent_credentials SET revoked_at = ${ Date . now ( ) } , revoked_reason = 'staging_test_cleanup'
336- WHERE id IN (${ quote ( direct . credentialId ) } , ${ quote ( approval . credentialId ) } , ${ quote ( readonly . credentialId ) } ,
337- ${ quote ( concurrentDirect . credentialId ) } , ${ quote ( concurrentReview . credentialId ) } );
397+ WHERE id IN (${ seededAgents . map ( ( agent ) => quote ( agent . credentialId ) ) . join ( ',' ) } );
338398 UPDATE agents SET status = 'retired', updated_at = ${ Date . now ( ) }
339- WHERE id IN (${ quote ( direct . id ) } , ${ quote ( approval . id ) } , ${ quote ( readonly . id ) } ,
340- ${ quote ( concurrentDirect . id ) } , ${ quote ( concurrentReview . id ) } );
399+ WHERE id IN (${ seededAgents . map ( ( agent ) => quote ( agent . id ) ) . join ( ',' ) } );
341400 UPDATE sessions SET revoked_at = ${ Date . now ( ) } , revoked_reason = 'staging_test_cleanup'
342401 WHERE id = ${ quote ( session . id ) }
343402` ) ;
0 commit comments