Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@

### Bug Fixes

* **i18n:** gate language-preference sync on edX JWT and mount inside AuthProvider ([a1829f9](https://github.com/iblai/os/commit/a1829f9db2b69428c2cb45d72bd4c209d5b0ffe7))
- **i18n:** gate language-preference sync on edX JWT and mount inside AuthProvider ([a1829f9](https://github.com/iblai/os/commit/a1829f9db2b69428c2cb45d72bd4c209d5b0ffe7))

## [0.89.0](https://github.com/iblai/os/compare/v0.88.3...v0.89.0) (2026-07-02)

### Features

* **tauri:** add allow_in_app_purchase command gated by build-time env ([1a1ddeb](https://github.com/iblai/os/commit/1a1ddebc5475ad80bb4cd19ea60d539248ac7701))
- **tauri:** add allow_in_app_purchase command gated by build-time env ([1a1ddeb](https://github.com/iblai/os/commit/1a1ddebc5475ad80bb4cd19ea60d539248ac7701))

### Tests

* **e2e:** wait for streaming before re-navigating in prompt-injection TC3 ([31b4edd](https://github.com/iblai/os/commit/31b4edd43968f25781982b6828ed6c06485000bc))
- **e2e:** wait for streaming before re-navigating in prompt-injection TC3 ([31b4edd](https://github.com/iblai/os/commit/31b4edd43968f25781982b6828ed6c06485000bc))

## [0.88.3](https://github.com/iblai/os/compare/v0.88.2...v0.88.3) (2026-07-01)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ let mockUsername: string | null = 'admin-user';
let mockIsLoggedIn = true;
let mockIsAdmin = true;
let mockUserIsStudent = false;
let mockTenantMetadata: Record<string, unknown> = {};
let mockCurrentTenant: any = {
is_admin: true,
is_advertising: false,
Expand Down Expand Up @@ -439,6 +440,7 @@ vi.mock('@iblai/iblai-js/web-utils', () => ({
selectStreaming: () => mockIsStreaming,
selectNumberOfActiveChatMessages: () => mockNumberOfActiveChatMessages,
selectActiveChatMessages: () => mockActiveChatMessages,
useTenantMetadata: () => ({ metadata: mockTenantMetadata }),
}));

vi.mock('@iblai/iblai-js/web-containers', () => ({
Expand Down Expand Up @@ -671,6 +673,7 @@ function resetState() {
mockIsLoggedIn = true;
mockIsAdmin = true;
mockUserIsStudent = false;
mockTenantMetadata = {};
mockCurrentTenant = {
is_admin: true,
is_advertising: false,
Expand Down Expand Up @@ -1348,6 +1351,51 @@ describe('AppSidebar — Chats section', () => {
).toBeInTheDocument();
});

it('hides Export but keeps Pin and Delete for a student when export is disabled', async () => {
mockUserIsStudent = true;
mockTenantMetadata = { enable_chat_history_export: false };
const user = userEvent.setup();
renderSidebar();
fireEvent.click(screen.getAllByRole('button', { name: 'Chats' })[0]);
const menus = screen.getAllByRole('button', { name: 'Chat actions' });
await user.click(menus[1]);
expect(
await screen.findByRole('menuitem', { name: /^Pin$/ }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: /^Delete$/ }),
).toBeInTheDocument();
expect(
screen.queryByRole('menuitem', { name: /^Export$/ }),
).not.toBeInTheDocument();
});

it('shows Export for a student when the export setting is absent (default on)', async () => {
mockUserIsStudent = true;
mockTenantMetadata = {};
const user = userEvent.setup();
renderSidebar();
fireEvent.click(screen.getAllByRole('button', { name: 'Chats' })[0]);
const menus = screen.getAllByRole('button', { name: 'Chat actions' });
await user.click(menus[1]);
expect(
await screen.findByRole('menuitem', { name: /^Export$/ }),
).toBeInTheDocument();
});

it('shows Export for a non-student even when the export setting is disabled', async () => {
mockUserIsStudent = false;
mockTenantMetadata = { enable_chat_history_export: false };
const user = userEvent.setup();
renderSidebar();
fireEvent.click(screen.getAllByRole('button', { name: 'Chats' })[0]);
const menus = screen.getAllByRole('button', { name: 'Chat actions' });
await user.click(menus[1]);
expect(
await screen.findByRole('menuitem', { name: /^Export$/ }),
).toBeInTheDocument();
});

it("shows Unpin (not Pin) for a pinned row's menu", async () => {
const user = userEvent.setup();
renderSidebar();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
selectNumberOfActiveChatMessages,
selectSessionId,
selectStreaming,
useTenantMetadata,
} from '@iblai/iblai-js/web-utils';
import {
Admin,
Expand Down Expand Up @@ -996,12 +997,14 @@ function chatRowLabel(row: ChatRow, noContentLabel: string): React.ReactNode {
function ChatThreeDotMenu({
isPinned,
isLoading,
canExport = true,
onPinToggle,
onExport,
onDelete,
}: {
isPinned: boolean;
isLoading: boolean;
canExport?: boolean;
onPinToggle: () => void;
onExport: () => void;
onDelete: () => void;
Expand Down Expand Up @@ -1049,14 +1052,16 @@ function ChatThreeDotMenu({
)}
{isPinned ? t('unpin') : t('pin')}
</DropdownMenuItem>
<DropdownMenuItem className="gap-2" onSelect={onExport}>
<Download
className="size-3.5 shrink-0"
strokeWidth={1.5}
aria-hidden
/>
{t('export')}
</DropdownMenuItem>
{canExport && (
<DropdownMenuItem className="gap-2" onSelect={onExport}>
<Download
className="size-3.5 shrink-0"
strokeWidth={1.5}
aria-hidden
/>
{t('export')}
</DropdownMenuItem>
)}
<DropdownMenuItem
className="gap-2 text-red-600 focus:text-red-700"
onSelect={onDelete}
Expand All @@ -1075,6 +1080,7 @@ function ChatRowItem({
onSelect,
isPinned,
isLoading,
canExport = true,
onPinToggle,
onExport,
onDelete,
Expand All @@ -1084,6 +1090,7 @@ function ChatRowItem({
onSelect: () => void;
isPinned: boolean;
isLoading: boolean;
canExport?: boolean;
onPinToggle: () => void;
onExport: () => void;
onDelete: () => void;
Expand All @@ -1109,6 +1116,7 @@ function ChatRowItem({
<ChatThreeDotMenu
isPinned={isPinned}
isLoading={isLoading}
canExport={canExport}
onPinToggle={onPinToggle}
onExport={onExport}
onDelete={onDelete}
Expand Down Expand Up @@ -1140,6 +1148,10 @@ function SidebarChatsSection({
const { onAfterNav } = useSidebarNavCallback();
const t = useTranslations('appSidebarIndex');
const appSessionId = useAppSelector(selectSessionId);
const userIsStudent = useUserIsStudent();
const { metadata } = useTenantMetadata({ org: tenantKey });
const canExport =
!userIsStudent || metadata?.enable_chat_history_export !== false;
const resolvedUserId = username ?? getUserName();
// The message-loader effect in `useAdvancedChat` keys EXCLUSIVELY on
// `cachedSessionId[mentorId]` (backed by localStorage `session_id`). Row
Expand Down Expand Up @@ -1478,6 +1490,7 @@ function SidebarChatsSection({
onSelect={() => handleSelectRow(row)}
isPinned={kind === 'pinned'}
isLoading={actingSessionId === row.session_id}
canExport={canExport}
onPinToggle={() =>
kind === 'pinned' ? handleUnpin(row) : handlePin(row)
}
Expand Down
16 changes: 15 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-06-30 | 499 checkpoints (477 covered, 2 pending/fixme, 8 not-reproducible in default env, 12 deprecated) | 57 journeys (56 active, 1 deprecated in #1431) | 100% covered | Auth: admin + non-admin storageState
> Last updated: 2026-07-03 | 504 checkpoints (482 covered, 2 pending/fixme, 8 not-reproducible in default env, 12 deprecated) | 58 journeys (57 active, 1 deprecated in #1431) | 100% covered | Auth: admin + non-admin storageState

## How This Works

Expand Down Expand Up @@ -979,3 +979,17 @@ End-to-end onboarding of a brand-new user via the `ECOMMERCE_CHECKOUT_URL` "free
- [x] fcc-04: Clicking "Upgrade Plan" in the credit dropdown redirects to a Stripe-hosted checkout page (`store.ibl.ai`) requiring credit-card input (secure Stripe Elements card iframe + pay button)

---

## Journey 56: Chat History Export Toggle (5 checkpoints) — `journeys/56-chat-history-export-toggle.spec.ts`

**Source files:** `app/platform/[tenantKey]/[mentorId]/_components/app-sidebar/index.tsx`

Covers issue #2068: a new tenant Advanced setting, `enable_chat_history_export` (org metadata boolean, default ON, rendered by the SDK's generic `AdvancedTab` metadata-switch list, label "Chat History Export"), gates the "Export" item in the sidebar Chats three-dot menu (`aria-label="Chat actions"`) COMBINED with the acting user's role: `canExport = !userIsStudent || metadata?.enable_chat_history_export !== false`. Non-students (admin/instructor) always see Export regardless of the setting; students only see it when the setting is ON or absent. "Student" is toggled via the same admin session using the nav-bar User/Admin `LearnerModeSwitch` (`aria-label` starting with "User mode") — the same technique already used by journey 42's "Non-Admin" describe block — rather than a separately-authenticated account, so the seeded chat stays visible to the acting session. Each test creates its own mentor via `createMentorPage.openAndCreate()` and restores both the tenant setting and the learner-mode toggle in a `finally` block so tests remain order-independent.

- [x] chexp-01: Tenant Advanced tab renders a "Chat History Export" switch, ON by default
- [x] chexp-02: Toggling the switch OFF persists across closing and reopening the Advanced tab (PATCH org metadata round trip)
- [x] chexp-03: Setting OFF + student (admin in User/Learner mode) — Export is hidden from the chat row menu; Pin and Delete remain visible
- [x] chexp-04: Setting ON + student — Export, Pin, and Delete are all visible in the chat row menu
- [x] chexp-05: Setting OFF + non-student (admin/instructor mode) — Export is still visible (role wins over the tenant setting)

---
45 changes: 40 additions & 5 deletions e2e/coverage.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"version": 2,
"lastUpdated": "2026-06-30",
"lastUpdated": "2026-07-03",
"summary": {
"totalCheckpoints": 499,
"coveredCheckpoints": 477,
"totalCheckpoints": 504,
"coveredCheckpoints": 482,
"deprecatedCheckpoints": 12,
"notReproducibleCheckpoints": 8,
"percent": 100,
"totalJourneys": 57,
"activeJourneys": 56
"totalJourneys": 58,
"activeJourneys": 57
},
"journeys": [
{
Expand Down Expand Up @@ -3210,6 +3210,41 @@
"status": "covered"
}
]
},
{
"id": "chat-history-export-toggle",
"name": "Chat History Export Toggle",
"spec": "56-chat-history-export-toggle.spec.ts",
"sourceFiles": [
"app/platform/[tenantKey]/[mentorId]/_components/app-sidebar/index.tsx"
],
"checkpoints": [
{
"id": "chexp-01",
"description": "Tenant Advanced tab renders a 'Chat History Export' switch, ON by default (SDK-driven org-metadata boolean, enable_chat_history_export)",
"status": "covered"
},
{
"id": "chexp-02",
"description": "Toggling the 'Chat History Export' switch OFF in the Advanced tab persists across closing and reopening the dialog (PATCH org metadata round trip)",
"status": "covered"
},
{
"id": "chexp-03",
"description": "With the setting OFF, a student (admin toggled into User/Learner mode) does not see Export in a chat row's three-dot menu, but still sees Pin and Delete",
"status": "covered"
},
{
"id": "chexp-04",
"description": "With the setting ON, a student (admin toggled into User/Learner mode) sees Export, Pin, and Delete in a chat row's three-dot menu",
"status": "covered"
},
{
"id": "chexp-05",
"description": "With the setting OFF, a non-student (admin/instructor mode) still sees Export in a chat row's three-dot menu — role wins over the tenant setting",
"status": "covered"
}
]
}
]
}
Loading
Loading