From fe58bf34938ae252ebf65a37463bd78b690e3e40 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 25 Mar 2026 15:07:12 +0000 Subject: [PATCH 1/3] rename spaces get-all to get to align with the rest of the cli --- README.md | 110 ++++++-------- src/base-command.ts | 9 ++ src/commands/spaces/cursors.ts | 2 +- .../spaces/cursors/{get-all.ts => get.ts} | 12 +- src/commands/spaces/cursors/index.ts | 2 +- src/commands/spaces/locations.ts | 2 +- src/commands/spaces/locations/get-all.ts | 105 -------------- src/commands/spaces/locations/get.ts | 100 +++++++++++++ src/commands/spaces/locations/index.ts | 2 +- src/commands/spaces/locks.ts | 2 +- src/commands/spaces/locks/get-all.ts | 86 ----------- src/commands/spaces/locks/get.ts | 118 ++++++++++----- src/commands/spaces/locks/index.ts | 2 +- src/commands/spaces/members.ts | 2 +- .../spaces/members/{get-all.ts => get.ts} | 15 +- src/commands/spaces/members/index.ts | 2 +- src/spaces-base-command.ts | 13 ++ src/utils/spaces-output.ts | 21 ++- test/e2e/spaces/spaces-e2e.test.ts | 2 +- .../cursors/{get-all.test.ts => get.test.ts} | 25 ++-- .../{get-all.test.ts => get.test.ts} | 16 +- .../commands/spaces/locks/get-all.test.ts | 127 ---------------- test/unit/commands/spaces/locks/get.test.ts | 137 ++++++++++++++---- .../members/{get-all.test.ts => get.test.ts} | 18 +-- 24 files changed, 417 insertions(+), 513 deletions(-) rename src/commands/spaces/cursors/{get-all.ts => get.ts} (85%) delete mode 100644 src/commands/spaces/locations/get-all.ts create mode 100644 src/commands/spaces/locations/get.ts delete mode 100644 src/commands/spaces/locks/get-all.ts rename src/commands/spaces/members/{get-all.ts => get.ts} (81%) rename test/unit/commands/spaces/cursors/{get-all.test.ts => get.test.ts} (82%) rename test/unit/commands/spaces/locations/{get-all.test.ts => get.test.ts} (85%) delete mode 100644 test/unit/commands/spaces/locks/get-all.test.ts rename test/unit/commands/spaces/members/{get-all.test.ts => get.test.ts} (88%) diff --git a/README.md b/README.md index b873d358..4420e09d 100644 --- a/README.md +++ b/README.md @@ -204,23 +204,22 @@ $ ably-interactive * [`ably spaces`](#ably-spaces) * [`ably spaces create SPACE_NAME`](#ably-spaces-create-space_name) * [`ably spaces cursors`](#ably-spaces-cursors) -* [`ably spaces cursors get-all SPACE_NAME`](#ably-spaces-cursors-get-all-space_name) +* [`ably spaces cursors get SPACE_NAME`](#ably-spaces-cursors-get-space_name) * [`ably spaces cursors set SPACE_NAME`](#ably-spaces-cursors-set-space_name) * [`ably spaces cursors subscribe SPACE_NAME`](#ably-spaces-cursors-subscribe-space_name) * [`ably spaces get SPACE_NAME`](#ably-spaces-get-space_name) * [`ably spaces list`](#ably-spaces-list) * [`ably spaces locations`](#ably-spaces-locations) -* [`ably spaces locations get-all SPACE_NAME`](#ably-spaces-locations-get-all-space_name) +* [`ably spaces locations get SPACE_NAME`](#ably-spaces-locations-get-space_name) * [`ably spaces locations set SPACE_NAME`](#ably-spaces-locations-set-space_name) * [`ably spaces locations subscribe SPACE_NAME`](#ably-spaces-locations-subscribe-space_name) * [`ably spaces locks`](#ably-spaces-locks) * [`ably spaces locks acquire SPACE_NAME LOCKID`](#ably-spaces-locks-acquire-space_name-lockid) -* [`ably spaces locks get SPACE_NAME LOCKID`](#ably-spaces-locks-get-space_name-lockid) -* [`ably spaces locks get-all SPACE_NAME`](#ably-spaces-locks-get-all-space_name) +* [`ably spaces locks get SPACE_NAME [LOCKID]`](#ably-spaces-locks-get-space_name-lockid) * [`ably spaces locks subscribe SPACE_NAME`](#ably-spaces-locks-subscribe-space_name) * [`ably spaces members`](#ably-spaces-members) * [`ably spaces members enter SPACE_NAME`](#ably-spaces-members-enter-space_name) -* [`ably spaces members get-all SPACE_NAME`](#ably-spaces-members-get-all-space_name) +* [`ably spaces members get SPACE_NAME`](#ably-spaces-members-get-space_name) * [`ably spaces members subscribe SPACE_NAME`](#ably-spaces-members-subscribe-space_name) * [`ably spaces occupancy`](#ably-spaces-occupancy) * [`ably spaces occupancy get SPACE_NAME`](#ably-spaces-occupancy-get-space_name) @@ -4573,18 +4572,18 @@ EXAMPLES $ ably spaces cursors subscribe my-space - $ ably spaces cursors get-all my-space + $ ably spaces cursors get my-space ``` _See code: [src/commands/spaces/cursors/index.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/cursors/index.ts)_ -## `ably spaces cursors get-all SPACE_NAME` +## `ably spaces cursors get SPACE_NAME` Get all current cursors in a space ``` USAGE - $ ably spaces cursors get-all SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] + $ ably spaces cursors get SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] ARGUMENTS SPACE_NAME Name of the space to get cursors from @@ -4600,14 +4599,14 @@ DESCRIPTION Get all current cursors in a space EXAMPLES - $ ably spaces cursors get-all my-space + $ ably spaces cursors get my-space - $ ably spaces cursors get-all my-space --json + $ ably spaces cursors get my-space --json - $ ably spaces cursors get-all my-space --pretty-json + $ ably spaces cursors get my-space --pretty-json ``` -_See code: [src/commands/spaces/cursors/get-all.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/cursors/get-all.ts)_ +_See code: [src/commands/spaces/cursors/get.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/cursors/get.ts)_ ## `ably spaces cursors set SPACE_NAME` @@ -4769,18 +4768,18 @@ EXAMPLES $ ably spaces locations subscribe my-space - $ ably spaces locations get-all my-space + $ ably spaces locations get my-space ``` _See code: [src/commands/spaces/locations/index.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/locations/index.ts)_ -## `ably spaces locations get-all SPACE_NAME` +## `ably spaces locations get SPACE_NAME` Get all current locations in a space ``` USAGE - $ ably spaces locations get-all SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] + $ ably spaces locations get SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] ARGUMENTS SPACE_NAME Name of the space to get locations from @@ -4796,14 +4795,14 @@ DESCRIPTION Get all current locations in a space EXAMPLES - $ ably spaces locations get-all my-space + $ ably spaces locations get my-space - $ ably spaces locations get-all my-space --json + $ ably spaces locations get my-space --json - $ ably spaces locations get-all my-space --pretty-json + $ ably spaces locations get my-space --pretty-json ``` -_See code: [src/commands/spaces/locations/get-all.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/locations/get-all.ts)_ +_See code: [src/commands/spaces/locations/get.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/locations/get.ts)_ ## `ably spaces locations set SPACE_NAME` @@ -4891,7 +4890,7 @@ EXAMPLES $ ably spaces locks get my-space my-lock-id - $ ably spaces locks get-all my-space + $ ably spaces locks get my-space ``` _See code: [src/commands/spaces/locks/index.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/locks/index.ts)_ @@ -4931,17 +4930,17 @@ EXAMPLES _See code: [src/commands/spaces/locks/acquire.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/locks/acquire.ts)_ -## `ably spaces locks get SPACE_NAME LOCKID` +## `ably spaces locks get SPACE_NAME [LOCKID]` -Get a lock in a space +Get a lock or all locks in a space ``` USAGE - $ ably spaces locks get SPACE_NAME LOCKID [-v] [--json | --pretty-json] [--client-id ] + $ ably spaces locks get SPACE_NAME [LOCKID] [-v] [--json | --pretty-json] [--client-id ] ARGUMENTS - SPACE_NAME Name of the space to get lock from - LOCKID Lock ID to get + SPACE_NAME Name of the space to get locks from + LOCKID Lock ID to get (omit to get all locks) FLAGS -v, --verbose Output verbose logs @@ -4951,49 +4950,20 @@ FLAGS --pretty-json Output in colorized JSON format DESCRIPTION - Get a lock in a space + Get a lock or all locks in a space EXAMPLES + $ ably spaces locks get my-space + + $ ably spaces locks get my-space --json + $ ably spaces locks get my-space my-lock $ ably spaces locks get my-space my-lock --json - - $ ably spaces locks get my-space my-lock --pretty-json ``` _See code: [src/commands/spaces/locks/get.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/locks/get.ts)_ -## `ably spaces locks get-all SPACE_NAME` - -Get all current locks in a space - -``` -USAGE - $ ably spaces locks get-all SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] - -ARGUMENTS - SPACE_NAME Name of the space to get locks from - -FLAGS - -v, --verbose Output verbose logs - --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set - no client ID. Not applicable when using token authentication. - --json Output in JSON format - --pretty-json Output in colorized JSON format - -DESCRIPTION - Get all current locks in a space - -EXAMPLES - $ ably spaces locks get-all my-space - - $ ably spaces locks get-all my-space --json - - $ ably spaces locks get-all my-space --pretty-json -``` - -_See code: [src/commands/spaces/locks/get-all.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/locks/get-all.ts)_ - ## `ably spaces locks subscribe SPACE_NAME` Subscribe to lock events in a space @@ -5044,7 +5014,7 @@ EXAMPLES $ ably spaces members subscribe my-space - $ ably spaces members get-all my-space + $ ably spaces members get my-space ``` _See code: [src/commands/spaces/members/index.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/members/index.ts)_ @@ -5085,34 +5055,36 @@ EXAMPLES _See code: [src/commands/spaces/members/enter.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/members/enter.ts)_ -## `ably spaces members get-all SPACE_NAME` +## `ably spaces members get SPACE_NAME` Get all members in a space ``` USAGE - $ ably spaces members get-all SPACE_NAME [-v] [--json | --pretty-json] + $ ably spaces members get SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] ARGUMENTS SPACE_NAME Name of the space to get members from FLAGS - -v, --verbose Output verbose logs - --json Output in JSON format - --pretty-json Output in colorized JSON format + -v, --verbose Output verbose logs + --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set + no client ID. Not applicable when using token authentication. + --json Output in JSON format + --pretty-json Output in colorized JSON format DESCRIPTION Get all members in a space EXAMPLES - $ ably spaces members get-all my-space + $ ably spaces members get my-space - $ ably spaces members get-all my-space --json + $ ably spaces members get my-space --json - $ ably spaces members get-all my-space --pretty-json + $ ably spaces members get my-space --pretty-json ``` -_See code: [src/commands/spaces/members/get-all.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/members/get-all.ts)_ +_See code: [src/commands/spaces/members/get.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/members/get.ts)_ ## `ably spaces members subscribe SPACE_NAME` diff --git a/src/base-command.ts b/src/base-command.ts index 52bacace..aafe3f90 100644 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -108,6 +108,7 @@ const SKIP_AUTH_INFO_COMMANDS = [ export abstract class AblyBaseCommand extends InteractiveBaseCommand { protected _authInfoShown = false; protected cleanupInProgress = false; + protected _suppressSdkErrorLogs = false; private _cachedRestClient: Ably.Rest | null = null; private _cachedRealtimeClient: Ably.Realtime | null = null; @@ -934,6 +935,14 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { // Always add a log handler to control SDK output formatting and destination options.logHandler = (message: string, level: number) => { + // Allow commands to suppress SDK error logs during cleanup. Set by + // SpacesBaseCommand.finally() to silence teardown noise (e.g. the + // Spaces SDK's cursors module queues a presence enter that fails with + // 80017 when the connection closes). Only suppresses error-level logs + // (level <= 1); only active after run() completes — see the comment + // in SpacesBaseCommand.finally() for the full safety rationale. + if (this._suppressSdkErrorLogs && level <= 1) return; + if (isJsonMode) { // JSON Mode Handling if (flags.verbose && level <= 2) { diff --git a/src/commands/spaces/cursors.ts b/src/commands/spaces/cursors.ts index 77ee1782..75c43656 100644 --- a/src/commands/spaces/cursors.ts +++ b/src/commands/spaces/cursors.ts @@ -10,6 +10,6 @@ export default class SpacesCursors extends BaseTopicCommand { static override examples = [ "<%= config.bin %> <%= command.id %> set my-space --x 100 --y 200", "<%= config.bin %> <%= command.id %> subscribe my-space", - "<%= config.bin %> <%= command.id %> get-all my-space", + "<%= config.bin %> <%= command.id %> get my-space", ]; } diff --git a/src/commands/spaces/cursors/get-all.ts b/src/commands/spaces/cursors/get.ts similarity index 85% rename from src/commands/spaces/cursors/get-all.ts rename to src/commands/spaces/cursors/get.ts index b6cc231e..9eedad05 100644 --- a/src/commands/spaces/cursors/get-all.ts +++ b/src/commands/spaces/cursors/get.ts @@ -16,7 +16,7 @@ import { formatCursorOutput, } from "../../../utils/spaces-output.js"; -export default class SpacesCursorsGetAll extends SpacesBaseCommand { +export default class SpacesCursorsGet extends SpacesBaseCommand { static override args = { space_name: Args.string({ description: "Name of the space to get cursors from", @@ -27,9 +27,9 @@ export default class SpacesCursorsGetAll extends SpacesBaseCommand { static override description = "Get all current cursors in a space"; static override examples = [ - "$ ably spaces cursors get-all my-space", - "$ ably spaces cursors get-all my-space --json", - "$ ably spaces cursors get-all my-space --pretty-json", + "$ ably spaces cursors get my-space", + "$ ably spaces cursors get my-space --json", + "$ ably spaces cursors get my-space --pretty-json", ]; static override flags = { @@ -38,7 +38,7 @@ export default class SpacesCursorsGetAll extends SpacesBaseCommand { }; async run(): Promise { - const { args, flags } = await this.parse(SpacesCursorsGetAll); + const { args, flags } = await this.parse(SpacesCursorsGet); const { space_name: spaceName } = args; try { @@ -82,7 +82,7 @@ export default class SpacesCursorsGetAll extends SpacesBaseCommand { }); } } catch (error) { - this.fail(error, flags, "cursorGetAll", { spaceName }); + this.fail(error, flags, "cursorGet", { spaceName }); } } } diff --git a/src/commands/spaces/cursors/index.ts b/src/commands/spaces/cursors/index.ts index a5cdddf7..816136ad 100644 --- a/src/commands/spaces/cursors/index.ts +++ b/src/commands/spaces/cursors/index.ts @@ -9,6 +9,6 @@ export default class SpacesCursorsIndex extends BaseTopicCommand { static override examples = [ "<%= config.bin %> <%= command.id %> set my-space --x 100 --y 200", "<%= config.bin %> <%= command.id %> subscribe my-space", - "<%= config.bin %> <%= command.id %> get-all my-space", + "<%= config.bin %> <%= command.id %> get my-space", ]; } diff --git a/src/commands/spaces/locations.ts b/src/commands/spaces/locations.ts index 87ab9b9c..0dcb5777 100644 --- a/src/commands/spaces/locations.ts +++ b/src/commands/spaces/locations.ts @@ -10,6 +10,6 @@ export default class SpacesLocations extends BaseTopicCommand { static override examples = [ "<%= config.bin %> <%= command.id %> set my-space", "<%= config.bin %> <%= command.id %> subscribe my-space", - "<%= config.bin %> <%= command.id %> get-all my-space", + "<%= config.bin %> <%= command.id %> get my-space", ]; } diff --git a/src/commands/spaces/locations/get-all.ts b/src/commands/spaces/locations/get-all.ts deleted file mode 100644 index 293e187d..00000000 --- a/src/commands/spaces/locations/get-all.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Args } from "@oclif/core"; - -import { productApiFlags, clientIdFlag } from "../../../flags.js"; -import { SpacesBaseCommand } from "../../../spaces-base-command.js"; -import { - formatCountLabel, - formatHeading, - formatIndex, - formatLabel, - formatProgress, - formatResource, - formatWarning, -} from "../../../utils/output.js"; -import type { LocationEntry } from "../../../utils/spaces-output.js"; - -export default class SpacesLocationsGetAll extends SpacesBaseCommand { - static override args = { - space_name: Args.string({ - description: "Name of the space to get locations from", - required: true, - }), - }; - - static override description = "Get all current locations in a space"; - - static override examples = [ - "$ ably spaces locations get-all my-space", - "$ ably spaces locations get-all my-space --json", - "$ ably spaces locations get-all my-space --pretty-json", - ]; - - static override flags = { - ...productApiFlags, - ...clientIdFlag, - }; - - async run(): Promise { - const { args, flags } = await this.parse(SpacesLocationsGetAll); - const { space_name: spaceName } = args; - - try { - await this.initializeSpace(flags, spaceName, { - enterSpace: false, - setupConnectionLogging: false, - }); - - if (!this.shouldOutputJson(flags)) { - this.log( - formatProgress( - `Fetching locations for space ${formatResource(spaceName)}`, - ), - ); - } - - try { - const locationsFromSpace = await this.space!.locations.getAll(); - - const entries: LocationEntry[] = Object.entries(locationsFromSpace) - .filter( - ([, loc]) => - loc != null && - !( - typeof loc === "object" && - Object.keys(loc as object).length === 0 - ), - ) - .map(([connectionId, loc]) => ({ connectionId, location: loc })); - - if (this.shouldOutputJson(flags)) { - this.logJsonResult( - { - locations: entries.map((entry) => ({ - connectionId: entry.connectionId, - location: entry.location, - })), - }, - flags, - ); - } else if (entries.length === 0) { - this.logToStderr( - formatWarning("No locations are currently set in this space."), - ); - } else { - this.log( - `\n${formatHeading("Current locations")} (${formatCountLabel(entries.length, "location")}):\n`, - ); - - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - this.log(`${formatIndex(i + 1)}`); - this.log(` ${formatLabel("Connection ID")} ${entry.connectionId}`); - this.log( - ` ${formatLabel("Location")} ${JSON.stringify(entry.location)}`, - ); - this.log(""); - } - } - } catch (error) { - this.fail(error, flags, "locationGetAll", { spaceName }); - } - } catch (error) { - this.fail(error, flags, "locationGetAll", { spaceName }); - } - } -} diff --git a/src/commands/spaces/locations/get.ts b/src/commands/spaces/locations/get.ts new file mode 100644 index 00000000..834be687 --- /dev/null +++ b/src/commands/spaces/locations/get.ts @@ -0,0 +1,100 @@ +import { Args } from "@oclif/core"; + +import { productApiFlags, clientIdFlag } from "../../../flags.js"; +import { SpacesBaseCommand } from "../../../spaces-base-command.js"; +import { + formatCountLabel, + formatHeading, + formatIndex, + formatLabel, + formatProgress, + formatResource, + formatWarning, +} from "../../../utils/output.js"; +import type { LocationEntry } from "../../../utils/spaces-output.js"; + +export default class SpacesLocationsGet extends SpacesBaseCommand { + static override args = { + space_name: Args.string({ + description: "Name of the space to get locations from", + required: true, + }), + }; + + static override description = "Get all current locations in a space"; + + static override examples = [ + "$ ably spaces locations get my-space", + "$ ably spaces locations get my-space --json", + "$ ably spaces locations get my-space --pretty-json", + ]; + + static override flags = { + ...productApiFlags, + ...clientIdFlag, + }; + + async run(): Promise { + const { args, flags } = await this.parse(SpacesLocationsGet); + const { space_name: spaceName } = args; + + try { + await this.initializeSpace(flags, spaceName, { + enterSpace: false, + setupConnectionLogging: false, + }); + + if (!this.shouldOutputJson(flags)) { + this.log( + formatProgress( + `Fetching locations for space ${formatResource(spaceName)}`, + ), + ); + } + + const locationsFromSpace = await this.space!.locations.getAll(); + + const entries: LocationEntry[] = Object.entries(locationsFromSpace) + .filter( + ([, loc]) => + loc != null && + !( + typeof loc === "object" && Object.keys(loc as object).length === 0 + ), + ) + .map(([connectionId, loc]) => ({ connectionId, location: loc })); + + if (this.shouldOutputJson(flags)) { + this.logJsonResult( + { + locations: entries.map((entry) => ({ + connectionId: entry.connectionId, + location: entry.location, + })), + }, + flags, + ); + } else if (entries.length === 0) { + this.logToStderr( + formatWarning("No locations are currently set in this space."), + ); + } else { + this.log( + `\n${formatHeading("Current locations")} (${formatCountLabel(entries.length, "location")}):\n`, + ); + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + this.log(`${formatIndex(i + 1)}`); + this.log(` ${formatLabel("Connection ID")} ${entry.connectionId}`); + this.log( + ` ${formatLabel("Location")} ${JSON.stringify(entry.location)}`, + ); + this.log(""); + } + } + } catch (error) { + this.fail(error, flags, "locationGet", { spaceName }); + } + } +} diff --git a/src/commands/spaces/locations/index.ts b/src/commands/spaces/locations/index.ts index 25191363..6bae84ac 100644 --- a/src/commands/spaces/locations/index.ts +++ b/src/commands/spaces/locations/index.ts @@ -10,6 +10,6 @@ export default class SpacesLocationsIndex extends BaseTopicCommand { static override examples = [ "<%= config.bin %> <%= command.id %> set my-space", "<%= config.bin %> <%= command.id %> subscribe my-space", - "<%= config.bin %> <%= command.id %> get-all my-space", + "<%= config.bin %> <%= command.id %> get my-space", ]; } diff --git a/src/commands/spaces/locks.ts b/src/commands/spaces/locks.ts index 9c62edd0..c5ecbd52 100644 --- a/src/commands/spaces/locks.ts +++ b/src/commands/spaces/locks.ts @@ -10,6 +10,6 @@ export default class SpacesLocks extends BaseTopicCommand { "<%= config.bin %> <%= command.id %> acquire my-space my-lock-id", "<%= config.bin %> <%= command.id %> subscribe my-space", "<%= config.bin %> <%= command.id %> get my-space my-lock-id", - "<%= config.bin %> <%= command.id %> get-all my-space", + "<%= config.bin %> <%= command.id %> get my-space", ]; } diff --git a/src/commands/spaces/locks/get-all.ts b/src/commands/spaces/locks/get-all.ts deleted file mode 100644 index 06b7ce10..00000000 --- a/src/commands/spaces/locks/get-all.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { Lock } from "@ably/spaces"; -import { Args } from "@oclif/core"; - -import { productApiFlags, clientIdFlag } from "../../../flags.js"; -import { SpacesBaseCommand } from "../../../spaces-base-command.js"; -import { - formatCountLabel, - formatHeading, - formatIndex, - formatProgress, - formatResource, - formatWarning, -} from "../../../utils/output.js"; -import { - formatLockBlock, - formatLockOutput, -} from "../../../utils/spaces-output.js"; - -export default class SpacesLocksGetAll extends SpacesBaseCommand { - static override args = { - space_name: Args.string({ - description: "Name of the space to get locks from", - required: true, - }), - }; - - static override description = "Get all current locks in a space"; - - static override examples = [ - "$ ably spaces locks get-all my-space", - "$ ably spaces locks get-all my-space --json", - "$ ably spaces locks get-all my-space --pretty-json", - ]; - - static override flags = { - ...productApiFlags, - ...clientIdFlag, - }; - - async run(): Promise { - const { args, flags } = await this.parse(SpacesLocksGetAll); - const { space_name: spaceName } = args; - - try { - await this.initializeSpace(flags, spaceName, { - enterSpace: false, - setupConnectionLogging: false, - }); - - if (!this.shouldOutputJson(flags)) { - this.log( - formatProgress( - `Fetching locks for space ${formatResource(spaceName)}`, - ), - ); - } - - const locks: Lock[] = await this.space!.locks.getAll(); - - if (this.shouldOutputJson(flags)) { - this.logJsonResult( - { - locks: locks.map((lock) => formatLockOutput(lock)), - }, - flags, - ); - } else if (locks.length === 0) { - this.logToStderr( - formatWarning("No locks are currently active in this space."), - ); - } else { - this.log( - `\n${formatHeading("Current locks")} (${formatCountLabel(locks.length, "lock")}):\n`, - ); - - for (let i = 0; i < locks.length; i++) { - this.log(`${formatIndex(i + 1)}`); - this.log(formatLockBlock(locks[i])); - this.log(""); - } - } - } catch (error) { - this.fail(error, flags, "lockGetAll", { spaceName }); - } - } -} diff --git a/src/commands/spaces/locks/get.ts b/src/commands/spaces/locks/get.ts index 23b357ad..45d2cd1a 100644 --- a/src/commands/spaces/locks/get.ts +++ b/src/commands/spaces/locks/get.ts @@ -1,8 +1,16 @@ +import type { Lock } from "@ably/spaces"; import { Args } from "@oclif/core"; import { productApiFlags, clientIdFlag } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; -import { formatResource, formatWarning } from "../../../utils/output.js"; +import { + formatCountLabel, + formatHeading, + formatIndex, + formatProgress, + formatResource, + formatWarning, +} from "../../../utils/output.js"; import { formatLockBlock, formatLockOutput, @@ -11,21 +19,22 @@ import { export default class SpacesLocksGet extends SpacesBaseCommand { static override args = { space_name: Args.string({ - description: "Name of the space to get lock from", + description: "Name of the space to get locks from", required: true, }), lockId: Args.string({ - description: "Lock ID to get", - required: true, + description: "Lock ID to get (omit to get all locks)", + required: false, }), }; - static override description = "Get a lock in a space"; + static override description = "Get a lock or all locks in a space"; static override examples = [ + "$ ably spaces locks get my-space", + "$ ably spaces locks get my-space --json", "$ ably spaces locks get my-space my-lock", "$ ably spaces locks get my-space my-lock --json", - "$ ably spaces locks get my-space my-lock --pretty-json", ]; static override flags = { @@ -35,8 +44,7 @@ export default class SpacesLocksGet extends SpacesBaseCommand { async run(): Promise { const { args, flags } = await this.parse(SpacesLocksGet); - const { space_name: spaceName } = args; - const { lockId } = args; + const { space_name: spaceName, lockId } = args; try { await this.initializeSpace(flags, spaceName, { @@ -44,33 +52,77 @@ export default class SpacesLocksGet extends SpacesBaseCommand { setupConnectionLogging: false, }); - try { - const lock = await this.space!.locks.get(lockId); - - if (!lock) { - if (this.shouldOutputJson(flags)) { - this.logJsonResult({ lock: null }, flags); - } else { - this.log( - formatWarning( - `Lock ${formatResource(lockId)} not found in space ${formatResource(spaceName)}.`, - ), - ); - } - - return; - } - - if (this.shouldOutputJson(flags)) { - this.logJsonResult({ lock: formatLockOutput(lock) }, flags); - } else { - this.log(formatLockBlock(lock)); - } - } catch (error) { - this.fail(error, flags, "lockGet"); + if (lockId) { + await this.getSingleLock(flags, spaceName, lockId); + } else { + await this.getAllLocks(flags, spaceName); } } catch (error) { - this.fail(error, flags, "lockGet"); + this.fail(error, flags, "lockGet", { spaceName }); + } + } + + private async getSingleLock( + flags: Record, + spaceName: string, + lockId: string, + ): Promise { + const lock = await this.space!.locks.get(lockId); + + if (!lock) { + if (this.shouldOutputJson(flags)) { + this.logJsonResult({ lock: null }, flags); + } else { + this.logToStderr( + formatWarning( + `Lock ${formatResource(lockId)} not found in space ${formatResource(spaceName)}.`, + ), + ); + } + + return; + } + + if (this.shouldOutputJson(flags)) { + this.logJsonResult({ lock: formatLockOutput(lock) }, flags); + } else { + this.log(formatLockBlock(lock)); + } + } + + private async getAllLocks( + flags: Record, + spaceName: string, + ): Promise { + if (!this.shouldOutputJson(flags)) { + this.log( + formatProgress(`Fetching locks for space ${formatResource(spaceName)}`), + ); + } + + const locks: Lock[] = await this.space!.locks.getAll(); + + if (this.shouldOutputJson(flags)) { + this.logJsonResult( + { + locks: locks.map((lock) => formatLockOutput(lock)), + }, + flags, + ); + } else if (locks.length === 0) { + this.logToStderr( + formatWarning("No locks are currently active in this space."), + ); + } else { + this.log( + `\n${formatHeading("Current locks")} (${formatCountLabel(locks.length, "lock")}):\n`, + ); + + for (let i = 0; i < locks.length; i++) { + this.log(`${formatIndex(i + 1)}`); + this.log(formatLockBlock(locks[i], { indent: " " })); + this.log(""); + } } } } diff --git a/src/commands/spaces/locks/index.ts b/src/commands/spaces/locks/index.ts index f4e5ca38..2679625e 100644 --- a/src/commands/spaces/locks/index.ts +++ b/src/commands/spaces/locks/index.ts @@ -10,6 +10,6 @@ export default class SpacesLocksIndex extends BaseTopicCommand { "<%= config.bin %> <%= command.id %> acquire my-space my-lock-id", "<%= config.bin %> <%= command.id %> subscribe my-space", "<%= config.bin %> <%= command.id %> get my-space my-lock-id", - "<%= config.bin %> <%= command.id %> get-all my-space", + "<%= config.bin %> <%= command.id %> get my-space", ]; } diff --git a/src/commands/spaces/members.ts b/src/commands/spaces/members.ts index d0d212a0..b37604eb 100644 --- a/src/commands/spaces/members.ts +++ b/src/commands/spaces/members.ts @@ -9,6 +9,6 @@ export default class SpacesMembers extends BaseTopicCommand { static override examples = [ "<%= config.bin %> <%= command.id %> enter my-space", "<%= config.bin %> <%= command.id %> subscribe my-space", - "<%= config.bin %> <%= command.id %> get-all my-space", + "<%= config.bin %> <%= command.id %> get my-space", ]; } diff --git a/src/commands/spaces/members/get-all.ts b/src/commands/spaces/members/get.ts similarity index 81% rename from src/commands/spaces/members/get-all.ts rename to src/commands/spaces/members/get.ts index 4d7f45e3..23c3ab12 100644 --- a/src/commands/spaces/members/get-all.ts +++ b/src/commands/spaces/members/get.ts @@ -1,7 +1,7 @@ import type { SpaceMember } from "@ably/spaces"; import { Args } from "@oclif/core"; -import { productApiFlags } from "../../../flags.js"; +import { productApiFlags, clientIdFlag } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatCountLabel, @@ -16,7 +16,7 @@ import { formatMemberOutput, } from "../../../utils/spaces-output.js"; -export default class SpacesMembersGetAll extends SpacesBaseCommand { +export default class SpacesMembersGet extends SpacesBaseCommand { static override args = { space_name: Args.string({ description: "Name of the space to get members from", @@ -27,17 +27,18 @@ export default class SpacesMembersGetAll extends SpacesBaseCommand { static override description = "Get all members in a space"; static override examples = [ - "$ ably spaces members get-all my-space", - "$ ably spaces members get-all my-space --json", - "$ ably spaces members get-all my-space --pretty-json", + "$ ably spaces members get my-space", + "$ ably spaces members get my-space --json", + "$ ably spaces members get my-space --pretty-json", ]; static override flags = { ...productApiFlags, + ...clientIdFlag, }; async run(): Promise { - const { args, flags } = await this.parse(SpacesMembersGetAll); + const { args, flags } = await this.parse(SpacesMembersGet); const { space_name: spaceName } = args; try { @@ -77,7 +78,7 @@ export default class SpacesMembersGetAll extends SpacesBaseCommand { } } } catch (error) { - this.fail(error, flags, "memberGetAll", { spaceName }); + this.fail(error, flags, "memberGet", { spaceName }); } } } diff --git a/src/commands/spaces/members/index.ts b/src/commands/spaces/members/index.ts index 4b538ff1..161e9e15 100644 --- a/src/commands/spaces/members/index.ts +++ b/src/commands/spaces/members/index.ts @@ -9,6 +9,6 @@ export default class SpacesMembersIndex extends BaseTopicCommand { static override examples = [ "<%= config.bin %> <%= command.id %> enter my-space", "<%= config.bin %> <%= command.id %> subscribe my-space", - "<%= config.bin %> <%= command.id %> get-all my-space", + "<%= config.bin %> <%= command.id %> get my-space", ]; } diff --git a/src/spaces-base-command.ts b/src/spaces-base-command.ts index 396ebba8..2a66e5b5 100644 --- a/src/spaces-base-command.ts +++ b/src/spaces-base-command.ts @@ -55,6 +55,19 @@ export abstract class SpacesBaseCommand extends AblyBaseCommand { } async finally(error: Error | undefined): Promise { + // Suppress SDK error logs during teardown. The Spaces SDK's cursors + // module lazily enters presence when getAll()/getChannel() is called, + // and the queued presence enter fails with error 80017 when the + // connection closes. This only happens because the CLI uses one-shot + // commands that tear down the connection immediately after fetching + // data — in a long-lived app the presence enter would complete normally. + // + // This is safe because finally() runs AFTER run() has returned, so any + // real SDK errors during command execution are already handled by the + // catch block in run(). The only errors suppressed here are teardown + // artifacts from the SDK's internal state being interrupted mid-flight. + this._suppressSdkErrorLogs = true; + // The Spaces SDK subscribes to channel.presence internally (in the Space // constructor) but provides no dispose/cleanup method. When the connection // closes, the SDK's internal handlers receive errors that surface as diff --git a/src/utils/spaces-output.ts b/src/utils/spaces-output.ts index 46029afa..32c559bc 100644 --- a/src/utils/spaces-output.ts +++ b/src/utils/spaces-output.ts @@ -177,13 +177,18 @@ export function formatCursorBlock( /** * Format a Lock as a multi-line labeled block. */ -export function formatLockBlock(lock: Lock): string { +export function formatLockBlock( + lock: Lock, + options?: { indent?: string }, +): string { + const indent = options?.indent ?? ""; + const memberIndent = indent ? indent + " " : " "; const lines: string[] = [ - `${formatLabel("Lock ID")} ${formatResource(lock.id)}`, - `${formatLabel("Status")} ${formatEventType(lock.status)}`, - `${formatLabel("Timestamp")} ${formatMessageTimestamp(lock.timestamp)}`, - `${formatLabel("Member")}`, - formatMemberBlock(lock.member, { indent: " " }), + `${indent}${formatLabel("Lock ID")} ${formatResource(lock.id)}`, + `${indent}${formatLabel("Status")} ${formatEventType(lock.status)}`, + `${indent}${formatLabel("Timestamp")} ${formatMessageTimestamp(lock.timestamp)}`, + `${indent}${formatLabel("Member")}`, + formatMemberBlock(lock.member, { indent: memberIndent }), ]; if ( @@ -191,13 +196,13 @@ export function formatLockBlock(lock: Lock): string { Object.keys(lock.attributes as Record).length > 0 ) { lines.push( - `${formatLabel("Attributes")} ${JSON.stringify(lock.attributes)}`, + `${indent}${formatLabel("Attributes")} ${JSON.stringify(lock.attributes)}`, ); } if (lock.reason) { lines.push( - `${formatLabel("Reason")} ${lock.reason.message || lock.reason.toString()}`, + `${indent}${formatLabel("Reason")} ${lock.reason.message || lock.reason.toString()}`, ); } diff --git a/test/e2e/spaces/spaces-e2e.test.ts b/test/e2e/spaces/spaces-e2e.test.ts index f1780b9a..5c03af3b 100644 --- a/test/e2e/spaces/spaces-e2e.test.ts +++ b/test/e2e/spaces/spaces-e2e.test.ts @@ -417,7 +417,7 @@ describe("Spaces E2E Tests", () => { // Test getAll functionality const getAllResult = await runBackgroundProcessAndGetOutput( - `bin/run.js spaces cursors get-all ${testSpaceId} --client-id ${client1Id}`, + `bin/run.js spaces cursors get ${testSpaceId} --client-id ${client1Id}`, 15000, ); diff --git a/test/unit/commands/spaces/cursors/get-all.test.ts b/test/unit/commands/spaces/cursors/get.test.ts similarity index 82% rename from test/unit/commands/spaces/cursors/get-all.test.ts rename to test/unit/commands/spaces/cursors/get.test.ts index 919a6b88..14f79f77 100644 --- a/test/unit/commands/spaces/cursors/get-all.test.ts +++ b/test/unit/commands/spaces/cursors/get.test.ts @@ -9,18 +9,18 @@ import { standardFlagTests, } from "../../../../helpers/standard-tests.js"; -describe("spaces:cursors:get-all command", () => { +describe("spaces:cursors:get command", () => { beforeEach(() => { // Initialize the mocks getMockAblyRealtime(); getMockAblySpaces(); }); - standardHelpTests("spaces:cursors:get-all", import.meta.url); - standardArgValidationTests("spaces:cursors:get-all", import.meta.url, { + standardHelpTests("spaces:cursors:get", import.meta.url); + standardArgValidationTests("spaces:cursors:get", import.meta.url, { requiredArgs: ["test-space"], }); - standardFlagTests("spaces:cursors:get-all", import.meta.url, ["--json"]); + standardFlagTests("spaces:cursors:get", import.meta.url, ["--json"]); describe("functionality", () => { it("should get all cursors from a space", async () => { @@ -42,7 +42,7 @@ describe("spaces:cursors:get-all command", () => { }); const { stdout } = await runCommand( - ["spaces:cursors:get-all", "test-space", "--json"], + ["spaces:cursors:get", "test-space", "--json"], import.meta.url, ); @@ -60,7 +60,7 @@ describe("spaces:cursors:get-all command", () => { space.cursors.getAll.mockResolvedValue({}); const { stdout } = await runCommand( - ["spaces:cursors:get-all", "test-space", "--json"], + ["spaces:cursors:get", "test-space", "--json"], import.meta.url, ); @@ -77,15 +77,12 @@ describe("spaces:cursors:get-all command", () => { new Error("Failed to get cursors"), ); - // The command catches getAll errors and continues with live updates only - // So this should complete without throwing - const { stdout } = await runCommand( - ["spaces:cursors:get-all", "test-space", "--json"], + const { error } = await runCommand( + ["spaces:cursors:get", "test-space"], import.meta.url, ); - // Command should still output JSON even if getAll fails - expect(stdout).toBeDefined(); + expect(error).toBeDefined(); expect(space.cursors.getAll).toHaveBeenCalled(); }); }); @@ -104,7 +101,7 @@ describe("spaces:cursors:get-all command", () => { }); const { stdout } = await runCommand( - ["spaces:cursors:get-all", "test-space", "--json"], + ["spaces:cursors:get", "test-space", "--json"], import.meta.url, ); @@ -128,7 +125,7 @@ describe("spaces:cursors:get-all command", () => { space.cursors.getAll.mockResolvedValue({}); await runCommand( - ["spaces:cursors:get-all", "test-space", "--json"], + ["spaces:cursors:get", "test-space", "--json"], import.meta.url, ); diff --git a/test/unit/commands/spaces/locations/get-all.test.ts b/test/unit/commands/spaces/locations/get.test.ts similarity index 85% rename from test/unit/commands/spaces/locations/get-all.test.ts rename to test/unit/commands/spaces/locations/get.test.ts index 75353b44..c8f84d77 100644 --- a/test/unit/commands/spaces/locations/get-all.test.ts +++ b/test/unit/commands/spaces/locations/get.test.ts @@ -9,18 +9,18 @@ import { standardFlagTests, } from "../../../../helpers/standard-tests.js"; -describe("spaces:locations:get-all command", () => { +describe("spaces:locations:get command", () => { beforeEach(() => { // Initialize the mocks getMockAblyRealtime(); getMockAblySpaces(); }); - standardHelpTests("spaces:locations:get-all", import.meta.url); - standardArgValidationTests("spaces:locations:get-all", import.meta.url, { + standardHelpTests("spaces:locations:get", import.meta.url); + standardArgValidationTests("spaces:locations:get", import.meta.url, { requiredArgs: ["test-space"], }); - standardFlagTests("spaces:locations:get-all", import.meta.url, ["--json"]); + standardFlagTests("spaces:locations:get", import.meta.url, ["--json"]); describe("functionality", () => { it("should get all locations from a space", async () => { @@ -31,7 +31,7 @@ describe("spaces:locations:get-all command", () => { }); const { stdout } = await runCommand( - ["spaces:locations:get-all", "test-space", "--json"], + ["spaces:locations:get", "test-space", "--json"], import.meta.url, ); @@ -48,7 +48,7 @@ describe("spaces:locations:get-all command", () => { }); const { stdout } = await runCommand( - ["spaces:locations:get-all", "test-space", "--json"], + ["spaces:locations:get", "test-space", "--json"], import.meta.url, ); @@ -78,7 +78,7 @@ describe("spaces:locations:get-all command", () => { space.locations.getAll.mockResolvedValue({}); const { stdout } = await runCommand( - ["spaces:locations:get-all", "test-space", "--json"], + ["spaces:locations:get", "test-space", "--json"], import.meta.url, ); @@ -95,7 +95,7 @@ describe("spaces:locations:get-all command", () => { ); const { error } = await runCommand( - ["spaces:locations:get-all", "test-space"], + ["spaces:locations:get", "test-space"], import.meta.url, ); expect(error).toBeDefined(); diff --git a/test/unit/commands/spaces/locks/get-all.test.ts b/test/unit/commands/spaces/locks/get-all.test.ts deleted file mode 100644 index 966cbe0c..00000000 --- a/test/unit/commands/spaces/locks/get-all.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { getMockAblySpaces } from "../../../../helpers/mock-ably-spaces.js"; -import { getMockAblyRealtime } from "../../../../helpers/mock-ably-realtime.js"; -import { parseNdjsonLines } from "../../../../helpers/ndjson.js"; -import { - standardHelpTests, - standardArgValidationTests, - standardFlagTests, -} from "../../../../helpers/standard-tests.js"; - -describe("spaces:locks:get-all command", () => { - beforeEach(() => { - // Initialize the mocks - getMockAblyRealtime(); - getMockAblySpaces(); - }); - - standardHelpTests("spaces:locks:get-all", import.meta.url); - standardArgValidationTests("spaces:locks:get-all", import.meta.url, { - requiredArgs: ["test-space"], - }); - standardFlagTests("spaces:locks:get-all", import.meta.url, ["--json"]); - - describe("functionality", () => { - it("should get all locks from a space", async () => { - const spacesMock = getMockAblySpaces(); - const space = spacesMock._getSpace("test-space"); - space.locks.getAll.mockResolvedValue([ - { - id: "lock-1", - member: { - clientId: "user-1", - connectionId: "conn-1", - isConnected: true, - profileData: null, - location: null, - lastEvent: { name: "enter", timestamp: Date.now() }, - }, - status: "locked", - timestamp: Date.now(), - attributes: undefined, - reason: undefined, - }, - ]); - - const { stdout } = await runCommand( - ["spaces:locks:get-all", "test-space", "--json"], - import.meta.url, - ); - - expect(space.enter).not.toHaveBeenCalled(); - expect(space.locks.getAll).toHaveBeenCalled(); - expect(stdout).toContain("locks"); - }); - - it("should output JSON envelope with type and command for lock results", async () => { - const spacesMock = getMockAblySpaces(); - const space = spacesMock._getSpace("test-space"); - space.locks.getAll.mockResolvedValue([ - { - id: "lock-1", - member: { - clientId: "user-1", - connectionId: "conn-1", - isConnected: true, - profileData: null, - location: null, - lastEvent: { name: "enter", timestamp: Date.now() }, - }, - status: "locked", - timestamp: Date.now(), - attributes: undefined, - reason: undefined, - }, - ]); - - const { stdout } = await runCommand( - ["spaces:locks:get-all", "test-space", "--json"], - import.meta.url, - ); - - const records = parseNdjsonLines(stdout); - const resultRecord = records.find( - (r) => r.type === "result" && Array.isArray(r.locks), - ); - expect(resultRecord).toBeDefined(); - expect(resultRecord).toHaveProperty("type", "result"); - expect(resultRecord).toHaveProperty("command"); - expect(resultRecord).toHaveProperty("success", true); - expect(resultRecord!.locks).toBeInstanceOf(Array); - expect(resultRecord!.locks[0]).toHaveProperty("id", "lock-1"); - expect(resultRecord!.locks[0]).toHaveProperty("member"); - expect(resultRecord!.locks[0].member).toHaveProperty( - "clientId", - "user-1", - ); - }); - - it("should handle no locks found", async () => { - const spacesMock = getMockAblySpaces(); - const space = spacesMock._getSpace("test-space"); - space.locks.getAll.mockResolvedValue([]); - - const { stdout } = await runCommand( - ["spaces:locks:get-all", "test-space", "--json"], - import.meta.url, - ); - - expect(stdout).toContain("locks"); - }); - }); - - describe("error handling", () => { - it("should handle errors gracefully", async () => { - const spacesMock = getMockAblySpaces(); - const space = spacesMock._getSpace("test-space"); - space.locks.getAll.mockRejectedValue(new Error("Failed to get locks")); - - const { error } = await runCommand( - ["spaces:locks:get-all", "test-space"], - import.meta.url, - ); - expect(error).toBeDefined(); - }); - }); -}); diff --git a/test/unit/commands/spaces/locks/get.test.ts b/test/unit/commands/spaces/locks/get.test.ts index 55e91dec..5a104e44 100644 --- a/test/unit/commands/spaces/locks/get.test.ts +++ b/test/unit/commands/spaces/locks/get.test.ts @@ -5,47 +5,21 @@ import { getMockAblyRealtime } from "../../../../helpers/mock-ably-realtime.js"; import { parseNdjsonLines } from "../../../../helpers/ndjson.js"; import { standardHelpTests, + standardArgValidationTests, standardFlagTests, } from "../../../../helpers/standard-tests.js"; describe("spaces:locks:get command", () => { beforeEach(() => { - // Initialize the mocks getMockAblyRealtime(); getMockAblySpaces(); }); standardHelpTests("spaces:locks:get", import.meta.url); - standardFlagTests("spaces:locks:get", import.meta.url, ["--json"]); - - describe("argument validation", () => { - it("should reject unknown flags", async () => { - const { error } = await runCommand( - ["spaces:locks:get", "test-space", "my-lock", "--unknown-flag-xyz"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error?.message).toMatch(/unknown|Nonexistent flag/i); - }); - - it("should require space argument", async () => { - const { error } = await runCommand(["spaces:locks:get"], import.meta.url); - - expect(error).toBeDefined(); - expect(error?.message).toMatch(/Missing .* required arg/); - }); - - it("should require lockId argument", async () => { - const { error } = await runCommand( - ["spaces:locks:get", "test-space"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error?.message).toMatch(/Missing .* required arg/); - }); + standardArgValidationTests("spaces:locks:get", import.meta.url, { + requiredArgs: ["test-space"], }); + standardFlagTests("spaces:locks:get", import.meta.url, ["--json"]); describe("functionality", () => { it("should get a specific lock by ID", async () => { @@ -77,7 +51,7 @@ describe("spaces:locks:get command", () => { expect(stdout).toContain("my-lock"); }); - it("should output JSON envelope with type and command for lock result", async () => { + it("should output JSON envelope with type and command for single lock result", async () => { const spacesMock = getMockAblySpaces(); const space = spacesMock._getSpace("test-space"); space.locks.get.mockResolvedValue({ @@ -130,10 +104,97 @@ describe("spaces:locks:get command", () => { expect(resultRecord).toBeDefined(); expect(resultRecord!.lock).toBeNull(); }); + + it("should get all locks when no lockId is provided", async () => { + const spacesMock = getMockAblySpaces(); + const space = spacesMock._getSpace("test-space"); + space.locks.getAll.mockResolvedValue([ + { + id: "lock-1", + member: { + clientId: "user-1", + connectionId: "conn-1", + isConnected: true, + profileData: null, + location: null, + lastEvent: { name: "enter", timestamp: Date.now() }, + }, + status: "locked", + timestamp: Date.now(), + attributes: undefined, + reason: undefined, + }, + ]); + + const { stdout } = await runCommand( + ["spaces:locks:get", "test-space", "--json"], + import.meta.url, + ); + + expect(space.enter).not.toHaveBeenCalled(); + expect(space.locks.getAll).toHaveBeenCalled(); + expect(stdout).toContain("locks"); + }); + + it("should output JSON envelope with type and command for all locks result", async () => { + const spacesMock = getMockAblySpaces(); + const space = spacesMock._getSpace("test-space"); + space.locks.getAll.mockResolvedValue([ + { + id: "lock-1", + member: { + clientId: "user-1", + connectionId: "conn-1", + isConnected: true, + profileData: null, + location: null, + lastEvent: { name: "enter", timestamp: Date.now() }, + }, + status: "locked", + timestamp: Date.now(), + attributes: undefined, + reason: undefined, + }, + ]); + + const { stdout } = await runCommand( + ["spaces:locks:get", "test-space", "--json"], + import.meta.url, + ); + + const records = parseNdjsonLines(stdout); + const resultRecord = records.find( + (r) => r.type === "result" && Array.isArray(r.locks), + ); + expect(resultRecord).toBeDefined(); + expect(resultRecord).toHaveProperty("type", "result"); + expect(resultRecord).toHaveProperty("command"); + expect(resultRecord).toHaveProperty("success", true); + expect(resultRecord!.locks).toBeInstanceOf(Array); + expect(resultRecord!.locks[0]).toHaveProperty("id", "lock-1"); + expect(resultRecord!.locks[0]).toHaveProperty("member"); + expect(resultRecord!.locks[0].member).toHaveProperty( + "clientId", + "user-1", + ); + }); + + it("should handle no locks found when getting all", async () => { + const spacesMock = getMockAblySpaces(); + const space = spacesMock._getSpace("test-space"); + space.locks.getAll.mockResolvedValue([]); + + const { stdout } = await runCommand( + ["spaces:locks:get", "test-space", "--json"], + import.meta.url, + ); + + expect(stdout).toContain("locks"); + }); }); describe("error handling", () => { - it("should handle errors gracefully", async () => { + it("should handle single lock get errors gracefully", async () => { const spacesMock = getMockAblySpaces(); const space = spacesMock._getSpace("test-space"); space.locks.get.mockRejectedValue(new Error("Failed to get lock")); @@ -144,5 +205,17 @@ describe("spaces:locks:get command", () => { ); expect(error).toBeDefined(); }); + + it("should handle get all locks errors gracefully", async () => { + const spacesMock = getMockAblySpaces(); + const space = spacesMock._getSpace("test-space"); + space.locks.getAll.mockRejectedValue(new Error("Failed to get locks")); + + const { error } = await runCommand( + ["spaces:locks:get", "test-space"], + import.meta.url, + ); + expect(error).toBeDefined(); + }); }); }); diff --git a/test/unit/commands/spaces/members/get-all.test.ts b/test/unit/commands/spaces/members/get.test.ts similarity index 88% rename from test/unit/commands/spaces/members/get-all.test.ts rename to test/unit/commands/spaces/members/get.test.ts index 6abda8e2..9c3356c0 100644 --- a/test/unit/commands/spaces/members/get-all.test.ts +++ b/test/unit/commands/spaces/members/get.test.ts @@ -9,17 +9,17 @@ import { standardFlagTests, } from "../../../../helpers/standard-tests.js"; -describe("spaces:members:get-all command", () => { +describe("spaces:members:get command", () => { beforeEach(() => { getMockAblyRealtime(); getMockAblySpaces(); }); - standardHelpTests("spaces:members:get-all", import.meta.url); - standardArgValidationTests("spaces:members:get-all", import.meta.url, { + standardHelpTests("spaces:members:get", import.meta.url); + standardArgValidationTests("spaces:members:get", import.meta.url, { requiredArgs: ["test-space"], }); - standardFlagTests("spaces:members:get-all", import.meta.url, ["--json"]); + standardFlagTests("spaces:members:get", import.meta.url, ["--json"]); describe("functionality", () => { it("should get all members from a space", async () => { @@ -37,7 +37,7 @@ describe("spaces:members:get-all command", () => { ]); const { stdout } = await runCommand( - ["spaces:members:get-all", "test-space", "--json"], + ["spaces:members:get", "test-space", "--json"], import.meta.url, ); @@ -61,7 +61,7 @@ describe("spaces:members:get-all command", () => { ]); const { stdout } = await runCommand( - ["spaces:members:get-all", "test-space", "--json"], + ["spaces:members:get", "test-space", "--json"], import.meta.url, ); @@ -84,7 +84,7 @@ describe("spaces:members:get-all command", () => { space.members.getAll.mockResolvedValue([]); const { stdout } = await runCommand( - ["spaces:members:get-all", "test-space", "--json"], + ["spaces:members:get", "test-space", "--json"], import.meta.url, ); @@ -106,7 +106,7 @@ describe("spaces:members:get-all command", () => { ]); const { stdout } = await runCommand( - ["spaces:members:get-all", "test-space"], + ["spaces:members:get", "test-space"], import.meta.url, ); @@ -127,7 +127,7 @@ describe("spaces:members:get-all command", () => { ); const { error } = await runCommand( - ["spaces:members:get-all", "test-space"], + ["spaces:members:get", "test-space"], import.meta.url, ); expect(error).toBeDefined(); From 6fc8d600f07a87371a409cfe9a48b7c7111c6a60 Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 25 Mar 2026 16:13:22 +0000 Subject: [PATCH 2/3] Remove --client-id flag from read-only get commands Ably capabilities are operation-based, not clientId-based, so client identity is irrelevant for pure read queries. Removed clientIdFlag from spaces members/locations/cursors/locks get and rooms occupancy get. Updated docs and skills to clarify when --client-id should be used. --- .claude/skills/ably-codebase-review/SKILL.md | 4 ++-- .claude/skills/ably-new-command/SKILL.md | 2 +- .claude/skills/ably-new-command/references/patterns.md | 2 +- .claude/skills/ably-review/SKILL.md | 2 +- src/commands/rooms/occupancy/get.ts | 3 +-- src/commands/spaces/cursors/get.ts | 3 +-- src/commands/spaces/locations/get.ts | 3 +-- src/commands/spaces/locks/get.ts | 3 +-- src/commands/spaces/members/get.ts | 3 +-- src/flags.ts | 3 ++- test/e2e/spaces/spaces-e2e.test.ts | 2 +- 11 files changed, 13 insertions(+), 17 deletions(-) diff --git a/.claude/skills/ably-codebase-review/SKILL.md b/.claude/skills/ably-codebase-review/SKILL.md index dbe59626..3a921fc2 100644 --- a/.claude/skills/ably-codebase-review/SKILL.md +++ b/.claude/skills/ably-codebase-review/SKILL.md @@ -132,14 +132,14 @@ Launch these agents **in parallel**. Each agent gets a focused mandate and uses - Subscribe/stream commands must have `durationFlag` - Subscribe commands with replay must have `rewindFlag` - History/stats commands must have `timeRangeFlags` - - Commands creating realtime connections or performing mutations (publish, update, delete, append) must have `clientIdFlag` + - Commands that perform writes (subscribe, publish, enter, set, acquire, update, delete, append) must have `clientIdFlag`; read-only queries (get, get-all, history, occupancy get) must NOT have `clientIdFlag` - Control API commands must use `ControlBaseCommand.globalFlags` **Method (LSP — for ambiguous cases):** 3. Use `LSP goToDefinition` on flag spread references to confirm they resolve to `src/flags.ts` (not a local redefinition) **Reasoning guidance:** -- A command that creates a realtime client or performs a mutation (publish, update, delete, append) but doesn't have `clientIdFlag` is a deviation +- A write command (subscribe, publish, enter, set, acquire, update, delete, append) without `clientIdFlag` is a deviation; a read-only query (get, get-all, history, occupancy get) WITH `clientIdFlag` is also a deviation - A non-subscribe command having `durationFlag` is suspicious but might be valid (e.g., presence enter) - Control API commands should NOT have `productApiFlags` diff --git a/.claude/skills/ably-new-command/SKILL.md b/.claude/skills/ably-new-command/SKILL.md index 2dc5f1c0..bd4dbc03 100644 --- a/.claude/skills/ably-new-command/SKILL.md +++ b/.claude/skills/ably-new-command/SKILL.md @@ -137,7 +137,7 @@ static flags = { - `await this.requireAppId(flags)` — resolves and validates the app ID, returns `Promise` (non-nullable). Calls `this.fail()` internally if no app found — no manual null check needed. - `await this.runControlCommand(flags, api => api.method(appId))` — creates the Control API client, executes the call, and handles errors in one step. Returns `Promise` (non-nullable). Useful for single API calls; for multi-step flows, use `this.createControlApi(flags)` directly. -**When to include `clientIdFlag`:** Add `...clientIdFlag` whenever the user might want to control which client identity performs the operation. This includes: presence enter/subscribe, spaces members, typing, cursors, publish, and any mutation where permissions may depend on the client (update, delete, annotate). The reason is that users may want to test auth scenarios — e.g., "can client B update client A's message?" — so they need the ability to set their client ID. +**When to include `clientIdFlag`:** Add `...clientIdFlag` to commands where client identity affects the operation: subscribe, publish, enter, set, acquire, update, delete, append, annotate. The reason is that users may want to test auth scenarios — e.g., "can client B update client A's message?" — so they need the ability to set their client ID. Do NOT add to read-only queries (get, get-all, history, occupancy get) — Ably capabilities are operation-based, not clientId-based, so client identity is irrelevant for pure reads. For history commands, also use `timeRangeFlags`: ```typescript diff --git a/.claude/skills/ably-new-command/references/patterns.md b/.claude/skills/ably-new-command/references/patterns.md index b3be44e1..778309f3 100644 --- a/.claude/skills/ably-new-command/references/patterns.md +++ b/.claude/skills/ably-new-command/references/patterns.md @@ -253,7 +253,7 @@ async run(): Promise { ## Get Pattern -Get commands perform one-shot queries for current state. They use REST clients and don't need `clientIdFlag`, `durationFlag`, or `rewindFlag`. +Get commands perform one-shot read-only queries for current state. They don't need `clientIdFlag` (Ably capabilities are operation-based, not clientId-based — client identity is irrelevant for reads), `durationFlag`, or `rewindFlag`. ```typescript static override flags = { diff --git a/.claude/skills/ably-review/SKILL.md b/.claude/skills/ably-review/SKILL.md index 6fd54073..df93b279 100644 --- a/.claude/skills/ably-review/SKILL.md +++ b/.claude/skills/ably-review/SKILL.md @@ -110,7 +110,7 @@ For each changed command file, run the relevant checks. Spawn agents for paralle **Flag architecture check (grep, with LSP for ambiguous cases):** 1. **Grep** for flag spreads (`productApiFlags`, `clientIdFlag`, `durationFlag`, `rewindFlag`, `timeRangeFlags`, `ControlBaseCommand.globalFlags`) 2. Verify correct flag sets per the skill rules -3. Check subscribe commands have `durationFlag`, `rewindFlag`, `clientIdFlag` as appropriate; mutation commands (publish, update, delete, append) should also have `clientIdFlag` +3. Check subscribe commands have `durationFlag`, `rewindFlag`, `clientIdFlag` as appropriate; write commands (publish, enter, set, acquire, update, delete, append) should also have `clientIdFlag`; read-only queries (get, get-all, history, occupancy get) must NOT have `clientIdFlag` 4. For ambiguous cases, use **LSP** `goToDefinition` to confirm flag imports resolve to `src/flags.ts` **JSON output check (grep/read):** diff --git a/src/commands/rooms/occupancy/get.ts b/src/commands/rooms/occupancy/get.ts index 31e9c28a..9d0e1033 100644 --- a/src/commands/rooms/occupancy/get.ts +++ b/src/commands/rooms/occupancy/get.ts @@ -1,7 +1,7 @@ import { Args } from "@oclif/core"; import { ChatClient, Room } from "@ably/chat"; import { ChatBaseCommand } from "../../../chat-base-command.js"; -import { clientIdFlag, productApiFlags } from "../../../flags.js"; +import { productApiFlags } from "../../../flags.js"; import { formatResource } from "../../../utils/output.js"; export default class RoomsOccupancyGet extends ChatBaseCommand { @@ -23,7 +23,6 @@ export default class RoomsOccupancyGet extends ChatBaseCommand { static override flags = { ...productApiFlags, - ...clientIdFlag, }; private chatClient: ChatClient | null = null; diff --git a/src/commands/spaces/cursors/get.ts b/src/commands/spaces/cursors/get.ts index 9eedad05..cdffdd8e 100644 --- a/src/commands/spaces/cursors/get.ts +++ b/src/commands/spaces/cursors/get.ts @@ -1,7 +1,7 @@ import { type CursorUpdate } from "@ably/spaces"; import { Args } from "@oclif/core"; -import { productApiFlags, clientIdFlag } from "../../../flags.js"; +import { productApiFlags } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatCountLabel, @@ -34,7 +34,6 @@ export default class SpacesCursorsGet extends SpacesBaseCommand { static override flags = { ...productApiFlags, - ...clientIdFlag, }; async run(): Promise { diff --git a/src/commands/spaces/locations/get.ts b/src/commands/spaces/locations/get.ts index 834be687..39b00547 100644 --- a/src/commands/spaces/locations/get.ts +++ b/src/commands/spaces/locations/get.ts @@ -1,6 +1,6 @@ import { Args } from "@oclif/core"; -import { productApiFlags, clientIdFlag } from "../../../flags.js"; +import { productApiFlags } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatCountLabel, @@ -31,7 +31,6 @@ export default class SpacesLocationsGet extends SpacesBaseCommand { static override flags = { ...productApiFlags, - ...clientIdFlag, }; async run(): Promise { diff --git a/src/commands/spaces/locks/get.ts b/src/commands/spaces/locks/get.ts index 45d2cd1a..09c77670 100644 --- a/src/commands/spaces/locks/get.ts +++ b/src/commands/spaces/locks/get.ts @@ -1,7 +1,7 @@ import type { Lock } from "@ably/spaces"; import { Args } from "@oclif/core"; -import { productApiFlags, clientIdFlag } from "../../../flags.js"; +import { productApiFlags } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatCountLabel, @@ -39,7 +39,6 @@ export default class SpacesLocksGet extends SpacesBaseCommand { static override flags = { ...productApiFlags, - ...clientIdFlag, }; async run(): Promise { diff --git a/src/commands/spaces/members/get.ts b/src/commands/spaces/members/get.ts index 23c3ab12..ba474563 100644 --- a/src/commands/spaces/members/get.ts +++ b/src/commands/spaces/members/get.ts @@ -1,7 +1,7 @@ import type { SpaceMember } from "@ably/spaces"; import { Args } from "@oclif/core"; -import { productApiFlags, clientIdFlag } from "../../../flags.js"; +import { productApiFlags } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatCountLabel, @@ -34,7 +34,6 @@ export default class SpacesMembersGet extends SpacesBaseCommand { static override flags = { ...productApiFlags, - ...clientIdFlag, }; async run(): Promise { diff --git a/src/flags.ts b/src/flags.ts index 6ed414e1..7bfbe2ff 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -61,7 +61,8 @@ export const hiddenControlApiFlags = { }; /** - * client-id flag for commands that support it (e.g., presence). + * client-id flag for commands where client identity matters (e.g., subscribe, publish, enter, update, delete). + * Not needed for read-only queries (get, get-all, occupancy get) — Ably capabilities are operation-based, not clientId-based. */ export const clientIdFlag = { "client-id": Flags.string({ diff --git a/test/e2e/spaces/spaces-e2e.test.ts b/test/e2e/spaces/spaces-e2e.test.ts index 5c03af3b..e578ca66 100644 --- a/test/e2e/spaces/spaces-e2e.test.ts +++ b/test/e2e/spaces/spaces-e2e.test.ts @@ -417,7 +417,7 @@ describe("Spaces E2E Tests", () => { // Test getAll functionality const getAllResult = await runBackgroundProcessAndGetOutput( - `bin/run.js spaces cursors get ${testSpaceId} --client-id ${client1Id}`, + `bin/run.js spaces cursors get ${testSpaceId}`, 15000, ); From b4816fa47be7973ae22ecabb39dacca7f2e013cb Mon Sep 17 00:00:00 2001 From: umair Date: Thu, 26 Mar 2026 13:22:00 +0000 Subject: [PATCH 3/3] Address PR review: fix locks get examples, enable enterSpace for lock syncing --- README.md | 60 +++++++++---------- src/commands/spaces/locks/get.ts | 18 +++++- test/unit/commands/spaces/cursors/get.test.ts | 40 ++++++------- test/unit/commands/spaces/locks/get.test.ts | 4 +- 4 files changed, 62 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 4420e09d..83a101ba 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ $ npm install -g @ably/cli $ ably COMMAND running command... $ ably (--version) -@ably/cli/0.17.0 darwin-arm64 node-v25.3.0 +@ably/cli/0.17.0 darwin-arm64 node-v24.4.1 $ ably --help [COMMAND] USAGE $ ably COMMAND @@ -4127,17 +4127,15 @@ Get current occupancy metrics for a room ``` USAGE - $ ably rooms occupancy get ROOM [-v] [--json | --pretty-json] [--client-id ] + $ ably rooms occupancy get ROOM [-v] [--json | --pretty-json] ARGUMENTS ROOM Room to get occupancy for FLAGS - -v, --verbose Output verbose logs - --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set - no client ID. Not applicable when using token authentication. - --json Output in JSON format - --pretty-json Output in colorized JSON format + -v, --verbose Output verbose logs + --json Output in JSON format + --pretty-json Output in colorized JSON format DESCRIPTION Get current occupancy metrics for a room @@ -4583,17 +4581,15 @@ Get all current cursors in a space ``` USAGE - $ ably spaces cursors get SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] + $ ably spaces cursors get SPACE_NAME [-v] [--json | --pretty-json] ARGUMENTS SPACE_NAME Name of the space to get cursors from FLAGS - -v, --verbose Output verbose logs - --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set - no client ID. Not applicable when using token authentication. - --json Output in JSON format - --pretty-json Output in colorized JSON format + -v, --verbose Output verbose logs + --json Output in JSON format + --pretty-json Output in colorized JSON format DESCRIPTION Get all current cursors in a space @@ -4779,17 +4775,15 @@ Get all current locations in a space ``` USAGE - $ ably spaces locations get SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] + $ ably spaces locations get SPACE_NAME [-v] [--json | --pretty-json] ARGUMENTS SPACE_NAME Name of the space to get locations from FLAGS - -v, --verbose Output verbose logs - --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set - no client ID. Not applicable when using token authentication. - --json Output in JSON format - --pretty-json Output in colorized JSON format + -v, --verbose Output verbose logs + --json Output in JSON format + --pretty-json Output in colorized JSON format DESCRIPTION Get all current locations in a space @@ -4936,18 +4930,16 @@ Get a lock or all locks in a space ``` USAGE - $ ably spaces locks get SPACE_NAME [LOCKID] [-v] [--json | --pretty-json] [--client-id ] + $ ably spaces locks get SPACE_NAME [LOCKID] [-v] [--json | --pretty-json] ARGUMENTS SPACE_NAME Name of the space to get locks from LOCKID Lock ID to get (omit to get all locks) FLAGS - -v, --verbose Output verbose logs - --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set - no client ID. Not applicable when using token authentication. - --json Output in JSON format - --pretty-json Output in colorized JSON format + -v, --verbose Output verbose logs + --json Output in JSON format + --pretty-json Output in colorized JSON format DESCRIPTION Get a lock or all locks in a space @@ -4957,9 +4949,13 @@ EXAMPLES $ ably spaces locks get my-space --json - $ ably spaces locks get my-space my-lock + $ ably spaces locks get my-space --pretty-json - $ ably spaces locks get my-space my-lock --json + $ ably spaces locks get my-space lock-id + + $ ably spaces locks get my-space lock-id --json + + $ ably spaces locks get my-space lock-id --pretty-json ``` _See code: [src/commands/spaces/locks/get.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/spaces/locks/get.ts)_ @@ -5061,17 +5057,15 @@ Get all members in a space ``` USAGE - $ ably spaces members get SPACE_NAME [-v] [--json | --pretty-json] [--client-id ] + $ ably spaces members get SPACE_NAME [-v] [--json | --pretty-json] ARGUMENTS SPACE_NAME Name of the space to get members from FLAGS - -v, --verbose Output verbose logs - --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set - no client ID. Not applicable when using token authentication. - --json Output in JSON format - --pretty-json Output in colorized JSON format + -v, --verbose Output verbose logs + --json Output in JSON format + --pretty-json Output in colorized JSON format DESCRIPTION Get all members in a space diff --git a/src/commands/spaces/locks/get.ts b/src/commands/spaces/locks/get.ts index 09c77670..4478e7e9 100644 --- a/src/commands/spaces/locks/get.ts +++ b/src/commands/spaces/locks/get.ts @@ -33,8 +33,10 @@ export default class SpacesLocksGet extends SpacesBaseCommand { static override examples = [ "$ ably spaces locks get my-space", "$ ably spaces locks get my-space --json", - "$ ably spaces locks get my-space my-lock", - "$ ably spaces locks get my-space my-lock --json", + "$ ably spaces locks get my-space --pretty-json", + "$ ably spaces locks get my-space lock-id", + "$ ably spaces locks get my-space lock-id --json", + "$ ably spaces locks get my-space lock-id --pretty-json", ]; static override flags = { @@ -47,7 +49,9 @@ export default class SpacesLocksGet extends SpacesBaseCommand { try { await this.initializeSpace(flags, spaceName, { - enterSpace: false, + // The SDK's Locks class stores locks in a Map that starts empty. + // Entering the space triggers syncing so locks.get()/getAll() return data. + enterSpace: true, setupConnectionLogging: false, }); @@ -66,6 +70,14 @@ export default class SpacesLocksGet extends SpacesBaseCommand { spaceName: string, lockId: string, ): Promise { + if (!this.shouldOutputJson(flags)) { + this.log( + formatProgress( + `Fetching lock ${formatResource(lockId)} from space ${formatResource(spaceName)}`, + ), + ); + } + const lock = await this.space!.locks.get(lockId); if (!lock) { diff --git a/test/unit/commands/spaces/cursors/get.test.ts b/test/unit/commands/spaces/cursors/get.test.ts index 14f79f77..60072c0a 100644 --- a/test/unit/commands/spaces/cursors/get.test.ts +++ b/test/unit/commands/spaces/cursors/get.test.ts @@ -67,27 +67,7 @@ describe("spaces:cursors:get command", () => { // The command outputs multiple JSON lines, last one has cursors array expect(stdout).toContain("cursors"); }); - }); - - describe("error handling", () => { - it("should handle getAll rejection gracefully", async () => { - const spacesMock = getMockAblySpaces(); - const space = spacesMock._getSpace("test-space"); - space.cursors.getAll.mockRejectedValue( - new Error("Failed to get cursors"), - ); - - const { error } = await runCommand( - ["spaces:cursors:get", "test-space"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(space.cursors.getAll).toHaveBeenCalled(); - }); - }); - describe("JSON output", () => { it("should output JSON envelope with type and command for cursor results", async () => { const spacesMock = getMockAblySpaces(); const space = spacesMock._getSpace("test-space"); @@ -115,9 +95,7 @@ describe("spaces:cursors:get command", () => { expect(resultRecord).toHaveProperty("success", true); expect(resultRecord!.cursors).toBeInstanceOf(Array); }); - }); - describe("cleanup behavior", () => { it("should leave space and close client on completion", async () => { const realtimeMock = getMockAblyRealtime(); const spacesMock = getMockAblySpaces(); @@ -133,4 +111,22 @@ describe("spaces:cursors:get command", () => { expect(realtimeMock.close).toHaveBeenCalled(); }); }); + + describe("error handling", () => { + it("should handle getAll rejection gracefully", async () => { + const spacesMock = getMockAblySpaces(); + const space = spacesMock._getSpace("test-space"); + space.cursors.getAll.mockRejectedValue( + new Error("Failed to get cursors"), + ); + + const { error } = await runCommand( + ["spaces:cursors:get", "test-space"], + import.meta.url, + ); + + expect(error).toBeDefined(); + expect(space.cursors.getAll).toHaveBeenCalled(); + }); + }); }); diff --git a/test/unit/commands/spaces/locks/get.test.ts b/test/unit/commands/spaces/locks/get.test.ts index 5a104e44..0995594f 100644 --- a/test/unit/commands/spaces/locks/get.test.ts +++ b/test/unit/commands/spaces/locks/get.test.ts @@ -46,7 +46,7 @@ describe("spaces:locks:get command", () => { import.meta.url, ); - expect(space.enter).not.toHaveBeenCalled(); + expect(space.enter).toHaveBeenCalled(); expect(space.locks.get).toHaveBeenCalledWith("my-lock"); expect(stdout).toContain("my-lock"); }); @@ -131,7 +131,7 @@ describe("spaces:locks:get command", () => { import.meta.url, ); - expect(space.enter).not.toHaveBeenCalled(); + expect(space.enter).toHaveBeenCalled(); expect(space.locks.getAll).toHaveBeenCalled(); expect(stdout).toContain("locks"); });