mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: satisfy Buzz CI boundaries (#113419)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { finalizeEvent, getPublicKey, type Event, type Filter } from "nostr-tools";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
|
||||
|
||||
const relayMocks = vi.hoisted(() => ({
|
||||
connect: vi.fn<() => Promise<void>>(),
|
||||
@@ -68,14 +70,16 @@ const ACCOUNT_ID = "default";
|
||||
const CHANNEL_ID = "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c";
|
||||
const BOT_PUBLIC_KEY = getPublicKey(Uint8Array.from(Buffer.from(PRIVATE_KEY, "hex")));
|
||||
const SENDER_PUBLIC_KEY = getPublicKey(Uint8Array.from(Buffer.from(SENDER_PRIVATE_KEY, "hex")));
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
const tempDirs = new Set<string>();
|
||||
let previousStateDir: string | undefined;
|
||||
let stateDir: string;
|
||||
|
||||
describe("Buzz bus lifecycle", () => {
|
||||
beforeEach(() => {
|
||||
previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
stateDir = tempDirs.make("openclaw-buzz-dedupe-");
|
||||
// openclaw-temp-dir: allow extension tests cannot import root test helpers.
|
||||
stateDir = mkdtempSync(path.join(tmpdir(), "openclaw-buzz-dedupe-"));
|
||||
tempDirs.add(stateDir);
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
vi.clearAllMocks();
|
||||
relayMocks.subscriptions.length = 0;
|
||||
@@ -106,6 +110,10 @@ describe("Buzz bus lifecycle", () => {
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||
}
|
||||
for (const tempDir of tempDirs) {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
tempDirs.clear();
|
||||
});
|
||||
|
||||
it("closes a connected relay when authentication fails", async () => {
|
||||
@@ -239,9 +247,15 @@ describe("Buzz bus lifecycle", () => {
|
||||
?.handlers.onevent(event);
|
||||
|
||||
await vi.waitFor(() => expect(onMessageError).toHaveBeenCalledWith(expect.any(Error)));
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
expect(relayMocks.publish.mock.calls.some(([event]) => event.kind === 0)).toBe(false);
|
||||
expect(relayMocks.publish.mock.calls.some(([event]) => event.kind === 10_100)).toBe(true);
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
expect(
|
||||
relayMocks.publish.mock.calls.some(([publishedEvent]) => publishedEvent.kind === 0),
|
||||
).toBe(false);
|
||||
expect(
|
||||
relayMocks.publish.mock.calls.some(([publishedEvent]) => publishedEvent.kind === 10_100),
|
||||
).toBe(true);
|
||||
expect(onProfilePublished).toHaveBeenCalledOnce();
|
||||
expect(onFatalError).not.toHaveBeenCalled();
|
||||
await bus.close();
|
||||
|
||||
@@ -139,25 +139,26 @@ async function sleepWithSignal(delayMs: number, signal?: AbortSignal): Promise<v
|
||||
signal?.throwIfAborted();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const finish = (error?: unknown) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
clearTimeout(timer);
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
if (error === undefined) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(error);
|
||||
reject(
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error("Buzz room membership refresh failed", { cause: error }),
|
||||
);
|
||||
}
|
||||
};
|
||||
const onAbort = () =>
|
||||
finish(signal?.reason ?? new Error("Buzz room membership refresh aborted"));
|
||||
timer = setTimeout(() => finish(), delayMs);
|
||||
const timer = setTimeout(() => finish(), delayMs);
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
if (signal?.aborted) {
|
||||
onAbort();
|
||||
@@ -175,18 +176,15 @@ async function queryBuzzRoomMemberships(params: {
|
||||
const memberships = new Map<string, BuzzRoomMembership>();
|
||||
return await new Promise<Map<string, BuzzRoomMembership>>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let subscription: ReturnType<Relay["subscribe"]> | undefined;
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const subscriptionRef: { current?: ReturnType<Relay["subscribe"]> } = {};
|
||||
const finish = (error?: unknown) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
params.signal?.removeEventListener("abort", onAbort);
|
||||
subscription?.close("membership snapshot loaded");
|
||||
subscriptionRef.current?.close("membership snapshot loaded");
|
||||
if (error === undefined) {
|
||||
resolve(memberships);
|
||||
} else {
|
||||
@@ -199,12 +197,12 @@ async function queryBuzzRoomMemberships(params: {
|
||||
};
|
||||
const onAbort = () =>
|
||||
finish(params.signal?.reason ?? new Error("Buzz room membership query aborted"));
|
||||
timeout = setTimeout(
|
||||
const timeout = setTimeout(
|
||||
() => finish(new Error("Timed out loading Buzz room membership")),
|
||||
params.timeoutMs ?? MEMBERSHIP_READY_TIMEOUT_MS,
|
||||
);
|
||||
params.signal?.addEventListener("abort", onAbort, { once: true });
|
||||
subscription = params.relay.subscribe(
|
||||
subscriptionRef.current = params.relay.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [BUZZ_ROOM_MEMBERSHIP_KIND],
|
||||
@@ -233,7 +231,7 @@ async function queryBuzzRoomMemberships(params: {
|
||||
},
|
||||
);
|
||||
if (settled) {
|
||||
subscription.close("membership snapshot loaded");
|
||||
subscriptionRef.current.close("membership snapshot loaded");
|
||||
}
|
||||
if (params.signal?.aborted) {
|
||||
onAbort();
|
||||
@@ -384,22 +382,22 @@ async function createBuzzRoomMembershipTracker(params: {
|
||||
|
||||
const handleSystemEvent = (event: Event): Promise<void> | undefined => {
|
||||
if (!markSystemEventSeen(event.id)) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
const channelId = event.tags
|
||||
.find((tag) => tag[0] === "h")?.[1]
|
||||
?.trim()
|
||||
.toLowerCase();
|
||||
if (!channelId) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
const membership = memberships.get(channelId);
|
||||
if (!membership) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
const change = parseBuzzRoomMembershipChangeEvent(event, membership);
|
||||
if (!change) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
// System events invalidate membership; the relay-signed roster decides the
|
||||
// final state. Removals deny immediately, while joins wait for confirmation.
|
||||
@@ -580,7 +578,7 @@ export async function startBuzzBus(options: {
|
||||
authTag,
|
||||
signal,
|
||||
});
|
||||
let subscriptions: Array<ReturnType<Relay["subscribe"]>> = [];
|
||||
const subscriptions: Array<ReturnType<Relay["subscribe"]>> = [];
|
||||
let stopPresenceHeartbeat = () => {};
|
||||
const bus: BuzzBus = {
|
||||
publicKey,
|
||||
|
||||
@@ -6,9 +6,7 @@ const PROFILE_QUERY_TIMEOUT_MS = 5_000;
|
||||
const DEFAULT_CHANNEL_ADD_POLICY = "anyone";
|
||||
const CHANNEL_ADD_POLICIES = new Set(["anyone", "owner_only", "nobody"]);
|
||||
|
||||
export type BuzzProfileSyncResult =
|
||||
| { status: "unchanged" }
|
||||
| { status: "published"; eventId: string };
|
||||
type BuzzProfileSyncResult = { status: "unchanged" } | { status: "published"; eventId: string };
|
||||
|
||||
function parseProfileContent(event: Event | undefined): Record<string, unknown> {
|
||||
if (!event) {
|
||||
@@ -27,9 +25,12 @@ function parseProfileContent(event: Event | undefined): Record<string, unknown>
|
||||
function resolveProfileTags(event: Event | undefined, authTag: string[] | undefined): string[][] {
|
||||
const existingTags = event?.tags ?? [];
|
||||
if (!authTag) {
|
||||
return existingTags.map((tag) => [...tag]);
|
||||
return existingTags.map((tag) => tag.slice());
|
||||
}
|
||||
return [...existingTags.filter((tag) => tag[0] !== "auth").map((tag) => [...tag]), [...authTag]];
|
||||
return [
|
||||
...existingTags.filter((tag) => tag[0] !== "auth").map((tag) => tag.slice()),
|
||||
[...authTag],
|
||||
];
|
||||
}
|
||||
|
||||
function hasConfiguredAuthTag(event: Event | undefined, authTag: string[] | undefined): boolean {
|
||||
@@ -192,7 +193,7 @@ export async function syncBuzzProfile(params: {
|
||||
kind: AGENT_PROFILE_KIND,
|
||||
content: agentContent,
|
||||
current: currentAgentProfile,
|
||||
tags: currentAgentProfile?.tags.map((tag) => [...tag]) ?? [],
|
||||
tags: currentAgentProfile?.tags.map((tag) => tag.slice()) ?? [],
|
||||
secretKey: params.secretKey,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -29,7 +29,11 @@ async function sleepWithSignal(delayMs: number, signal: AbortSignal): Promise<vo
|
||||
clearTimeout(timer);
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
if (error !== undefined) {
|
||||
reject(error);
|
||||
reject(
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error("Buzz room access wait failed", { cause: error }),
|
||||
);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
@@ -71,7 +75,7 @@ export async function waitForBuzzRoomAccess(params: {
|
||||
let settled = false;
|
||||
let checking = false;
|
||||
let queuedRetry = false;
|
||||
let subscription: ReturnType<Relay["subscribe"]> | undefined;
|
||||
const subscriptionRef: { current?: ReturnType<Relay["subscribe"]> } = {};
|
||||
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
||||
const seenEvents = new Set<string>();
|
||||
|
||||
@@ -84,7 +88,7 @@ export async function waitForBuzzRoomAccess(params: {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
}
|
||||
subscription?.close("room access found");
|
||||
subscriptionRef.current?.close("room access found");
|
||||
if (error !== undefined) {
|
||||
reject(
|
||||
error instanceof Error
|
||||
@@ -141,7 +145,7 @@ export async function waitForBuzzRoomAccess(params: {
|
||||
};
|
||||
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
subscription = relay.subscribe(
|
||||
subscriptionRef.current = relay.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [MEMBER_ADDED_KIND],
|
||||
@@ -179,7 +183,7 @@ export async function waitForBuzzRoomAccess(params: {
|
||||
},
|
||||
);
|
||||
if (settled) {
|
||||
subscription.close("room access found");
|
||||
subscriptionRef.current.close("room access found");
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -7,7 +7,7 @@ export const BUZZ_ROOM_SYSTEM_KIND = 40099;
|
||||
const HEX_PUBLIC_KEY_PATTERN = /^[0-9a-f]{64}$/u;
|
||||
const MEMBERSHIP_CHANGE_TYPES = new Set(["member_joined", "member_left", "member_removed"]);
|
||||
|
||||
export type BuzzRoomMembershipChange = {
|
||||
type BuzzRoomMembershipChange = {
|
||||
type: "member_joined" | "member_left" | "member_removed";
|
||||
targetPublicKey: string;
|
||||
};
|
||||
|
||||
@@ -300,11 +300,10 @@ export function createBuzzSetupWizard(
|
||||
},
|
||||
configure: async ({ cfg, prompter, options }) => {
|
||||
const existingBuzzConfig = cfg.channels?.buzz;
|
||||
const hasExistingAccessConfig = Boolean(
|
||||
const hasExistingAccessConfig =
|
||||
existingBuzzConfig?.groupPolicy !== undefined ||
|
||||
existingBuzzConfig?.groupAllowFrom !== undefined ||
|
||||
existingBuzzConfig?.groups !== undefined,
|
||||
);
|
||||
existingBuzzConfig?.groups !== undefined;
|
||||
const useFreshAccessDefaults = !isBuzzSetupConfigured(cfg) && !hasExistingAccessConfig;
|
||||
const configuredRelayUrl =
|
||||
existingBuzzConfig?.relayUrl?.trim() || process.env.BUZZ_RELAY_URL?.trim();
|
||||
@@ -434,8 +433,7 @@ export function createBuzzSetupWizard(
|
||||
roomId,
|
||||
{
|
||||
enabled: configuredGroups[roomId]?.enabled ?? true,
|
||||
requireMention:
|
||||
configuredGroups[roomId]?.requireMention ?? (useFreshAccessDefaults ? false : true),
|
||||
requireMention: configuredGroups[roomId]?.requireMention ?? !useFreshAccessDefaults,
|
||||
},
|
||||
]),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user