@@ -462,3 +462,168 @@ func TestIntegration_InspectModel(t *testing.T) {
462462 require .NoError (t , err )
463463 require .Empty (t , strings .TrimSpace (models ), "Model should be removed" )
464464}
465+
466+ // TestIntegration_TagModel tests tagging a model with various source and target reference formats
467+ // to ensure proper reference normalization and tag creation.
468+ func TestIntegration_TagModel (t * testing.T ) {
469+ env := setupTestEnv (t )
470+
471+ // Ensure no models exist initially
472+ models , err := listModels (false , env .client , true , false , "" )
473+ require .NoError (t , err )
474+ if len (models ) != 0 {
475+ t .Fatal ("Expected no initial models, but found some" )
476+ }
477+
478+ // Create and push a test model with default org (ai/tag-test:latest)
479+ modelRef := "ai/tag-test:latest"
480+ modelID , hostFQDN , networkFQDN , digest := createAndPushTestModel (t , env .registryURL , modelRef , 2048 )
481+ t .Logf ("Test model pushed: %s (ID: %s) FQDN: %s Digest: %s" , hostFQDN , modelID , networkFQDN , digest )
482+
483+ // Pull the model using a simple reference
484+ pullRef := "tag-test"
485+ t .Logf ("Pulling model with reference: %s" , pullRef )
486+ err = pullModel (newPullCmd (), env .client , pullRef , true )
487+ require .NoError (t , err , "Failed to pull model" )
488+
489+ // Verify the model was pulled
490+ models , err = listModels (false , env .client , true , false , "" )
491+ require .NoError (t , err )
492+ truncatedID := modelID [7 :19 ]
493+ require .Equal (t , truncatedID , strings .TrimSpace (models ), "Model not found after pull" )
494+
495+ // Generate all possible source references using the unified system
496+ sourceInfo := modelInfo {
497+ name : "tag-test" ,
498+ org : "ai" ,
499+ tag : "latest" ,
500+ registry : "registry.local:5000" ,
501+ modelID : modelID ,
502+ digest : digest ,
503+ expectedName : "ai/tag-test:v1" ,
504+ }
505+ sourceRefs := generateReferenceTestCases (sourceInfo )
506+
507+ // Define target reference formats to test (including explicit registry references)
508+ targetFormats := []struct {
509+ name string
510+ target string
511+ }{
512+ {name : "simple name" , target : "tag-test" },
513+ {name : "simple name with tag" , target : "target-tag-test:v2" },
514+ {name : "with org" , target : "ai/target-tag-test:latest" },
515+ {name : "different org" , target : "test/target-tag-test:v1" },
516+ {name : "custom org" , target : "custom/target-tag-test:cross-org" },
517+ {name : "explicit registry with org" , target : "registry.local:5000/ai/target-tag-test:explicit" },
518+ {name : "explicit registry different org" , target : "registry.local:5000/target-test/tag-test:fqdn" },
519+ {name : "explicit registry custom org" , target : "registry.local:5000/custom/target-tag-test:with-reg" },
520+ {name : "explicit custom registry custom org" , target : "other-registry.local:5000/custom/target-tag-test:with-reg" },
521+ }
522+
523+ // Build test cases by combining source references with target formats
524+ type tagTestCase struct {
525+ name string
526+ sourceRef string
527+ targetRef string
528+ }
529+
530+ var testCases []tagTestCase
531+
532+ // Test all combinations of source references and target formats
533+ for _ , srcCase := range sourceRefs {
534+
535+ if strings .Contains (srcCase .name , "model ID" ) {
536+ // Skip ID-based references for tagging tests
537+ // TODO : Support tagging by ID in the future
538+ continue
539+ }
540+
541+ // Nested loop - test this source with ALL targets
542+ for _ , targetFormat := range targetFormats {
543+ testCases = append (testCases , tagTestCase {
544+ name : fmt .Sprintf ("source: %s -> target: %s" , srcCase .name , targetFormat .name ),
545+ sourceRef : srcCase .ref ,
546+ targetRef : targetFormat .target ,
547+ })
548+ }
549+ }
550+
551+ // Track all created tags for verification
552+ createdTags := make (map [string ]bool )
553+
554+ for _ , tc := range testCases {
555+ t .Run (tc .name , func (t * testing.T ) {
556+ t .Logf ("Tagging %s as %s" , tc .sourceRef , tc .targetRef )
557+
558+ // Perform the tag operation
559+ err := tagModel (newTagCmd (), env .client , tc .sourceRef , tc .targetRef )
560+ require .NoError (t , err , "Failed to tag model with source=%s target=%s" , tc .sourceRef , tc .targetRef )
561+
562+ // Track this tag
563+ createdTags [tc .targetRef ] = true
564+
565+ // Verify the new tag exists and points to the correct model
566+ taggedModel , err := env .client .Inspect (tc .targetRef , false )
567+ require .NoError (t , err , "Failed to inspect newly tagged model with reference: %s" , tc .targetRef )
568+
569+ // Verify the model ID matches the original
570+ require .Equal (t , modelID , taggedModel .ID ,
571+ "Tagged model ID mismatch. Expected: %s, Got: %s" , modelID , taggedModel .ID )
572+
573+ // Verify the digest matches
574+ require .Equal (t , digest , taggedModel .ID ,
575+ "Tagged model digest mismatch. Expected: %s, Got: %s" , digest , taggedModel .ID )
576+
577+ t .Logf ("✓ Successfully created tag %s pointing to model %s" , tc .targetRef , truncatedID )
578+
579+ // Verify the original source tag/reference still exists
580+ originalModel , err := env .client .Inspect (tc .sourceRef , false )
581+ require .NoError (t , err , "Original source reference should still be valid: %s" , tc .sourceRef )
582+ require .Equal (t , modelID , originalModel .ID , "Original source should point to same model" )
583+ t .Logf ("✓ Verified original source %s still exists" , tc .sourceRef )
584+ })
585+ }
586+
587+ // Final verification: List the model and verify all tags are present
588+ t .Run ("verify all tags in model inspect" , func (t * testing.T ) {
589+ inspectedModel , err := env .client .Inspect (modelID , false )
590+ require .NoError (t , err , "Failed to inspect model by ID" )
591+
592+ t .Logf ("Model has %d tags: %v" , len (inspectedModel .Tags ), inspectedModel .Tags )
593+
594+ // The model should have at least the original tag plus all created tags
595+ require .GreaterOrEqual (t , len (inspectedModel .Tags ), len (createdTags )+ 1 ,
596+ "Model should have at least %d tags (original + created)" , len (createdTags )+ 1 )
597+
598+ // Verify each created tag is in the model's tag list
599+ for expectedTag := range createdTags {
600+ found := false
601+ for _ , actualTag := range inspectedModel .Tags {
602+ if actualTag == expectedTag || actualTag == fmt .Sprintf ("%s:latest" , expectedTag ) { // Handle implicit latest tag
603+ found = true
604+ break
605+ }
606+ }
607+ require .True (t , found , "Expected tag %s not found in model's tag list" , expectedTag )
608+ }
609+
610+ t .Logf ("✓ All %d created tags verified in model's tag list" , len (createdTags ))
611+ })
612+
613+ // Test error case: tagging non-existent model
614+ t .Run ("error on non-existent model" , func (t * testing.T ) {
615+ err := tagModel (newTagCmd (), env .client , "non-existent-model:v1" , "ai/should-fail:latest" )
616+ require .Error (t , err , "Should fail when tagging non-existent model" )
617+ t .Logf ("✓ Correctly failed to tag non-existent model: %v" , err )
618+ })
619+
620+ // Cleanup: remove the model
621+ t .Logf ("Removing model %s" , truncatedID )
622+ err = removeModel (env .client , modelID )
623+ require .NoError (t , err , "Failed to remove model" )
624+
625+ // Verify model was removed
626+ models , err = listModels (false , env .client , true , false , "" )
627+ require .NoError (t , err )
628+ require .Empty (t , strings .TrimSpace (models ), "Model should be removed" )
629+ }
0 commit comments