package api import ( "testing" "github.com/alexei/tinyforge/internal/store" ) // TestChildChainNode_MarksPreviewChildren verifies the /chain DTO builder // distinguishes branch-preview children (materialized by the preview package) // from operator-created stage children that merely share the parent link. // The discriminator is preview.IsPreviewChild, which reverses the // MaterializeForBranch naming formula: name == template.Name + "/" + slug. func TestChildChainNode_MarksPreviewChildren(t *testing.T) { template := store.Workload{ ID: "tmpl-1", Name: "myapp", SourceKind: "dockerfile", } tests := []struct { name string child store.Workload wantPrev bool wantBranch string }{ { name: "preview child is marked with its branch", child: store.Workload{ ID: "child-prev", Name: "myapp/feat-login", SourceKind: "dockerfile", SourceConfig: `{"branch":"feat/login","port":3000}`, ParentWorkloadID: "tmpl-1", }, wantPrev: true, wantBranch: "feat/login", }, { name: "operator-named stage child sharing the parent is not a preview", child: store.Workload{ ID: "child-stage", Name: "myapp-staging", SourceKind: "dockerfile", SourceConfig: `{"branch":"main"}`, ParentWorkloadID: "tmpl-1", }, wantPrev: false, wantBranch: "", }, { name: "child of a different parent is not a preview of self", child: store.Workload{ ID: "child-other", Name: "myapp/feat-login", SourceKind: "dockerfile", SourceConfig: `{"branch":"feat/login"}`, ParentWorkloadID: "some-other-template", }, wantPrev: false, wantBranch: "", }, { name: "child with no branch in source_config is not a preview", child: store.Workload{ ID: "child-nobranch", Name: "myapp/feat-login", SourceKind: "dockerfile", SourceConfig: `{}`, ParentWorkloadID: "tmpl-1", }, wantPrev: false, wantBranch: "", }, { // Same parent + a valid branch, but the name carries an extra // suffix so it fails ONLY the slug-equality check (expected // "myapp/feat-login", got "myapp/feat-login-staging"). The // branch alone must not be enough to mark a preview. name: "valid branch but name fails the slug match is not a preview", child: store.Workload{ ID: "child-slugmiss", Name: "myapp/feat-login-staging", SourceKind: "dockerfile", SourceConfig: `{"branch":"feat/login","port":3000}`, ParentWorkloadID: "tmpl-1", }, wantPrev: false, wantBranch: "", }, { // Uppercase + slash branch: slugifyBranch lowercases and maps // "/" -> "-", so "Feature/Login" -> "feature-login" and the name // "myapp/feature-login" matches. PreviewBranch must echo the RAW // branch from source_config ("Feature/Login"), not the slug. name: "uppercase slash branch matches and keeps raw branch", child: store.Workload{ ID: "child-upper", Name: "myapp/feature-login", SourceKind: "dockerfile", SourceConfig: `{"branch":"Feature/Login","port":8080}`, ParentWorkloadID: "tmpl-1", }, wantPrev: true, wantBranch: "Feature/Login", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { node := childChainNode(template, tc.child) if node.IsPreview != tc.wantPrev { t.Errorf("IsPreview = %v, want %v", node.IsPreview, tc.wantPrev) } if node.PreviewBranch != tc.wantBranch { t.Errorf("PreviewBranch = %q, want %q", node.PreviewBranch, tc.wantBranch) } // Base fields must always round-trip regardless of preview status. if node.ID != tc.child.ID || node.Name != tc.child.Name { t.Errorf("base fields mangled: got id=%q name=%q", node.ID, node.Name) } }) } } // TestPreviewBranchOf_ToleratesMalformedConfig confirms the branch extractor // returns "" rather than panicking on a missing or invalid source_config. func TestPreviewBranchOf_ToleratesMalformedConfig(t *testing.T) { cases := []struct { name string cfg string want string }{ {"valid branch", `{"branch":"release/v1"}`, "release/v1"}, {"empty config", ``, ""}, {"empty object", `{}`, ""}, {"malformed json", `{not-json`, ""}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := previewBranchOf(store.Workload{SourceConfig: c.cfg}) if got != c.want { t.Errorf("previewBranchOf(%q) = %q, want %q", c.cfg, got, c.want) } }) } }