diff --git a/src/components/engines/EnginesPage.tsx b/src/components/engines/EnginesPage.tsx
index b4a1c41..f176748 100644
--- a/src/components/engines/EnginesPage.tsx
+++ b/src/components/engines/EnginesPage.tsx
@@ -44,6 +44,7 @@ import { commands } from "@/bindings";
import { Route } from "@/routes/engines";
import { enginesAtom } from "@/state/atoms";
import {
+ duplicateEngine,
type Engine,
engineSchema,
type LocalEngine,
@@ -566,11 +567,7 @@ function EngineSettings({
leftSection={}
variant="default"
onClick={() => {
- const duplicatedEngine: LocalEngine = {
- ...engine,
- id: crypto.randomUUID(),
- name: `${engine.name} (Copy)`,
- };
+ const duplicatedEngine = duplicateEngine(engine);
setEngines(async (prev) => {
const copy = [...(await prev)];
copy.splice(selected + 1, 0, duplicatedEngine);
diff --git a/src/utils/engines.ts b/src/utils/engines.ts
index 30f3ae5..5c010d5 100644
--- a/src/utils/engines.ts
+++ b/src/utils/engines.ts
@@ -69,6 +69,14 @@ export type RemoteEngine = z.output;
export const engineSchema = z.union([localEngineSchema, remoteEngineSchema]);
export type Engine = z.output;
+export function duplicateEngine(engine: LocalEngine): LocalEngine {
+ return {
+ ...structuredClone(engine),
+ id: crypto.randomUUID(),
+ name: `${engine.name} (Copy)`,
+ };
+}
+
export function stopEngine(engine: LocalEngine, tab: string): Promise {
return commands.stopEngine(engine.id, tab).then((r) => {
unwrap(r);
diff --git a/src/utils/tests/engines.test.ts b/src/utils/tests/engines.test.ts
new file mode 100644
index 0000000..0ad3e46
--- /dev/null
+++ b/src/utils/tests/engines.test.ts
@@ -0,0 +1,49 @@
+import { expect, test } from "vitest";
+import { duplicateEngine, type LocalEngine } from "../engines";
+
+const stockfish: () => LocalEngine = () => ({
+ type: "local",
+ id: "stockfish",
+ name: "Stockfish",
+ version: "17",
+ path: "/usr/bin/stockfish",
+ go: { t: "Depth", c: 20 },
+ settings: [
+ { name: "MultiPV", value: 1 },
+ { name: "Threads", value: 4 },
+ { name: "Hash", value: 16 },
+ ],
+});
+
+test("duplicateEngine gives the copy a new id and name", () => {
+ const original = stockfish();
+ const copy = duplicateEngine(original);
+
+ expect(copy.id).not.toBe(original.id);
+ expect(copy.name).toBe("Stockfish (Copy)");
+ expect(copy.settings).toEqual(original.settings);
+ expect(copy.go).toEqual(original.go);
+});
+
+test("duplicateEngine does not share settings with the original", () => {
+ const original = stockfish();
+ const copy = duplicateEngine(original);
+
+ copy.settings!.find((s) => s.name === "Threads")!.value = 8;
+ copy.settings!.push({ name: "Ponder", value: true });
+
+ expect(original.settings).toEqual([
+ { name: "MultiPV", value: 1 },
+ { name: "Threads", value: 4 },
+ { name: "Hash", value: 16 },
+ ]);
+});
+
+test("duplicateEngine does not share the go mode with the original", () => {
+ const original = stockfish();
+ const copy = duplicateEngine(original);
+
+ (copy.go as { c: number }).c = 30;
+
+ expect(original.go).toEqual({ t: "Depth", c: 20 });
+});