mirror of
https://github.com/block/buzz.git
synced 2026-11-06 17:31:19 +00:00
## Summary - preserve OSS relay-agent mentions under shared channel and agent policy - restrict owner-only release builds to relay agents with cryptographically verified ownership matching the current user - remove the remote policy replay loop that repeatedly rebuilt the relay directory, while retaining focused polling and send-time revalidation - query relay profiles and managed policies by exact author coordinates to prevent noisy events from crowding out valid agents ## Diagnosis The packaged Block release compiles `BUZZ_DESKTOP_BUILD_AGENT_ACCESS_OWNER_ONLY`, while ordinary OSS/dev builds do not. Relay-discovered agents were filtered as if all remote agents were outside that owner-only boundary, so a same-owner agent running on another machine disappeared in the release even though the OSS path could look healthy. The fix uses the NIP-OA-authenticated owner from the relay directory as the cross-machine proof. Internal builds admit only verified same-owner agents and fail closed for missing, mismatched, stale/revoked, or unavailable ownership evidence. OSS builds retain shared channel/policy behavior. ## Validation - desktop focused unit coverage: 39 tests passed - desktop typecheck and focused static checks passed - focused Tauri Rust policy/directory tests passed - production-style E2E build succeeded - targeted Playwright mention scenarios passed: - owner-only release hides other-owned relay agent - owner-only release shows verified same-owner relay agent - OSS build shows shared `anyone` agent - repository pre-push hook passed on `4d40b6e5bb032f2c0755127172c50dee213f65a3`: - branch skew - desktop check and typecheck - desktop tests - Rust tests - Tauri checks - mobile tests --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
204 lines
5.8 KiB
JavaScript
204 lines
5.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test, { mock } from "node:test";
|
|
|
|
import { relayClient } from "@/shared/api/relayClient";
|
|
import {
|
|
KIND_DELETION,
|
|
KIND_MANAGED_AGENT,
|
|
KIND_PERSONA,
|
|
KIND_TEAM,
|
|
} from "@/shared/constants/kinds";
|
|
import {
|
|
coalesceManagedAgentBackfill,
|
|
startPersonaSync,
|
|
} from "./usePersonaSync.ts";
|
|
|
|
const EXPECTED_KINDS = [
|
|
KIND_PERSONA,
|
|
KIND_TEAM,
|
|
KIND_MANAGED_AGENT,
|
|
KIND_DELETION,
|
|
];
|
|
|
|
function event({
|
|
id,
|
|
kind = KIND_MANAGED_AGENT,
|
|
createdAt,
|
|
pubkey = "owner-pubkey",
|
|
dTag = "agent-pubkey",
|
|
}) {
|
|
return {
|
|
id,
|
|
pubkey,
|
|
created_at: createdAt,
|
|
kind,
|
|
tags: dTag ? [["d", dTag]] : [],
|
|
content: "{}",
|
|
sig: "sig",
|
|
};
|
|
}
|
|
|
|
test("startup backfill keeps only the newest managed-agent head per coordinate", () => {
|
|
const persona = event({
|
|
id: "persona",
|
|
kind: KIND_PERSONA,
|
|
createdAt: 1,
|
|
dTag: "persona-id",
|
|
});
|
|
const otherAgent = event({
|
|
id: "other-agent",
|
|
createdAt: 2,
|
|
dTag: "other-agent",
|
|
});
|
|
const oldest = event({ id: "oldest", createdAt: 1 });
|
|
const sameSecondLoser = event({ id: "f", createdAt: 3 });
|
|
const newest = event({ id: "a", createdAt: 3 });
|
|
|
|
assert.deepEqual(
|
|
coalesceManagedAgentBackfill([
|
|
oldest,
|
|
persona,
|
|
newest,
|
|
otherAgent,
|
|
sameSecondLoser,
|
|
]).map(({ id }) => id),
|
|
["persona", "a", "other-agent"],
|
|
"NIP-33 uses newest created_at and lowest id on a tie",
|
|
);
|
|
});
|
|
|
|
// Regression guard for the fresh-start backfill gap (F3): a device that comes
|
|
// online AFTER another published gets zero history from a live-only `limit: 0`
|
|
// subscription, because reconnect-replay's since-cursor is undefined until the
|
|
// first live event. `startPersonaSync` MUST do a one-shot history fetch up
|
|
// front, and both the backfill and the live sub MUST carry the deletion kind
|
|
// so tombstones catch up too.
|
|
test("startPersonaSync backfills history including the deletion kind", () => {
|
|
const fetchCalls = [];
|
|
const liveCalls = [];
|
|
mock.method(relayClient, "fetchEvents", (filter) => {
|
|
fetchCalls.push(filter);
|
|
return Promise.resolve([]);
|
|
});
|
|
mock.method(relayClient, "subscribeLive", (filter) => {
|
|
liveCalls.push(filter);
|
|
return Promise.resolve(() => Promise.resolve());
|
|
});
|
|
|
|
startPersonaSync("owner-pubkey", "wss://relay.example", () => false);
|
|
|
|
assert.equal(fetchCalls.length, 1, "must do exactly one backfill fetch");
|
|
assert.deepEqual(
|
|
fetchCalls[0].kinds,
|
|
EXPECTED_KINDS,
|
|
"backfill must cover persona/team/agent + deletion",
|
|
);
|
|
assert.ok(
|
|
fetchCalls[0].limit > 0,
|
|
"backfill must request a positive limit — limit:0 returns no history",
|
|
);
|
|
assert.deepEqual(fetchCalls[0].authors, ["owner-pubkey"]);
|
|
|
|
assert.equal(liveCalls.length, 1);
|
|
assert.deepEqual(
|
|
liveCalls[0].kinds,
|
|
EXPECTED_KINDS,
|
|
"live sub must also carry the deletion kind",
|
|
);
|
|
|
|
mock.reset();
|
|
});
|
|
|
|
// Regression guard for the arrival-scope fix (F6): the reconcile must carry the
|
|
// relay this subscription was opened on, NOT whichever community happens to be
|
|
// active when the reconcile runs. Without the forwarded URL the backend falls
|
|
// back to the active workspace and an in-flight event lands in the wrong
|
|
// community's scoped retention store on a mid-flight switch.
|
|
test("startPersonaSync forwards its own relay as the event arrival relay", async () => {
|
|
const invokes = [];
|
|
// @tauri-apps/api/core reads `window.__TAURI_INTERNALS__.invoke`.
|
|
globalThis.window = {
|
|
__TAURI_INTERNALS__: {
|
|
invoke: (cmd, args) => {
|
|
invokes.push({ cmd, args });
|
|
return Promise.resolve();
|
|
},
|
|
},
|
|
};
|
|
|
|
const ownEvent = { id: "e1", pubkey: "owner-pubkey", kind: KIND_PERSONA };
|
|
const foreignEvent = { id: "e2", pubkey: "someone-else", kind: KIND_PERSONA };
|
|
|
|
mock.method(relayClient, "fetchEvents", () =>
|
|
Promise.resolve([ownEvent, foreignEvent]),
|
|
);
|
|
mock.method(relayClient, "subscribeLive", () =>
|
|
Promise.resolve(() => Promise.resolve()),
|
|
);
|
|
|
|
startPersonaSync("owner-pubkey", "wss://community-a.example", () => false);
|
|
// Let the backfill promise chain and the reconcile invoke settle.
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
const reconciles = invokes.filter(
|
|
(call) => call.cmd === "reconcile_inbound_persona_event",
|
|
);
|
|
assert.equal(
|
|
reconciles.length,
|
|
1,
|
|
"only the subscribed author's event reconciles",
|
|
);
|
|
assert.equal(
|
|
reconciles[0].args.arrivalRelayUrl,
|
|
"wss://community-a.example",
|
|
"reconcile must carry the subscription's relay as the arrival relay",
|
|
);
|
|
assert.equal(JSON.parse(reconciles[0].args.eventJson).id, "e1");
|
|
|
|
mock.reset();
|
|
delete globalThis.window;
|
|
});
|
|
|
|
test("startPersonaSync serializes inbound reconciliation in relay order", async () => {
|
|
const resolvers = [];
|
|
const invokedIds = [];
|
|
globalThis.window = {
|
|
__TAURI_INTERNALS__: {
|
|
invoke: (_cmd, args) => {
|
|
invokedIds.push(JSON.parse(args.eventJson).id);
|
|
return new Promise((resolve) => resolvers.push(resolve));
|
|
},
|
|
},
|
|
};
|
|
|
|
let onEvent;
|
|
mock.method(relayClient, "fetchEvents", () => Promise.resolve([]));
|
|
mock.method(relayClient, "subscribeLive", (_filter, listener) => {
|
|
onEvent = listener;
|
|
return Promise.resolve(() => Promise.resolve());
|
|
});
|
|
|
|
startPersonaSync("owner-pubkey", "wss://community.example", () => false);
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
onEvent({ id: "broad", pubkey: "owner-pubkey", kind: KIND_MANAGED_AGENT });
|
|
onEvent({
|
|
id: "restricted",
|
|
pubkey: "owner-pubkey",
|
|
kind: KIND_MANAGED_AGENT,
|
|
});
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
assert.deepEqual(
|
|
invokedIds,
|
|
["broad"],
|
|
"newer event waits for prior deployment",
|
|
);
|
|
resolvers.shift()();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(invokedIds, ["broad", "restricted"]);
|
|
resolvers.shift()();
|
|
|
|
mock.reset();
|
|
delete globalThis.window;
|
|
});
|