@@ -551,6 +551,235 @@ void main() {
551551
552552 await VaultService .close (handle: reopened2);
553553 });
554+ // -- Defragmentation ------------------------------------------------
555+
556+ test ('defragment compacts free space' , () async {
557+ final path = '${tempDir .path }/defrag.vault' ;
558+ final key = await generateAes256GcmKey ();
559+
560+ final handle = await VaultService .create (
561+ path: path,
562+ key: key,
563+ algorithm: 'aes-256-gcm' ,
564+ capacityBytes: 2 * 1024 * 1024 ,
565+ );
566+
567+ // Write A, B, C
568+ final dataA = Uint8List .fromList (List .generate (1000 , (i) => i % 256 ));
569+ final dataC = Uint8List .fromList (List .generate (500 , (i) => (i * 3 ) % 256 ));
570+ await VaultService .write (handle: handle, name: 'a.txt' , data: dataA);
571+ await VaultService .write (
572+ handle: handle,
573+ name: 'b.txt' ,
574+ data: Uint8List (2000 ),
575+ );
576+ await VaultService .write (handle: handle, name: 'c.txt' , data: dataC);
577+
578+ // Delete B → creates a gap
579+ await VaultService .delete (handle: handle, name: 'b.txt' );
580+
581+ final beforeHealth = await VaultService .health (handle: handle);
582+ expect (beforeHealth.freeRegionCount, greaterThan (0 ));
583+ expect (beforeHealth.fragmentationRatio, greaterThan (0.0 ));
584+
585+ // Defragment
586+ final result = await VaultService .defragment (handle: handle);
587+ expect (result.segmentsMoved, greaterThan (0 ));
588+ expect (result.bytesReclaimed, greaterThan (BigInt .zero));
589+
590+ // After defrag: no fragmentation
591+ final afterHealth = await VaultService .health (handle: handle);
592+ expect (afterHealth.freeRegionCount, 0 );
593+ expect (afterHealth.fragmentationRatio, 0.0 );
594+
595+ // Data still readable
596+ expect (await VaultService .read (handle: handle, name: 'a.txt' ), dataA);
597+ expect (await VaultService .read (handle: handle, name: 'c.txt' ), dataC);
598+
599+ await VaultService .close (handle: handle);
600+ });
601+
602+ test ('defragment on empty vault is no-op' , () async {
603+ final path = '${tempDir .path }/defrag_empty.vault' ;
604+ final key = await generateAes256GcmKey ();
605+
606+ final handle = await VaultService .create (
607+ path: path,
608+ key: key,
609+ algorithm: 'aes-256-gcm' ,
610+ capacityBytes: 1024 * 1024 ,
611+ );
612+
613+ final result = await VaultService .defragment (handle: handle);
614+ expect (result.segmentsMoved, 0 );
615+ expect (result.bytesReclaimed, BigInt .zero);
616+
617+ await VaultService .close (handle: handle);
618+ });
619+
620+ // -- Resize ---------------------------------------------------------
621+
622+ test ('resize grow then write in new space' , () async {
623+ final path = '${tempDir .path }/grow.vault' ;
624+ final key = await generateAes256GcmKey ();
625+
626+ final handle = await VaultService .create (
627+ path: path,
628+ key: key,
629+ algorithm: 'aes-256-gcm' ,
630+ capacityBytes: 512 * 1024 , // 512KB
631+ );
632+
633+ // Grow to 1MB
634+ await VaultService .resize (
635+ handle: handle,
636+ newCapacityBytes: 1024 * 1024 ,
637+ );
638+
639+ final health = await VaultService .health (handle: handle);
640+ expect (health.totalBytes, BigInt .from (1024 * 1024 ));
641+
642+ // Write data that wouldn't fit in original 512KB
643+ final bigData = Uint8List (600 * 1024 ); // 600KB
644+ await VaultService .write (handle: handle, name: 'big.bin' , data: bigData);
645+
646+ final result = await VaultService .read (handle: handle, name: 'big.bin' );
647+ expect (result, bigData);
648+
649+ await VaultService .close (handle: handle);
650+ });
651+
652+ test ('resize shrink after defrag' , () async {
653+ final path = '${tempDir .path }/shrink.vault' ;
654+ final key = await generateAes256GcmKey ();
655+
656+ final handle = await VaultService .create (
657+ path: path,
658+ key: key,
659+ algorithm: 'aes-256-gcm' ,
660+ capacityBytes: 1024 * 1024 , // 1MB
661+ );
662+
663+ // Write small data
664+ final data = Uint8List .fromList (List .generate (100 , (i) => i % 256 ));
665+ await VaultService .write (handle: handle, name: 'small.bin' , data: data);
666+
667+ // Defrag to compact
668+ await VaultService .defragment (handle: handle);
669+
670+ // Shrink to 512KB (data fits)
671+ await VaultService .resize (
672+ handle: handle,
673+ newCapacityBytes: 512 * 1024 ,
674+ );
675+
676+ final health = await VaultService .health (handle: handle);
677+ expect (health.totalBytes, BigInt .from (512 * 1024 ));
678+
679+ // Data still readable
680+ expect (await VaultService .read (handle: handle, name: 'small.bin' ), data);
681+
682+ await VaultService .close (handle: handle);
683+ });
684+
685+ test ('shrink below used space throws error' , () async {
686+ final path = '${tempDir .path }/shrink_fail.vault' ;
687+ final key = await generateAes256GcmKey ();
688+
689+ final handle = await VaultService .create (
690+ path: path,
691+ key: key,
692+ algorithm: 'aes-256-gcm' ,
693+ capacityBytes: 1024 * 1024 ,
694+ );
695+
696+ // Write enough data to occupy space
697+ final data = Uint8List (200 * 1024 ); // 200KB
698+ await VaultService .write (handle: handle, name: 'data.bin' , data: data);
699+
700+ // Try to shrink to 1KB → should fail
701+ try {
702+ await VaultService .resize (handle: handle, newCapacityBytes: 1024 );
703+ fail ('Expected VaultFull error' );
704+ } catch (e) {
705+ expect (e.toString (), contains ('vaultFull' ));
706+ }
707+
708+ await VaultService .close (handle: handle);
709+ });
710+
711+ // -- Health Check ---------------------------------------------------
712+
713+ test ('health info on fresh vault' , () async {
714+ final path = '${tempDir .path }/health.vault' ;
715+ final key = await generateAes256GcmKey ();
716+
717+ final cap = 1024 * 1024 ;
718+ final handle = await VaultService .create (
719+ path: path,
720+ key: key,
721+ algorithm: 'aes-256-gcm' ,
722+ capacityBytes: cap,
723+ );
724+
725+ final h = await VaultService .health (handle: handle);
726+ expect (h.totalBytes, BigInt .from (cap));
727+ expect (h.usedBytes, BigInt .zero);
728+ expect (h.segmentCount, 0 );
729+ expect (h.freeRegionCount, 0 );
730+ expect (h.fragmentationRatio, 0.0 );
731+ expect (h.isConsistent, true );
732+ expect (h.largestFreeBlock, BigInt .from (cap));
733+
734+ await VaultService .close (handle: handle);
735+ });
736+
737+ test ('health after write and delete shows fragmentation' , () async {
738+ final path = '${tempDir .path }/health_frag.vault' ;
739+ final key = await generateAes256GcmKey ();
740+
741+ final handle = await VaultService .create (
742+ path: path,
743+ key: key,
744+ algorithm: 'aes-256-gcm' ,
745+ capacityBytes: 2 * 1024 * 1024 ,
746+ );
747+
748+ await VaultService .write (
749+ handle: handle,
750+ name: 'a.txt' ,
751+ data: Uint8List (1000 ),
752+ );
753+ await VaultService .write (
754+ handle: handle,
755+ name: 'b.txt' ,
756+ data: Uint8List (2000 ),
757+ );
758+ await VaultService .write (
759+ handle: handle,
760+ name: 'c.txt' ,
761+ data: Uint8List (500 ),
762+ );
763+
764+ // Delete middle segment
765+ await VaultService .delete (handle: handle, name: 'b.txt' );
766+
767+ final h = await VaultService .health (handle: handle);
768+ expect (h.segmentCount, 2 );
769+ expect (h.freeRegionCount, greaterThan (0 ));
770+ expect (h.fragmentationRatio, greaterThan (0.0 ));
771+ expect (h.isConsistent, true );
772+
773+ // Defrag → fragmentation goes to 0
774+ await VaultService .defragment (handle: handle);
775+ final after = await VaultService .health (handle: handle);
776+ expect (after.freeRegionCount, 0 );
777+ expect (after.fragmentationRatio, 0.0 );
778+ expect (after.isConsistent, true );
779+
780+ await VaultService .close (handle: handle);
781+ });
782+
554783 test ('corrupted primary index falls back to shadow' , () async {
555784 final path = '${tempDir .path }/shadow.vault' ;
556785 final key = await generateAes256GcmKey ();
0 commit comments