A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

have Holds post with new og card

+196 -264
+1
pkg/hold/oci/xrpc.go
··· 317 317 318 318 postURI, err = h.pds.CreateManifestPost( 319 319 ctx, 320 + h.driver, 320 321 req.Repository, 321 322 req.Tag, 322 323 req.UserHandle,
+86 -79
pkg/hold/pds/manifest_post.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "io" 6 7 "log/slog" 8 + "net/http" 7 9 "strings" 8 10 "time" 9 11 10 12 bsky "github.com/bluesky-social/indigo/api/bsky" 13 + "github.com/distribution/distribution/v3/registry/storage/driver" 11 14 ) 12 15 13 16 // CreateManifestPost creates a Bluesky post announcing a manifest upload 14 - // Includes facets for clickable mentions and links 15 - // For multi-arch images (platforms non-empty), shows platforms instead of size 17 + // Includes mention facet for the user and an OG card embed with thumbnail 16 18 func (p *HoldPDS) CreateManifestPost( 17 19 ctx context.Context, 20 + storageDriver driver.StorageDriver, 18 21 repository, tag, userHandle, userDID, digest string, 19 22 totalSize int64, 20 23 platforms []string, ··· 24 27 // Build AppView repository URL 25 28 appViewURL := fmt.Sprintf("https://atcr.io/r/%s/%s", userHandle, repository) 26 29 27 - // Format post text components 28 - digestShort := formatDigest(digest) 30 + // Build simplified text with mention - OG card handles the link 29 31 repoWithTag := fmt.Sprintf("%s:%s", repository, tag) 32 + text := fmt.Sprintf("@%s pushed %s", userHandle, repoWithTag) 30 33 31 - // Build text based on whether this is multi-arch or single-arch 32 - var text string 33 - if len(platforms) > 0 { 34 - // Multi-arch: show platforms 35 - platformsStr := strings.Join(platforms, ", ") 36 - text = fmt.Sprintf("@%s just pushed %s\nDigest: %s Platforms: %s", userHandle, repoWithTag, digestShort, platformsStr) 34 + // Only build mention facet - the OG card embed provides the link 35 + facets := buildMentionFacet(text, userHandle, userDID) 36 + 37 + // Build embed with OG card 38 + var embed *bsky.FeedPost_Embed 39 + 40 + ogImageData, err := fetchOGImage(ctx, userHandle, repository) 41 + if err != nil { 42 + slog.Warn("Failed to fetch OG image, posting without embed", "error", err) 37 43 } else { 38 - // Single-arch: show size 39 - sizeStr := formatSize(totalSize) 40 - text = fmt.Sprintf("@%s just pushed %s\nDigest: %s Size: %s", userHandle, repoWithTag, digestShort, sizeStr) 44 + // Upload OG image as blob 45 + thumbBlob, err := uploadBlobToStorage(ctx, storageDriver, p.did, ogImageData, "image/png") 46 + if err != nil { 47 + slog.Warn("Failed to upload OG image blob", "error", err) 48 + } else { 49 + // Build dynamic description 50 + var description string 51 + if len(platforms) > 0 { 52 + description = fmt.Sprintf("Multi-arch: %s", strings.Join(platforms, ", ")) 53 + } else { 54 + description = fmt.Sprintf("Pushed %s to ATCR", formatSize(totalSize)) 55 + } 56 + 57 + embed = &bsky.FeedPost_Embed{ 58 + EmbedExternal: &bsky.EmbedExternal{ 59 + LexiconTypeID: "app.bsky.embed.external", 60 + External: &bsky.EmbedExternal_External{ 61 + Uri: appViewURL, 62 + Title: fmt.Sprintf("%s/%s:%s", userHandle, repository, tag), 63 + Description: description, 64 + Thumb: thumbBlob, 65 + }, 66 + }, 67 + } 68 + } 41 69 } 42 70 43 - // Create facets for mentions and links 44 - facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 45 - 46 - // Create post struct with facets 71 + // Create post struct with facets and embed 47 72 post := &bsky.FeedPost{ 48 73 LexiconTypeID: "app.bsky.feed.post", 49 74 Text: text, 50 75 Facets: facets, 76 + Embed: embed, 51 77 CreatedAt: now.Format(time.RFC3339), 78 + Langs: []string{"en"}, 52 79 } 53 80 54 81 // Create record with auto-generated TID ··· 73 100 return postURI, nil 74 101 } 75 102 76 - // formatDigest truncates digest to first 10 chars 77 - // Example: sha256:abc1234567890fedcba9876543210 -> sha256:abc1234567... 78 - func formatDigest(digest string) string { 79 - if !strings.HasPrefix(digest, "sha256:") { 80 - return digest // Return as-is if not sha256 103 + // fetchOGImage downloads the OG card image from AppView 104 + func fetchOGImage(ctx context.Context, userHandle, repository string) ([]byte, error) { 105 + url := fmt.Sprintf("https://atcr.io/og/r/%s/%s", userHandle, repository) 106 + 107 + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 108 + if err != nil { 109 + return nil, err 110 + } 111 + 112 + client := &http.Client{Timeout: 10 * time.Second} 113 + resp, err := client.Do(req) 114 + if err != nil { 115 + return nil, err 116 + } 117 + defer resp.Body.Close() 118 + 119 + if resp.StatusCode != http.StatusOK { 120 + return nil, fmt.Errorf("OG image fetch failed: %d", resp.StatusCode) 81 121 } 82 122 83 - hash := strings.TrimPrefix(digest, "sha256:") 84 - if len(hash) <= 10 { 85 - return digest // Too short to truncate 123 + return io.ReadAll(resp.Body) 124 + } 125 + 126 + // buildMentionFacet creates a mention facet for the user handle 127 + // IMPORTANT: Byte offsets must be calculated for UTF-8 encoded text 128 + func buildMentionFacet(text, userHandle, userDID string) []*bsky.RichtextFacet { 129 + mentionText := "@" + userHandle 130 + mentionStart := strings.Index(text, mentionText) 131 + if mentionStart < 0 { 132 + return nil 86 133 } 87 134 88 - return fmt.Sprintf("sha256:%s...", hash[:10]) 135 + byteStart := int64(len(text[:mentionStart])) 136 + byteEnd := int64(len(text[:mentionStart+len(mentionText)])) 137 + 138 + return []*bsky.RichtextFacet{{ 139 + Index: &bsky.RichtextFacet_ByteSlice{ 140 + ByteStart: byteStart, 141 + ByteEnd: byteEnd, 142 + }, 143 + Features: []*bsky.RichtextFacet_Features_Elem{{ 144 + RichtextFacet_Mention: &bsky.RichtextFacet_Mention{ 145 + Did: userDID, 146 + }, 147 + }}, 148 + }} 89 149 } 90 150 91 151 // formatSize converts bytes to human-readable format ··· 108 168 return fmt.Sprintf("%d B", bytes) 109 169 } 110 170 } 111 - 112 - // buildFacets creates mention and link facets for rich text 113 - // IMPORTANT: Byte offsets must be calculated for UTF-8 encoded text 114 - func buildFacets(text, userHandle, userDID, repoWithTag, appViewURL string) []*bsky.RichtextFacet { 115 - facets := []*bsky.RichtextFacet{} 116 - 117 - // Find mention: "@alice.bsky.social" 118 - mentionText := "@" + userHandle 119 - mentionStart := strings.Index(text, mentionText) 120 - if mentionStart >= 0 { 121 - // Calculate byte offsets (not character offsets!) 122 - byteStart := int64(len(text[:mentionStart])) 123 - byteEnd := int64(len(text[:mentionStart+len(mentionText)])) 124 - 125 - facets = append(facets, &bsky.RichtextFacet{ 126 - Index: &bsky.RichtextFacet_ByteSlice{ 127 - ByteStart: byteStart, 128 - ByteEnd: byteEnd, 129 - }, 130 - Features: []*bsky.RichtextFacet_Features_Elem{ 131 - { 132 - RichtextFacet_Mention: &bsky.RichtextFacet_Mention{ 133 - Did: userDID, 134 - }, 135 - }, 136 - }, 137 - }) 138 - } 139 - 140 - // Find repository link: "hsm-secrets-operator:latest" 141 - linkStart := strings.Index(text, repoWithTag) 142 - if linkStart >= 0 { 143 - // Calculate byte offsets 144 - byteStart := int64(len(text[:linkStart])) 145 - byteEnd := int64(len(text[:linkStart+len(repoWithTag)])) 146 - 147 - facets = append(facets, &bsky.RichtextFacet{ 148 - Index: &bsky.RichtextFacet_ByteSlice{ 149 - ByteStart: byteStart, 150 - ByteEnd: byteEnd, 151 - }, 152 - Features: []*bsky.RichtextFacet_Features_Elem{ 153 - { 154 - RichtextFacet_Link: &bsky.RichtextFacet_Link{ 155 - Uri: appViewURL, 156 - }, 157 - }, 158 - }, 159 - }) 160 - } 161 - 162 - return facets 163 - }
+109 -185
pkg/hold/pds/manifest_post_test.go
··· 7 7 bsky "github.com/bluesky-social/indigo/api/bsky" 8 8 ) 9 9 10 - func TestFormatDigest(t *testing.T) { 11 - tests := []struct { 12 - name string 13 - digest string 14 - expected string 15 - }{ 16 - { 17 - name: "standard sha256 digest", 18 - digest: "sha256:abc1234567890fedcba9876543210", 19 - expected: "sha256:abc1234567...", // First 10 chars 20 - }, 21 - { 22 - name: "short digest (no truncation)", 23 - digest: "sha256:abc123", 24 - expected: "sha256:abc123", 25 - }, 26 - { 27 - name: "non-sha256 digest", 28 - digest: "sha512:abc123", 29 - expected: "sha512:abc123", 30 - }, 31 - { 32 - name: "real sha256 digest", 33 - digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", 34 - expected: "sha256:e692418e4c...", // First 10 chars 35 - }, 36 - } 37 - 38 - for _, tt := range tests { 39 - t.Run(tt.name, func(t *testing.T) { 40 - result := formatDigest(tt.digest) 41 - if result != tt.expected { 42 - t.Errorf("formatDigest(%q) = %q, want %q", tt.digest, result, tt.expected) 43 - } 44 - }) 45 - } 46 - } 47 - 48 10 func TestFormatSize(t *testing.T) { 49 11 tests := []struct { 50 12 name string ··· 103 65 } 104 66 } 105 67 106 - func TestBuildFacets(t *testing.T) { 68 + func TestBuildMentionFacet(t *testing.T) { 107 69 tests := []struct { 108 - name string 109 - text string 110 - userHandle string 111 - userDID string 112 - repoWithTag string 113 - appViewURL string 114 - wantFacets int // number of facets expected 70 + name string 71 + text string 72 + userHandle string 73 + userDID string 74 + wantFacets int // number of facets expected 115 75 }{ 116 76 { 117 - name: "standard post with mention and link", 118 - text: "@alice.bsky.social just pushed myapp:latest\nDigest: sha256:abc...def Size: 12.2 MB", 119 - userHandle: "alice.bsky.social", 120 - userDID: "did:plc:alice123", 121 - repoWithTag: "myapp:latest", 122 - appViewURL: "https://atcr.io/r/alice.bsky.social/myapp", 123 - wantFacets: 2, 77 + name: "standard post with mention", 78 + text: "@alice.bsky.social pushed myapp:latest", 79 + userHandle: "alice.bsky.social", 80 + userDID: "did:plc:alice123", 81 + wantFacets: 1, 124 82 }, 125 83 { 126 - name: "no matches found", 127 - text: "random text", 128 - userHandle: "alice.bsky.social", 129 - userDID: "did:plc:alice123", 130 - repoWithTag: "myapp:latest", 131 - appViewURL: "https://atcr.io/r/alice.bsky.social/myapp", 132 - wantFacets: 0, 84 + name: "no mention found", 85 + text: "random text", 86 + userHandle: "alice.bsky.social", 87 + userDID: "did:plc:alice123", 88 + wantFacets: 0, 133 89 }, 134 90 { 135 - name: "only mention found", 136 - text: "@alice.bsky.social did something", 137 - userHandle: "alice.bsky.social", 138 - userDID: "did:plc:alice123", 139 - repoWithTag: "myapp:latest", 140 - appViewURL: "https://atcr.io/r/alice.bsky.social/myapp", 141 - wantFacets: 1, 91 + name: "mention at start", 92 + text: "@alice.bsky.social did something", 93 + userHandle: "alice.bsky.social", 94 + userDID: "did:plc:alice123", 95 + wantFacets: 1, 142 96 }, 143 97 } 144 98 145 99 for _, tt := range tests { 146 100 t.Run(tt.name, func(t *testing.T) { 147 - facets := buildFacets(tt.text, tt.userHandle, tt.userDID, tt.repoWithTag, tt.appViewURL) 101 + facets := buildMentionFacet(tt.text, tt.userHandle, tt.userDID) 148 102 149 103 if len(facets) != tt.wantFacets { 150 - t.Errorf("buildFacets() returned %d facets, want %d", len(facets), tt.wantFacets) 104 + t.Errorf("buildMentionFacet() returned %d facets, want %d", len(facets), tt.wantFacets) 151 105 } 152 106 153 107 // Verify facet structure for standard case 154 - if tt.name == "standard post with mention and link" && len(facets) == 2 { 155 - // Check mention facet 108 + if tt.wantFacets > 0 && len(facets) > 0 { 156 109 mentionFacet := facets[0] 157 110 if mentionFacet.Index == nil { 158 111 t.Error("mention facet has nil Index") ··· 163 116 if mentionFacet.Features[0].RichtextFacet_Mention == nil { 164 117 t.Error("mention facet feature is not a mention") 165 118 } 166 - 167 - // Check link facet 168 - linkFacet := facets[1] 169 - if linkFacet.Index == nil { 170 - t.Error("link facet has nil Index") 171 - } 172 - if len(linkFacet.Features) != 1 { 173 - t.Errorf("link facet has %d features, want 1", len(linkFacet.Features)) 174 - } 175 - if linkFacet.Features[0].RichtextFacet_Link == nil { 176 - t.Error("link facet feature is not a link") 177 - } 178 - if linkFacet.Features[0].RichtextFacet_Link.Uri != tt.appViewURL { 179 - t.Errorf("link facet URI = %q, want %q", linkFacet.Features[0].RichtextFacet_Link.Uri, tt.appViewURL) 119 + if mentionFacet.Features[0].RichtextFacet_Mention.Did != tt.userDID { 120 + t.Errorf("mention DID = %q, want %q", mentionFacet.Features[0].RichtextFacet_Mention.Did, tt.userDID) 180 121 } 181 122 } 182 123 }) 183 124 } 184 125 } 185 126 186 - func TestBuildFacets_ByteOffsets(t *testing.T) { 127 + func TestBuildMentionFacet_ByteOffsets(t *testing.T) { 187 128 // Test that byte offsets are correctly calculated 188 - text := "@alice.bsky.social just pushed myapp:latest" 129 + text := "@alice.bsky.social pushed myapp:latest" 189 130 userHandle := "alice.bsky.social" 190 131 userDID := "did:plc:alice123" 191 - repoWithTag := "myapp:latest" 192 - appViewURL := "https://atcr.io/r/alice.bsky.social/myapp" 193 132 194 - facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 133 + facets := buildMentionFacet(text, userHandle, userDID) 195 134 196 - if len(facets) != 2 { 197 - t.Fatalf("expected 2 facets, got %d", len(facets)) 135 + if len(facets) != 1 { 136 + t.Fatalf("expected 1 facet, got %d", len(facets)) 198 137 } 199 138 200 139 // Check mention facet byte offsets ··· 215 154 if extractedMention != mentionText { 216 155 t.Errorf("extracted mention = %q, want %q", extractedMention, mentionText) 217 156 } 218 - 219 - // Check link facet byte offsets 220 - linkFacet := facets[1] 221 - linkStart := len("@alice.bsky.social just pushed ") 222 - expectedLinkStart := int64(linkStart) 223 - expectedLinkEnd := int64(linkStart + len(repoWithTag)) 224 - 225 - if linkFacet.Index.ByteStart != expectedLinkStart { 226 - t.Errorf("link ByteStart = %d, want %d", linkFacet.Index.ByteStart, expectedLinkStart) 227 - } 228 - if linkFacet.Index.ByteEnd != expectedLinkEnd { 229 - t.Errorf("link ByteEnd = %d, want %d", linkFacet.Index.ByteEnd, expectedLinkEnd) 230 - } 231 - 232 - // Verify the link text extraction 233 - extractedLink := text[linkFacet.Index.ByteStart:linkFacet.Index.ByteEnd] 234 - if extractedLink != repoWithTag { 235 - t.Errorf("extracted link = %q, want %q", extractedLink, repoWithTag) 236 - } 237 157 } 238 158 239 - func TestBuildFacets_UTF8Handling(t *testing.T) { 159 + func TestBuildMentionFacet_UTF8Handling(t *testing.T) { 240 160 // Test with Unicode characters to ensure byte offsets work correctly 241 - text := "@alice.bsky.social just pushed 🚀myapp:latest" 161 + text := "@alice.bsky.social pushed 🚀myapp:latest" 242 162 userHandle := "alice.bsky.social" 243 163 userDID := "did:plc:alice123" 244 - repoWithTag := "🚀myapp:latest" // Note: emoji is multi-byte 245 - appViewURL := "https://atcr.io/r/alice.bsky.social/myapp" 246 164 247 - facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 165 + facets := buildMentionFacet(text, userHandle, userDID) 248 166 249 - if len(facets) != 2 { 250 - t.Fatalf("expected 2 facets, got %d", len(facets)) 167 + if len(facets) != 1 { 168 + t.Fatalf("expected 1 facet, got %d", len(facets)) 251 169 } 252 170 253 171 // Verify that byte extraction works with UTF-8 ··· 256 174 expectedMention := "@alice.bsky.social" 257 175 if extractedMention != expectedMention { 258 176 t.Errorf("extracted mention = %q, want %q", extractedMention, expectedMention) 259 - } 260 - 261 - linkFacet := facets[1] 262 - extractedLink := text[linkFacet.Index.ByteStart:linkFacet.Index.ByteEnd] 263 - if extractedLink != repoWithTag { 264 - t.Errorf("extracted link = %q, want %q", extractedLink, repoWithTag) 265 - } 266 - } 267 - 268 - func TestBuildFacets_NoOverlap(t *testing.T) { 269 - // Ensure facets don't overlap 270 - text := "@alice.bsky.social just pushed myapp:latest" 271 - userHandle := "alice.bsky.social" 272 - userDID := "did:plc:alice123" 273 - repoWithTag := "myapp:latest" 274 - appViewURL := "https://atcr.io/r/alice.bsky.social/myapp" 275 - 276 - facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 277 - 278 - if len(facets) != 2 { 279 - t.Fatalf("expected 2 facets, got %d", len(facets)) 280 - } 281 - 282 - // Facets should not overlap 283 - facet1 := facets[0] 284 - facet2 := facets[1] 285 - 286 - if facet1.Index.ByteEnd > facet2.Index.ByteStart { 287 - t.Errorf("facets overlap: facet1 ends at %d, facet2 starts at %d", 288 - facet1.Index.ByteEnd, facet2.Index.ByteStart) 289 177 } 290 178 } 291 179 292 - func TestBuildFacets_RealWorldExample(t *testing.T) { 293 - // Test with the actual example from the requirements 180 + func TestSimplifiedPostFormat(t *testing.T) { 181 + // Test the new simplified post format: "@user pushed repo:tag" 294 182 repository := "hsm-secrets-operator" 295 183 tag := "latest" 296 184 userHandle := "evan.jarrett.net" 297 185 userDID := "did:plc:pddp4xt5lgnv2qsegbzzs4xg" 298 - digest := "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" 299 - totalSize := int64(12800000) // ~12.2 MB 300 186 301 187 repoWithTag := repository + ":" + tag 302 - digestShort := formatDigest(digest) 303 - sizeStr := formatSize(totalSize) 188 + text := "@" + userHandle + " pushed " + repoWithTag 304 189 305 - text := "@" + userHandle + " just pushed " + repoWithTag + "\nDigest: " + digestShort + " Size: " + sizeStr 306 - appViewURL := "https://atcr.io/r/" + userHandle + "/" + repository 190 + facets := buildMentionFacet(text, userHandle, userDID) 307 191 308 - facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 309 - 310 - // Should have 2 facets: mention and link 311 - if len(facets) != 2 { 312 - t.Fatalf("expected 2 facets, got %d", len(facets)) 192 + // Should have 1 facet: mention only (link is provided by embed) 193 + if len(facets) != 1 { 194 + t.Fatalf("expected 1 facet, got %d", len(facets)) 313 195 } 314 196 315 197 // Verify the complete post structure ··· 317 199 LexiconTypeID: "app.bsky.feed.post", 318 200 Text: text, 319 201 Facets: facets, 202 + Langs: []string{"en"}, 320 203 } 321 204 322 205 if post.Text == "" { 323 206 t.Error("post text is empty") 324 207 } 325 208 326 - if len(post.Facets) != 2 { 327 - t.Errorf("post has %d facets, want 2", len(post.Facets)) 209 + if len(post.Facets) != 1 { 210 + t.Errorf("post has %d facets, want 1", len(post.Facets)) 328 211 } 329 212 330 213 // Verify text contains expected components 331 214 expectedTexts := []string{ 332 215 "@" + userHandle, 333 216 repoWithTag, 334 - digestShort, 335 - sizeStr, 336 217 } 337 218 338 219 for _, expected := range expectedTexts { ··· 340 221 t.Errorf("post text missing expected component: %q", expected) 341 222 } 342 223 } 224 + 225 + // Verify post does NOT contain digest or size (now in embed description) 226 + if strings.Contains(text, "Digest:") { 227 + t.Error("simplified post should not contain Digest:") 228 + } 229 + if strings.Contains(text, "Size:") { 230 + t.Error("simplified post should not contain Size:") 231 + } 343 232 } 344 233 345 - func TestBuildFacets_MultiArchExample(t *testing.T) { 346 - // Test with a multi-arch manifest (platforms instead of size) 234 + func TestSimplifiedPostFormat_MultiArch(t *testing.T) { 235 + // Test the new simplified post format for multi-arch images 347 236 repository := "myapp" 348 237 tag := "latest" 349 238 userHandle := "alice.bsky.social" 350 239 userDID := "did:plc:alice123" 351 - digest := "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" 352 - platforms := []string{"linux/amd64", "linux/arm64"} 353 240 354 241 repoWithTag := repository + ":" + tag 355 - digestShort := formatDigest(digest) 356 - platformsStr := strings.Join(platforms, ", ") 242 + text := "@" + userHandle + " pushed " + repoWithTag 357 243 358 - text := "@" + userHandle + " just pushed " + repoWithTag + "\nDigest: " + digestShort + " Platforms: " + platformsStr 359 - appViewURL := "https://atcr.io/r/" + userHandle + "/" + repository 244 + facets := buildMentionFacet(text, userHandle, userDID) 360 245 361 - facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 362 - 363 - // Should have 2 facets: mention and link 364 - if len(facets) != 2 { 365 - t.Fatalf("expected 2 facets, got %d", len(facets)) 246 + // Should have 1 facet: mention only 247 + if len(facets) != 1 { 248 + t.Fatalf("expected 1 facet, got %d", len(facets)) 366 249 } 367 250 368 251 // Verify the complete post structure ··· 370 253 LexiconTypeID: "app.bsky.feed.post", 371 254 Text: text, 372 255 Facets: facets, 256 + Langs: []string{"en"}, 373 257 } 374 258 375 259 if post.Text == "" { ··· 380 264 expectedTexts := []string{ 381 265 "@" + userHandle, 382 266 repoWithTag, 383 - digestShort, 384 - "Platforms:", 385 - "linux/amd64", 386 - "linux/arm64", 387 267 } 388 268 389 269 for _, expected := range expectedTexts { ··· 392 272 } 393 273 } 394 274 395 - // Verify Size is NOT in multi-arch post 396 - if strings.Contains(post.Text, "Size:") { 397 - t.Error("multi-arch post should not contain Size:") 275 + // Verify Platforms is NOT in text (now in embed description) 276 + if strings.Contains(post.Text, "Platforms:") { 277 + t.Error("simplified post should not contain Platforms:") 278 + } 279 + } 280 + 281 + func TestEmbedDescription(t *testing.T) { 282 + // Test the dynamic description generation for embeds 283 + tests := []struct { 284 + name string 285 + platforms []string 286 + totalSize int64 287 + wantContain string 288 + }{ 289 + { 290 + name: "single-arch with size", 291 + platforms: []string{}, 292 + totalSize: 12800000, // ~12.2 MB 293 + wantContain: "Pushed 12.2 MB to ATCR", 294 + }, 295 + { 296 + name: "multi-arch with platforms", 297 + platforms: []string{"linux/amd64", "linux/arm64"}, 298 + totalSize: 0, 299 + wantContain: "Multi-arch: linux/amd64, linux/arm64", 300 + }, 301 + { 302 + name: "single platform", 303 + platforms: []string{"linux/amd64"}, 304 + totalSize: 0, 305 + wantContain: "Multi-arch: linux/amd64", 306 + }, 307 + } 308 + 309 + for _, tt := range tests { 310 + t.Run(tt.name, func(t *testing.T) { 311 + var description string 312 + if len(tt.platforms) > 0 { 313 + description = "Multi-arch: " + strings.Join(tt.platforms, ", ") 314 + } else { 315 + description = "Pushed " + formatSize(tt.totalSize) + " to ATCR" 316 + } 317 + 318 + if !strings.Contains(description, tt.wantContain) { 319 + t.Errorf("description = %q, want to contain %q", description, tt.wantContain) 320 + } 321 + }) 398 322 } 399 323 }