diff --git a/src/cli/plugins-authoring-command.test.ts b/src/cli/plugins-authoring-command.test.ts index 95f4b7fdd0a0..8da9f69cf60c 100644 --- a/src/cli/plugins-authoring-command.test.ts +++ b/src/cli/plugins-authoring-command.test.ts @@ -3,13 +3,15 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { Type } from "typebox"; -import { beforeAll, describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it, vi } from "vitest"; import { defineToolPlugin, getToolPluginMetadata } from "../plugin-sdk/tool-plugin.js"; +import { defaultRuntime } from "../runtime.js"; import { VERSION } from "../version.js"; import { buildToolPluginManifest, buildToolPluginPackageManifest, loadToolPlugin, + runPluginsBuildCommand, runPluginsInitCommand, validateToolPluginProject, } from "./plugins-authoring-command.js"; @@ -314,6 +316,68 @@ describe("plugin authoring commands", () => { expect(loaded.metadata.tools.map((tool) => tool.name)).toEqual(["source_echo"]); }); + it("finishes a build from an absolute root after the launch directory is removed", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-deleted-cwd-build-")); + const packagePath = path.join(tmpDir, "package.json"); + const entryPath = writeSourceToolPluginProject({ + tmpDir, + packageName: "openclaw-plugin-deleted-cwd-build", + pluginId: "deleted-cwd-build", + toolName: "deleted_cwd_echo", + }); + const originalCwd = process.cwd(); + const originalWriteFileSync = fs.writeFileSync.bind(fs); + let cwdRemoved = false; + const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); + const cwd = vi.spyOn(process, "cwd").mockImplementation(() => { + if (cwdRemoved) { + throw new Error("ENOENT: no such file or directory, uv_cwd"); + } + return originalCwd; + }); + const writeFileSync = vi + .spyOn(fs, "writeFileSync") + .mockImplementation((file, data, options) => { + originalWriteFileSync(file, data, options); + if (file === packagePath) { + cwdRemoved = true; + } + }); + + try { + await runPluginsBuildCommand({ root: tmpDir, entry: entryPath }); + + expect(fs.existsSync(path.join(tmpDir, "openclaw.plugin.json"))).toBe(true); + expect(log).toHaveBeenCalledWith(`Wrote ${path.join(tmpDir, "openclaw.plugin.json")}`); + expect(log).toHaveBeenCalledWith(`Updated ${packagePath}`); + } finally { + writeFileSync.mockRestore(); + cwd.mockRestore(); + log.mockRestore(); + fs.rmSync(tmpDir, { force: true, recursive: true }); + } + }); + + it("finishes init with an absolute directory after the launch directory is removed", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-deleted-cwd-init-")); + const projectDir = path.join(tmpDir, "demo"); + const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); + const cwd = vi.spyOn(process, "cwd").mockImplementation(() => { + throw new Error("ENOENT: no such file or directory, uv_cwd"); + }); + + try { + await runPluginsInitCommand("demo", { directory: projectDir }); + + expect(fs.existsSync(path.join(projectDir, "package.json"))).toBe(true); + expect(log).toHaveBeenCalledWith(`Created ${projectDir}`); + } finally { + cwd.mockRestore(); + log.mockRestore(); + fs.rmSync(tmpDir, { force: true, recursive: true }); + } + }); + it("scaffolds a dist-entry tool plugin project", async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-init-")); const projectDir = path.join(tmpDir, "stock-quotes"); diff --git a/src/cli/plugins-authoring-command.ts b/src/cli/plugins-authoring-command.ts index 093e8f352022..f1415cf58f26 100644 --- a/src/cli/plugins-authoring-command.ts +++ b/src/cli/plugins-authoring-command.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; +import { tryProcessCwd } from "../infra/safe-cwd.js"; import { getToolPluginMetadata, type ToolPluginMetadata } from "../plugin-sdk/tool-plugin.js"; import { loadPluginManifest, @@ -62,6 +63,13 @@ function writeJsonFile(filePath: string, value: unknown): void { fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); } +// A removed launch directory must not turn completed authoring work into an +// uv_cwd failure while rendering the final path. +function formatOutputPath(targetPath: string, fallback: string): string { + const cwd = tryProcessCwd(); + return cwd ? path.relative(cwd, targetPath) || fallback : targetPath; +} + function jsStringLiteral(value: string): string { return JSON.stringify(value); } @@ -307,10 +315,8 @@ export async function runPluginsBuildCommand(opts: PluginsBuildOptions): Promise writeJsonFile(manifestPath, manifest); writeJsonFile(packagePath, nextPackageManifest); - defaultRuntime.log( - `Wrote ${path.relative(process.cwd(), manifestPath) || PLUGIN_MANIFEST_FILENAME}`, - ); - defaultRuntime.log(`Updated ${path.relative(process.cwd(), packagePath) || "package.json"}`); + defaultRuntime.log(`Wrote ${formatOutputPath(manifestPath, PLUGIN_MANIFEST_FILENAME)}`); + defaultRuntime.log(`Updated ${formatOutputPath(packagePath, "package.json")}`); } export async function runPluginsValidateCommand(opts: PluginsValidateOptions): Promise { @@ -808,5 +814,5 @@ export async function runPluginsInitCommand( } writeJsonFile(path.join(rootDir, "tsconfig.json"), tsconfig); writeScaffoldVitestConfig(rootDir); - defaultRuntime.log(`Created ${path.relative(process.cwd(), rootDir) || "."}`); + defaultRuntime.log(`Created ${formatOutputPath(rootDir, ".")}`); }