Download src/node-host/claude-skill-session.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 7.46 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/node-host/claude-skill-session.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/node-host/claude-skill-session.test.ts
-
curl -L -o claude-skill-session.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/node-host/claude-skill-session.test.ts
7.46 kB
| import fs from "node:fs/promises"; | |
| import path from "node:path"; | |
| import { afterEach, describe, expect, it, vi } from "vitest"; | |
| import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js"; | |
| import { resetLogger, setLoggerOverride } from "../logging/logger.js"; | |
| import { loggingState } from "../logging/state.js"; | |
| import { loadWorkspaceSkills } from "../skills/loading/workspace-skill-loader.js"; | |
| import { buildSkillSnapshot } from "../skills/loading/workspace-skill-prompt.js"; | |
| import { prepareSkillResourceDelivery } from "../skills/runtime/resources.js"; | |
| import { prepareNodeClaudeSkillSession } from "./claude-skill-session.js"; | |
| const temps = useAutoCleanupTempDirTracker(afterEach); | |
| describe("node Claude skill artifact cleanup", () => { | |
| it.skipIf(process.platform === "win32" || process.getuid?.() === 0).each([false, true])( | |
| "grants only the artifact root and cleans independent inputs after close (Workshop: %s)", | |
| async (workshop) => { | |
| const workspace = temps.make("node-skill-session-"); | |
| const skillDir = path.join(workspace, "skills", "guide"); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| await fs.writeFile( | |
| path.join(skillDir, "SKILL.md"), | |
| "---\ndescription: Guide\n---\n# Guide\n", | |
| ); | |
| const resources = await prepareSkillResourceDelivery( | |
| await buildSkillSnapshot(workspace, { | |
| entries: loadWorkspaceSkills(workspace, { workspaceOnly: true }), | |
| }), | |
| () => {}, | |
| ); | |
| const session = await prepareNodeClaudeSkillSession({ | |
| signal: new AbortController().signal, | |
| emitChunk: vi.fn(), | |
| onInput: vi.fn(), | |
| frames: { | |
| send: vi.fn(), | |
| onMessage: (listener) => { | |
| void listener( | |
| Buffer.from( | |
| JSON.stringify({ | |
| type: "init", | |
| resources, | |
| ...(workshop ? { workshop: { description: "Fixture", inputSchema: {} } } : {}), | |
| }), | |
| ), | |
| ); | |
| return vi.fn(); | |
| }, | |
| }, | |
| }); | |
| const artifactRoot = session.argv[session.argv.indexOf("--add-dir") + 1]!; | |
| const skillPath = session.rewriteReferences(path.join(skillDir, "SKILL.md")); | |
| const configPath = workshop | |
| ? session.argv[session.argv.indexOf("--mcp-config") + 1]! | |
| : undefined; | |
| const warn = vi.fn(); | |
| const previousConsole = loggingState.rawConsole; | |
| setLoggerOverride({ level: "silent", consoleLevel: "warn" }); | |
| loggingState.rawConsole = { log: vi.fn(), info: vi.fn(), warn, error: vi.fn() }; | |
| try { | |
| expect(session.argv.filter((arg) => arg === "--add-dir")).toHaveLength(1); | |
| expect(path.dirname(path.dirname(skillPath))).toBe(artifactRoot); | |
| expect((await fs.stat(artifactRoot)).mode & 0o777).toBe(0o700); | |
| expect(session.catalog).toContain(skillPath); | |
| if (configPath) { | |
| expect(path.relative(artifactRoot, configPath).startsWith(`..${path.sep}`)).toBe(true); | |
| expect((await fs.stat(path.dirname(configPath))).mode & 0o777).toBe(0o700); | |
| } | |
| await fs.chmod(path.dirname(skillPath), 0o500); | |
| await session.close(); | |
| await expect(session.writeStdout("late output")).rejects.toThrow("invocation closed"); | |
| await expect(session.cleanup()).resolves.toBeUndefined(); | |
| expect(await fs.readFile(skillPath, "utf8")).toContain("# Guide"); | |
| if (configPath) { | |
| await expect(fs.stat(path.dirname(configPath))).rejects.toMatchObject({ code: "ENOENT" }); | |
| } | |
| expect(warn).toHaveBeenCalledOnce(); | |
| expect(warn.mock.calls.flat().join("\n")).toContain(artifactRoot); | |
| } finally { | |
| await session.close(); | |
| await fs.chmod(path.dirname(skillPath), 0o700); | |
| await session.cleanup(); | |
| loggingState.rawConsole = previousConsole; | |
| setLoggerOverride(null); | |
| resetLogger(); | |
| } | |
| }, | |
| ); | |
| it("preserves the exact frozen setup error when partial skill cleanup fails", async () => { | |
| const workspace = temps.make("node-skill-rollback-"); | |
| const skillDir = path.join(workspace, "skills", "partial"); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const markdown = "---\ndescription: Partial materialization proof\n---\n# Instructions\n"; | |
| await fs.writeFile(path.join(skillDir, "SKILL.md"), markdown); | |
| await fs.writeFile(path.join(skillDir, "reference.md"), "supporting resource"); | |
| const resources = await prepareSkillResourceDelivery( | |
| await buildSkillSnapshot(workspace, { | |
| entries: loadWorkspaceSkills(workspace, { workspaceOnly: true }), | |
| }), | |
| () => {}, | |
| ); | |
| const primary = new Error("Node skill setup cancelled"); | |
| primary.name = "AbortError"; | |
| Object.freeze(primary); | |
| const controller = new AbortController(); | |
| let directory: string | undefined; | |
| let retainedFile: string | undefined; | |
| const originalWriteFile = fs.writeFile; | |
| const writeFile = vi | |
| .spyOn(fs, "writeFile") | |
| .mockImplementation(async (target, data, options) => { | |
| await originalWriteFile(target, data, options); | |
| if (typeof target === "string" && path.basename(target) === "SKILL.md") { | |
| retainedFile = target; | |
| directory = path.dirname(path.dirname(target)); | |
| controller.abort(primary); | |
| } | |
| }); | |
| const originalRm = fs.rm; | |
| const deletionError = new Error("EACCES: retained node skill artifact"); | |
| const rm = vi.spyOn(fs, "rm").mockImplementation((target, options) => { | |
| if ( | |
| directory && | |
| (String(target) === directory || String(target).startsWith(`${directory}${path.sep}`)) | |
| ) { | |
| return Promise.reject(deletionError); | |
| } | |
| return originalRm(target, options); | |
| }); | |
| const warn = vi.fn(); | |
| const previousConsole = loggingState.rawConsole; | |
| setLoggerOverride({ level: "silent", consoleLevel: "warn" }); | |
| loggingState.rawConsole = { log: vi.fn(), info: vi.fn(), warn, error: vi.fn() }; | |
| const unsubscribe = vi.fn(); | |
| try { | |
| const result = await prepareNodeClaudeSkillSession({ | |
| signal: controller.signal, | |
| emitChunk: vi.fn(), | |
| onInput: vi.fn(), | |
| frames: { | |
| send: vi.fn(), | |
| onMessage: (listener) => { | |
| void listener(Buffer.from(JSON.stringify({ type: "init", resources }))); | |
| return unsubscribe; | |
| }, | |
| }, | |
| }).catch((error: unknown) => error); | |
| expect.soft(result).toBe(primary); | |
| expect(unsubscribe).toHaveBeenCalledOnce(); | |
| expect(retainedFile).toBeDefined(); | |
| expect(await fs.readFile(retainedFile!, "utf8")).toBe(markdown); | |
| await expect( | |
| fs.stat(path.join(path.dirname(retainedFile!), "reference.md")), | |
| ).rejects.toMatchObject({ code: "ENOENT" }); | |
| const warning = warn.mock.calls.flat().map(String).join("\n"); | |
| expect(warning).toContain("Materialized skill cleanup failed"); | |
| expect(warn).toHaveBeenCalledOnce(); | |
| expect(warning).toContain(directory); | |
| expect(warning).toContain("EACCES"); | |
| expect(warning).not.toContain(markdown); | |
| } finally { | |
| writeFile.mockRestore(); | |
| rm.mockRestore(); | |
| loggingState.rawConsole = previousConsole; | |
| setLoggerOverride(null); | |
| resetLogger(); | |
| if (directory) { | |
| await fs.rm(directory, { recursive: true, force: true }); | |
| } | |
| } | |
| }); | |
| }); | |