@@ -113,6 +113,164 @@ describe.skipIf(SHOULD_SKIP_E2E || SHOULD_SKIP_MUTABLE_TESTS)(
113113 } ,
114114 ) ;
115115
116+ it (
117+ "should retrieve a message via channels get-message" ,
118+ { timeout : 60000 } ,
119+ async ( ) => {
120+ setupTestFailureHandler (
121+ "should retrieve a message via channels get-message" ,
122+ ) ;
123+
124+ // Use a fresh channel/serial so we don't see updates from other tests
125+ const getChannel = getMutableChannelName ( "msg-get" ) ;
126+ const serial = await publishAndGetSerial ( getChannel , "fresh-message" ) ;
127+
128+ const result = await runCommand (
129+ [ "channels" , "get-message" , getChannel , serial , "--json" ] ,
130+ {
131+ env : { ABLY_API_KEY : E2E_API_KEY || "" } ,
132+ timeoutMs : 30000 ,
133+ } ,
134+ ) ;
135+
136+ expect ( result . exitCode ) . toBe ( 0 ) ;
137+
138+ const records = parseNdjsonLines ( result . stdout ) ;
139+ const parsed = records . find ( ( r ) => r . type === "result" ) ?? records [ 0 ] ;
140+ expect ( parsed . success ) . toBe ( true ) ;
141+ expect ( parsed . message ) . toBeDefined ( ) ;
142+ const message = parsed . message as Record < string , unknown > ;
143+ expect ( message . serial ) . toBe ( serial ) ;
144+ expect ( message . data ) . toBe ( "fresh-message" ) ;
145+ // Timestamp must be ISO 8601 (history-style normalisation)
146+ expect ( message . timestamp ) . toMatch (
147+ / ^ \d { 4 } - \d { 2 } - \d { 2 } T \d { 2 } : \d { 2 } : \d { 2 } \. \d { 3 } Z $ / ,
148+ ) ;
149+ } ,
150+ ) ;
151+
152+ it (
153+ "should return the latest version after an update via channels get-message" ,
154+ { timeout : 60000 } ,
155+ async ( ) => {
156+ setupTestFailureHandler (
157+ "should return the latest version after an update via channels get-message" ,
158+ ) ;
159+
160+ // Publish, update, then verify get-message returns the updated payload
161+ const updateChannel = getMutableChannelName ( "msg-get-after-update" ) ;
162+ const serial = await publishAndGetSerial ( updateChannel , "original" ) ;
163+
164+ const updateResult = await runCommand (
165+ [
166+ "channels" ,
167+ "update" ,
168+ updateChannel ,
169+ serial ,
170+ "edited-text" ,
171+ "--json" ,
172+ ] ,
173+ {
174+ env : { ABLY_API_KEY : E2E_API_KEY || "" } ,
175+ timeoutMs : 30000 ,
176+ } ,
177+ ) ;
178+ expect ( updateResult . exitCode ) . toBe ( 0 ) ;
179+
180+ // Retry get-message — update is eventually consistent
181+ let latestMessage : Record < string , unknown > | undefined ;
182+ for ( let attempt = 0 ; attempt < 10 ; attempt ++ ) {
183+ const getResult = await runCommand (
184+ [ "channels" , "get-message" , updateChannel , serial , "--json" ] ,
185+ {
186+ env : { ABLY_API_KEY : E2E_API_KEY || "" } ,
187+ timeoutMs : 30000 ,
188+ } ,
189+ ) ;
190+ if ( getResult . exitCode === 0 ) {
191+ const records = parseNdjsonLines ( getResult . stdout ) ;
192+ const parsed =
193+ records . find ( ( r ) => r . type === "result" ) ?? records [ 0 ] ;
194+ latestMessage = parsed . message as
195+ | Record < string , unknown >
196+ | undefined ;
197+ if ( latestMessage ?. data === "edited-text" ) break ;
198+ }
199+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
200+ }
201+
202+ expect ( latestMessage ) . toBeDefined ( ) ;
203+ expect ( latestMessage ! . data ) . toBe ( "edited-text" ) ;
204+ // The action must reflect that this is an update, not the original create
205+ expect ( latestMessage ! . action ) . toBe ( "message.update" ) ;
206+ // The version block must be populated and differ from the message serial
207+ expect ( latestMessage ! . version ) . toBeDefined ( ) ;
208+ const version = latestMessage ! . version as Record < string , unknown > ;
209+ expect ( version . serial ) . toBeDefined ( ) ;
210+ expect ( version . serial ) . not . toBe ( serial ) ;
211+ } ,
212+ ) ;
213+
214+ it (
215+ "should render human-readable output without --json" ,
216+ { timeout : 60000 } ,
217+ async ( ) => {
218+ setupTestFailureHandler (
219+ "should render human-readable output without --json" ,
220+ ) ;
221+
222+ const humanChannel = getMutableChannelName ( "msg-get-human" ) ;
223+ const serial = await publishAndGetSerial ( humanChannel , "human-text" ) ;
224+
225+ const result = await runCommand (
226+ [ "channels" , "get-message" , humanChannel , serial ] ,
227+ {
228+ env : { ABLY_API_KEY : E2E_API_KEY || "" } ,
229+ timeoutMs : 30000 ,
230+ } ,
231+ ) ;
232+
233+ expect ( result . exitCode ) . toBe ( 0 ) ;
234+ // Field labels rendered by formatMessagesOutput must appear
235+ expect ( result . stdout ) . toContain ( "Channel" ) ;
236+ expect ( result . stdout ) . toContain ( "Serial" ) ;
237+ expect ( result . stdout ) . toContain ( serial ) ;
238+ expect ( result . stdout ) . toContain ( "Data" ) ;
239+ expect ( result . stdout ) . toContain ( "human-text" ) ;
240+ } ,
241+ ) ;
242+
243+ it (
244+ "should fail with a non-zero exit code for an unknown serial" ,
245+ { timeout : 60000 } ,
246+ async ( ) => {
247+ setupTestFailureHandler (
248+ "should fail with a non-zero exit code for an unknown serial" ,
249+ ) ;
250+
251+ const result = await runCommand (
252+ [
253+ "channels" ,
254+ "get-message" ,
255+ channelName ,
256+ "0000000000-000@deadbeef:000" ,
257+ "--json" ,
258+ ] ,
259+ {
260+ env : { ABLY_API_KEY : E2E_API_KEY || "" } ,
261+ timeoutMs : 30000 ,
262+ } ,
263+ ) ;
264+
265+ expect ( result . exitCode ) . not . toBe ( 0 ) ;
266+
267+ const records = parseNdjsonLines ( result . stdout ) ;
268+ const errorRecord = records . find ( ( r ) => r . type === "error" ) ;
269+ expect ( errorRecord ) . toBeDefined ( ) ;
270+ expect ( errorRecord ! . success ) . toBe ( false ) ;
271+ } ,
272+ ) ;
273+
116274 it (
117275 "should delete a message via channels delete" ,
118276 { timeout : 60000 } ,
0 commit comments