package models import ( "strings" "testing" "tangled.org/core/workflow" ) func sp(s string) *string { return &s } func TestBuildCloneStep_PushTrigger(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 1, Submodules: false, Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: &workflow.PushTriggerData{ NewSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", OldSha: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Ref: "refs/heads/main", }, Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) if step.Kind() != StepKindSystem { t.Errorf("Expected StepKindSystem, got %v", step.Kind()) } if step.Name() != "Clone repository into workspace" { t.Errorf("Expected 'Clone repository into workspace', got '%s'", step.Name()) } commands := step.Commands() if len(commands) != 4 { t.Errorf("Expected 4 commands, got %d", len(commands)) } // Verify commands contain expected git operations allCmds := strings.Join(commands, " ") if !strings.Contains(allCmds, "git init --object-format=sha1") { t.Error("Commands should contain 'git init'") } if !strings.Contains(allCmds, "git remote add origin") { t.Error("Commands should contain 'git remote add origin'") } if !strings.Contains(allCmds, "git fetch") { t.Error("Commands should contain 'git fetch'") } if !strings.Contains(allCmds, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") { t.Error("Commands should contain commit SHA") } if !strings.Contains(allCmds, "git checkout FETCH_HEAD") { t.Error("Commands should contain 'git checkout FETCH_HEAD'") } if !strings.Contains(allCmds, "https://example.com/did:plc:boltless") { t.Error("Commands should contain expected repo URL") } } func TestBuildCloneStep_PullRequestTrigger(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 1, Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPullRequest), PullRequest: &workflow.PullRequestTriggerData{ SourceSha: "cccccccccccccccccccccccccccccccccccccccc", SourceBranch: "feature-branch", TargetBranch: "main", }, Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "cccccccccccccccccccccccccccccccccccccccc") { t.Error("Commands should contain PR commit SHA") } } func TestBuildCloneStep_SourceRepo(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 1, Skip: false, }, } sourceRepoDid := "did:plc:fork" tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPullRequest), PullRequest: &workflow.PullRequestTriggerData{ SourceSha: "cccccccccccccccccccccccccccccccccccccccc", SourceBranch: "feature-branch", TargetBranch: "main", }, Repo: &workflow.TriggerRepo{ Knot: "fork.example.com", Did: "did:plc:user456", Repo: sp("fork-repo"), RepoDid: &sourceRepoDid, }, SourceRepo: &sourceRepoDid, } step := BuildCloneStep(twf, tr, false) allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "https://fork.example.com/did:plc:fork") { t.Error("Commands should clone from source repo URL") } if strings.Contains(allCmds, "https://target.example.com/did:plc:target") { t.Error("Commands should not clone from target repo URL when sourceRepo is set") } } func TestBuildCloneStep_ManualTrigger(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 1, Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindManual), Manual: &workflow.ManualTriggerData{ Sha: "dddddddddddddddddddddddddddddddddddddddd", Inputs: nil, }, Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) allCmds := strings.Join(step.Commands(), " ") // Should still have basic git commands if !strings.Contains(allCmds, "git init") { t.Error("Commands should contain 'git init'") } if !strings.Contains(allCmds, "git fetch") { t.Error("Commands should contain 'git fetch'") } // Manual triggers now carry an explicit SHA, which the fetch targets if !strings.Contains(allCmds, "dddddddddddddddddddddddddddddddddddddddd") { t.Error("Commands should contain the manual trigger SHA") } } func TestBuildCloneStep_SkipFlag(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Skip: true, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: &workflow.PushTriggerData{ NewSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) // Empty step when skip is true if step.Name() != "" { t.Error("Expected empty step name when Skip is true") } if len(step.Commands()) != 0 { t.Errorf("Expected no commands when Skip is true, got %d commands", len(step.Commands())) } } func TestBuildCloneStep_DevMode(t *testing.T) { twf := workflow.WorkflowSpec{ Engine: "nixery", Clone: &workflow.CloneSpec{ Depth: 1, Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: &workflow.PushTriggerData{ NewSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, Repo: &workflow.TriggerRepo{ Knot: "knot.tngl.boltless.dev", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, true) // In dev mode, sslVerify should be disabled allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "git -c http.sslVerify=false fetch") { t.Error("Expected sslVerify to be disabled in dev mode clone commands") } } func TestBuildCloneStep_DepthAndSubmodules(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 10, Submodules: true, Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: &workflow.PushTriggerData{ NewSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "--depth=10") { t.Error("Commands should contain '--depth=10'") } if !strings.Contains(allCmds, "--recurse-submodules=yes") { t.Error("Commands should contain '--recurse-submodules=yes'") } } func TestBuildCloneStep_DefaultDepth(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 0, // Default should be 1 Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: &workflow.PushTriggerData{ NewSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "--depth=1") { t.Error("Commands should default to '--depth=1'") } } func TestBuildCloneStep_NilPushData(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 1, Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: nil, // Nil push data should create error step Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) // Should return an error step if !strings.Contains(step.Name(), "error") { t.Error("Expected error in step name when push data is nil") } allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "Failed to get clone info") { t.Error("Commands should contain error message") } if !strings.Contains(allCmds, "exit 1") { t.Error("Commands should exit with error") } } func TestBuildCloneStep_NilPRData(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 1, Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPullRequest), PullRequest: nil, // Nil PR data should create error step Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) // Should return an error step if !strings.Contains(step.Name(), "error") { t.Error("Expected error in step name when pull request data is nil") } allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "Failed to get clone info") { t.Error("Commands should contain error message") } } func TestBuildCloneStep_UnknownTriggerKind(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: &workflow.CloneSpec{ Depth: 1, Skip: false, }, } tr := workflow.TriggerMetadata{ Kind: "unknown_trigger", Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) // Should return an error step if !strings.Contains(step.Name(), "error") { t.Error("Expected error in step name for unknown trigger kind") } allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "unknown trigger kind") { t.Error("Commands should contain error message about unknown trigger kind") } } func TestBuildCloneStep_NilCloneOpts(t *testing.T) { twf := workflow.WorkflowSpec{ Clone: nil, // Nil clone options should use defaults } tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: &workflow.PushTriggerData{ NewSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", Repo: sp("my-repo"), RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(twf, tr, false) // Should still work with default options if step.Kind() != StepKindSystem { t.Errorf("Expected StepKindSystem, got %v", step.Kind()) } allCmds := strings.Join(step.Commands(), " ") if !strings.Contains(allCmds, "--depth=1") { t.Error("Commands should default to '--depth=1' when Clone is nil") } if !strings.Contains(allCmds, "git init") { t.Error("Commands should contain 'git init'") } } func TestBuildCloneStep_ScheduleTrigger(t *testing.T) { tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindSchedule), Schedule: &workflow.ScheduleTriggerData{ Sha: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", Ref: "refs/heads/main", ScheduledAt: "2026-08-10T09:00:00Z", }, Repo: &workflow.TriggerRepo{ Knot: "example.com", Did: "did:plc:user123", RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(workflow.WorkflowSpec{}, tr, false) if commands := strings.Join(step.Commands(), " "); !strings.Contains(commands, "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") { t.Fatalf("clone commands do not target scheduled SHA: %s", commands) } } func TestBuildCloneStep_ObjectFormat(t *testing.T) { for _, tt := range []struct { name string sha string format string }{ {"sha1", strings.Repeat("a", 40), "sha1"}, {"sha256", strings.Repeat("b", 64), "sha256"}, {"empty", "", ""}, {"abbreviated", "abc123", ""}, {"short_sha1", strings.Repeat("a", 39), ""}, {"long_sha1", strings.Repeat("a", 41), ""}, {"short_sha256", strings.Repeat("b", 63), ""}, {"long_sha256", strings.Repeat("b", 65), ""}, } { t.Run(tt.name, func(t *testing.T) { tr := workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Push: &workflow.PushTriggerData{NewSha: tt.sha}, Repo: &workflow.TriggerRepo{ Knot: "example.com", RepoDid: sp("did:plc:boltless"), }, } step := BuildCloneStep(workflow.WorkflowSpec{}, tr, false) if step.Kind() != StepKindSystem { t.Fatalf("expected system step, got %v", step.Kind()) } commands := step.Commands() if tt.format == "" { if step.Name() != "Clone repository into workspace (error)" { t.Errorf("expected error step, got %q", step.Name()) } if len(commands) != 1 || commands[0] != "echo 'Failed to determine repo object format' && exit 1" { t.Fatalf("expected only an error command, got %q", commands) } return } if step.Name() != "Clone repository into workspace" { t.Errorf("unexpected step name: %q", step.Name()) } if len(commands) != 4 { t.Fatalf("expected four checkout commands, got %q", commands) } if want := "git init --object-format=" + tt.format; commands[0] != want { t.Errorf("init command = %q, want %q", commands[0], want) } if !strings.HasSuffix(commands[2], "origin "+tt.sha) { t.Errorf("fetch does not target full commit ID: %q", commands[2]) } }) } }