Files
t8y2--dbx/packages/app-tests/redisCompletion.test.ts
T
2026-07-13 13:09:14 +08:00

148 lines
5.9 KiB
TypeScript
Vendored

import assert from "node:assert/strict";
import { test } from "vitest";
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);
}
test("command mode: completes command names by prefix (case-insensitive)", () => {
const items = buildRedisCompletionItems("GE", 2);
const names = labels(items);
assert.ok(names.includes("GET"));
assert.ok(names.includes("GETSET"));
assert.ok(names.includes("GETRANGE"));
// no subcommand forms leak in
assert.ok(!names.some((n) => n.includes(" ")));
// lowercase prefix also works
assert.ok(labels(buildRedisCompletionItems("ge", 2)).includes("GET"));
});
test("command mode: does not surface bare subcommand labels", () => {
const items = buildRedisCompletionItems("XGROUP", 6);
const names = labels(items);
assert.ok(names.includes("XGROUP"));
// "CREATE" alone is not a top-level command
assert.ok(!names.includes("CREATE"));
});
test("command mode: detail/info carry group + arity + safety", () => {
const items = buildRedisCompletionItems("FLUSHALL", 9);
const flush = items.find((item) => item.label === "FLUSHALL");
assert.ok(flush);
assert.match(flush!.detail ?? "", /server.*blocked/i);
assert.match(flush!.info ?? "", /Group:\s*server/i);
assert.match(flush!.info ?? "", /Arity:/i);
assert.match(flush!.info ?? "", /Safety:\s*blocked/i);
});
test("subcommand mode: after 'XGROUP ' suggests its subcommands", () => {
const items = buildRedisCompletionItems("XGROUP ", 7);
const names = labels(items);
assert.ok(names.includes("CREATE"));
assert.ok(names.includes("DESTROY"));
assert.ok(names.includes("SETID"));
// unrelated commands absent
assert.ok(!names.includes("GET"));
});
test("subcommand mode: filters subcommands by prefix", () => {
const items = buildRedisCompletionItems("XGROUP C", 8);
const names = labels(items);
assert.ok(names.includes("CREATE"));
assert.ok(names.includes("CREATECONSUMER"));
assert.ok(!names.includes("DESTROY"));
});
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);
assert.ok(names.includes("user:1"));
assert.ok(names.includes("user:2"));
});
test("argument mode: filters keys by substring prefix", () => {
const items = buildRedisCompletionItems("GET user", 8, { keys: ["user:1", "user:2", "config:db"] });
const names = labels(items);
assert.ok(names.includes("user:1"));
assert.ok(names.includes("user:2"));
assert.ok(!names.includes("config:db"));
});
test("argument mode: empty keys yields no key items", () => {
const items = buildRedisCompletionItems("GET ", 4, { keys: [] });
assert.equal(items.length, 0);
});
test("argument mode: does not suggest keys once the key argument is filled in", () => {
// GET takes exactly one key argument; after "GET key " the next slot is out of range.
assert.equal(buildRedisCompletionItems("GET key ", 8, { keys: ["user:1"] }).length, 0);
assert.equal(buildRedisCompletionItems("GET key extra ", 14, { keys: ["user:1"] }).length, 0);
// HSET takes key then field/value; after the key only non-key args remain.
assert.equal(buildRedisCompletionItems("HSET k ", 7, { keys: ["user:1"] }).length, 0);
});
test("argument mode: variadic key-list commands keep suggesting keys", () => {
// DEL accepts multiple keys — keep suggesting after the first one.
assert.ok(labels(buildRedisCompletionItems("DEL k1 ", 7, { keys: ["user:1"] })).includes("user:1"));
assert.ok(labels(buildRedisCompletionItems("EXISTS k1 k2 ", 13, { keys: ["user:1"] })).includes("user:1"));
});
test("argument mode: still suggests keys at the first argument slot", () => {
// GET <here> — first argument position
const items = buildRedisCompletionItems("GET ", 4, { keys: ["user:1", "user:2"] });
assert.ok(labels(items).includes("user:1"));
});
test("argument mode: non-key commands (FLUSHDB) do not suggest keys", () => {
const items = buildRedisCompletionItems("FLUSHDB ", 8, { keys: ["user:1", "user:2"] });
assert.equal(items.length, 0);
});
test("takesKeyArgument reflects command group", () => {
assert.equal(takesKeyArgument("GET"), true); // string
assert.equal(takesKeyArgument("HSET"), true); // hash
assert.equal(takesKeyArgument("DEL"), true); // generic
assert.equal(takesKeyArgument("FLUSHDB"), false); // server
assert.equal(takesKeyArgument("SELECT"), false); // connection
assert.equal(takesKeyArgument(undefined), false);
});
test("context parsing: empty line → command mode", () => {
const ctx = getRedisCompletionContext("", 0);
assert.equal(ctx.mode, "command");
assert.equal(ctx.prefix, "");
});
test("context parsing: command + space → subcommand mode when the command has subcommands", () => {
const ctx = getRedisCompletionContext("XGROUP ", 7);
assert.equal(ctx.mode, "subcommand");
assert.equal(ctx.mainCommand, "XGROUP");
});
test("context parsing: GET + space → argument mode", () => {
const ctx = getRedisCompletionContext("GET ", 4);
assert.equal(ctx.mode, "argument");
assert.equal(ctx.mainCommand, "GET");
});
test("shouldAutoOpenRedisCompletion opens on word/space chars, not newline", () => {
assert.equal(shouldAutoOpenRedisCompletion("GET", 3), true);
assert.equal(shouldAutoOpenRedisCompletion("GET ", 4), true); // space → offer keys/subcommands
assert.equal(shouldAutoOpenRedisCompletion("GET\n", 4), false);
assert.equal(shouldAutoOpenRedisCompletion("user:", 5), true);
});
test("result validFor covers command-name and key-name characters", () => {
const re = getRedisCompletionResultValidFor();
assert.equal(re.test("GET"), true);
assert.equal(re.test("user:1"), true);
assert.equal(re.test("a.b-c"), true);
});