mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(desktop): prefer live agent mentions (#2149)
Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
coalesceAgentAutocompleteCandidates,
|
coalesceAgentAutocompleteCandidates,
|
||||||
getMentionableAgentPubkeys,
|
getMentionableAgentPubkeys,
|
||||||
getSharedChannelIds,
|
getSharedChannelIds,
|
||||||
|
isAgentIdentityInManagedList,
|
||||||
relayAgentIsSharedWithUser,
|
relayAgentIsSharedWithUser,
|
||||||
shouldHideAgentFromMentions,
|
shouldHideAgentFromMentions,
|
||||||
} from "./agentAutocompleteEligibility.ts";
|
} from "./agentAutocompleteEligibility.ts";
|
||||||
@@ -135,6 +136,32 @@ test("getMentionableAgentPubkeys: keeps managed agents and shared relay agents",
|
|||||||
assert.deepEqual(result, new Set([PUB_A, PUB_B, PUB_C]));
|
assert.deepEqual(result, new Set([PUB_A, PUB_B, PUB_C]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("isAgentIdentityInManagedList: keeps people and only current managed agent identities", () => {
|
||||||
|
const managedAgentPubkeys = new Set([PUB_A]);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
isAgentIdentityInManagedList(
|
||||||
|
{ isAgent: false, pubkey: PUB_B },
|
||||||
|
managedAgentPubkeys,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isAgentIdentityInManagedList(
|
||||||
|
{ isAgent: true, pubkey: PUB_A.toUpperCase() },
|
||||||
|
managedAgentPubkeys,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isAgentIdentityInManagedList(
|
||||||
|
{ isAgent: true, pubkey: PUB_B },
|
||||||
|
managedAgentPubkeys,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("shouldHideAgentFromMentions: never hides non-agents", () => {
|
test("shouldHideAgentFromMentions: never hides non-agents", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldHideAgentFromMentions({
|
shouldHideAgentFromMentions({
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ export function getMentionableAgentPubkeys({
|
|||||||
return pubkeys;
|
return pubkeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAgentIdentityInManagedList(
|
||||||
|
candidate: { isAgent?: boolean; pubkey: string },
|
||||||
|
managedAgentPubkeys: ReadonlySet<string>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
candidate.isAgent !== true ||
|
||||||
|
managedAgentPubkeys.has(normalizePubkey(candidate.pubkey))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldHideAgentFromMentions({
|
export function shouldHideAgentFromMentions({
|
||||||
isAgent,
|
isAgent,
|
||||||
isMember,
|
isMember,
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { Bot, UserRoundPlus, X } from "lucide-react";
|
|||||||
import {
|
import {
|
||||||
useAddChannelMembersMutation,
|
useAddChannelMembersMutation,
|
||||||
useChannelMembersQuery,
|
useChannelMembersQuery,
|
||||||
useChannelsQuery,
|
|
||||||
} from "@/features/channels/hooks";
|
} from "@/features/channels/hooks";
|
||||||
import {
|
import {
|
||||||
coalesceAgentAutocompleteCandidates,
|
coalesceAgentAutocompleteCandidates,
|
||||||
getMentionableAgentPubkeys,
|
isAgentIdentityInManagedList,
|
||||||
getSharedChannelIds,
|
|
||||||
} from "@/features/agents/lib/agentAutocompleteEligibility";
|
} from "@/features/agents/lib/agentAutocompleteEligibility";
|
||||||
import { useIsArchivedPredicate } from "@/features/identity-archive/hooks";
|
import { useIsArchivedPredicate } from "@/features/identity-archive/hooks";
|
||||||
import { useClassifiedMembers } from "@/features/channels/lib/useClassifiedMembers";
|
import { useClassifiedMembers } from "@/features/channels/lib/useClassifiedMembers";
|
||||||
@@ -146,7 +144,6 @@ export function MembersSidebar({
|
|||||||
const identityQuery = useIdentityQuery();
|
const identityQuery = useIdentityQuery();
|
||||||
const membersQuery = useChannelMembersQuery(channelId, open);
|
const membersQuery = useChannelMembersQuery(channelId, open);
|
||||||
const addMembersMutation = useAddChannelMembersMutation(channelId);
|
const addMembersMutation = useAddChannelMembersMutation(channelId);
|
||||||
const channelsQuery = useChannelsQuery({ enabled: open });
|
|
||||||
const changeRoleMutation = useMutation({
|
const changeRoleMutation = useMutation({
|
||||||
mutationFn: async ({ pubkey, role }: { pubkey: string; role: string }) => {
|
mutationFn: async ({ pubkey, role }: { pubkey: string; role: string }) => {
|
||||||
if (!channelId) throw new Error("No channel selected.");
|
if (!channelId) throw new Error("No channel selected.");
|
||||||
@@ -262,15 +259,7 @@ export function MembersSidebar({
|
|||||||
.map((member) => member.displayName?.trim().toLowerCase())
|
.map((member) => member.displayName?.trim().toLowerCase())
|
||||||
.filter((label): label is string => Boolean(label)),
|
.filter((label): label is string => Boolean(label)),
|
||||||
);
|
);
|
||||||
const sharedChannelIds = getSharedChannelIds(channelsQuery.data);
|
const managedAgentPubkeys = new Set(managedAgentsByPubkey.keys());
|
||||||
const eligibleAgentPubkeys = getMentionableAgentPubkeys({
|
|
||||||
currentPubkey,
|
|
||||||
managedAgentPubkeys: (managedAgentsQuery.data ?? []).map(
|
|
||||||
(agent) => agent.pubkey,
|
|
||||||
),
|
|
||||||
relayAgents: relayAgentsQuery.data,
|
|
||||||
sharedChannelIds,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addCandidate = (candidate: AddMemberSearchCandidate) => {
|
const addCandidate = (candidate: AddMemberSearchCandidate) => {
|
||||||
const pubkey = normalizePubkey(candidate.pubkey);
|
const pubkey = normalizePubkey(candidate.pubkey);
|
||||||
@@ -281,7 +270,7 @@ export function MembersSidebar({
|
|||||||
)) ||
|
)) ||
|
||||||
memberPubkeys.has(pubkey) ||
|
memberPubkeys.has(pubkey) ||
|
||||||
isArchivedDiscovery(pubkey) ||
|
isArchivedDiscovery(pubkey) ||
|
||||||
(candidate.isAgent && !eligibleAgentPubkeys.has(pubkey))
|
!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -320,10 +309,6 @@ export function MembersSidebar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const agent of relayAgentsQuery.data ?? []) {
|
for (const agent of relayAgentsQuery.data ?? []) {
|
||||||
if (!eligibleAgentPubkeys.has(normalizePubkey(agent.pubkey))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
addCandidate({
|
addCandidate({
|
||||||
pubkey: agent.pubkey,
|
pubkey: agent.pubkey,
|
||||||
displayName: agent.name,
|
displayName: agent.name,
|
||||||
@@ -365,7 +350,6 @@ export function MembersSidebar({
|
|||||||
}, [
|
}, [
|
||||||
canAddMembers,
|
canAddMembers,
|
||||||
isArchivedDiscovery,
|
isArchivedDiscovery,
|
||||||
channelsQuery.data,
|
|
||||||
currentPubkey,
|
currentPubkey,
|
||||||
managedAgentsQuery.data,
|
managedAgentsQuery.data,
|
||||||
memberPubkeys,
|
memberPubkeys,
|
||||||
@@ -377,8 +361,7 @@ export function MembersSidebar({
|
|||||||
const isAddSearchLoading =
|
const isAddSearchLoading =
|
||||||
userSearchQuery.isLoading ||
|
userSearchQuery.isLoading ||
|
||||||
managedAgentsQuery.isLoading ||
|
managedAgentsQuery.isLoading ||
|
||||||
relayAgentsQuery.isLoading ||
|
relayAgentsQuery.isLoading;
|
||||||
channelsQuery.isLoading;
|
|
||||||
const handlePeopleSearchScroll = useUserSearchFetchMoreOnScroll(
|
const handlePeopleSearchScroll = useUserSearchFetchMoreOnScroll(
|
||||||
userSearchQuery,
|
userSearchQuery,
|
||||||
canAddMembers && normalizedDeferredSearchQuery.length > 0,
|
canAddMembers && normalizedDeferredSearchQuery.length > 0,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
coalesceAutocompleteCandidatesByKey,
|
coalesceAutocompleteCandidatesByKey,
|
||||||
getMentionableAgentPubkeys,
|
getMentionableAgentPubkeys,
|
||||||
getSharedChannelIds,
|
getSharedChannelIds,
|
||||||
|
isAgentIdentityInManagedList,
|
||||||
shouldHideAgentFromMentions,
|
shouldHideAgentFromMentions,
|
||||||
} from "@/features/agents/lib/agentAutocompleteEligibility";
|
} from "@/features/agents/lib/agentAutocompleteEligibility";
|
||||||
import {
|
import {
|
||||||
@@ -245,6 +246,9 @@ export function useMentions(
|
|||||||
if (isArchivedDiscovery(pubkey)) {
|
if (isArchivedDiscovery(pubkey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
shouldHideAgentFromMentions({
|
shouldHideAgentFromMentions({
|
||||||
isAgent: candidate.isAgent === true,
|
isAgent: candidate.isAgent === true,
|
||||||
@@ -256,7 +260,6 @@ export function useMentions(
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = candidatesByPubkey.get(pubkey);
|
const current = candidatesByPubkey.get(pubkey);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
candidatesByPubkey.set(pubkey, { ...candidate, pubkey });
|
candidatesByPubkey.set(pubkey, { ...candidate, pubkey });
|
||||||
@@ -289,7 +292,6 @@ export function useMentions(
|
|||||||
isManagedAgent: current.isManagedAgent || candidate.isManagedAgent,
|
isManagedAgent: current.isManagedAgent || candidate.isManagedAgent,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const member of members ?? []) {
|
for (const member of members ?? []) {
|
||||||
const pubkey = normalizePubkey(member.pubkey);
|
const pubkey = normalizePubkey(member.pubkey);
|
||||||
const linkedPersonaId = activePersonaById.has(pubkey)
|
const linkedPersonaId = activePersonaById.has(pubkey)
|
||||||
@@ -418,6 +420,7 @@ export function useMentions(
|
|||||||
managedAgentNamesByPubkey,
|
managedAgentNamesByPubkey,
|
||||||
managedAgentPersonaIds,
|
managedAgentPersonaIds,
|
||||||
managedAgentPersonaIdsByPubkey,
|
managedAgentPersonaIdsByPubkey,
|
||||||
|
managedAgentPubkeys,
|
||||||
managedAgentsQuery.data,
|
managedAgentsQuery.data,
|
||||||
memberPubkeys,
|
memberPubkeys,
|
||||||
members,
|
members,
|
||||||
@@ -540,10 +543,7 @@ export function useMentions(
|
|||||||
mentionQuery,
|
mentionQuery,
|
||||||
activePersonaIds,
|
activePersonaIds,
|
||||||
)
|
)
|
||||||
.slice(
|
.slice(0, MENTION_SUGGESTION_LIMIT)
|
||||||
0,
|
|
||||||
Math.max(MENTION_SUGGESTION_LIMIT, mentionCandidatesWithTeams.length),
|
|
||||||
)
|
|
||||||
.map(({ candidate, label }) =>
|
.map(({ candidate, label }) =>
|
||||||
mapMentionCandidateToSuggestion({
|
mapMentionCandidateToSuggestion({
|
||||||
candidate,
|
candidate,
|
||||||
|
|||||||
@@ -812,10 +812,17 @@ test("routes an agent mention from an existing DM to the expanded conversation",
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("routes a relay-agent mention from an existing DM to the expanded conversation", async ({
|
test("routes a managed relay-agent mention from an existing DM to the expanded conversation", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await installMockBridge(page, {
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: DM_RELAY_AGENT_PUBKEY,
|
||||||
|
name: "quinn",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
relayAgents: [
|
relayAgents: [
|
||||||
{
|
{
|
||||||
pubkey: DM_RELAY_AGENT_PUBKEY,
|
pubkey: DM_RELAY_AGENT_PUBKEY,
|
||||||
@@ -856,15 +863,13 @@ test("routes a relay-agent mention from an existing DM to the expanded conversat
|
|||||||
page.locator("[data-active='true'][data-channel-id]"),
|
page.locator("[data-active='true'][data-channel-id]"),
|
||||||
).toHaveAttribute("data-channel-id", sentChannelId ?? "");
|
).toHaveAttribute("data-channel-id", sentChannelId ?? "");
|
||||||
await expect(page.getByTestId("chat-title")).toContainText("alice");
|
await expect(page.getByTestId("chat-title")).toContainText("alice");
|
||||||
await expect(page.getByTestId("chat-title")).toContainText(
|
await expect(page.getByTestId("chat-title")).toContainText("quinn");
|
||||||
DM_RELAY_AGENT_PUBKEY.slice(0, 8),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendCommands = (await readCommandPayloadLog(page)).slice(
|
const sendCommands = (await readCommandPayloadLog(page)).slice(
|
||||||
baselineCommands.length,
|
baselineCommands.length,
|
||||||
);
|
);
|
||||||
expect(sendCommands.map((entry) => entry.command)).toContain("open_dm");
|
expect(sendCommands.map((entry) => entry.command)).toContain("open_dm");
|
||||||
expect(sendCommands.map((entry) => entry.command)).not.toContain(
|
expect(sendCommands.map((entry) => entry.command)).toContain(
|
||||||
"start_managed_agent",
|
"start_managed_agent",
|
||||||
);
|
);
|
||||||
expect(sendCommands.map((entry) => entry.command)).not.toContain(
|
expect(sendCommands.map((entry) => entry.command)).not.toContain(
|
||||||
@@ -2554,7 +2559,18 @@ test("home inbox manage affordance opens management without leaving home", async
|
|||||||
await expect(page).not.toHaveURL(/#\/channels\//);
|
await expect(page).not.toHaveURL(/#\/channels\//);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("members sidebar can invite and remove members", async ({ page }) => {
|
test("members sidebar can invite and remove managed agents", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||||||
|
name: "charlie",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await openMembersSidebar(page, "general");
|
await openMembersSidebar(page, "general");
|
||||||
const initialMemberCount = await readMembersTriggerCount(page);
|
const initialMemberCount = await readMembersTriggerCount(page);
|
||||||
@@ -2847,9 +2863,18 @@ test("members sidebar collapses same-persona managed agents", async ({
|
|||||||
await expect(page.getByText("Pinky", { exact: true })).toHaveCount(1);
|
await expect(page.getByText("Pinky", { exact: true })).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("private-channel members can add members and bots without admin", async ({
|
test("private-channel members can add people and managed agents without admin", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||||||
|
name: "charlie",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
// secret-projects is a private (non-DM) channel where the current user is a
|
// secret-projects is a private (non-DM) channel where the current user is a
|
||||||
// plain member, not owner/admin. They should still be able to add members
|
// plain member, not owner/admin. They should still be able to add members
|
||||||
|
|||||||
@@ -166,38 +166,38 @@ test.describe("NIP-IA hide archived from discovery", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Member-adder (ChannelMemberInviteCard) — the invite search excludes
|
// Member-adder (ChannelMemberInviteCard) — the invite search excludes
|
||||||
// existing channel members, so we test in #agents where Alice is NOT a
|
// existing channel members, so we test in #agents where Bob is NOT a
|
||||||
// member (only the mock identity + Charlie are): the ONLY thing that can
|
// member (only the mock identity + Charlie are): the ONLY thing that can
|
||||||
// drop her from results is the archive filter. The control run (nothing
|
// drop Bob from results is the archive filter. The control run (nothing
|
||||||
// archived) proves she would otherwise appear — guarding a vacuous green.
|
// archived) proves he would otherwise appear — guarding a vacuous green.
|
||||||
test("member-adder: archived non-member is omitted from invite search", async ({
|
test("member-adder: archived non-member is omitted from invite search", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await installMockBridge(page, { archivedIdentities: [ALICE_PUBKEY] });
|
await installMockBridge(page, { archivedIdentities: [BOB_PUBKEY] });
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("channel-agents").click();
|
await page.getByTestId("channel-agents").click();
|
||||||
await page.getByTestId("channel-members-trigger").click();
|
await page.getByTestId("channel-members-trigger").click();
|
||||||
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
||||||
await page.getByTestId("channel-management-search-users").fill("alice");
|
await page.getByTestId("channel-management-search-users").fill("bob");
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId(`channel-user-search-result-${ALICE_PUBKEY}`),
|
page.getByTestId(`channel-user-search-result-${BOB_PUBKEY}`),
|
||||||
).toHaveCount(0);
|
).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("member-adder: control — non-archived non-member IS in invite search", async ({
|
test("member-adder: control — non-archived non-member IS in invite search", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
// Same channel/query, nothing archived: Alice now appears. This is the
|
// Same channel/query, nothing archived: Bob now appears. This is the
|
||||||
// companion that makes the test above non-vacuous (proves she was dropped
|
// companion that makes the test above non-vacuous (proves he was dropped
|
||||||
// by the archive filter, not by member-exclusion or a broken search).
|
// by the archive filter, not by member-exclusion or a broken search).
|
||||||
await installMockBridge(page, { archivedIdentities: [] });
|
await installMockBridge(page, { archivedIdentities: [] });
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("channel-agents").click();
|
await page.getByTestId("channel-agents").click();
|
||||||
await page.getByTestId("channel-members-trigger").click();
|
await page.getByTestId("channel-members-trigger").click();
|
||||||
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
await expect(page.getByTestId("members-sidebar")).toBeVisible();
|
||||||
await page.getByTestId("channel-management-search-users").fill("alice");
|
await page.getByTestId("channel-management-search-users").fill("bob");
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId(`channel-user-search-result-${ALICE_PUBKEY}`),
|
page.getByTestId(`channel-user-search-result-${BOB_PUBKEY}`),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -187,11 +187,18 @@ async function expectAgentProfileActionsHidden(
|
|||||||
).toHaveCount(0);
|
).toHaveCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
test("@ trigger prioritizes channel members before runnable personas and other agents", async ({
|
test("@ trigger prioritizes channel members before runnable personas and other managed agents", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await installMockBridge(page, {
|
await installMockBridge(page, {
|
||||||
activePersonaIds: ["builtin:fizz"],
|
activePersonaIds: ["builtin:fizz"],
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||||||
|
name: "charlie",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("channel-general").click();
|
await page.getByTestId("channel-general").click();
|
||||||
@@ -202,7 +209,7 @@ test("@ trigger prioritizes channel members before runnable personas and other a
|
|||||||
|
|
||||||
const dropdown = autocomplete(page);
|
const dropdown = autocomplete(page);
|
||||||
await expect(dropdown).toBeVisible();
|
await expect(dropdown).toBeVisible();
|
||||||
await expect(dropdown.getByText("alice")).toBeVisible();
|
await expect(dropdown.getByText("alice")).toHaveCount(0);
|
||||||
await expect(dropdown.getByText("bob")).toBeVisible();
|
await expect(dropdown.getByText("bob")).toBeVisible();
|
||||||
await expect(dropdown.getByText("Fizz")).toBeVisible();
|
await expect(dropdown.getByText("Fizz")).toBeVisible();
|
||||||
await expect(dropdown.getByText("charlie")).toBeVisible();
|
await expect(dropdown.getByText("charlie")).toBeVisible();
|
||||||
@@ -219,7 +226,6 @@ test("@ trigger prioritizes channel members before runnable personas and other a
|
|||||||
const suggestions = dropdown.locator("button");
|
const suggestions = dropdown.locator("button");
|
||||||
const suggestionText = await suggestions.allInnerTexts();
|
const suggestionText = await suggestions.allInnerTexts();
|
||||||
const fizzIndex = suggestionText.findIndex((text) => text.includes("Fizz"));
|
const fizzIndex = suggestionText.findIndex((text) => text.includes("Fizz"));
|
||||||
const aliceIndex = suggestionText.findIndex((text) => text.includes("alice"));
|
|
||||||
const bobIndex = suggestionText.findIndex((text) => text.includes("bob"));
|
const bobIndex = suggestionText.findIndex((text) => text.includes("bob"));
|
||||||
const charlieIndex = suggestionText.findIndex((text) =>
|
const charlieIndex = suggestionText.findIndex((text) =>
|
||||||
text.includes("charlie"),
|
text.includes("charlie"),
|
||||||
@@ -228,11 +234,9 @@ test("@ trigger prioritizes channel members before runnable personas and other a
|
|||||||
text.includes("outsider"),
|
text.includes("outsider"),
|
||||||
);
|
);
|
||||||
expect(fizzIndex).toBeGreaterThanOrEqual(0);
|
expect(fizzIndex).toBeGreaterThanOrEqual(0);
|
||||||
expect(aliceIndex).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(bobIndex).toBeGreaterThanOrEqual(0);
|
expect(bobIndex).toBeGreaterThanOrEqual(0);
|
||||||
expect(charlieIndex).toBeGreaterThanOrEqual(0);
|
expect(charlieIndex).toBeGreaterThanOrEqual(0);
|
||||||
expect(outsiderIndex).toEqual(-1);
|
expect(outsiderIndex).toEqual(-1);
|
||||||
expect(aliceIndex).toBeLessThan(fizzIndex);
|
|
||||||
expect(bobIndex).toBeLessThan(fizzIndex);
|
expect(bobIndex).toBeLessThan(fizzIndex);
|
||||||
expect(fizzIndex).toBeLessThan(charlieIndex);
|
expect(fizzIndex).toBeLessThan(charlieIndex);
|
||||||
});
|
});
|
||||||
@@ -425,7 +429,18 @@ test("defers agent mentions until DM members finish loading", async ({
|
|||||||
await expect(threadPanel).toContainText("before members resolve");
|
await expect(threadPanel).toContainText("before members resolve");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("autocomplete filters suggestions as user types", async ({ page }) => {
|
test("autocomplete filters managed-agent suggestions as user types", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: TEST_IDENTITIES.alice.pubkey,
|
||||||
|
name: "alice",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("channel-general").click();
|
await page.getByTestId("channel-general").click();
|
||||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||||
@@ -462,7 +477,7 @@ test("autocomplete searches global non-member people from the first typed charac
|
|||||||
await expect(tessaRow.getByText("not in channel")).toBeVisible();
|
await expect(tessaRow.getByText("not in channel")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mention autocomplete pages global people search beyond the first 50 results", async ({
|
test("mention autocomplete caps global people search at 50 results", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const searchProfiles = Array.from({ length: 55 }, (_, index) => ({
|
const searchProfiles = Array.from({ length: 55 }, (_, index) => ({
|
||||||
@@ -479,10 +494,8 @@ test("mention autocomplete pages global people search beyond the first 50 result
|
|||||||
|
|
||||||
const dropdown = autocomplete(page);
|
const dropdown = autocomplete(page);
|
||||||
await expect(dropdown.locator("button")).toHaveCount(50);
|
await expect(dropdown.locator("button")).toHaveCount(50);
|
||||||
await dropdown.evaluate((node) => node.scrollTo(0, node.scrollHeight));
|
await expect(dropdown.getByText("Alex 50")).toBeVisible();
|
||||||
|
await expect(dropdown.getByText("Alex 55")).toHaveCount(0);
|
||||||
await expect(dropdown.locator("button")).toHaveCount(55);
|
|
||||||
await expect(dropdown.getByText("Alex 55")).toBeVisible();
|
|
||||||
await expect(dropdown.getByText("not in channel").last()).toBeVisible();
|
await expect(dropdown.getByText("not in channel").last()).toBeVisible();
|
||||||
|
|
||||||
const searchCalls = (await readCommandPayloadLog(page)).filter(
|
const searchCalls = (await readCommandPayloadLog(page)).filter(
|
||||||
@@ -493,6 +506,10 @@ test("mention autocomplete pages global people search beyond the first 50 result
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
payload: expect.objectContaining({ cursor: null, limit: 50 }),
|
payload: expect.objectContaining({ cursor: null, limit: 50 }),
|
||||||
}),
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(searchCalls).not.toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
payload: expect.objectContaining({ cursor: "2", limit: 50 }),
|
payload: expect.objectContaining({ cursor: "2", limit: 50 }),
|
||||||
}),
|
}),
|
||||||
@@ -521,9 +538,18 @@ test("selecting a person mention inserts @Name into input", async ({
|
|||||||
await expect(mentionChip).not.toHaveClass(/agent-mention-highlight/);
|
await expect(mentionChip).not.toHaveClass(/agent-mention-highlight/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("selecting an agent mention inserts @Name into input", async ({
|
test("selecting a managed agent mention inserts @Name into input", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: TEST_IDENTITIES.alice.pubkey,
|
||||||
|
name: "alice",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("channel-general").click();
|
await page.getByTestId("channel-general").click();
|
||||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||||
@@ -691,9 +717,18 @@ test("selecting a persona mention reuses an existing persona agent", async ({
|
|||||||
await expect(mentionChip).toHaveText("Fizz");
|
await expect(mentionChip).toHaveText("Fizz");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("relay-profile agents with member roles use the agent composer style", async ({
|
test("managed relay-profile agents with member roles use the agent composer style", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||||||
|
name: "charlie",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
await openChannelBrowser(page);
|
await openChannelBrowser(page);
|
||||||
@@ -759,10 +794,17 @@ test("own profile-only agents are hidden from channel mentions", async ({
|
|||||||
await expect(autocomplete(page)).toHaveCount(0);
|
await expect(autocomplete(page)).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("allowlisted relay agents are visible in channel mentions", async ({
|
test("managed relay agents are visible in channel mentions regardless of relay policy", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await installMockBridge(page, {
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
|
||||||
|
name: "quinn",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
relayAgents: [
|
relayAgents: [
|
||||||
{
|
{
|
||||||
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
|
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
|
||||||
@@ -784,7 +826,7 @@ test("allowlisted relay agents are visible in channel mentions", async ({
|
|||||||
await expect(dropdown.getByText("agent")).toBeVisible();
|
await expect(dropdown.getByText("agent")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("non-allowlisted relay agents stay hidden from channel mentions", async ({
|
test("relay-only agents stay hidden from channel mentions even when allowlisted", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await installMockBridge(page, {
|
await installMockBridge(page, {
|
||||||
@@ -793,7 +835,7 @@ test("non-allowlisted relay agents stay hidden from channel mentions", async ({
|
|||||||
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
|
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
|
||||||
name: "quinn",
|
name: "quinn",
|
||||||
respondTo: "allowlist",
|
respondTo: "allowlist",
|
||||||
respondToAllowlist: [TEST_IDENTITIES.outsider.pubkey],
|
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1364,9 +1406,18 @@ test("system member-joined rows render the joined person as a plain profile name
|
|||||||
await expect(joinedPersonName).not.toHaveAttribute("data-mention");
|
await expect(joinedPersonName).not.toHaveAttribute("data-mention");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("selecting a non-member agent from a DM inserts @Name into input", async ({
|
test("selecting a managed non-member agent from a DM inserts @Name into input", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||||||
|
name: "charlie",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("channel-bob-tyler").click();
|
await page.getByTestId("channel-bob-tyler").click();
|
||||||
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
|
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
|
||||||
@@ -1450,9 +1501,18 @@ test("sent non-member person mention uses the normal mention style", async ({
|
|||||||
await expect(mentionChip.locator("svg")).toHaveCount(0);
|
await expect(mentionChip.locator("svg")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sent non-member agent mention uses the agent mention style", async ({
|
test("sent managed non-member agent mention uses the agent mention style", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||||||
|
name: "charlie",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("channel-bob-tyler").click();
|
await page.getByTestId("channel-bob-tyler").click();
|
||||||
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
|
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
|
||||||
|
|||||||
@@ -143,7 +143,16 @@ test("focus and split preserve reading context and interaction ownership", async
|
|||||||
await page.addInitScript(() => {
|
await page.addInitScript(() => {
|
||||||
localStorage.setItem("buzz.channels.threadViewMode", "focus");
|
localStorage.setItem("buzz.channels.threadViewMode", "focus");
|
||||||
});
|
});
|
||||||
await installMockBridge(page);
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey:
|
||||||
|
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f",
|
||||||
|
name: "alice",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
const rootId = await seedLongThread(page);
|
const rootId = await seedLongThread(page);
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,16 @@ test.describe("list virtualization", () => {
|
|||||||
test("03 — members search shows both sticky titles under content-visibility", async ({
|
test("03 — members search shows both sticky titles under content-visibility", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await installMockBridge(page);
|
await installMockBridge(page, {
|
||||||
|
managedAgents: [
|
||||||
|
{
|
||||||
|
pubkey:
|
||||||
|
"554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea",
|
||||||
|
name: "charlie",
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("channel-general").click();
|
await page.getByTestId("channel-general").click();
|
||||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||||
|
|||||||
Reference in New Issue
Block a user