@@ -457,6 +457,155 @@ export function shouldBehaveLikeCommitmentRegistry(): void {
457457 } ) ;
458458 } ) ;
459459
460+ // ── postCommitmentsSafe (idempotent) ────────────────────────────────
461+
462+ describe ( "Post Commitments Safe" , function ( ) {
463+ beforeEach ( async function ( ) {
464+ await this . registry . setVersionStatus ( VERSION_1 , VersionStatus . Active ) ;
465+ } ) ;
466+
467+ it ( "should write all handles and emit (newlyPosted=N, skipped=0) on first call" , async function ( ) {
468+ const handles = Array . from ( { length : 3 } , ( ) => randomBytes32 ( ) ) ;
469+ const commitHashes = Array . from ( { length : 3 } , ( ) => randomBytes32 ( ) ) ;
470+ const registryAsPoster = this . registry . connect ( this . poster ) ;
471+
472+ await expect ( registryAsPoster . postCommitmentsSafe ( VERSION_1 , handles , commitHashes ) )
473+ . to . emit ( this . registry , "CommitmentsPostedSafe" )
474+ . withArgs ( VERSION_1 , 3 , 0 ) ;
475+
476+ for ( let i = 0 ; i < 3 ; i ++ ) {
477+ expect ( await this . registry . getCommitment ( VERSION_1 , handles [ i ] ) ) . to . equal ( commitHashes [ i ] ) ;
478+ }
479+ expect ( await this . registry . getSize ( VERSION_1 ) ) . to . equal ( 3 ) ;
480+ } ) ;
481+
482+ it ( "should silently skip already-committed handles" , async function ( ) {
483+ const handle = randomBytes32 ( ) ;
484+ const original = randomBytes32 ( ) ;
485+ const registryAsPoster = this . registry . connect ( this . poster ) ;
486+
487+ await registryAsPoster . postCommitmentsSafe ( VERSION_1 , [ handle ] , [ original ] ) ;
488+
489+ // Re-post same handle with a different commit hash — should NOT revert
490+ // and should NOT overwrite the original.
491+ await expect (
492+ registryAsPoster . postCommitmentsSafe ( VERSION_1 , [ handle ] , [ randomBytes32 ( ) ] )
493+ )
494+ . to . emit ( this . registry , "CommitmentsPostedSafe" )
495+ . withArgs ( VERSION_1 , 0 , 1 ) ;
496+
497+ expect ( await this . registry . getCommitment ( VERSION_1 , handle ) ) . to . equal ( original ) ;
498+ expect ( await this . registry . getSize ( VERSION_1 ) ) . to . equal ( 1 ) ;
499+ } ) ;
500+
501+ it ( "should write only new handles in a mixed batch" , async function ( ) {
502+ const existingHandle = randomBytes32 ( ) ;
503+ const newHandle1 = randomBytes32 ( ) ;
504+ const newHandle2 = randomBytes32 ( ) ;
505+ const existingCommit = randomBytes32 ( ) ;
506+ const registryAsPoster = this . registry . connect ( this . poster ) ;
507+
508+ await registryAsPoster . postCommitmentsSafe ( VERSION_1 , [ existingHandle ] , [ existingCommit ] ) ;
509+
510+ const newCommit1 = randomBytes32 ( ) ;
511+ const newCommit2 = randomBytes32 ( ) ;
512+ await expect (
513+ registryAsPoster . postCommitmentsSafe (
514+ VERSION_1 ,
515+ [ existingHandle , newHandle1 , newHandle2 ] ,
516+ [ randomBytes32 ( ) , newCommit1 , newCommit2 ]
517+ )
518+ )
519+ . to . emit ( this . registry , "CommitmentsPostedSafe" )
520+ . withArgs ( VERSION_1 , 2 , 1 ) ;
521+
522+ // existing one preserved
523+ expect ( await this . registry . getCommitment ( VERSION_1 , existingHandle ) ) . to . equal ( existingCommit ) ;
524+ // new ones written
525+ expect ( await this . registry . getCommitment ( VERSION_1 , newHandle1 ) ) . to . equal ( newCommit1 ) ;
526+ expect ( await this . registry . getCommitment ( VERSION_1 , newHandle2 ) ) . to . equal ( newCommit2 ) ;
527+ expect ( await this . registry . getSize ( VERSION_1 ) ) . to . equal ( 3 ) ;
528+ } ) ;
529+
530+ it ( "should dedup duplicate handles within the same batch" , async function ( ) {
531+ const handle = randomBytes32 ( ) ;
532+ const commitHash = randomBytes32 ( ) ;
533+ const registryAsPoster = this . registry . connect ( this . poster ) ;
534+
535+ // Same handle three times in one call — should write once, skip twice.
536+ await expect (
537+ registryAsPoster . postCommitmentsSafe (
538+ VERSION_1 ,
539+ [ handle , handle , handle ] ,
540+ [ commitHash , randomBytes32 ( ) , randomBytes32 ( ) ]
541+ )
542+ )
543+ . to . emit ( this . registry , "CommitmentsPostedSafe" )
544+ . withArgs ( VERSION_1 , 1 , 2 ) ;
545+
546+ expect ( await this . registry . getCommitment ( VERSION_1 , handle ) ) . to . equal ( commitHash ) ;
547+ expect ( await this . registry . getSize ( VERSION_1 ) ) . to . equal ( 1 ) ;
548+ } ) ;
549+
550+ it ( "should still revert on zero commitHash" , async function ( ) {
551+ const handle = randomBytes32 ( ) ;
552+ const registryAsPoster = this . registry . connect ( this . poster ) ;
553+
554+ await expect (
555+ registryAsPoster . postCommitmentsSafe ( VERSION_1 , [ handle ] , [ ethers . ZeroHash ] )
556+ )
557+ . to . be . revertedWithCustomError ( this . registry , "ZeroCommitHash" )
558+ . withArgs ( handle ) ;
559+ } ) ;
560+
561+ it ( "should still revert on empty batch" , async function ( ) {
562+ const registryAsPoster = this . registry . connect ( this . poster ) ;
563+ await expect (
564+ registryAsPoster . postCommitmentsSafe ( VERSION_1 , [ ] , [ ] )
565+ ) . to . be . revertedWithCustomError ( this . registry , "EmptyBatch" ) ;
566+ } ) ;
567+
568+ it ( "should still revert on length mismatch" , async function ( ) {
569+ const registryAsPoster = this . registry . connect ( this . poster ) ;
570+ await expect (
571+ registryAsPoster . postCommitmentsSafe (
572+ VERSION_1 ,
573+ [ randomBytes32 ( ) ] ,
574+ [ randomBytes32 ( ) , randomBytes32 ( ) ]
575+ )
576+ ) . to . be . revertedWithCustomError ( this . registry , "LengthMismatch" ) ;
577+ } ) ;
578+
579+ it ( "should revert when version is not active" , async function ( ) {
580+ const registryAsPoster = this . registry . connect ( this . poster ) ;
581+ await expect (
582+ registryAsPoster . postCommitmentsSafe ( VERSION_2 , [ randomBytes32 ( ) ] , [ randomBytes32 ( ) ] )
583+ )
584+ . to . be . revertedWithCustomError ( this . registry , "VersionNotActive" )
585+ . withArgs ( VERSION_2 ) ;
586+ } ) ;
587+
588+ it ( "should revert when caller is not a poster" , async function ( ) {
589+ const registryAsOther = this . registry . connect ( this . otherAccount ) ;
590+ await expect (
591+ registryAsOther . postCommitmentsSafe ( VERSION_1 , [ randomBytes32 ( ) ] , [ randomBytes32 ( ) ] )
592+ )
593+ . to . be . revertedWithCustomError ( this . registry , "OnlyPosterAllowed" )
594+ . withArgs ( this . otherAccount . address ) ;
595+ } ) ;
596+
597+ it ( "should not double-count handles in handlesByVersion when re-posted" , async function ( ) {
598+ const handle = randomBytes32 ( ) ;
599+ const registryAsPoster = this . registry . connect ( this . poster ) ;
600+
601+ await registryAsPoster . postCommitmentsSafe ( VERSION_1 , [ handle ] , [ randomBytes32 ( ) ] ) ;
602+ await registryAsPoster . postCommitmentsSafe ( VERSION_1 , [ handle ] , [ randomBytes32 ( ) ] ) ;
603+ await registryAsPoster . postCommitmentsSafe ( VERSION_1 , [ handle ] , [ randomBytes32 ( ) ] ) ;
604+
605+ expect ( await this . registry . getSize ( VERSION_1 ) ) . to . equal ( 1 ) ;
606+ } ) ;
607+ } ) ;
608+
460609 // ── Access Control ─────────────────────────────────────────────────
461610
462611 describe ( "Access Control" , function ( ) {
0 commit comments