From 3c467baa2d2a8469aef9a89b6ab98ee4d7e47f64 Mon Sep 17 00:00:00 2001 From: Sebastian <19554889+sebslight@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:32:41 -0500 Subject: [PATCH] test(skills): add status-to-install apt fallback coverage --- src/agents/skills-install-fallback.test.ts | 69 +++++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/src/agents/skills-install-fallback.test.ts b/src/agents/skills-install-fallback.test.ts index fedbb77b078..08f47926c3a 100644 --- a/src/agents/skills-install-fallback.test.ts +++ b/src/agents/skills-install-fallback.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { installSkill } from "./skills-install.js"; +import { buildWorkspaceSkillStatus } from "./skills-status.js"; const runCommandWithTimeoutMock = vi.fn(); const scanDirectoryWithSummaryMock = vi.fn(); @@ -24,29 +25,31 @@ vi.mock("../security/skill-scanner.js", async (importOriginal) => { }; }); -vi.mock("../shared/config-eval.js", () => ({ - hasBinary: (...args: unknown[]) => hasBinaryMock(...args), -})); +vi.mock("../shared/config-eval.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + hasBinary: (...args: unknown[]) => hasBinaryMock(...args), + }; +}); vi.mock("../infra/brew.js", () => ({ resolveBrewExecutable: () => undefined, })); -async function writeSkillWithInstaller( +async function writeSkillWithInstallers( workspaceDir: string, name: string, - kind: string, - extra: Record, + installSpecs: Array>, ): Promise { const skillDir = path.join(workspaceDir, "skills", name); await fs.mkdir(skillDir, { recursive: true }); - const installSpec = { id: "deps", kind, ...extra }; await fs.writeFile( path.join(skillDir, "SKILL.md"), `--- name: ${name} description: test skill -metadata: ${JSON.stringify({ openclaw: { install: [installSpec] } })} +metadata: ${JSON.stringify({ openclaw: { install: installSpecs } })} --- # ${name} @@ -57,11 +60,22 @@ metadata: ${JSON.stringify({ openclaw: { install: [installSpec] } })} return skillDir; } +async function writeSkillWithInstaller( + workspaceDir: string, + name: string, + kind: string, + extra: Record, +): Promise { + return writeSkillWithInstallers(workspaceDir, name, [{ id: "deps", kind, ...extra }]); +} + describe("skills-install fallback edge cases", () => { let workspaceDir: string; beforeEach(async () => { - vi.clearAllMocks(); + runCommandWithTimeoutMock.mockReset(); + scanDirectoryWithSummaryMock.mockReset(); + hasBinaryMock.mockReset(); workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-fallback-test-")); scanDirectoryWithSummaryMock.mockResolvedValue({ critical: 0, warn: 0, findings: [] }); }); @@ -119,6 +133,43 @@ describe("skills-install fallback edge cases", () => { expect(aptCalls).toHaveLength(0); }); + it("status-selected go installer fails gracefully when apt fallback needs sudo", async () => { + await writeSkillWithInstallers(workspaceDir, "go-tool", [ + { id: "brew", kind: "brew", formula: "go" }, + { id: "go", kind: "go", module: "example.com/tool@latest" }, + ]); + + // no go/brew, but apt and sudo are present + hasBinaryMock.mockImplementation((bin: string) => { + if (bin === "go" || bin === "brew") { + return false; + } + if (bin === "apt-get" || bin === "sudo") { + return true; + } + return false; + }); + + runCommandWithTimeoutMock.mockResolvedValueOnce({ + code: 1, + stdout: "", + stderr: "sudo: a password is required", + }); + + const status = buildWorkspaceSkillStatus(workspaceDir); + const skill = status.skills.find((entry) => entry.name === "go-tool"); + expect(skill?.install[0]?.id).toBe("go"); + + const result = await installSkill({ + workspaceDir, + skillName: "go-tool", + installId: skill?.install[0]?.id ?? "", + }); + + expect(result.ok).toBe(false); + expect(result.message).toContain("sudo is not usable"); + }); + it("handles sudo probe spawn failures without throwing", async () => { await writeSkillWithInstaller(workspaceDir, "go-tool", "go", { module: "example.com/tool@latest",