mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(desktop): distinguish duplicate agent devices (#6337)
## Summary - distinguish same-name owned agents by management provenance: `managed here` for Desktop-managed identities and `managed elsewhere` for same-owner relay identities - show provenance only when same-name suggestions collide, alongside each identity's short npub - preserve exact-pubkey selection and keep unique-agent autocomplete unchanged - add a composed mock-bridge regression covering relay owner propagation, rendered labels, keyboard/pointer selection, and outbound mention pubkeys ## Testing - focused mention suggestion mapping and label tests - composed Desktop E2E passes for both same-name identities and exact-pubkey routing - causal mutation verified: replacing the relay candidate's `ownerPubkey` with `null` makes the composed E2E fail on the `managed elsewhere` assertion - pre-push Desktop checks: Biome, TypeScript, file-size ratchet, and 5,103 Desktop tests ## Manual test With two same-name owned agents visible in a channel, type `@<name>`. Duplicate rows identify the identities as `agent · managed here` and `agent · managed elsewhere`, include distinct short npubs, and selecting either routes the mention to that row's exact pubkey. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { mapMentionCandidateToSuggestion } from "./mentionSuggestionMapping.ts";
|
||||
|
||||
const OWNER = "a".repeat(64);
|
||||
|
||||
function candidate(overrides = {}) {
|
||||
return {
|
||||
kind: "identity",
|
||||
pubkey: "b".repeat(64),
|
||||
isAgent: true,
|
||||
isMember: true,
|
||||
ownerPubkey: OWNER,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function suggestion(overrides = {}) {
|
||||
return mapMentionCandidateToSuggestion({
|
||||
candidate: candidate(overrides),
|
||||
currentPubkey: OWNER,
|
||||
label: "Carl",
|
||||
});
|
||||
}
|
||||
|
||||
test("labels Desktop-managed agent identities as managed here", () => {
|
||||
assert.equal(
|
||||
suggestion({ isManagedAgent: true }).agentProvenance,
|
||||
"managed-here",
|
||||
);
|
||||
});
|
||||
|
||||
test("labels same-owner relay agent identities as managed elsewhere", () => {
|
||||
assert.equal(suggestion().agentProvenance, "managed-elsewhere");
|
||||
});
|
||||
|
||||
test("does not attribute another owner's agent to a device", () => {
|
||||
assert.equal(
|
||||
suggestion({ ownerPubkey: "c".repeat(64) }).agentProvenance,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("does not attribute people or personas to a device", () => {
|
||||
assert.equal(suggestion({ isAgent: false }).agentProvenance, undefined);
|
||||
assert.equal(
|
||||
suggestion({ kind: "persona", pubkey: undefined }).agentProvenance,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
@@ -13,6 +13,7 @@ export type MentionSuggestionCandidate = {
|
||||
teamMembers?: TeamMentionMember[];
|
||||
avatarUrl?: string | null;
|
||||
isAgent: boolean;
|
||||
isManagedAgent?: boolean;
|
||||
isMember: boolean;
|
||||
role?: ChannelRole | null;
|
||||
ownerPubkey?: string | null;
|
||||
@@ -52,6 +53,17 @@ export function mapMentionCandidateToSuggestion(opts: {
|
||||
: null) ??
|
||||
null,
|
||||
isAgent: candidate.isAgent,
|
||||
agentProvenance:
|
||||
candidate.kind === "identity" && candidate.isAgent
|
||||
? candidate.isManagedAgent
|
||||
? "managed-here"
|
||||
: candidate.ownerPubkey &&
|
||||
currentPubkey &&
|
||||
normalizePubkey(candidate.ownerPubkey) ===
|
||||
normalizePubkey(currentPubkey)
|
||||
? "managed-elsewhere"
|
||||
: undefined
|
||||
: undefined,
|
||||
notInChannel:
|
||||
candidate.kind !== "team" &&
|
||||
channelType !== "dm" &&
|
||||
|
||||
@@ -349,7 +349,7 @@ export function useMentions(
|
||||
personaId:
|
||||
managedAgentPersonaIdsByPubkey.get(pubkey) ??
|
||||
(activePersonaById.has(pubkey) ? pubkey : undefined),
|
||||
ownerPubkey: null,
|
||||
ownerPubkey: agent.ownerPubkey,
|
||||
isAgent: true,
|
||||
});
|
||||
}
|
||||
@@ -515,13 +515,14 @@ export function useMentions(
|
||||
searchableNamesLowerRef.current = searchableNamesLower;
|
||||
}, [searchableNamesLower]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
if (debounceTimerRef.current !== null) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const matchingSuggestions = React.useMemo<MentionSuggestion[]>(() => {
|
||||
if (mentionQuery === null) {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { mentionAgentLabel } from "./MentionAutocomplete.tsx";
|
||||
|
||||
function suggestion(agentProvenance) {
|
||||
return {
|
||||
pubkey: "1".repeat(64),
|
||||
displayName: "Carl",
|
||||
isAgent: true,
|
||||
agentProvenance,
|
||||
};
|
||||
}
|
||||
|
||||
test("duplicate owned agents show their management provenance", () => {
|
||||
assert.equal(
|
||||
mentionAgentLabel(suggestion("managed-here"), true),
|
||||
"agent · managed here",
|
||||
);
|
||||
assert.equal(
|
||||
mentionAgentLabel(suggestion("managed-elsewhere"), true),
|
||||
"agent · managed elsewhere",
|
||||
);
|
||||
});
|
||||
|
||||
test("unique agents keep the compact generic label", () => {
|
||||
assert.equal(mentionAgentLabel(suggestion("managed-here"), false), "agent");
|
||||
});
|
||||
|
||||
test("agents without trustworthy provenance keep the generic label", () => {
|
||||
assert.equal(mentionAgentLabel(suggestion(undefined), true), "agent");
|
||||
});
|
||||
@@ -22,6 +22,7 @@ export type MentionSuggestion = {
|
||||
displayName: string;
|
||||
avatarUrl?: string | null;
|
||||
isAgent?: boolean;
|
||||
agentProvenance?: "managed-here" | "managed-elsewhere";
|
||||
notInChannel?: boolean;
|
||||
ownerLabel?: string | null;
|
||||
role?: string | null;
|
||||
@@ -35,6 +36,16 @@ type MentionAutocompleteProps = {
|
||||
position?: "above" | "below";
|
||||
};
|
||||
|
||||
export function mentionAgentLabel(
|
||||
suggestion: MentionSuggestion,
|
||||
hasNameCollision: boolean,
|
||||
) {
|
||||
if (!hasNameCollision || !suggestion.agentProvenance) return "agent";
|
||||
return suggestion.agentProvenance === "managed-here"
|
||||
? "agent · managed here"
|
||||
: "agent · managed elsewhere";
|
||||
}
|
||||
|
||||
export const MentionAutocomplete = React.memo(function MentionAutocomplete({
|
||||
suggestions,
|
||||
selectedIndex,
|
||||
@@ -100,9 +111,9 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({
|
||||
(suggestion.personaId ? `persona-${suggestion.personaId}` : null) ??
|
||||
(suggestion.teamId ? `team-${suggestion.teamId}` : null) ??
|
||||
suggestion.displayName;
|
||||
const agentLabel = "agent";
|
||||
const hasNameCollision =
|
||||
(nameCounts.get(suggestion.displayName.toLowerCase()) ?? 0) > 1;
|
||||
const agentLabel = mentionAgentLabel(suggestion, hasNameCollision);
|
||||
const collisionNpub =
|
||||
hasNameCollision && suggestion.pubkey
|
||||
? safeNpub(suggestion.pubkey)
|
||||
|
||||
@@ -320,6 +320,112 @@ test("@ trigger prioritizes channel members before runnable personas and other m
|
||||
expect(fizzIndex).toBeLessThan(charlieIndex);
|
||||
});
|
||||
|
||||
test("duplicate owned agents preserve provenance and exact pubkey selection", async ({
|
||||
page,
|
||||
}) => {
|
||||
const managedPubkey = IN_CHANNEL_MANAGED_AGENT_PUBKEY;
|
||||
const relayPubkey = ALLOWLIST_RELAY_AGENT_PUBKEY;
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [
|
||||
{
|
||||
pubkey: managedPubkey,
|
||||
name: "carl",
|
||||
status: "running",
|
||||
channelNames: ["general"],
|
||||
backend: {
|
||||
type: "provider",
|
||||
id: "mock",
|
||||
config: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
relayAgents: [
|
||||
{
|
||||
pubkey: relayPubkey,
|
||||
ownerPubkey: MOCK_VIEWER_PUBKEY,
|
||||
name: "carl",
|
||||
respondTo: "owner-only",
|
||||
channelNames: ["general"],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await page.evaluate(
|
||||
async ({ channelId, pubkey }) => {
|
||||
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
|
||||
if (!invoke) throw new Error("Mock bridge is not installed.");
|
||||
await invoke("add_channel_members", {
|
||||
channelId,
|
||||
pubkeys: [pubkey],
|
||||
role: "bot",
|
||||
});
|
||||
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
|
||||
queryKey: ["channels"],
|
||||
});
|
||||
},
|
||||
{ channelId: GENERAL_CHANNEL_ID, pubkey: relayPubkey },
|
||||
);
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
await input.fill("@carl");
|
||||
const dropdown = autocomplete(page);
|
||||
const managedRow = dropdown.getByTestId(
|
||||
`mention-suggestion-${managedPubkey}`,
|
||||
);
|
||||
const relayRow = dropdown.getByTestId(`mention-suggestion-${relayPubkey}`);
|
||||
await expect(managedRow).toContainText("agent · managed here");
|
||||
await expect(relayRow).toContainText("agent · managed elsewhere");
|
||||
|
||||
const collisionKeys = dropdown.getByTestId("mention-collision-npub");
|
||||
await expect(collisionKeys).toHaveCount(2);
|
||||
const fullNpubs = await collisionKeys.evaluateAll((nodes) =>
|
||||
nodes.map((node) => node.getAttribute("title")),
|
||||
);
|
||||
expect(fullNpubs).toHaveLength(2);
|
||||
expect(new Set(fullNpubs).size).toBe(2);
|
||||
|
||||
const initialRows = dropdown.locator("button");
|
||||
const managedIndex = await initialRows.evaluateAll(
|
||||
(buttons, pubkey) =>
|
||||
buttons.findIndex(
|
||||
(button) =>
|
||||
button.getAttribute("data-testid") === `mention-suggestion-${pubkey}`,
|
||||
),
|
||||
managedPubkey,
|
||||
);
|
||||
expect(managedIndex).toBeGreaterThanOrEqual(0);
|
||||
for (let index = 0; index < managedIndex; index += 1) {
|
||||
await input.press("ArrowDown");
|
||||
}
|
||||
await input.press("Enter");
|
||||
await page.keyboard.type("local");
|
||||
await page.getByTestId("send-message").click();
|
||||
await expect
|
||||
.poll(() => readOutgoingMentionPubkeys(page, "@carl local"))
|
||||
.toEqual([managedPubkey]);
|
||||
await expect(input).toBeEmpty();
|
||||
|
||||
await input.fill("@carl");
|
||||
const reopenedDropdown = autocomplete(page);
|
||||
await expect(reopenedDropdown).toBeVisible();
|
||||
await reopenedDropdown
|
||||
.getByTestId(`mention-suggestion-${relayPubkey}`)
|
||||
.click();
|
||||
await page.keyboard.type("remote");
|
||||
await page.getByTestId("send-message").click();
|
||||
const sendWithoutInviting = page.getByRole("button", { name: "Do nothing" });
|
||||
try {
|
||||
await sendWithoutInviting.waitFor({ state: "visible", timeout: 2_000 });
|
||||
await sendWithoutInviting.click();
|
||||
} catch {
|
||||
// In-channel selections send immediately without opening the prompt.
|
||||
}
|
||||
await expect
|
||||
.poll(() => readOutgoingMentionPubkeys(page, "@carl remote"))
|
||||
.toEqual([relayPubkey]);
|
||||
});
|
||||
|
||||
test("relay-only shared agents emit an outbound mention tag when selected", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user