Monorepo for Tangled tangled.org
3

Configure Feed

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

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