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])
}
})
}
}