From 107cf55d8c4e3b176c62f3fdc70f9d5da9b1be87 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Mon, 28 Sep 2026 06:36:01 +0800 Subject: [PATCH] fix(split-screen): allow layout creation without randomUUID (#1490) * fix(split-screen): allow layout creation without randomUUID * test(credentials): avoid flaky Ed25519 key generation --- scripts/split-screen-selection.test.ts | 98 +++++++++++++++++++ .../tests/plugins/ctx-credentials.test.ts | 2 +- src/ui/AppShell.tsx | 5 +- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 scripts/split-screen-selection.test.ts diff --git a/scripts/split-screen-selection.test.ts b/scripts/split-screen-selection.test.ts new file mode 100644 index 000000000..5d0995941 --- /dev/null +++ b/scripts/split-screen-selection.test.ts @@ -0,0 +1,98 @@ +import { readFileSync } from "node:fs"; +import { runInNewContext } from "node:vm"; +import ts from "typescript"; +import { expect, it, vi } from "vitest"; +import { + assignTabsToSplit, + createSplitConfig, +} from "../src/ui/shell/splitTabUtils"; +import type { Tab, SplitMode } from "../src/types/ui-types"; + +const source = readFileSync( + new URL("../src/ui/AppShell.tsx", import.meta.url), + "utf8", +); +const ast = ts.createSourceFile( + "AppShell.tsx", + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, +); +const functions: string[] = []; +function visit(node: ts.Node) { + if ( + ts.isFunctionDeclaration(node) && + ["splitTabQuick", "selectSplitMode"].includes(node.name?.text ?? "") + ) + functions.push(node.getText(ast)); + ts.forEachChild(node, visit); +} +visit(ast); +const code = ts.transpileModule(functions.join("\n"), { + compilerOptions: { target: ts.ScriptTarget.ES2022 }, +}).outputText; + +function shell(crypto: object, activeTabId = "terminal-1") { + let tabs = [ + { id: "dashboard", type: "dashboard" }, + { id: "terminal-1", type: "terminal" }, + { id: "terminal-2", type: "terminal" }, + ] as Tab[]; + const setActiveTabId = vi.fn(); + const setSplitMode = vi.fn(); + const context = { + tabs, + activeTabId, + crypto, + PANE_COUNTS: { "2-way": 2, "4-way": 4 }, + defaultSizes: () => ({ rowSizes: [100], rowColSizes: [[50, 50]] }), + assignTabsToSplit, + createSplitConfig, + setTabs: (update: (tabs: Tab[]) => Tab[]) => { + tabs = update(tabs); + }, + setActiveTabId, + setSplitMode, + setPaneTabIds: vi.fn(), + setRowSizes: vi.fn(), + setRowColSizes: vi.fn(), + changeSplitMode: vi.fn(), + doCloseTab: vi.fn(), + }; + const select = runInNewContext(`${code}\nselectSplitMode`, context) as ( + mode: SplitMode, + ) => void; + return { select, getTabs: () => tabs, setActiveTabId, setSplitMode }; +} + +it.each(["terminal-1", "dashboard"])( + "selects a split layout without randomUUID from %s", + (active) => { + const state = shell({}, active); + state.select("2-way"); + const tabs = state.getTabs(); + const split = tabs.find((tab) => tab.type === "split-screen")!; + expect(split).toBeDefined(); + expect(split.instanceId).toBeTruthy(); + expect(split.splitConfig?.paneTabIds.slice(0, 2)).toEqual([ + "terminal-1", + "terminal-2", + ]); + expect(tabs.find((tab) => tab.id === "terminal-1")?.parentSplitTabId).toBe( + split.id, + ); + expect(state.setActiveTabId).toHaveBeenCalledWith(split.id); + expect(state.setSplitMode).toHaveBeenCalledWith("2-way"); + }, +); + +it("uses native UUIDs when available", () => { + const randomUUID = vi.fn(() => "native-id"); + const state = shell({ randomUUID }); + state.select("2-way"); + expect(randomUUID).toHaveBeenCalledOnce(); + expect( + state.getTabs().find((tab) => tab.type === "split-screen")?.instanceId, + ).toBe("native-id"); +}); diff --git a/src/backend/tests/plugins/ctx-credentials.test.ts b/src/backend/tests/plugins/ctx-credentials.test.ts index 63068f828..04ea3ff76 100644 --- a/src/backend/tests/plugins/ctx-credentials.test.ts +++ b/src/backend/tests/plugins/ctx-credentials.test.ts @@ -328,7 +328,7 @@ describe("ctx.credentials.registerSecretResolver", () => { }); describe("ctx.credentials.listSshKeys", () => { - const pair = ssh2.utils.generateKeyPairSync("ed25519"); + const pair = ssh2.utils.generateKeyPairSync("rsa", { bits: 2048 }); const derived = (() => { const parsed = ssh2.utils.parseKey(pair.private); const key = Array.isArray(parsed) ? parsed[0] : parsed; diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index df8f6e5fb..c0fcbda83 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -1926,7 +1926,10 @@ export function AppShell({ } const splitNumber = tabs.filter((tab) => tab.type === "split-screen").length + 1; - const instanceId = crypto.randomUUID(); + const instanceId = + typeof crypto.randomUUID === "function" + ? crypto.randomUUID() + : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`; const id = `split-${instanceId}`; const sizes = defaultSizes(mode); const splitTab: Tab = {