Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
337 lines
12 KiB
TypeScript
337 lines
12 KiB
TypeScript
import { buildSkillMd, formatSkillsCatalogInstruction, parseSkillMd } from "@rakazo/core";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { BUILTIN_AGENT_SKILLS } from "./builtin-skills.js";
|
|
import {
|
|
listAgentSkillRecords,
|
|
skillCreateFromTool,
|
|
skillDeleteFromTool,
|
|
skillReadFromTool,
|
|
skillUpdateFromTool,
|
|
} from "./skill-tools.js";
|
|
|
|
function makePrisma(rows: Array<Record<string, unknown>> = []) {
|
|
const store = [...rows];
|
|
return {
|
|
agentSkill: {
|
|
findMany: vi.fn(async ({ where }: { where?: Record<string, string> } = {}) =>
|
|
store
|
|
.filter((row) => {
|
|
if (!where) return true;
|
|
if (where.spaceId && row.spaceId !== where.spaceId) return false;
|
|
if (where.userId && row.userId !== where.userId) return false;
|
|
return true;
|
|
})
|
|
.map((row) => ({ ...row })),
|
|
),
|
|
create: vi.fn(async ({ data }: { data: Record<string, unknown> }) => {
|
|
const row = {
|
|
id: `skill-${store.length + 1}`,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
source: "user",
|
|
...data,
|
|
};
|
|
store.push(row);
|
|
return row;
|
|
}),
|
|
update: vi.fn(
|
|
async ({ where, data }: { where: { id: string }; data: Record<string, unknown> }) => {
|
|
const index = store.findIndex((row) => row.id === where.id);
|
|
if (index < 0) throw new Error("missing");
|
|
store[index] = { ...store[index], ...data, updatedAt: new Date() };
|
|
return store[index];
|
|
},
|
|
),
|
|
updateMany: vi.fn(
|
|
async ({
|
|
where,
|
|
data,
|
|
}: {
|
|
where: Record<string, string>;
|
|
data: Record<string, unknown>;
|
|
}) => {
|
|
const index = store.findIndex(
|
|
(row) =>
|
|
row.id === where.id &&
|
|
row.spaceId === where.spaceId &&
|
|
row.userId === where.userId &&
|
|
row.source === where.source,
|
|
);
|
|
if (index < 0) return { count: 0 };
|
|
store[index] = { ...store[index], ...data, updatedAt: new Date() };
|
|
return { count: 1 };
|
|
},
|
|
),
|
|
delete: vi.fn(async ({ where }: { where: { id: string } }) => {
|
|
const index = store.findIndex((row) => row.id === where.id);
|
|
if (index < 0) throw new Error("missing");
|
|
const [removed] = store.splice(index, 1);
|
|
return removed;
|
|
}),
|
|
deleteMany: vi.fn(async ({ where }: { where: Record<string, string> }) => {
|
|
const index = store.findIndex(
|
|
(row) =>
|
|
row.id === where.id &&
|
|
row.spaceId === where.spaceId &&
|
|
row.userId === where.userId &&
|
|
row.source === where.source,
|
|
);
|
|
if (index < 0) return { count: 0 };
|
|
store.splice(index, 1);
|
|
return { count: 1 };
|
|
}),
|
|
},
|
|
_store: store,
|
|
};
|
|
}
|
|
|
|
const owner = { spaceId: "ws-1", userId: "user-1" };
|
|
|
|
describe("skill tools", () => {
|
|
let prisma: ReturnType<typeof makePrisma>;
|
|
|
|
beforeEach(() => {
|
|
prisma = makePrisma();
|
|
});
|
|
|
|
it("creates, reads, updates, and deletes user skills", async () => {
|
|
const created = await skillCreateFromTool(prisma as never, owner, {
|
|
name: "Daily standup",
|
|
description: "Prepare standup notes",
|
|
body: "1. Summarize yesterday.\n2. List blockers.",
|
|
});
|
|
expect(created).toMatchObject({ ok: true, name: "Daily standup" });
|
|
expect(String(created.hint)).toContain("/Daily standup");
|
|
|
|
const read = await skillReadFromTool(prisma as never, owner, { name: "Daily standup" });
|
|
expect(read).toMatchObject({
|
|
name: "Daily standup",
|
|
description: "Prepare standup notes",
|
|
readOnly: false,
|
|
});
|
|
expect(String(read.content)).toContain("Summarize yesterday");
|
|
|
|
const updated = await skillUpdateFromTool(prisma as never, owner, {
|
|
name: "Daily standup",
|
|
description: "Updated when-to-use",
|
|
});
|
|
expect(updated).toMatchObject({ ok: true, description: "Updated when-to-use" });
|
|
|
|
const deleted = await skillDeleteFromTool(prisma as never, owner, { name: "Daily standup" });
|
|
expect(deleted).toEqual({ ok: true, name: "Daily standup" });
|
|
expect(await skillReadFromTool(prisma as never, owner, { name: "Daily standup" })).toEqual({
|
|
error: "Skill not found.",
|
|
});
|
|
});
|
|
|
|
it("preserves extra frontmatter keys on update", async () => {
|
|
const content = buildSkillMd({
|
|
name: "Review PR",
|
|
description: "Review a pull request",
|
|
body: "1. Read the diff.",
|
|
frontmatter: { compatibility: "cursor", "allowed-tools": ["shell"] },
|
|
});
|
|
await skillCreateFromTool(prisma as never, owner, { content });
|
|
await skillUpdateFromTool(prisma as never, owner, {
|
|
name: "Review PR",
|
|
body: "1. Read the diff.\n2. Leave comments.",
|
|
});
|
|
const read = await skillReadFromTool(prisma as never, owner, { name: "Review PR" });
|
|
expect(String(read.content)).toContain("compatibility: cursor");
|
|
expect(String(read.content)).toContain("Leave comments");
|
|
});
|
|
|
|
it("renames only via newName, not the name lookup key", async () => {
|
|
const created = await skillCreateFromTool(prisma as never, owner, {
|
|
name: "Original",
|
|
description: "Keep name unless newName",
|
|
body: "body",
|
|
});
|
|
const skillId = String(created.id);
|
|
const mistyped = await skillUpdateFromTool(prisma as never, owner, {
|
|
skillId,
|
|
name: "Should not apply",
|
|
description: "still original name",
|
|
});
|
|
expect(mistyped).toMatchObject({ ok: true, name: "Original" });
|
|
|
|
const renamed = await skillUpdateFromTool(prisma as never, owner, {
|
|
skillId,
|
|
newName: "Renamed",
|
|
});
|
|
expect(renamed).toMatchObject({ ok: true, name: "Renamed" });
|
|
});
|
|
|
|
it("scopes list to the owner workspace and injects catalog lines", async () => {
|
|
prisma = makePrisma([
|
|
{
|
|
id: "other-1",
|
|
spaceId: "ws-2",
|
|
userId: "user-2",
|
|
name: "Other workspace skill",
|
|
description: "not visible",
|
|
content: buildSkillMd({
|
|
name: "Other workspace skill",
|
|
description: "not visible",
|
|
body: "x",
|
|
}),
|
|
source: "user",
|
|
},
|
|
]);
|
|
await skillCreateFromTool(prisma as never, owner, {
|
|
name: "Daily standup",
|
|
description: "Prepare standup notes",
|
|
body: "steps",
|
|
});
|
|
const records = await listAgentSkillRecords(prisma as never, owner);
|
|
expect(records.map((row) => row.name)).toContain("Daily standup");
|
|
expect(records.map((row) => row.name)).not.toContain("Other workspace skill");
|
|
const catalog = formatSkillsCatalogInstruction(records);
|
|
expect(catalog).toContain("- Daily standup: Prepare standup notes");
|
|
expect(catalog).toContain("skill_read");
|
|
});
|
|
|
|
it("rejects update/delete for plugin and builtin skills", async () => {
|
|
prisma = makePrisma([
|
|
{
|
|
id: "plugin-1",
|
|
spaceId: owner.spaceId,
|
|
userId: owner.userId,
|
|
name: "Plugin recipe",
|
|
description: "From a plugin",
|
|
content: buildSkillMd({
|
|
name: "Plugin recipe",
|
|
description: "From a plugin",
|
|
body: "do not edit",
|
|
}),
|
|
source: "plugin",
|
|
},
|
|
]);
|
|
expect(
|
|
await skillUpdateFromTool(prisma as never, owner, {
|
|
name: "Plugin recipe",
|
|
description: "hijack",
|
|
}),
|
|
).toEqual({ error: "Builtin and plugin skills are read-only." });
|
|
expect(await skillDeleteFromTool(prisma as never, owner, { name: "Plugin recipe" })).toEqual({
|
|
error: "Builtin and plugin skills are read-only.",
|
|
});
|
|
});
|
|
|
|
it("ships builtin skills as valid, readable, read-only SKILL.md records", async () => {
|
|
expect(BUILTIN_AGENT_SKILLS.map((skill) => skill.name)).toContain("Interrogate");
|
|
for (const skill of BUILTIN_AGENT_SKILLS) {
|
|
const parsed = parseSkillMd(skill.content);
|
|
expect(parsed).toMatchObject({ name: skill.name, description: skill.description });
|
|
// / picker truncates at 72; keep descriptions short so "review only" stays visible.
|
|
expect(skill.description.length).toBeLessThanOrEqual(72);
|
|
}
|
|
|
|
const records = await listAgentSkillRecords(prisma as never, owner);
|
|
const interrogate = records.find((row) => row.name === "Interrogate");
|
|
expect(interrogate).toMatchObject({
|
|
id: "builtin:Interrogate",
|
|
source: "builtin",
|
|
readOnly: true,
|
|
});
|
|
expect(formatSkillsCatalogInstruction(records)).toContain("- Interrogate:");
|
|
expect(formatSkillsCatalogInstruction(records)).toContain("Review only");
|
|
|
|
const read = await skillReadFromTool(prisma as never, owner, { name: "interrogate" });
|
|
expect(read).toMatchObject({ name: "Interrogate", source: "builtin", readOnly: true });
|
|
expect(String(read.content)).toContain("Do not modify files");
|
|
|
|
expect(
|
|
await skillCreateFromTool(prisma as never, owner, {
|
|
name: "interrogate",
|
|
description: "shadow the builtin",
|
|
body: "steps",
|
|
}),
|
|
).toEqual({ error: 'A skill named "Interrogate" already exists.' });
|
|
expect(
|
|
await skillUpdateFromTool(prisma as never, owner, {
|
|
name: "Interrogate",
|
|
description: "hijack",
|
|
}),
|
|
).toEqual({ error: "Builtin and plugin skills are read-only." });
|
|
expect(await skillDeleteFromTool(prisma as never, owner, { name: "Interrogate" })).toEqual({
|
|
error: "Builtin and plugin skills are read-only.",
|
|
});
|
|
});
|
|
|
|
it.each(["interrogate", " Interrogate "])("preserves a saved skill named %j", async (name) => {
|
|
prisma = makePrisma([
|
|
{
|
|
id: "legacy-1",
|
|
spaceId: owner.spaceId,
|
|
userId: owner.userId,
|
|
name,
|
|
description: "my review recipe",
|
|
content: buildSkillMd({
|
|
name,
|
|
description: "my review recipe",
|
|
body: "my steps",
|
|
}),
|
|
source: "user",
|
|
},
|
|
]);
|
|
|
|
const records = await listAgentSkillRecords(prisma as never, owner);
|
|
expect(records.filter((skill) => skill.name.trim().toLowerCase() === "interrogate")).toEqual([
|
|
expect.objectContaining({ id: "legacy-1", source: "user", readOnly: false }),
|
|
]);
|
|
expect(await skillReadFromTool(prisma as never, owner, { name: "Interrogate" })).toMatchObject({
|
|
name,
|
|
source: "user",
|
|
readOnly: false,
|
|
});
|
|
expect(await skillReadFromTool(prisma as never, owner, { skillId: "legacy-1" })).toMatchObject({
|
|
name,
|
|
source: "user",
|
|
});
|
|
expect(
|
|
await skillUpdateFromTool(prisma as never, owner, {
|
|
name: "Interrogate",
|
|
description: "still mine",
|
|
}),
|
|
).toMatchObject({ ok: true });
|
|
expect(await skillDeleteFromTool(prisma as never, owner, { name: "Interrogate" })).toEqual({
|
|
ok: true,
|
|
name: name.trim(),
|
|
});
|
|
});
|
|
|
|
it("rejects oversized skill content", async () => {
|
|
const body = "x".repeat(100_001);
|
|
const created = await skillCreateFromTool(prisma as never, owner, {
|
|
name: "Huge",
|
|
description: "too big",
|
|
body,
|
|
});
|
|
expect(created).toMatchObject({ error: expect.stringContaining("at most") });
|
|
});
|
|
|
|
it("rejects overlong skill names on structured create and rename", async () => {
|
|
const longName = "N".repeat(81);
|
|
expect(
|
|
await skillCreateFromTool(prisma as never, owner, {
|
|
name: longName,
|
|
description: "ok description",
|
|
body: "steps",
|
|
}),
|
|
).toEqual({ error: "Skill name must be at most 80 characters." });
|
|
|
|
await skillCreateFromTool(prisma as never, owner, {
|
|
name: "Short",
|
|
description: "ok description",
|
|
body: "steps",
|
|
});
|
|
expect(
|
|
await skillUpdateFromTool(prisma as never, owner, {
|
|
name: "Short",
|
|
newName: longName,
|
|
}),
|
|
).toEqual({ error: "Skill name must be at most 80 characters." });
|
|
});
|
|
});
|