A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
81
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 }