feat: move snippets, keybinding actions and macros out of core into plugins

This commit is contained in:
LukeGus committed 2026-09-29 11:39:14 -05:00
1 parent d9bba10612
commit e0c30a4e6f
262 files changed
+9091 -10744

No files matched your search

@@ -98,6 +98,28 @@ describe("snippets activate", () => {
expect(after).toHaveLength(0);
});
it("registers its host settings hooks and the variables helper", async () => {
server = await startServer();
const registry = server.mock.ctx.registry;
for (const key of [
"snippets.hostImportNormalizer",
"snippets.hostPayloadLegacy",
"snippets.hostSettingsSync",
]) {
expect(registry.consume(key), key).toBeDefined();
}
const variables = registry.consume<{
resolve: (c: string, h: { ip?: string } | null) => string;
extractInputs: (c: string) => Array<{ key: string }>;
}>("snippets.variables");
expect(variables?.resolve("ping $HOST", { ip: "10.0.0.1" })).toBe(
"ping 10.0.0.1",
);
expect(variables?.extractInputs("echo $INPUT_1")).toEqual([
{ key: "INPUT_1", label: "Input 1" },
]);
});
it("fails closed without db:own", async () => {
db = await createTestDb(pluginDir);
const mock = createMockCtx({
@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import {
createHostSettingsSync,
hostImportNormalizer,
hostPayloadLegacy,
} from "../../src/backend/host-import.js";
describe("hostImportNormalizer", () => {
it("reads a 2.8 export's quick actions and startup snippet", () => {
expect(
hostImportNormalizer({
quickActions: [
{ name: "Restart", snippetId: "3" },
{ name: "Broken", snippetId: "" },
],
terminalConfig: JSON.stringify({ startupSnippetId: 5, fontSize: 12 }),
}),
).toEqual({
quickActions: [{ name: "Restart", snippetId: 3 }],
startupSnippetId: 5,
});
});
it("leaves an export that carries this plugin's settings to core", () => {
expect(
hostImportNormalizer({
pluginSettings: { snippets: { startupSnippetId: 1 } },
terminalConfig: { startupSnippetId: 5 },
}),
).toBeNull();
});
it("writes nothing for a host without either", () => {
expect(hostImportNormalizer({ name: "web" })).toBeNull();
});
});
describe("hostPayloadLegacy", () => {
it("puts the 2.8 fields back for older clients", () => {
expect(
hostPayloadLegacy({
quickActions: [{ name: "Up", snippetId: 2 }],
startupSnippetId: 7,
}),
).toEqual({
quickActions: [{ name: "Up", snippetId: 2 }],
terminalConfig: { startupSnippetId: 7 },
});
expect(hostPayloadLegacy({})).toEqual({ quickActions: [] });
});
});
describe("hostSettingsSync", () => {
const sync = createHostSettingsSync({
findSyncIdById: async (id) => (id === 2 ? "sync-2" : null),
findIdBySyncId: async (syncId) => (syncId === "sync-2" ? 9 : null),
});
it("sends sync ids and drops what does not map", async () => {
expect(await sync.exportValue("startupSnippetId", 2)).toBe("sync-2");
expect(await sync.exportValue("startupSnippetId", 4)).toBeNull();
expect(
await sync.exportValue("quickActions", [
{ name: "A", snippetId: 2 },
{ name: "B", snippetId: 4 },
]),
).toEqual([{ name: "A", snippetId: "sync-2" }]);
});
it("maps sync ids back to this side's rows", async () => {
expect(await sync.importValue("startupSnippetId", "sync-2")).toBe(9);
expect(await sync.importValue("startupSnippetId", "missing")).toBeNull();
expect(
await sync.importValue("quickActions", [
{ name: "A", snippetId: "sync-2" },
]),
).toEqual([{ name: "A", snippetId: 9 }]);
});
});
@@ -3,7 +3,7 @@ import {
extractSnippetInputs,
hasSnippetInputs,
resolveSnippetContent,
} from "../../src/frontend/snippet-variables";
} from "../../src/shared/variables.js";
describe("extractSnippetInputs", () => {
it("extracts a single unlabeled input", () => {
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { screen } from "@testing-library/react";
import { act, fireEvent, screen } from "@testing-library/react";
import { runKeybindingAction } from "@termix/plugin-sdk/frontend";
import {
renderWithApp,
type RenderedPluginApp,
@@ -41,11 +42,24 @@ describe(`${manifest.id} activate`, () => {
expect(railItem?.permission).toBe("snippets.view");
});
it("registers the snippets.resolveForTerminal action", async () => {
it("registers what other surfaces reach it through", async () => {
rendered = await renderWithApp(plugin, { manifest, locales });
expect(rendered.registered.actions()).toContain(
"snippets.resolveForTerminal",
expect(rendered.registered.actions()).toEqual(
expect.arrayContaining([
"snippets.startupCommand",
"snippets.list",
"snippets.extractInputs",
]),
);
expect(rendered.registered.paletteGroups()).toEqual(["snippets"]);
expect(rendered.registered.keybindingActions()).toEqual(["runSnippet"]);
expect(rendered.registered.hostEditorSections()).toEqual(["snippets"]);
expect(rendered.registered.slot("host-metrics.toolbar")).toEqual([
"snippets.quickActions",
]);
expect(rendered.registered.slot("shell.overlay")).toEqual([
"snippets.overlay",
]);
});
it("removes everything it registered on deactivate", async () => {
@@ -54,6 +68,143 @@ describe(`${manifest.id} activate`, () => {
expect(app.registered.panels()).toEqual([]);
expect(app.registered.railItems()).toEqual([]);
expect(app.registered.actions()).toEqual([]);
expect(app.registered.paletteGroups()).toEqual([]);
expect(app.registered.keybindingActions()).toEqual([]);
});
});
const deploy = {
id: 3,
name: "Deploy",
content: "deploy $HOST",
description: null,
isNote: false,
};
describe("snippets in the command palette", () => {
it("lists snippets that run in the target tab", async () => {
const api = { get: vi.fn(async () => ({ data: [deploy] })) };
rendered = await renderWithApp(plugin, {
manifest,
locales,
api: api as never,
permissions: ["snippets.view"],
});
const items = await rendered.loadPaletteGroup("snippets");
expect(items).toEqual([
expect.objectContaining({
id: "3",
title: "Deploy",
description: "deploy $HOST",
needsTarget: true,
}),
]);
});
it("offers nothing without the view permission", async () => {
rendered = await renderWithApp(plugin, {
manifest,
locales,
permissions: [],
});
expect(await rendered.loadPaletteGroup("snippets")).toEqual([]);
});
});
describe("the runSnippet keybinding", () => {
it("types the resolved snippet into the session it was pressed in", async () => {
const api = { get: vi.fn(async () => ({ data: deploy })) };
rendered = await renderWithApp(plugin, {
manifest,
locales,
api: api as never,
});
const send = vi.fn();
const handled = runKeybindingAction(
{ type: "runSnippet", snippetId: "3", appendEnter: true },
{ host: { ip: "10.0.0.9" }, send },
);
expect(handled).toBe(true);
await vi.waitFor(() =>
expect(send).toHaveBeenCalledWith("deploy 10.0.0.9\r"),
);
expect(api.get).toHaveBeenCalledWith("/3");
});
});
describe("the startup command", () => {
it("resolves the host's startup snippet, or nothing without one", async () => {
const api = { get: vi.fn(async () => ({ data: deploy })) };
rendered = await renderWithApp(plugin, {
manifest,
locales,
api: api as never,
});
const host = {
id: "1",
name: "web",
ip: "10.0.0.2",
port: 22,
pluginSettings: { snippets: { startupSnippetId: 3 } },
};
expect(
await rendered.app.invokeAction("snippets.startupCommand", host),
).toBe("deploy 10.0.0.2");
expect(
await rendered.app.invokeAction("snippets.startupCommand", {
...host,
pluginSettings: {},
}),
).toBeNull();
});
});
describe("the host editor's Snippets tab", () => {
it("writes the startup snippet and quick actions into this plugin's settings", async () => {
const api = { get: vi.fn(async () => ({ data: [deploy] })) };
rendered = await renderWithApp(plugin, {
manifest,
locales,
api: api as never,
});
let form: Record<string, unknown> = {
pluginSettings: { other: { keep: 1 } },
};
const updateForm = (
patch: (current: Record<string, unknown>) => Record<string, unknown>,
) => {
form = patch(form);
};
rendered.renderHostEditorSection("snippets", { form, updateForm });
const startup = await screen.findByRole("combobox", {
name: "Startup Snippet",
});
await screen.findAllByRole("option", { name: "Deploy" });
act(() => {
fireEvent.change(startup, { target: { value: "3" } });
});
expect(form.pluginSettings).toEqual({
other: { keep: 1 },
snippets: { startupSnippetId: 3 },
});
});
});
describe("the Host Metrics quick action buttons", () => {
it("draws a button per quick action and nothing without any", async () => {
rendered = await renderWithApp(plugin, {
manifest,
locales,
permissions: ["snippets.view"],
});
const host = {
id: 4,
pluginSettings: {
snippets: { quickActions: [{ name: "Restart", snippetId: 3 }] },
},
};
rendered.renderSlot("host-metrics.toolbar", { hostId: 4, host });
expect(await screen.findByRole("button", { name: "Restart" })).toBeTruthy();
});
});
@@ -31,22 +31,23 @@ afterEach(async () => {
rendered = null;
});
describe("snippet.list", () => {
it("lists the user's own snippets", async () => {
describe("snippet pickers while an admin edits another user's host", () => {
it("list the user's own snippets", async () => {
const api = fakeApi([{ id: 1, name: "a", content: "ls", isNote: true }]);
rendered = await renderWithApp(plugin, { manifest, locales, api });
const rows = await rendered.app.invokeAction("snippet.list");
rendered.renderHostEditorSection("snippets", { form: {} });
await screen.findAllByRole("option", { name: "a" });
expect(api.get).toHaveBeenCalledWith("/");
expect(rows).toEqual([
{ id: 1, name: "a", content: "ls", folder: null, isNote: true },
]);
});
it("lists another user's snippets for an admin", async () => {
it("list the target user's snippets", async () => {
const api = fakeApi([]);
rendered = await renderWithApp(plugin, { manifest, locales, api });
await rendered.app.invokeAction("snippet.list", { targetUserId: "u2" });
expect(api.get).toHaveBeenCalledWith("/", TARGET);
rendered.renderHostEditorSection("snippets", {
form: {},
adminTargetUserId: "u2",
});
await waitFor(() => expect(api.get).toHaveBeenCalledWith("/", TARGET));
});
});