Dedupe and organize skills/sources (#8731)
This commit is contained in:
@@ -96,6 +96,7 @@ export function buildInitScript(options?: {
|
||||
content: s.instructions ?? s.content ?? "",
|
||||
directory: (s.path ?? ("/mock/.agents/skills/" + s.name + "/SKILL.md")).replace(/\\/SKILL\\.md$/, ""),
|
||||
global: true,
|
||||
supportingFiles: [],
|
||||
});
|
||||
|
||||
function nowIso() {
|
||||
@@ -195,26 +196,43 @@ export function buildInitScript(options?: {
|
||||
content: message.params?.content ?? "",
|
||||
directory: "/mock/.agents/skills/" + (message.params?.name ?? "new-skill"),
|
||||
global: message.params?.global ?? true,
|
||||
supportingFiles: [],
|
||||
},
|
||||
});
|
||||
case "_goose/sources/update":
|
||||
case "_goose/sources/update": {
|
||||
const path = message.params?.path ?? "/mock/.agents/skills/updated-skill";
|
||||
const nextName = message.params?.name;
|
||||
const name =
|
||||
typeof nextName === "string" && nextName.length > 0
|
||||
? nextName
|
||||
: String(path).split("/").filter(Boolean).at(-1) ?? "updated-skill";
|
||||
const segments = String(path).split("/").filter(Boolean);
|
||||
if (segments.length > 0) {
|
||||
segments[segments.length - 1] = name;
|
||||
}
|
||||
const directory = \`/\${segments.join("/")}\`;
|
||||
return jsonRpcResult(message.id, {
|
||||
source: {
|
||||
name: message.params?.name ?? "updated-skill",
|
||||
name,
|
||||
type: "skill",
|
||||
description: message.params?.description ?? "",
|
||||
content: message.params?.content ?? "",
|
||||
directory: "/mock/.agents/skills/" + (message.params?.name ?? "updated-skill"),
|
||||
global: message.params?.global ?? true,
|
||||
directory,
|
||||
global: true,
|
||||
supportingFiles: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
case "_goose/sources/delete":
|
||||
return jsonRpcResult(message.id, {});
|
||||
case "_goose/sources/export":
|
||||
case "_goose/sources/export": {
|
||||
const path = message.params?.path ?? "/mock/.agents/skills/skill";
|
||||
const name = String(path).split("/").filter(Boolean).at(-1) ?? "skill";
|
||||
return jsonRpcResult(message.id, {
|
||||
json: "{}",
|
||||
filename: (message.params?.name ?? "skill") + ".skill.json",
|
||||
filename: name + ".skill.json",
|
||||
});
|
||||
}
|
||||
case "_goose/sources/import":
|
||||
return jsonRpcResult(message.id, { sources: SKILLS.map(skillToSourceEntry) });
|
||||
default:
|
||||
|
||||
@@ -95,17 +95,17 @@ test.describe("Skills view", () => {
|
||||
await expect(nameInput).toHaveValue("my-skill-name");
|
||||
});
|
||||
|
||||
test("shows kebab-case validation error for trailing hyphen", async ({
|
||||
test("shows validation error for trailing hyphen", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByRole("button", { name: "New Skill" }).first().click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
// Type something that ends with a hyphen (the auto-formatter will produce "test-")
|
||||
await dialog.getByPlaceholder("my-skill-name").pressSequentially("test ");
|
||||
// The regex /^[a-z0-9]+(-[a-z0-9]+)*$/ won't match "test-", so error shows
|
||||
await expect(
|
||||
dialog.getByText("Must be kebab-case (e.g. code-review)"),
|
||||
dialog.getByText(
|
||||
"Use 1–64 lowercase letters, numbers, or hyphens. Names cannot start or end with a hyphen.",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -145,19 +145,45 @@ test.describe("Skills view", () => {
|
||||
await expect(menu.getByRole("menuitem", { name: "Delete" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Edit opens edit dialog with pre-filled fields", async ({
|
||||
test("Edit opens edit dialog with pre-filled editable fields", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByLabel("Options for code-review").click();
|
||||
await page.getByRole("menuitem", { name: "Edit" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.locator("h2", { hasText: "Edit Skill" })).toBeVisible();
|
||||
// Name should be pre-filled and read-only
|
||||
|
||||
const nameInput = dialog.getByPlaceholder("my-skill-name");
|
||||
const descriptionInput = dialog.getByPlaceholder(
|
||||
"What it does and when to use it...",
|
||||
);
|
||||
const instructionsInput = dialog.getByPlaceholder(
|
||||
"Markdown instructions the agent will follow...",
|
||||
);
|
||||
|
||||
await expect(nameInput).toHaveValue("code-review");
|
||||
await expect(nameInput).toHaveAttribute("readonly", "");
|
||||
await expect(descriptionInput).toHaveValue(
|
||||
"Reviews code for quality and best practices",
|
||||
);
|
||||
await expect(instructionsInput).toHaveValue(
|
||||
"When asked to review code, analyze the diff and provide feedback on code quality, potential bugs, and best practices.",
|
||||
);
|
||||
await expect(
|
||||
dialog.getByText(
|
||||
"Path on disk: /mock/.agents/skills/code-review/SKILL.md",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await nameInput.fill("renamed-skill");
|
||||
await expect(nameInput).toHaveValue("renamed-skill");
|
||||
await expect(
|
||||
dialog.getByText(
|
||||
"Path on disk: /mock/.agents/skills/renamed-skill/SKILL.md",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Delete triggers confirmation dialog", async ({ tauriMocked: page }) => {
|
||||
|
||||
Reference in New Issue
Block a user