* Agents streaming * Fix bug and lint * Add reasoning summaries to mobile agents * Add tool call approval UI for mobile agents * Add citations and annotations support for mobile agents Implements Phase 4 from the agents mobile plan: - WebSocket event handling for annotations control signal - CitationsList component displaying sources below agent responses - Collapsible sources section with source count - Each citation shows title, domain, and opens URL on tap - Citations persisted in post.props.annotations - Touch-optimized UI with 44pt minimum tap targets * Add stop and regenerate controls for mobile agents * Add tests * Fix tool approval buttons not updating after accept/reject - Remove optimistic WebSocket event approach that didn't work - Clear component streaming state on ENDED event to force switch to persisted data - Remove delays in streaming store cleanup (500ms and 100ms) - POST_EDITED event now properly updates UI for both accept and reject - Remove debug console.log statements - Fix ESLint issues (unused vars, nested callbacks, nested ternary) * Refactor agents code: remove unused client mixin, fix bugs, and simplify logic * Fixes * Add CLAUDE.md * Review feedback. * Review feedback inline functions * Learnings * Address review feedback: StyleSheet.create, parent mount checks, utils * Move to observables * Last style fix * Style tweaks
134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {AGENT_POST_TYPES} from '@agents/constants';
|
|
|
|
import TestHelper from '@test/test_helper';
|
|
|
|
import {isAgentPost, isPostRequester} from './utils';
|
|
|
|
describe('isAgentPost', () => {
|
|
describe('with Post objects', () => {
|
|
it('returns true when post type is custom_llmbot', () => {
|
|
const post = TestHelper.fakePost({type: AGENT_POST_TYPES.LLMBOT});
|
|
expect(isAgentPost(post)).toBe(true);
|
|
});
|
|
|
|
it('returns true when post type is custom_llm_postback', () => {
|
|
const post = TestHelper.fakePost({type: AGENT_POST_TYPES.LLM_POSTBACK});
|
|
expect(isAgentPost(post)).toBe(true);
|
|
});
|
|
|
|
it('returns false for non-agent post types', () => {
|
|
const post = TestHelper.fakePost({type: ''});
|
|
expect(isAgentPost(post)).toBe(false);
|
|
|
|
const systemPost = TestHelper.fakePost({type: 'system_join_channel'});
|
|
expect(isAgentPost(systemPost)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('with PostModel objects', () => {
|
|
it('returns true when post type is custom_llmbot', () => {
|
|
const postModel = TestHelper.fakePostModel({type: AGENT_POST_TYPES.LLMBOT});
|
|
expect(isAgentPost(postModel)).toBe(true);
|
|
});
|
|
|
|
it('returns true when post type is custom_llm_postback', () => {
|
|
const postModel = TestHelper.fakePostModel({type: AGENT_POST_TYPES.LLM_POSTBACK});
|
|
expect(isAgentPost(postModel)).toBe(true);
|
|
});
|
|
|
|
it('returns false for non-agent post types', () => {
|
|
const postModel = TestHelper.fakePostModel({type: ''});
|
|
expect(isAgentPost(postModel)).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('isPostRequester', () => {
|
|
const currentUserId = 'user123';
|
|
|
|
describe('with Post objects', () => {
|
|
it('returns true when llm_requester_user_id matches current user ID', () => {
|
|
const post = TestHelper.fakePost({
|
|
props: {
|
|
llm_requester_user_id: currentUserId,
|
|
},
|
|
});
|
|
expect(isPostRequester(post, currentUserId)).toBe(true);
|
|
});
|
|
|
|
it('returns false when llm_requester_user_id does not match', () => {
|
|
const post = TestHelper.fakePost({
|
|
props: {
|
|
llm_requester_user_id: 'different_user',
|
|
},
|
|
});
|
|
expect(isPostRequester(post, currentUserId)).toBe(false);
|
|
});
|
|
|
|
it('returns false when props is undefined', () => {
|
|
const post = TestHelper.fakePost({
|
|
props: undefined,
|
|
});
|
|
expect(isPostRequester(post, currentUserId)).toBe(false);
|
|
});
|
|
|
|
it('returns false when props is empty object', () => {
|
|
const post = TestHelper.fakePost({
|
|
props: {},
|
|
});
|
|
expect(isPostRequester(post, currentUserId)).toBe(false);
|
|
});
|
|
|
|
it('returns false when llm_requester_user_id is missing', () => {
|
|
const post = TestHelper.fakePost({
|
|
props: {
|
|
some_other_prop: 'value',
|
|
},
|
|
});
|
|
expect(isPostRequester(post, currentUserId)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('with PostModel objects', () => {
|
|
it('returns true when llm_requester_user_id matches current user ID', () => {
|
|
const postModel = TestHelper.fakePostModel({
|
|
props: {
|
|
llm_requester_user_id: currentUserId,
|
|
},
|
|
});
|
|
expect(isPostRequester(postModel, currentUserId)).toBe(true);
|
|
});
|
|
|
|
it('returns false when llm_requester_user_id does not match', () => {
|
|
const postModel = TestHelper.fakePostModel({
|
|
props: {
|
|
llm_requester_user_id: 'different_user',
|
|
},
|
|
});
|
|
expect(isPostRequester(postModel, currentUserId)).toBe(false);
|
|
});
|
|
|
|
it('returns false when props is empty object', () => {
|
|
const postModel = TestHelper.fakePostModel({
|
|
props: {},
|
|
});
|
|
expect(isPostRequester(postModel, currentUserId)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('handles exceptions gracefully and returns false', () => {
|
|
// Create a post object that throws when accessing props
|
|
const faultyPost = {
|
|
get props() {
|
|
throw new Error('Access denied');
|
|
},
|
|
} as any;
|
|
|
|
expect(isPostRequester(faultyPost, currentUserId)).toBe(false);
|
|
});
|
|
});
|
|
});
|