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
58 changes: 29 additions & 29 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,43 @@

### Features

* **mentor:** sidebar, agent settings layout changes ([3bc2762](https://github.com/iblai/os/commit/3bc276244face45d4dd5db2668c4a4bbba3a20c6))

### Bug Fixes

* **e2e:** fix for the e2e tests ([3733d96](https://github.com/iblai/os/commit/3733d967e2c3093a4e8df10041cb42ba8dc6fa05))
* **e2e:** fix the explore page test failiures ([ecf8a27](https://github.com/iblai/os/commit/ecf8a27a7711b08326aac17afe58daebbd39a55c))
* **e2e:** fixing coverage.json for missing components ([c63ca0e](https://github.com/iblai/os/commit/c63ca0eb7794fbe57b9e28d518e726f045f4757e))
* **e2e:** fixing for the e2e tests ([d44cc81](https://github.com/iblai/os/commit/d44cc8160fa2e7e86b1884a9e797d4c5d7555dbf))
* **e2e:** fixing for the e2e tests ([08965fd](https://github.com/iblai/os/commit/08965fd81c7dcd2782e438df218c972f03fa5309))
* **e2e:** more fixes for the tests ([8a40c32](https://github.com/iblai/os/commit/8a40c321f77c2ba446c92fa5a96459ca1d7f0e0a))
* **e2e:** more fixes for the tests ([3104d2e](https://github.com/iblai/os/commit/3104d2e352201a8281421b708dc51c703bcce161))
* **mentor:** adding app-sidebar test ([0e5f654](https://github.com/iblai/os/commit/0e5f65411086e8bc433dc0d04983e6dbb76a72d8))
* **mentor:** adding app-sidebar test ([deaa14c](https://github.com/iblai/os/commit/deaa14ce048be7433f1e8b302dc3a586e011ffb9))
* **mentor:** adding data state ([9a1a7fb](https://github.com/iblai/os/commit/9a1a7fb5da3d61c8980e1e884db798ad5a98a5dd))
* **mentor:** adding unit tests coverage ([740a2d3](https://github.com/iblai/os/commit/740a2d34cb95ea23fc1c7b767596c85f8c96f7ac))
* **mentor:** coverage fixes ([606508b](https://github.com/iblai/os/commit/606508b445a7eb04c0601d726cae1a87f69327ff))
* **mentor:** fix for the excel ([d49413a](https://github.com/iblai/os/commit/d49413a0510e86b0d8b2e59549556103869bc765))
* **mentor:** fix for the packages ([420ae98](https://github.com/iblai/os/commit/420ae981689dd3cbc3ea643ed7a6b2ce682b3ee5))
* **mentor:** fix for the settings tab unit tests ([c4a6d50](https://github.com/iblai/os/commit/c4a6d50dfdcbfd827b11ff92ec35802e5085340f))
* **mentor:** fixes for the e2e tests and tabs for agent settings ([276df4e](https://github.com/iblai/os/commit/276df4e304d92dce95dc33a39fa60026edb6347f))
* **mentor:** fixing for the tabs inconsistant height ([d1f3535](https://github.com/iblai/os/commit/d1f35359596c634e835e010eafa0df5b9305b8db))
* **mentor:** type check issue fix ([87d9277](https://github.com/iblai/os/commit/87d9277e018b2354604b99b1d3c2dd5e39a24c15))
* **mentor:** unit test coverage ([d8a0555](https://github.com/iblai/os/commit/d8a05551ad4f4ce29be8943c875eb23f7579e20a))
* **mentor:** unit test fixes for providers ([44336a5](https://github.com/iblai/os/commit/44336a58ae0a0001f5c3508384edda09fee9c0cc))
- **mentor:** sidebar, agent settings layout changes ([3bc2762](https://github.com/iblai/os/commit/3bc276244face45d4dd5db2668c4a4bbba3a20c6))

### Bug Fixes

- **e2e:** fix for the e2e tests ([3733d96](https://github.com/iblai/os/commit/3733d967e2c3093a4e8df10041cb42ba8dc6fa05))
- **e2e:** fix the explore page test failiures ([ecf8a27](https://github.com/iblai/os/commit/ecf8a27a7711b08326aac17afe58daebbd39a55c))
- **e2e:** fixing coverage.json for missing components ([c63ca0e](https://github.com/iblai/os/commit/c63ca0eb7794fbe57b9e28d518e726f045f4757e))
- **e2e:** fixing for the e2e tests ([d44cc81](https://github.com/iblai/os/commit/d44cc8160fa2e7e86b1884a9e797d4c5d7555dbf))
- **e2e:** fixing for the e2e tests ([08965fd](https://github.com/iblai/os/commit/08965fd81c7dcd2782e438df218c972f03fa5309))
- **e2e:** more fixes for the tests ([8a40c32](https://github.com/iblai/os/commit/8a40c321f77c2ba446c92fa5a96459ca1d7f0e0a))
- **e2e:** more fixes for the tests ([3104d2e](https://github.com/iblai/os/commit/3104d2e352201a8281421b708dc51c703bcce161))
- **mentor:** adding app-sidebar test ([0e5f654](https://github.com/iblai/os/commit/0e5f65411086e8bc433dc0d04983e6dbb76a72d8))
- **mentor:** adding app-sidebar test ([deaa14c](https://github.com/iblai/os/commit/deaa14ce048be7433f1e8b302dc3a586e011ffb9))
- **mentor:** adding data state ([9a1a7fb](https://github.com/iblai/os/commit/9a1a7fb5da3d61c8980e1e884db798ad5a98a5dd))
- **mentor:** adding unit tests coverage ([740a2d3](https://github.com/iblai/os/commit/740a2d34cb95ea23fc1c7b767596c85f8c96f7ac))
- **mentor:** coverage fixes ([606508b](https://github.com/iblai/os/commit/606508b445a7eb04c0601d726cae1a87f69327ff))
- **mentor:** fix for the excel ([d49413a](https://github.com/iblai/os/commit/d49413a0510e86b0d8b2e59549556103869bc765))
- **mentor:** fix for the packages ([420ae98](https://github.com/iblai/os/commit/420ae981689dd3cbc3ea643ed7a6b2ce682b3ee5))
- **mentor:** fix for the settings tab unit tests ([c4a6d50](https://github.com/iblai/os/commit/c4a6d50dfdcbfd827b11ff92ec35802e5085340f))
- **mentor:** fixes for the e2e tests and tabs for agent settings ([276df4e](https://github.com/iblai/os/commit/276df4e304d92dce95dc33a39fa60026edb6347f))
- **mentor:** fixing for the tabs inconsistant height ([d1f3535](https://github.com/iblai/os/commit/d1f35359596c634e835e010eafa0df5b9305b8db))
- **mentor:** type check issue fix ([87d9277](https://github.com/iblai/os/commit/87d9277e018b2354604b99b1d3c2dd5e39a24c15))
- **mentor:** unit test coverage ([d8a0555](https://github.com/iblai/os/commit/d8a05551ad4f4ce29be8943c875eb23f7579e20a))
- **mentor:** unit test fixes for providers ([44336a5](https://github.com/iblai/os/commit/44336a58ae0a0001f5c3508384edda09fee9c0cc))

## [0.71.1](https://github.com/iblai/os/compare/v0.71.0...v0.71.1) (2026-06-03)

### Bug Fixes

* **deps:** bump axios >=1.16.0 and minimatch's brace-expansion to 2.0.3 ([5a9b63f](https://github.com/iblai/os/commit/5a9b63f2f18774e1b47fa57a0f762961bc71976a))
- **deps:** bump axios >=1.16.0 and minimatch's brace-expansion to 2.0.3 ([5a9b63f](https://github.com/iblai/os/commit/5a9b63f2f18774e1b47fa57a0f762961bc71976a))

### Chores

* change agentAI -> OS ([ec6cc3c](https://github.com/iblai/os/commit/ec6cc3c0a3a07ec2ce5fad28397c7668c9e79470))
* change IBL.ai to ibl.ai ([df48cc5](https://github.com/iblai/os/commit/df48cc5f518785d45abb2b5374b91a30dfda4488))
* update app name in AGENTS.md ([0bc8a6e](https://github.com/iblai/os/commit/0bc8a6e4fd54c0921d20f26ac5f5830256f24c88))
* update documentation ([d802e80](https://github.com/iblai/os/commit/d802e802227135715d19097a5a08d80771d83614))
- change agentAI -> OS ([ec6cc3c](https://github.com/iblai/os/commit/ec6cc3c0a3a07ec2ce5fad28397c7668c9e79470))
- change IBL.ai to ibl.ai ([df48cc5](https://github.com/iblai/os/commit/df48cc5f518785d45abb2b5374b91a30dfda4488))
- update app name in AGENTS.md ([0bc8a6e](https://github.com/iblai/os/commit/0bc8a6e4fd54c0921d20f26ac5f5830256f24c88))
- update documentation ([d802e80](https://github.com/iblai/os/commit/d802e802227135715d19097a5a08d80771d83614))

## [0.71.0](https://github.com/iblai/os/compare/v0.70.1...v0.71.0) (2026-06-02)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { describe, it, expect } from 'vitest';
import {
isDataUrl,
dataUrlToFile,
buildEmbedIconSelectionData,
buildFloatingBubbleConfigFromSettings,
hasCustomIconData,
} from '../utils';
import type { CustomFloatingBubbleConfig } from '../hooks/useEmbedTab';

// A minimal 1x1 transparent PNG data URL.
const PNG_DATA_URL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';

const baseConfig: CustomFloatingBubbleConfig = {
image: 'https://cdn.example.com/icon.png',
position: 'bottom-right',
size: 'small',
backgroundColor: 'transparent',
textColor: '#ffffff',
subtitleTextColor: '#e5e7eb',
accentColor: '#1d4ed8',
borderRadius: 16,
shadow: false,
title: 'Hello',
subtitle: 'World',
height: 48,
fontSize: 14,
subtitleFontSize: 12,
padding: 12,
imageSize: 32,
strokeColor: '#000',
strokeWidth: 0,
};

describe('isDataUrl', () => {
it('returns true for a data URL', () => {
expect(isDataUrl(PNG_DATA_URL)).toBe(true);
expect(isDataUrl('data:text/plain;base64,aGk=')).toBe(true);
});

it('returns false for a resolved URL, relative path, null or undefined', () => {
expect(isDataUrl('https://cdn.example.com/icon.png')).toBe(false);
expect(isDataUrl('/message-circle.svg')).toBe(false);
expect(isDataUrl(null)).toBe(false);
expect(isDataUrl(undefined)).toBe(false);
});
});

describe('dataUrlToFile', () => {
it('converts a PNG data URL into a File with the right mime + extension', () => {
const file = dataUrlToFile(PNG_DATA_URL);
expect(file).toBeInstanceOf(File);
expect(file?.type).toBe('image/png');
expect(file?.name).toBe('embed-custom-image.png');
expect((file as File).size).toBeGreaterThan(0);
});

it('honors a provided filename and keeps an explicit extension', () => {
const file = dataUrlToFile(PNG_DATA_URL, 'launcher.webp');
expect(file?.name).toBe('launcher.webp');
});

it('derives an extension from a "+suffix" mime type', () => {
const file = dataUrlToFile('data:image/svg+xml;base64,PHN2Zy8+');
expect(file?.type).toBe('image/svg+xml');
expect(file?.name).toBe('embed-custom-image.svg');
});

it('falls back to octet-stream when the mime is absent', () => {
const file = dataUrlToFile('data:;base64,aGk=');
expect(file?.type).toBe('application/octet-stream');
expect(file?.name).toBe('embed-custom-image.octet-stream');
});

it('returns null for a non-data URL', () => {
expect(dataUrlToFile('https://cdn.example.com/icon.png')).toBeNull();
});

it('returns null when the data URL has no payload', () => {
expect(dataUrlToFile('data:image/png;base64,')).toBeNull();
});
});

describe('buildEmbedIconSelectionData', () => {
it('serializes the config minus the image', () => {
const data = buildEmbedIconSelectionData(baseConfig);
expect(data).not.toHaveProperty('image');
expect(data.title).toBe('Hello');
expect(data.position).toBe('bottom-right');
});

it('does not embed an icon_selection key (mode is derived from existence)', () => {
const data = buildEmbedIconSelectionData(baseConfig);
expect(data).not.toHaveProperty('icon_selection');
});
});

describe('hasCustomIconData', () => {
it('returns true only for a non-empty object', () => {
expect(hasCustomIconData({ title: 'x' })).toBe(true);
});

it('returns false for an empty object (the backend cleared state)', () => {
expect(hasCustomIconData({})).toBe(false);
});

it('returns false for null, undefined, strings and other non-objects', () => {
expect(hasCustomIconData(null)).toBe(false);
expect(hasCustomIconData(undefined)).toBe(false);
expect(hasCustomIconData('')).toBe(false);
expect(hasCustomIconData('null')).toBe(false);
expect(hasCustomIconData(0)).toBe(false);
});
});

describe('buildFloatingBubbleConfigFromSettings', () => {
it('derives iconSelection "custom" when a non-empty JSON map exists', () => {
const result = buildFloatingBubbleConfigFromSettings(
baseConfig,
{
title: 'Persisted',
position: 'top-left',
},
'https://cdn.example.com/saved.png',
);
expect(result).not.toBeNull();
expect(result?.iconSelection).toBe('custom');
expect(result?.config.title).toBe('Persisted');
expect(result?.config.position).toBe('top-left');
expect(result?.config.image).toBe('https://cdn.example.com/saved.png');
// Untouched fields fall back to defaults.
expect(result?.config.borderRadius).toBe(16);
});

it('drops a legacy icon_selection key but still derives from existence', () => {
const result = buildFloatingBubbleConfigFromSettings(
baseConfig,
// Older payloads may still carry this key; it is no longer authoritative.
{ icon_selection: 'default', title: 'Legacy' },
null,
);
expect(result?.iconSelection).toBe('custom');
expect(result?.config).not.toHaveProperty('icon_selection');
});

it('falls back to the default image when no custom image is provided', () => {
const result = buildFloatingBubbleConfigFromSettings(
baseConfig,
{ title: 'NoImage' },
null,
);
expect(result?.config.image).toBe(baseConfig.image);
});

it('ignores a persisted image key inside the JSON map', () => {
const result = buildFloatingBubbleConfigFromSettings(
baseConfig,
{ image: 'data:should-be-ignored', title: 'X' },
'https://cdn.example.com/real.png',
);
expect(result?.config.image).toBe('https://cdn.example.com/real.png');
});

it('hydrates from only a custom image and derives "default" (no JSON map)', () => {
const result = buildFloatingBubbleConfigFromSettings(
baseConfig,
null,
'https://cdn.example.com/only-image.png',
);
expect(result).not.toBeNull();
expect(result?.config.image).toBe('https://cdn.example.com/only-image.png');
// No JSON map => default mode even though a stale image URL lingers.
expect(result?.iconSelection).toBe('default');
});

it('derives "default" when the JSON map is an empty string (cleared)', () => {
const result = buildFloatingBubbleConfigFromSettings(
baseConfig,
'',
'https://cdn.example.com/stale.png',
);
expect(result).not.toBeNull();
expect(result?.iconSelection).toBe('default');
});

it('derives "default" when the JSON map is an empty object {} (backend cleared state)', () => {
// The backend clears `embed_icon_selection_data` by storing `{}` (verified
// live). An empty object still hydrates the config from defaults, but the
// mode must be 'default'. A lingering image must NOT force 'custom'.
const result = buildFloatingBubbleConfigFromSettings(
baseConfig,
{},
'https://cdn.example.com/stale.png',
);
expect(result).not.toBeNull();
expect(result?.iconSelection).toBe('default');
// Config still hydrates (image from the stale URL, rest from defaults).
expect(result?.config.image).toBe('https://cdn.example.com/stale.png');
expect(result?.config.borderRadius).toBe(16);
});

it('returns null when there is neither icon data nor a custom image', () => {
expect(
buildFloatingBubbleConfigFromSettings(baseConfig, null, null),
).toBeNull();
expect(
buildFloatingBubbleConfigFromSettings(baseConfig, undefined, undefined),
).toBeNull();
});

it('treats a non-object icon-selection payload as absent', () => {
expect(
buildFloatingBubbleConfigFromSettings(baseConfig, 'not-an-object', null),
).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getEmbedCode } from '../utils';
import type { EmbedFormValues } from '../hooks/useEmbedTab';

const DM_BASE = 'https://api.iblai.org/dm';
const AXD_BASE = 'https://api.iblai.org/axd';

// The default launcher thumbnail is a DM (manager) endpoint. On the unified API
// gateway axdUrl() resolves to an invalid `/axd` prefix (HTTP 404), so the embed
// code builder must use dmUrl() for the thumbnail. These mocks let us assert the
// exact base used without hitting the network.
vi.mock('@/lib/config', () => ({
config: {
dmUrl: () => DM_BASE,
axdUrl: () => AXD_BASE,
mentorIframeUrl: () => 'https://mentor.example.com',
authUrl: () => 'https://auth.example.com',
},
}));

const settings: EmbedFormValues = {
custom_css: '',
description: '',
website_url: '',
mode: 'default',
allow_anonymous: false,
mentor_visibility: null,
is_context_aware: false,
safety_disclaimer: false,
sso: false,
auto_open: false,
sso_provider: '',
metadata: {
primary_color: '#2467eb',
secondary_color: '#000',
safety_disclaimer: false,
},
slug: 'my-mentor',
icon_selection: 'default',
embed_show_attachment: true,
embed_show_voice_call: true,
embed_show_voice_record: true,
show_catalogue: true,
starter_prompts: 'guided_prompt',
};

describe('getEmbedCode default bubble image (thumbnail uses dm, not axd)', () => {
beforeEach(() => {
vi.stubGlobal(
'fetch',
vi.fn(async () => ({ ok: true }) as Response),
);
});

afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

it('fetches the thumbnail from the dm base, never /axd/', async () => {
const code = await getEmbedCode('acme', settings, 'redirect-token');

const expectedThumbnailUrl = `${DM_BASE}/api/core/orgs/acme/thumbnail/`;

// fetch was called with the dm thumbnail URL...
expect(fetch).toHaveBeenCalledWith(expectedThumbnailUrl);

// ...and never with an /axd/ thumbnail URL.
const fetchMock = fetch as unknown as ReturnType<typeof vi.fn>;
const fetchedUrls = fetchMock.mock.calls.map((c) => String(c[0]));
expect(fetchedUrls.some((u) => u.includes('/axd/'))).toBe(false);

// The generated embed snippet wires the bubble image to the dm URL.
expect(code).toContain(expectedThumbnailUrl);
expect(code).not.toContain(`${AXD_BASE}/api/core/orgs/`);
});

it('falls back to the main-tenant thumbnail via the dm base on a non-ok primary response', async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce({ ok: false } as Response)
.mockResolvedValueOnce({ ok: true } as Response);
vi.stubGlobal('fetch', fetchMock);

const code = await getEmbedCode('acme', settings, 'redirect-token');

expect(fetchMock).toHaveBeenNthCalledWith(
1,
`${DM_BASE}/api/core/orgs/acme/thumbnail/`,
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
`${DM_BASE}/api/core/orgs/main/thumbnail/`,
);
expect(code).toContain(`${DM_BASE}/api/core/orgs/main/thumbnail/`);
});
});
Loading
Loading