Monorepo for Tangled tangled.org
2

Configure Feed

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

1package appview 2 3import ( 4 "encoding/json" 5 "io" 6 "log/slog" 7 "path/filepath" 8 "strings" 9 "testing" 10 "time" 11 12 jmodels "github.com/bluesky-social/jetstream/pkg/models" 13 "tangled.org/core/api/tangled" 14 "tangled.org/core/appview/db" 15 "tangled.org/core/appview/models" 16 "tangled.org/core/appview/validator" 17 "tangled.org/core/orm" 18) 19 20func newStringIngester(t *testing.T) *Ingester { 21 t.Helper() 22 path := filepath.Join(t.TempDir(), "test.db") 23 d, err := db.Make(t.Context(), path) 24 if err != nil { 25 t.Fatalf("db.Make: %v", err) 26 } 27 t.Cleanup(func() { d.Close() }) 28 return &Ingester{ 29 Db: d, 30 Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), 31 Validator: &validator.Validator{}, 32 } 33} 34 35func makeStringEvent(t *testing.T, op, did, rkey string, record tangled.String) *jmodels.Event { 36 t.Helper() 37 raw, err := json.Marshal(record) 38 if err != nil { 39 t.Fatalf("marshal record: %v", err) 40 } 41 return &jmodels.Event{ 42 Did: did, 43 Kind: jmodels.EventKindCommit, 44 Commit: &jmodels.Commit{ 45 Operation: op, 46 Collection: tangled.StringNSID, 47 RKey: rkey, 48 Record: raw, 49 }, 50 } 51} 52 53func loadString(t *testing.T, ing *Ingester, did, rkey string) (models.String, bool) { 54 t.Helper() 55 rows, err := db.GetStrings(ing.Db, 0, 56 orm.FilterEq("did", did), 57 orm.FilterEq("rkey", rkey), 58 ) 59 if err != nil { 60 t.Fatalf("GetStrings: %v", err) 61 } 62 if len(rows) == 0 { 63 return models.String{}, false 64 } 65 if len(rows) > 1 { 66 t.Fatalf("expected at most one row for (%s,%s), got %d", did, rkey, len(rows)) 67 } 68 return rows[0], true 69} 70 71func TestIngestString_CreateRoundTrip(t *testing.T) { 72 ing := newStringIngester(t) 73 created := time.Date(2025, 9, 14, 10, 30, 0, 0, time.UTC) 74 75 e := makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "rk1", tangled.String{ 76 Filename: "hello.txt", 77 Description: "a greeting", 78 Contents: "hello world\n", 79 CreatedAt: created.Format(time.RFC3339), 80 }) 81 82 if err := ing.ingestString(e); err != nil { 83 t.Fatalf("ingestString: %v", err) 84 } 85 86 s, ok := loadString(t, ing, "did:plc:boltless", "rk1") 87 if !ok { 88 t.Fatal("row not inserted") 89 } 90 if s.Filename != "hello.txt" { 91 t.Errorf("filename = %q, want hello.txt", s.Filename) 92 } 93 if s.Description != "a greeting" { 94 t.Errorf("description = %q", s.Description) 95 } 96 if s.Contents != "hello world\n" { 97 t.Errorf("contents = %q", s.Contents) 98 } 99 if !s.Created.Equal(created) { 100 t.Errorf("created = %v, want %v (record CreatedAt must round-trip)", s.Created, created) 101 } 102 if s.Edited != nil { 103 t.Errorf("edited = %v, want nil on create", s.Edited) 104 } 105} 106 107func TestIngestString_UpdateBumpsEditedOnContentChange(t *testing.T) { 108 ing := newStringIngester(t) 109 created := time.Date(2025, 9, 14, 10, 30, 0, 0, time.UTC) 110 base := tangled.String{ 111 Filename: "hello.txt", 112 Description: "a greeting", 113 Contents: "hello world\n", 114 CreatedAt: created.Format(time.RFC3339), 115 } 116 117 if err := ing.ingestString(makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "rk1", base)); err != nil { 118 t.Fatalf("ingestString create: %v", err) 119 } 120 121 updated := base 122 updated.Contents = "hello, world!\n" 123 if err := ing.ingestString(makeStringEvent(t, jmodels.CommitOperationUpdate, "did:plc:boltless", "rk1", updated)); err != nil { 124 t.Fatalf("ingestString update: %v", err) 125 } 126 127 s, ok := loadString(t, ing, "did:plc:boltless", "rk1") 128 if !ok { 129 t.Fatal("row missing after update") 130 } 131 if s.Contents != "hello, world!\n" { 132 t.Errorf("contents = %q, want updated value", s.Contents) 133 } 134 if !s.Created.Equal(created) { 135 t.Errorf("update overwrote created: got %v, want %v", s.Created, created) 136 } 137 if s.Edited == nil { 138 t.Fatal("edited not set after content change") 139 } 140} 141 142func TestIngestString_UpdateNoChangeKeepsEditedNil(t *testing.T) { 143 ing := newStringIngester(t) 144 rec := tangled.String{ 145 Filename: "hello.txt", 146 Description: "a greeting", 147 Contents: "hello world\n", 148 CreatedAt: time.Date(2025, 9, 14, 10, 30, 0, 0, time.UTC).Format(time.RFC3339), 149 } 150 151 if err := ing.ingestString(makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "rk2", rec)); err != nil { 152 t.Fatalf("create: %v", err) 153 } 154 if err := ing.ingestString(makeStringEvent(t, jmodels.CommitOperationUpdate, "did:plc:akshay", "rk2", rec)); err != nil { 155 t.Fatalf("update: %v", err) 156 } 157 158 s, _ := loadString(t, ing, "did:plc:akshay", "rk2") 159 if s.Edited != nil { 160 t.Errorf("edited = %v, want nil when no field changed", s.Edited) 161 } 162} 163 164func TestIngestString_DeleteRemovesRow(t *testing.T) { 165 ing := newStringIngester(t) 166 rec := tangled.String{ 167 Filename: "hello.txt", 168 Contents: "x", 169 CreatedAt: time.Now().UTC().Format(time.RFC3339), 170 } 171 if err := ing.ingestString(makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "rk1", rec)); err != nil { 172 t.Fatalf("create: %v", err) 173 } 174 175 del := &jmodels.Event{ 176 Did: "did:plc:boltless", 177 Kind: jmodels.EventKindCommit, 178 Commit: &jmodels.Commit{ 179 Operation: jmodels.CommitOperationDelete, 180 Collection: tangled.StringNSID, 181 RKey: "rk1", 182 }, 183 } 184 if err := ing.ingestString(del); err != nil { 185 t.Fatalf("delete: %v", err) 186 } 187 188 if _, ok := loadString(t, ing, "did:plc:boltless", "rk1"); ok { 189 t.Fatal("row still present after delete") 190 } 191} 192 193func TestIngestString_ValidatorRejects(t *testing.T) { 194 ing := newStringIngester(t) 195 now := time.Now().UTC().Format(time.RFC3339) 196 197 cases := []struct { 198 name string 199 rec tangled.String 200 }{ 201 {"empty contents", tangled.String{Filename: "x", Contents: "", CreatedAt: now}}, 202 {"long filename", tangled.String{Filename: strings.Repeat("a", 141), Contents: "x", CreatedAt: now}}, 203 {"long description", tangled.String{Filename: "x", Description: strings.Repeat("d", 281), Contents: "x", CreatedAt: now}}, 204 } 205 206 for _, tc := range cases { 207 t.Run(tc.name, func(t *testing.T) { 208 e := makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "bad", tc.rec) 209 if err := ing.ingestString(e); err == nil { 210 t.Fatal("expected validator error, got nil") 211 } 212 if _, ok := loadString(t, ing, "did:plc:akshay", "bad"); ok { 213 t.Fatal("row inserted despite validator failure") 214 } 215 }) 216 } 217} 218 219func TestIngestString_ColdReplayPreservesCreated(t *testing.T) { 220 created := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC) 221 rec := tangled.String{ 222 Filename: "snippet.go", 223 Description: "first cut", 224 Contents: "package main\n", 225 CreatedAt: created.Format(time.RFC3339), 226 } 227 event := makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "rkcold", rec) 228 229 first := newStringIngester(t) 230 if err := first.ingestString(event); err != nil { 231 t.Fatalf("first ingest: %v", err) 232 } 233 live, _ := loadString(t, first, "did:plc:boltless", "rkcold") 234 235 second := newStringIngester(t) 236 if err := second.ingestString(event); err != nil { 237 t.Fatalf("replay ingest: %v", err) 238 } 239 replayed, _ := loadString(t, second, "did:plc:boltless", "rkcold") 240 241 if !live.Created.Equal(replayed.Created) { 242 t.Fatalf("cold replay drifted: live=%v replayed=%v", live.Created, replayed.Created) 243 } 244 if !replayed.Created.Equal(created) { 245 t.Fatalf("replay lost record CreatedAt: got %v, want %v", replayed.Created, created) 246 } 247}