From 2f3bc8cafb0404c1797b3e401e8db4040e564bb1 Mon Sep 17 00:00:00 2001 From: Thomas Rademaker Date: Sat, 16 May 2026 20:06:44 -0400 Subject: [PATCH] fix subscribeComments decoder to convert snake_case so .created events decode --- Sources/EffemKit/CommentSubscription.swift | 7 +++++ Tests/EffemKitTests/EffemKitTests.swift | 34 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Sources/EffemKit/CommentSubscription.swift b/Sources/EffemKit/CommentSubscription.swift index 47a2e1d..6dcb9ae 100644 --- a/Sources/EffemKit/CommentSubscription.swift +++ b/Sources/EffemKit/CommentSubscription.swift @@ -43,7 +43,14 @@ enum CommentSubscription { socket.resume() defer { socket.cancel(with: .goingAway, reason: nil) } + // commentWireValue on the AppView emits snake_case JSON + // (created_at, like_count, self_labels, viewer_like_uri, + // subject_uri / subject_cid). The matching Swift Comment + // properties are camelCase, so the decoder must convert + // — otherwise required keys like `createdAt` go missing + // and every .created frame throws, killing the stream. let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase while !Task.isCancelled { do { diff --git a/Tests/EffemKitTests/EffemKitTests.swift b/Tests/EffemKitTests/EffemKitTests.swift index 80d675e..b06dc84 100644 --- a/Tests/EffemKitTests/EffemKitTests.swift +++ b/Tests/EffemKitTests/EffemKitTests.swift @@ -224,6 +224,10 @@ struct EpisodeRecordTests { @Suite("CommentEvent") struct CommentEventTests { @Test func decodesCreated() throws { + // Mirrors the production wire format emitted by commentWireValue on + // the AppView side: every Comment field that's snake_case on disk + // (created_at, like_count, self_labels) stays snake_case on the + // websocket. The CommentSubscription decoder must convert. let json = """ { "type": "created", @@ -233,7 +237,7 @@ struct CommentEventTests { "uri": "at://did:plc:a/xyz.effem.feed.comment/r1", "subject": {"uri": "at://catalog/ep/1", "cid": "cid-ep"}, "text": "hi", - "createdAt": "2026-05-15T10:00:00.000Z", + "created_at": "2026-05-15T10:00:00.000Z", "like_count": 0 } } @@ -248,6 +252,34 @@ struct CommentEventTests { } #expect(c.did == "did:plc:a") #expect(c.text == "hi") + #expect(c.createdAt == "2026-05-15T10:00:00.000Z") + #expect(c.likeCount == 0) + } + + @Test func decodesCreatedFailsWithoutSnakeCaseStrategy() { + // Documents why CommentSubscription configures keyDecodingStrategy: + // without it, the required `createdAt` and `likeCount` properties + // can't be matched against the wire's snake_case keys, and every + // .created frame would throw, killing the live-comments stream. + let json = """ + { + "type": "created", + "comment": { + "did": "did:plc:a", + "rkey": "r1", + "uri": "at://did:plc:a/xyz.effem.feed.comment/r1", + "subject": {"uri": "at://catalog/ep/1", "cid": "cid-ep"}, + "text": "hi", + "created_at": "2026-05-15T10:00:00.000Z", + "like_count": 0 + } + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + #expect(throws: DecodingError.self) { + _ = try decoder.decode(CommentEvent.self, from: json) + } } @Test func decodesDeleted() throws { -- 2.51.2