mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 13:42:21 +00:00
528 lines
20 KiB
TypeScript
528 lines
20 KiB
TypeScript
import { expect, test, mock, spyOn, beforeEach, afterEach, describe } from "bun:test";
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import multiModelTool, { __testing_helpers } from "./tools/multi-model";
|
|
|
|
let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
|
let mockCommandResponses: Record<string, { ok: boolean; stdout: string; stderr: string }> = {};
|
|
let executedCommands: string[] = [];
|
|
|
|
function setupMockBun$() {
|
|
const mockFn = mock((strings: TemplateStringsArray, ...values: any[]) => {
|
|
const parts = values[0] as string[];
|
|
const commandSignature = parts.join(" ");
|
|
|
|
executedCommands.push(commandSignature);
|
|
|
|
return {
|
|
quiet: () => ({
|
|
nothrow: async () => {
|
|
const response = mockCommandResponses[commandSignature] ?? { ok: true, stdout: "", stderr: "" };
|
|
return {
|
|
exitCode: response.ok ? 0 : 1,
|
|
stdout: { toString: () => response.stdout },
|
|
stderr: { toString: () => response.stderr },
|
|
};
|
|
},
|
|
}),
|
|
};
|
|
});
|
|
|
|
(globalThis as any).Bun.$ = mockFn;
|
|
return mockFn;
|
|
}
|
|
|
|
function restoreOriginalBun$() {
|
|
(globalThis as any).Bun.$ = originalBun$;
|
|
}
|
|
|
|
describe("multi-model tool", () => {
|
|
let existsSyncMock: ReturnType<typeof spyOn>;
|
|
let bunMock: ReturnType<typeof setupMockBun$>;
|
|
|
|
beforeEach(() => {
|
|
spyOn(os, "homedir").mockReturnValue("/mock/home");
|
|
existsSyncMock = spyOn(fs, "existsSync").mockReturnValue(false);
|
|
bunMock = setupMockBun$();
|
|
executedCommands = [];
|
|
|
|
mockCommandResponses = {
|
|
"git rev-parse --is-inside-work-tree": { ok: true, stdout: "true", stderr: "" },
|
|
"command -v tmux": { ok: true, stdout: "/usr/bin/tmux", stderr: "" },
|
|
"command -v opencode": { ok: true, stdout: "/usr/bin/opencode", stderr: "" },
|
|
"opencode models": { ok: true, stdout: "openai/gpt-4o\nanthropic/claude-3-5-sonnet", stderr: "" },
|
|
"tmux has-session -t test-session": { ok: false, stdout: "", stderr: "session not found" },
|
|
"git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o": { ok: false, stdout: "", stderr: "" },
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
restoreOriginalBun$();
|
|
});
|
|
|
|
test("fails if session name is empty", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: `sessionName` must not be empty.");
|
|
expect(executedCommands).toEqual([]);
|
|
});
|
|
|
|
test("fails if no models are provided", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: [] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: Model names must not be empty.");
|
|
expect(executedCommands).toEqual([]);
|
|
});
|
|
|
|
test("fails if duplicate models are provided", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o", "openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Duplicate model names are not allowed");
|
|
expect(executedCommands).toEqual([]);
|
|
});
|
|
|
|
test("fails if not inside a git repository", async () => {
|
|
mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: false, stdout: "", stderr: "not a git repo" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: `multi-model` requires a git repository.");
|
|
expect(executedCommands).toEqual(["git rev-parse --is-inside-work-tree"]);
|
|
});
|
|
|
|
test("fails if tmux is not installed", async () => {
|
|
mockCommandResponses["command -v tmux"] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: `tmux` is not installed or not on `PATH`.");
|
|
expect(executedCommands).toEqual([
|
|
"git rev-parse --is-inside-work-tree",
|
|
"command -v tmux"
|
|
]);
|
|
});
|
|
|
|
test("fails if opencode is not installed", async () => {
|
|
mockCommandResponses["command -v opencode"] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: `opencode` is not installed or not on `PATH`.");
|
|
expect(executedCommands).toEqual([
|
|
"git rev-parse --is-inside-work-tree",
|
|
"command -v tmux",
|
|
"command -v opencode"
|
|
]);
|
|
});
|
|
|
|
test("fails if model name is not in allowlist", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["invalid/model"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: Model name 'invalid/model' not found");
|
|
expect(executedCommands).toEqual([
|
|
"git rev-parse --is-inside-work-tree",
|
|
"command -v tmux",
|
|
"command -v opencode",
|
|
"opencode models"
|
|
]);
|
|
});
|
|
|
|
test("fails if session already exists", async () => {
|
|
mockCommandResponses["tmux has-session -t test-session"] = { ok: true, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: Session already exists");
|
|
expect(executedCommands).toEqual([
|
|
"git rev-parse --is-inside-work-tree",
|
|
"command -v tmux",
|
|
"command -v opencode",
|
|
"opencode models",
|
|
"tmux has-session -t test-session"
|
|
]);
|
|
});
|
|
|
|
test("successfully plans and executes tmux launch with single model", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Use `tmux attach -t test-session` to join session");
|
|
expect(executedCommands.some(c => c.startsWith("tmux new-session -d -s test-session -n gpt-4o"))).toBe(true);
|
|
expect(executedCommands.some(c => c.startsWith("tmux send-keys -t test-session:gpt-4o"))).toBe(true);
|
|
});
|
|
|
|
test("successfully launches multiple models", async () => {
|
|
mockCommandResponses["git show-ref --verify --quiet refs/heads/opencode/test-session/claude-3-5-sonnet"] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Use `tmux attach -t test-session` to join session");
|
|
expect(executedCommands.some(c => c.startsWith("tmux new-session -d -s test-session -n gpt-4o"))).toBe(true);
|
|
expect(executedCommands.some(c => c.startsWith("tmux new-window -d -t test-session -n claude-3-5-sonnet"))).toBe(true);
|
|
});
|
|
|
|
test("sanitizes session name", async () => {
|
|
mockCommandResponses["tmux has-session -t test session!"] = { ok: false, stdout: "", stderr: "" };
|
|
mockCommandResponses["git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o"] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test session!", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Use `tmux attach -t test session!` to join session");
|
|
expect(executedCommands.some(c => c.startsWith("git worktree add -b opencode/test-session/gpt-4o"))).toBe(true);
|
|
});
|
|
|
|
test("fails if tmux new-session fails for the first model and triggers undo", async () => {
|
|
mockCommandResponses["tmux new-session -d -s test-session -n gpt-4o -c /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o"] = { ok: false, stdout: "", stderr: "tmux error" };
|
|
mockCommandResponses["git worktree remove -f /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o"] = { ok: true, stdout: "", stderr: "" };
|
|
mockCommandResponses["git branch -D opencode/test-session/gpt-4o"] = { ok: true, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Failed to create tmux session");
|
|
expect(executedCommands).toEqual([
|
|
"git rev-parse --is-inside-work-tree",
|
|
"command -v tmux",
|
|
"command -v opencode",
|
|
"opencode models",
|
|
"tmux has-session -t test-session",
|
|
"git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o",
|
|
"git worktree add -b opencode/test-session/gpt-4o /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o",
|
|
"tmux new-session -d -s test-session -n gpt-4o -c /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o",
|
|
"git worktree remove -f /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o",
|
|
"git branch -D opencode/test-session/gpt-4o"
|
|
]);
|
|
});
|
|
|
|
test("fails if worktree path exists for first model", async () => {
|
|
existsSyncMock.mockImplementation((path: string) => {
|
|
return path.includes("test-session/gpt-4o");
|
|
});
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Worktree path already exists");
|
|
expect(executedCommands).toEqual([
|
|
"git rev-parse --is-inside-work-tree",
|
|
"command -v tmux",
|
|
"command -v opencode",
|
|
"opencode models",
|
|
"tmux has-session -t test-session"
|
|
]);
|
|
});
|
|
|
|
test("fails if git branch exists for first model", async () => {
|
|
mockCommandResponses["git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o"] = { ok: true, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Branch 'opencode/test-session/gpt-4o' already exists");
|
|
expect(executedCommands).toEqual([
|
|
"git rev-parse --is-inside-work-tree",
|
|
"command -v tmux",
|
|
"command -v opencode",
|
|
"opencode models",
|
|
"tmux has-session -t test-session",
|
|
"git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o"
|
|
]);
|
|
});
|
|
|
|
test("fails if git worktree add fails for first model", async () => {
|
|
mockCommandResponses["git worktree add -b opencode/test-session/gpt-4o /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o"] = { ok: false, stdout: "", stderr: "git error" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Failed to create worktree");
|
|
expect(executedCommands).toEqual([
|
|
"git rev-parse --is-inside-work-tree",
|
|
"command -v tmux",
|
|
"command -v opencode",
|
|
"opencode models",
|
|
"tmux has-session -t test-session",
|
|
"git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o",
|
|
"git worktree add -b opencode/test-session/gpt-4o /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o"
|
|
]);
|
|
});
|
|
|
|
test("model name collision creates unique window names", async () => {
|
|
mockCommandResponses["opencode models"] = { ok: true, stdout: "openai/gpt-4o\nanthropic/gpt-4o", stderr: "" };
|
|
mockCommandResponses["git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o"] = { ok: false, stdout: "", stderr: "" };
|
|
mockCommandResponses["git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o-2"] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o", "anthropic/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Use `tmux attach -t test-session` to join session");
|
|
expect(executedCommands.some(c => c.startsWith("tmux new-session -d -s test-session -n gpt-4o"))).toBe(true);
|
|
expect(executedCommands.some(c => c.startsWith("tmux new-window -d -t test-session -n gpt-4o-2"))).toBe(true);
|
|
});
|
|
|
|
test("long model name is truncated in window name", async () => {
|
|
const longModel = "verylongmodelfrontexampleprovider";
|
|
mockCommandResponses["opencode models"] = { ok: true, stdout: longModel, stderr: "" };
|
|
mockCommandResponses[`git show-ref --verify --quiet refs/heads/opencode/test-session/${longModel.slice(0, 24)}`] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: [longModel] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Use `tmux attach -t test-session` to join session");
|
|
const windowName = executedCommands.find(c => c.includes("tmux new-session"))?.match(/-n (\S+)/)?.[1];
|
|
expect(windowName?.length).toBeLessThanOrEqual(24);
|
|
});
|
|
});
|
|
|
|
describe("__testing_helpers", () => {
|
|
describe("shellQuote", () => {
|
|
test("normal string without special characters", () => {
|
|
expect(__testing_helpers.shellQuote("test")).toBe("'test'");
|
|
});
|
|
|
|
test("string with spaces", () => {
|
|
expect(__testing_helpers.shellQuote("test value")).toBe("'test value'");
|
|
});
|
|
|
|
test("string with single quotes", () => {
|
|
expect(__testing_helpers.shellQuote("test'value")).toBe("'test'\"'\"'value'");
|
|
});
|
|
|
|
test("string with multiple single quotes", () => {
|
|
expect(__testing_helpers.shellQuote("te's't'v'alue")).toBe("'te'\"'\"'s'\"'\"'t'\"'\"'v'\"'\"'alue'");
|
|
});
|
|
|
|
test("empty string", () => {
|
|
expect(__testing_helpers.shellQuote("")).toBe("''");
|
|
});
|
|
});
|
|
|
|
describe("normalizeModels", () => {
|
|
test("empty array", () => {
|
|
expect(__testing_helpers.normalizeModels([])).toEqual([]);
|
|
});
|
|
|
|
test("undefined input", () => {
|
|
expect(__testing_helpers.normalizeModels(undefined)).toEqual([]);
|
|
});
|
|
|
|
test("array with whitespace-only strings", () => {
|
|
expect(__testing_helpers.normalizeModels([" ", " "])).toEqual([]);
|
|
});
|
|
|
|
test("array with mixed valid models, padded models, and empty strings", () => {
|
|
expect(__testing_helpers.normalizeModels([" openai/gpt-4o ", "", " anthropic/claude "])).toEqual(["openai/gpt-4o", "anthropic/claude"]);
|
|
});
|
|
});
|
|
|
|
describe("findDuplicates", () => {
|
|
test("array with no duplicates", () => {
|
|
expect(__testing_helpers.findDuplicates(["a", "b", "c"])).toEqual([]);
|
|
});
|
|
|
|
test("array with one duplicate pair", () => {
|
|
expect(__testing_helpers.findDuplicates(["a", "b", "a"])).toEqual(["a"]);
|
|
});
|
|
|
|
test("array with multiple different duplicates", () => {
|
|
expect(__testing_helpers.findDuplicates(["a", "b", "a", "c", "b"])).toEqual(["a", "b"]);
|
|
});
|
|
|
|
test("array with a single value repeated more than twice", () => {
|
|
expect(__testing_helpers.findDuplicates(["a", "a", "a"])).toEqual(["a"]);
|
|
});
|
|
|
|
test("verifying first-repeated-occurrence order", () => {
|
|
expect(__testing_helpers.findDuplicates(["x", "a", "b", "a", "x", "b"])).toEqual(["a", "x", "b"]);
|
|
});
|
|
});
|
|
|
|
describe("createWindowBaseName", () => {
|
|
test("normal model name without slashes", () => {
|
|
expect(__testing_helpers.createWindowBaseName("gpt-4o")).toBe("gpt-4o");
|
|
});
|
|
|
|
test("model with slashes", () => {
|
|
expect(__testing_helpers.createWindowBaseName("vendor/namespace/model")).toBe("model");
|
|
});
|
|
|
|
test("model exceeding WINDOW_NAME_LIMIT", () => {
|
|
const longName = "a".repeat(30);
|
|
expect(__testing_helpers.createWindowBaseName(longName)).toBe("aaaaaaaaaaaaaaaaaaaaaaaa");
|
|
});
|
|
|
|
test("model with special characters", () => {
|
|
expect(__testing_helpers.createWindowBaseName("OpenAI/GPT_4.0!")).toBe("gpt-4-0");
|
|
});
|
|
|
|
test("model made entirely of special characters", () => {
|
|
expect(__testing_helpers.createWindowBaseName("!@#$%^&*()")).toBe("model");
|
|
});
|
|
});
|
|
|
|
describe("createWindowPlans", () => {
|
|
test("single model", () => {
|
|
expect(__testing_helpers.createWindowPlans(["openai/gpt-4o"])).toEqual([{ model: "openai/gpt-4o", windowName: "gpt-4o" }]);
|
|
});
|
|
|
|
test("two models with the same base name", () => {
|
|
expect(__testing_helpers.createWindowPlans(["openai/gpt-4o", "anthropic/gpt-4o"])).toEqual([
|
|
{ model: "openai/gpt-4o", windowName: "gpt-4o" },
|
|
{ model: "anthropic/gpt-4o", windowName: "gpt-4o-2" }
|
|
]);
|
|
});
|
|
|
|
test("multiple collisions", () => {
|
|
const result = __testing_helpers.createWindowPlans(["a", "b", "c", "a", "b", "a"]);
|
|
expect(result[0]?.windowName).toBe("a");
|
|
expect(result[3]?.windowName).toBe("a-2");
|
|
expect(result[5]?.windowName).toBe("a-3");
|
|
});
|
|
|
|
test("truncation to accommodate suffix", () => {
|
|
const longBase = "a".repeat(23);
|
|
const result = __testing_helpers.createWindowPlans([longBase, "b"]);
|
|
expect(result[1]?.windowName.length).toBeLessThanOrEqual(24);
|
|
});
|
|
});
|
|
|
|
describe("levenshtein", () => {
|
|
test("identical strings", () => {
|
|
expect(__testing_helpers.levenshtein("test", "test")).toBe(0);
|
|
});
|
|
|
|
test("one substitution", () => {
|
|
expect(__testing_helpers.levenshtein("test", "tent")).toBe(1);
|
|
});
|
|
|
|
test("one insertion", () => {
|
|
expect(__testing_helpers.levenshtein("test", "tests")).toBe(1);
|
|
});
|
|
|
|
test("one deletion", () => {
|
|
expect(__testing_helpers.levenshtein("tests", "test")).toBe(1);
|
|
});
|
|
|
|
test("completely different strings", () => {
|
|
expect(__testing_helpers.levenshtein("abc", "xyz")).toBe(3);
|
|
});
|
|
|
|
test("one empty string", () => {
|
|
expect(__testing_helpers.levenshtein("", "test")).toBe(4);
|
|
});
|
|
|
|
test("both empty strings", () => {
|
|
expect(__testing_helpers.levenshtein("", "")).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("suggestModels", () => {
|
|
test("exact match", () => {
|
|
const result = __testing_helpers.suggestModels("gpt-4o", ["gpt-4o", "gpt-4o-mini"]);
|
|
expect(result[0]).toBe("gpt-4o");
|
|
});
|
|
|
|
test("fuzzy match/typo", () => {
|
|
expect(__testing_helpers.suggestModels("gpt5.4", ["gpt-5.4"])).toEqual(["gpt-5.4"]);
|
|
});
|
|
|
|
test("fuzzy match with contains boost", () => {
|
|
const suggestions = __testing_helpers.suggestModels("gpt4o", ["gpt-4o", "gpt-4o-mini"]);
|
|
expect(suggestions[0]).toBe("gpt-4o");
|
|
});
|
|
|
|
test("maximum of 3 suggestions", () => {
|
|
const allowlist = ["gpt-4o", "gpt-4o-mini", "gpt-4o-pro", "gpt-4o-ultra"];
|
|
expect(__testing_helpers.suggestModels("gpt4o", allowlist).length).toBe(3);
|
|
});
|
|
|
|
test("sorting logic", () => {
|
|
const suggestions = __testing_helpers.suggestModels("abc", ["abc", "abd", "abe"]);
|
|
expect(suggestions).toEqual(["abc", "abd", "abe"]);
|
|
});
|
|
});
|
|
|
|
describe("formatInvalidModelError", () => {
|
|
test("single invalid model with available suggestions", () => {
|
|
const error = __testing_helpers.formatInvalidModelError(["gpt5.4"], ["gpt-5.4", "gpt-4o"]);
|
|
expect(error).toContain("Model name 'gpt5.4' not found");
|
|
expect(error).toContain("Did you mean");
|
|
});
|
|
|
|
test("single invalid model without close matches", () => {
|
|
const error = __testing_helpers.formatInvalidModelError(["xyz"], ["abc", "def"]);
|
|
expect(error).toContain("Model name 'xyz' not found");
|
|
expect(error).toContain("Did you mean");
|
|
});
|
|
|
|
test("multiple invalid models", () => {
|
|
const error = __testing_helpers.formatInvalidModelError(["xyz", "abc"], ["def"]);
|
|
expect(error).toContain("Model names not found");
|
|
expect(error).toContain("'xyz'");
|
|
expect(error).toContain("'abc'");
|
|
});
|
|
});
|
|
|
|
describe("sanitizeName", () => {
|
|
test("normal alphanumeric name", () => {
|
|
expect(__testing_helpers.sanitizeName("test-session")).toBe("test-session");
|
|
});
|
|
|
|
test("name with spaces and special chars", () => {
|
|
expect(__testing_helpers.sanitizeName("test session!")).toBe("test-session");
|
|
});
|
|
|
|
test("name with consecutive hyphens", () => {
|
|
expect(__testing_helpers.sanitizeName("test--session")).toBe("test-session");
|
|
});
|
|
|
|
test("name with leading/trailing hyphens", () => {
|
|
expect(__testing_helpers.sanitizeName("-test-session-")).toBe("test-session");
|
|
});
|
|
});
|
|
});
|