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