From 8f524a23b23189df8d4a005e602ce3cc1198abbc Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 26 Jul 2026 14:06:41 +0100 Subject: [PATCH] fix: satisfy Buzz CI boundaries (#113419) --- .../buzz/src/buzz-bus.lifecycle.test.ts | 26 ++++++++++--- extensions/buzz/src/buzz-bus.ts | 38 +++++++++---------- extensions/buzz/src/profile.ts | 13 ++++--- extensions/buzz/src/room-access-wait.ts | 14 ++++--- extensions/buzz/src/room-membership.ts | 2 +- extensions/buzz/src/setup-surface.ts | 8 ++-- 6 files changed, 58 insertions(+), 43 deletions(-) diff --git a/extensions/buzz/src/buzz-bus.lifecycle.test.ts b/extensions/buzz/src/buzz-bus.lifecycle.test.ts index 5bed6c74448a..3713d86625b9 100644 --- a/extensions/buzz/src/buzz-bus.lifecycle.test.ts +++ b/extensions/buzz/src/buzz-bus.lifecycle.test.ts @@ -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>(), @@ -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(); 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((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((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(); diff --git a/extensions/buzz/src/buzz-bus.ts b/extensions/buzz/src/buzz-bus.ts index 5d187b742317..1ec1f5d4b18d 100644 --- a/extensions/buzz/src/buzz-bus.ts +++ b/extensions/buzz/src/buzz-bus.ts @@ -139,25 +139,26 @@ async function sleepWithSignal(delayMs: number, signal?: AbortSignal): Promise((resolve, reject) => { let settled = false; - let timer: ReturnType | 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(); return await new Promise>((resolve, reject) => { let settled = false; - let subscription: ReturnType | undefined; - let timeout: ReturnType | undefined; + const subscriptionRef: { current?: ReturnType } = {}; 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 | 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> = []; + const subscriptions: Array> = []; let stopPresenceHeartbeat = () => {}; const bus: BuzzBus = { publicKey, diff --git a/extensions/buzz/src/profile.ts b/extensions/buzz/src/profile.ts index 81b70975d2ba..2052198c0705 100644 --- a/extensions/buzz/src/profile.ts +++ b/extensions/buzz/src/profile.ts @@ -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 { if (!event) { @@ -27,9 +25,12 @@ function parseProfileContent(event: Event | undefined): Record 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, }), ); diff --git a/extensions/buzz/src/room-access-wait.ts b/extensions/buzz/src/room-access-wait.ts index c3c2c155c23a..c721a3d8a280 100644 --- a/extensions/buzz/src/room-access-wait.ts +++ b/extensions/buzz/src/room-access-wait.ts @@ -29,7 +29,11 @@ async function sleepWithSignal(delayMs: number, signal: AbortSignal): Promise | undefined; + const subscriptionRef: { current?: ReturnType } = {}; let pollTimer: ReturnType | undefined; const seenEvents = new Set(); @@ -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 { diff --git a/extensions/buzz/src/room-membership.ts b/extensions/buzz/src/room-membership.ts index c38df66d93d0..e079e20d15dc 100644 --- a/extensions/buzz/src/room-membership.ts +++ b/extensions/buzz/src/room-membership.ts @@ -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; }; diff --git a/extensions/buzz/src/setup-surface.ts b/extensions/buzz/src/setup-surface.ts index a1b38d198f7a..5eab5d2de406 100644 --- a/extensions/buzz/src/setup-surface.ts +++ b/extensions/buzz/src/setup-surface.ts @@ -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, }, ]), ),