Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions desktop/src/features/channels/ui/useChannelAgentSessions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import assert from "node:assert/strict";
import test from "node:test";

import { getChannelAgentSessionAgents } from "./useChannelAgentSessions.ts";

const OWNER = "a".repeat(64);
const AGENT = "b".repeat(64);
const OTHER_AGENT = "c".repeat(64);
const ROOM_ID = "11111111-1111-4111-8111-111111111111";
const DM_ID = "22222222-2222-4222-8222-222222222222";

function makeChannel(overrides = {}) {
return {
id: ROOM_ID,
name: "ops",
channelType: "stream",
visibility: "private",
description: "",
topic: null,
purpose: null,
memberCount: 2,
memberPubkeys: [OWNER, AGENT],
lastMessageAt: null,
archivedAt: null,
participants: [],
participantPubkeys: [],
isMember: true,
ttlSeconds: null,
ttlDeadline: null,
...overrides,
};
}

function makeDm(overrides = {}) {
return makeChannel({
id: DM_ID,
name: "",
channelType: "dm",
participants: ["Owner", "Agent"],
participantPubkeys: [OWNER, AGENT],
...overrides,
});
}

function member(pubkey, role) {
return {
pubkey,
role,
isAgent: role === "bot",
joinedAt: "2026-08-01T00:00:00Z",
displayName: null,
};
}

function relayAgent(overrides = {}) {
return {
pubkey: AGENT,
name: "ops-agent",
status: "deployed",
agentSource: "relay",
canInterruptTurn: false,
channelIds: [ROOM_ID],
channels: [],
...overrides,
};
}

test("relay agent stays scoped to its declared bot-role channel in rooms", () => {
const agents = getChannelAgentSessionAgents({
activeChannel: makeChannel(),
activeChannelId: ROOM_ID,
agents: [relayAgent()],
channelMembers: [member(OWNER, "owner"), member(AGENT, "bot")],
});

assert.deepEqual(
agents.map((agent) => agent.pubkey),
[AGENT],
);
});

test("relay agent with declared scope is excluded from undeclared rooms", () => {
const otherRoom = makeChannel({
id: "33333333-3333-4333-8333-333333333333",
name: "elsewhere",
});
const agents = getChannelAgentSessionAgents({
activeChannel: otherRoom,
activeChannelId: otherRoom.id,
agents: [relayAgent()],
channelMembers: [member(OWNER, "owner"), member(AGENT, "member")],
});

assert.deepEqual(agents, []);
});

// DM rosters cannot carry the bot role — the relay hardcodes DM participants
// to `member` and no DM member is elevated enough to change a role — so an
// agent's declared bot-role channel scope must not exclude it from a DM it
// participates in. Without this, the agent's live observer activity hides
// behind the human typing row in every DM.
test("relay agent with declared scope is included in a DM it participates in", () => {
const agents = getChannelAgentSessionAgents({
activeChannel: makeDm(),
activeChannelId: DM_ID,
agents: [relayAgent()],
channelMembers: [member(OWNER, "member"), member(AGENT, "member")],
});

assert.deepEqual(
agents.map((agent) => agent.pubkey),
[AGENT],
);
});

test("relay agent outside the DM roster stays excluded", () => {
const agents = getChannelAgentSessionAgents({
activeChannel: makeDm(),
activeChannelId: DM_ID,
agents: [relayAgent({ pubkey: OTHER_AGENT, name: "other-agent" })],
channelMembers: [member(OWNER, "member"), member(AGENT, "member")],
});

assert.deepEqual(agents, []);
});

test("DM inclusion falls back to participant pubkeys before members load", () => {
const agents = getChannelAgentSessionAgents({
activeChannel: makeDm(),
activeChannelId: DM_ID,
agents: [
relayAgent(),
relayAgent({ pubkey: OTHER_AGENT, name: "other-agent" }),
],
channelMembers: undefined,
});

assert.deepEqual(
agents.map((agent) => agent.pubkey),
[AGENT],
);
});

test("loaded DM members are authoritative over stale participant pubkeys", () => {
const agents = getChannelAgentSessionAgents({
activeChannel: makeDm({ participantPubkeys: [OWNER, OTHER_AGENT] }),
activeChannelId: DM_ID,
agents: [relayAgent()],
channelMembers: [member(OWNER, "member"), member(AGENT, "member")],
});

assert.deepEqual(
agents.map((agent) => agent.pubkey),
[AGENT],
);
});
18 changes: 18 additions & 0 deletions desktop/src/features/channels/ui/useChannelAgentSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ export function getChannelAgentSessionAgents({
return true;
}

// A DM roster can never satisfy the declared-channel check: relay agents
// declare their bot-role channels, and DM participants always hold the
// plain member role (the relay hardcodes it at DM creation and no DM
// member is elevated enough to change it). Falling through to the
// no-declared-scope membership test below would therefore exclude a relay
// agent from every DM it participates in, hiding its live activity behind
// the human typing row. Membership itself is the authoritative signal for
// DMs, with the channel's participant roster covering the window before
// the members query resolves.
if (activeChannel.channelType === "dm") {
if (memberPubkeys) {
return memberPubkeys.has(normalizedPubkey);
}
return activeChannel.participantPubkeys.some(
(pubkey) => normalizePubkey(pubkey) === normalizedPubkey,
);
}

return (
!hasDeclaredChannelScope && Boolean(memberPubkeys?.has(normalizedPubkey))
);
Expand Down
Loading