mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
## Summary - Skip Subconscious ticks before writing per-task activity rows when no OpenHuman session/local provider path is available. - Add provider availability fields to `subconscious_status` and show a paused/configuration banner in Intelligence > Subconscious. - Route the tick evaluator through `subconscious_provider` while reusing the existing memory-tree chat provider plumbing. Closes #1374 ## Testing - [x] `cargo fmt --all --check` - [x] `pnpm --filter openhuman-app test -- src/components/intelligence/__tests__/IntelligenceSubconsciousTab.test.tsx` - [x] `pnpm --filter openhuman-app compile` - [x] `pnpm i18n:check` - [x] `git diff --check` - [x] `cargo test -p openhuman subconscious::engine::tests --lib` attempted locally, blocked before tests by `whisper-rs-sys` missing `libclang` (`LIBCLANG_PATH` unset). ## PR Checklist - [x] I linked the relevant issue(s) above. - [x] I added or updated tests for the changed behavior, or explained why this is not needed. - [x] I ran the relevant local checks, or documented why they could not be run. - [x] I kept the change scoped to the issue. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Show an amber warning when the AI provider is unavailable, display the unavailable reason, provide a quick link to AI settings, disable the "Run Now" button, and show a translated tooltip explaining the unavailable status. * **Internationalization** * Added provider-unavailable title and settings label translations across multiple languages. * **Tests** * Added tests covering the provider-unavailable UI, disabled Run Now behavior, and settings navigation. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2314?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: aqilaziz <gonzes7@gmail.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
/**
|
|
* Vitest for the Intelligence Subconscious tab (#623).
|
|
*
|
|
* Covers `handleNavigateToReflectionThread` — the callback passed to
|
|
* `SubconsciousReflectionCards`. The function is small but load-bearing:
|
|
* it dispatches `setSelectedThread(threadId)` so `Conversations` resumes
|
|
* the new thread on mount, then routes to `/chat` (the unified chat
|
|
* surface; `/conversations` redirects to `/home`). Both dispatch and
|
|
* navigate are mocked so we can assert the contract without spinning up
|
|
* the full Redux/router stack.
|
|
*/
|
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
import type { ComponentProps } from 'react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { setSelectedThread } from '../../../store/threadSlice';
|
|
import IntelligenceSubconsciousTab from '../IntelligenceSubconsciousTab';
|
|
|
|
const mockDispatch = vi.fn();
|
|
const mockNavigate = vi.fn();
|
|
|
|
vi.mock('react-redux', () => ({ useDispatch: () => mockDispatch, useSelector: () => 'en' }));
|
|
|
|
vi.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate }));
|
|
|
|
// Stub out the cards component so we can trigger the navigate callback
|
|
// directly without exercising the RPC / polling path (already covered by
|
|
// `SubconsciousReflectionCards.test.tsx`). The stub renders a button
|
|
// that fires `onNavigateToThread` with a known thread id when clicked.
|
|
vi.mock('../SubconsciousReflectionCards', () => ({
|
|
default: ({ onNavigateToThread }: { onNavigateToThread?: (id: string) => void }) => (
|
|
<button
|
|
type="button"
|
|
data-testid="cards-stub-trigger"
|
|
onClick={() => onNavigateToThread?.('spawned-thread-42')}>
|
|
trigger
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
function baseProps() {
|
|
return {
|
|
addSubconsciousTask: vi.fn(),
|
|
approveEscalation: vi.fn(),
|
|
dismissEscalation: vi.fn(),
|
|
expandedLogIds: new Set<string>(),
|
|
logEntries: [],
|
|
newTaskTitle: '',
|
|
removeSubconsciousTask: vi.fn(),
|
|
setExpandedLogIds: vi.fn(),
|
|
setNewTaskTitle: vi.fn(),
|
|
status: null as ComponentProps<typeof IntelligenceSubconsciousTab>['status'],
|
|
tasks: [],
|
|
toggleSubconsciousTask: vi.fn(),
|
|
triggerTick: vi.fn(),
|
|
triggering: false,
|
|
escalations: [],
|
|
loading: false,
|
|
};
|
|
}
|
|
|
|
describe('IntelligenceSubconsciousTab', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('on Act → dispatches setSelectedThread + navigates to /chat', () => {
|
|
render(<IntelligenceSubconsciousTab {...baseProps()} />);
|
|
fireEvent.click(screen.getByTestId('cards-stub-trigger'));
|
|
// Redux dispatch payload should match the slice's action creator
|
|
// exactly — comparing the produced action keeps the assertion robust
|
|
// if the slice path changes.
|
|
expect(mockDispatch).toHaveBeenCalledWith(setSelectedThread('spawned-thread-42'));
|
|
// Route must be `/chat` (the unified chat surface), not
|
|
// `/conversations` — the latter falls through to a `/home` redirect
|
|
// and the user lands somewhere unexpected.
|
|
expect(mockNavigate).toHaveBeenCalledWith('/chat');
|
|
});
|
|
|
|
it('shows provider unavailable state and blocks manual ticks', () => {
|
|
const triggerTick = vi.fn();
|
|
render(
|
|
<IntelligenceSubconsciousTab
|
|
{...baseProps()}
|
|
triggerTick={triggerTick}
|
|
status={{
|
|
enabled: true,
|
|
provider_available: false,
|
|
provider_unavailable_reason: 'Sign in or configure a local Subconscious provider.',
|
|
interval_minutes: 5,
|
|
last_tick_at: null,
|
|
total_ticks: 0,
|
|
task_count: 3,
|
|
pending_escalations: 0,
|
|
consecutive_failures: 1,
|
|
}}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText('Subconscious is paused')).toBeInTheDocument();
|
|
expect(screen.getByText(/configure a local Subconscious provider/i)).toBeInTheDocument();
|
|
|
|
const runNow = screen.getByRole('button', { name: /Run Now/i });
|
|
expect(runNow).toBeDisabled();
|
|
fireEvent.click(runNow);
|
|
expect(triggerTick).not.toHaveBeenCalled();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /AI settings/i }));
|
|
expect(mockNavigate).toHaveBeenCalledWith('/settings/llm');
|
|
});
|
|
});
|