@agent-relay/dashboard 2.0.80 → 2.0.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useDirectMessage hook - DM filtering and deduplication logic
|
|
3
|
+
*
|
|
4
|
+
* TDD approach: Write failing tests first, then fix the implementation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import type { Agent, Message } from '../../types';
|
|
9
|
+
|
|
10
|
+
// Replicate the filtering and deduplication logic from useDirectMessage hook
|
|
11
|
+
// to test in isolation without React
|
|
12
|
+
|
|
13
|
+
interface DirectMessageTestContext {
|
|
14
|
+
currentHuman: Agent | null;
|
|
15
|
+
currentUserName: string | null;
|
|
16
|
+
messages: Message[];
|
|
17
|
+
agents: Agent[];
|
|
18
|
+
selectedDmAgents: string[];
|
|
19
|
+
removedDmAgents: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function computeDmParticipantAgents(ctx: DirectMessageTestContext): string[] {
|
|
23
|
+
const { currentHuman, messages, agents, selectedDmAgents, removedDmAgents } = ctx;
|
|
24
|
+
if (!currentHuman) return [];
|
|
25
|
+
|
|
26
|
+
const agentNameSet = new Set(agents.map((a) => a.name));
|
|
27
|
+
const humanName = currentHuman.name;
|
|
28
|
+
const derived = new Set<string>();
|
|
29
|
+
|
|
30
|
+
for (const msg of messages) {
|
|
31
|
+
const { from, to } = msg;
|
|
32
|
+
if (!from || !to) continue;
|
|
33
|
+
if (from === humanName && agentNameSet.has(to)) derived.add(to);
|
|
34
|
+
if (to === humanName && agentNameSet.has(from)) derived.add(from);
|
|
35
|
+
if (selectedDmAgents.includes(from) && agentNameSet.has(to)) derived.add(to);
|
|
36
|
+
if (selectedDmAgents.includes(to) && agentNameSet.has(from)) derived.add(from);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const participants = new Set<string>([...selectedDmAgents, ...derived]);
|
|
40
|
+
removedDmAgents.forEach((a) => participants.delete(a));
|
|
41
|
+
return Array.from(participants);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function filterVisibleMessages(ctx: DirectMessageTestContext): Message[] {
|
|
45
|
+
const { currentHuman, currentUserName, messages } = ctx;
|
|
46
|
+
if (!currentHuman) return messages;
|
|
47
|
+
|
|
48
|
+
const dmParticipantAgents = computeDmParticipantAgents(ctx);
|
|
49
|
+
const participants = new Set<string>([currentHuman.name, ...dmParticipantAgents]);
|
|
50
|
+
// Add current user to participants - use "Dashboard" as fallback for local mode
|
|
51
|
+
const effectiveUserName = currentUserName || 'Dashboard';
|
|
52
|
+
participants.add(effectiveUserName);
|
|
53
|
+
|
|
54
|
+
return messages.filter((msg) => {
|
|
55
|
+
if (!msg.from || !msg.to) return false;
|
|
56
|
+
return participants.has(msg.from) && participants.has(msg.to);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Helper to create test messages
|
|
61
|
+
function createMessage(from: string, to: string, content: string, id?: string): Message {
|
|
62
|
+
return {
|
|
63
|
+
id: id || `msg-${Math.random().toString(36).slice(2)}`,
|
|
64
|
+
from,
|
|
65
|
+
to,
|
|
66
|
+
content,
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Helper to create test agents
|
|
72
|
+
function createAgent(name: string, isHuman = false): Agent {
|
|
73
|
+
return {
|
|
74
|
+
name,
|
|
75
|
+
status: 'online',
|
|
76
|
+
isHuman,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('useDirectMessage', () => {
|
|
81
|
+
describe('basic DM filtering', () => {
|
|
82
|
+
it('should show messages between current user and human in 1:1 DM', () => {
|
|
83
|
+
const ctx: DirectMessageTestContext = {
|
|
84
|
+
currentHuman: createAgent('alice', true),
|
|
85
|
+
currentUserName: 'bob',
|
|
86
|
+
messages: [
|
|
87
|
+
createMessage('bob', 'alice', 'Hi Alice!'),
|
|
88
|
+
createMessage('alice', 'bob', 'Hi Bob!'),
|
|
89
|
+
],
|
|
90
|
+
agents: [],
|
|
91
|
+
selectedDmAgents: [],
|
|
92
|
+
removedDmAgents: [],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const visible = filterVisibleMessages(ctx);
|
|
96
|
+
expect(visible).toHaveLength(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should filter out messages not involving DM participants', () => {
|
|
100
|
+
const ctx: DirectMessageTestContext = {
|
|
101
|
+
currentHuman: createAgent('alice', true),
|
|
102
|
+
currentUserName: 'bob',
|
|
103
|
+
messages: [
|
|
104
|
+
createMessage('bob', 'alice', 'Hi Alice!'),
|
|
105
|
+
createMessage('charlie', 'dave', 'Unrelated message'),
|
|
106
|
+
],
|
|
107
|
+
agents: [],
|
|
108
|
+
selectedDmAgents: [],
|
|
109
|
+
removedDmAgents: [],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const visible = filterVisibleMessages(ctx);
|
|
113
|
+
expect(visible).toHaveLength(1);
|
|
114
|
+
expect(visible[0].content).toBe('Hi Alice!');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('group DM with agents', () => {
|
|
119
|
+
it('should show messages when agent is invited to DM', () => {
|
|
120
|
+
const ctx: DirectMessageTestContext = {
|
|
121
|
+
currentHuman: createAgent('alice', true),
|
|
122
|
+
currentUserName: 'bob',
|
|
123
|
+
messages: [
|
|
124
|
+
createMessage('bob', 'alice', 'Hi Alice!'),
|
|
125
|
+
createMessage('bob', 'Agent1', 'Hi Agent1!'),
|
|
126
|
+
],
|
|
127
|
+
agents: [createAgent('Agent1')],
|
|
128
|
+
selectedDmAgents: ['Agent1'],
|
|
129
|
+
removedDmAgents: [],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const visible = filterVisibleMessages(ctx);
|
|
133
|
+
// Both messages should be visible since Agent1 is invited
|
|
134
|
+
expect(visible).toHaveLength(2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* THE BUG: Agent response should appear in group DM
|
|
139
|
+
*
|
|
140
|
+
* Scenario:
|
|
141
|
+
* - Bob is viewing a DM with Alice
|
|
142
|
+
* - Bob invites Agent1 to the conversation
|
|
143
|
+
* - Bob sends a message (goes to both Alice and Agent1)
|
|
144
|
+
* - Agent1 responds TO BOB
|
|
145
|
+
*
|
|
146
|
+
* Expected: Agent1's response should appear in the group DM view
|
|
147
|
+
* Actual: Agent1's response may be filtered out or appear in wrong place
|
|
148
|
+
*/
|
|
149
|
+
it('should show agent response in group DM when agent responds to sender', () => {
|
|
150
|
+
const ctx: DirectMessageTestContext = {
|
|
151
|
+
currentHuman: createAgent('alice', true),
|
|
152
|
+
currentUserName: 'bob',
|
|
153
|
+
messages: [
|
|
154
|
+
createMessage('bob', 'alice', 'Hi Alice!'),
|
|
155
|
+
createMessage('bob', 'Agent1', 'Agent1, help us please'),
|
|
156
|
+
createMessage('Agent1', 'bob', 'Sure, I can help!'), // Agent responds to Bob
|
|
157
|
+
],
|
|
158
|
+
agents: [createAgent('Agent1')],
|
|
159
|
+
selectedDmAgents: ['Agent1'],
|
|
160
|
+
removedDmAgents: [],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const visible = filterVisibleMessages(ctx);
|
|
164
|
+
|
|
165
|
+
// All three messages should be visible:
|
|
166
|
+
// 1. bob -> alice
|
|
167
|
+
// 2. bob -> Agent1
|
|
168
|
+
// 3. Agent1 -> bob (THIS IS THE KEY TEST)
|
|
169
|
+
expect(visible).toHaveLength(3);
|
|
170
|
+
expect(visible.map(m => m.content)).toContain('Sure, I can help!');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should show agent response when agent responds to human (not current user)', () => {
|
|
174
|
+
const ctx: DirectMessageTestContext = {
|
|
175
|
+
currentHuman: createAgent('alice', true),
|
|
176
|
+
currentUserName: 'bob',
|
|
177
|
+
messages: [
|
|
178
|
+
createMessage('bob', 'alice', 'Hi Alice!'),
|
|
179
|
+
createMessage('bob', 'Agent1', 'Agent1, help Alice'),
|
|
180
|
+
createMessage('Agent1', 'alice', 'Hi Alice, I am here to help'), // Agent responds to Alice
|
|
181
|
+
],
|
|
182
|
+
agents: [createAgent('Agent1')],
|
|
183
|
+
selectedDmAgents: ['Agent1'],
|
|
184
|
+
removedDmAgents: [],
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const visible = filterVisibleMessages(ctx);
|
|
188
|
+
|
|
189
|
+
// All three messages should be visible
|
|
190
|
+
expect(visible).toHaveLength(3);
|
|
191
|
+
expect(visible.map(m => m.content)).toContain('Hi Alice, I am here to help');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle multiple agents in group DM', () => {
|
|
195
|
+
const ctx: DirectMessageTestContext = {
|
|
196
|
+
currentHuman: createAgent('alice', true),
|
|
197
|
+
currentUserName: 'bob',
|
|
198
|
+
messages: [
|
|
199
|
+
createMessage('bob', 'alice', 'Group meeting'),
|
|
200
|
+
createMessage('bob', 'Agent1', 'Agent1 invited'),
|
|
201
|
+
createMessage('bob', 'Agent2', 'Agent2 invited'),
|
|
202
|
+
createMessage('Agent1', 'bob', 'Agent1 here'),
|
|
203
|
+
createMessage('Agent2', 'bob', 'Agent2 here'),
|
|
204
|
+
createMessage('Agent1', 'Agent2', 'Agent-to-agent chat'), // Agents talking to each other
|
|
205
|
+
],
|
|
206
|
+
agents: [createAgent('Agent1'), createAgent('Agent2')],
|
|
207
|
+
selectedDmAgents: ['Agent1', 'Agent2'],
|
|
208
|
+
removedDmAgents: [],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const visible = filterVisibleMessages(ctx);
|
|
212
|
+
|
|
213
|
+
// All 6 messages should be visible since all participants are in the group
|
|
214
|
+
expect(visible).toHaveLength(6);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should NOT show agent messages if agent is removed from DM', () => {
|
|
218
|
+
const ctx: DirectMessageTestContext = {
|
|
219
|
+
currentHuman: createAgent('alice', true),
|
|
220
|
+
currentUserName: 'bob',
|
|
221
|
+
messages: [
|
|
222
|
+
createMessage('bob', 'alice', 'Hi Alice!'),
|
|
223
|
+
createMessage('Agent1', 'bob', 'Message from removed agent'),
|
|
224
|
+
],
|
|
225
|
+
agents: [createAgent('Agent1')],
|
|
226
|
+
selectedDmAgents: [],
|
|
227
|
+
removedDmAgents: ['Agent1'], // Agent was removed
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const visible = filterVisibleMessages(ctx);
|
|
231
|
+
|
|
232
|
+
// Only bob->alice should be visible, agent message filtered out
|
|
233
|
+
expect(visible).toHaveLength(1);
|
|
234
|
+
expect(visible[0].content).toBe('Hi Alice!');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('participant derivation from message history', () => {
|
|
239
|
+
it('should derive agent as participant if human messaged agent', () => {
|
|
240
|
+
const ctx: DirectMessageTestContext = {
|
|
241
|
+
currentHuman: createAgent('alice', true),
|
|
242
|
+
currentUserName: 'bob',
|
|
243
|
+
messages: [
|
|
244
|
+
createMessage('alice', 'Agent1', 'Alice messaged agent directly'),
|
|
245
|
+
],
|
|
246
|
+
agents: [createAgent('Agent1')],
|
|
247
|
+
selectedDmAgents: [], // Not explicitly selected
|
|
248
|
+
removedDmAgents: [],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
252
|
+
|
|
253
|
+
// Agent1 should be derived as participant because alice messaged it
|
|
254
|
+
expect(participants).toContain('Agent1');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should derive agent as participant if agent messaged human', () => {
|
|
258
|
+
const ctx: DirectMessageTestContext = {
|
|
259
|
+
currentHuman: createAgent('alice', true),
|
|
260
|
+
currentUserName: 'bob',
|
|
261
|
+
messages: [
|
|
262
|
+
createMessage('Agent1', 'alice', 'Agent proactively messaged Alice'),
|
|
263
|
+
],
|
|
264
|
+
agents: [createAgent('Agent1')],
|
|
265
|
+
selectedDmAgents: [],
|
|
266
|
+
removedDmAgents: [],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
270
|
+
|
|
271
|
+
expect(participants).toContain('Agent1');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should NOT derive agent if it was removed', () => {
|
|
275
|
+
const ctx: DirectMessageTestContext = {
|
|
276
|
+
currentHuman: createAgent('alice', true),
|
|
277
|
+
currentUserName: 'bob',
|
|
278
|
+
messages: [
|
|
279
|
+
createMessage('alice', 'Agent1', 'Before removal'),
|
|
280
|
+
],
|
|
281
|
+
agents: [createAgent('Agent1')],
|
|
282
|
+
selectedDmAgents: [],
|
|
283
|
+
removedDmAgents: ['Agent1'], // Explicitly removed
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
287
|
+
|
|
288
|
+
expect(participants).not.toContain('Agent1');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('edge cases - currentUserName scenarios', () => {
|
|
293
|
+
/**
|
|
294
|
+
* CRITICAL BUG TEST: When currentUserName is null (local mode),
|
|
295
|
+
* the current user is not added to participants, causing agent
|
|
296
|
+
* responses to the current user to be filtered out!
|
|
297
|
+
*/
|
|
298
|
+
it('BUG: should show agent response even when currentUserName is null', () => {
|
|
299
|
+
const ctx: DirectMessageTestContext = {
|
|
300
|
+
currentHuman: createAgent('alice', true),
|
|
301
|
+
currentUserName: null, // Local mode - no cloud auth
|
|
302
|
+
messages: [
|
|
303
|
+
// Dashboard sends as "Dashboard" in local mode
|
|
304
|
+
createMessage('Dashboard', 'alice', 'Hi Alice!'),
|
|
305
|
+
createMessage('Dashboard', 'Agent1', 'Agent1, help us'),
|
|
306
|
+
createMessage('Agent1', 'Dashboard', 'Sure, I can help!'), // Agent responds to Dashboard
|
|
307
|
+
],
|
|
308
|
+
agents: [createAgent('Agent1')],
|
|
309
|
+
selectedDmAgents: ['Agent1'],
|
|
310
|
+
removedDmAgents: [],
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const visible = filterVisibleMessages(ctx);
|
|
314
|
+
|
|
315
|
+
// BUG: This will likely fail because "Dashboard" is not in participants
|
|
316
|
+
// when currentUserName is null
|
|
317
|
+
expect(visible).toHaveLength(3);
|
|
318
|
+
expect(visible.map(m => m.content)).toContain('Sure, I can help!');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should handle "Dashboard" as sender when no currentUserName', () => {
|
|
322
|
+
const ctx: DirectMessageTestContext = {
|
|
323
|
+
currentHuman: createAgent('alice', true),
|
|
324
|
+
currentUserName: null,
|
|
325
|
+
messages: [
|
|
326
|
+
createMessage('Dashboard', 'alice', 'Message from local mode'),
|
|
327
|
+
],
|
|
328
|
+
agents: [],
|
|
329
|
+
selectedDmAgents: [],
|
|
330
|
+
removedDmAgents: [],
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const visible = filterVisibleMessages(ctx);
|
|
334
|
+
|
|
335
|
+
// In local mode, Dashboard should still be able to send to humans
|
|
336
|
+
// But the filter requires BOTH from and to to be in participants
|
|
337
|
+
// participants = {alice} only when currentUserName is null
|
|
338
|
+
// So "Dashboard" is not in participants!
|
|
339
|
+
expect(visible).toHaveLength(1); // This might fail!
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('edge cases', () => {
|
|
344
|
+
it('should handle empty messages array', () => {
|
|
345
|
+
const ctx: DirectMessageTestContext = {
|
|
346
|
+
currentHuman: createAgent('alice', true),
|
|
347
|
+
currentUserName: 'bob',
|
|
348
|
+
messages: [],
|
|
349
|
+
agents: [createAgent('Agent1')],
|
|
350
|
+
selectedDmAgents: ['Agent1'],
|
|
351
|
+
removedDmAgents: [],
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const visible = filterVisibleMessages(ctx);
|
|
355
|
+
expect(visible).toHaveLength(0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should handle null currentHuman', () => {
|
|
359
|
+
const ctx: DirectMessageTestContext = {
|
|
360
|
+
currentHuman: null,
|
|
361
|
+
currentUserName: 'bob',
|
|
362
|
+
messages: [createMessage('bob', 'alice', 'Test')],
|
|
363
|
+
agents: [],
|
|
364
|
+
selectedDmAgents: [],
|
|
365
|
+
removedDmAgents: [],
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const visible = filterVisibleMessages(ctx);
|
|
369
|
+
// Should return all messages when not in DM mode
|
|
370
|
+
expect(visible).toHaveLength(1);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should handle messages with missing from/to', () => {
|
|
374
|
+
const ctx: DirectMessageTestContext = {
|
|
375
|
+
currentHuman: createAgent('alice', true),
|
|
376
|
+
currentUserName: 'bob',
|
|
377
|
+
messages: [
|
|
378
|
+
{ id: '1', content: 'No from', to: 'alice', timestamp: new Date().toISOString() } as Message,
|
|
379
|
+
{ id: '2', content: 'No to', from: 'bob', timestamp: new Date().toISOString() } as Message,
|
|
380
|
+
createMessage('bob', 'alice', 'Valid message'),
|
|
381
|
+
],
|
|
382
|
+
agents: [],
|
|
383
|
+
selectedDmAgents: [],
|
|
384
|
+
removedDmAgents: [],
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const visible = filterVisibleMessages(ctx);
|
|
388
|
+
// Only valid message should be included
|
|
389
|
+
expect(visible).toHaveLength(1);
|
|
390
|
+
expect(visible[0].content).toBe('Valid message');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should be case-sensitive for participant matching', () => {
|
|
394
|
+
const ctx: DirectMessageTestContext = {
|
|
395
|
+
currentHuman: createAgent('Alice', true), // Capital A
|
|
396
|
+
currentUserName: 'Bob', // Capital B
|
|
397
|
+
messages: [
|
|
398
|
+
createMessage('Bob', 'Alice', 'Correct case'),
|
|
399
|
+
createMessage('bob', 'alice', 'Wrong case'), // lowercase - should NOT match
|
|
400
|
+
],
|
|
401
|
+
agents: [],
|
|
402
|
+
selectedDmAgents: [],
|
|
403
|
+
removedDmAgents: [],
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const visible = filterVisibleMessages(ctx);
|
|
407
|
+
// Only correctly-cased message should appear
|
|
408
|
+
// (Note: This may need to be case-insensitive depending on requirements)
|
|
409
|
+
expect(visible).toHaveLength(1);
|
|
410
|
+
expect(visible[0].content).toBe('Correct case');
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe('advanced agent derivation', () => {
|
|
415
|
+
it('should derive second agent when selected agent messages another agent', () => {
|
|
416
|
+
const ctx: DirectMessageTestContext = {
|
|
417
|
+
currentHuman: createAgent('alice', true),
|
|
418
|
+
currentUserName: 'bob',
|
|
419
|
+
messages: [
|
|
420
|
+
createMessage('Agent1', 'Agent2', 'Agent1 talking to Agent2'),
|
|
421
|
+
],
|
|
422
|
+
agents: [createAgent('Agent1'), createAgent('Agent2')],
|
|
423
|
+
selectedDmAgents: ['Agent1'], // Only Agent1 explicitly selected
|
|
424
|
+
removedDmAgents: [],
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
428
|
+
|
|
429
|
+
// Agent2 should be derived because Agent1 (selected) messaged it
|
|
430
|
+
expect(participants).toContain('Agent1');
|
|
431
|
+
expect(participants).toContain('Agent2');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should derive agent chain: agent1 → agent2 → agent3', () => {
|
|
435
|
+
const ctx: DirectMessageTestContext = {
|
|
436
|
+
currentHuman: createAgent('alice', true),
|
|
437
|
+
currentUserName: 'bob',
|
|
438
|
+
messages: [
|
|
439
|
+
createMessage('alice', 'Agent1', 'Alice starts with Agent1'),
|
|
440
|
+
createMessage('Agent1', 'Agent2', 'Agent1 brings in Agent2'),
|
|
441
|
+
// Note: Agent2 -> Agent3 won't derive Agent3 because Agent2 wasn't in selectedDmAgents
|
|
442
|
+
// This tests the current behavior - derivation only goes one level deep from selected agents
|
|
443
|
+
],
|
|
444
|
+
agents: [createAgent('Agent1'), createAgent('Agent2'), createAgent('Agent3')],
|
|
445
|
+
selectedDmAgents: [], // No explicit selection, relying on derivation
|
|
446
|
+
removedDmAgents: [],
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
450
|
+
|
|
451
|
+
// Agent1 derived from alice -> Agent1
|
|
452
|
+
expect(participants).toContain('Agent1');
|
|
453
|
+
// Agent2 NOT derived because Agent1 wasn't in selectedDmAgents
|
|
454
|
+
// (derivation requires explicit selection or human involvement)
|
|
455
|
+
expect(participants).not.toContain('Agent2');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should derive agents from both human and selected agent interactions', () => {
|
|
459
|
+
const ctx: DirectMessageTestContext = {
|
|
460
|
+
currentHuman: createAgent('alice', true),
|
|
461
|
+
currentUserName: 'bob',
|
|
462
|
+
messages: [
|
|
463
|
+
createMessage('alice', 'Agent1', 'Alice to Agent1'),
|
|
464
|
+
createMessage('Agent1', 'Agent2', 'Agent1 to Agent2'),
|
|
465
|
+
],
|
|
466
|
+
agents: [createAgent('Agent1'), createAgent('Agent2')],
|
|
467
|
+
selectedDmAgents: ['Agent1'], // Agent1 explicitly selected
|
|
468
|
+
removedDmAgents: [],
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
472
|
+
|
|
473
|
+
// Both should be derived
|
|
474
|
+
expect(participants).toContain('Agent1'); // From alice -> Agent1 AND selectedDmAgents
|
|
475
|
+
expect(participants).toContain('Agent2'); // From Agent1 (selected) -> Agent2
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe('complex removal scenarios', () => {
|
|
480
|
+
it('should handle removing one agent from multiple agents', () => {
|
|
481
|
+
const ctx: DirectMessageTestContext = {
|
|
482
|
+
currentHuman: createAgent('alice', true),
|
|
483
|
+
currentUserName: 'bob',
|
|
484
|
+
messages: [
|
|
485
|
+
createMessage('bob', 'Agent1', 'To Agent1'),
|
|
486
|
+
createMessage('bob', 'Agent2', 'To Agent2'),
|
|
487
|
+
createMessage('bob', 'Agent3', 'To Agent3'),
|
|
488
|
+
],
|
|
489
|
+
agents: [createAgent('Agent1'), createAgent('Agent2'), createAgent('Agent3')],
|
|
490
|
+
selectedDmAgents: ['Agent1', 'Agent2', 'Agent3'],
|
|
491
|
+
removedDmAgents: ['Agent2'], // Only Agent2 removed
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
495
|
+
|
|
496
|
+
expect(participants).toContain('Agent1');
|
|
497
|
+
expect(participants).not.toContain('Agent2');
|
|
498
|
+
expect(participants).toContain('Agent3');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should filter messages involving removed agent', () => {
|
|
502
|
+
const ctx: DirectMessageTestContext = {
|
|
503
|
+
currentHuman: createAgent('alice', true),
|
|
504
|
+
currentUserName: 'bob',
|
|
505
|
+
messages: [
|
|
506
|
+
createMessage('bob', 'alice', 'To Alice'),
|
|
507
|
+
createMessage('bob', 'Agent1', 'To Agent1'),
|
|
508
|
+
createMessage('Agent1', 'bob', 'From Agent1'),
|
|
509
|
+
createMessage('bob', 'Agent2', 'To removed Agent2'),
|
|
510
|
+
createMessage('Agent2', 'bob', 'From removed Agent2'),
|
|
511
|
+
],
|
|
512
|
+
agents: [createAgent('Agent1'), createAgent('Agent2')],
|
|
513
|
+
selectedDmAgents: ['Agent1', 'Agent2'],
|
|
514
|
+
removedDmAgents: ['Agent2'],
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const visible = filterVisibleMessages(ctx);
|
|
518
|
+
|
|
519
|
+
// Should see: bob->alice, bob->Agent1, Agent1->bob
|
|
520
|
+
// Should NOT see: bob->Agent2, Agent2->bob
|
|
521
|
+
expect(visible).toHaveLength(3);
|
|
522
|
+
expect(visible.map(m => m.content)).not.toContain('To removed Agent2');
|
|
523
|
+
expect(visible.map(m => m.content)).not.toContain('From removed Agent2');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should handle re-adding agent after removal via selection', () => {
|
|
527
|
+
// Scenario: Agent was removed but is now back in selectedDmAgents
|
|
528
|
+
const ctx: DirectMessageTestContext = {
|
|
529
|
+
currentHuman: createAgent('alice', true),
|
|
530
|
+
currentUserName: 'bob',
|
|
531
|
+
messages: [
|
|
532
|
+
createMessage('Agent1', 'bob', 'Agent1 message'),
|
|
533
|
+
],
|
|
534
|
+
agents: [createAgent('Agent1')],
|
|
535
|
+
selectedDmAgents: ['Agent1'], // Re-added via selection
|
|
536
|
+
removedDmAgents: [], // No longer in removed list
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
540
|
+
const visible = filterVisibleMessages(ctx);
|
|
541
|
+
|
|
542
|
+
expect(participants).toContain('Agent1');
|
|
543
|
+
expect(visible).toHaveLength(1);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe('agent-to-agent communication', () => {
|
|
548
|
+
it('should show agent-to-agent messages when both are participants', () => {
|
|
549
|
+
const ctx: DirectMessageTestContext = {
|
|
550
|
+
currentHuman: createAgent('alice', true),
|
|
551
|
+
currentUserName: 'bob',
|
|
552
|
+
messages: [
|
|
553
|
+
createMessage('Agent1', 'Agent2', 'Collaboration message'),
|
|
554
|
+
createMessage('Agent2', 'Agent1', 'Response'),
|
|
555
|
+
],
|
|
556
|
+
agents: [createAgent('Agent1'), createAgent('Agent2')],
|
|
557
|
+
selectedDmAgents: ['Agent1', 'Agent2'],
|
|
558
|
+
removedDmAgents: [],
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const visible = filterVisibleMessages(ctx);
|
|
562
|
+
|
|
563
|
+
expect(visible).toHaveLength(2);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should derive Agent2 when Agent1 (selected) messages it', () => {
|
|
567
|
+
// This tests that derivation works: when a selected agent messages another agent,
|
|
568
|
+
// the recipient agent becomes a derived participant
|
|
569
|
+
const ctx: DirectMessageTestContext = {
|
|
570
|
+
currentHuman: createAgent('alice', true),
|
|
571
|
+
currentUserName: 'bob',
|
|
572
|
+
messages: [
|
|
573
|
+
createMessage('Agent1', 'Agent2', 'Message triggers derivation'),
|
|
574
|
+
],
|
|
575
|
+
agents: [createAgent('Agent1'), createAgent('Agent2')],
|
|
576
|
+
selectedDmAgents: ['Agent1'], // Only Agent1 is selected
|
|
577
|
+
removedDmAgents: [],
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const participants = computeDmParticipantAgents(ctx);
|
|
581
|
+
const visible = filterVisibleMessages(ctx);
|
|
582
|
+
|
|
583
|
+
// Agent2 gets derived because Agent1 (selected) messaged it
|
|
584
|
+
expect(participants).toContain('Agent2');
|
|
585
|
+
expect(visible).toHaveLength(1);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should NOT show message from non-participant to non-participant', () => {
|
|
589
|
+
const ctx: DirectMessageTestContext = {
|
|
590
|
+
currentHuman: createAgent('alice', true),
|
|
591
|
+
currentUserName: 'bob',
|
|
592
|
+
messages: [
|
|
593
|
+
createMessage('Agent3', 'Agent4', 'Neither agent is a participant'),
|
|
594
|
+
],
|
|
595
|
+
agents: [createAgent('Agent1'), createAgent('Agent3'), createAgent('Agent4')],
|
|
596
|
+
selectedDmAgents: ['Agent1'], // Only Agent1 is selected, not Agent3 or Agent4
|
|
597
|
+
removedDmAgents: [],
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const visible = filterVisibleMessages(ctx);
|
|
601
|
+
|
|
602
|
+
// Neither Agent3 nor Agent4 are participants, so message is filtered
|
|
603
|
+
expect(visible).toHaveLength(0);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
describe('human participant scenarios', () => {
|
|
608
|
+
it('should show human (currentHuman) initiating conversation with agent', () => {
|
|
609
|
+
const ctx: DirectMessageTestContext = {
|
|
610
|
+
currentHuman: createAgent('alice', true),
|
|
611
|
+
currentUserName: 'bob',
|
|
612
|
+
messages: [
|
|
613
|
+
createMessage('alice', 'Agent1', 'Alice starts the conversation'),
|
|
614
|
+
createMessage('Agent1', 'alice', 'Agent1 responds to Alice'),
|
|
615
|
+
],
|
|
616
|
+
agents: [createAgent('Agent1')],
|
|
617
|
+
selectedDmAgents: ['Agent1'],
|
|
618
|
+
removedDmAgents: [],
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const visible = filterVisibleMessages(ctx);
|
|
622
|
+
|
|
623
|
+
expect(visible).toHaveLength(2);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('should show messages from human to current user', () => {
|
|
627
|
+
const ctx: DirectMessageTestContext = {
|
|
628
|
+
currentHuman: createAgent('alice', true),
|
|
629
|
+
currentUserName: 'bob',
|
|
630
|
+
messages: [
|
|
631
|
+
createMessage('alice', 'bob', 'Alice messages Bob directly'),
|
|
632
|
+
createMessage('bob', 'alice', 'Bob responds'),
|
|
633
|
+
],
|
|
634
|
+
agents: [],
|
|
635
|
+
selectedDmAgents: [],
|
|
636
|
+
removedDmAgents: [],
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const visible = filterVisibleMessages(ctx);
|
|
640
|
+
|
|
641
|
+
expect(visible).toHaveLength(2);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
describe('cloud mode scenarios', () => {
|
|
646
|
+
it('should work with GitHub username as currentUserName', () => {
|
|
647
|
+
const ctx: DirectMessageTestContext = {
|
|
648
|
+
currentHuman: createAgent('alice', true),
|
|
649
|
+
currentUserName: 'github-user-1', // GitHub username (generic)
|
|
650
|
+
messages: [
|
|
651
|
+
createMessage('github-user-1', 'alice', 'Hi from GitHub user'),
|
|
652
|
+
createMessage('alice', 'github-user-1', 'Hi back!'),
|
|
653
|
+
createMessage('github-user-1', 'Agent1', 'Agent help'),
|
|
654
|
+
createMessage('Agent1', 'github-user-1', 'Agent response'),
|
|
655
|
+
],
|
|
656
|
+
agents: [createAgent('Agent1')],
|
|
657
|
+
selectedDmAgents: ['Agent1'],
|
|
658
|
+
removedDmAgents: [],
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const visible = filterVisibleMessages(ctx);
|
|
662
|
+
|
|
663
|
+
expect(visible).toHaveLength(4);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('should handle mixed Dashboard and username messages', () => {
|
|
667
|
+
// Edge case: what if both Dashboard and username appear in messages?
|
|
668
|
+
const ctx: DirectMessageTestContext = {
|
|
669
|
+
currentHuman: createAgent('alice', true),
|
|
670
|
+
currentUserName: 'bob', // Cloud mode with username
|
|
671
|
+
messages: [
|
|
672
|
+
createMessage('Dashboard', 'alice', 'Old local mode message'),
|
|
673
|
+
createMessage('bob', 'alice', 'New cloud mode message'),
|
|
674
|
+
],
|
|
675
|
+
agents: [],
|
|
676
|
+
selectedDmAgents: [],
|
|
677
|
+
removedDmAgents: [],
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const visible = filterVisibleMessages(ctx);
|
|
681
|
+
|
|
682
|
+
// Only bob->alice should show (Dashboard is not bob)
|
|
683
|
+
// This tests that we correctly use currentUserName when provided
|
|
684
|
+
expect(visible).toHaveLength(1);
|
|
685
|
+
expect(visible[0].content).toBe('New cloud mode message');
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// CLOUD MODE SPECIFIC TESTS - currentUserName is NEVER null in cloud
|
|
689
|
+
it('CLOUD: should route agent responses correctly with GitHub username', () => {
|
|
690
|
+
// This is the original bug scenario but in cloud mode
|
|
691
|
+
const ctx: DirectMessageTestContext = {
|
|
692
|
+
currentHuman: createAgent('alice', true),
|
|
693
|
+
currentUserName: 'github-user-1', // Cloud mode - always has username
|
|
694
|
+
messages: [
|
|
695
|
+
createMessage('github-user-1', 'alice', 'Hi Alice!'),
|
|
696
|
+
createMessage('github-user-1', 'Agent1', 'Agent1, help us please'),
|
|
697
|
+
createMessage('Agent1', 'github-user-1', 'Sure, I can help!'), // Agent responds to user
|
|
698
|
+
],
|
|
699
|
+
agents: [createAgent('Agent1')],
|
|
700
|
+
selectedDmAgents: ['Agent1'],
|
|
701
|
+
removedDmAgents: [],
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const visible = filterVisibleMessages(ctx);
|
|
705
|
+
|
|
706
|
+
// All messages should be visible - agent response stays in group DM
|
|
707
|
+
expect(visible).toHaveLength(3);
|
|
708
|
+
expect(visible.map(m => m.content)).toContain('Sure, I can help!');
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('CLOUD: should handle multiple agents with GitHub username', () => {
|
|
712
|
+
const ctx: DirectMessageTestContext = {
|
|
713
|
+
currentHuman: createAgent('alice', true),
|
|
714
|
+
currentUserName: 'github-user-2', // Different GitHub user
|
|
715
|
+
messages: [
|
|
716
|
+
createMessage('github-user-2', 'alice', 'Team meeting'),
|
|
717
|
+
createMessage('github-user-2', 'Frontend', 'Frontend agent invited'),
|
|
718
|
+
createMessage('github-user-2', 'Backend', 'Backend agent invited'),
|
|
719
|
+
createMessage('Frontend', 'github-user-2', 'Frontend here'),
|
|
720
|
+
createMessage('Backend', 'github-user-2', 'Backend here'),
|
|
721
|
+
createMessage('Frontend', 'Backend', 'Agent collaboration'),
|
|
722
|
+
],
|
|
723
|
+
agents: [createAgent('Frontend'), createAgent('Backend')],
|
|
724
|
+
selectedDmAgents: ['Frontend', 'Backend'],
|
|
725
|
+
removedDmAgents: [],
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const visible = filterVisibleMessages(ctx);
|
|
729
|
+
|
|
730
|
+
// All 6 messages visible
|
|
731
|
+
expect(visible).toHaveLength(6);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('CLOUD: currentUserName is never null - Dashboard fallback not used', () => {
|
|
735
|
+
// Verify that when username is provided, "Dashboard" is NOT in participants
|
|
736
|
+
const ctx: DirectMessageTestContext = {
|
|
737
|
+
currentHuman: createAgent('alice', true),
|
|
738
|
+
currentUserName: 'github-user-1', // Cloud mode
|
|
739
|
+
messages: [
|
|
740
|
+
createMessage('Dashboard', 'alice', 'This should NOT appear'),
|
|
741
|
+
createMessage('github-user-1', 'alice', 'This should appear'),
|
|
742
|
+
],
|
|
743
|
+
agents: [],
|
|
744
|
+
selectedDmAgents: [],
|
|
745
|
+
removedDmAgents: [],
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const visible = filterVisibleMessages(ctx);
|
|
749
|
+
|
|
750
|
+
// Dashboard message filtered out - only github-user-1 messages shown
|
|
751
|
+
expect(visible).toHaveLength(1);
|
|
752
|
+
expect(visible[0].from).toBe('github-user-1');
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('CLOUD: agent removal works with GitHub username', () => {
|
|
756
|
+
const ctx: DirectMessageTestContext = {
|
|
757
|
+
currentHuman: createAgent('alice', true),
|
|
758
|
+
currentUserName: 'github-user-1',
|
|
759
|
+
messages: [
|
|
760
|
+
createMessage('github-user-1', 'Agent1', 'To Agent1'),
|
|
761
|
+
createMessage('Agent1', 'github-user-1', 'From Agent1'),
|
|
762
|
+
createMessage('github-user-1', 'Agent2', 'To Agent2'),
|
|
763
|
+
createMessage('Agent2', 'github-user-1', 'From Agent2'),
|
|
764
|
+
],
|
|
765
|
+
agents: [createAgent('Agent1'), createAgent('Agent2')],
|
|
766
|
+
selectedDmAgents: ['Agent1', 'Agent2'],
|
|
767
|
+
removedDmAgents: ['Agent2'], // Agent2 removed
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const visible = filterVisibleMessages(ctx);
|
|
771
|
+
|
|
772
|
+
// Only Agent1 messages should be visible
|
|
773
|
+
expect(visible).toHaveLength(2);
|
|
774
|
+
expect(visible.every(m => m.from === 'Agent1' || m.to === 'Agent1')).toBe(true);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe('boundary conditions', () => {
|
|
779
|
+
it('should handle many agents (5+)', () => {
|
|
780
|
+
const agents = ['Agent1', 'Agent2', 'Agent3', 'Agent4', 'Agent5', 'Agent6'].map(
|
|
781
|
+
name => createAgent(name)
|
|
782
|
+
);
|
|
783
|
+
const ctx: DirectMessageTestContext = {
|
|
784
|
+
currentHuman: createAgent('alice', true),
|
|
785
|
+
currentUserName: 'bob',
|
|
786
|
+
messages: agents.map(a => createMessage('bob', a.name, `Message to ${a.name}`)),
|
|
787
|
+
agents,
|
|
788
|
+
selectedDmAgents: agents.map(a => a.name),
|
|
789
|
+
removedDmAgents: [],
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const visible = filterVisibleMessages(ctx);
|
|
793
|
+
|
|
794
|
+
expect(visible).toHaveLength(6);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('should handle conversation with many message exchanges', () => {
|
|
798
|
+
const messages: Message[] = [];
|
|
799
|
+
for (let i = 0; i < 50; i++) {
|
|
800
|
+
messages.push(createMessage('bob', 'alice', `Message ${i}`));
|
|
801
|
+
messages.push(createMessage('alice', 'bob', `Response ${i}`));
|
|
802
|
+
messages.push(createMessage('bob', 'Agent1', `Agent message ${i}`));
|
|
803
|
+
messages.push(createMessage('Agent1', 'bob', `Agent response ${i}`));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const ctx: DirectMessageTestContext = {
|
|
807
|
+
currentHuman: createAgent('alice', true),
|
|
808
|
+
currentUserName: 'bob',
|
|
809
|
+
messages,
|
|
810
|
+
agents: [createAgent('Agent1')],
|
|
811
|
+
selectedDmAgents: ['Agent1'],
|
|
812
|
+
removedDmAgents: [],
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const visible = filterVisibleMessages(ctx);
|
|
816
|
+
|
|
817
|
+
expect(visible).toHaveLength(200); // 50 * 4 messages
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('should handle agent with same prefix as human name', () => {
|
|
821
|
+
const ctx: DirectMessageTestContext = {
|
|
822
|
+
currentHuman: createAgent('alice', true),
|
|
823
|
+
currentUserName: 'bob',
|
|
824
|
+
messages: [
|
|
825
|
+
createMessage('bob', 'alice', 'To human alice'),
|
|
826
|
+
createMessage('bob', 'alice-agent', 'To agent alice-agent'),
|
|
827
|
+
],
|
|
828
|
+
agents: [createAgent('alice-agent')], // Agent name starts with human name
|
|
829
|
+
selectedDmAgents: ['alice-agent'],
|
|
830
|
+
removedDmAgents: [],
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const visible = filterVisibleMessages(ctx);
|
|
834
|
+
|
|
835
|
+
expect(visible).toHaveLength(2);
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
describe('GitHub username vs Dashboard routing', () => {
|
|
840
|
+
/**
|
|
841
|
+
* BUG FIX TEST: Messages to GitHub username should be visible
|
|
842
|
+
*
|
|
843
|
+
* When an agent sends a message TO a user's GitHub username,
|
|
844
|
+
* the message should appear in the user's DM view.
|
|
845
|
+
*
|
|
846
|
+
* Scenario:
|
|
847
|
+
* - User "khaliqgant" is viewing DMs
|
|
848
|
+
* - Lead agent sends message to "khaliqgant"
|
|
849
|
+
* - Message should be visible to khaliqgant
|
|
850
|
+
*/
|
|
851
|
+
it('should show messages sent TO user GitHub username', () => {
|
|
852
|
+
const ctx: DirectMessageTestContext = {
|
|
853
|
+
currentHuman: createAgent('khaliqgant', true), // Viewing own DMs
|
|
854
|
+
currentUserName: 'khaliqgant', // User's GitHub username
|
|
855
|
+
messages: [
|
|
856
|
+
createMessage('Lead', 'khaliqgant', 'Hello khaliqgant, here is an update'),
|
|
857
|
+
],
|
|
858
|
+
agents: [createAgent('Lead')], // Lead is an AI agent
|
|
859
|
+
selectedDmAgents: ['Lead'], // Lead was invited to conversation
|
|
860
|
+
removedDmAgents: [],
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
const visible = filterVisibleMessages(ctx);
|
|
864
|
+
|
|
865
|
+
// Message from Lead to khaliqgant should be visible
|
|
866
|
+
expect(visible).toHaveLength(1);
|
|
867
|
+
expect(visible[0].content).toBe('Hello khaliqgant, here is an update');
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Dashboard messages should NOT appear in DM view
|
|
872
|
+
*
|
|
873
|
+
* Dashboard is a system client, not a real participant.
|
|
874
|
+
* Messages addressed to Dashboard should not be shown.
|
|
875
|
+
*/
|
|
876
|
+
it('should NOT show messages sent TO Dashboard', () => {
|
|
877
|
+
const ctx: DirectMessageTestContext = {
|
|
878
|
+
currentHuman: createAgent('khaliqgant', true),
|
|
879
|
+
currentUserName: 'khaliqgant',
|
|
880
|
+
messages: [
|
|
881
|
+
createMessage('Lead', 'Dashboard', 'Message to system client'),
|
|
882
|
+
],
|
|
883
|
+
agents: [createAgent('Lead')],
|
|
884
|
+
selectedDmAgents: ['Lead'],
|
|
885
|
+
removedDmAgents: [],
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const visible = filterVisibleMessages(ctx);
|
|
889
|
+
|
|
890
|
+
// Message to Dashboard should NOT be visible (it's not a participant)
|
|
891
|
+
expect(visible).toHaveLength(0);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Both GitHub username AND Dashboard messages in same conversation
|
|
896
|
+
*
|
|
897
|
+
* When an agent sends to both targets, only the GitHub username
|
|
898
|
+
* message should be visible.
|
|
899
|
+
*/
|
|
900
|
+
it('should show GitHub username messages but not Dashboard messages', () => {
|
|
901
|
+
const ctx: DirectMessageTestContext = {
|
|
902
|
+
currentHuman: createAgent('khaliqgant', true),
|
|
903
|
+
currentUserName: 'khaliqgant',
|
|
904
|
+
messages: [
|
|
905
|
+
createMessage('Lead', 'khaliqgant', 'Direct to user'),
|
|
906
|
+
createMessage('Lead', 'Dashboard', 'To system client'),
|
|
907
|
+
createMessage('khaliqgant', 'Lead', 'Reply from user'),
|
|
908
|
+
],
|
|
909
|
+
agents: [createAgent('Lead')],
|
|
910
|
+
selectedDmAgents: ['Lead'],
|
|
911
|
+
removedDmAgents: [],
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
const visible = filterVisibleMessages(ctx);
|
|
915
|
+
|
|
916
|
+
// Only 2 messages visible (Lead->khaliqgant and khaliqgant->Lead)
|
|
917
|
+
// Lead->Dashboard should be filtered out
|
|
918
|
+
expect(visible).toHaveLength(2);
|
|
919
|
+
expect(visible.map(m => m.to)).not.toContain('Dashboard');
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Edge case: currentUserName is different from currentHuman.name
|
|
924
|
+
*
|
|
925
|
+
* When viewing another user's DMs (e.g., admin view),
|
|
926
|
+
* messages to currentUserName should still be filtered correctly.
|
|
927
|
+
*/
|
|
928
|
+
it('should use effectiveUserName (currentUserName) for filtering, not currentHuman.name', () => {
|
|
929
|
+
const ctx: DirectMessageTestContext = {
|
|
930
|
+
currentHuman: createAgent('alice', true), // Viewing DMs with alice
|
|
931
|
+
currentUserName: 'bob', // Current logged-in user is bob
|
|
932
|
+
messages: [
|
|
933
|
+
createMessage('Lead', 'bob', 'Message to bob (current user)'),
|
|
934
|
+
createMessage('Lead', 'alice', 'Message to alice (DM partner)'),
|
|
935
|
+
createMessage('Lead', 'charlie', 'Message to charlie (third party)'),
|
|
936
|
+
],
|
|
937
|
+
agents: [createAgent('Lead')],
|
|
938
|
+
selectedDmAgents: ['Lead'],
|
|
939
|
+
removedDmAgents: [],
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const visible = filterVisibleMessages(ctx);
|
|
943
|
+
|
|
944
|
+
// Lead->bob and Lead->alice should be visible
|
|
945
|
+
// Lead->charlie should NOT be visible
|
|
946
|
+
expect(visible).toHaveLength(2);
|
|
947
|
+
expect(visible.map(m => m.to)).toContain('bob');
|
|
948
|
+
expect(visible.map(m => m.to)).toContain('alice');
|
|
949
|
+
expect(visible.map(m => m.to)).not.toContain('charlie');
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
});
|