Skip to content

Commit fbd392a

Browse files
committed
Implemented custom error message formatting logic for enabling channel rules
for annotations
1 parent 3be4b12 commit fbd392a

6 files changed

Lines changed: 173 additions & 12 deletions

File tree

src/base-command.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from "./utils/long-running.js";
2222
import isTestMode from "./utils/test-mode.js";
2323
import isWebCliMode from "./utils/web-mode.js";
24+
import { enhanceErrorMessage, errorMessage } from "./utils/errors.js";
2425

2526
// List of commands not allowed in web CLI mode - EXPORTED
2627
export const WEB_CLI_RESTRICTED_COMMANDS = [
@@ -805,7 +806,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
805806
} catch (error) {
806807
// Fallback to regular JSON.stringify
807808
this.debug(
808-
`Error using color-json: ${error instanceof Error ? error.message : String(error)}. Falling back to regular JSON.`,
809+
`Error using color-json: ${errorMessage(error)}. Falling back to regular JSON.`,
809810
);
810811
return JSON.stringify(data, null, 2);
811812
}
@@ -1446,7 +1447,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
14461447
try {
14471448
return JSON.parse(value.trim());
14481449
} catch (error) {
1449-
const errorMsg = `Invalid ${flagName} JSON: ${error instanceof Error ? error.message : String(error)}`;
1450+
const errorMsg = `Invalid ${flagName} JSON: ${errorMessage(error)}`;
14501451
if (this.shouldOutputJson(flags)) {
14511452
this.jsonError({ error: errorMsg, success: false }, flags);
14521453
} else {
@@ -1471,13 +1472,18 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
14711472
component: string,
14721473
context?: Record<string, unknown>,
14731474
): void {
1474-
const errorMsg = error instanceof Error ? error.message : String(error);
1475-
this.logCliEvent(flags, component, "fatalError", `Error: ${errorMsg}`, {
1476-
error: errorMsg,
1475+
const baseErrorMsg = errorMessage(error);
1476+
// Enhance error message with CLI-specific hints for known Ably error codes
1477+
const errorMsg = enhanceErrorMessage(error, baseErrorMsg);
1478+
this.logCliEvent(flags, component, "fatalError", `Error: ${baseErrorMsg}`, {
1479+
error: baseErrorMsg,
14771480
...context,
14781481
});
14791482
if (this.shouldOutputJson(flags)) {
1480-
this.jsonError({ error: errorMsg, success: false, ...context }, flags);
1483+
this.jsonError(
1484+
{ error: baseErrorMsg, success: false, ...context },
1485+
flags,
1486+
);
14811487
} else {
14821488
this.error(errorMsg);
14831489
}

src/utils/errors.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,56 @@
44
export function errorMessage(error: unknown): string {
55
return error instanceof Error ? error.message : String(error);
66
}
7+
8+
/**
9+
* Ably error code to enhanced error message mapping.
10+
* These provide more helpful CLI-specific guidance for common errors.
11+
*/
12+
const ABLY_ERROR_ENHANCEMENTS: Record<
13+
number,
14+
{ hint: string; helpUrl?: string }
15+
> = {
16+
// Mutable messages feature not enabled (required for annotations, updates, deletes, appends)
17+
93002: {
18+
hint: 'Enable the "Message annotations, updates, deletes, and appends" channel rule:\n 1. Go to your app\'s Settings tab in the Ably dashboard\n 2. Under Channel rules, click "Add new rule"\n 3. Enter the channel namespace (e.g., the part before ":" in your channel name)\n 4. Check "Message annotations, updates, deletes, and appends"\n 5. Click "Create channel rule"',
19+
helpUrl: "https://ably.com/docs/messages/annotations#enable",
20+
},
21+
};
22+
23+
/**
24+
* Extract the Ably error code from an error object.
25+
* Ably SDK errors have a `code` property.
26+
*/
27+
export function getAblyErrorCode(error: unknown): number | undefined {
28+
if (error && typeof error === "object" && "code" in error) {
29+
const code = (error as { code: unknown }).code;
30+
if (typeof code === "number") {
31+
return code;
32+
}
33+
}
34+
return undefined;
35+
}
36+
37+
/**
38+
* Enhance an error message with CLI-specific guidance if the error
39+
* is a known Ably error code.
40+
*
41+
* @param error - The error object
42+
* @param baseMessage - The base error message
43+
* @returns Enhanced error message with hints, or the original message
44+
*/
45+
export function enhanceErrorMessage(
46+
error: unknown,
47+
baseMessage: string,
48+
): string {
49+
const errorCode = getAblyErrorCode(error);
50+
if (errorCode && ABLY_ERROR_ENHANCEMENTS[errorCode]) {
51+
const enhancement = ABLY_ERROR_ENHANCEMENTS[errorCode];
52+
let enhanced = `${baseMessage}\n\nHint: ${enhancement.hint}`;
53+
if (enhancement.helpUrl) {
54+
enhanced += `\n\nFor more information, see: ${enhancement.helpUrl}`;
55+
}
56+
return enhanced;
57+
}
58+
return baseMessage;
59+
}

test/e2e/rooms/rooms-e2e.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
cleanupRunners,
2727
} from "../../helpers/command-helpers.js";
2828
import { CliRunner } from "../../helpers/cli-runner.js";
29+
import { errorMessage } from "../../../src/utils/errors.js";
2930

3031
describe("Rooms E2E Tests", () => {
3132
// Skip all tests if API key not available
@@ -223,7 +224,7 @@ describe("Rooms E2E Tests", () => {
223224
} catch (error) {
224225
// Re-throw with additional context
225226
throw new Error(
226-
`Test failed: ${error instanceof Error ? error.message : String(error)}`,
227+
`Test failed: ${errorMessage(error)}`,
227228
);
228229
}
229230
} finally {
@@ -354,7 +355,7 @@ describe("Rooms E2E Tests", () => {
354355
} catch (error) {
355356
// Re-throw with additional context
356357
throw new Error(
357-
`Test failed: ${error instanceof Error ? error.message : String(error)}`,
358+
`Test failed: ${errorMessage(error)}`,
358359
);
359360
} finally {
360361
if (subscribeRunner) {

test/e2e/spaces/spaces-e2e.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
resetTestTracking,
2626
} from "../../helpers/e2e-test-helper.js";
2727
import { ChildProcess } from "node:child_process";
28+
import { errorMessage } from "../../../src/utils/errors.js";
2829

2930
describe("Spaces E2E Tests", () => {
3031
// Skip all tests if API key not available
@@ -427,7 +428,7 @@ describe("Spaces E2E Tests", () => {
427428
} catch (error) {
428429
// Re-throw with additional context
429430
throw new Error(
430-
`Test failed: ${error instanceof Error ? error.message : String(error)}`,
431+
`Test failed: ${errorMessage(error)}`,
431432
);
432433
} finally {
433434
if (cursorSetProcess) {

test/helpers/e2e-test-helper.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "../setup.js";
1313
import stripAnsi from "strip-ansi";
1414
import { onTestFailed } from "vitest";
15+
import { errorMessage } from "../../src/utils/errors.js";
1516

1617
// Constants
1718
export const E2E_API_KEY = process.env.E2E_ABLY_API_KEY;
@@ -532,12 +533,10 @@ async function attemptProcessStart(
532533
// Output stream closed
533534
});
534535
} catch (error: unknown) {
535-
const errorMessage =
536-
error instanceof Error ? error.message : String(error);
537536
// If we can't create the stream, the process output won't be captured.
538537
// This will likely lead to readiness timeout or ENOENT later.
539538
throw new Error(
540-
`Failed to create output stream for ${outputPath}: ${errorMessage}`,
539+
`Failed to create output stream for ${outputPath}: ${errorMessage(error)}`,
541540
);
542541
}
543542

test/unit/utils/errors.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
errorMessage,
4+
getAblyErrorCode,
5+
enhanceErrorMessage,
6+
} from "../../../src/utils/errors.js";
7+
8+
describe("errorMessage", () => {
9+
it("should extract message from Error object", () => {
10+
const error = new Error("Test error message");
11+
expect(errorMessage(error)).toBe("Test error message");
12+
});
13+
14+
it("should convert non-Error to string", () => {
15+
expect(errorMessage("string error")).toBe("string error");
16+
expect(errorMessage(123)).toBe("123");
17+
expect(errorMessage(null)).toBe("null");
18+
});
19+
20+
it("should handle undefined", () => {
21+
let undefinedValue: unknown;
22+
expect(errorMessage(undefinedValue)).toBe("undefined");
23+
});
24+
});
25+
26+
describe("getAblyErrorCode", () => {
27+
it("should extract code from Ably-style error object", () => {
28+
const error = { code: 93002, message: "Test error" };
29+
expect(getAblyErrorCode(error)).toBe(93002);
30+
});
31+
32+
it("should return undefined for Error without code", () => {
33+
const error = new Error("Test error");
34+
expect(getAblyErrorCode(error)).toBeUndefined();
35+
});
36+
37+
it("should return undefined for non-object", () => {
38+
expect(getAblyErrorCode("string")).toBeUndefined();
39+
expect(getAblyErrorCode(123)).toBeUndefined();
40+
expect(getAblyErrorCode(null)).toBeUndefined();
41+
let undefinedValue: unknown;
42+
expect(getAblyErrorCode(undefinedValue)).toBeUndefined();
43+
});
44+
45+
it("should return undefined for object with non-numeric code", () => {
46+
const error = { code: "not-a-number", message: "Test error" };
47+
expect(getAblyErrorCode(error)).toBeUndefined();
48+
});
49+
});
50+
51+
describe("enhanceErrorMessage", () => {
52+
describe("error code 93002 (mutable messages not enabled)", () => {
53+
it("should enhance error message with channel rule hint", () => {
54+
const error = { code: 93002, message: "Mutable messages not enabled" };
55+
const baseMessage = "Mutable messages not enabled";
56+
const enhanced = enhanceErrorMessage(error, baseMessage);
57+
58+
expect(enhanced).toContain(baseMessage);
59+
expect(enhanced).toContain("Hint:");
60+
expect(enhanced).toContain(
61+
"Message annotations, updates, deletes, and appends",
62+
);
63+
expect(enhanced).toContain("Channel rules");
64+
expect(enhanced).toContain(
65+
"https://ably.com/docs/messages/annotations#enable-annotations",
66+
);
67+
});
68+
69+
it("should include step-by-step instructions", () => {
70+
const error = { code: 93002, message: "Test" };
71+
const enhanced = enhanceErrorMessage(error, "Test");
72+
73+
expect(enhanced).toContain("Settings tab");
74+
expect(enhanced).toContain("Add new rule");
75+
expect(enhanced).toContain("Create channel rule");
76+
});
77+
});
78+
79+
describe("unknown error codes", () => {
80+
it("should return original message for unknown error codes", () => {
81+
const error = { code: 99999, message: "Unknown error" };
82+
const baseMessage = "Unknown error";
83+
const enhanced = enhanceErrorMessage(error, baseMessage);
84+
85+
expect(enhanced).toBe(baseMessage);
86+
});
87+
88+
it("should return original message for errors without code", () => {
89+
const error = new Error("Regular error");
90+
const baseMessage = "Regular error";
91+
const enhanced = enhanceErrorMessage(error, baseMessage);
92+
93+
expect(enhanced).toBe(baseMessage);
94+
});
95+
96+
it("should return original message for non-object errors", () => {
97+
const enhanced = enhanceErrorMessage("string error", "string error");
98+
expect(enhanced).toBe("string error");
99+
});
100+
});
101+
});

0 commit comments

Comments
 (0)