Files
openclaw/src/commands/agents.identity.test.ts
2026-02-22 11:09:43 +00:00

212 lines
6.5 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { makeTempWorkspace } from "../test-helpers/workspace.js";
import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js";
const configMocks = vi.hoisted(() => ({
readConfigFileSnapshot: vi.fn(),
writeConfigFile: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../config/config.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../config/config.js")>()),
readConfigFileSnapshot: configMocks.readConfigFileSnapshot,
writeConfigFile: configMocks.writeConfigFile,
}));
import { agentsSetIdentityCommand } from "./agents.js";
const runtime = createTestRuntime();
type ConfigWritePayload = {
agents?: { list?: Array<{ id: string; identity?: Record<string, string> }> };
};
async function createIdentityWorkspace(subdir = "work") {
const root = await makeTempWorkspace("openclaw-identity-");
const workspace = path.join(root, subdir);
await fs.mkdir(workspace, { recursive: true });
return { root, workspace };
}
async function writeIdentityFile(workspace: string, lines: string[]) {
const identityPath = path.join(workspace, "IDENTITY.md");
await fs.writeFile(identityPath, `${lines.join("\n")}\n`, "utf-8");
return identityPath;
}
function getWrittenMainIdentity() {
const written = configMocks.writeConfigFile.mock.calls[0]?.[0] as ConfigWritePayload;
return written.agents?.list?.find((entry) => entry.id === "main")?.identity;
}
async function runIdentityCommandFromWorkspace(workspace: string, fromIdentity = true) {
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: { agents: { list: [{ id: "main", workspace }] } },
});
await agentsSetIdentityCommand({ workspace, fromIdentity }, runtime);
}
describe("agents set-identity command", () => {
beforeEach(() => {
configMocks.readConfigFileSnapshot.mockClear();
configMocks.writeConfigFile.mockClear();
runtime.log.mockClear();
runtime.error.mockClear();
runtime.exit.mockClear();
});
it("sets identity from workspace IDENTITY.md", async () => {
const { root, workspace } = await createIdentityWorkspace();
await writeIdentityFile(workspace, [
"- Name: OpenClaw",
"- Creature: helpful sloth",
"- Emoji: :)",
"- Avatar: avatars/openclaw.png",
"",
]);
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: {
agents: {
list: [
{ id: "main", workspace },
{ id: "ops", workspace: path.join(root, "ops") },
],
},
},
});
await agentsSetIdentityCommand({ workspace }, runtime);
expect(configMocks.writeConfigFile).toHaveBeenCalledTimes(1);
expect(getWrittenMainIdentity()).toEqual({
name: "OpenClaw",
theme: "helpful sloth",
emoji: ":)",
avatar: "avatars/openclaw.png",
});
});
it("errors when multiple agents match the same workspace", async () => {
const { workspace } = await createIdentityWorkspace("shared");
await writeIdentityFile(workspace, ["- Name: Echo"]);
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: {
agents: {
list: [
{ id: "main", workspace },
{ id: "ops", workspace },
],
},
},
});
await agentsSetIdentityCommand({ workspace }, runtime);
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("Multiple agents match"));
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
});
it("overrides identity file values with explicit flags", async () => {
const { workspace } = await createIdentityWorkspace();
await writeIdentityFile(workspace, [
"- Name: OpenClaw",
"- Theme: space lobster",
"- Emoji: :)",
"- Avatar: avatars/openclaw.png",
"",
]);
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: { agents: { list: [{ id: "main", workspace }] } },
});
await agentsSetIdentityCommand(
{
workspace,
fromIdentity: true,
name: "Nova",
emoji: "🦞",
avatar: "https://example.com/override.png",
},
runtime,
);
expect(getWrittenMainIdentity()).toEqual({
name: "Nova",
theme: "space lobster",
emoji: "🦞",
avatar: "https://example.com/override.png",
});
});
it("reads identity from an explicit IDENTITY.md path", async () => {
const { workspace } = await createIdentityWorkspace();
const identityPath = await writeIdentityFile(workspace, [
"- **Name:** C-3PO",
"- **Creature:** Flustered Protocol Droid",
"- **Emoji:** 🤖",
"- **Avatar:** avatars/c3po.png",
"",
]);
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: { agents: { list: [{ id: "main" }] } },
});
await agentsSetIdentityCommand({ agent: "main", identityFile: identityPath }, runtime);
expect(getWrittenMainIdentity()).toEqual({
name: "C-3PO",
theme: "Flustered Protocol Droid",
emoji: "🤖",
avatar: "avatars/c3po.png",
});
});
it("accepts avatar-only identity from IDENTITY.md", async () => {
const { workspace } = await createIdentityWorkspace();
await writeIdentityFile(workspace, ["- Avatar: avatars/only.png"]);
await runIdentityCommandFromWorkspace(workspace);
expect(getWrittenMainIdentity()).toEqual({
avatar: "avatars/only.png",
});
});
it("accepts avatar-only updates via flags", async () => {
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: { agents: { list: [{ id: "main" }] } },
});
await agentsSetIdentityCommand(
{ agent: "main", avatar: "https://example.com/avatar.png" },
runtime,
);
expect(getWrittenMainIdentity()).toEqual({
avatar: "https://example.com/avatar.png",
});
});
it("errors when identity data is missing", async () => {
const { workspace } = await createIdentityWorkspace();
await runIdentityCommandFromWorkspace(workspace);
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("No identity data found"));
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
});
});