From 549cb65ba4e3406be44269990ffbcc4c440996cc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 19:48:06 +0000 Subject: [PATCH] test: add executable path and backup summary coverage --- src/infra/backup-create.test.ts | 84 +++++++++++++++++++++++++++++++ src/infra/executable-path.test.ts | 50 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/infra/backup-create.test.ts create mode 100644 src/infra/executable-path.test.ts diff --git a/src/infra/backup-create.test.ts b/src/infra/backup-create.test.ts new file mode 100644 index 00000000000..a91d30c774a --- /dev/null +++ b/src/infra/backup-create.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; +import { formatBackupCreateSummary, type BackupCreateResult } from "./backup-create.js"; + +function makeResult(overrides: Partial = {}): BackupCreateResult { + return { + createdAt: "2026-01-01T00:00:00.000Z", + archiveRoot: "openclaw-backup-2026-01-01", + archivePath: "/tmp/openclaw-backup.tar.gz", + dryRun: false, + includeWorkspace: true, + onlyConfig: false, + verified: false, + assets: [], + skipped: [], + ...overrides, + }; +} + +describe("formatBackupCreateSummary", () => { + it("formats created archives with included and skipped paths", () => { + const lines = formatBackupCreateSummary( + makeResult({ + verified: true, + assets: [ + { + kind: "state", + sourcePath: "/state", + archivePath: "archive/state", + displayPath: "~/.openclaw", + }, + ], + skipped: [ + { + kind: "workspace", + sourcePath: "/workspace", + displayPath: "~/Projects/openclaw", + reason: "covered", + coveredBy: "~/.openclaw", + }, + ], + }), + ); + + expect(lines).toEqual([ + "Backup archive: /tmp/openclaw-backup.tar.gz", + "Included 1 path:", + "- state: ~/.openclaw", + "Skipped 1 path:", + "- workspace: ~/Projects/openclaw (covered by ~/.openclaw)", + "Created /tmp/openclaw-backup.tar.gz", + "Archive verification: passed", + ]); + }); + + it("formats dry runs and pluralized counts", () => { + const lines = formatBackupCreateSummary( + makeResult({ + dryRun: true, + assets: [ + { + kind: "config", + sourcePath: "/config", + archivePath: "archive/config", + displayPath: "~/.openclaw/config.json", + }, + { + kind: "oauth", + sourcePath: "/oauth", + archivePath: "archive/oauth", + displayPath: "~/.openclaw/oauth", + }, + ], + }), + ); + + expect(lines).toEqual([ + "Backup archive: /tmp/openclaw-backup.tar.gz", + "Included 2 paths:", + "- config: ~/.openclaw/config.json", + "- oauth: ~/.openclaw/oauth", + "Dry run only; archive was not written.", + ]); + }); +}); diff --git a/src/infra/executable-path.test.ts b/src/infra/executable-path.test.ts new file mode 100644 index 00000000000..731457ab183 --- /dev/null +++ b/src/infra/executable-path.test.ts @@ -0,0 +1,50 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { + isExecutableFile, + resolveExecutableFromPathEnv, + resolveExecutablePath, +} from "./executable-path.js"; + +describe("executable path helpers", () => { + it("detects executable files and rejects directories or non-executables", async () => { + const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-exec-path-")); + const execPath = path.join(base, "tool"); + const filePath = path.join(base, "plain.txt"); + const dirPath = path.join(base, "dir"); + await fs.writeFile(execPath, "#!/bin/sh\nexit 0\n", "utf8"); + await fs.chmod(execPath, 0o755); + await fs.writeFile(filePath, "nope", "utf8"); + await fs.mkdir(dirPath); + + expect(isExecutableFile(execPath)).toBe(true); + expect(isExecutableFile(filePath)).toBe(false); + expect(isExecutableFile(dirPath)).toBe(false); + expect(isExecutableFile(path.join(base, "missing"))).toBe(false); + }); + + it("resolves executables from PATH entries and cwd-relative paths", async () => { + const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-exec-path-")); + const binDir = path.join(base, "bin"); + const cwd = path.join(base, "cwd"); + await fs.mkdir(binDir, { recursive: true }); + await fs.mkdir(cwd, { recursive: true }); + + const pathTool = path.join(binDir, "runner"); + const cwdTool = path.join(cwd, "local-tool"); + await fs.writeFile(pathTool, "#!/bin/sh\nexit 0\n", "utf8"); + await fs.writeFile(cwdTool, "#!/bin/sh\nexit 0\n", "utf8"); + await fs.chmod(pathTool, 0o755); + await fs.chmod(cwdTool, 0o755); + + expect(resolveExecutableFromPathEnv("runner", `${binDir}${path.delimiter}/usr/bin`)).toBe( + pathTool, + ); + expect(resolveExecutableFromPathEnv("missing", binDir)).toBeUndefined(); + expect(resolveExecutablePath("./local-tool", { cwd })).toBe(cwdTool); + expect(resolveExecutablePath("runner", { env: { PATH: binDir } })).toBe(pathTool); + expect(resolveExecutablePath("missing", { env: { PATH: binDir } })).toBeUndefined(); + }); +});