mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 17:24:16 +00:00
have Holds post with new og card
This commit is contained in:
@@ -317,6 +317,7 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
postURI, err = h.pds.CreateManifestPost(
|
||||
ctx,
|
||||
h.driver,
|
||||
req.Repository,
|
||||
req.Tag,
|
||||
req.UserHandle,
|
||||
|
||||
@@ -3,18 +3,21 @@ package pds
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bsky "github.com/bluesky-social/indigo/api/bsky"
|
||||
"github.com/distribution/distribution/v3/registry/storage/driver"
|
||||
)
|
||||
|
||||
// CreateManifestPost creates a Bluesky post announcing a manifest upload
|
||||
// Includes facets for clickable mentions and links
|
||||
// For multi-arch images (platforms non-empty), shows platforms instead of size
|
||||
// Includes mention facet for the user and an OG card embed with thumbnail
|
||||
func (p *HoldPDS) CreateManifestPost(
|
||||
ctx context.Context,
|
||||
storageDriver driver.StorageDriver,
|
||||
repository, tag, userHandle, userDID, digest string,
|
||||
totalSize int64,
|
||||
platforms []string,
|
||||
@@ -24,31 +27,55 @@ func (p *HoldPDS) CreateManifestPost(
|
||||
// Build AppView repository URL
|
||||
appViewURL := fmt.Sprintf("https://atcr.io/r/%s/%s", userHandle, repository)
|
||||
|
||||
// Format post text components
|
||||
digestShort := formatDigest(digest)
|
||||
// Build simplified text with mention - OG card handles the link
|
||||
repoWithTag := fmt.Sprintf("%s:%s", repository, tag)
|
||||
text := fmt.Sprintf("@%s pushed %s", userHandle, repoWithTag)
|
||||
|
||||
// Build text based on whether this is multi-arch or single-arch
|
||||
var text string
|
||||
if len(platforms) > 0 {
|
||||
// Multi-arch: show platforms
|
||||
platformsStr := strings.Join(platforms, ", ")
|
||||
text = fmt.Sprintf("@%s just pushed %s\nDigest: %s Platforms: %s", userHandle, repoWithTag, digestShort, platformsStr)
|
||||
// Only build mention facet - the OG card embed provides the link
|
||||
facets := buildMentionFacet(text, userHandle, userDID)
|
||||
|
||||
// Build embed with OG card
|
||||
var embed *bsky.FeedPost_Embed
|
||||
|
||||
ogImageData, err := fetchOGImage(ctx, userHandle, repository)
|
||||
if err != nil {
|
||||
slog.Warn("Failed to fetch OG image, posting without embed", "error", err)
|
||||
} else {
|
||||
// Single-arch: show size
|
||||
sizeStr := formatSize(totalSize)
|
||||
text = fmt.Sprintf("@%s just pushed %s\nDigest: %s Size: %s", userHandle, repoWithTag, digestShort, sizeStr)
|
||||
// Upload OG image as blob
|
||||
thumbBlob, err := uploadBlobToStorage(ctx, storageDriver, p.did, ogImageData, "image/png")
|
||||
if err != nil {
|
||||
slog.Warn("Failed to upload OG image blob", "error", err)
|
||||
} else {
|
||||
// Build dynamic description
|
||||
var description string
|
||||
if len(platforms) > 0 {
|
||||
description = fmt.Sprintf("Multi-arch: %s", strings.Join(platforms, ", "))
|
||||
} else {
|
||||
description = fmt.Sprintf("Pushed %s to ATCR", formatSize(totalSize))
|
||||
}
|
||||
|
||||
embed = &bsky.FeedPost_Embed{
|
||||
EmbedExternal: &bsky.EmbedExternal{
|
||||
LexiconTypeID: "app.bsky.embed.external",
|
||||
External: &bsky.EmbedExternal_External{
|
||||
Uri: appViewURL,
|
||||
Title: fmt.Sprintf("%s/%s:%s", userHandle, repository, tag),
|
||||
Description: description,
|
||||
Thumb: thumbBlob,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create facets for mentions and links
|
||||
facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL)
|
||||
|
||||
// Create post struct with facets
|
||||
// Create post struct with facets and embed
|
||||
post := &bsky.FeedPost{
|
||||
LexiconTypeID: "app.bsky.feed.post",
|
||||
Text: text,
|
||||
Facets: facets,
|
||||
Embed: embed,
|
||||
CreatedAt: now.Format(time.RFC3339),
|
||||
Langs: []string{"en"},
|
||||
}
|
||||
|
||||
// Create record with auto-generated TID
|
||||
@@ -73,19 +100,52 @@ func (p *HoldPDS) CreateManifestPost(
|
||||
return postURI, nil
|
||||
}
|
||||
|
||||
// formatDigest truncates digest to first 10 chars
|
||||
// Example: sha256:abc1234567890fedcba9876543210 -> sha256:abc1234567...
|
||||
func formatDigest(digest string) string {
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
return digest // Return as-is if not sha256
|
||||
// fetchOGImage downloads the OG card image from AppView
|
||||
func fetchOGImage(ctx context.Context, userHandle, repository string) ([]byte, error) {
|
||||
url := fmt.Sprintf("https://atcr.io/og/r/%s/%s", userHandle, repository)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash := strings.TrimPrefix(digest, "sha256:")
|
||||
if len(hash) <= 10 {
|
||||
return digest // Too short to truncate
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("OG image fetch failed: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("sha256:%s...", hash[:10])
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// buildMentionFacet creates a mention facet for the user handle
|
||||
// IMPORTANT: Byte offsets must be calculated for UTF-8 encoded text
|
||||
func buildMentionFacet(text, userHandle, userDID string) []*bsky.RichtextFacet {
|
||||
mentionText := "@" + userHandle
|
||||
mentionStart := strings.Index(text, mentionText)
|
||||
if mentionStart < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
byteStart := int64(len(text[:mentionStart]))
|
||||
byteEnd := int64(len(text[:mentionStart+len(mentionText)]))
|
||||
|
||||
return []*bsky.RichtextFacet{{
|
||||
Index: &bsky.RichtextFacet_ByteSlice{
|
||||
ByteStart: byteStart,
|
||||
ByteEnd: byteEnd,
|
||||
},
|
||||
Features: []*bsky.RichtextFacet_Features_Elem{{
|
||||
RichtextFacet_Mention: &bsky.RichtextFacet_Mention{
|
||||
Did: userDID,
|
||||
},
|
||||
}},
|
||||
}}
|
||||
}
|
||||
|
||||
// formatSize converts bytes to human-readable format
|
||||
@@ -108,56 +168,3 @@ func formatSize(bytes int64) string {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// buildFacets creates mention and link facets for rich text
|
||||
// IMPORTANT: Byte offsets must be calculated for UTF-8 encoded text
|
||||
func buildFacets(text, userHandle, userDID, repoWithTag, appViewURL string) []*bsky.RichtextFacet {
|
||||
facets := []*bsky.RichtextFacet{}
|
||||
|
||||
// Find mention: "@alice.bsky.social"
|
||||
mentionText := "@" + userHandle
|
||||
mentionStart := strings.Index(text, mentionText)
|
||||
if mentionStart >= 0 {
|
||||
// Calculate byte offsets (not character offsets!)
|
||||
byteStart := int64(len(text[:mentionStart]))
|
||||
byteEnd := int64(len(text[:mentionStart+len(mentionText)]))
|
||||
|
||||
facets = append(facets, &bsky.RichtextFacet{
|
||||
Index: &bsky.RichtextFacet_ByteSlice{
|
||||
ByteStart: byteStart,
|
||||
ByteEnd: byteEnd,
|
||||
},
|
||||
Features: []*bsky.RichtextFacet_Features_Elem{
|
||||
{
|
||||
RichtextFacet_Mention: &bsky.RichtextFacet_Mention{
|
||||
Did: userDID,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Find repository link: "hsm-secrets-operator:latest"
|
||||
linkStart := strings.Index(text, repoWithTag)
|
||||
if linkStart >= 0 {
|
||||
// Calculate byte offsets
|
||||
byteStart := int64(len(text[:linkStart]))
|
||||
byteEnd := int64(len(text[:linkStart+len(repoWithTag)]))
|
||||
|
||||
facets = append(facets, &bsky.RichtextFacet{
|
||||
Index: &bsky.RichtextFacet_ByteSlice{
|
||||
ByteStart: byteStart,
|
||||
ByteEnd: byteEnd,
|
||||
},
|
||||
Features: []*bsky.RichtextFacet_Features_Elem{
|
||||
{
|
||||
RichtextFacet_Link: &bsky.RichtextFacet_Link{
|
||||
Uri: appViewURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return facets
|
||||
}
|
||||
|
||||
+109
-185
@@ -7,44 +7,6 @@ import (
|
||||
bsky "github.com/bluesky-social/indigo/api/bsky"
|
||||
)
|
||||
|
||||
func TestFormatDigest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
digest string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "standard sha256 digest",
|
||||
digest: "sha256:abc1234567890fedcba9876543210",
|
||||
expected: "sha256:abc1234567...", // First 10 chars
|
||||
},
|
||||
{
|
||||
name: "short digest (no truncation)",
|
||||
digest: "sha256:abc123",
|
||||
expected: "sha256:abc123",
|
||||
},
|
||||
{
|
||||
name: "non-sha256 digest",
|
||||
digest: "sha512:abc123",
|
||||
expected: "sha512:abc123",
|
||||
},
|
||||
{
|
||||
name: "real sha256 digest",
|
||||
digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
|
||||
expected: "sha256:e692418e4c...", // First 10 chars
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := formatDigest(tt.digest)
|
||||
if result != tt.expected {
|
||||
t.Errorf("formatDigest(%q) = %q, want %q", tt.digest, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -103,56 +65,47 @@ func TestFormatSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFacets(t *testing.T) {
|
||||
func TestBuildMentionFacet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
userHandle string
|
||||
userDID string
|
||||
repoWithTag string
|
||||
appViewURL string
|
||||
wantFacets int // number of facets expected
|
||||
name string
|
||||
text string
|
||||
userHandle string
|
||||
userDID string
|
||||
wantFacets int // number of facets expected
|
||||
}{
|
||||
{
|
||||
name: "standard post with mention and link",
|
||||
text: "@alice.bsky.social just pushed myapp:latest\nDigest: sha256:abc...def Size: 12.2 MB",
|
||||
userHandle: "alice.bsky.social",
|
||||
userDID: "did:plc:alice123",
|
||||
repoWithTag: "myapp:latest",
|
||||
appViewURL: "https://atcr.io/r/alice.bsky.social/myapp",
|
||||
wantFacets: 2,
|
||||
name: "standard post with mention",
|
||||
text: "@alice.bsky.social pushed myapp:latest",
|
||||
userHandle: "alice.bsky.social",
|
||||
userDID: "did:plc:alice123",
|
||||
wantFacets: 1,
|
||||
},
|
||||
{
|
||||
name: "no matches found",
|
||||
text: "random text",
|
||||
userHandle: "alice.bsky.social",
|
||||
userDID: "did:plc:alice123",
|
||||
repoWithTag: "myapp:latest",
|
||||
appViewURL: "https://atcr.io/r/alice.bsky.social/myapp",
|
||||
wantFacets: 0,
|
||||
name: "no mention found",
|
||||
text: "random text",
|
||||
userHandle: "alice.bsky.social",
|
||||
userDID: "did:plc:alice123",
|
||||
wantFacets: 0,
|
||||
},
|
||||
{
|
||||
name: "only mention found",
|
||||
text: "@alice.bsky.social did something",
|
||||
userHandle: "alice.bsky.social",
|
||||
userDID: "did:plc:alice123",
|
||||
repoWithTag: "myapp:latest",
|
||||
appViewURL: "https://atcr.io/r/alice.bsky.social/myapp",
|
||||
wantFacets: 1,
|
||||
name: "mention at start",
|
||||
text: "@alice.bsky.social did something",
|
||||
userHandle: "alice.bsky.social",
|
||||
userDID: "did:plc:alice123",
|
||||
wantFacets: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
facets := buildFacets(tt.text, tt.userHandle, tt.userDID, tt.repoWithTag, tt.appViewURL)
|
||||
facets := buildMentionFacet(tt.text, tt.userHandle, tt.userDID)
|
||||
|
||||
if len(facets) != tt.wantFacets {
|
||||
t.Errorf("buildFacets() returned %d facets, want %d", len(facets), tt.wantFacets)
|
||||
t.Errorf("buildMentionFacet() returned %d facets, want %d", len(facets), tt.wantFacets)
|
||||
}
|
||||
|
||||
// Verify facet structure for standard case
|
||||
if tt.name == "standard post with mention and link" && len(facets) == 2 {
|
||||
// Check mention facet
|
||||
if tt.wantFacets > 0 && len(facets) > 0 {
|
||||
mentionFacet := facets[0]
|
||||
if mentionFacet.Index == nil {
|
||||
t.Error("mention facet has nil Index")
|
||||
@@ -163,38 +116,24 @@ func TestBuildFacets(t *testing.T) {
|
||||
if mentionFacet.Features[0].RichtextFacet_Mention == nil {
|
||||
t.Error("mention facet feature is not a mention")
|
||||
}
|
||||
|
||||
// Check link facet
|
||||
linkFacet := facets[1]
|
||||
if linkFacet.Index == nil {
|
||||
t.Error("link facet has nil Index")
|
||||
}
|
||||
if len(linkFacet.Features) != 1 {
|
||||
t.Errorf("link facet has %d features, want 1", len(linkFacet.Features))
|
||||
}
|
||||
if linkFacet.Features[0].RichtextFacet_Link == nil {
|
||||
t.Error("link facet feature is not a link")
|
||||
}
|
||||
if linkFacet.Features[0].RichtextFacet_Link.Uri != tt.appViewURL {
|
||||
t.Errorf("link facet URI = %q, want %q", linkFacet.Features[0].RichtextFacet_Link.Uri, tt.appViewURL)
|
||||
if mentionFacet.Features[0].RichtextFacet_Mention.Did != tt.userDID {
|
||||
t.Errorf("mention DID = %q, want %q", mentionFacet.Features[0].RichtextFacet_Mention.Did, tt.userDID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFacets_ByteOffsets(t *testing.T) {
|
||||
func TestBuildMentionFacet_ByteOffsets(t *testing.T) {
|
||||
// Test that byte offsets are correctly calculated
|
||||
text := "@alice.bsky.social just pushed myapp:latest"
|
||||
text := "@alice.bsky.social pushed myapp:latest"
|
||||
userHandle := "alice.bsky.social"
|
||||
userDID := "did:plc:alice123"
|
||||
repoWithTag := "myapp:latest"
|
||||
appViewURL := "https://atcr.io/r/alice.bsky.social/myapp"
|
||||
|
||||
facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL)
|
||||
facets := buildMentionFacet(text, userHandle, userDID)
|
||||
|
||||
if len(facets) != 2 {
|
||||
t.Fatalf("expected 2 facets, got %d", len(facets))
|
||||
if len(facets) != 1 {
|
||||
t.Fatalf("expected 1 facet, got %d", len(facets))
|
||||
}
|
||||
|
||||
// Check mention facet byte offsets
|
||||
@@ -215,39 +154,18 @@ func TestBuildFacets_ByteOffsets(t *testing.T) {
|
||||
if extractedMention != mentionText {
|
||||
t.Errorf("extracted mention = %q, want %q", extractedMention, mentionText)
|
||||
}
|
||||
|
||||
// Check link facet byte offsets
|
||||
linkFacet := facets[1]
|
||||
linkStart := len("@alice.bsky.social just pushed ")
|
||||
expectedLinkStart := int64(linkStart)
|
||||
expectedLinkEnd := int64(linkStart + len(repoWithTag))
|
||||
|
||||
if linkFacet.Index.ByteStart != expectedLinkStart {
|
||||
t.Errorf("link ByteStart = %d, want %d", linkFacet.Index.ByteStart, expectedLinkStart)
|
||||
}
|
||||
if linkFacet.Index.ByteEnd != expectedLinkEnd {
|
||||
t.Errorf("link ByteEnd = %d, want %d", linkFacet.Index.ByteEnd, expectedLinkEnd)
|
||||
}
|
||||
|
||||
// Verify the link text extraction
|
||||
extractedLink := text[linkFacet.Index.ByteStart:linkFacet.Index.ByteEnd]
|
||||
if extractedLink != repoWithTag {
|
||||
t.Errorf("extracted link = %q, want %q", extractedLink, repoWithTag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFacets_UTF8Handling(t *testing.T) {
|
||||
func TestBuildMentionFacet_UTF8Handling(t *testing.T) {
|
||||
// Test with Unicode characters to ensure byte offsets work correctly
|
||||
text := "@alice.bsky.social just pushed 🚀myapp:latest"
|
||||
text := "@alice.bsky.social pushed 🚀myapp:latest"
|
||||
userHandle := "alice.bsky.social"
|
||||
userDID := "did:plc:alice123"
|
||||
repoWithTag := "🚀myapp:latest" // Note: emoji is multi-byte
|
||||
appViewURL := "https://atcr.io/r/alice.bsky.social/myapp"
|
||||
|
||||
facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL)
|
||||
facets := buildMentionFacet(text, userHandle, userDID)
|
||||
|
||||
if len(facets) != 2 {
|
||||
t.Fatalf("expected 2 facets, got %d", len(facets))
|
||||
if len(facets) != 1 {
|
||||
t.Fatalf("expected 1 facet, got %d", len(facets))
|
||||
}
|
||||
|
||||
// Verify that byte extraction works with UTF-8
|
||||
@@ -257,59 +175,23 @@ func TestBuildFacets_UTF8Handling(t *testing.T) {
|
||||
if extractedMention != expectedMention {
|
||||
t.Errorf("extracted mention = %q, want %q", extractedMention, expectedMention)
|
||||
}
|
||||
|
||||
linkFacet := facets[1]
|
||||
extractedLink := text[linkFacet.Index.ByteStart:linkFacet.Index.ByteEnd]
|
||||
if extractedLink != repoWithTag {
|
||||
t.Errorf("extracted link = %q, want %q", extractedLink, repoWithTag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFacets_NoOverlap(t *testing.T) {
|
||||
// Ensure facets don't overlap
|
||||
text := "@alice.bsky.social just pushed myapp:latest"
|
||||
userHandle := "alice.bsky.social"
|
||||
userDID := "did:plc:alice123"
|
||||
repoWithTag := "myapp:latest"
|
||||
appViewURL := "https://atcr.io/r/alice.bsky.social/myapp"
|
||||
|
||||
facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL)
|
||||
|
||||
if len(facets) != 2 {
|
||||
t.Fatalf("expected 2 facets, got %d", len(facets))
|
||||
}
|
||||
|
||||
// Facets should not overlap
|
||||
facet1 := facets[0]
|
||||
facet2 := facets[1]
|
||||
|
||||
if facet1.Index.ByteEnd > facet2.Index.ByteStart {
|
||||
t.Errorf("facets overlap: facet1 ends at %d, facet2 starts at %d",
|
||||
facet1.Index.ByteEnd, facet2.Index.ByteStart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFacets_RealWorldExample(t *testing.T) {
|
||||
// Test with the actual example from the requirements
|
||||
func TestSimplifiedPostFormat(t *testing.T) {
|
||||
// Test the new simplified post format: "@user pushed repo:tag"
|
||||
repository := "hsm-secrets-operator"
|
||||
tag := "latest"
|
||||
userHandle := "evan.jarrett.net"
|
||||
userDID := "did:plc:pddp4xt5lgnv2qsegbzzs4xg"
|
||||
digest := "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
totalSize := int64(12800000) // ~12.2 MB
|
||||
|
||||
repoWithTag := repository + ":" + tag
|
||||
digestShort := formatDigest(digest)
|
||||
sizeStr := formatSize(totalSize)
|
||||
text := "@" + userHandle + " pushed " + repoWithTag
|
||||
|
||||
text := "@" + userHandle + " just pushed " + repoWithTag + "\nDigest: " + digestShort + " Size: " + sizeStr
|
||||
appViewURL := "https://atcr.io/r/" + userHandle + "/" + repository
|
||||
facets := buildMentionFacet(text, userHandle, userDID)
|
||||
|
||||
facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL)
|
||||
|
||||
// Should have 2 facets: mention and link
|
||||
if len(facets) != 2 {
|
||||
t.Fatalf("expected 2 facets, got %d", len(facets))
|
||||
// Should have 1 facet: mention only (link is provided by embed)
|
||||
if len(facets) != 1 {
|
||||
t.Fatalf("expected 1 facet, got %d", len(facets))
|
||||
}
|
||||
|
||||
// Verify the complete post structure
|
||||
@@ -317,22 +199,21 @@ func TestBuildFacets_RealWorldExample(t *testing.T) {
|
||||
LexiconTypeID: "app.bsky.feed.post",
|
||||
Text: text,
|
||||
Facets: facets,
|
||||
Langs: []string{"en"},
|
||||
}
|
||||
|
||||
if post.Text == "" {
|
||||
t.Error("post text is empty")
|
||||
}
|
||||
|
||||
if len(post.Facets) != 2 {
|
||||
t.Errorf("post has %d facets, want 2", len(post.Facets))
|
||||
if len(post.Facets) != 1 {
|
||||
t.Errorf("post has %d facets, want 1", len(post.Facets))
|
||||
}
|
||||
|
||||
// Verify text contains expected components
|
||||
expectedTexts := []string{
|
||||
"@" + userHandle,
|
||||
repoWithTag,
|
||||
digestShort,
|
||||
sizeStr,
|
||||
}
|
||||
|
||||
for _, expected := range expectedTexts {
|
||||
@@ -340,29 +221,31 @@ func TestBuildFacets_RealWorldExample(t *testing.T) {
|
||||
t.Errorf("post text missing expected component: %q", expected)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify post does NOT contain digest or size (now in embed description)
|
||||
if strings.Contains(text, "Digest:") {
|
||||
t.Error("simplified post should not contain Digest:")
|
||||
}
|
||||
if strings.Contains(text, "Size:") {
|
||||
t.Error("simplified post should not contain Size:")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFacets_MultiArchExample(t *testing.T) {
|
||||
// Test with a multi-arch manifest (platforms instead of size)
|
||||
func TestSimplifiedPostFormat_MultiArch(t *testing.T) {
|
||||
// Test the new simplified post format for multi-arch images
|
||||
repository := "myapp"
|
||||
tag := "latest"
|
||||
userHandle := "alice.bsky.social"
|
||||
userDID := "did:plc:alice123"
|
||||
digest := "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
platforms := []string{"linux/amd64", "linux/arm64"}
|
||||
|
||||
repoWithTag := repository + ":" + tag
|
||||
digestShort := formatDigest(digest)
|
||||
platformsStr := strings.Join(platforms, ", ")
|
||||
text := "@" + userHandle + " pushed " + repoWithTag
|
||||
|
||||
text := "@" + userHandle + " just pushed " + repoWithTag + "\nDigest: " + digestShort + " Platforms: " + platformsStr
|
||||
appViewURL := "https://atcr.io/r/" + userHandle + "/" + repository
|
||||
facets := buildMentionFacet(text, userHandle, userDID)
|
||||
|
||||
facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL)
|
||||
|
||||
// Should have 2 facets: mention and link
|
||||
if len(facets) != 2 {
|
||||
t.Fatalf("expected 2 facets, got %d", len(facets))
|
||||
// Should have 1 facet: mention only
|
||||
if len(facets) != 1 {
|
||||
t.Fatalf("expected 1 facet, got %d", len(facets))
|
||||
}
|
||||
|
||||
// Verify the complete post structure
|
||||
@@ -370,6 +253,7 @@ func TestBuildFacets_MultiArchExample(t *testing.T) {
|
||||
LexiconTypeID: "app.bsky.feed.post",
|
||||
Text: text,
|
||||
Facets: facets,
|
||||
Langs: []string{"en"},
|
||||
}
|
||||
|
||||
if post.Text == "" {
|
||||
@@ -380,10 +264,6 @@ func TestBuildFacets_MultiArchExample(t *testing.T) {
|
||||
expectedTexts := []string{
|
||||
"@" + userHandle,
|
||||
repoWithTag,
|
||||
digestShort,
|
||||
"Platforms:",
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
}
|
||||
|
||||
for _, expected := range expectedTexts {
|
||||
@@ -392,8 +272,52 @@ func TestBuildFacets_MultiArchExample(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Size is NOT in multi-arch post
|
||||
if strings.Contains(post.Text, "Size:") {
|
||||
t.Error("multi-arch post should not contain Size:")
|
||||
// Verify Platforms is NOT in text (now in embed description)
|
||||
if strings.Contains(post.Text, "Platforms:") {
|
||||
t.Error("simplified post should not contain Platforms:")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbedDescription(t *testing.T) {
|
||||
// Test the dynamic description generation for embeds
|
||||
tests := []struct {
|
||||
name string
|
||||
platforms []string
|
||||
totalSize int64
|
||||
wantContain string
|
||||
}{
|
||||
{
|
||||
name: "single-arch with size",
|
||||
platforms: []string{},
|
||||
totalSize: 12800000, // ~12.2 MB
|
||||
wantContain: "Pushed 12.2 MB to ATCR",
|
||||
},
|
||||
{
|
||||
name: "multi-arch with platforms",
|
||||
platforms: []string{"linux/amd64", "linux/arm64"},
|
||||
totalSize: 0,
|
||||
wantContain: "Multi-arch: linux/amd64, linux/arm64",
|
||||
},
|
||||
{
|
||||
name: "single platform",
|
||||
platforms: []string{"linux/amd64"},
|
||||
totalSize: 0,
|
||||
wantContain: "Multi-arch: linux/amd64",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var description string
|
||||
if len(tt.platforms) > 0 {
|
||||
description = "Multi-arch: " + strings.Join(tt.platforms, ", ")
|
||||
} else {
|
||||
description = "Pushed " + formatSize(tt.totalSize) + " to ATCR"
|
||||
}
|
||||
|
||||
if !strings.Contains(description, tt.wantContain) {
|
||||
t.Errorf("description = %q, want to contain %q", description, tt.wantContain)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user