Skip to content

Commit a08e9f2

Browse files
committed
Updated history and subscribe message fields with missing data fields
1 parent 7c78e27 commit a08e9f2

5 files changed

Lines changed: 332 additions & 94 deletions

File tree

SUBSCRIBE_HISTORY_FORMATTING_PLAN.md

Lines changed: 196 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,126 @@
11
# Implementation Plan: Consistent Message Output for channels subscribe & history
22

3+
## Implementation Status: ✅ COMPLETE (2026-03-09)
4+
5+
All steps have been implemented and verified:
6+
- `pnpm prepare` — ✅ builds successfully
7+
- `pnpm exec eslint .` — ✅ 0 errors in changed files
8+
- `pnpm test:unit` — ✅ 122/122 test files pass (1115 tests passed, 6 pre-existing skips)
9+
10+
### Files changed
11+
12+
| File | Status | What was done |
13+
|------|--------|---------------|
14+
| `src/utils/output.ts` | ✅ Done | Added `MessageDisplayFields` interface (timestamp is `number` — raw ms), `formatMessagesOutput()`, `toMessageJson()`. Imports `isJsonData`/`formatMessageData` from `json-formatter.ts`. |
15+
| `src/commands/channels/subscribe.ts` | ✅ Done | Uses `formatMessagesOutput([msgFields])` + `toMessageJson(msgFields)`. Passes `message.timestamp` as raw number (no `formatMessageTimestamp` conversion). Added `serial` field. Removed `connectionId`/`encoding` from user-facing JSON. Sequence numbers added separately to JSON. |
16+
| `src/commands/channels/history.ts` | ✅ Done | Uses `formatMessagesOutput(displayMessages)` + `displayMessages.map((msg) => toMessageJson(msg))`. Passes `message.timestamp` as raw number. Plain array JSON output. Added `channel`/`serial`/`indexPrefix`. Preserved `limitWarning`. Kept current error handling. |
17+
| `.claude/CLAUDE.md` | ✅ Done | Added "Message display" conventions subsection with raw ms timestamp convention. Updated "History output" bullet to note `channels history` exception. |
18+
| `test/unit/commands/channels/subscribe.test.ts` | ✅ No changes needed | Tests already expected the new format (survived merge). |
19+
| `test/unit/commands/channels/history.test.ts` | ✅ No changes needed | Tests already expected the new format (survived merge). |
20+
21+
### Lint fixes applied
22+
- `output.ts`: Fixed prettier formatting for ternary expression, combined consecutive `Array#push()` calls per `unicorn/no-array-push-push`, fixed prettier formatting for function parameter
23+
- `history.ts`: Wrapped `toMessageJson` in arrow function per `unicorn/no-array-callback-reference`
24+
25+
### Deep cross-check findings (plan vs codebase vs CLAUDE.md)
26+
27+
These are additional issues found during a thorough review that the original plan doesn't fully address:
28+
29+
#### 1. CLAUDE.md "History output" convention divergence
30+
31+
**CLAUDE.md line 191** currently says:
32+
> Use `[index] timestamp` ordering: `` `${chalk.dim(`[${index + 1}]`)} ${formatTimestamp(timestamp)}` ``. Consistent across all history commands (channels, logs, connection-lifecycle, push).
33+
34+
The plan changes `channels history` to put `[index]` on its own line and `Timestamp:` as a labeled field — **intentionally diverging** from the other history commands (`logs history`, `logs push history`, `logs connection-lifecycle history`) which still use the old `[index] [timestamp]` single-line format.
35+
36+
**Action needed**: The CLAUDE.md update (Step 5) must clarify that `channels history` now uses the new `formatMessagesOutput` convention while other history commands retain the old `[index] [timestamp]` pattern. The existing "History output" bullet should be updated to note this exception.
37+
38+
#### 2. `formatTimestamp()` returns `[timestamp]` with brackets — plan uses bare timestamp
39+
40+
The existing `formatTimestamp()` in `src/utils/output.ts` returns `chalk.dim(`[${ts}]`)` — dim text **with square brackets**. The plan's target format shows `Timestamp: 2026-03-06T05:13:09.160Z` (no brackets, no dim on the value).
41+
42+
**Action needed**: `formatMessagesOutput()` must NOT use `formatTimestamp()` for the timestamp value. It should display the raw ISO string directly. The plan's pseudocode (line 285) is correct: `${chalk.dim("Timestamp:")} ${timestamp}`. Step 3 correctly says to remove the `formatTimestamp` import from subscribe.
43+
44+
#### 3. `history.ts` error handling — `handleCommandError` would break error test
45+
46+
The current `history.ts` (lines 127-134) uses a manual try/catch with `this.jsonError()` + `this.error()`. Per CLAUDE.md convention, it should use `this.handleCommandError()`. However, the current code prefixes the error with `"Error retrieving channel history: "`, and the test at line 233 of `history.test.ts` asserts `error?.message` contains `"Error retrieving channel history"`.
47+
48+
If we switch to `this.handleCommandError()`, it would pass the raw error message (e.g., `"API error"`) without the prefix, **breaking the test**.
49+
50+
**Decision**: Keep the current error handling pattern for now (don't switch to `handleCommandError`). This is a minor inconsistency with CLAUDE.md convention but avoids an unnecessary test change. The error handling improvement can be done in a separate PR if desired.
51+
52+
#### 4. `history.ts` `limitWarning` — must be preserved
53+
54+
The current code shows a limit warning at the end (`limitWarning(messages.length, flags.limit, "messages")`). The plan's Step 4 doesn't explicitly mention keeping it.
55+
56+
**Action needed**: Step 4 must preserve the `limitWarning` call after `formatMessagesOutput`. Add it after the `this.log(formatMessagesOutput(...))` call.
57+
58+
#### 5. `formatMessagesOutput` needs `isJsonData` for data display
59+
60+
The plan says data should be inline for simple values and on a new line for JSON objects/arrays. The existing `formatMessageData()` from `src/utils/json-formatter.ts` handles colorized formatting but doesn't distinguish inline vs block. The `isJsonData()` function from the same file can be used to decide.
61+
62+
**Action needed**: `formatMessagesOutput` should use `isJsonData(data)` to decide:
63+
- Simple data: `${chalk.dim("Data:")} ${String(data)}` (inline)
64+
- JSON data: `${chalk.dim("Data:")}\n${formatMessageData(data)}` (block)
65+
66+
Import both `isJsonData` and `formatMessageData` from `src/utils/json-formatter.ts` into `src/utils/output.ts`.
67+
68+
#### 6. Test coverage gaps (non-blocking)
69+
70+
- **Subscribe test**: No assertion for `"Serial:"` — acceptable since mock doesn't include `serial` and the field is optional (omitted when undefined).
71+
- **Subscribe JSON test**: Only checks `error` is undefined and `stdout` is defined — doesn't verify JSON structure. Could be improved but not blocking.
72+
- **History test**: Mock data doesn't include `serial` — acceptable for same reason.
73+
74+
#### 7. E2E compatibility — verified safe
75+
76+
- `channel-subscribe-e2e.test.ts` uses `readySignal = "Subscribed to channel"` — the subscribe command still outputs `success("Subscribed to channel: ...")` which contains this string. ✅ Safe.
77+
- `channel-occupancy-e2e.test.ts` also uses `readySignal: "Subscribed to channel"` — same reasoning. ✅ Safe.
78+
- Data check uses `output.includes("Subscribe E2E Test")` — the message data will still appear in the new format. ✅ Safe.
79+
80+
#### 8. `serial` field confirmed in Ably SDK types
81+
82+
The Ably SDK `Message` interface has `serial?: string` (optional). For realtime `InboundMessage`, `serial: string` (required). Both subscribe (realtime) and history (REST) messages will have this field available.
83+
84+
#### 9. `logCliEvent` in subscribe — keep full message details
85+
86+
The current subscribe code (line 212-218) passes a `messageEvent` object to `logCliEvent` that includes `connectionId`, `encoding`, and `sequence`. This is internal verbose logging, not user-facing output. The `logCliEvent` call should continue to pass the full message details for debugging purposes. Only the user-facing output (human-readable and `--json`) should use the new `formatMessagesOutput`/`toMessageJson` helpers.
87+
88+
**Action needed**: When updating subscribe.ts, keep the `logCliEvent` call with its current `messageEvent` object (or update it to include `serial` too). The `logCliEvent` data is separate from the user-facing output.
89+
90+
#### 10. `sequence` field in subscribe JSON output
91+
92+
The current code adds `sequence: this.sequenceCounter` to the JSON output when `--sequence-numbers` is used. The plan's `toMessageJson` doesn't include `sequence`. Since `sequence` is subscribe-specific (not part of the shared `MessageDisplayFields`), it should be added to the JSON output separately in subscribe.ts after calling `toMessageJson`:
93+
94+
```typescript
95+
const jsonMsg = toMessageJson(msgFields);
96+
if (flags["sequence-numbers"]) {
97+
jsonMsg.sequence = this.sequenceCounter;
98+
}
99+
this.log(this.formatJsonOutput(jsonMsg, flags));
100+
```
101+
102+
**Action needed**: Step 3 should note that `sequence` is added to JSON output separately, not via `toMessageJson`.
103+
104+
### Implementation order (updated)
105+
106+
To fix all 4 test failures, implement these steps:
107+
108+
1. **Step 1**: Add `MessageDisplayFields`, `formatMessagesOutput()`, `toMessageJson()` to `src/utils/output.ts`
109+
- Import `isJsonData`, `formatMessageData` from `json-formatter.ts`
110+
- Do NOT use `formatTimestamp()` — display raw ISO string
111+
2. **Step 2**: Update `src/commands/channels/subscribe.ts` to use the new helpers
112+
- Remove `formatTimestamp` import (no longer needed)
113+
- Add `serial` from `message.serial`
114+
3. **Step 3**: Update `src/commands/channels/history.ts` to use the new helpers
115+
- Preserve `limitWarning` after `formatMessagesOutput`
116+
- Keep current error handling (don't switch to `handleCommandError` — would break error test)
117+
4. **Step 5** (recommended): Update `.claude/CLAUDE.md` with message display conventions
118+
- Note the divergence from the old `[index] timestamp` pattern for `channels history`
119+
120+
Step 6 (tests) is already done — the tests are correct and just need the source to match.
121+
122+
---
123+
3124
## Original Request
4125

5126
Currently when running `bin/run.js channels subscribe test`, the output is:
@@ -255,9 +376,13 @@ Changes from current:
255376

256377
### 1. Add `formatMessagesOutput` helper to `src/utils/output.ts`
257378

258-
Create a single shared function that accepts an array of messages:
379+
Create a single shared function that accepts an array of messages.
380+
381+
**Important**: Import `isJsonData` and `formatMessageData` from `src/utils/json-formatter.ts`. Do NOT use `formatTimestamp()` (which wraps in `[brackets]`) — display the raw ISO string directly.
259382

260383
```typescript
384+
import { isJsonData, formatMessageData } from "./json-formatter.js";
385+
261386
export interface MessageDisplayFields {
262387
channel: string;
263388
clientId?: string;
@@ -282,13 +407,15 @@ The function:
282407
- Returns `"No messages found."` for empty arrays
283408
- For each message, builds lines:
284409
- (if indexPrefix) `${chalk.dim(indexPrefix)}` on its own line
285-
- `${sequencePrefix || ""}${chalk.dim("Timestamp:")} ${timestamp}`
410+
- `${sequencePrefix || ""}${chalk.dim("Timestamp:")} ${timestamp}` — raw ISO string, NOT `formatTimestamp()`
286411
- `${chalk.dim("Channel:")} ${resource(channel)}`
287412
- `${chalk.dim("Event:")} ${chalk.yellow(event)}`
288413
- (if id) `${chalk.dim("ID:")} ${id}`
289414
- (if clientId) `${chalk.dim("Client ID:")} ${chalk.blue(clientId)}`
290415
- (if serial) `${chalk.dim("Serial:")} ${serial}`
291-
- Data: `${chalk.dim("Data:")} ${value}` for simple, or `${chalk.dim("Data:")}` + formatted JSON block on next lines
416+
- Data: Use `isJsonData(data)` to decide:
417+
- Simple data: `${chalk.dim("Data:")} ${String(data)}` (inline on same line)
418+
- JSON data: `${chalk.dim("Data:")}\n${formatMessageData(data)}` (label on its own line, formatted JSON below)
292419
- Joins messages with `\n\n` (blank line separator)
293420
294421
### 2. Add `toMessageJson` helper to `src/utils/output.ts`
@@ -322,33 +449,79 @@ Returns:
322449

323450
### 3. Update `src/commands/channels/subscribe.ts`
324451

325-
- Import `formatMessagesOutput`, `toMessageJson`
326-
- Remove imports of `formatJson`, `isJsonData`, `formatTimestamp`
452+
- Import `formatMessagesOutput`, `toMessageJson` from `../../utils/output.js`
453+
- Remove imports of `formatMessageData` from `../../utils/json-formatter.js`
454+
- Remove imports of `formatTimestamp` from `../../utils/output.js` (no longer needed`formatMessagesOutput` handles timestamp display)
455+
- Keep imports of `formatMessageTimestamp`, `listening`, `progress`, `resource`, `success` from `../../utils/output.js`
456+
- Remove `chalk` import (no longer needed for message formatting)
327457
- Add `serial` to the message fields (from `message.serial`)
328-
- Build a `MessageDisplayFields` object from the Ably message
458+
- Build a `MessageDisplayFields` object from the Ably message:
459+
```typescript
460+
const msgFields: MessageDisplayFields = {
461+
channel: channel.name,
462+
clientId: message.clientId,
463+
data: message.data,
464+
event: message.name || "(none)",
465+
id: message.id,
466+
serial: message.serial,
467+
timestamp,
468+
...(flags["sequence-numbers"] ? { sequencePrefix: `${chalk.dim(`[${this.sequenceCounter}]`)} ` } : {}),
469+
};
470+
```
471+
Note: If `sequencePrefix` uses chalk, keep the chalk import. Otherwise remove it.
329472
- Human output: `this.log(formatMessagesOutput([msgFields]))`
330-
- JSON output: `this.log(this.formatJsonOutput(toMessageJson(msgFields), flags))`
331-
- Remove `connectionId` and `encoding` from JSON output
473+
- JSON output: Build from `toMessageJson`, then add `sequence` if `--sequence-numbers`:
474+
```typescript
475+
const jsonMsg = toMessageJson(msgFields);
476+
if (flags["sequence-numbers"]) {
477+
jsonMsg.sequence = this.sequenceCounter;
478+
}
479+
this.log(this.formatJsonOutput(jsonMsg, flags));
480+
```
481+
- Remove `connectionId` and `encoding` from user-facing JSON output (the `toMessageJson` helper handles this)
482+
- Keep the `logCliEvent` call with full message details (including `connectionId`, `encoding`, `serial`) — this is internal verbose logging, not user-facing output
332483

333484
### 4. Update `src/commands/channels/history.ts`
334485

335-
- Import `formatMessagesOutput`, `toMessageJson` from output utils
486+
- Import `formatMessagesOutput`, `toMessageJson`, `MessageDisplayFields` from `../../utils/output.js`
487+
- Remove imports of `formatMessageData` from `../../utils/json-formatter.js`
488+
- Remove imports of `countLabel`, `formatTimestamp` from `../../utils/output.js` (no longer needed)
489+
- Keep imports of `formatMessageTimestamp`, `limitWarning`, `resource` from `../../utils/output.js`
336490
- Build a `MessageDisplayFields[]` array from history results:
337-
- `channel` from `args.channel`
338-
- `event` from `message.name || "(none)"`
339-
- `serial` from `message.serial`
340-
- `timestamp` as ISO string (convert from milliseconds)
341-
- `indexPrefix` as `[${index + 1}]` (per CLAUDE.md convention for history output)
491+
```typescript
492+
const displayMessages: MessageDisplayFields[] = messages.map((message, index) => ({
493+
channel: channelName,
494+
clientId: message.clientId,
495+
data: message.data,
496+
event: message.name || "(none)",
497+
id: message.id,
498+
indexPrefix: `[${index + 1}]`,
499+
serial: message.serial,
500+
timestamp: formatMessageTimestamp(message.timestamp),
501+
}));
502+
```
342503
- Human output: `this.log(formatMessagesOutput(displayMessages))`
343504
- The "No messages found" case is handled by `formatMessagesOutput` returning the appropriate string
344505
- Remove the for-loop and the "Found N messages" header (index is now part of `MessageDisplayFields`)
506+
- **Preserve `limitWarning`** after `formatMessagesOutput`:
507+
```typescript
508+
const warning = limitWarning(messages.length, flags.limit, "messages");
509+
if (warning) this.log(warning);
510+
```
345511
- JSON output: `this.log(this.formatJsonOutput(displayMessages.map(toMessageJson), flags))`
346512
- Output a plain array instead of `{ messages: [...] }` wrapper
513+
- **Keep current error handling**do NOT switch to `handleCommandError()` because the test expects the `"Error retrieving channel history: "` prefix (see finding #3 above). The current `this.jsonError()` + `this.error()` pattern is fine for this PR.
347514

348515
### 5. Update `.claude/CLAUDE.md`
349516

350-
Add a "Message display" subsection under "CLI Output & Flag Conventions". Note: The existing "History output" convention (line 219) with `[index] timestamp` ordering is preservedhistory commands continue to use index prefixes.
517+
Add a "Message display" subsection under "CLI Output & Flag Conventions". Also update the existing "History output" bullet to note that `channels history` now uses the new format.
518+
519+
Update the existing "History output" bullet (line 191):
520+
```markdown
521+
- **History output**: Use `[index] timestamp` ordering: `` `${chalk.dim(`[${index + 1}]`)} ${formatTimestamp(timestamp)}` ``. Consistent across log history commands (logs, connection-lifecycle, push). Exception: `channels history` uses `formatMessagesOutput()` with `indexPrefix` for richer field display.
522+
```
351523

524+
Add new subsection:
352525
```markdown
353526
### Message display (channels subscribe, channels history, etc.)
354527
- Use `formatMessagesOutput()` from `src/utils/output.ts` for all message rendering
@@ -358,9 +531,10 @@ Add a "Message display" subsection under "CLI Output & Flag Conventions". Note:
358531
- History output includes `[index]` prefix per existing convention; subscribe does not (but supports `--sequence-numbers`)
359532
- Omit optional fields (ID, Client ID, Serial) if the value is undefined/null
360533
- Event always shown; use `(none)` when message has no event name
361-
- Data: inline for simple values, block for JSON objects/arrays
534+
- Data: inline for simple values, block for JSON objects/arrays (uses `isJsonData()` to decide)
362535
- Multiple messages separated by blank lines
363536
- JSON output uses consistent field names (`event` not `name`), ISO 8601 timestamps
537+
- `formatMessagesOutput` does NOT use `formatTimestamp()` (which adds `[brackets]`) — it displays raw ISO strings
364538
```
365539

366540
### 6. Update tests
@@ -381,9 +555,9 @@ The E2E test (`test/e2e/channels/channel-subscribe-e2e.test.ts`) only checks `ou
381555

382556
## Files Changed
383557

384-
1. `src/utils/output.ts`Add `formatMessagesOutput`, `toMessageJson`, and `MessageDisplayFields`
385-
2. `src/commands/channels/subscribe.ts`Use `formatMessagesOutput([msg])` + `toMessageJson(msg)`, add serial
386-
3. `src/commands/channels/history.ts`Use `formatMessagesOutput(messages)` + `messages.map(toMessageJson)`, add channel/serial/indexPrefix, plain array JSON
387-
4. `.claude/CLAUDE.md`Document message display conventions (note: existing history output convention with `[index]` is preserved)
388-
5. `test/unit/commands/channels/subscribe.test.ts`Update assertions for new format
389-
6. `test/unit/commands/channels/history.test.ts`Update assertions for new format + JSON structure
558+
1. `src/utils/output.ts`Add `MessageDisplayFields` interface, `formatMessagesOutput()`, `toMessageJson()`. Import `isJsonData`/`formatMessageData` from `json-formatter.ts`.
559+
2. `src/commands/channels/subscribe.ts`Use `formatMessagesOutput([msg])` + `toMessageJson(msg)`, add `serial`, remove `connectionId`/`encoding` from user-facing JSON, add `sequence` to JSON separately when `--sequence-numbers`, keep `logCliEvent` with full details, remove `formatTimestamp`/`formatMessageData` imports (keep `chalk` if `sequencePrefix` uses it).
560+
3. `src/commands/channels/history.ts`Use `formatMessagesOutput(messages)` + `messages.map(toMessageJson)`, add `channel`/`serial`/`indexPrefix`, plain array JSON, preserve `limitWarning`. Keep current error handling.
561+
4. `.claude/CLAUDE.md`Add "Message display" conventions subsection, update "History output" bullet to note `channels history` exception.
562+
5. `test/unit/commands/channels/subscribe.test.ts`Already updated (survived merge). No changes needed.
563+
6. `test/unit/commands/channels/history.test.ts`Already updated (survived merge). No changes needed.

0 commit comments

Comments
 (0)