mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 14:31:34 +00:00
396 lines
13 KiB
TypeScript
396 lines
13 KiB
TypeScript
import { nip19 } from "nostr-tools";
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
|
import type { WizardPrompter } from "openclaw/plugin-sdk/setup";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { createBuzzSetupWizard } from "./setup-surface.js";
|
|
|
|
const ROOM_A = "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c";
|
|
const ROOM_B = "940d0c32-4eb7-46d7-9d5b-d975aaef87f7";
|
|
const GENERATED_KEY = Uint8Array.from({ length: 32 }, (_, index) => index + 1);
|
|
const AUTH_TAG = '["auth","bot","kind=9","signature"]';
|
|
|
|
function createPrompter(): WizardPrompter {
|
|
return {
|
|
intro: vi.fn(async () => {}),
|
|
outro: vi.fn(async () => {}),
|
|
note: vi.fn(async () => {}),
|
|
plain: vi.fn(async () => {}),
|
|
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
|
select: vi.fn(async ({ message }) => {
|
|
if (message.includes("room access")) {
|
|
return "retry";
|
|
}
|
|
if (message.includes("default")) {
|
|
return ROOM_B;
|
|
}
|
|
throw new Error(`Unexpected select prompt: ${message}`);
|
|
}) as WizardPrompter["select"],
|
|
multiselect: vi.fn(async () => [ROOM_A, ROOM_B]) as WizardPrompter["multiselect"],
|
|
text: vi.fn(async ({ message }) => {
|
|
if (message.includes("relay")) {
|
|
return "wss://buzz.example.com";
|
|
}
|
|
throw new Error(`Unexpected text prompt: ${message}`);
|
|
}),
|
|
confirm: vi.fn(async ({ message }) => {
|
|
throw new Error(`Unexpected confirm prompt: ${message}`);
|
|
}),
|
|
};
|
|
}
|
|
|
|
function createRuntime(): RuntimeEnv {
|
|
return {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn() as RuntimeEnv["exit"],
|
|
};
|
|
}
|
|
|
|
describe("Buzz guided setup", () => {
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("generates a dedicated plaintext bot key and configures discovered rooms", async () => {
|
|
const discoverRooms = vi.fn(async () => [
|
|
{ id: ROOM_A, name: "General", about: "Team room" },
|
|
{ id: ROOM_B, name: "Agents" },
|
|
]);
|
|
const verifyAfterWrite = vi.fn(async () => {});
|
|
const wizard = createBuzzSetupWizard({
|
|
discoverRooms,
|
|
generateSecretKey: () => GENERATED_KEY,
|
|
verifyAfterWrite,
|
|
});
|
|
const prompter = createPrompter();
|
|
const runtime = createRuntime();
|
|
const hooks: Array<{
|
|
run: (ctx: { cfg: OpenClawConfig; runtime: RuntimeEnv }) => void | Promise<void>;
|
|
}> = [];
|
|
|
|
const result = await wizard.configure({
|
|
cfg: { channels: { buzz: { authTag: AUTH_TAG } } } as OpenClawConfig,
|
|
runtime,
|
|
prompter,
|
|
options: {
|
|
onPostWriteHook: (hook) => hooks.push(hook),
|
|
},
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
const expectedPrivateKey = nip19.nsecEncode(GENERATED_KEY);
|
|
expect(result.accountId).toBe("default");
|
|
expect(result.cfg.channels?.buzz).toEqual({
|
|
enabled: true,
|
|
relayUrl: "wss://buzz.example.com",
|
|
privateKey: expectedPrivateKey,
|
|
groupPolicy: "open",
|
|
groupAllowFrom: undefined,
|
|
groups: {
|
|
[ROOM_A]: { enabled: true, requireMention: false },
|
|
[ROOM_B]: { enabled: true, requireMention: false },
|
|
},
|
|
defaultTo: ROOM_B,
|
|
});
|
|
expect(discoverRooms).toHaveBeenCalledWith({
|
|
relayUrl: "wss://buzz.example.com",
|
|
privateKey: expectedPrivateKey,
|
|
});
|
|
expect(result.cfg.channels?.buzz?.authTag).toBeUndefined();
|
|
expect(
|
|
vi.mocked(prompter.note).mock.calls.some(([message]) => message.includes(expectedPrivateKey)),
|
|
).toBe(false);
|
|
expect(hooks).toHaveLength(1);
|
|
await hooks[0]!.run({ cfg: result.cfg, runtime });
|
|
expect(verifyAfterWrite).toHaveBeenCalledWith({
|
|
accountId: "default",
|
|
target: ROOM_B,
|
|
runtime,
|
|
});
|
|
});
|
|
|
|
it("uses the standard SecretRef route for an existing bot key", async () => {
|
|
const secretRef = { source: "env" as const, provider: "default", id: "BUZZ_BOT_KEY" };
|
|
const privateKey = nip19.nsecEncode(GENERATED_KEY);
|
|
type BuzzSetupDependencies = NonNullable<Parameters<typeof createBuzzSetupWizard>[0]>;
|
|
type RunSecretStep = NonNullable<BuzzSetupDependencies["runSecretStep"]>;
|
|
const runSecretStep = vi.fn(async ({ cfg, applySet }: Parameters<RunSecretStep>[0]) => ({
|
|
cfg: await applySet!(cfg, secretRef, privateKey),
|
|
action: "set" as const,
|
|
resolvedValue: privateKey,
|
|
}));
|
|
const wizard = createBuzzSetupWizard({
|
|
discoverRooms: vi.fn(async () => [{ id: ROOM_A, name: "General" }]),
|
|
runSecretStep,
|
|
verifyAfterWrite: vi.fn(async () => {}),
|
|
});
|
|
const prompter = createPrompter();
|
|
vi.mocked(prompter.multiselect).mockResolvedValue([ROOM_A]);
|
|
|
|
const result = await wizard.configure({
|
|
cfg: {} as OpenClawConfig,
|
|
runtime: createRuntime(),
|
|
prompter,
|
|
options: { secretInputMode: "ref" },
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
expect(runSecretStep).toHaveBeenCalledWith(
|
|
expect.objectContaining({ secretInputMode: "ref", providerHint: "buzz" }),
|
|
);
|
|
expect(result.cfg.channels?.buzz?.privateKey).toEqual(secretRef);
|
|
});
|
|
|
|
it("falls back to retry without rotating the identity when automatic discovery expires", async () => {
|
|
const discoverRooms = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([{ id: ROOM_A, name: "General" }]);
|
|
const wizard = createBuzzSetupWizard({
|
|
discoverRooms,
|
|
generateSecretKey: () => GENERATED_KEY,
|
|
waitForRoomAccess: vi.fn(async () => []),
|
|
});
|
|
const prompter = createPrompter();
|
|
|
|
const result = await wizard.configure({
|
|
cfg: {
|
|
channels: {
|
|
buzz: {
|
|
enabled: true,
|
|
relayUrl: "wss://old.example.com",
|
|
privateKey: "11".repeat(32),
|
|
groups: { [ROOM_A]: { enabled: true } },
|
|
defaultTo: ROOM_A,
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
runtime: createRuntime(),
|
|
prompter,
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
expect(result.cfg.channels?.buzz?.enabled).toBe(true);
|
|
expect(result.cfg.channels?.buzz?.privateKey).toBe("11".repeat(32));
|
|
expect(result.completion).toBeUndefined();
|
|
expect(result.accountId).toBe("default");
|
|
expect(discoverRooms).toHaveBeenCalledTimes(2);
|
|
expect(discoverRooms.mock.calls[0]?.[0].privateKey).toBe(
|
|
discoverRooms.mock.calls[1]?.[0].privateKey,
|
|
);
|
|
expect(prompter.note).toHaveBeenCalledWith(
|
|
expect.stringContaining("Local `just dev` needs no separate community-member step"),
|
|
"Buzz room access required",
|
|
);
|
|
expect(prompter.text).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({ message: expect.stringContaining("room UUID") }),
|
|
);
|
|
expect(prompter.select).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
message: "Buzz room access is not ready",
|
|
initialValue: "retry",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("reports a paused identity as configured but disabled", async () => {
|
|
const wizard = createBuzzSetupWizard();
|
|
|
|
await expect(
|
|
wizard.getStatus({
|
|
cfg: {
|
|
channels: {
|
|
buzz: {
|
|
enabled: false,
|
|
relayUrl: "wss://buzz.example.com",
|
|
privateKey: "11".repeat(32),
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
accountOverrides: {},
|
|
}),
|
|
).resolves.toEqual({
|
|
channel: "buzz",
|
|
configured: true,
|
|
statusLines: ["Buzz: configured but disabled"],
|
|
selectionHint: "configured but disabled",
|
|
});
|
|
});
|
|
|
|
it("warns before using an unencrypted remote relay", async () => {
|
|
const wizard = createBuzzSetupWizard({
|
|
discoverRooms: vi.fn(async () => [{ id: ROOM_A, name: "General" }]),
|
|
generateSecretKey: () => GENERATED_KEY,
|
|
});
|
|
const prompter = createPrompter();
|
|
vi.mocked(prompter.text).mockResolvedValueOnce("wss://buzz.example.com");
|
|
vi.mocked(prompter.confirm).mockImplementation(async ({ message }) => {
|
|
if (message.includes("unencrypted")) {
|
|
return false;
|
|
}
|
|
throw new Error(`Unexpected confirm prompt: ${message}`);
|
|
});
|
|
|
|
const result = await wizard.configure({
|
|
cfg: {
|
|
channels: { buzz: { relayUrl: "ws://127.attacker.example" } },
|
|
} as OpenClawConfig,
|
|
runtime: createRuntime(),
|
|
prompter,
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
expect(result.cfg.channels?.buzz?.relayUrl).toBe("wss://buzz.example.com");
|
|
expect(prompter.confirm).toHaveBeenCalledWith({
|
|
message: "This remote ws:// relay is unencrypted. Continue anyway?",
|
|
initialValue: false,
|
|
});
|
|
});
|
|
|
|
it("reuses an existing identity without a second credential prompt", async () => {
|
|
const runSecretStep = vi.fn();
|
|
const wizard = createBuzzSetupWizard({
|
|
discoverRooms: vi.fn(async () => [{ id: ROOM_A, name: "General" }]),
|
|
runSecretStep,
|
|
});
|
|
const prompter = createPrompter();
|
|
|
|
const result = await wizard.configure({
|
|
cfg: {
|
|
channels: {
|
|
defaults: { groupPolicy: "disabled" },
|
|
buzz: {
|
|
relayUrl: "wss://buzz.example.com",
|
|
privateKey: "11".repeat(32),
|
|
groups: { [ROOM_A]: { enabled: false, requireMention: false } },
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
runtime: createRuntime(),
|
|
prompter,
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
expect(runSecretStep).not.toHaveBeenCalled();
|
|
expect(result.cfg.channels?.buzz?.privateKey).toBe("11".repeat(32));
|
|
expect(result.cfg.channels?.buzz?.groupPolicy).toBeUndefined();
|
|
expect(result.cfg.channels?.buzz?.groups?.[ROOM_A]).toEqual({
|
|
enabled: false,
|
|
requireMention: false,
|
|
});
|
|
expect(result.cfg.channels?.defaults?.groupPolicy).toBe("disabled");
|
|
});
|
|
|
|
it("does not use BUZZ_PRIVATE_KEY when reusing an unresolved SecretRef", async () => {
|
|
vi.stubEnv("BUZZ_PRIVATE_KEY", nip19.nsecEncode(GENERATED_KEY));
|
|
const secretRef = { source: "env" as const, provider: "default", id: "OTHER_BUZZ_KEY" };
|
|
const discoverRooms = vi.fn(async () => [{ id: ROOM_A, name: "General" }]);
|
|
const wizard = createBuzzSetupWizard({ discoverRooms });
|
|
const prompter = createPrompter();
|
|
vi.mocked(prompter.text).mockImplementation(async ({ message }) => {
|
|
if (message.includes("relay")) {
|
|
return "wss://buzz.example.com";
|
|
}
|
|
throw new Error(`Unexpected text prompt: ${message}`);
|
|
});
|
|
|
|
const result = await wizard.configure({
|
|
cfg: {
|
|
channels: {
|
|
buzz: { relayUrl: "wss://buzz.example.com", privateKey: secretRef },
|
|
},
|
|
} as OpenClawConfig,
|
|
runtime: createRuntime(),
|
|
prompter,
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
expect(discoverRooms).not.toHaveBeenCalled();
|
|
expect(result.cfg.channels?.buzz?.privateKey).toEqual(secretRef);
|
|
expect(result.cfg.channels?.buzz?.enabled).toBe(false);
|
|
expect(result.completion).toBe("paused");
|
|
});
|
|
|
|
it("does not replace an existing identity during SecretRef setup", async () => {
|
|
const runSecretStep = vi.fn();
|
|
const discoverRooms = vi.fn(async () => [{ id: ROOM_A, name: "General" }]);
|
|
const wizard = createBuzzSetupWizard({
|
|
discoverRooms,
|
|
runSecretStep,
|
|
});
|
|
const prompter = createPrompter();
|
|
|
|
const result = await wizard.configure({
|
|
cfg: {
|
|
channels: {
|
|
buzz: {
|
|
relayUrl: "wss://buzz.example.com",
|
|
privateKey: "11".repeat(32),
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
runtime: createRuntime(),
|
|
prompter,
|
|
options: { secretInputMode: "ref" },
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
expect(runSecretStep).not.toHaveBeenCalled();
|
|
expect(discoverRooms).toHaveBeenCalledWith({
|
|
relayUrl: "wss://buzz.example.com",
|
|
privateKey: "11".repeat(32),
|
|
});
|
|
expect(result.cfg.channels?.buzz?.privateKey).toBe("11".repeat(32));
|
|
expect(result.cfg.channels?.buzz?.enabled).toBe(true);
|
|
expect(result.completion).toBeUndefined();
|
|
});
|
|
|
|
it("waits for authenticated room access without rotating the generated identity", async () => {
|
|
const discoverRooms = vi.fn(async () => []);
|
|
const waitForRoomAccess = vi.fn(async () => [{ id: ROOM_A, name: "General" }]);
|
|
const wizard = createBuzzSetupWizard({
|
|
discoverRooms,
|
|
generateSecretKey: () => GENERATED_KEY,
|
|
waitForRoomAccess,
|
|
verifyAfterWrite: vi.fn(async () => {}),
|
|
});
|
|
const prompter = createPrompter();
|
|
vi.mocked(prompter.multiselect).mockResolvedValue([ROOM_A]);
|
|
|
|
const result = await wizard.configure({
|
|
cfg: {} as OpenClawConfig,
|
|
runtime: createRuntime(),
|
|
prompter,
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
expect(discoverRooms).toHaveBeenCalledOnce();
|
|
const expectedPrivateKey = nip19.nsecEncode(GENERATED_KEY);
|
|
expect(discoverRooms).toHaveBeenCalledWith(
|
|
expect.objectContaining({ privateKey: expectedPrivateKey }),
|
|
);
|
|
expect(waitForRoomAccess).toHaveBeenCalledWith(
|
|
expect.objectContaining({ privateKey: expectedPrivateKey }),
|
|
);
|
|
expect(result.cfg.channels?.buzz?.enabled).toBe(true);
|
|
expect(result.cfg.channels?.buzz?.defaultTo).toBe(ROOM_A);
|
|
});
|
|
});
|