@@ -555,14 +555,15 @@ func TestFirecrackerSnapshotFeature(t *testing.T) {
555555
556556// TestFirecrackerForkFromTemplate exercises the full template-driven fork
557557// path under the state-machine design: a firecracker source goes Running →
558- // Standby, the first fork implicitly promotes it to Template, and the fork:
558+ // Standby → Template (explicit promote), then a fork:
559559//
560560// (a) reaches Running,
561561// (b) has its mem-file hardlinked to the source's snapshot mem-file
562562// (the fan-out optimisation),
563- // (c) bumps the template's ForkCount to 1 ,
563+ // (c) is counted as a live fork of the template ,
564564// (d) registers with the per-template uffd page server,
565- // (e) on delete, decrements the ForkCount and detaches from uffd.
565+ // (e) on delete, the fork count drops back to 0 and the fork detaches
566+ // from uffd.
566567func TestFirecrackerForkFromTemplate (t * testing.T ) {
567568 t .Parallel ()
568569 requireFirecrackerIntegrationPrereqs (t )
@@ -602,7 +603,11 @@ func TestFirecrackerForkFromTemplate(t *testing.T) {
602603 require .Equal (t , StateStandby , source .State )
603604 require .True (t , source .HasSnapshot )
604605
605- // Forking from the standby source implicitly promotes it to Template.
606+ // Promote to Template explicitly — only Template sources get fan-out.
607+ source , err = mgr .PromoteToTemplate (ctx , sourceID )
608+ require .NoError (t , err )
609+ require .Equal (t , StateTemplate , source .State )
610+
606611 forked , err := mgr .ForkInstance (ctx , sourceID , ForkInstanceRequest {
607612 Name : "fc-tpl-fork" ,
608613 TargetState : StateRunning ,
@@ -622,10 +627,15 @@ func TestFirecrackerForkFromTemplate(t *testing.T) {
622627 // (b) The fork's mem-file must share the source's inode (hardlink), not
623628 // be a copy. We can't compare paths because the link is by inode; we
624629 // compare st_ino + st_dev between the two instances' mem-files.
625- forkMemPath := filepath .Join (p .InstanceSnapshotLatest (forkID ), templateSharedMemFileName )
630+ //
631+ // Firecracker retains the post-restore snapshot dir as snapshot-base
632+ // (see restoreRetainedSnapshotBase), so after the Standby -> Running
633+ // transition the hardlink lives under snapshot-base/, not snapshot-latest/.
634+ // Hardlinks survive the rename because they bind to the inode.
635+ forkMemPath := filepath .Join (p .InstanceSnapshotBase (forkID ), templateSharedMemFileName )
626636 srcMemPath := filepath .Join (p .InstanceSnapshotLatest (sourceID ), templateSharedMemFileName )
627637 forkInfo , err := os .Stat (forkMemPath )
628- require .NoError (t , err , "fork mem-file should exist at snapshot-latest /memory" )
638+ require .NoError (t , err , "fork mem-file should exist at snapshot-base /memory after restore " )
629639 assert .True (t , forkInfo .Mode ().IsRegular (), "fork mem-file should be a regular file (hardlink), not a symlink" )
630640 srcInfo , err := os .Stat (srcMemPath )
631641 require .NoError (t , err )
@@ -634,11 +644,13 @@ func TestFirecrackerForkFromTemplate(t *testing.T) {
634644 assert .Equal (t , srcSys .Ino , forkSys .Ino , "fork mem-file should share the source's inode (hardlink, not copy)" )
635645 assert .Equal (t , srcSys .Dev , forkSys .Dev , "fork mem-file should be on the same filesystem as source" )
636646
637- // (c) The source instance is now a Template with ForkCount=1 .
647+ // (c) The source instance is a Template with exactly one live fork .
638648 sourceMeta , err := mgr .loadMetadata (sourceID )
639649 require .NoError (t , err )
640- assert .True (t , sourceMeta .StoredMetadata .IsTemplate , "source should be promoted to Template on first fork" )
641- assert .Equal (t , 1 , sourceMeta .StoredMetadata .ForkCount , "template fork refcount should be 1 after one fork" )
650+ assert .True (t , sourceMeta .StoredMetadata .IsTemplate , "source should be a Template" )
651+ forks , err := mgr .countTemplateForks (sourceID )
652+ require .NoError (t , err )
653+ assert .Equal (t , 1 , forks , "template fork count should be 1 after one fork" )
642654
643655 // (d) The per-template uffd page server should be tracking this fork.
644656 require .NotNil (t , mgr .uffd )
@@ -648,8 +660,8 @@ func TestFirecrackerForkFromTemplate(t *testing.T) {
648660 require .NoError (t , mgr .DeleteInstance (ctx , forkID ))
649661 deletedFork = true
650662
651- sourceMetaAfter , err := mgr .loadMetadata (sourceID )
663+ forksAfter , err := mgr .countTemplateForks (sourceID )
652664 require .NoError (t , err )
653- assert .Equal (t , 0 , sourceMetaAfter . StoredMetadata . ForkCount , "template fork refcount should drop back to 0" )
665+ assert .Equal (t , 0 , forksAfter , "template fork count should drop back to 0" )
654666 assert .False (t , mgr .uffd .hasFork (sourceID , forkID ), "uffd tracker should no longer track the deleted fork" )
655667}
0 commit comments