From 7ca5e76c147e951e2f6e5825038a397dd6f7600a Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 22 Jul 2026 23:11:22 +0800 Subject: [PATCH] fix(redis): recognize OBJECT subcommands --- .../src/lib/redis/redisCommandTable.ts | 12 +++++++----- packages/app-tests/redisCommandTable.test.ts | 19 +++++++++++++++++++ packages/app-tests/redisCompletion.test.ts | 18 +++++++++++------- .../app-tests/redisSyntaxDiagnostics.test.ts | 18 ++++++++++++++++++ 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/lib/redis/redisCommandTable.ts b/apps/desktop/src/lib/redis/redisCommandTable.ts index 4fb2f18a2..5dd6dde7e 100644 --- a/apps/desktop/src/lib/redis/redisCommandTable.ts +++ b/apps/desktop/src/lib/redis/redisCommandTable.ts @@ -115,11 +115,13 @@ const RAW_COMMANDS: Record = { KEYS: [2, "generic", "blocked"], MIGRATE: [-6, "generic", "blocked"], MOVE: [3, "generic", "confirm"], - OBJECT_ENCODING: [3, "generic"], - OBJECT_FREQ: [3, "generic"], - OBJECT_IDLETIME: [3, "generic"], - OBJECT_REFCOUNT: [3, "generic"], - OBJECT_HELP: [2, "generic"], + // Redis exposes OBJECT operations as space-delimited subcommands; keep these + // keys aligned with COMMAND metadata so shared parsing and completion work. + "OBJECT ENCODING": [3, "generic"], + "OBJECT FREQ": [3, "generic"], + "OBJECT IDLETIME": [3, "generic"], + "OBJECT REFCOUNT": [3, "generic"], + "OBJECT HELP": [2, "generic"], PERSIST: [2, "generic", "confirm"], PEXPIRE: [-3, "generic", "confirm"], PEXPIREAT: [-3, "generic", "confirm"], diff --git a/packages/app-tests/redisCommandTable.test.ts b/packages/app-tests/redisCommandTable.test.ts index 8be1ef303..fe4db571b 100644 --- a/packages/app-tests/redisCommandTable.test.ts +++ b/packages/app-tests/redisCommandTable.test.ts @@ -59,6 +59,25 @@ test("resolveRedisCommandSpec resolves subcommand then main", () => { assert.equal(main?.group, "string"); }); +test("resolveRedisCommandSpec resolves every OBJECT subcommand with Redis arity", () => { + const expectedArities = new Map([ + ["ENCODING", 3], + ["FREQ", 3], + ["IDLETIME", 3], + ["REFCOUNT", 3], + ["HELP", 2], + ]); + + for (const [subcommand, arity] of expectedArities) { + const spec = resolveRedisCommandSpec(["OBJECT", subcommand]); + assert.ok(spec, `OBJECT ${subcommand} should resolve`); + assert.equal(spec.arity, arity); + assert.equal(spec.group, "generic"); + } + + assert.equal(resolveRedisCommandSpec(["OBJECT", "UNKNOWN"]), undefined); +}); + test("normal writes do not require confirmation but destructive commands do", () => { assert.equal(resolveRedisCommandSpec(["SET"])?.safety, "write"); assert.equal(resolveRedisCommandSpec(["HSET"])?.safety, "write"); diff --git a/packages/app-tests/redisCompletion.test.ts b/packages/app-tests/redisCompletion.test.ts index 26e77cced..3341f5b4f 100644 --- a/packages/app-tests/redisCompletion.test.ts +++ b/packages/app-tests/redisCompletion.test.ts @@ -1,12 +1,6 @@ import assert from "node:assert/strict"; import { test } from "vitest"; -import { - buildRedisCompletionItems, - getRedisCompletionContext, - getRedisCompletionResultValidFor, - shouldAutoOpenRedisCompletion, - takesKeyArgument, -} from "../../apps/desktop/src/lib/redis/redisCompletion.ts"; +import { buildRedisCompletionItems, getRedisCompletionContext, getRedisCompletionResultValidFor, shouldAutoOpenRedisCompletion, takesKeyArgument } from "../../apps/desktop/src/lib/redis/redisCompletion.ts"; function labels(items: { label: string }[]): string[] { return items.map((item) => item.label); @@ -60,6 +54,16 @@ test("subcommand mode: filters subcommands by prefix", () => { assert.ok(!names.includes("DESTROY")); }); +test("OBJECT completion uses Redis space-delimited subcommands", () => { + const commandNames = labels(buildRedisCompletionItems("OBJECT", 6)); + assert.ok(commandNames.includes("OBJECT")); + assert.ok(!commandNames.some((name) => name.startsWith("OBJECT_"))); + + const subcommands = labels(buildRedisCompletionItems("OBJECT ", 7)); + assert.deepEqual(new Set(subcommands), new Set(["ENCODING", "FREQ", "IDLETIME", "REFCOUNT", "HELP"])); + assert.ok(!subcommands.some((name) => name.includes("_"))); +}); + test("argument mode: offers key names for key-taking commands", () => { const items = buildRedisCompletionItems("GET ", 4, { keys: ["user:1", "user:2", "config:db"] }); const names = labels(items); diff --git a/packages/app-tests/redisSyntaxDiagnostics.test.ts b/packages/app-tests/redisSyntaxDiagnostics.test.ts index 21562bf39..f5ca958cd 100644 --- a/packages/app-tests/redisSyntaxDiagnostics.test.ts +++ b/packages/app-tests/redisSyntaxDiagnostics.test.ts @@ -90,6 +90,24 @@ test("subcommands resolve via MAIN SUB key", () => { assert.equal(diags.length, 0); }); +test("accepts all Redis OBJECT subcommands", () => { + for (const command of ["OBJECT ENCODING key", "OBJECT FREQ key", "OBJECT IDLETIME key", "OBJECT REFCOUNT key", "OBJECT HELP"]) { + assert.deepEqual(messages(command), [], `${command} should be valid`); + } +}); + +test("validates OBJECT subcommand arity and rejects unknown subcommands", () => { + for (const command of ["OBJECT ENCODING", "OBJECT FREQ", "OBJECT IDLETIME", "OBJECT REFCOUNT", "OBJECT HELP extra"]) { + const diags = buildRedisSyntaxDiagnostics(command); + assert.equal(diags.length, 1, `${command} should have one diagnostic`); + assert.match(diags[0].message, /Wrong number of arguments for 'OBJECT'/); + } + + const unknown = buildRedisSyntaxDiagnostics("OBJECT UNKNOWN key"); + assert.equal(unknown.length, 1); + assert.match(unknown[0].message, /Unknown command 'OBJECT'/); +}); + test("treats command names case-insensitively", () => { assert.deepEqual(messages("get foo"), []); const setDiag = buildRedisSyntaxDiagnostics("Set a b");