fix(integrations): polish agent filter UX and embed session rebind

Add compact agent filter beside channel list titles with selected name
display, fix embed localStorage agentId read on rebind, add IM agent
rebind tests, and deep-link agent editor counts to the matching tab.
This commit is contained in:
wizardchen
2026-06-24 20:08:14 +08:00
committed by lyingbug
parent ec5c3ac638
commit babe3f2315
11 changed files with 309 additions and 120 deletions
@@ -3,6 +3,7 @@
<div class="channels-section">
<div class="channels-header">
<span class="channels-title">{{ $t('embedPublish.channelsTitle') }}</span>
<IntegrationsAgentFilter v-model="filterAgentId" :agents="agents" />
<span class="channels-count">{{ channels.length }}</span>
</div>
@@ -29,7 +30,7 @@
{{ $t('embedPublish.disabled') }}
</t-tag>
</div>
<span v-if="tenantMode && agentDisplayName(ch)" class="channel-card__agent-name">
<span v-if="agentDisplayName(ch)" class="channel-card__agent-name">
{{ agentDisplayName(ch) }}
</span>
</div>
@@ -135,7 +136,7 @@
<section class="setting-drawer__section embed-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('embedPublish.sectionChannel') }}</h4>
<div v-if="tenantMode" class="form-item">
<div class="form-item">
<label class="form-label required">{{ $t('integrations.boundAgent') }}</label>
<div class="agent-field-row">
<t-select
@@ -542,14 +543,13 @@
</template>
<script setup lang="ts">
import { computed, nextTick, reactive, ref, watch } from 'vue'
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { MessagePlugin } from 'tdesign-vue-next'
import { useAuthStore } from '@/stores/auth'
import SettingDrawer from '@/components/settings/SettingDrawer.vue'
import EmbedChannelPreview from '@/components/EmbedChannelPreview.vue'
import {
listEmbedChannels,
listAllEmbedChannels,
createEmbedChannel,
updateEmbedChannel,
@@ -575,16 +575,9 @@ import {
type AllowedOriginsValidationError,
} from '@/utils/embedAllowedOrigins'
import { listAgents, type CustomAgent } from '@/api/agent'
import IntegrationsAgentFilter from '@/components/IntegrationsAgentFilter.vue'
const props = withDefaults(defineProps<{
agentId?: string
tenantMode?: boolean
initialAgentId?: string
agentWebSearchEnabled?: boolean
agentImageUploadEnabled?: boolean
}>(), {
tenantMode: false,
})
const filterAgentId = defineModel<string>('filterAgentId', { default: '' })
const { t } = useI18n()
const authStore = useAuthStore()
@@ -592,7 +585,12 @@ const isAdmin = computed(() => authStore.hasRole('admin'))
const loading = ref(false)
const saving = ref(false)
const channels = ref<EmbedChannel[]>([])
const allChannels = ref<EmbedChannel[]>([])
const channels = computed(() => {
const filter = filterAgentId.value?.trim()
if (!filter) return allChannels.value
return allChannels.value.filter((ch) => ch.agent_id === filter)
})
const tokenByChannel = ref<Record<string, string>>({})
const revealedTokens = reactive<Record<string, boolean>>({})
const previewVisible = ref(false)
@@ -621,7 +619,6 @@ const agentOptions = computed(() =>
)
const drawerAgentId = computed(() => {
if (!props.tenantMode) return props.agentId || ''
if (editingId.value) {
return drawerChannel.value?.agent_id || ''
}
@@ -632,19 +629,13 @@ const drawerAgent = computed(() =>
agents.value.find((agent) => agent.id === drawerAgentId.value),
)
const agentWebSearchEnabledEffective = computed(() => {
if (props.tenantMode) {
return drawerAgent.value?.config?.web_search_enabled === true
}
return props.agentWebSearchEnabled === true
})
const agentWebSearchEnabledEffective = computed(() =>
drawerAgent.value?.config?.web_search_enabled === true,
)
const agentImageUploadEnabledEffective = computed(() => {
if (props.tenantMode) {
return drawerAgent.value?.config?.image_upload_enabled === true
}
return props.agentImageUploadEnabled === true
})
const agentImageUploadEnabledEffective = computed(() =>
drawerAgent.value?.config?.image_upload_enabled === true,
)
const EMBED_TOKEN_STORAGE = 'weknora_embed_publish_tokens'
const WEKNORA_BRAND_COLOR = '#07C05F'
@@ -724,9 +715,7 @@ function handleChannelMenuClick(data: { value?: string }, ch: EmbedChannel) {
const drawerTitle = computed(() => {
if (!editingId.value) return t('embedPublish.createTitle')
const ch = drawerChannel.value
const agentId = props.tenantMode
? (createAgentId.value || ch?.agent_id || '')
: (ch?.agent_id || props.agentId || '')
const agentId = createAgentId.value || ch?.agent_id || ''
return channelDisplayName({
id: editingId.value,
name: form.value.name,
@@ -734,7 +723,7 @@ const drawerTitle = computed(() => {
} as EmbedChannel)
})
function defaultEmbedChannelName(agentId = props.tenantMode ? createAgentId.value : props.agentId): string {
function defaultEmbedChannelName(agentId = createAgentId.value): string {
const agent = agentId ? agents.value.find((item) => item.id === agentId) : undefined
if (agent?.name?.trim()) {
return t('embedPublish.defaultChannelNameWithAgent', { agent: agent.name.trim() })
@@ -776,7 +765,7 @@ const drawerConfirmText = computed(() =>
)
function validateWizardStep(step: number): boolean {
if (step === 0 && props.tenantMode && !createAgentId.value) {
if (step === 0 && !createAgentId.value) {
MessagePlugin.warning(t('integrations.selectAgentHint'))
return false
}
@@ -845,22 +834,16 @@ const storeToken = (channelId: string, token: string) => {
}
const load = async () => {
if (!props.tenantMode && !props.agentId) return
loading.value = true
try {
if (props.tenantMode) {
const [res, agentRes] = await Promise.all([
listAllEmbedChannels(),
listAgents(),
])
channels.value = res?.data || []
agents.value = agentRes?.data || []
} else {
const res = await listEmbedChannels(props.agentId!)
channels.value = res?.data || []
}
const [res, agentRes] = await Promise.all([
listAllEmbedChannels(),
listAgents(),
])
allChannels.value = res?.data || []
agents.value = agentRes?.data || []
tokenByChannel.value = { ...loadStoredTokens(), ...tokenByChannel.value }
await Promise.all(channels.value.map(async (ch) => {
await Promise.all(allChannels.value.map(async (ch) => {
try {
const statsRes = await getEmbedChannelStats(ch.id)
if (statsRes?.data?.session_count != null) {
@@ -875,9 +858,15 @@ const load = async () => {
}
}
watch(() => [props.tenantMode, props.agentId], () => {
if (props.tenantMode || props.agentId) load()
}, { immediate: true })
watch(filterAgentId, (id) => {
if (!showDrawer.value && !editingId.value && id) {
createAgentId.value = id
}
})
onMounted(() => {
void load()
})
watch(stepTitles, (titles) => {
if (wizardStep.value >= titles.length) {
@@ -1010,7 +999,7 @@ const openCreate = () => {
channelNameTouched.value = false
editingId.value = ''
editingEnabled.value = true
createAgentId.value = props.tenantMode ? (props.initialAgentId || '') : ''
createAgentId.value = filterAgentId.value || ''
form.value = defaultForm()
applyDefaultChannelNameIfNeeded()
originsText.value = ''
@@ -1056,7 +1045,7 @@ const mapOriginsApiError = (message: string): string | null => {
const saveForm = async () => {
if (!isAdmin.value) return
if (props.tenantMode && !createAgentId.value) {
if (!createAgentId.value) {
MessagePlugin.warning(t('integrations.selectAgentHint'))
return
}
@@ -1086,7 +1075,7 @@ const saveForm = async () => {
webhook_url: form.value.webhook_url || '',
webhook_secret: form.value.webhook_secret || undefined,
enabled: editingId.value ? editingEnabled.value : true,
...(props.tenantMode ? { agent_id: createAgentId.value } : {}),
agent_id: createAgentId.value,
}
if (editingId.value) {
const previousAgentId = drawerChannel.value?.agent_id
@@ -1106,7 +1095,7 @@ const saveForm = async () => {
}
}
} else {
const targetAgentId = props.tenantMode ? createAgentId.value : props.agentId
const targetAgentId = createAgentId.value
if (!targetAgentId) {
MessagePlugin.warning(t('integrations.selectAgentHint'))
return
@@ -1157,9 +1146,7 @@ async function openPreviewForChannel(
) {
previewLoading.value = true
try {
const agentId = props.tenantMode
? ((opts?.useDraft ? createAgentId.value : '') || ch.agent_id)
: ch.agent_id
const agentId = ((opts?.useDraft ? createAgentId.value : '') || ch.agent_id)
if (agentId) {
clearEmbedStoredChatSessionIfAgentMismatch(ch.id, agentId)
}
+32 -48
View File
@@ -3,6 +3,7 @@
<div class="channels-section">
<div class="channels-header">
<span class="channels-title">{{ $t('agentEditor.im.channelsTitle') }}</span>
<IntegrationsAgentFilter v-model="filterAgentId" :agents="agents" />
<span class="channels-count">{{ channels.length }}</span>
</div>
@@ -35,7 +36,7 @@
{{ $t('agentEditor.im.disabled') }}
</t-tag>
</div>
<span v-if="tenantMode && agentDisplayName(channel)" class="channel-card__agent-name">
<span v-if="agentDisplayName(channel)" class="channel-card__agent-name">
{{ agentDisplayName(channel) }}
</span>
</div>
@@ -148,7 +149,7 @@
<section class="setting-drawer__section im-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('agentEditor.im.sectionChannel') }}</h4>
<div v-if="tenantMode" class="form-item">
<div class="form-item">
<label class="form-label required">{{ $t('integrations.boundAgent') }}</label>
<div class="agent-field-row">
<t-select
@@ -595,6 +596,7 @@ import { useChatResourcesStore } from '@/stores/chatResources';
import type { IMChannel } from '@/api/agent';
import { useAuthStore } from '@/stores/auth';
import SettingDrawer from '@/components/settings/SettingDrawer.vue';
import IntegrationsAgentFilter from '@/components/IntegrationsAgentFilter.vue';
import wecomLogo from '@/assets/img/im/wecom.svg';
import feishuLogo from '@/assets/img/im/feishu.svg';
import slackLogo from '@/assets/img/im/slack.svg';
@@ -620,20 +622,19 @@ const platformLogo = (platform: string): string => (platform ? PLATFORM_LOGO[pla
const { t } = useI18n();
const authStore = useAuthStore();
const props = withDefaults(defineProps<{
agentId?: string;
tenantMode?: boolean;
initialAgentId?: string;
}>(), {
tenantMode: false,
});
const filterAgentId = defineModel<string>('filterAgentId', { default: '' });
const agents = ref<CustomAgent[]>([]);
const agentOptions = computed(() =>
agents.value.map((agent) => ({ label: agent.name, value: agent.id })),
);
const channels = ref<Array<IMChannel | IMChannelOverview>>([]);
const allChannels = ref<Array<IMChannel | IMChannelOverview>>([]);
const channels = computed(() => {
const filter = filterAgentId.value?.trim();
if (!filter) return allChannels.value;
return allChannels.value.filter((channel) => channel.agent_id === filter);
});
const loading = ref(false);
const saving = ref(false);
const showCreateDialog = ref(false);
@@ -673,7 +674,7 @@ const drawerTitle = computed(() => {
});
function validateWizardStep(step: number): boolean {
if (step === 0 && props.tenantMode && !formData.value.target_agent_id) {
if (step === 0 && !formData.value.target_agent_id) {
MessagePlugin.warning(t('integrations.selectAgentHint'));
return false;
}
@@ -886,26 +887,16 @@ async function loadChannels() {
loading.value = true;
try {
const chatResources = useChatResourcesStore();
if (props.tenantMode) {
const [channelRes, agentRes] = await Promise.all([
listAllIMChannels(),
listAgents(),
chatResources.ensureKnowledgeBases(),
]);
channels.value = channelRes.data || [];
agents.value = agentRes?.data || [];
} else if (props.agentId) {
const [channelRes] = await Promise.all([
listIMChannels(props.agentId),
chatResources.ensureKnowledgeBases(),
]);
channels.value = channelRes.data || [];
} else {
channels.value = [];
}
const [channelRes, agentRes] = await Promise.all([
listAllIMChannels(),
listAgents(),
chatResources.ensureKnowledgeBases(),
]);
allChannels.value = channelRes.data || [];
agents.value = agentRes?.data || [];
knowledgeBases.value = chatResources.rawKnowledgeBases.map((kb: any) => ({ id: kb.id, name: kb.name }));
} catch {
channels.value = [];
allChannels.value = [];
} finally {
loading.value = false;
}
@@ -940,8 +931,8 @@ async function copyUrl(channel: IMChannel) {
function openCreate() {
resetForm();
if (props.tenantMode && props.initialAgentId) {
formData.value.target_agent_id = props.initialAgentId;
if (filterAgentId.value) {
formData.value.target_agent_id = filterAgentId.value;
}
showCreateDialog.value = true;
}
@@ -953,7 +944,7 @@ function openDrawer(channel: IMChannel | IMChannelOverview) {
async function editChannel(channel: IMChannel | IMChannelOverview) {
wizardStep.value = 0;
let fullChannel: IMChannel | null = null;
if (props.tenantMode && !('credentials' in channel)) {
if (!('credentials' in channel)) {
try {
const res = await listIMChannels(channel.agent_id);
fullChannel = (res.data || []).find((item) => item.id === channel.id) || null;
@@ -994,7 +985,7 @@ function resetForm() {
wechatQRCode.value = '';
wechatQRStatus.value = '';
formData.value = {
target_agent_id: props.tenantMode ? (props.initialAgentId || '') : '',
target_agent_id: filterAgentId.value || '',
platform: 'wecom',
name: defaultChannelName('wecom'),
mode: 'websocket',
@@ -1023,13 +1014,11 @@ async function handleSave() {
knowledge_base_id: formData.value.knowledge_base_id,
credentials: formData.value.credentials,
enabled: editingEnabled.value,
...(props.tenantMode && formData.value.target_agent_id
? { agent_id: formData.value.target_agent_id }
: {}),
...(formData.value.target_agent_id ? { agent_id: formData.value.target_agent_id } : {}),
});
MessagePlugin.success(t('common.updateSuccess'));
} else {
const targetAgentId = props.tenantMode ? formData.value.target_agent_id : props.agentId;
const targetAgentId = formData.value.target_agent_id;
if (!targetAgentId) {
MessagePlugin.warning(t('integrations.selectAgentHint'));
return;
@@ -1076,19 +1065,14 @@ async function handleDelete(id: string) {
}
onMounted(() => {
if (props.tenantMode || props.agentId) {
loadChannels();
}
loadChannels();
});
watch(
() => [props.tenantMode, props.agentId],
() => {
if (props.tenantMode || props.agentId) {
loadChannels();
}
},
);
watch(filterAgentId, (id) => {
if (!showCreateDialog.value && !editingChannel.value && id) {
formData.value.target_agent_id = id;
}
});
onUnmounted(() => {
stopWeChatPolling();
@@ -0,0 +1,113 @@
<template>
<t-dropdown
:options="options"
trigger="click"
placement="bottom-left"
attach="body"
:max-column-width="240"
@click="onSelect"
>
<button
type="button"
class="integrations-agent-filter"
:class="{ 'integrations-agent-filter--active': modelValue }"
:aria-label="ariaLabel"
>
<t-icon name="filter" size="14px" class="integrations-agent-filter__icon" />
<span v-if="selectedAgentName" class="integrations-agent-filter__name">{{ selectedAgentName }}</span>
<t-icon name="chevron-down" size="12px" class="integrations-agent-filter__chevron" />
</button>
</t-dropdown>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { CustomAgent } from '@/api/agent'
const props = defineProps<{
modelValue: string
agents: CustomAgent[]
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const { t } = useI18n()
const selectedAgentName = computed(() => {
if (!props.modelValue) return ''
return props.agents.find((item) => item.id === props.modelValue)?.name || ''
})
const ariaLabel = computed(() =>
selectedAgentName.value
? t('integrations.filterByAgentWithName', { name: selectedAgentName.value })
: t('integrations.filterByAgent'),
)
const options = computed(() => [
{ content: t('integrations.filterAllAgents'), value: '' },
...props.agents.map((agent) => ({
content: agent.name,
value: agent.id,
active: agent.id === props.modelValue,
})),
])
function onSelect(data: { value?: string }) {
emit('update:modelValue', data?.value || '')
}
</script>
<style scoped lang="less">
.integrations-agent-filter {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 220px;
padding: 2px 6px 2px 4px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--td-text-color-placeholder);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
&:hover {
background: var(--td-bg-color-container-hover);
color: var(--td-text-color-secondary);
}
&--active {
color: var(--td-brand-color);
background: var(--td-bg-color-secondarycontainer);
&:hover {
color: var(--td-brand-color);
background: var(--td-bg-color-secondarycontainer);
}
}
&__icon {
flex-shrink: 0;
}
&__name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 500;
color: inherit;
}
&__chevron {
flex-shrink: 0;
opacity: 0.7;
}
}
</style>
+6 -1
View File
@@ -33,7 +33,12 @@ function readStoredSession(channelId: string): StoredSession | null {
if (!raw) return null
const parsed = JSON.parse(raw)
if (parsed && typeof parsed.id === 'string' && typeof parsed.sig === 'string' && parsed.id) {
return { id: parsed.id, sig: parsed.sig }
const agentId = typeof parsed.agentId === 'string' ? parsed.agentId.trim() : ''
return {
id: parsed.id,
sig: parsed.sig,
...(agentId ? { agentId } : {}),
}
}
} catch {
// Malformed / legacy (plain-string) entry: ignore so a fresh signed
+3
View File
@@ -5615,6 +5615,9 @@ export default {
selectAgentPlaceholder: 'Choose an agent',
selectAgentHint: 'Please select an agent first',
boundAgent: 'Bound agent',
filterByAgent: 'Filter by agent',
filterByAgentWithName: 'Filter by agent: {name}',
filterAllAgents: 'All agents',
manageChannel: 'Manage channel',
goToCenter: 'Open Integrations',
imOverview: {
+3
View File
@@ -5626,6 +5626,9 @@ export default {
selectAgentPlaceholder: "에이전트를 선택하세요",
selectAgentHint: "먼저 에이전트를 선택하세요",
boundAgent: "연결된 에이전트",
filterByAgent: "에이전트별 필터",
filterByAgentWithName: "에이전트별 필터: {name}",
filterAllAgents: "전체 에이전트",
manageChannel: "채널 관리",
goToCenter: "통합 센터로 이동",
imOverview: {
+3
View File
@@ -5448,6 +5448,9 @@ export default {
selectAgentPlaceholder: 'Выберите агента',
selectAgentHint: 'Сначала выберите агента',
boundAgent: 'Привязанный агент',
filterByAgent: 'Фильтр по агенту',
filterByAgentWithName: 'Фильтр по агенту: {name}',
filterAllAgents: 'Все агенты',
manageChannel: 'Управлять каналом',
goToCenter: 'Открыть интеграции',
imOverview: {
+3
View File
@@ -5633,6 +5633,9 @@ export default {
selectAgentPlaceholder: "请选择智能体",
selectAgentHint: "请先选择一个智能体",
boundAgent: "绑定智能体",
filterByAgent: "按智能体筛选",
filterByAgentWithName: "按智能体筛选:{name}",
filterAllAgents: "全部智能体",
manageChannel: "管理渠道",
goToCenter: "前往集成中心",
imOverview: {
+24 -10
View File
@@ -79,15 +79,13 @@
</div>
<div class="setting-control">
<div class="integration-inline">
<span class="integration-inline__stat">
{{ $t('integrations.tabs.im') }} · {{ agentIMChannelCount }}
</span>
<button type="button" class="integration-inline__stat integration-inline__link" @click="gotoIntegrations('im')">
<span>{{ $t('integrations.tabs.im') }} · {{ agentIMChannelCount }}</span>
<t-icon name="chevron-right" size="14px" />
</button>
<span class="integration-inline__sep" aria-hidden="true">|</span>
<span class="integration-inline__stat">
{{ $t('integrations.tabs.embed') }} · {{ agentEmbedChannelCount }}
</span>
<button type="button" class="integration-inline__link" @click="gotoIntegrations">
<span>{{ $t('integrations.agentEditor.manage') }}</span>
<button type="button" class="integration-inline__stat integration-inline__link" @click="gotoIntegrations('embed')">
<span>{{ $t('integrations.tabs.embed') }} · {{ agentEmbedChannelCount }}</span>
<t-icon name="chevron-right" size="14px" />
</button>
</div>
@@ -2084,11 +2082,11 @@ async function loadAgentIntegrationCounts(agentId: string) {
}
}
function gotoIntegrations() {
function gotoIntegrations(tab: 'im' | 'embed') {
const agentId = editorAgent.value?.id;
if (!agentId) return;
handleClose();
router.push({ path: '/platform/integrations', query: { agentId } });
router.push({ path: '/platform/integrations', query: { agentId, tab } });
}
const filteredIntentPlaceholders = computed(() => {
@@ -4285,6 +4283,22 @@ const handleSave = async () => {
&__stat {
font-size: 13px;
color: var(--td-text-color-secondary);
&.integration-inline__link {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 0;
border: none;
background: transparent;
line-height: 1;
color: var(--td-brand-color);
cursor: pointer;
&:hover {
opacity: 0.85;
}
}
}
&__sep {
@@ -46,7 +46,7 @@
</a>
</p>
</div>
<IMChannelPanel tenant-mode :initial-agent-id="initialAgentId" />
<IMChannelPanel v-model:filter-agent-id="filterAgentId" />
</div>
<div v-if="currentSection === 'embed'" class="section">
@@ -54,7 +54,7 @@
<h2>{{ $t('agentEditor.embed.title') }}</h2>
<p class="section-description">{{ $t('agentEditor.embed.description') }}</p>
</div>
<AgentEmbedChannelPanel tenant-mode :initial-agent-id="initialAgentId" />
<AgentEmbedChannelPanel v-model:filter-agent-id="filterAgentId" />
</div>
</div>
</div>
@@ -77,7 +77,7 @@ const route = useRoute();
const router = useRouter();
const currentSection = ref<'im' | 'embed'>('im');
const initialAgentId = ref('');
const filterAgentId = ref('');
const visible = computed(() => route.name === 'integrations');
@@ -91,13 +91,13 @@ function applyRouteQuery() {
if (tab === 'im' || tab === 'embed') {
currentSection.value = tab;
}
initialAgentId.value = (route.query.agentId as string) || '';
filterAgentId.value = (route.query.agentId as string) || '';
}
function syncRouteQuery() {
const query: Record<string, string> = { tab: currentSection.value };
if (initialAgentId.value) {
query.agentId = initialAgentId.value;
if (filterAgentId.value) {
query.agentId = filterAgentId.value;
}
router.replace({ path: route.path, query });
}
@@ -119,6 +119,10 @@ watch(currentSection, () => {
if (visible.value) syncRouteQuery();
});
watch(filterAgentId, () => {
if (visible.value) syncRouteQuery();
});
watch(
() => [route.query.tab, route.query.agentId],
() => {
+70
View File
@@ -0,0 +1,70 @@
package im
import (
"context"
"testing"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)
type stubIMAgentService struct {
interfaces.CustomAgentService
agent *types.CustomAgent
}
func (s *stubIMAgentService) GetAgentByID(_ context.Context, id string) (*types.CustomAgent, error) {
if s.agent != nil && s.agent.ID == id {
return s.agent, nil
}
return nil, nil
}
func TestSetChannelAgentID(t *testing.T) {
svc := &Service{
agentService: &stubIMAgentService{
agent: &types.CustomAgent{ID: "agent-new", TenantID: 42},
},
}
channel := &IMChannel{
ID: "im-1",
TenantID: 42,
AgentID: "agent-old",
}
if err := svc.SetChannelAgentID(context.Background(), channel, "agent-new"); err != nil {
t.Fatalf("SetChannelAgentID() error = %v", err)
}
if channel.AgentID != "agent-new" {
t.Fatalf("channel.AgentID = %q, want agent-new", channel.AgentID)
}
}
func TestSetChannelAgentIDRejectsForeignTenant(t *testing.T) {
svc := &Service{
agentService: &stubIMAgentService{
agent: &types.CustomAgent{ID: "agent-new", TenantID: 99},
},
}
channel := &IMChannel{
ID: "im-1",
TenantID: 42,
AgentID: "agent-old",
}
if err := svc.SetChannelAgentID(context.Background(), channel, "agent-new"); err == nil {
t.Fatal("SetChannelAgentID() expected error for foreign tenant agent")
}
if channel.AgentID != "agent-old" {
t.Fatalf("channel.AgentID = %q, want unchanged agent-old", channel.AgentID)
}
}
func TestSetChannelAgentIDRequiresAgentID(t *testing.T) {
svc := &Service{agentService: &stubIMAgentService{}}
channel := &IMChannel{ID: "im-1", TenantID: 42, AgentID: "agent-old"}
if err := svc.SetChannelAgentID(context.Background(), channel, " "); err == nil {
t.Fatal("SetChannelAgentID() expected error for empty agent_id")
}
}