feat: point core frontend snippets pickers at the plugin (B2)

This commit is contained in:
LukeGus committed 2026-09-23 08:59:06 -05:00
1 parent e07221eaf3
commit 1eac0f54b0
22 files changed
+170 -2968

No files matched your search

+34 -17
View File
@@ -1,7 +1,8 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { dispatchKeybindingAction } from "../../lib/keybinding-dispatch";
import type { Terminal } from "@xterm/xterm";
import type { KeybindingDispatchContext } from "../../lib/keybinding-dispatch";
import * as actionRegistry from "../../shell/action-registry";
function makeContext(
overrides: Partial<KeybindingDispatchContext> = {},
@@ -25,7 +26,6 @@ function makeContext(
webSocketRef: { current: ws as unknown as WebSocket },
writeTextToClipboard: vi.fn().mockResolvedValue(true),
readTextFromClipboard: vi.fn().mockResolvedValue(""),
getSnippetById: vi.fn().mockResolvedValue(undefined),
sentData,
...overrides,
};
@@ -33,6 +33,10 @@ function makeContext(
}
describe("dispatchKeybindingAction", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("copy writes the selection to clipboard and clears it", () => {
const ctx = makeContext();
(ctx.terminal.getSelection as ReturnType<typeof vi.fn>).mockReturnValue(
@@ -84,45 +88,58 @@ describe("dispatchKeybindingAction", () => {
expect(ctx.sentData).toEqual(["ls -la\r"]);
});
it("runSnippet resolves the snippet and sends its content with a trailing return", async () => {
const ctx = makeContext({
getSnippetById: vi.fn().mockResolvedValue({ content: "uptime" }),
it("runSnippet resolves the snippet through the snippets.resolveForTerminal action and sends its content with a trailing return", async () => {
vi.spyOn(actionRegistry, "invokeAction").mockResolvedValue({
needsInputs: false,
content: "uptime",
});
const ctx = makeContext();
dispatchKeybindingAction({ type: "runSnippet", snippetId: "1" }, ctx);
await Promise.resolve();
await Promise.resolve();
expect(actionRegistry.invokeAction).toHaveBeenCalledWith(
"snippets.resolveForTerminal",
1,
null,
);
expect(ctx.sentData).toEqual(["uptime\r"]);
});
it("runSnippet does nothing when the snippet no longer exists", async () => {
const ctx = makeContext({
getSnippetById: vi.fn().mockResolvedValue(undefined),
});
vi.spyOn(actionRegistry, "invokeAction").mockResolvedValue(undefined);
const ctx = makeContext();
dispatchKeybindingAction({ type: "runSnippet", snippetId: "1" }, ctx);
await Promise.resolve();
await Promise.resolve();
expect(ctx.sentData).toEqual([]);
});
it("runSnippet silently resolves host variables from hostContext", async () => {
it("runSnippet passes hostContext through to the resolve action", async () => {
vi.spyOn(actionRegistry, "invokeAction").mockResolvedValue({
needsInputs: false,
content: "ping 10.0.0.5 -p 22",
});
const ctx = makeContext({
getSnippetById: vi
.fn()
.mockResolvedValue({ content: "ping $HOST -p $PORT" }),
hostContext: { ip: "10.0.0.5", username: "root", port: 22 },
});
dispatchKeybindingAction({ type: "runSnippet", snippetId: "1" }, ctx);
await Promise.resolve();
await Promise.resolve();
expect(actionRegistry.invokeAction).toHaveBeenCalledWith(
"snippets.resolveForTerminal",
1,
{ ip: "10.0.0.5", username: "root", port: 22 },
);
expect(ctx.sentData).toEqual(["ping 10.0.0.5 -p 22\r"]);
});
it("runSnippet defers to onSnippetNeedsInputs instead of sending when $INPUT_n is present", async () => {
const onSnippetNeedsInputs = vi.fn();
const ctx = makeContext({
getSnippetById: vi.fn().mockResolvedValue({ content: "echo $INPUT_1" }),
onSnippetNeedsInputs,
it("runSnippet defers to onSnippetNeedsInputs instead of sending when the action reports needsInputs", async () => {
vi.spyOn(actionRegistry, "invokeAction").mockResolvedValue({
needsInputs: true,
content: "echo $INPUT_1",
});
const onSnippetNeedsInputs = vi.fn();
const ctx = makeContext({ onSnippetNeedsInputs });
dispatchKeybindingAction({ type: "runSnippet", snippetId: "1" }, ctx);
await Promise.resolve();
await Promise.resolve();
@@ -1,79 +0,0 @@
import { describe, it, expect } from "vitest";
import {
extractSnippetInputs,
hasSnippetInputs,
resolveSnippetContent,
} from "../../lib/snippet-variables";
describe("extractSnippetInputs", () => {
it("extracts a single unlabeled input", () => {
expect(extractSnippetInputs("ping $INPUT_1")).toEqual([
{ key: "INPUT_1", label: "Input 1" },
]);
});
it("extracts a labeled input using brace syntax", () => {
expect(
extractSnippetInputs("nc -zv $HOST ${INPUT_1:Port to check}"),
).toEqual([{ key: "INPUT_1", label: "Port to check" }]);
});
it("plain (non-braced) references have no label", () => {
expect(extractSnippetInputs("echo $INPUT_2")).toEqual([
{ key: "INPUT_2", label: "Input 2" },
]);
});
it("de-duplicates repeated references and preserves first-seen order", () => {
expect(
extractSnippetInputs("$INPUT_2 then $INPUT_1 then $INPUT_2 again"),
).toEqual([
{ key: "INPUT_2", label: "Input 2" },
{ key: "INPUT_1", label: "Input 1" },
]);
});
it("returns an empty array when there are no placeholders", () => {
expect(extractSnippetInputs("uptime")).toEqual([]);
});
});
describe("hasSnippetInputs", () => {
it("returns true when a snippet has an input placeholder", () => {
expect(hasSnippetInputs("echo $INPUT_1")).toBe(true);
});
it("returns false when a snippet has no input placeholder", () => {
expect(hasSnippetInputs("echo $HOST")).toBe(false);
});
});
describe("resolveSnippetContent", () => {
const host = { ip: "10.0.0.5", username: "root", port: 22, name: "web-01" };
it("substitutes host variables", () => {
expect(
resolveSnippetContent("ssh $USER@$HOST -p $PORT # $NAME", host),
).toBe("ssh root@10.0.0.5 -p 22 # web-01");
});
it("supports brace syntax for host variables", () => {
expect(resolveSnippetContent("ping ${HOST}", host)).toBe("ping 10.0.0.5");
});
it("substitutes input placeholders", () => {
expect(
resolveSnippetContent("nc -zv $HOST ${INPUT_1:Port}", host, {
INPUT_1: "8080",
}),
).toBe("nc -zv 10.0.0.5 8080");
});
it("leaves host variables literal when no host context is given", () => {
expect(resolveSnippetContent("ping $HOST", null)).toBe("ping $HOST");
});
it("leaves input placeholders literal when no value was supplied", () => {
expect(resolveSnippetContent("echo $INPUT_1", null)).toBe("echo $INPUT_1");
});
});
@@ -51,7 +51,7 @@ describe("ONBOARDING_STEPS", () => {
describe("onboarding step body translations", () => {
const KEYS: Record<string, string[]> = {
welcome: ["hosts", "terminal", "files"],
feature: ["files", "tunnels", "snippets"],
feature: ["files", "tunnels"],
workflow: ["palette", "split", "dock"],
security: ["credentials", "twofa", "identity", "sharing"],
done: ["settings", "rerun", "docs"],
+3 -12
View File
@@ -59,7 +59,6 @@ describe("RAIL_ITEMS", () => {
"serial",
"ssh-tools",
"sftp",
"snippets",
"macros",
"history",
"session-logs",
@@ -74,10 +73,10 @@ describe("RAIL_ITEMS", () => {
);
});
it("marks exactly the four mobile primary slots", () => {
it("marks exactly the three mobile primary slots", () => {
expect(
RAIL_ITEMS.filter((item) => item.mobilePrimary).map((item) => item.id),
).toEqual(["hosts", "quick-connect", "ssh-tools", "snippets"]);
).toEqual(["hosts", "quick-connect", "ssh-tools"]);
});
it("marks the panels that can open as a tab", () => {
@@ -85,14 +84,7 @@ describe("RAIL_ITEMS", () => {
[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
.filter((item) => item.promotable)
.map((item) => item.id),
).toEqual([
"termix-id",
"ssh-tools",
"snippets",
"macros",
"history",
"session-logs",
]);
).toEqual(["termix-id", "ssh-tools", "macros", "history", "session-logs"]);
});
it("derives promotableIds from the promotable flag", () => {
@@ -132,7 +124,6 @@ describe("RAIL_ITEMS", () => {
expect(rightDockableIds()).toEqual([
"connections",
"ssh-tools",
"snippets",
"macros",
"history",
"session-logs",