Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,6 @@ const buildContext = (
isMemsearchEnabled: true,
isMemoryComponentEnabled: true,
isClawEnabled: false,
clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
Expand Down Expand Up @@ -1317,7 +1316,6 @@ describe('NavBar - Menu Filtering Logic (filterMentorSegments)', () => {
isMemsearchEnabled: false,
isMemoryComponentEnabled: true,
isClawEnabled: false,
clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
Expand All @@ -1340,7 +1338,6 @@ describe('NavBar - Menu Filtering Logic (filterMentorSegments)', () => {
isMemsearchEnabled: true,
isMemoryComponentEnabled: true,
isClawEnabled: false,
clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
Expand All @@ -1363,7 +1360,6 @@ describe('NavBar - Menu Filtering Logic (filterMentorSegments)', () => {
isMemsearchEnabled: true,
isMemoryComponentEnabled: true,
isClawEnabled: false,
clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
Expand All @@ -1382,7 +1378,6 @@ describe('NavBar - Menu Filtering Logic (filterMentorSegments)', () => {
isMemsearchEnabled: false,
isMemoryComponentEnabled: true,
isClawEnabled: false,
clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ vi.mock('../tabs', () => ({
ScreenShareTab: () => (
<div data-testid="screenshare-tab">Screen Share Tab</div>
),
HumanSupportTab: () => (
<div data-testid="human-support-tab">Human Support Tab</div>
),
LtiTab: () => <div data-testid="lti-tab">LTI Tab</div>,
AnalyticsTab: () => <div data-testid="analytics-tab">Analytics Tab</div>,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ vi.mock('./tabs', () => ({
ScreenShareTab: () => (
<div data-testid="screenshare-tab">Screen Share Tab</div>
),
HumanSupportTab: () => (
<div data-testid="human-support-tab">Human Support Tab</div>
),
LtiTab: () => <div data-testid="lti-tab">LTI Tab</div>,
AnalyticsTab: () => <div data-testid="analytics-tab">Analytics Tab</div>,
}));
Expand Down
2 changes: 2 additions & 0 deletions components/modals/edit-mentor-modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
AuditLogTab,
VoiceTab,
ScreenShareTab,
HumanSupportTab,
LtiTab,
AnalyticsTab,
} from './tabs';
Expand Down Expand Up @@ -78,6 +79,7 @@ export const EDIT_MENTOR_TAB_COMPONENTS: Record<string, ReactNode> = {
[MODALS.EDIT_MENTOR.tabs.mcp]: <McpTab />,
[MODALS.EDIT_MENTOR.tabs.memory]: <MemoryTab />,
[MODALS.EDIT_MENTOR.tabs.history]: <HistoryTab />,
[MODALS.EDIT_MENTOR.tabs.human_support]: <HumanSupportTab />,
[MODALS.EDIT_MENTOR.tabs.audit_log]: <AuditLogTab />,
[MODALS.EDIT_MENTOR.tabs.datasets]: <DatasetsTab />,
[MODALS.EDIT_MENTOR.tabs.evaluation]: <EvaluationTab />,
Expand Down
198 changes: 198 additions & 0 deletions components/modals/edit-mentor-modal/tabs/human-support-tab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';

import { HumanSupportTab } from './human-support-tab';

// ============================================================================
// MOCKS
// ============================================================================

const mockUseParams = vi.fn();
const mockGetMentorId = vi.fn();
const mockUseUsername = vi.fn();
const mockEnableRBAC = vi.fn();
const mockAgentSettingsProvider = vi.fn();
const mockAgentHumanSupportTab = vi.fn();

vi.mock('next/navigation', () => ({
useParams: () => mockUseParams(),
}));

vi.mock('@/hooks/user-navigate', () => ({
useNavigate: () => ({
getMentorId: mockGetMentorId,
}),
}));

vi.mock('@/hooks/use-user', () => ({
useUsername: () => mockUseUsername(),
}));

vi.mock('@/lib/config', () => ({
config: {
enableRBAC: () => mockEnableRBAC(),
},
}));

// HumanSupportTab imports from `@iblai/iblai-js/web-containers/next` (the
// Next-only entry — that's where AgentHumanSupportTab / AgentSettingsProvider
// are actually exported). Vitest keys mocks by module specifier, so we mock
// the exact path the source uses.
vi.mock('@iblai/iblai-js/web-containers/next', () => ({
AgentSettingsProvider: ({
children,
...value
}: {
children: React.ReactNode;
tenantKey: string;
mentorId: string;
username: string;
enableRBAC: boolean;
}) => {
mockAgentSettingsProvider(value);
return (
<div
data-testid="agent-settings-provider"
data-tenant-key={value.tenantKey}
data-mentor-id={value.mentorId}
data-username={value.username}
data-enable-rbac={String(value.enableRBAC)}
>
{children}
</div>
);
},
AgentHumanSupportTab: (props: unknown) => {
mockAgentHumanSupportTab(props);
return (
<div data-testid="agent-human-support-tab">AgentHumanSupportTab</div>
);
},
}));

// ============================================================================
// TESTS
// ============================================================================

describe('HumanSupportTab', () => {
beforeEach(() => {
cleanup();
vi.clearAllMocks();

mockUseParams.mockReturnValue({
tenantKey: 'test-tenant',
mentorId: 'test-mentor',
});
mockGetMentorId.mockReturnValue(null);
mockUseUsername.mockReturnValue('test-user');
mockEnableRBAC.mockReturnValue(false);
});

afterEach(() => {
cleanup();
});

describe('Rendering', () => {
it('wraps AgentHumanSupportTab in an AgentSettingsProvider with the resolved identity', () => {
render(<HumanSupportTab />);

const provider = screen.getByTestId('agent-settings-provider');
expect(provider).toHaveAttribute('data-tenant-key', 'test-tenant');
expect(provider).toHaveAttribute('data-mentor-id', 'test-mentor');
expect(provider).toHaveAttribute('data-username', 'test-user');
expect(provider).toHaveAttribute('data-enable-rbac', 'false');

expect(screen.getByTestId('agent-human-support-tab')).toBeInTheDocument();
expect(mockAgentSettingsProvider).toHaveBeenCalledWith({
tenantKey: 'test-tenant',
mentorId: 'test-mentor',
username: 'test-user',
enableRBAC: false,
});
});

it('does not pass a labels override so the SDK i18n catalog stays authoritative', () => {
render(<HumanSupportTab />);

expect(mockAgentHumanSupportTab).toHaveBeenCalledWith(
expect.not.objectContaining({ labels: expect.anything() }),
);
});

it('forwards the config-derived enableRBAC flag to the provider', () => {
mockEnableRBAC.mockReturnValue(true);

render(<HumanSupportTab />);

expect(screen.getByTestId('agent-settings-provider')).toHaveAttribute(
'data-enable-rbac',
'true',
);
expect(mockAgentSettingsProvider).toHaveBeenCalledWith(
expect.objectContaining({ enableRBAC: true }),
);
});
});

describe('Active mentor id resolution', () => {
it('prefers getMentorId() from navigate hook when provided', () => {
mockGetMentorId.mockReturnValue('nav-mentor-xyz');

render(<HumanSupportTab />);

expect(mockAgentSettingsProvider).toHaveBeenCalledWith(
expect.objectContaining({ mentorId: 'nav-mentor-xyz' }),
);
});

it('falls back to params.mentorId when getMentorId() returns null', () => {
mockGetMentorId.mockReturnValue(null);

render(<HumanSupportTab />);

expect(mockAgentSettingsProvider).toHaveBeenCalledWith(
expect.objectContaining({ mentorId: 'test-mentor' }),
);
});
});

describe('Guard clauses', () => {
it('renders nothing when tenantKey is missing', () => {
mockUseParams.mockReturnValue({
tenantKey: undefined,
mentorId: 'test-mentor',
});

const { container } = render(<HumanSupportTab />);

expect(container.firstChild).toBeNull();
expect(mockAgentSettingsProvider).not.toHaveBeenCalled();
expect(mockAgentHumanSupportTab).not.toHaveBeenCalled();
});

it('renders nothing when both mentorId and getMentorId() are missing', () => {
mockUseParams.mockReturnValue({
tenantKey: 'test-tenant',
mentorId: undefined,
});
mockGetMentorId.mockReturnValue(null);

const { container } = render(<HumanSupportTab />);

expect(container.firstChild).toBeNull();
expect(mockAgentSettingsProvider).not.toHaveBeenCalled();
expect(mockAgentHumanSupportTab).not.toHaveBeenCalled();
});

it('renders nothing when username is missing', () => {
mockUseUsername.mockReturnValue(undefined);

const { container } = render(<HumanSupportTab />);

expect(container.firstChild).toBeNull();
expect(mockAgentSettingsProvider).not.toHaveBeenCalled();
expect(mockAgentHumanSupportTab).not.toHaveBeenCalled();
});
});
});
37 changes: 37 additions & 0 deletions components/modals/edit-mentor-modal/tabs/human-support-tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import { useParams } from 'next/navigation';
import {
AgentHumanSupportTab,
AgentSettingsProvider,
} from '@iblai/iblai-js/web-containers/next';

import { useNavigate } from '@/hooks/user-navigate';
import { useUsername } from '@/hooks/use-user';
import { config } from '@/lib/config';
import { TenantKeyMentorIdParams } from '@/lib/types';

export function HumanSupportTab() {
const { tenantKey, mentorId } = useParams<TenantKeyMentorIdParams>();
const { getMentorId } = useNavigate();
const username = useUsername();
const activeMentorId = getMentorId() ?? mentorId;

if (!tenantKey || !activeMentorId || !username) return null;

// AgentHumanSupportTab reads tenant/mentor/username from the nearest
// AgentSettingsProvider (like AgentTasksTab), so the provider is required
// here. No `labels` override is passed — the SDK resolves every string
// through its own i18n catalog, which follows the app locale via the
// WebContainersI18nProvider bridge, so all copy stays translated.
return (
<AgentSettingsProvider
tenantKey={tenantKey}
mentorId={activeMentorId}
username={username}
enableRBAC={config.enableRBAC()}
>
<AgentHumanSupportTab />
</AgentSettingsProvider>
);
}
1 change: 1 addition & 0 deletions components/modals/edit-mentor-modal/tabs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export * from './skills-tab';
export * from './audit-log-tab';
export * from './voice-tab';
export * from './screenshare-tab';
export * from './human-support-tab';
export * from './lti-tab';
export * from './analytics-tab';
31 changes: 30 additions & 1 deletion e2e/COVERAGE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MentorAI E2E Coverage — User Journey Checklist

> Last updated: 2026-07-23 | 586 checkpoints (559 covered, 7 pending/fixme, 8 not-reproducible in default env, 12 deprecated) | 66 journeys (65 active, 1 deprecated in #1431) | 100% covered | Auth: admin + non-admin storageState
> Last updated: 2026-07-25 | 594 checkpoints (567 covered, 7 pending/fixme, 8 not-reproducible in default env, 12 deprecated) | 67 journeys (66 active, 1 deprecated in #1431) | 100% covered | Auth: admin + non-admin storageState

## How This Works

Expand Down Expand Up @@ -1203,3 +1203,32 @@ path.
- [x] shl-01: Non-admin user denied RBAC #chat permission opens the mentor chat with a VALID shareable-link token — the textarea is enabled (no RBAC denial placeholder), and the user can send a message and receive a response
- [x] shl-02: Safety-boundary guard — the same non-admin user opens the same mentor chat with an INVALID/never-issued shareable-link token — the backend never creates a session, so the textarea stays disabled exactly as with no token (token presence alone must not unlock chat)
- [x] shl-03: Regression guard — the same non-admin user opens the same mentor chat with NO token — the textarea stays disabled and the RBAC denial placeholder ("Sorry about that! You don't have permission to chat.") is shown

## Journey 65: Mentor Human Support Tab (8 checkpoints) — `journeys/65-mentor-human-support-tab.spec.ts`

**Source files:** `components/modals/edit-mentor-modal/tabs/human-support-tab.tsx`, `e2e/page-objects/edit-mentor/human-support.tab.ts`

The Support tab (host sidebar label "Support"; panel value `human_support`,
Edit Agent → Runtime) is the SDK's `AgentHumanSupportTab` admin ticket inbox.
Tickets are filed by the AGENT during chat via the human-support tool, so the
lifecycle checkpoints drive the real chat surface. All DOM access flows
through the SDK's `human-support-tab-helpers` (`@iblai/iblai-js/playwright`).
Hard-won sequencing facts encoded in the spec: (1) the chat session snapshots
the mentor's tool list at session creation, and a page reload RESTORES the
same session — so the spec starts a NEW chat after enabling the support
toggle, otherwise the model has no support tool and hallucinates success via
`write_todos`; (2) the SDK's status-filter-bounce refresh only refetches once
(RTK Query cache), so the list poll refreshes via full reload + modal
re-open; (3) the creation prompt forbids canvas/document output — otherwise
the model streams a canvas artifact and the stop-streaming wait times out.
Destructive (mentor settings + tickets): file-level serial with a dedicated
per-test mentor (Journey 47 pattern), tracked and deleted in `afterAll`.

- [x] support-01: Admin opens Edit Agent → Runtime → Support on a fresh dedicated mentor — the tab renders its header and either the ticket list or the "No support tickets found" empty state (render/API contract)
- [x] support-02: Admin round-trips the status filter through Open / In Progress / Closed / All Statuses — each change re-queries and the pane returns to a stable list-or-empty state
- [x] support-03: Support availability toggle — enable the human-support tool from the tab's info box and verify it persists (round-trip read after reload); skipped when the tenant tool catalog has no human-support tool
- [x] support-04: Full agent-chat creation cycle — after enabling the tool and starting a NEW chat session, the admin asks the agent (plain chat, canvas forbidden) to file a ticket with a unique subject; the ticket appears in the Support list with status Open for the same mentor the message was sent to
- [x] support-05: Opening the created ticket shows the detail pane with non-empty agent-authored description content
- [x] support-06: Admin sends a reply from the detail-pane composer — the message appears in the ticket's Conversation section
- [x] support-07: Admin moves the ticket to In Progress via the detail Status select — the list row's status badge updates
- [x] support-08: Admin closes the ticket — the closed notice replaces the reply composer and the list badge shows Closed
Loading
Loading