mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 04:50:23 +00:00
feat: add native clawhub install flows
This commit is contained in:
158
src/gateway/server-methods/skills.clawhub.test.ts
Normal file
158
src/gateway/server-methods/skills.clawhub.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadConfigMock = vi.fn(() => ({}));
|
||||
const resolveDefaultAgentIdMock = vi.fn(() => "main");
|
||||
const resolveAgentWorkspaceDirMock = vi.fn(() => "/tmp/workspace");
|
||||
const installSkillFromClawHubMock = vi.fn();
|
||||
const updateSkillsFromClawHubMock = vi.fn();
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: () => loadConfigMock(),
|
||||
writeConfigFile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
listAgentIds: vi.fn(() => ["main"]),
|
||||
resolveDefaultAgentId: () => resolveDefaultAgentIdMock(),
|
||||
resolveAgentWorkspaceDir: () => resolveAgentWorkspaceDirMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/skills-clawhub.js", () => ({
|
||||
installSkillFromClawHub: (...args: unknown[]) => installSkillFromClawHubMock(...args),
|
||||
updateSkillsFromClawHub: (...args: unknown[]) => updateSkillsFromClawHubMock(...args),
|
||||
}));
|
||||
|
||||
const { skillsHandlers } = await import("./skills.js");
|
||||
|
||||
describe("skills gateway handlers (clawhub)", () => {
|
||||
beforeEach(() => {
|
||||
loadConfigMock.mockReset();
|
||||
resolveDefaultAgentIdMock.mockReset();
|
||||
resolveAgentWorkspaceDirMock.mockReset();
|
||||
installSkillFromClawHubMock.mockReset();
|
||||
updateSkillsFromClawHubMock.mockReset();
|
||||
|
||||
loadConfigMock.mockReturnValue({});
|
||||
resolveDefaultAgentIdMock.mockReturnValue("main");
|
||||
resolveAgentWorkspaceDirMock.mockReturnValue("/tmp/workspace");
|
||||
});
|
||||
|
||||
it("installs a ClawHub skill through skills.install", async () => {
|
||||
installSkillFromClawHubMock.mockResolvedValue({
|
||||
ok: true,
|
||||
slug: "calendar",
|
||||
version: "1.2.3",
|
||||
targetDir: "/tmp/workspace/skills/calendar",
|
||||
});
|
||||
|
||||
let ok: boolean | null = null;
|
||||
let response: unknown;
|
||||
let error: unknown;
|
||||
await skillsHandlers["skills.install"]({
|
||||
params: {
|
||||
source: "clawhub",
|
||||
slug: "calendar",
|
||||
version: "1.2.3",
|
||||
},
|
||||
req: {} as never,
|
||||
client: null as never,
|
||||
isWebchatConnect: () => false,
|
||||
context: {} as never,
|
||||
respond: (success, result, err) => {
|
||||
ok = success;
|
||||
response = result;
|
||||
error = err;
|
||||
},
|
||||
});
|
||||
|
||||
expect(installSkillFromClawHubMock).toHaveBeenCalledWith({
|
||||
workspaceDir: "/tmp/workspace",
|
||||
slug: "calendar",
|
||||
version: "1.2.3",
|
||||
force: false,
|
||||
});
|
||||
expect(ok).toBe(true);
|
||||
expect(error).toBeUndefined();
|
||||
expect(response).toMatchObject({
|
||||
ok: true,
|
||||
message: "Installed calendar@1.2.3",
|
||||
slug: "calendar",
|
||||
version: "1.2.3",
|
||||
});
|
||||
});
|
||||
|
||||
it("updates ClawHub skills through skills.update", async () => {
|
||||
updateSkillsFromClawHubMock.mockResolvedValue([
|
||||
{
|
||||
ok: true,
|
||||
slug: "calendar",
|
||||
previousVersion: "1.2.2",
|
||||
version: "1.2.3",
|
||||
changed: true,
|
||||
targetDir: "/tmp/workspace/skills/calendar",
|
||||
},
|
||||
]);
|
||||
|
||||
let ok: boolean | null = null;
|
||||
let response: unknown;
|
||||
let error: unknown;
|
||||
await skillsHandlers["skills.update"]({
|
||||
params: {
|
||||
source: "clawhub",
|
||||
slug: "calendar",
|
||||
},
|
||||
req: {} as never,
|
||||
client: null as never,
|
||||
isWebchatConnect: () => false,
|
||||
context: {} as never,
|
||||
respond: (success, result, err) => {
|
||||
ok = success;
|
||||
response = result;
|
||||
error = err;
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateSkillsFromClawHubMock).toHaveBeenCalledWith({
|
||||
workspaceDir: "/tmp/workspace",
|
||||
slug: "calendar",
|
||||
});
|
||||
expect(ok).toBe(true);
|
||||
expect(error).toBeUndefined();
|
||||
expect(response).toMatchObject({
|
||||
ok: true,
|
||||
skillKey: "calendar",
|
||||
config: {
|
||||
source: "clawhub",
|
||||
results: [
|
||||
{
|
||||
ok: true,
|
||||
slug: "calendar",
|
||||
version: "1.2.3",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects ClawHub skills.update requests without slug or all", async () => {
|
||||
let ok: boolean | null = null;
|
||||
let error: { code?: string; message?: string } | undefined;
|
||||
await skillsHandlers["skills.update"]({
|
||||
params: {
|
||||
source: "clawhub",
|
||||
},
|
||||
req: {} as never,
|
||||
client: null as never,
|
||||
isWebchatConnect: () => false,
|
||||
context: {} as never,
|
||||
respond: (success, _result, err) => {
|
||||
ok = success;
|
||||
error = err as { code?: string; message?: string } | undefined;
|
||||
},
|
||||
});
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(error?.message).toContain('requires "slug" or "all"');
|
||||
expect(updateSkillsFromClawHubMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { installSkillFromClawHub, updateSkillsFromClawHub } from "../../agents/skills-clawhub.js";
|
||||
import { installSkill } from "../../agents/skills-install.js";
|
||||
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
||||
import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js";
|
||||
@@ -123,13 +124,44 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
||||
if (params && typeof params === "object" && "source" in params && params.source === "clawhub") {
|
||||
const p = params as {
|
||||
source: "clawhub";
|
||||
slug: string;
|
||||
version?: string;
|
||||
force?: boolean;
|
||||
};
|
||||
const result = await installSkillFromClawHub({
|
||||
workspaceDir: workspaceDirRaw,
|
||||
slug: p.slug,
|
||||
version: p.version,
|
||||
force: Boolean(p.force),
|
||||
});
|
||||
respond(
|
||||
result.ok,
|
||||
result.ok
|
||||
? {
|
||||
ok: true,
|
||||
message: `Installed ${result.slug}@${result.version}`,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
slug: result.slug,
|
||||
version: result.version,
|
||||
targetDir: result.targetDir,
|
||||
}
|
||||
: result,
|
||||
result.ok ? undefined : errorShape(ErrorCodes.UNAVAILABLE, result.error),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const p = params as {
|
||||
name: string;
|
||||
installId: string;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
const cfg = loadConfig();
|
||||
const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
||||
const result = await installSkill({
|
||||
workspaceDir: workspaceDirRaw,
|
||||
skillName: p.name,
|
||||
@@ -155,6 +187,54 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (params && typeof params === "object" && "source" in params && params.source === "clawhub") {
|
||||
const p = params as {
|
||||
source: "clawhub";
|
||||
slug?: string;
|
||||
all?: boolean;
|
||||
};
|
||||
if (!p.slug && !p.all) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, 'clawhub skills.update requires "slug" or "all"'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (p.slug && p.all) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
'clawhub skills.update accepts either "slug" or "all", not both',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
||||
const results = await updateSkillsFromClawHub({
|
||||
workspaceDir,
|
||||
slug: p.slug,
|
||||
});
|
||||
const errors = results.filter((result) => !result.ok);
|
||||
respond(
|
||||
errors.length === 0,
|
||||
{
|
||||
ok: errors.length === 0,
|
||||
skillKey: p.slug ?? "*",
|
||||
config: {
|
||||
source: "clawhub",
|
||||
results,
|
||||
},
|
||||
},
|
||||
errors.length === 0
|
||||
? undefined
|
||||
: errorShape(ErrorCodes.UNAVAILABLE, errors.map((result) => result.error).join("; ")),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const p = params as {
|
||||
skillKey: string;
|
||||
enabled?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user