Skip to content

Commit b39b7d7

Browse files
halskclaude
andauthored
[Phase 1] Enhance Integration Context for Channel Selection (#184)
* [Phase 1] Enhance Integration Context for Channel Selection - Updated IntegrationContext.tsx with new state for selected channels - Added methods for selecting/deselecting channels for analysis - Added method for saving channel selection to the backend - Added typings for channel selection operations - Added proper error handling for selection operations - Added comprehensive test coverage for new functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Fix formatting and IntegrationList test mock - Fixed formatting issues in modified files - Updated IntegrationList.test.tsx mock context with new channel selection properties - Ran CI compatibility checks to ensure all tests pass 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 3230f9d commit b39b7d7

File tree

4 files changed

+704
-0
lines changed

4 files changed

+704
-0
lines changed

frontend/src/__tests__/components/integration/IntegrationList.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ const mockIntegrationContext = {
6565
teamIntegrations: { 'team-1': mockIntegrations },
6666
currentIntegration: null,
6767
currentResources: [],
68+
selectedChannels: [],
6869
loading: false,
6970
loadingResources: false,
71+
loadingChannelSelection: false,
7072
error: null,
7173
resourceError: null,
74+
channelSelectionError: null,
7275
fetchIntegrations: vi.fn().mockResolvedValue(undefined),
7376
fetchIntegration: vi.fn(),
7477
createIntegration: vi.fn(),
@@ -80,7 +83,13 @@ const mockIntegrationContext = {
8083
revokeShare: vi.fn(),
8184
grantResourceAccess: vi.fn(),
8285
selectIntegration: vi.fn(),
86+
fetchSelectedChannels: vi.fn(),
87+
selectChannelsForAnalysis: vi.fn(),
88+
deselectChannelsForAnalysis: vi.fn(),
89+
isChannelSelectedForAnalysis: vi.fn(),
90+
analyzeChannel: vi.fn(),
8391
clearErrors: vi.fn(),
92+
clearChannelSelectionError: vi.fn(),
8493
}
8594

8695
describe('IntegrationList', () => {

frontend/src/__tests__/context/IntegrationContext.test.tsx

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ vi.mock('../../lib/integrationService', () => ({
3434
revokeShare: vi.fn(),
3535
grantResourceAccess: vi.fn(),
3636
isApiError: vi.fn(),
37+
getSelectedChannels: vi.fn(),
38+
selectChannelsForAnalysis: vi.fn(),
39+
analyzeChannel: vi.fn(),
3740
},
3841
IntegrationType: {
3942
SLACK: 'slack',
@@ -100,6 +103,32 @@ const mockResource = {
100103
updated_at: '2023-01-01T00:00:00Z',
101104
}
102105

106+
const mockSelectedChannels = [
107+
{
108+
id: 'res-1',
109+
integration_id: 'test-int-1',
110+
resource_type: ResourceType.SLACK_CHANNEL,
111+
external_id: 'C12345',
112+
name: 'general',
113+
created_at: '2023-01-01T00:00:00Z',
114+
updated_at: '2023-01-01T00:00:00Z',
115+
},
116+
{
117+
id: 'res-2',
118+
integration_id: 'test-int-1',
119+
resource_type: ResourceType.SLACK_CHANNEL,
120+
external_id: 'C67890',
121+
name: 'random',
122+
created_at: '2023-01-01T00:00:00Z',
123+
updated_at: '2023-01-01T00:00:00Z',
124+
},
125+
]
126+
127+
const mockAnalysisResult = {
128+
status: 'success',
129+
analysis_id: 'analysis-123',
130+
}
131+
103132
// Helper component for testing hooks
104133
const TestHookComponent = ({ callback }: { callback: () => void }) => {
105134
callback()
@@ -184,6 +213,18 @@ describe('IntegrationContext', () => {
184213
message: 'Resources synced',
185214
})
186215

216+
// Setup channel selection mocks
217+
vi.mocked(integrationService.getSelectedChannels).mockResolvedValue(
218+
mockSelectedChannels
219+
)
220+
vi.mocked(integrationService.selectChannelsForAnalysis).mockResolvedValue({
221+
status: 'success',
222+
message: 'Channels selected for analysis',
223+
})
224+
vi.mocked(integrationService.analyzeChannel).mockResolvedValue(
225+
mockAnalysisResult
226+
)
227+
187228
// Mock share response
188229
const mockShare = {
189230
id: 'share-1',
@@ -468,6 +509,290 @@ describe('IntegrationContext', () => {
468509
expect(hookResult?.error).toBeNull()
469510
})
470511

512+
it('should fetch selected channels when requested', async () => {
513+
let hookResult: ReturnType<typeof useIntegration> | undefined
514+
515+
await act(async () => {
516+
render(
517+
<MockAuthProvider>
518+
<IntegrationProvider>
519+
<TestHookComponent
520+
callback={() => {
521+
hookResult = useIntegration()
522+
}}
523+
/>
524+
</IntegrationProvider>
525+
</MockAuthProvider>
526+
)
527+
})
528+
529+
// Verify we have the function
530+
expect(hookResult).toBeDefined()
531+
expect(hookResult?.fetchSelectedChannels).toBeDefined()
532+
533+
// Call the function
534+
await act(async () => {
535+
await hookResult?.fetchSelectedChannels('test-int-1')
536+
})
537+
538+
// Check that the service was called correctly
539+
expect(integrationService.getSelectedChannels).toHaveBeenCalledWith(
540+
'test-int-1'
541+
)
542+
543+
// Verify the state was updated
544+
expect(hookResult?.selectedChannels.length).toBe(2)
545+
expect(hookResult?.selectedChannels[0].id).toBe('res-1')
546+
expect(hookResult?.selectedChannels[1].id).toBe('res-2')
547+
expect(hookResult?.loadingChannelSelection).toBe(false)
548+
})
549+
550+
it('should select channels for analysis', async () => {
551+
let hookResult: ReturnType<typeof useIntegration> | undefined
552+
553+
await act(async () => {
554+
render(
555+
<MockAuthProvider>
556+
<IntegrationProvider>
557+
<TestHookComponent
558+
callback={() => {
559+
hookResult = useIntegration()
560+
}}
561+
/>
562+
</IntegrationProvider>
563+
</MockAuthProvider>
564+
)
565+
})
566+
567+
// Verify we have the function
568+
expect(hookResult).toBeDefined()
569+
expect(hookResult?.selectChannelsForAnalysis).toBeDefined()
570+
571+
// Call the function
572+
let result: boolean | undefined
573+
await act(async () => {
574+
result = await hookResult?.selectChannelsForAnalysis('test-int-1', [
575+
'res-1',
576+
'res-2',
577+
])
578+
})
579+
580+
// Check that the service was called correctly
581+
expect(integrationService.selectChannelsForAnalysis).toHaveBeenCalledWith(
582+
'test-int-1',
583+
{ channel_ids: ['res-1', 'res-2'], for_analysis: true }
584+
)
585+
586+
// Should also refresh the selected channels
587+
expect(integrationService.getSelectedChannels).toHaveBeenCalledWith(
588+
'test-int-1'
589+
)
590+
591+
// Verify the state was updated and the result is correct
592+
expect(result).toBe(true)
593+
expect(hookResult?.loadingChannelSelection).toBe(false)
594+
})
595+
596+
it('should deselect channels for analysis', async () => {
597+
let hookResult: ReturnType<typeof useIntegration> | undefined
598+
599+
await act(async () => {
600+
render(
601+
<MockAuthProvider>
602+
<IntegrationProvider>
603+
<TestHookComponent
604+
callback={() => {
605+
hookResult = useIntegration()
606+
}}
607+
/>
608+
</IntegrationProvider>
609+
</MockAuthProvider>
610+
)
611+
})
612+
613+
// Verify we have the function
614+
expect(hookResult).toBeDefined()
615+
expect(hookResult?.deselectChannelsForAnalysis).toBeDefined()
616+
617+
// Call the function
618+
let result: boolean | undefined
619+
await act(async () => {
620+
result = await hookResult?.deselectChannelsForAnalysis('test-int-1', [
621+
'res-2',
622+
])
623+
})
624+
625+
// Check that the service was called correctly
626+
expect(integrationService.selectChannelsForAnalysis).toHaveBeenCalledWith(
627+
'test-int-1',
628+
{ channel_ids: ['res-2'], for_analysis: false }
629+
)
630+
631+
// Should also refresh the selected channels
632+
expect(integrationService.getSelectedChannels).toHaveBeenCalledWith(
633+
'test-int-1'
634+
)
635+
636+
// Verify the result is correct
637+
expect(result).toBe(true)
638+
})
639+
640+
it('should check if a channel is selected for analysis', async () => {
641+
let hookResult: ReturnType<typeof useIntegration> | undefined
642+
643+
await act(async () => {
644+
render(
645+
<MockAuthProvider>
646+
<IntegrationProvider>
647+
<TestHookComponent
648+
callback={() => {
649+
hookResult = useIntegration()
650+
}}
651+
/>
652+
</IntegrationProvider>
653+
</MockAuthProvider>
654+
)
655+
})
656+
657+
// First fetch selected channels to populate state
658+
await act(async () => {
659+
await hookResult?.fetchSelectedChannels('test-int-1')
660+
})
661+
662+
// Verify we have the function
663+
expect(hookResult).toBeDefined()
664+
expect(hookResult?.isChannelSelectedForAnalysis).toBeDefined()
665+
666+
// Check for known selected channel
667+
expect(hookResult?.isChannelSelectedForAnalysis('res-1')).toBe(true)
668+
expect(hookResult?.isChannelSelectedForAnalysis('C12345')).toBe(true) // Should work with external_id too
669+
670+
// Check for non-selected channel
671+
expect(hookResult?.isChannelSelectedForAnalysis('non-existent')).toBe(
672+
false
673+
)
674+
})
675+
676+
it('should run analysis on a channel', async () => {
677+
let hookResult: ReturnType<typeof useIntegration> | undefined
678+
679+
await act(async () => {
680+
render(
681+
<MockAuthProvider>
682+
<IntegrationProvider>
683+
<TestHookComponent
684+
callback={() => {
685+
hookResult = useIntegration()
686+
}}
687+
/>
688+
</IntegrationProvider>
689+
</MockAuthProvider>
690+
)
691+
})
692+
693+
// Verify we have the function
694+
expect(hookResult).toBeDefined()
695+
expect(hookResult?.analyzeChannel).toBeDefined()
696+
697+
// Call the function with analysis options
698+
let result: { status: string; analysis_id: string } | null | undefined
699+
const options = {
700+
start_date: '2023-01-01',
701+
end_date: '2023-01-31',
702+
include_threads: true,
703+
include_reactions: false,
704+
}
705+
706+
await act(async () => {
707+
result = await hookResult?.analyzeChannel(
708+
'test-int-1',
709+
'res-1',
710+
options
711+
)
712+
})
713+
714+
// Check that the service was called correctly
715+
expect(integrationService.analyzeChannel).toHaveBeenCalledWith(
716+
'test-int-1',
717+
'res-1',
718+
options
719+
)
720+
721+
// Verify the result contains the expected data
722+
expect(result).toBeDefined()
723+
expect(result?.status).toBe('success')
724+
expect(result?.analysis_id).toBe('analysis-123')
725+
})
726+
727+
it('should handle errors when selecting channels', async () => {
728+
// Save original implementations to restore after the test
729+
const originalSelectChannelsForAnalysis = vi
730+
.mocked(integrationService.selectChannelsForAnalysis)
731+
.getMockImplementation()
732+
const originalIsApiError = vi
733+
.mocked(integrationService.isApiError)
734+
.getMockImplementation()
735+
736+
// Mock the service to return an error
737+
vi.mocked(
738+
integrationService.selectChannelsForAnalysis
739+
).mockResolvedValueOnce({
740+
status: 400,
741+
message: 'Failed to select channels',
742+
})
743+
744+
// Mock isApiError to identify this as an error
745+
vi.mocked(integrationService.isApiError).mockImplementation(() => true)
746+
747+
let hookResult: ReturnType<typeof useIntegration> | undefined
748+
749+
await act(async () => {
750+
render(
751+
<MockAuthProvider>
752+
<IntegrationProvider>
753+
<TestHookComponent
754+
callback={() => {
755+
hookResult = useIntegration()
756+
}}
757+
/>
758+
</IntegrationProvider>
759+
</MockAuthProvider>
760+
)
761+
})
762+
763+
// Call the function that should fail
764+
let result: boolean | undefined
765+
await act(async () => {
766+
result = await hookResult?.selectChannelsForAnalysis('test-int-1', [
767+
'res-1',
768+
])
769+
})
770+
771+
// Verify the result and error state
772+
expect(result).toBe(false)
773+
expect(hookResult?.channelSelectionError).toBeDefined()
774+
expect(hookResult?.loadingChannelSelection).toBe(false)
775+
776+
// Test clearing the error
777+
await act(async () => {
778+
hookResult?.clearChannelSelectionError()
779+
})
780+
781+
expect(hookResult?.channelSelectionError).toBeNull()
782+
783+
// Restore original implementations
784+
if (originalSelectChannelsForAnalysis) {
785+
vi.mocked(
786+
integrationService.selectChannelsForAnalysis
787+
).mockImplementation(originalSelectChannelsForAnalysis)
788+
}
789+
if (originalIsApiError) {
790+
vi.mocked(integrationService.isApiError).mockImplementation(
791+
originalIsApiError
792+
)
793+
}
794+
})
795+
471796
// Add timeout to the full test suite to prevent hanging
472797
vi.setConfig({
473798
testTimeout: 5000, // 5 second timeout

0 commit comments

Comments
 (0)