From 8791e742ecbceb832e9bd08e82fa5e9fadce34ad Mon Sep 17 00:00:00 2001 From: Raza Fayyaz Date: Mon, 6 Jul 2026 20:52:27 +0500 Subject: [PATCH 1/4] feat(web-containers): adding human support --- .../__tests__/index.test.tsx | 3 + .../edit-mentor-modal.test.tsx | 3 + components/modals/edit-mentor-modal/index.tsx | 2 + .../tabs/human-support-tab.test.tsx | 198 ++++++++++++++++++ .../tabs/human-support-tab.tsx | 37 ++++ .../modals/edit-mentor-modal/tabs/index.ts | 1 + .../edit-mentor/edit-mentor.page.ts | 1 + .../edit-mentor/human-support.tab.ts | 164 +++++++++++++++ hooks/__tests__/use-mentor-segments.test.tsx | 8 +- hooks/use-mentor-segments.ts | 17 ++ lib/constants.ts | 1 + package.json | 8 +- pnpm-lock.yaml | 130 +++++++----- pnpm-workspace.yaml | 17 +- 14 files changed, 530 insertions(+), 60 deletions(-) create mode 100644 components/modals/edit-mentor-modal/tabs/human-support-tab.test.tsx create mode 100644 components/modals/edit-mentor-modal/tabs/human-support-tab.tsx create mode 100644 e2e/page-objects/edit-mentor/human-support.tab.ts diff --git a/components/modals/edit-mentor-modal/__tests__/index.test.tsx b/components/modals/edit-mentor-modal/__tests__/index.test.tsx index 6ec0bc19..048bed27 100644 --- a/components/modals/edit-mentor-modal/__tests__/index.test.tsx +++ b/components/modals/edit-mentor-modal/__tests__/index.test.tsx @@ -248,6 +248,9 @@ vi.mock('../tabs', () => ({ ScreenShareTab: () => (
Screen Share Tab
), + HumanSupportTab: () => ( +
Human Support Tab
+ ), })); vi.mock('../tabs/memory-tab', () => ({ diff --git a/components/modals/edit-mentor-modal/edit-mentor-modal.test.tsx b/components/modals/edit-mentor-modal/edit-mentor-modal.test.tsx index 4089beba..b24f2041 100644 --- a/components/modals/edit-mentor-modal/edit-mentor-modal.test.tsx +++ b/components/modals/edit-mentor-modal/edit-mentor-modal.test.tsx @@ -202,6 +202,9 @@ vi.mock('./tabs', () => ({ ScreenShareTab: () => (
Screen Share Tab
), + HumanSupportTab: () => ( +
Human Support Tab
+ ), })); vi.mock('./tabs/memory-tab', () => ({ diff --git a/components/modals/edit-mentor-modal/index.tsx b/components/modals/edit-mentor-modal/index.tsx index 606c98f5..beb5e2b2 100644 --- a/components/modals/edit-mentor-modal/index.tsx +++ b/components/modals/edit-mentor-modal/index.tsx @@ -32,6 +32,7 @@ import { AuditLogTab, VoiceTab, ScreenShareTab, + HumanSupportTab, } from './tabs'; import { useNavigate } from '@/hooks/user-navigate'; import { MODALS } from '@/lib/constants'; @@ -75,6 +76,7 @@ export const EDIT_MENTOR_TAB_COMPONENTS: Record = { [MODALS.EDIT_MENTOR.tabs.mcp]: , [MODALS.EDIT_MENTOR.tabs.memory]: , [MODALS.EDIT_MENTOR.tabs.history]: , + [MODALS.EDIT_MENTOR.tabs.human_support]: , [MODALS.EDIT_MENTOR.tabs.audit_log]: , [MODALS.EDIT_MENTOR.tabs.datasets]: , [MODALS.EDIT_MENTOR.tabs.api]: , diff --git a/components/modals/edit-mentor-modal/tabs/human-support-tab.test.tsx b/components/modals/edit-mentor-modal/tabs/human-support-tab.test.tsx new file mode 100644 index 00000000..63ba3a9b --- /dev/null +++ b/components/modals/edit-mentor-modal/tabs/human-support-tab.test.tsx @@ -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 ( +
+ {children} +
+ ); + }, + AgentHumanSupportTab: (props: unknown) => { + mockAgentHumanSupportTab(props); + return ( +
AgentHumanSupportTab
+ ); + }, +})); + +// ============================================================================ +// 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(); + + 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(); + + expect(mockAgentHumanSupportTab).toHaveBeenCalledWith( + expect.not.objectContaining({ labels: expect.anything() }), + ); + }); + + it('forwards the config-derived enableRBAC flag to the provider', () => { + mockEnableRBAC.mockReturnValue(true); + + render(); + + 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(); + + expect(mockAgentSettingsProvider).toHaveBeenCalledWith( + expect.objectContaining({ mentorId: 'nav-mentor-xyz' }), + ); + }); + + it('falls back to params.mentorId when getMentorId() returns null', () => { + mockGetMentorId.mockReturnValue(null); + + render(); + + 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(); + + 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(); + + 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(); + + expect(container.firstChild).toBeNull(); + expect(mockAgentSettingsProvider).not.toHaveBeenCalled(); + expect(mockAgentHumanSupportTab).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/components/modals/edit-mentor-modal/tabs/human-support-tab.tsx b/components/modals/edit-mentor-modal/tabs/human-support-tab.tsx new file mode 100644 index 00000000..d8ea2766 --- /dev/null +++ b/components/modals/edit-mentor-modal/tabs/human-support-tab.tsx @@ -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(); + // const { getMentorId } = useNavigate(); + const username = useUsername(); + // const activeMentorId = getMentorId() ?? mentorId; + console.log('HumanSupportTab', tenantKey, mentorId, username); + // 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 ( + + + + ); +} diff --git a/components/modals/edit-mentor-modal/tabs/index.ts b/components/modals/edit-mentor-modal/tabs/index.ts index fb4c8e96..93900046 100644 --- a/components/modals/edit-mentor-modal/tabs/index.ts +++ b/components/modals/edit-mentor-modal/tabs/index.ts @@ -17,3 +17,4 @@ export * from './skills-tab'; export * from './audit-log-tab'; export * from './voice-tab'; export * from './screenshare-tab'; +export * from './human-support-tab'; diff --git a/e2e/page-objects/edit-mentor/edit-mentor.page.ts b/e2e/page-objects/edit-mentor/edit-mentor.page.ts index 1903683e..4eda263c 100644 --- a/e2e/page-objects/edit-mentor/edit-mentor.page.ts +++ b/e2e/page-objects/edit-mentor/edit-mentor.page.ts @@ -15,6 +15,7 @@ import { PrivacyTab } from './privacy.tab'; import { TasksTab } from './tasks.tab'; import { VoiceTab } from './voice.tab'; import { ScreenShareTab } from './screenshare.tab'; +import { HumanSupportTab } from './human-support.tab'; /** * Which sidebar category each segment lives in. Mirrors the `navCategory` diff --git a/e2e/page-objects/edit-mentor/human-support.tab.ts b/e2e/page-objects/edit-mentor/human-support.tab.ts new file mode 100644 index 00000000..49ffefbb --- /dev/null +++ b/e2e/page-objects/edit-mentor/human-support.tab.ts @@ -0,0 +1,164 @@ +import { Page, Locator } from '@playwright/test'; +import { AGENT_HUMAN_SUPPORT_TAB_LABELS } from '@iblai/iblai-js/web-containers/next'; + +/** + * Page object for the Human Support tab inside the Edit Mentor modal. + * + * Rendered by the SDK's `AgentHumanSupportTab` + * (`@iblai/iblai-js/web-containers/next`), wrapped in `AgentSettingsProvider` + * by `components/modals/edit-mentor-modal/tabs/human-support-tab.tsx`. The tab + * is admin-only (userTypes: [UserType.ADMIN]) and lives under the "Analytics" + * category in the modal sidebar, after History and before Audit. + * + * Because the SDK does not yet export semantic Playwright helpers for this tab + * (no `@iblai/iblai-js/playwright` equivalents), all locators are derived from + * `AGENT_HUMAN_SUPPORT_TAB_LABELS` (the SDK-exported label object) and from + * `data-testid` attributes baked into the SDK component. This keeps the + * page-object decoupled from CSS class names and positional DOM structure — a + * label change in the SDK updates `AGENT_HUMAN_SUPPORT_TAB_LABELS` and the + * page-object picks it up automatically. + * + * Selector policy (do not regress): + * 1. Sidebar tab trigger → `[role="tab"][aria-controls="panel-human_support"]:visible` + * The `aria-controls` id is `panel-${tab.value}` (see + * `components/modals/edit-mentor-modal/index.tsx`), and `tab.value` is + * `MODALS.EDIT_MENTOR.tabs.human_support === "human_support"`. Filtering + * on `:visible` excludes the hidden responsive twin (desktop + compact + * sidebar both render the same trigger; only one is visible at a time). + * 2. Tab header title → `role="heading"` + `AGENT_HUMAN_SUPPORT_TAB_LABELS.header.title` + * 3. "Search for User" combobox trigger → `role="combobox"` + aria-label + * 4. Status filter select → `aria-label` from labels.filters.allStatuses i18n key + * 5. Ticket list region → `data-testid="human-support-ticket-list"` (when tickets exist) + * 6. Empty state → `labels.list.noTickets` text (when no tickets) + * 7. Detail pane region → `data-testid="human-support-ticket-detail"` + * 8. Detail pane prompt → `labels.detail.selectPrompt` text (nothing selected) + */ +export class HumanSupportTab { + readonly page: Page; + readonly dialog: Locator; + + /** Default labels exported by the SDK — use for text assertions. */ + static readonly LABELS = AGENT_HUMAN_SUPPORT_TAB_LABELS; + + /** + * Sidebar tab trigger (host-rendered). + * + * `:visible` excludes the host's hidden responsive twin — the sidebar is + * rendered twice (desktop `#desktop-tab-human_support` + compact + * `#tab-human_support`), both owning `aria-controls="panel-human_support"`, + * and only the viewport-appropriate one is visible at any given time. + */ + readonly tabLink: Locator; + + constructor(page: Page, dialog: Locator) { + this.page = page; + this.dialog = dialog; + + this.tabLink = dialog.locator( + '[role="tab"][aria-controls="panel-human_support"]:visible', + ); + } + + // --------------------------------------------------------------------------- + // Navigation + // --------------------------------------------------------------------------- + + /** + * Returns true when the Human Support tab trigger is visible in the sidebar. + * Used to assert admin-only visibility and skip non-admin tests gracefully. + */ + async isTabVisible(): Promise { + return this.tabLink.isVisible({ timeout: 5_000 }).catch(() => false); + } + + // --------------------------------------------------------------------------- + // Surface locators + // --------------------------------------------------------------------------- + + /** + * The "Human Support" heading at the top of the tab panel. + * Resolved by `role="heading"` so it never collides with the sidebar `tab` + * element that also displays the label "Human Support". + */ + heading(): Locator { + return this.dialog.getByRole('heading', { + name: HumanSupportTab.LABELS.header.title, + exact: true, + }); + } + + /** + * The header description paragraph below the "Human Support" heading. + * Matched by a substring of the SDK label text so minor i18n rewording + * doesn't break the assertion (use `{ exact: false }`). + */ + headerDescription(): Locator { + return this.dialog.getByText(HumanSupportTab.LABELS.header.description, { + exact: false, + }); + } + + /** + * The "Search for User" combobox trigger (opens the user-search popover). + * Resolved by `role="combobox"` + `aria-label` set by the SDK. + */ + userSearchCombobox(): Locator { + return this.dialog.getByRole('combobox', { + name: HumanSupportTab.LABELS.filters.searchUser, + exact: true, + }); + } + + /** + * The status-filter `` trigger. The SDK sets - * `aria-label={t('tabsHumanSupportTab.filterByStatus')}` on the trigger, but - * the i18n key value is "Filter by status" (from the `tabsHumanSupportTab` - * namespace, not the same as `AGENT_HUMAN_SUPPORT_TAB_LABELS`). We locate it - * by the displayed placeholder text ("All Statuses") which is available via - * the SDK labels object and is unique in the panel. - */ - statusFilterTrigger(): Locator { - return this.dialog.getByRole('combobox', { - name: HumanSupportTab.LABELS.filters.allStatuses, - exact: false, - }); + toggle(): Locator { + return getSupportToggle(this.dialog); } /** - * The ticket-list region (`data-testid="human-support-ticket-list"`). - * Only mounted by the SDK when there are tickets; assert with a short - * timeout and fall back to `noTicketsMessage()` in the empty state. + * Whether the availability toggle is present at all. Hidden when the + * tenant's tool catalog has no human-support tool — callers must check + * this before `setEnabled`/`enable`/`disable`, which otherwise hang + * waiting for a control that will never render. */ + async hasToggle(): Promise { + let visible = false; + try { + await this.toggle().waitFor({ state: 'visible', timeout: 5_000 }); + visible = true; + } catch { + visible = false; + } + return visible; + } + + isEnabled(): Promise { + return isSupportEnabled(this.dialog); + } + + setEnabled(enabled: boolean): Promise { + return setSupportEnabled(this.dialog, enabled); + } + + enable(): Promise { + return enableSupport(this.dialog); + } + + disable(): Promise { + return disableSupport(this.dialog); + } + + // --------------------------------------------------------------------------- + // Ticket list + // --------------------------------------------------------------------------- + ticketList(): Locator { - return this.dialog.getByTestId('human-support-ticket-list'); + return getTicketList(this.dialog); } - /** - * The empty-state message rendered when there are no support tickets. - * Text matches `AGENT_HUMAN_SUPPORT_TAB_LABELS.list.noTickets`. - */ + /** The empty-state message rendered when there are no support tickets. */ noTicketsMessage(): Locator { - return this.dialog.getByText(HumanSupportTab.LABELS.list.noTickets, { + return this.dialog.getByText(SUPPORT_LABELS.list.noTickets, { exact: true, }); } + ticketRow(subject: string | RegExp): Locator { + return getTicketRow(this.dialog, subject); + } + + ticketRowByIndex(index: number): Locator { + return getTicketRowByIndex(this.dialog, index); + } + + expectTicketInList( + subject: string | RegExp, + timeoutMs?: number, + ): Promise { + return expectTicketInList(this.dialog, subject, timeoutMs); + } + + expectNoTickets(): Promise { + return expectNoTickets(this.dialog); + } + + expectTicketStatusInList( + subject: string | RegExp, + status: SupportTicketStatus, + ): Promise { + return expectTicketStatusInList(this.dialog, subject, status); + } + + statusFilter(): Locator { + return getStatusFilter(this.dialog); + } + + filterByStatus(status: SupportTicketStatus | 'all'): Promise { + return filterTicketsByStatus(this.dialog, status); + } + + userFilter(): Locator { + return getUserFilter(this.dialog); + } + + filterByUser(search: string, optionText?: string | RegExp): Promise { + return filterTicketsByUser(this.dialog, search, optionText); + } + + clearUserFilter(): Promise { + return clearUserFilter(this.dialog); + } + + /** Force the ticket list to re-query (bounces the status filter). */ + refresh(): Promise { + return refreshTickets(this.dialog); + } + /** - * The detail pane region (`data-testid="human-support-ticket-detail"`). - * Rendered on desktop viewports (hidden on mobile). When no ticket is - * selected it shows `labels.detail.selectPrompt`. + * Wait for a ticket to appear in the list, refreshing between polls. + * Agent-created tickets land after the chat tool call completes + * server-side, so allow a generous deadline. */ + waitForTicketInList( + subject: string | RegExp, + opts?: { timeoutMs?: number; refresh?: () => Promise }, + ): Promise { + return waitForTicketInList(this.dialog, subject, opts); + } + + // --------------------------------------------------------------------------- + // Ticket detail + // --------------------------------------------------------------------------- + detailPane(): Locator { - return this.dialog.getByTestId('human-support-ticket-detail'); + return getTicketDetail(this.dialog); } - /** - * The "Select a ticket to view details." prompt shown in the detail pane - * before any ticket is selected. - */ + /** The "Select a ticket to view details." prompt shown before any ticket + * is selected. */ detailSelectPrompt(): Locator { - return this.dialog.getByText(HumanSupportTab.LABELS.detail.selectPrompt, { + return this.dialog.getByText(SUPPORT_LABELS.detail.selectPrompt, { exact: true, }); } + + description(): Locator { + return getTicketDescription(this.dialog); + } + + /** Open a ticket from the list and wait for its detail to render. */ + openTicket(subject: string | RegExp): Promise { + return openTicket(this.dialog, subject); + } + + expectDescriptionContains(text: string | RegExp): Promise { + return expectTicketDescriptionContains(this.dialog, text); + } + + /** + * Change the opened ticket's status via the detail pane's Status select. + * Selecting "closed" routes through the dedicated close endpoint and + * replaces the composer with the closed notice. + */ + setStatus(status: SupportTicketStatus): Promise { + return setTicketStatus(this.dialog, status); + } + + expectClosedNotice(): Promise { + return expectTicketClosedNotice(this.dialog); + } + + replyComposer(): Locator { + return getReplyComposer(this.dialog); + } + + /** Send a reply on the opened ticket; waits for the composer to clear. */ + reply(message: string): Promise { + return replyToTicket(this.dialog, message); + } + + expectMessageInConversation(message: string | RegExp): Promise { + return expectMessageInConversation(this.dialog, message); + } + + expectNoRepliesYet(): Promise { + return expectNoRepliesYet(this.dialog); + } + + // --------------------------------------------------------------------------- + // Chat-driven ticket creation + // --------------------------------------------------------------------------- + + /** + * Full flow: with support enabled, ask the agent (via the main chat + * surface) for a ticket, then verify it shows up in this tab. + * + * The Edit Agent modal is host-specific to open/close, so the caller + * supplies `openEditAgentModal` (and optionally `closeEditAgentModal`, + * used to get the modal out of the chat's way before sending). Steps: + * + * 1. `openEditAgentModal()` → Support tab → ensure the toggle is ON. + * 2. `closeEditAgentModal()` (when provided) → chat-request the ticket. + * 3. `openEditAgentModal()` → Support tab → wait for the ticket row. + * + * Returns the dialog locator with the Support tab open and the new ticket + * listed, so specs can keep asserting (open it, reply, close it). + */ + createTicketViaChatAndVerify(opts: { + subject: string; + description?: string; + openEditAgentModal: () => Promise; + closeEditAgentModal?: () => Promise; + listTimeoutMs?: number; + }): Promise { + return createSupportTicketViaChatAndVerify(this.page, opts); + } } diff --git a/hooks/__tests__/use-mentor-segments-screenshare.test.ts b/hooks/__tests__/use-mentor-segments-screenshare.test.ts index 2553e1ba..52aa3c43 100644 --- a/hooks/__tests__/use-mentor-segments-screenshare.test.ts +++ b/hooks/__tests__/use-mentor-segments-screenshare.test.ts @@ -101,7 +101,6 @@ const screenshareSegment = MENTOR_SEGMENTS.find( const baseFlags = { isMemsearchEnabled: false, isClawEnabled: false, - clawConfigExists: false, isMemoryComponentEnabled: false, isScreenshareEnabled: false, // Voice calls default on so the Screen Share gating tests aren't perturbed diff --git a/hooks/__tests__/use-mentor-segments.test.tsx b/hooks/__tests__/use-mentor-segments.test.tsx index 56920911..928979b7 100644 --- a/hooks/__tests__/use-mentor-segments.test.tsx +++ b/hooks/__tests__/use-mentor-segments.test.tsx @@ -16,7 +16,11 @@ import { EDIT_MENTOR_TAB_COMPONENTS } from '@/components/modals/edit-mentor-moda const mockMentorSettings = vi.fn(); const mockMemsearchEnabled = vi.fn(); -const mockClawMentorConfig = vi.fn(); +// Call-tracking guard: the segments hook must NOT fetch the claw-config — +// Sandbox/Skills are always visible, and fetching here would fire on every +// page through the NavBar. Only the Sandbox/Prompts tab contents (mounted on +// demand) may query it. +const mockUseGetClawMentorConfigQuery = vi.fn(() => ({ data: null })); const mockGetMentorId = vi.fn(); const mockTenantKey = vi.fn(); const mockMentorIdParam = vi.fn(); @@ -42,7 +46,8 @@ vi.mock('@iblai/iblai-js/data-layer', () => ({ useGetMemsearchStatusQuery: () => ({ data: { enable_memsearch: mockMemsearchEnabled() }, }), - useGetClawMentorConfigQuery: () => ({ data: mockClawMentorConfig() }), + useGetClawMentorConfigQuery: (...args: unknown[]) => + mockUseGetClawMentorConfigQuery(...(args as [])), })); vi.mock('@/hooks/use-user', () => ({ @@ -121,10 +126,7 @@ const setupDefaults = () => { ); mockUserType.mockReturnValue(UserType.ADMIN); mockMemsearchEnabled.mockReturnValue(true); - // Default: a wired claw-config exists, so Skills tab gating only depends on - // isClawEnabled in most existing tests. Tests that need the unwired state - // override this in their own arrangement. - mockClawMentorConfig.mockReturnValue({ id: 1, enabled: true, server: 7 }); + mockUseGetClawMentorConfigQuery.mockClear(); mockMentorSettings.mockReturnValue({ platform_key: 'custom-tenant', mentor_visibility: MentorVisibilityEnum.VIEWABLE_BY_TENANT_ADMINS, @@ -144,12 +146,12 @@ describe('useMentorSegments', () => { setupDefaults(); }); - it('returns the canonical 22 mentor segments unfiltered', () => { + it('returns the canonical 23 mentor segments unfiltered', () => { const { result } = renderHook(() => useMentorSegments()); expect(result.current.segments).toBe(MENTOR_SEGMENTS); // 17 original + Voice + Screen Share (feat/mentor/1763) + Tasks // (feat/mentor/715) + LTI + Analytics hub (feat/2040). - expect(MENTOR_SEGMENTS).toHaveLength(22); + expect(MENTOR_SEGMENTS).toHaveLength(23); }); it('places the Sandbox segment right after Settings', () => { @@ -583,20 +585,8 @@ describe('useMentorSegments', () => { enable_claw: true, }; - it('shows both Sandbox and Skills when claw is enabled but no config wired', () => { + it('shows both Sandbox and Skills when claw is enabled', () => { mockMentorSettings.mockReturnValue(adminClawEnabledSettings); - mockClawMentorConfig.mockReturnValue(null); // 404 → null - - const { result } = renderHook(() => useMentorSegments()); - const labels = result.current.filteredSegments.map((s) => s.label); - - expect(labels).toContain('Sandbox'); - expect(labels).toContain('Skills'); - }); - - it('shows both Sandbox and Skills when claw is enabled AND a config is wired', () => { - mockMentorSettings.mockReturnValue(adminClawEnabledSettings); - mockClawMentorConfig.mockReturnValue({ id: 1, enabled: true, server: 7 }); const { result } = renderHook(() => useMentorSegments()); const labels = result.current.filteredSegments.map((s) => s.label); @@ -610,7 +600,6 @@ describe('useMentorSegments', () => { ...adminClawEnabledSettings, enable_claw: false, }); - mockClawMentorConfig.mockReturnValue(null); const { result } = renderHook(() => useMentorSegments()); const labels = result.current.filteredSegments.map((s) => s.label); @@ -618,6 +607,17 @@ describe('useMentorSegments', () => { expect(labels).toContain('Skills'); expect(labels).toContain('Sandbox'); }); + + it('never fetches the claw-config, even when claw is enabled', () => { + // The hook runs on every page via the NavBar; visibility no longer + // depends on a wired ClawMentorConfig, so the query must not fire here. + // The Sandbox/Prompts tabs fetch it themselves when they mount. + mockMentorSettings.mockReturnValue(adminClawEnabledSettings); + + renderHook(() => useMentorSegments()); + + expect(mockUseGetClawMentorConfigQuery).not.toHaveBeenCalled(); + }); }); }); diff --git a/hooks/use-mentor-segments.ts b/hooks/use-mentor-segments.ts index d4d8fea3..79ce0dfe 100644 --- a/hooks/use-mentor-segments.ts +++ b/hooks/use-mentor-segments.ts @@ -32,7 +32,6 @@ import { MentorVisibilityEnum } from '@iblai/iblai-api'; import { useGetMentorSettingsQuery, useGetMemsearchStatusQuery, - useGetClawMentorConfigQuery, } from '@iblai/iblai-js/data-layer'; import { MODALS, UserType } from '@/lib/constants'; @@ -55,8 +54,6 @@ import { config } from '@/lib/config'; export type MentorSegmentConfigFlags = { isMemsearchEnabled: boolean; isClawEnabled: boolean; - /** True when a ClawMentorConfig exists for this mentor (sandbox wired to an instance). */ - clawConfigExists: boolean; isMemoryComponentEnabled: boolean; /** True when `enable_privacy_router` is on for this mentor. */ isPrivacyEnabled: boolean; @@ -373,7 +370,7 @@ export const MENTOR_SEGMENTS: MentorSegment[] = [ }, { value: MODALS.EDIT_MENTOR.tabs.human_support, - label: 'Human Support', + label: 'Support', icon: Headset, // Admin-only ticket inbox (view / reply / close support requests). // No `rbacResource` yet — the backend doesn't expose one for support @@ -385,7 +382,7 @@ export const MENTOR_SEGMENTS: MentorSegment[] = [ MentorVisibilityEnum.VIEWABLE_BY_TENANT_ADMINS, MentorVisibilityEnum.VIEWABLE_BY_TENANT_STUDENTS, ], - navCategory: 'analytics', + navCategory: 'runtime', }, { value: MODALS.EDIT_MENTOR.tabs.audit_log, @@ -598,21 +595,11 @@ export function useMentorSegments(options: UseMentorSegmentsOptions = {}) { // @ts-expect-error enable_claw is not yet in the MentorSettingsPublic type const isClawEnabled: boolean = mentorSettings?.enable_claw ?? false; - // The claw-config endpoint is keyed by the mentor's UUID. Use the value from - // mentor settings; fall back to the resolved id (which may already be a UUID - // when navigating directly). - const mentorUuid: string | undefined = - mentorSettings?.mentor_unique_id ?? resolvedMentorId; - - // The data-layer normalises 404 → null, so a non-null result means the - // mentor has a wired ClawMentorConfig (sandbox connected to an instance). - // Skip the query until we know claw is enabled — there's no point fetching - // the config when we'd never gate on it. - const { data: clawMentorConfig } = useGetClawMentorConfigQuery( - { org: tenantKey!, mentorUniqueId: mentorUuid! }, - { skip: !isClawEnabled || !tenantKey || !mentorUuid }, - ); - const clawConfigExists = !!clawMentorConfig; + // NOTE: no claw-config fetch here. The Sandbox and Skills tabs are always + // visible, so nothing in the segment filter needs to know whether a + // ClawMentorConfig is wired. The tabs that do care fetch it themselves when + // they mount (Sandbox via , Prompts via its own query) — + // fetching it here would fire on every page through the NavBar. const isMemoryComponentEnabled = // @ts-ignore - enable_memory_component exists on API but not typed @@ -653,7 +640,6 @@ export function useMentorSegments(options: UseMentorSegmentsOptions = {}) { isMemsearchEnabled, isMemoryComponentEnabled, isClawEnabled, - clawConfigExists, isPrivacyEnabled, isScreenshareEnabled, isVoiceCallEnabled, @@ -668,7 +654,6 @@ export function useMentorSegments(options: UseMentorSegmentsOptions = {}) { userType, isMemsearchEnabled, isClawEnabled, - clawConfigExists, isMemoryComponentEnabled, isPrivacyEnabled, isScreenshareEnabled, diff --git a/package.json b/package.json index c84e0529..1b463b2a 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,10 @@ "tauri:bump": "node scripts/tauri-bump.mjs" }, "dependencies": { - "@iblai/agent-ai": "file:.yalc/@iblai/agent-ai", - "@iblai/data-layer": "file:.yalc/@iblai/data-layer", + "@iblai/agent-ai": "2.5.2", "@iblai/iblai-api": "4.166.0-ai", "@iblai/iblai-js": "1.25.3", "@iblai/iblai-web-mentor": "1.3.4", - "@iblai/web-containers": "file:.yalc/@iblai/web-containers", - "@iblai/web-utils": "file:.yalc/@iblai/web-utils", "@livekit/components-react": "2.8.1", "@livekit/components-styles": "1.1.4", "@radix-ui/react-accordion": "1.2.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afcc6399..e73caf40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,11 +51,17 @@ importers: specifier: 4.166.0-ai version: 4.166.0-ai '@iblai/iblai-js': - specifier: 1.25.3 - version: 1.25.3(09ad73cc8de68f3b29da137077ebeb71) + specifier: file:.yalc/@iblai/iblai-js + version: file:.yalc/@iblai/iblai-js(6ce570a478e5a131d40a543be997f8bf) '@iblai/iblai-web-mentor': specifier: 1.3.4 version: 1.3.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.62.2)(typescript@5.9.3) + '@iblai/web-containers': + specifier: file:.yalc/@iblai/web-containers + version: file:.yalc/@iblai/web-containers(dd29c8192150225d5f6967c673719996) + '@iblai/web-utils': + specifier: file:.yalc/@iblai/web-utils + version: file:.yalc/@iblai/web-utils(@iblai/data-layer@file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) '@livekit/components-react': specifier: 2.8.1 version: 2.8.1(livekit-client@2.9.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tslib@2.8.1) @@ -867,8 +873,8 @@ packages: react: 19.1.0 react-dom: 19.1.0 - '@iblai/data-layer@1.9.3': - resolution: {integrity: sha512-NTd9QsA56gjgQRhJTq8a1f6DhdvJdO/FL6w2e0HFYboQ9p7pu9bI5tqUZiuuxSL3o52f7wkBDfbjaqOu6zyZEA==} + '@iblai/data-layer@file:.yalc/@iblai/data-layer': + resolution: {directory: .yalc/@iblai/data-layer, type: directory} engines: {node: 25.3.0} peerDependencies: '@reduxjs/toolkit': 2.7.0 @@ -879,8 +885,8 @@ packages: '@iblai/iblai-api@4.166.0-ai': resolution: {integrity: sha512-dY9Jv+CkXEyTxRsOQFay5YvYe8K2LSQluJJvtssGQV2RbrxPPzONaKfnhcyarDs7Ft35Xqk1fuErjhUwNG3OIg==} - '@iblai/iblai-js@1.25.3': - resolution: {integrity: sha512-78My/wsnQYXUCzCaQmBs8xNoxoFHYvGnLgG9FMClvHMFyH/Yr46FrFT6Xx1LLOxXh/7fBiOWpSfI1VdOeoMRLw==} + '@iblai/iblai-js@file:.yalc/@iblai/iblai-js': + resolution: {directory: .yalc/@iblai/iblai-js, type: directory} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: @@ -904,18 +910,18 @@ packages: '@iblai/iblai-web-mentor@1.3.4': resolution: {integrity: sha512-w0K22cQl/LquXDDnVi7DgiOeuS+iha85poZxuLCtJqvwyCxAMLch1DWN3LbNCxw+YGhrV7BOEENp8s7bZ+7ZGw==} - '@iblai/mcp@1.8.0': - resolution: {integrity: sha512-5B8TbJ2V73F4/l+iNsDgkaKUJqASutH+QVu/DffpdolYZH3peEDmxbwCdK/p4Qaf+Hl0AdVzrm+N5Q9VUyw+AA==} + '@iblai/mcp@1.8.2': + resolution: {integrity: sha512-zUppU29ZO/nzs1X1seToym3MmgcSnqpVhKVCd4vEE+Y5chet/iL6Ym0XOFNeVIh8veEe4Ks4lA8h+BYK4kQ3rQ==} engines: {node: '>=20.0.0'} hasBin: true - '@iblai/web-containers@1.14.1': - resolution: {integrity: sha512-5XyBe1l0fp22pxHMdvi5m3/EianpTQUKJ3vzZtX2TilrONAs1xvtoB6x5iuY2dryoPKfJtgMj8a/ZFaXnoi7qA==} + '@iblai/web-containers@file:.yalc/@iblai/web-containers': + resolution: {directory: .yalc/@iblai/web-containers, type: directory} engines: {node: 25.3.0} peerDependencies: - '@iblai/data-layer': 1.9.3 + '@iblai/data-layer': '*' '@iblai/iblai-api': 4.166.0-ai - '@iblai/web-utils': 1.12.1 + '@iblai/web-utils': '*' '@livekit/components-react': 2.8.1 '@radix-ui/react-dialog': ^1.1.7 '@reduxjs/toolkit': 2.7.0 @@ -935,8 +941,8 @@ packages: sonner: optional: true - '@iblai/web-utils@1.12.1': - resolution: {integrity: sha512-71snmynkUlJqN0uBdgVp8TtJpRHKGUjgUHlCQ9oSPw04O++mgJggdiIj61W5Tw5aP0NFs5nsdJv1Rka46eVwsg==} + '@iblai/web-utils@file:.yalc/@iblai/web-utils': + resolution: {directory: .yalc/@iblai/web-utils, type: directory} engines: {node: 25.3.0} peerDependencies: '@iblai/data-layer': ^1.1.2 @@ -9926,7 +9932,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@iblai/data-layer@1.9.3(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0)': + '@iblai/data-layer@file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0)': dependencies: '@iblai/iblai-api': 4.166.0-ai '@reduxjs/toolkit': 2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) @@ -9940,13 +9946,13 @@ snapshots: dependencies: tslib: 2.8.1 - '@iblai/iblai-js@1.25.3(09ad73cc8de68f3b29da137077ebeb71)': + '@iblai/iblai-js@file:.yalc/@iblai/iblai-js(6ce570a478e5a131d40a543be997f8bf)': dependencies: - '@iblai/data-layer': 1.9.3(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) + '@iblai/data-layer': file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) '@iblai/iblai-api': 4.166.0-ai - '@iblai/mcp': 1.8.0 - '@iblai/web-containers': 1.14.1(630fe2ecfaf476800ee25f4f42336034) - '@iblai/web-utils': 1.12.1(@iblai/data-layer@1.9.3(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + '@iblai/mcp': 1.8.2 + '@iblai/web-containers': file:.yalc/@iblai/web-containers(dd29c8192150225d5f6967c673719996) + '@iblai/web-utils': file:.yalc/@iblai/web-utils(@iblai/data-layer@file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) '@radix-ui/react-dialog': 1.1.7(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@reduxjs/toolkit': 2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) axios: 1.18.1 @@ -9987,7 +9993,7 @@ snapshots: - supports-color - typescript - '@iblai/mcp@1.8.0': + '@iblai/mcp@1.8.2': dependencies: '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6) zod: 4.3.6 @@ -9995,11 +10001,11 @@ snapshots: - '@cfworker/json-schema' - supports-color - '@iblai/web-containers@1.14.1(630fe2ecfaf476800ee25f4f42336034)': + '@iblai/web-containers@file:.yalc/@iblai/web-containers(dd29c8192150225d5f6967c673719996)': dependencies: - '@iblai/data-layer': 1.9.3(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) + '@iblai/data-layer': file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) '@iblai/iblai-api': 4.166.0-ai - '@iblai/web-utils': 1.12.1(@iblai/data-layer@1.9.3(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + '@iblai/web-utils': file:.yalc/@iblai/web-utils(@iblai/data-layer@file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) '@radix-ui/react-accordion': 1.2.8(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-avatar': 1.1.7(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-checkbox': 1.2.3(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -10028,7 +10034,7 @@ snapshots: '@tiptap/extension-heading': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-image': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-link': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) - '@tiptap/extension-placeholder': 3.20.4(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-placeholder': 3.20.4(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) '@tiptap/extension-table': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) '@tiptap/extension-table-cell': 3.20.4(@tiptap/extension-table@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) '@tiptap/extension-table-header': 3.20.4(@tiptap/extension-table@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) @@ -10083,9 +10089,9 @@ snapshots: - supports-color - vinxi - '@iblai/web-utils@1.12.1(@iblai/data-layer@1.9.3(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': + '@iblai/web-utils@file:.yalc/@iblai/web-utils(@iblai/data-layer@file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': dependencies: - '@iblai/data-layer': 1.9.3(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) + '@iblai/data-layer': file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) '@iblai/iblai-api': 4.166.0-ai '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) axios: 1.18.1 @@ -12801,7 +12807,7 @@ snapshots: dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) - '@tiptap/extension-bullet-list@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + '@tiptap/extension-bullet-list@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': dependencies: '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12840,7 +12846,7 @@ snapshots: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) '@tiptap/pm': 2.11.7 - '@tiptap/extension-dropcursor@3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + '@tiptap/extension-dropcursor@3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': dependencies: '@tiptap/extensions': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12862,7 +12868,7 @@ snapshots: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) '@tiptap/pm': 2.11.7 - '@tiptap/extension-gapcursor@3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + '@tiptap/extension-gapcursor@3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': dependencies: '@tiptap/extensions': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12929,11 +12935,11 @@ snapshots: dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) - '@tiptap/extension-list-item@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + '@tiptap/extension-list-item@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': dependencies: '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-list-keymap@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + '@tiptap/extension-list-keymap@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': dependencies: '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12942,7 +12948,7 @@ snapshots: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) '@tiptap/pm': 2.11.7 - '@tiptap/extension-list@3.26.1(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + '@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': dependencies: '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) '@tiptap/pm': 3.20.4 @@ -12951,7 +12957,7 @@ snapshots: dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) - '@tiptap/extension-ordered-list@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + '@tiptap/extension-ordered-list@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': dependencies: '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12963,7 +12969,7 @@ snapshots: dependencies: '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) - '@tiptap/extension-placeholder@3.20.4(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': + '@tiptap/extension-placeholder@3.20.4(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': dependencies: '@tiptap/extensions': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -13046,7 +13052,7 @@ snapshots: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) '@tiptap/pm': 2.11.7 - '@tiptap/extensions@3.26.1(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': + '@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': dependencies: '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) '@tiptap/pm': 3.20.4 @@ -13151,26 +13157,26 @@ snapshots: '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) '@tiptap/extension-blockquote': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-bold': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) - '@tiptap/extension-bullet-list': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-bullet-list': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) '@tiptap/extension-code': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-code-block': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) '@tiptap/extension-document': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) - '@tiptap/extension-dropcursor': 3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) - '@tiptap/extension-gapcursor': 3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-dropcursor': 3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) + '@tiptap/extension-gapcursor': 3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) '@tiptap/extension-hard-break': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-heading': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-horizontal-rule': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) '@tiptap/extension-italic': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-link': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) - '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-list-item': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) - '@tiptap/extension-list-keymap': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) - '@tiptap/extension-ordered-list': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-list': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/extension-list-item': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) + '@tiptap/extension-list-keymap': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) + '@tiptap/extension-ordered-list': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) '@tiptap/extension-paragraph': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-strike': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-text': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-underline': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) - '@tiptap/extensions': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extensions': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) '@tiptap/pm': 3.20.4 '@tootallnate/quickjs-emscripten@0.23.0': {} From 9a42ac385df6f4de4a57b639f4a5a902db679b1a Mon Sep 17 00:00:00 2001 From: Raza Fayyaz Date: Sat, 25 Jul 2026 03:21:27 +0500 Subject: [PATCH 3/4] fix(mentor): fixing merge conflicts changes and test updates --- e2e/COVERAGE.md | 31 ++++- e2e/coverage.json | 59 +++++++- ...ts => 65-mentor-human-support-tab.spec.ts} | 4 +- hooks/__tests__/use-mentor-segments.test.tsx | 7 +- hooks/use-mentor-segments.ts | 1 + messages/en.json | 1 + messages/es.json | 1 + messages/fr.json | 1 + messages/zh.json | 1 + package.json | 2 +- pnpm-lock.yaml | 131 ++++++++---------- pnpm-workspace.yaml | 17 +-- 12 files changed, 156 insertions(+), 100 deletions(-) rename e2e/journeys/{64-mentor-human-support-tab.spec.ts => 65-mentor-human-support-tab.spec.ts} (99%) diff --git a/e2e/COVERAGE.md b/e2e/COVERAGE.md index e1290074..a2ce4912 100644 --- a/e2e/COVERAGE.md +++ b/e2e/COVERAGE.md @@ -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 @@ -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 diff --git a/e2e/coverage.json b/e2e/coverage.json index 346f5f59..cd313065 100644 --- a/e2e/coverage.json +++ b/e2e/coverage.json @@ -2,14 +2,14 @@ "version": 2, "lastUpdated": "2026-07-23", "summary": { - "totalCheckpoints": 586, - "coveredCheckpoints": 559, + "totalCheckpoints": 594, + "coveredCheckpoints": 567, "deprecatedCheckpoints": 12, "notReproducibleCheckpoints": 8, "pendingCheckpoints": 7, "percent": 100, - "totalJourneys": 66, - "activeJourneys": 65 + "totalJourneys": 67, + "activeJourneys": 66 }, "journeys": [ { @@ -3752,6 +3752,57 @@ "status": "covered" } ] + }, + { + "id": "mentor-human-support-tab", + "name": "Mentor Human Support Tab", + "spec": "65-mentor-human-support-tab.spec.ts", + "sourceFiles": [ + "components/modals/edit-mentor-modal/tabs/human-support-tab.tsx", + "e2e/page-objects/edit-mentor/human-support.tab.ts" + ], + "checkpoints": [ + { + "id": "support-01", + "description": "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)", + "status": "covered" + }, + { + "id": "support-02", + "description": "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", + "status": "covered" + }, + { + "id": "support-03", + "description": "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", + "status": "covered" + }, + { + "id": "support-04", + "description": "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", + "status": "covered" + }, + { + "id": "support-05", + "description": "Opening the created ticket shows the detail pane with non-empty agent-authored description content", + "status": "covered" + }, + { + "id": "support-06", + "description": "Admin sends a reply from the detail-pane composer — the message appears in the ticket's Conversation section", + "status": "covered" + }, + { + "id": "support-07", + "description": "Admin moves the ticket to In Progress via the detail Status select — the list row's status badge updates", + "status": "covered" + }, + { + "id": "support-08", + "description": "Admin closes the ticket — the closed notice replaces the reply composer and the list badge shows Closed", + "status": "covered" + } + ] } ] } diff --git a/e2e/journeys/64-mentor-human-support-tab.spec.ts b/e2e/journeys/65-mentor-human-support-tab.spec.ts similarity index 99% rename from e2e/journeys/64-mentor-human-support-tab.spec.ts rename to e2e/journeys/65-mentor-human-support-tab.spec.ts index de8694eb..b171c1cf 100644 --- a/e2e/journeys/64-mentor-human-support-tab.spec.ts +++ b/e2e/journeys/65-mentor-human-support-tab.spec.ts @@ -12,7 +12,7 @@ import { } from '@iblai/iblai-js/playwright'; /** - * Journey 64 — Mentor Human Support Tab. + * Journey 65 — Mentor Human Support Tab. * * The Support tab (host sidebar label "Support"; SDK panel value * `human_support`) is rendered by the SDK's `AgentHumanSupportTab` @@ -59,7 +59,7 @@ import { test.describe.configure({ mode: 'serial' }); -test.describe('Journey 64: Mentor Human Support Tab', () => { +test.describe('Journey 65: Mentor Human Support Tab', () => { const tracker = new MentorTracker(); test.beforeEach(async ({ page, editMentorPage, createMentorPage }) => { diff --git a/hooks/__tests__/use-mentor-segments.test.tsx b/hooks/__tests__/use-mentor-segments.test.tsx index 014d4bcf..5b54e054 100644 --- a/hooks/__tests__/use-mentor-segments.test.tsx +++ b/hooks/__tests__/use-mentor-segments.test.tsx @@ -150,12 +150,13 @@ describe('useMentorSegments', () => { setupDefaults(); }); - it('returns the canonical 23 mentor segments unfiltered', () => { + it('returns the canonical 24 mentor segments unfiltered', () => { const { result } = renderHook(() => useMentorSegments()); expect(result.current.segments).toBe(MENTOR_SEGMENTS); // 17 original + Voice + Screen Share (feat/mentor/1763) + Tasks - // (feat/mentor/715) + LTI + Analytics hub (feat/2040). - expect(MENTOR_SEGMENTS).toHaveLength(23); + // (feat/mentor/715) + LTI + Analytics hub (feat/2040) + Human Support + // (feat/2081) + Evals (feat/1178). + expect(MENTOR_SEGMENTS).toHaveLength(24); }); it('places the Sandbox segment right after Settings', () => { diff --git a/hooks/use-mentor-segments.ts b/hooks/use-mentor-segments.ts index a11f3471..57932cd3 100644 --- a/hooks/use-mentor-segments.ts +++ b/hooks/use-mentor-segments.ts @@ -398,6 +398,7 @@ export const MENTOR_SEGMENTS: MentorSegment[] = [ { value: MODALS.EDIT_MENTOR.tabs.human_support, label: 'Support', + labelKey: 'support', icon: Headset, // Admin-only ticket inbox (view / reply / close support requests). // No `rbacResource` yet — the backend doesn't expose one for support diff --git a/messages/en.json b/messages/en.json index 7b312ae0..6909fdd7 100644 --- a/messages/en.json +++ b/messages/en.json @@ -809,6 +809,7 @@ "skills": "Skills", "privacy": "Privacy", "tasks": "Tasks", + "support": "Support", "disclaimers": "Disclaimers", "memory": "Memory", "evals": "Evals", diff --git a/messages/es.json b/messages/es.json index 786b033a..a3f2ede2 100644 --- a/messages/es.json +++ b/messages/es.json @@ -809,6 +809,7 @@ "skills": "Habilidades", "privacy": "Privacidad", "tasks": "Tareas", + "support": "Soporte", "disclaimers": "Avisos legales", "memory": "Memoria", "evals": "Evaluaciones", diff --git a/messages/fr.json b/messages/fr.json index 043bbcf3..8c67ad70 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -809,6 +809,7 @@ "skills": "Compétences", "privacy": "Confidentialité", "tasks": "Tâches", + "support": "Support", "disclaimers": "Avertissements", "memory": "Mémoire", "evals": "Évaluations", diff --git a/messages/zh.json b/messages/zh.json index f3934886..db1dd194 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -809,6 +809,7 @@ "skills": "技能", "privacy": "隐私", "tasks": "任务", + "support": "支持", "disclaimers": "免责声明", "memory": "记忆", "evals": "评估", diff --git a/package.json b/package.json index 0e05042e..1eec47d0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "dependencies": { "@iblai/agent-ai": "2.5.2", "@iblai/iblai-api": "4.166.0-ai", - "@iblai/iblai-js": "1.26.7", + "@iblai/iblai-js": "1.26.11", "@iblai/iblai-web-mentor": "1.3.4", "@livekit/components-react": "2.8.1", "@livekit/components-styles": "1.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88e0ac87..b70e0ee7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,10 +5,6 @@ settings: excludeLinksFromLockfile: false overrides: - '@iblai/iblai-js': file:.yalc/@iblai/iblai-js - '@iblai/data-layer': file:.yalc/@iblai/data-layer - '@iblai/web-utils': file:.yalc/@iblai/web-utils - '@iblai/web-containers': file:.yalc/@iblai/web-containers vite: '>=7.3.2' basic-ftp: '>=5.2.1' follow-redirects: '>=1.16.0' @@ -43,26 +39,17 @@ importers: .: dependencies: '@iblai/agent-ai': - specifier: file:.yalc/@iblai/agent-ai - version: file:.yalc/@iblai/agent-ai(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@iblai/data-layer': - specifier: file:.yalc/@iblai/data-layer - version: file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) + specifier: 2.5.2 + version: 2.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@iblai/iblai-api': specifier: 4.166.0-ai version: 4.166.0-ai '@iblai/iblai-js': - specifier: 1.26.7 - version: 1.26.7(e1f0450bf09a5428dbc94a2402b481a6) + specifier: 1.26.11 + version: 1.26.11(e1f0450bf09a5428dbc94a2402b481a6) '@iblai/iblai-web-mentor': specifier: 1.3.4 version: 1.3.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.62.2)(typescript@5.9.3) - '@iblai/web-containers': - specifier: file:.yalc/@iblai/web-containers - version: file:.yalc/@iblai/web-containers(dd29c8192150225d5f6967c673719996) - '@iblai/web-utils': - specifier: file:.yalc/@iblai/web-utils - version: file:.yalc/@iblai/web-utils(@iblai/data-layer@file:.yalc/@iblai/data-layer(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) '@livekit/components-react': specifier: 2.8.1 version: 2.8.1(livekit-client@2.9.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tslib@2.8.1) @@ -867,15 +854,15 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@iblai/agent-ai@file:.yalc/@iblai/agent-ai': - resolution: {directory: .yalc/@iblai/agent-ai, type: directory} + '@iblai/agent-ai@2.5.2': + resolution: {integrity: sha512-ObIQYKdYB6s/0J6WOUo2VAykRMwDuGY+2dUbB8Xssv/VQ3QYhILWgW1IXG46kODG+O1rBgORQd+9tS7XZD0//g==} engines: {node: 25.3.0} peerDependencies: react: 19.1.0 react-dom: 19.1.0 - '@iblai/data-layer@1.9.5': - resolution: {integrity: sha512-iZw73nIfje8ChdEQYGa/phwzMl8Ci2Ly7HoayVTumxvhoiggkKZgitQcCG3GKlVhHdnTNAqRkcqF1SXq9Kfjsw==} + '@iblai/data-layer@1.9.7': + resolution: {integrity: sha512-cFQVESmMM+Zd1gxv8jJRIOwhyjQKgkZnrAPWPkbQkdItT0IhWSXW7T1ZjwlmFgdBlYJUoxtE989Uzeg84sTS3Q==} engines: {node: 25.3.0} peerDependencies: '@reduxjs/toolkit': 2.7.0 @@ -886,8 +873,8 @@ packages: '@iblai/iblai-api@4.166.0-ai': resolution: {integrity: sha512-dY9Jv+CkXEyTxRsOQFay5YvYe8K2LSQluJJvtssGQV2RbrxPPzONaKfnhcyarDs7Ft35Xqk1fuErjhUwNG3OIg==} - '@iblai/iblai-js@1.26.7': - resolution: {integrity: sha512-F+AX73UbQY38jreA6zEG1AxCa9rjg8H0v27AhNynBaREC0v2wpQjhWN77TvSwwanLULFQgFAgGgn6/fdhXqF2A==} + '@iblai/iblai-js@1.26.11': + resolution: {integrity: sha512-QwHhfsdB831Iz+tZc3evmNrcYALrUfUZaVHZHYqua7MM5LPjtPFdXvpOfd7+TNtXVNaOrbQdzr8k4J8L+NBV6w==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: @@ -911,18 +898,18 @@ packages: '@iblai/iblai-web-mentor@1.3.4': resolution: {integrity: sha512-w0K22cQl/LquXDDnVi7DgiOeuS+iha85poZxuLCtJqvwyCxAMLch1DWN3LbNCxw+YGhrV7BOEENp8s7bZ+7ZGw==} - '@iblai/mcp@1.8.2': - resolution: {integrity: sha512-zUppU29ZO/nzs1X1seToym3MmgcSnqpVhKVCd4vEE+Y5chet/iL6Ym0XOFNeVIh8veEe4Ks4lA8h+BYK4kQ3rQ==} + '@iblai/mcp@1.8.3': + resolution: {integrity: sha512-3hmPUyfOaS07NUABSnrsyXSDzc/AROSJyUJmk4xSaCE7eMtBuIUXxfImPa04jUV76s9e8aU1ZaUbkmjLkAMyqQ==} engines: {node: '>=20.0.0'} hasBin: true - '@iblai/web-containers@1.15.4': - resolution: {integrity: sha512-opAcnqVJ/349mkjuHEPhq+ZQAw7lXGKVMagGYG7B2bovYyhp2P+2w+vt/5FZZ7wkaNUJq+bPGEEZy8lcf/2RyQ==} + '@iblai/web-containers@1.15.6': + resolution: {integrity: sha512-XSX2iLRkgXpdLJgdXK/Jc0WiQ5XjtmG9JjiU/QGhKA644HsLXsZB3PDk/Ha8WeqeFtaiqjDyEdcHk/3AU2b6mA==} engines: {node: 25.3.0} peerDependencies: - '@iblai/data-layer': 1.9.5 + '@iblai/data-layer': 1.9.7 '@iblai/iblai-api': 4.166.0-ai - '@iblai/web-utils': 1.13.4 + '@iblai/web-utils': 1.13.6 '@livekit/components-react': 2.8.1 '@radix-ui/react-dialog': ^1.1.7 '@reduxjs/toolkit': 2.7.0 @@ -942,14 +929,15 @@ packages: sonner: optional: true - '@iblai/web-utils@1.13.5': - resolution: {integrity: sha512-E/9Xrm+kj6hNwkrkdVD3Hbx2RERoZAuXyM/vs1auMt6PFZFyPdGgTZQl/QEt3XlgpEgFO/iALVTDb1PovUDJUg==} + '@iblai/web-utils@1.13.6': + resolution: {integrity: sha512-MPZJrGN08TP/yCng1pqgedT0nhJ2Gaw06bsV5sG/HBMx9Dej7jj/OKRhRBAV0Ek5I1nZ9gRoTI/IQC0KkXEXGg==} engines: {node: 25.3.0} peerDependencies: '@iblai/data-layer': ^1.1.2 '@iblai/iblai-api': 4.166.0-ai '@tauri-apps/api': '*' '@tauri-apps/plugin-os': 2.3.2 + next: '>=14' react: 19.1.0 react-dom: 19.1.0 sonner: ^2.0.0 @@ -958,6 +946,8 @@ packages: optional: true '@tauri-apps/plugin-os': optional: true + next: + optional: true sonner: optional: true @@ -9942,12 +9932,12 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@iblai/agent-ai@file:.yalc/@iblai/agent-ai(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@iblai/agent-ai@2.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@iblai/data-layer@1.9.5(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0)': + '@iblai/data-layer@1.9.7(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0)': dependencies: '@iblai/iblai-api': 4.166.0-ai '@reduxjs/toolkit': 2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) @@ -9961,13 +9951,13 @@ snapshots: dependencies: tslib: 2.8.1 - '@iblai/iblai-js@1.26.7(e1f0450bf09a5428dbc94a2402b481a6)': + '@iblai/iblai-js@1.26.11(e1f0450bf09a5428dbc94a2402b481a6)': dependencies: - '@iblai/data-layer': 1.9.5(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) + '@iblai/data-layer': 1.9.7(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) '@iblai/iblai-api': 4.166.0-ai - '@iblai/mcp': 1.8.2 - '@iblai/web-containers': 1.15.4(83e38c06ab0d4ff3f58ef2ac0a09f7e0) - '@iblai/web-utils': 1.13.5(@iblai/data-layer@1.9.5(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + '@iblai/mcp': 1.8.3 + '@iblai/web-containers': 1.15.6(951c8500f2f21982635689bf72e24ec8) + '@iblai/web-utils': 1.13.6(@iblai/data-layer@1.9.7(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.0)(@types/node@20.17.48)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) '@radix-ui/react-dialog': 1.1.7(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@reduxjs/toolkit': 2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) axios: 1.18.1 @@ -10008,7 +9998,7 @@ snapshots: - supports-color - typescript - '@iblai/mcp@1.8.2': + '@iblai/mcp@1.8.3': dependencies: '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6) zod: 4.3.6 @@ -10016,11 +10006,11 @@ snapshots: - '@cfworker/json-schema' - supports-color - '@iblai/web-containers@1.15.4(83e38c06ab0d4ff3f58ef2ac0a09f7e0)': + '@iblai/web-containers@1.15.6(951c8500f2f21982635689bf72e24ec8)': dependencies: - '@iblai/data-layer': 1.9.5(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) + '@iblai/data-layer': 1.9.7(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) '@iblai/iblai-api': 4.166.0-ai - '@iblai/web-utils': 1.13.5(@iblai/data-layer@1.9.5(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + '@iblai/web-utils': 1.13.6(@iblai/data-layer@1.9.7(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.0)(@types/node@20.17.48)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) '@radix-ui/react-accordion': 1.2.8(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-avatar': 1.1.7(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-checkbox': 1.2.3(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -10045,17 +10035,17 @@ snapshots: '@shadcn/ui': 0.0.4 '@tanstack/react-form': 1.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) - '@tiptap/extension-color': 3.20.4(@tiptap/extension-text-style@3.20.4(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))) + '@tiptap/extension-color': 3.20.4(@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))) '@tiptap/extension-heading': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-image': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-link': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) - '@tiptap/extension-placeholder': 3.20.4(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) + '@tiptap/extension-placeholder': 3.20.4(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) '@tiptap/extension-table': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) '@tiptap/extension-table-cell': 3.20.4(@tiptap/extension-table@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) '@tiptap/extension-table-header': 3.20.4(@tiptap/extension-table@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) '@tiptap/extension-table-row': 3.20.4(@tiptap/extension-table@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) '@tiptap/extension-text-align': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) - '@tiptap/extension-text-style': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) + '@tiptap/extension-text-style': 3.20.4(@tiptap/core@2.27.2(@tiptap/pm@2.11.7)) '@tiptap/extension-underline': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/pm': 3.20.4 '@tiptap/react': 3.14.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@types/react-dom@19.2.1(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -10104,9 +10094,9 @@ snapshots: - supports-color - vinxi - '@iblai/web-utils@1.13.5(@iblai/data-layer@1.9.5(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': + '@iblai/web-utils@1.13.6(@iblai/data-layer@1.9.7(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(@iblai/iblai-api@4.166.0-ai)(@tauri-apps/api@2.11.0)(@tauri-apps/plugin-os@2.3.2)(@types/react@19.1.17)(next@15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.0)(@types/node@20.17.48)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(redux@5.0.1)(sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': dependencies: - '@iblai/data-layer': 1.9.5(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) + '@iblai/data-layer': 1.9.7(@reduxjs/toolkit@2.7.0(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) '@iblai/iblai-api': 4.166.0-ai '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.1.17)(react@19.1.0)(redux@5.0.1))(react@19.1.0) axios: 1.18.1 @@ -10122,6 +10112,7 @@ snapshots: optionalDependencies: '@tauri-apps/api': 2.11.0 '@tauri-apps/plugin-os': 2.3.2 + next: 15.5.18(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.0)(@types/node@20.17.48)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) sonner: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - '@types/react' @@ -12832,7 +12823,7 @@ snapshots: dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) - '@tiptap/extension-bullet-list@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': + '@tiptap/extension-bullet-list@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': dependencies: '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12854,7 +12845,7 @@ snapshots: dependencies: '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) - '@tiptap/extension-color@3.20.4(@tiptap/extension-text-style@3.20.4(@tiptap/core@2.27.2(@tiptap/pm@2.11.7)))': + '@tiptap/extension-color@3.20.4(@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)))': dependencies: '@tiptap/extension-text-style': 3.20.4(@tiptap/core@2.27.2(@tiptap/pm@2.11.7)) @@ -12871,7 +12862,7 @@ snapshots: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) '@tiptap/pm': 2.11.7 - '@tiptap/extension-dropcursor@3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': + '@tiptap/extension-dropcursor@3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': dependencies: '@tiptap/extensions': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12893,7 +12884,7 @@ snapshots: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) '@tiptap/pm': 2.11.7 - '@tiptap/extension-gapcursor@3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': + '@tiptap/extension-gapcursor@3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': dependencies: '@tiptap/extensions': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12960,11 +12951,11 @@ snapshots: dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) - '@tiptap/extension-list-item@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': + '@tiptap/extension-list-item@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': dependencies: '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) - '@tiptap/extension-list-keymap@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': + '@tiptap/extension-list-keymap@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': dependencies: '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12973,16 +12964,11 @@ snapshots: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) '@tiptap/pm': 2.11.7 - '@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': - dependencies: - '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) - '@tiptap/pm': 3.20.4 - '@tiptap/extension-ordered-list@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))': dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) - '@tiptap/extension-ordered-list@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': + '@tiptap/extension-ordered-list@3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': dependencies: '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -12994,7 +12980,7 @@ snapshots: dependencies: '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) - '@tiptap/extension-placeholder@3.20.4(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))': + '@tiptap/extension-placeholder@3.20.4(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))': dependencies: '@tiptap/extensions': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) @@ -13052,10 +13038,6 @@ snapshots: dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) - '@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))': - dependencies: - '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) - '@tiptap/extension-text@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))': dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) @@ -13077,11 +13059,6 @@ snapshots: '@tiptap/core': 2.27.2(@tiptap/pm@2.11.7) '@tiptap/pm': 2.11.7 - '@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)': - dependencies: - '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) - '@tiptap/pm': 3.20.4 - '@tiptap/pm@2.11.7': dependencies: prosemirror-changeset: 2.4.1 @@ -13182,26 +13159,26 @@ snapshots: '@tiptap/core': 3.20.4(@tiptap/pm@3.20.4) '@tiptap/extension-blockquote': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-bold': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) - '@tiptap/extension-bullet-list': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) + '@tiptap/extension-bullet-list': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) '@tiptap/extension-code': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-code-block': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) '@tiptap/extension-document': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) - '@tiptap/extension-dropcursor': 3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) - '@tiptap/extension-gapcursor': 3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) + '@tiptap/extension-dropcursor': 3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-gapcursor': 3.27.2(@tiptap/extensions@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) '@tiptap/extension-hard-break': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-heading': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-horizontal-rule': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) '@tiptap/extension-italic': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-link': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) - '@tiptap/extension-list': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) - '@tiptap/extension-list-item': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) - '@tiptap/extension-list-keymap': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) - '@tiptap/extension-ordered-list': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)) + '@tiptap/extension-list': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) + '@tiptap/extension-list-item': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-list-keymap': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) + '@tiptap/extension-ordered-list': 3.27.2(@tiptap/extension-list@3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)) '@tiptap/extension-paragraph': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-strike': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-text': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) '@tiptap/extension-underline': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)) - '@tiptap/extensions': 3.27.2(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4) + '@tiptap/extensions': 3.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7) '@tiptap/pm': 3.20.4 '@tootallnate/quickjs-emscripten@0.23.0': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 30c01a37..63524e98 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,19 +11,12 @@ onlyBuiltDependencies: # package.json, which pnpm v10 no longer reads — this file is the supported home # for them (and for patchedDependencies, below). # -# NOTE: the @iblai `file:.yalc/...` overrides pin every consumer (including -# @iblai/iblai-js's own `"*"` deps on data-layer/web-utils/web-containers) to -# the yalc-pushed builds. Without them pnpm satisfies those `"*"` ranges from -# the registry, so the app and the SDK re-exports load two different copies — -# missing new components (e.g. AgentHumanSupportTab) and duplicated React -# contexts. These must be dropped (together with the file:.yalc specs in -# package.json) once the new SDK versions are published — .yalc is gitignored, -# so CI installs fail while they're in place. +# NOTE: web-containers is now consumed exclusively via @iblai/iblai-js +# (>=1.20.11), which bundles the i18n-aware build (WebContainersI18nProvider + +# the re-exported components share a single context). The previous +# `@iblai/web-containers: file:.yalc/...` override is therefore obsolete and was +# removed — it broke CI installs since .yalc is gitignored. overrides: - '@iblai/iblai-js': 'file:.yalc/@iblai/iblai-js' - '@iblai/data-layer': 'file:.yalc/@iblai/data-layer' - '@iblai/web-utils': 'file:.yalc/@iblai/web-utils' - '@iblai/web-containers': 'file:.yalc/@iblai/web-containers' vite: '>=7.3.2' basic-ftp: '>=5.2.1' follow-redirects: '>=1.16.0' From f216d978f87919c0044d95c53fd84a336f856b56 Mon Sep 17 00:00:00 2001 From: bnsoni Date: Fri, 31 Jul 2026 00:33:02 +0300 Subject: [PATCH 4/4] ci: adopt the current PR-e2e caller on this branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only .github/workflows/pr-e2e-tests.yml, copied from main. No other code from main is pulled in. A pull_request workflow runs from the MERGE commit, and this branch is 20 commits behind — so it would still run the caller as it was this morning: no wait-for-slot (would risk evicting a queued PR), and a 2h timeout that counts QUEUE time, which is exactly why this PR's last run reported no report link while its result was fine. Brings: ECR registry, wait-for-slot + stagger, eviction retry, orphan cancel, busy-vs-offline liveness guard, execution-only timeout, and the result-artifact download retry. --- .github/workflows/pr-e2e-tests.yml | 242 +++++++++++++++++++---------- 1 file changed, 163 insertions(+), 79 deletions(-) diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index a494871d..78b446e7 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -178,97 +178,171 @@ jobs: fi echo "target=$TARGET" >> "$GITHUB_OUTPUT" echo "target env: $TARGET ($WHY)" - - name: Dispatch e2e + - name: Dispatch e2e and wait for the result id: dispatch env: - GH_TOKEN: ${{ secrets.DEPLOY_OPS_DISPATCH_TOKEN }} - PR: ${{ github.event.pull_request.number }} - run: | - set -euo pipefail - BEFORE=$(gh run list -R iblai/iblai-deploy-ops --workflow=pr-e2e.yml -L 1 --json databaseId --jq '.[0].databaseId // 0') - # github.sha on a pull_request IS the merge commit — the same ref actions/checkout used to - # build the app image, so the specs and the app code come from one commit. - gh workflow run pr-e2e.yml -R iblai/iblai-deploy-ops --ref main \ - -f source_repo=os \ - -f pr_number="$PR" \ - -f merge_sha='${{ github.sha }}' \ - -f head_sha='${{ github.event.pull_request.head.sha }}' \ - -f app_image='${{ needs.build-app-image.outputs.image-uri }}' \ - -f target_env='${{ steps.env.outputs.target }}' \ - -f mode='${{ steps.specs.outputs.mode }}' \ - -f specs='${{ steps.specs.outputs.specs }}' - for i in $(seq 1 30); do - sleep 5 - ID=$(gh run list -R iblai/iblai-deploy-ops --workflow=pr-e2e.yml -L 1 --json databaseId --jq '.[0].databaseId // 0') - if [ "$ID" != "$BEFORE" ] && [ "$ID" != "0" ]; then - echo "run_id=$ID" >> "$GITHUB_OUTPUT" - echo "dispatched run $ID"; exit 0 - fi - done - echo "::error::dispatched but the run never appeared"; exit 1 - - - name: Wait for e2e - id: wait - env: - GH_TOKEN: ${{ secrets.DEPLOY_OPS_DISPATCH_TOKEN }} - RUN_ID: ${{ steps.dispatch.outputs.run_id }} + GH_TOKEN: ${{ secrets.DEPLOY_OPS_DISPATCH_TOKEN }} + PR: ${{ github.event.pull_request.number }} TARGET_ENV: ${{ steps.env.outputs.target }} run: | set -uo pipefail - # The central pipeline lives in a PRIVATE repo, so most contributors here cannot open its - # Actions log. Mirror every phase transition into THIS log, which is public — otherwise a - # developer stares at a spinner for an hour with no idea what is happening. - echo "Phases: resolve -> baseline drift check -> sync (only if drifted) -> swap SPA -> e2e -> restore" - # A dispatched run that never STARTS means the target env's runner is offline — the picker - # cannot tell that apart from idle. Without this guard the poll ran the full 2h and reported - # a bare "timed_out", which says nothing about the cause. Fail in 15 min with a diagnosis. - STARTED=0 - STRIKES=0 - LAST="" - for i in $(seq 1 240); do - if [ "$STARTED" = "0" ]; then - ACTIVE=$(gh api "repos/iblai/iblai-deploy-ops/actions/runs/${RUN_ID}/jobs" \ - --jq '[.jobs[] | select(.status != "queued")] | length' 2>/dev/null || echo 0) - [ "${ACTIVE:-0}" -gt 0 ] && STARTED=1 - if [ "$STARTED" = "0" ]; then - # Distinguish OFFLINE from BUSY. Elapsed time alone cannot: with a single serialized - # runner and ~84-minute mentor suites, queueing well past 15 min is entirely normal, - # and an earlier version of this guard killed healthy runs because of that. An offline - # runner, by contrast, never appears executing ANY job anywhere — that is the signal. - RUNNER="${TARGET_ENV}-spa" - BUSY=$(for rid in $(gh run list -R iblai/iblai-deploy-ops --limit 10 \ - --json databaseId --jq '.[].databaseId' 2>/dev/null); do - gh api "repos/iblai/iblai-deploy-ops/actions/runs/$rid/jobs" \ + TARGET="$TARGET_ENV" + RUNNER="${TARGET}-spa" + OPS=iblai/iblai-deploy-ops + + # GitHub keeps only ONE pending run per concurrency group. A third dispatch CANCELS the + # previously-pending one before it starts a single job — silently, with no diagnosis on the + # PR. Three PRs labelled close together is enough to trigger it, and pinning everything to + # stg1 made that routine. So: never dispatch while another run for this env is waiting. + wait_for_slot() { + local i w + for i in $(seq 1 240); do + w=$(gh run list -R "$OPS" --workflow=pr-e2e.yml --limit 20 --json status,displayTitle \ + --jq "[.[] | select((.status==\"queued\" or .status==\"pending\") and (.displayTitle|contains(\"→ ${TARGET}\")))] | length" 2>/dev/null || echo 0) + [ "${w:-0}" -eq 0 ] && return 0 + [ $(( (i-1) % 10 )) -eq 0 ] && echo "[$(date -u +%H:%M:%SZ)] waiting for a free dispatch slot on ${TARGET} — ${w} run(s) already queued ahead" + sleep 30 + done + echo "::error::no free dispatch slot on ${TARGET} after 2h"; return 1 + } + + do_dispatch() { + local before id i + before=$(gh run list -R "$OPS" --workflow=pr-e2e.yml -L 1 --json databaseId --jq '.[0].databaseId // 0') + # github.sha on a pull_request IS the merge commit — the same ref actions/checkout used to + # build the app image, so the specs and the app code come from one commit. + gh workflow run pr-e2e.yml -R "$OPS" --ref main \ + -f source_repo=os \ + -f pr_number="$PR" \ + -f merge_sha='${{ github.sha }}' \ + -f head_sha='${{ github.event.pull_request.head.sha }}' \ + -f app_image='${{ needs.build-app-image.outputs.image-uri }}' \ + -f target_env="$TARGET" \ + -f mode='${{ steps.specs.outputs.mode }}' \ + -f specs='${{ steps.specs.outputs.specs }}' || return 1 + for i in $(seq 1 30); do + sleep 5 + id=$(gh run list -R "$OPS" --workflow=pr-e2e.yml -L 1 --json databaseId --jq '.[0].databaseId // 0') + if [ "$id" != "$before" ] && [ "$id" != "0" ]; then RUN_ID="$id"; return 0; fi + done + return 1 + } + + # Returns: 0 = completed (CONCLUSION set) · 1 = timed out · 2 = EVICTED (retryable) + poll_run() { + local i started=0 strikes=0 any_job=0 exec_ticks=0 last="" phases active busy s + echo "Phases: resolve -> baseline drift check -> sync (only if drifted) -> swap SPA -> e2e -> restore" + # Two separate budgets. Queue time must NOT count against the execution timeout: with one + # serialized runner and ~75-min suites, a PR queued behind one other already exceeds 2h, + # and the caller would give up on a perfectly healthy run — then download a result + # artifact that does not exist yet and report "unknown". os#367 lost its report exactly + # this way (dispatched 17:11, started 18:27, verdict 19:45, caller gave up 19:25). + # While QUEUED the liveness guard below is the protection, not the clock. + for i in $(seq 1 960); do + phases=$(gh api "repos/$OPS/actions/runs/${RUN_ID}/jobs" \ + --jq '[.jobs[] | "\(.name): \(.conclusion // .status)"] | join(" | ")' 2>/dev/null || echo "") + if [ -n "$phases" ] && [ "$phases" != "$last" ]; then + echo "[$(date -u +%H:%M:%SZ)] $phases"; last="$phases" + fi + if [ "$started" = "0" ]; then + active=$(gh api "repos/$OPS/actions/runs/${RUN_ID}/jobs" \ + --jq '[.jobs[] | select(.status != "queued")] | length' 2>/dev/null || echo 0) + if [ "${active:-0}" -gt 0 ]; then + started=1; any_job=1 + echo "[$(date -u +%H:%M:%SZ)] run started executing — 2h execution budget begins now" + fi + else + exec_ticks=$((exec_ticks + 1)) + if [ "$exec_ticks" -ge 240 ]; then + CONCLUSION=timed_out + echo "::error::the run has been EXECUTING for 2h without finishing. https://github.com/$OPS/actions/runs/${RUN_ID}" + return 1 + fi + fi + if [ "$started" = "0" ]; then + # An offline runner never appears executing ANY job anywhere; a merely BUSY one does. + # Elapsed time cannot tell them apart when one serialized runner handles ~74-min suites. + busy=$(for rid in $(gh run list -R "$OPS" --limit 10 --json databaseId --jq '.[].databaseId' 2>/dev/null); do + gh api "repos/$OPS/actions/runs/$rid/jobs" \ --jq '.jobs[] | select(.status=="in_progress") | .runner_name // empty' 2>/dev/null done | grep -cx "$RUNNER" || true) - if [ "${BUSY:-0}" -gt 0 ]; then - STRIKES=0 # runner is alive, we are simply queued behind work + if [ "${busy:-0}" -gt 0 ]; then + strikes=0 else - STRIKES=$((STRIKES + 1)) # queued AND nothing running on it - if [ "$STRIKES" -ge 30 ]; then # 15 min of both, consecutively - echo "conclusion=runner_unavailable" >> "$GITHUB_OUTPUT" - echo "::error::${RUNNER} has been idle for 15 min while this run stayed queued — the runner is offline or not accepting jobs. https://github.com/iblai/iblai-deploy-ops/actions/runs/${RUN_ID}" - exit 1 + strikes=$((strikes + 1)) + if [ "$strikes" -ge 30 ]; then + CONCLUSION=runner_unavailable + echo "::error::${RUNNER} has been idle for 15 min while this run stayed queued — the runner is offline or not accepting jobs. https://github.com/$OPS/actions/runs/${RUN_ID}" + return 1 fi fi fi - fi # up to 2h: image build + full suite - PHASES=$(gh api "repos/iblai/iblai-deploy-ops/actions/runs/${RUN_ID}/jobs" \ - --jq '[.jobs[] | "\(.name): \(.conclusion // .status)"] | join(" | ")' 2>/dev/null || echo "") - if [ -n "$PHASES" ] && [ "$PHASES" != "$LAST" ]; then - echo "[$(date -u +%H:%M:%SZ)] $PHASES" - LAST="$PHASES" + s=$(gh api "repos/$OPS/actions/runs/${RUN_ID}" --jq '.status+"|"+(.conclusion//"")' 2>/dev/null) + if [ "${s%%|*}" = "completed" ]; then + CONCLUSION="${s##*|}" + # Cancelled having never started a job == evicted from the pending slot, not a result. + if [ "$CONCLUSION" = "cancelled" ] && [ "$any_job" = "0" ]; then return 2; fi + echo "e2e finished: $CONCLUSION" + return 0 + fi + sleep 30 + done + CONCLUSION=timed_out + echo "::error::gave up after 8h — the run neither started nor finished"; return 1 + } + + # Every waiter notices the freed slot within the same poll window and would dispatch in the + # same instant — a classic check-then-act race. A deterministic per-PR offset spreads them + # out; the RE-CHECK after it is what actually closes the window, since the stagger alone + # would only move the collision. PR number rather than $RANDOM: unique, stable across + # retries, and needs no coordination between callers. + stagger_then_claim() { + local s + wait_for_slot || return 1 + s=$(( (PR % 12) * 5 )) + if [ "$s" -gt 0 ]; then + echo "[$(date -u +%H:%M:%SZ)] stagger ${s}s (PR ${PR}) before claiming the slot" + sleep "$s" fi - S=$(gh api "repos/iblai/iblai-deploy-ops/actions/runs/${RUN_ID}" --jq '.status+"|"+(.conclusion//"")' 2>/dev/null) - if [ "${S%%|*}" = "completed" ]; then - echo "conclusion=${S##*|}" >> "$GITHUB_OUTPUT" - echo "e2e finished: ${S##*|}" - exit 0 + wait_for_slot || return 1 # someone may have claimed it while we staggered + } + + RUN_ID=""; CONCLUSION=""; EVICTIONS=0 + for attempt in $(seq 1 10); do + stagger_then_claim || { echo "conclusion=no_slot" >> "$GITHUB_OUTPUT"; exit 1; } + do_dispatch || { echo "::error::dispatched but the run never appeared"; echo "conclusion=dispatch_failed" >> "$GITHUB_OUTPUT"; exit 1; } + echo "dispatched run $RUN_ID (attempt $attempt)" + echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" + poll_run; rc=$? + if [ "$rc" -eq 2 ]; then + EVICTIONS=$((EVICTIONS + 1)) + # Greppable marker: this is the signal to watch. If evictions become routine the + # caller-side queue is under-sized and the fix is capacity (a second env) or a real + # external FIFO queue — not more retries. See docs/pr-e2e.md. + echo "::warning::QUEUE_EVICTION run ${RUN_ID} was cancelled before starting any job — evicted from the concurrency queue by a newer dispatch. Re-dispatching (attempt ${attempt}/10)." + sleep $(( (RANDOM % 20) + 5 )) # jitter so retriers do not re-collide + continue fi - sleep 30 + break done - echo "conclusion=timed_out" >> "$GITHUB_OUTPUT" - echo "::error::timed out after 2h waiting for the e2e run"; exit 1 + echo "evictions=${EVICTIONS}" >> "$GITHUB_OUTPUT" + if [ "$EVICTIONS" -gt 0 ]; then + echo "::notice::QUEUE_EVICTION_COUNT=${EVICTIONS} for this PR" + fi + echo "conclusion=${CONCLUSION:-unknown}" >> "$GITHUB_OUTPUT" + - name: Cancel the dispatched run if this caller is cancelled + # A caller can be superseded (concurrency: pr-e2e-, cancel-in-progress) or cancelled by + # hand — but the central run it dispatched keeps going. That orphan occupies the single env + # runner for a full suite, produces a result nobody reads, and deepens the queue for everyone + # else. Worse, it can PASS while the PR shows red: os#394 did exactly that (456/0/3 green, + # PR reported "unknown"), because the surviving caller was watching a different run. + if: cancelled() && steps.dispatch.outputs.run_id != '' + env: + GH_TOKEN: ${{ secrets.DEPLOY_OPS_DISPATCH_TOKEN }} + RUN_ID: ${{ steps.dispatch.outputs.run_id }} + run: | + echo "caller cancelled — cancelling central run ${RUN_ID} so it does not orphan" + gh run cancel "$RUN_ID" -R iblai/iblai-deploy-ops || true - name: Publish the result on this PR if: always() && steps.dispatch.outputs.run_id != '' @@ -281,7 +355,17 @@ jobs: set -uo pipefail # The Playwright report on tests.iblai.app IS public, so it is the link that actually helps # a developer here — unlike the private Actions run. Relay it onto the PR itself. - gh run download "$RUN_ID" -R iblai/iblai-deploy-ops -n pr-e2e-result -D /tmp/res 2>/dev/null || true + # Retry briefly: the artifact is uploaded by the central run's LAST job, so a caller that + # reaches here early (or races the upload) would otherwise silently render "unknown" and + # throw away a perfectly good result — which is exactly how os#367 lost its report link. + for a in 1 2 3 4 5 6; do + gh run download "$RUN_ID" -R iblai/iblai-deploy-ops -n pr-e2e-result -D /tmp/res 2>/dev/null && break + echo " result artifact not available yet (attempt $a/6) — retrying in 20s" + sleep 20 + done + if [ ! -f /tmp/res/pr-e2e-result.json ]; then + echo "::warning::could not download pr-e2e-result from run ${RUN_ID} — the comment will lack a report link. https://github.com/iblai/iblai-deploy-ops/actions/runs/${RUN_ID}" + fi python3 - <<'PY' > /tmp/body.md import json, pathlib p = pathlib.Path("/tmp/res/pr-e2e-result.json") @@ -325,7 +409,7 @@ jobs: - name: Gate on the e2e result if: always() run: | - C="${{ steps.wait.outputs.conclusion }}" + C="${{ steps.dispatch.outputs.conclusion }}" echo "e2e conclusion: ${C:-}" [ "$C" = "success" ] # ============================================================