From 93dbaed661d742aa239d1ed8df02ee7abf0c6426 Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 31 Mar 2026 13:37:22 +0100 Subject: [PATCH 1/4] Annotations missed count from response where appropriate. Same with the reaction type/name --- src/commands/channels/annotations/delete.ts | 7 ++++++- src/commands/channels/annotations/publish.ts | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/commands/channels/annotations/delete.ts b/src/commands/channels/annotations/delete.ts index 79f04772..bc2c89cf 100644 --- a/src/commands/channels/annotations/delete.ts +++ b/src/commands/channels/annotations/delete.ts @@ -8,6 +8,7 @@ import { validateAnnotationParams, } from "../../../utils/annotations.js"; import { + formatLabel, formatProgress, formatResource, formatSuccess, @@ -105,7 +106,7 @@ export default class ChannelsAnnotationsDelete extends AblyBaseCommand { channel: channelName, serial, type, - name: flags.name, + ...(flags.name === undefined ? {} : { name: flags.name }), }, }, flags, @@ -116,6 +117,10 @@ export default class ChannelsAnnotationsDelete extends AblyBaseCommand { `Annotation deleted on message ${formatResource(serial)} in channel ${formatResource(channelName)}.`, ), ); + this.log(` ${formatLabel("Type")} ${formatResource(type)}`); + if (flags.name !== undefined) { + this.log(` ${formatLabel("Name")} ${formatResource(flags.name)}`); + } } } catch (error) { this.fail(error, flags, "annotationDelete", { diff --git a/src/commands/channels/annotations/publish.ts b/src/commands/channels/annotations/publish.ts index 7a7e49a1..12d01444 100644 --- a/src/commands/channels/annotations/publish.ts +++ b/src/commands/channels/annotations/publish.ts @@ -8,6 +8,7 @@ import { validateAnnotationParams, } from "../../../utils/annotations.js"; import { + formatLabel, formatProgress, formatResource, formatSuccess, @@ -118,7 +119,12 @@ export default class ChannelsAnnotationsPublish extends AblyBaseCommand { channel: channelName, serial, type, - name: flags.name, + ...(flags.name === undefined ? {} : { name: flags.name }), + ...(flags.count === undefined ? {} : { count: flags.count }), + ...(flags.data === undefined ? {} : { data: flags.data }), + ...(flags.encoding === undefined + ? {} + : { encoding: flags.encoding }), }, }, flags, @@ -129,6 +135,16 @@ export default class ChannelsAnnotationsPublish extends AblyBaseCommand { `Annotation published on message ${formatResource(serial)} in channel ${formatResource(channelName)}.`, ), ); + this.log(` ${formatLabel("Type")} ${formatResource(type)}`); + if (flags.name !== undefined) { + this.log(` ${formatLabel("Name")} ${formatResource(flags.name)}`); + } + + if (flags.count !== undefined) { + this.log( + ` ${formatLabel("Count")} ${formatResource(String(flags.count))}`, + ); + } } } catch (error) { this.fail(error, flags, "annotationPublish", { From 5197a7b0a2a70615fcf47cc661dfc7cc545f885d Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 31 Mar 2026 13:55:10 +0100 Subject: [PATCH 2/4] Fix annotation output: omit undefined fields, add field parity, use parsed data in JSON --- src/commands/channels/annotations/delete.ts | 2 +- src/commands/channels/annotations/publish.ts | 21 ++++- .../channels/annotations/delete.test.ts | 38 +++++++++ .../channels/annotations/publish.test.ts | 85 +++++++++++++++++++ 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/commands/channels/annotations/delete.ts b/src/commands/channels/annotations/delete.ts index bc2c89cf..470b64cb 100644 --- a/src/commands/channels/annotations/delete.ts +++ b/src/commands/channels/annotations/delete.ts @@ -95,7 +95,7 @@ export default class ChannelsAnnotationsDelete extends AblyBaseCommand { channel: channelName, serial, type, - name: flags.name, + ...(flags.name === undefined ? {} : { name: flags.name }), }, ); diff --git a/src/commands/channels/annotations/publish.ts b/src/commands/channels/annotations/publish.ts index 12d01444..0dc977db 100644 --- a/src/commands/channels/annotations/publish.ts +++ b/src/commands/channels/annotations/publish.ts @@ -109,7 +109,12 @@ export default class ChannelsAnnotationsPublish extends AblyBaseCommand { "annotationPublish", "annotationPublished", `Published annotation on message ${serial} in channel ${channelName}`, - { channel: channelName, serial, type, name: flags.name }, + { + channel: channelName, + serial, + type, + ...(flags.name === undefined ? {} : { name: flags.name }), + }, ); if (this.shouldOutputJson(flags)) { @@ -121,7 +126,9 @@ export default class ChannelsAnnotationsPublish extends AblyBaseCommand { type, ...(flags.name === undefined ? {} : { name: flags.name }), ...(flags.count === undefined ? {} : { count: flags.count }), - ...(flags.data === undefined ? {} : { data: flags.data }), + ...(annotation.data === undefined + ? {} + : { data: annotation.data }), ...(flags.encoding === undefined ? {} : { encoding: flags.encoding }), @@ -145,6 +152,16 @@ export default class ChannelsAnnotationsPublish extends AblyBaseCommand { ` ${formatLabel("Count")} ${formatResource(String(flags.count))}`, ); } + + if (flags.data !== undefined) { + this.log(` ${formatLabel("Data")} ${formatResource(flags.data)}`); + } + + if (flags.encoding !== undefined) { + this.log( + ` ${formatLabel("Encoding")} ${formatResource(flags.encoding)}`, + ); + } } } catch (error) { this.fail(error, flags, "annotationPublish", { diff --git a/test/unit/commands/channels/annotations/delete.test.ts b/test/unit/commands/channels/annotations/delete.test.ts index c6b2a7d5..72180271 100644 --- a/test/unit/commands/channels/annotations/delete.test.ts +++ b/test/unit/commands/channels/annotations/delete.test.ts @@ -124,6 +124,44 @@ describe("channels:annotations:delete command", () => { expect(result.annotation).toHaveProperty("channel", "test-channel"); expect(result.annotation).toHaveProperty("serial", "serial-001"); }); + + it("should not include name in JSON output when not provided", async () => { + const records = await captureJsonLogs(async () => { + await runCommand( + [ + "channels:annotations:delete", + "test-channel", + "serial-001", + "reactions:flag.v1", + "--json", + ], + import.meta.url, + ); + }); + + const result = records[0]; + expect(result.annotation).not.toHaveProperty("name"); + }); + + it("should include name in JSON output when provided", async () => { + const records = await captureJsonLogs(async () => { + await runCommand( + [ + "channels:annotations:delete", + "test-channel", + "serial-001", + "reactions:flag.v1", + "--name", + "thumbsup", + "--json", + ], + import.meta.url, + ); + }); + + const result = records[0]; + expect(result.annotation).toHaveProperty("name", "thumbsup"); + }); }); describe("error handling", () => { diff --git a/test/unit/commands/channels/annotations/publish.test.ts b/test/unit/commands/channels/annotations/publish.test.ts index d09882de..6b4543d2 100644 --- a/test/unit/commands/channels/annotations/publish.test.ts +++ b/test/unit/commands/channels/annotations/publish.test.ts @@ -272,6 +272,91 @@ describe("channels:annotations:publish command", () => { expect(result.annotation).toHaveProperty("channel", "test-channel"); expect(result.annotation).toHaveProperty("serial", "serial-001"); }); + + it("should not include undefined fields in JSON output", async () => { + const { stdout } = await runCommand( + [ + "channels:annotations:publish", + "test-channel", + "serial-001", + "reactions:flag.v1", + "--json", + ], + import.meta.url, + ); + + const result = JSON.parse(stdout); + expect(result.annotation).not.toHaveProperty("name"); + expect(result.annotation).not.toHaveProperty("count"); + expect(result.annotation).not.toHaveProperty("data"); + expect(result.annotation).not.toHaveProperty("encoding"); + }); + + it("should include optional fields in JSON output when provided", async () => { + const { stdout } = await runCommand( + [ + "channels:annotations:publish", + "test-channel", + "serial-001", + "reactions:multiple.v1", + "--name", + "thumbsup", + "--count", + "3", + "--data", + "test-data", + "--encoding", + "utf8", + "--json", + ], + import.meta.url, + ); + + const result = JSON.parse(stdout); + expect(result.annotation).toHaveProperty("name", "thumbsup"); + expect(result.annotation).toHaveProperty("count", 3); + expect(result.annotation).toHaveProperty("data", "test-data"); + expect(result.annotation).toHaveProperty("encoding", "utf8"); + }); + + it("should output parsed JSON data in JSON output", async () => { + const { stdout } = await runCommand( + [ + "channels:annotations:publish", + "test-channel", + "serial-001", + "reactions:flag.v1", + "--data", + '{"foo":"bar"}', + "--json", + ], + import.meta.url, + ); + + const result = JSON.parse(stdout); + expect(result.annotation.data).toEqual({ foo: "bar" }); + }); + + it("should show data and encoding in non-JSON output when provided", async () => { + const { stdout } = await runCommand( + [ + "channels:annotations:publish", + "test-channel", + "serial-001", + "reactions:flag.v1", + "--data", + "test-data", + "--encoding", + "utf8", + ], + import.meta.url, + ); + + expect(stdout).toContain("Data"); + expect(stdout).toContain("test-data"); + expect(stdout).toContain("Encoding"); + expect(stdout).toContain("utf8"); + }); }); describe("error handling", () => { From 646da6b79990f221ac4340fea62e93ee23426b29 Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 31 Mar 2026 14:14:42 +0100 Subject: [PATCH 3/4] Use parsed annotation data in non-JSON output for parity with JSON mode, add non-JSON delete test --- src/commands/channels/annotations/publish.ts | 8 ++++++-- .../channels/annotations/delete.test.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/commands/channels/annotations/publish.ts b/src/commands/channels/annotations/publish.ts index 0dc977db..1623d484 100644 --- a/src/commands/channels/annotations/publish.ts +++ b/src/commands/channels/annotations/publish.ts @@ -153,8 +153,12 @@ export default class ChannelsAnnotationsPublish extends AblyBaseCommand { ); } - if (flags.data !== undefined) { - this.log(` ${formatLabel("Data")} ${formatResource(flags.data)}`); + if (annotation.data !== undefined) { + const displayData = + typeof annotation.data === "string" + ? annotation.data + : JSON.stringify(annotation.data, null, 2); + this.log(` ${formatLabel("Data")} ${formatResource(displayData)}`); } if (flags.encoding !== undefined) { diff --git a/test/unit/commands/channels/annotations/delete.test.ts b/test/unit/commands/channels/annotations/delete.test.ts index 72180271..b191dcf9 100644 --- a/test/unit/commands/channels/annotations/delete.test.ts +++ b/test/unit/commands/channels/annotations/delete.test.ts @@ -125,6 +125,25 @@ describe("channels:annotations:delete command", () => { expect(result.annotation).toHaveProperty("serial", "serial-001"); }); + it("should show Type and Name labels in non-JSON output", async () => { + const { stdout } = await runCommand( + [ + "channels:annotations:delete", + "test-channel", + "serial-001", + "reactions:distinct.v1", + "--name", + "thumbsup", + ], + import.meta.url, + ); + + expect(stdout).toContain("Type"); + expect(stdout).toContain("reactions:distinct.v1"); + expect(stdout).toContain("Name"); + expect(stdout).toContain("thumbsup"); + }); + it("should not include name in JSON output when not provided", async () => { const records = await captureJsonLogs(async () => { await runCommand( From 6dff08a85323c9ca7a5ffb5c211d42ab8f9d0a30 Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 31 Mar 2026 16:29:41 +0100 Subject: [PATCH 4/4] Add missing SDK fields (encoding, messageSerial, extras) to annotation output, use conditional spread for optional JSON fields --- src/commands/channels/annotations/get.ts | 3 + .../channels/annotations/subscribe.ts | 22 +++++- src/utils/output.ts | 19 +++++ .../commands/channels/annotations/get.test.ts | 31 ++++++++ .../channels/annotations/subscribe.test.ts | 73 +++++++++++++++++++ 5 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/commands/channels/annotations/get.ts b/src/commands/channels/annotations/get.ts index 6d715daf..fab54087 100644 --- a/src/commands/channels/annotations/get.ts +++ b/src/commands/channels/annotations/get.ts @@ -110,6 +110,9 @@ export default class ChannelsAnnotationsGet extends AblyBaseCommand { count: annotation.count, serial: annotation.serial, data: annotation.data, + encoding: annotation.encoding, + messageSerial: annotation.messageSerial, + extras: annotation.extras, indexPrefix: `${formatIndex(index + 1)} ${formatTimestamp(formatMessageTimestamp(ts))}`, }; }, diff --git a/src/commands/channels/annotations/subscribe.ts b/src/commands/channels/annotations/subscribe.ts index 838eb1c0..856926f2 100644 --- a/src/commands/channels/annotations/subscribe.ts +++ b/src/commands/channels/annotations/subscribe.ts @@ -117,11 +117,22 @@ export default class ChannelsAnnotationsSubscribe extends AblyBaseCommand { channel: channelName, annotationType: annotation.type, action: annotation.action, - name: annotation.name, - clientId: annotation.clientId, - count: annotation.count, serial: annotation.serial, - data: annotation.data, + messageSerial: annotation.messageSerial, + ...(annotation.name === undefined ? {} : { name: annotation.name }), + ...(annotation.clientId === undefined + ? {} + : { clientId: annotation.clientId }), + ...(annotation.count === undefined + ? {} + : { count: annotation.count }), + ...(annotation.data === undefined ? {} : { data: annotation.data }), + ...(annotation.encoding === undefined + ? {} + : { encoding: annotation.encoding }), + ...(annotation.extras === undefined + ? {} + : { extras: annotation.extras }), }; this.logCliEvent( @@ -149,6 +160,9 @@ export default class ChannelsAnnotationsSubscribe extends AblyBaseCommand { count: annotation.count, serial: annotation.serial, data: annotation.data, + encoding: annotation.encoding, + messageSerial: annotation.messageSerial, + extras: annotation.extras, }; this.log(formatAnnotationsOutput([displayFields])); this.log(""); diff --git a/src/utils/output.ts b/src/utils/output.ts index 88f11801..087f2587 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -298,6 +298,9 @@ export interface AnnotationDisplayFields { count?: number; serial?: string; data?: unknown; + encoding?: string; + messageSerial?: string; + extras?: unknown; indexPrefix?: string; } @@ -359,6 +362,22 @@ export function formatAnnotationsOutput( } } + if (ann.encoding) { + lines.push(`${formatLabel("Encoding")} ${ann.encoding}`); + } + + if (ann.messageSerial) { + lines.push(`${formatLabel("Message Serial")} ${ann.messageSerial}`); + } + + if (ann.extras !== undefined && ann.extras !== null) { + if (isJsonData(ann.extras)) { + lines.push(`${formatLabel("Extras")}`, formatMessageData(ann.extras)); + } else { + lines.push(`${formatLabel("Extras")} ${String(ann.extras)}`); + } + } + blocks.push(lines.join("\n")); } diff --git a/test/unit/commands/channels/annotations/get.test.ts b/test/unit/commands/channels/annotations/get.test.ts index f7806241..3d457a3c 100644 --- a/test/unit/commands/channels/annotations/get.test.ts +++ b/test/unit/commands/channels/annotations/get.test.ts @@ -163,6 +163,37 @@ describe("channels:annotations:get command", () => { expect(stdout).toContain("Data:"); expect(stdout).toContain("extra"); }); + + it("should display encoding, messageSerial, and extras when present", async () => { + const mock = getMockAblyRest(); + const channel = mock.channels._getChannel("test-channel"); + channel.annotations.get.mockResolvedValue({ + items: [ + { + id: "ann-extras-001", + type: "reactions:flag.v1", + name: "thumbsup", + serial: "ann-serial-001", + messageSerial: "msg-serial-001", + timestamp: 1700000000000, + encoding: "utf8", + extras: { headers: { key: "value" } }, + }, + ], + }); + + const { stdout } = await runCommand( + ["channels:annotations:get", "test-channel", "serial-001"], + import.meta.url, + ); + + expect(stdout).toContain("Encoding:"); + expect(stdout).toContain("utf8"); + expect(stdout).toContain("Message Serial:"); + expect(stdout).toContain("msg-serial-001"); + expect(stdout).toContain("Extras:"); + expect(stdout).toContain("headers"); + }); }); describe("error handling", () => { diff --git a/test/unit/commands/channels/annotations/subscribe.test.ts b/test/unit/commands/channels/annotations/subscribe.test.ts index 8d54b10b..c0650ed7 100644 --- a/test/unit/commands/channels/annotations/subscribe.test.ts +++ b/test/unit/commands/channels/annotations/subscribe.test.ts @@ -239,6 +239,79 @@ describe("channels:annotations:subscribe command", () => { expect(stdout).toContain("thumbs-up"); }); + it("should display encoding, messageSerial, and extras when present", async () => { + const commandPromise = runCommand( + ["channels:annotations:subscribe", "test-channel"], + import.meta.url, + ); + + await vi.waitFor(() => { + expect(mockAnnotationCallback).not.toBeNull(); + }); + + mockAnnotationCallback!({ + id: "ann-extras-001", + action: "annotation.create", + type: "reactions:flag.v1", + name: "thumbsup", + serial: "ann-serial-001", + messageSerial: "msg-serial-001", + timestamp: Date.now(), + encoding: "utf8", + extras: { headers: { key: "value" } }, + }); + + const { stdout } = await commandPromise; + + expect(stdout).toContain("Encoding:"); + expect(stdout).toContain("utf8"); + expect(stdout).toContain("Message Serial:"); + expect(stdout).toContain("msg-serial-001"); + expect(stdout).toContain("Extras:"); + expect(stdout).toContain("headers"); + }); + + it("should omit optional fields from JSON when undefined", async () => { + const records = await captureJsonLogs(async () => { + const commandPromise = runCommand( + ["channels:annotations:subscribe", "test-channel", "--json"], + import.meta.url, + ); + + await vi.waitFor(() => { + expect(mockAnnotationCallback).not.toBeNull(); + }); + + mockAnnotationCallback!({ + id: "ann-minimal-001", + action: "annotation.create", + type: "reactions:flag.v1", + serial: "ann-001", + messageSerial: "msg-001", + timestamp: Date.now(), + }); + + await commandPromise; + }); + + const events = records.filter( + (r) => + r.type === "event" && + (r as Record).annotation && + ((r as Record).annotation as Record) + .id === "ann-minimal-001", + ); + expect(events.length).toBeGreaterThan(0); + const annotation = (events[0] as Record) + .annotation as Record; + expect(annotation).not.toHaveProperty("name"); + expect(annotation).not.toHaveProperty("clientId"); + expect(annotation).not.toHaveProperty("count"); + expect(annotation).not.toHaveProperty("data"); + expect(annotation).not.toHaveProperty("encoding"); + expect(annotation).not.toHaveProperty("extras"); + }); + it("should pass --type filter to subscribe", async () => { const mock = getMockAblyRealtime(); const channel = mock.channels._getChannel("test-channel");