@@ -2275,6 +2275,45 @@ func TestLegacyManifestFileImportAndEmbeddingDecodeErrors(t *testing.T) {
22752275 require .NoError (t , tx .Rollback ())
22762276}
22772277
2278+ func TestValidateSnapshotRowRejectsMalformedDeletedAtBeforeImport (t * testing.T ) {
2279+ require .NoError (t , validateSnapshotRow ("messages" , map [string ]any {"deleted_at" : nil }))
2280+ blank := map [string ]any {"deleted_at" : " " }
2281+ require .NoError (t , validateSnapshotRow ("messages" , blank ))
2282+ require .Nil (t , blank ["deleted_at" ])
2283+ require .NoError (t , validateSnapshotRow ("messages" , map [string ]any {"deleted_at" : "2026-07-14T12:00:00.123456789Z" }))
2284+ padded := map [string ]any {"deleted_at" : " 2026-07-14T12:00:00Z " }
2285+ require .NoError (t , validateSnapshotRow ("messages" , padded ))
2286+ require .Equal (t , "2026-07-14T12:00:00Z" , padded ["deleted_at" ])
2287+ require .ErrorContains (t , validateSnapshotRow ("messages" , map [string ]any {"deleted_at" : "not-a-timestamp" }), "must be RFC3339" )
2288+ require .ErrorContains (t , validateSnapshotRow ("messages" , map [string ]any {"deleted_at" : json .Number ("123" )}), "must be a string or null" )
2289+ require .NoError (t , validateSnapshotRow ("guilds" , map [string ]any {"deleted_at" : "not-a-timestamp" }))
2290+
2291+ ctx := context .Background ()
2292+ s , err := store .Open (ctx , filepath .Join (t .TempDir (), "discrawl.db" ))
2293+ require .NoError (t , err )
2294+ defer func () { _ = s .Close () }()
2295+ repo := t .TempDir ()
2296+ rel := filepath .ToSlash (filepath .Join ("tables" , "messages" , "tombstones.jsonl.gz" ))
2297+ require .NoError (t , os .MkdirAll (filepath .Dir (filepath .Join (repo , filepath .FromSlash (rel ))), 0o755 ))
2298+ writeGzipJSONLines (t , filepath .Join (repo , filepath .FromSlash (rel )), []string {
2299+ `{"id":"m1","guild_id":"g1","channel_id":"c1","author_id":null,"message_type":0,"created_at":"2026-07-14T12:00:00Z","edited_at":null,"deleted_at":null,"content":"one","normalized_content":"one","reply_to_message_id":null,"pinned":0,"has_attachments":0,"raw_json":"{}","updated_at":"2026-07-14T12:00:00Z"}` ,
2300+ `{"id":"m2","guild_id":"g1","channel_id":"c1","author_id":null,"message_type":0,"created_at":"2026-07-14T12:00:00Z","edited_at":null,"deleted_at":"not-a-timestamp","content":"two","normalized_content":"two","reply_to_message_id":null,"pinned":0,"has_attachments":0,"raw_json":"{}","updated_at":"2026-07-14T12:00:00Z"}` ,
2301+ })
2302+ tx , err := s .DB ().BeginTx (ctx , nil )
2303+ require .NoError (t , err )
2304+ err = importTable (ctx , tx , Options {RepoPath : repo }, TableManifest {
2305+ Name : "messages" , File : rel ,
2306+ Columns : []string {"id" , "guild_id" , "channel_id" , "author_id" , "message_type" , "created_at" , "edited_at" , "deleted_at" , "content" , "normalized_content" , "reply_to_message_id" , "pinned" , "has_attachments" , "raw_json" , "updated_at" },
2307+ })
2308+ require .ErrorContains (t , err , "messages.deleted_at must be RFC3339" )
2309+ var count int
2310+ require .NoError (t , tx .QueryRowContext (ctx , `select count(*) from messages` ).Scan (& count ))
2311+ require .Equal (t , 1 , count )
2312+ require .NoError (t , tx .Rollback ())
2313+ require .NoError (t , s .DB ().QueryRowContext (ctx , `select count(*) from messages` ).Scan (& count ))
2314+ require .Zero (t , count )
2315+ }
2316+
22782317func TestImportEmbeddingsRejectsUnsafeManifestFiles (t * testing.T ) {
22792318 t .Parallel ()
22802319
0 commit comments