mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: make each plugin an npm workspace built by a termix-plugin CLI
This commit is contained in:
1 parent
4758016f41
commit
5796186a1c
448 files changed
+3087
-2003
No files matched your search
@@ -0,0 +1,98 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import type { AiProvider } from "@/api/ai-api";
|
||||
|
||||
const api = vi.hoisted(() => ({
|
||||
createAiProvider: vi.fn(),
|
||||
deleteAiProvider: vi.fn(),
|
||||
getAiProviderModels: vi.fn(),
|
||||
probeAiModels: vi.fn(),
|
||||
updateAiProvider: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/api/ai-api", () => api);
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
vi.mock("sonner", () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
import { AiProviderSettings } from "@/features/ai/AiProviderSettings";
|
||||
|
||||
const provider: AiProvider = {
|
||||
id: 7,
|
||||
providerType: "ollama",
|
||||
label: "Local Ollama",
|
||||
baseUrl: "http://localhost:11434",
|
||||
apiKeyPrefix: null,
|
||||
defaultModel: "llama3.1",
|
||||
enabled: true,
|
||||
createdAt: "2026-08-21T00:00:00Z",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
api.getAiProviderModels.mockReset();
|
||||
api.getAiProviderModels.mockResolvedValue([]);
|
||||
api.updateAiProvider.mockReset();
|
||||
api.updateAiProvider.mockResolvedValue(provider);
|
||||
});
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("AiProviderSettings", () => {
|
||||
it("edits an existing provider name and configured model", async () => {
|
||||
const onChanged = vi.fn();
|
||||
render(<AiProviderSettings providers={[provider]} onChanged={onChanged} />);
|
||||
|
||||
expect(screen.getByText(/llama3\.1/)).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "ai.editProvider" }));
|
||||
|
||||
const label = screen.getByLabelText("ai.providerLabel");
|
||||
const model = screen.getByLabelText("ai.defaultModel");
|
||||
fireEvent.change(label, { target: { value: "Production Ollama" } });
|
||||
fireEvent.change(model, { target: { value: "qwen3:32b" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "ai.save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.updateAiProvider).toHaveBeenCalledWith(7, {
|
||||
label: "Production Ollama",
|
||||
defaultModel: "qwen3:32b",
|
||||
});
|
||||
});
|
||||
expect(onChanged).toHaveBeenCalledWith(7);
|
||||
});
|
||||
|
||||
it("loads the saved provider model list when editing starts", async () => {
|
||||
api.getAiProviderModels.mockResolvedValue(["llama3.1", "qwen3:32b"]);
|
||||
render(<AiProviderSettings providers={[provider]} onChanged={() => {}} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "ai.editProvider" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.getAiProviderModels).toHaveBeenCalledWith(7);
|
||||
});
|
||||
expect(
|
||||
screen.getByRole("combobox", { name: "ai.defaultModel" }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows the provider error when refreshing models fails", async () => {
|
||||
api.getAiProviderModels.mockRejectedValue(
|
||||
new Error("Add llm.internal to the AI endpoint allowlist"),
|
||||
);
|
||||
render(<AiProviderSettings providers={[provider]} onChanged={() => {}} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "ai.editProvider" }));
|
||||
|
||||
expect(
|
||||
await screen.findByText("Add llm.internal to the AI endpoint allowlist"),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
fieldLabel,
|
||||
humanize,
|
||||
mentionLabel,
|
||||
toolLabel,
|
||||
} from "@/features/ai/labels";
|
||||
import { AI_TOOLS } from "../../src/backend/tools/catalog.js";
|
||||
|
||||
describe("humanize", () => {
|
||||
it("splits camelCase into words", () => {
|
||||
// The case that shipped as "thresholddurationseconds".
|
||||
expect(humanize("thresholdDurationSeconds")).toBe(
|
||||
"Threshold duration seconds",
|
||||
);
|
||||
});
|
||||
|
||||
it("splits snake_case into words", () => {
|
||||
expect(humanize("propose_create_automation")).toBe(
|
||||
"Propose create automation",
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves a single word alone apart from casing", () => {
|
||||
expect(humanize("port")).toBe("Port");
|
||||
});
|
||||
|
||||
it("returns the input when there is nothing to split", () => {
|
||||
expect(humanize("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fieldLabel", () => {
|
||||
it("uses the mapped name where there is one", () => {
|
||||
expect(fieldLabel("thresholdDurationSeconds")).toBe("Duration (seconds)");
|
||||
expect(fieldLabel("cooldownMinutes")).toBe("Cooldown (minutes)");
|
||||
expect(fieldLabel("ip")).toBe("Address");
|
||||
});
|
||||
|
||||
it("falls back to humanizing an unmapped key", () => {
|
||||
expect(fieldLabel("someFutureField")).toBe("Some future field");
|
||||
});
|
||||
|
||||
it("never returns a run-on identifier", () => {
|
||||
for (const key of ["thresholdDurationSeconds", "hostIds", "triggerType"]) {
|
||||
expect(fieldLabel(key)).not.toBe(key.toLowerCase());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("toolLabel", () => {
|
||||
it("phrases tools as actions rather than function names", () => {
|
||||
expect(toolLabel("list_hosts")).toBe("Reading hosts");
|
||||
expect(toolLabel("propose_create_fleet")).toBe("Create fleet");
|
||||
});
|
||||
|
||||
it("covers every tool in the catalog", () => {
|
||||
// A new tool without a label would surface its raw snake_case name.
|
||||
for (const tool of AI_TOOLS) {
|
||||
const label = toolLabel(tool.name);
|
||||
expect(label, tool.name).not.toContain("_");
|
||||
expect(label, tool.name).not.toBe(tool.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("mentionLabel", () => {
|
||||
it("capitalises the mention kinds", () => {
|
||||
expect(mentionLabel("host")).toBe("Host");
|
||||
expect(mentionLabel("snippet")).toBe("Snippet");
|
||||
expect(mentionLabel("automation")).toBe("Automation");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user