mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 08:12:21 +00:00
test: add outbound and hardlink helper coverage
This commit is contained in:
38
src/infra/binaries.test.ts
Normal file
38
src/infra/binaries.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { ensureBinary } from "./binaries.js";
|
||||
|
||||
describe("ensureBinary", () => {
|
||||
it("passes through when the binary exists", async () => {
|
||||
const exec: typeof runExec = vi.fn().mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
});
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
await ensureBinary("node", exec, runtime);
|
||||
|
||||
expect(exec).toHaveBeenCalledWith("which", ["node"]);
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs and exits when the binary is missing", async () => {
|
||||
const exec: typeof runExec = vi.fn().mockRejectedValue(new Error("missing"));
|
||||
const error = vi.fn();
|
||||
const exit = vi.fn(() => {
|
||||
throw new Error("exit");
|
||||
});
|
||||
|
||||
await expect(ensureBinary("ghost", exec, { log: vi.fn(), error, exit })).rejects.toThrow(
|
||||
"exit",
|
||||
);
|
||||
expect(error).toHaveBeenCalledWith("Missing required binary: ghost. Please install it.");
|
||||
expect(exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
67
src/infra/hardlink-guards.test.ts
Normal file
67
src/infra/hardlink-guards.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import { assertNoHardlinkedFinalPath } from "./hardlink-guards.js";
|
||||
|
||||
describe("assertNoHardlinkedFinalPath", () => {
|
||||
it("allows missing paths, directories, and explicit unlink opt-in", async () => {
|
||||
await withTempDir({ prefix: "openclaw-hardlink-guards-" }, async (root) => {
|
||||
const dirPath = path.join(root, "dir");
|
||||
await fs.mkdir(dirPath);
|
||||
|
||||
await expect(
|
||||
assertNoHardlinkedFinalPath({
|
||||
filePath: path.join(root, "missing.txt"),
|
||||
root,
|
||||
boundaryLabel: "workspace",
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
await expect(
|
||||
assertNoHardlinkedFinalPath({
|
||||
filePath: dirPath,
|
||||
root,
|
||||
boundaryLabel: "workspace",
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
const source = path.join(root, "source.txt");
|
||||
const linked = path.join(root, "linked.txt");
|
||||
await fs.writeFile(source, "hello", "utf8");
|
||||
await fs.link(source, linked);
|
||||
|
||||
await expect(
|
||||
assertNoHardlinkedFinalPath({
|
||||
filePath: linked,
|
||||
root,
|
||||
boundaryLabel: "workspace",
|
||||
allowFinalHardlinkForUnlink: true,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects hardlinked files and shortens home-relative paths in the error", async () => {
|
||||
await withTempDir({ prefix: "openclaw-hardlink-guards-" }, async (root) => {
|
||||
const source = path.join(root, "source.txt");
|
||||
const linked = path.join(root, "linked.txt");
|
||||
await fs.writeFile(source, "hello", "utf8");
|
||||
await fs.link(source, linked);
|
||||
const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(root);
|
||||
|
||||
try {
|
||||
await expect(
|
||||
assertNoHardlinkedFinalPath({
|
||||
filePath: linked,
|
||||
root,
|
||||
boundaryLabel: "workspace",
|
||||
}),
|
||||
).rejects.toThrow("Hardlinked path is not allowed under workspace (~): ~/linked.txt");
|
||||
} finally {
|
||||
homedirSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,5 @@
|
||||
import os from "node:os";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { ensureBinary } from "./binaries.js";
|
||||
import {
|
||||
__testing,
|
||||
consumeGatewaySigusr1RestartAuthorization,
|
||||
@@ -31,35 +28,6 @@ describe("infra runtime", () => {
|
||||
});
|
||||
}
|
||||
|
||||
describe("ensureBinary", () => {
|
||||
it("passes through when binary exists", async () => {
|
||||
const exec: typeof runExec = vi.fn().mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
});
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
await ensureBinary("node", exec, runtime);
|
||||
expect(exec).toHaveBeenCalledWith("which", ["node"]);
|
||||
});
|
||||
|
||||
it("logs and exits when missing", async () => {
|
||||
const exec: typeof runExec = vi.fn().mockRejectedValue(new Error("missing"));
|
||||
const error = vi.fn();
|
||||
const exit = vi.fn(() => {
|
||||
throw new Error("exit");
|
||||
});
|
||||
await expect(ensureBinary("ghost", exec, { log: vi.fn(), error, exit })).rejects.toThrow(
|
||||
"exit",
|
||||
);
|
||||
expect(error).toHaveBeenCalledWith("Missing required binary: ghost. Please install it.");
|
||||
expect(exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("restart authorization", () => {
|
||||
setupRestartSignalSuite();
|
||||
|
||||
|
||||
69
src/infra/outbound/identity.test.ts
Normal file
69
src/infra/outbound/identity.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const resolveAgentIdentityMock = vi.hoisted(() => vi.fn());
|
||||
const resolveAgentAvatarMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../agents/identity.js", () => ({
|
||||
resolveAgentIdentity: (...args: unknown[]) => resolveAgentIdentityMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/identity-avatar.js", () => ({
|
||||
resolveAgentAvatar: (...args: unknown[]) => resolveAgentAvatarMock(...args),
|
||||
}));
|
||||
|
||||
import { normalizeOutboundIdentity, resolveAgentOutboundIdentity } from "./identity.js";
|
||||
|
||||
describe("normalizeOutboundIdentity", () => {
|
||||
it("trims fields and drops empty identities", () => {
|
||||
expect(
|
||||
normalizeOutboundIdentity({
|
||||
name: " Demo Bot ",
|
||||
avatarUrl: " https://example.com/a.png ",
|
||||
emoji: " 🤖 ",
|
||||
}),
|
||||
).toEqual({
|
||||
name: "Demo Bot",
|
||||
avatarUrl: "https://example.com/a.png",
|
||||
emoji: "🤖",
|
||||
});
|
||||
expect(
|
||||
normalizeOutboundIdentity({
|
||||
name: " ",
|
||||
avatarUrl: "\n",
|
||||
emoji: "",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAgentOutboundIdentity", () => {
|
||||
it("builds normalized identity data and keeps only remote avatars", () => {
|
||||
resolveAgentIdentityMock.mockReturnValueOnce({
|
||||
name: " Agent Smith ",
|
||||
emoji: " 🕶️ ",
|
||||
});
|
||||
resolveAgentAvatarMock.mockReturnValueOnce({
|
||||
kind: "remote",
|
||||
url: "https://example.com/avatar.png",
|
||||
});
|
||||
|
||||
expect(resolveAgentOutboundIdentity({} as never, "main")).toEqual({
|
||||
name: "Agent Smith",
|
||||
emoji: "🕶️",
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
});
|
||||
});
|
||||
|
||||
it("drops blank and non-remote avatar values after normalization", () => {
|
||||
resolveAgentIdentityMock.mockReturnValueOnce({
|
||||
name: " ",
|
||||
emoji: "",
|
||||
});
|
||||
resolveAgentAvatarMock.mockReturnValueOnce({
|
||||
kind: "data",
|
||||
dataUrl: "data:image/png;base64,abc",
|
||||
});
|
||||
|
||||
expect(resolveAgentOutboundIdentity({} as never, "main")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
55
src/infra/outbound/session-context.test.ts
Normal file
55
src/infra/outbound/session-context.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const resolveSessionAgentIdMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveSessionAgentId: (...args: unknown[]) => resolveSessionAgentIdMock(...args),
|
||||
}));
|
||||
|
||||
import { buildOutboundSessionContext } from "./session-context.js";
|
||||
|
||||
describe("buildOutboundSessionContext", () => {
|
||||
it("returns undefined when both session key and agent id are blank", () => {
|
||||
expect(
|
||||
buildOutboundSessionContext({
|
||||
cfg: {} as never,
|
||||
sessionKey: " ",
|
||||
agentId: null,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(resolveSessionAgentIdMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("derives the agent id from the trimmed session key when no explicit agent is given", () => {
|
||||
resolveSessionAgentIdMock.mockReturnValueOnce("derived-agent");
|
||||
|
||||
expect(
|
||||
buildOutboundSessionContext({
|
||||
cfg: { agents: {} } as never,
|
||||
sessionKey: " session:main:123 ",
|
||||
}),
|
||||
).toEqual({
|
||||
key: "session:main:123",
|
||||
agentId: "derived-agent",
|
||||
});
|
||||
expect(resolveSessionAgentIdMock).toHaveBeenCalledWith({
|
||||
sessionKey: "session:main:123",
|
||||
config: { agents: {} },
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers an explicit trimmed agent id over the derived one", () => {
|
||||
resolveSessionAgentIdMock.mockReturnValueOnce("derived-agent");
|
||||
|
||||
expect(
|
||||
buildOutboundSessionContext({
|
||||
cfg: {} as never,
|
||||
sessionKey: "session:main:123",
|
||||
agentId: " explicit-agent ",
|
||||
}),
|
||||
).toEqual({
|
||||
key: "session:main:123",
|
||||
agentId: "explicit-agent",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user