feat(M6): Weitere Hosts — MiniApps in Fenstern + AI-Agenten-Ausgabe-Bloecke (#364)

- Windows-Host: openMiniAppWindow-Helper + MiniAppWindowContent (windowStore);
  Oeffnen-Buttons im Chat-Block (MiniAppBlock) und Dashboard-Widget
- AI-Agenten-Host: Core-Tool send_miniapp (app/ai/miniapp_tools.py) —
  miniapp-Block in Agent-Chat (approval_request-Praezedenz), Permission
  fail-closed gegen aufrufenden User pro App; Registrierung im lifespan
- agent_loop: tool_context + agent_name (Raum-Aufloesung)
- Fix: MiniAppBlock nutzt useMiniapps (component-Feld) statt Legacy /comm/miniapps
- Tests: M6 7/7 (TDD rot->gruen), Backend-Regression 57/57, Vitest 26/26
  (4 neue Window-Tests), tsc clean, build OK
This commit is contained in:
Agent Zero
2026-08-31 01:04:33 +02:00
parent 63aa0cf788
commit 335762dd3d
11 changed files with 538 additions and 29 deletions
@@ -0,0 +1,103 @@
import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MiniAppWindowContent } from '@/components/dashboard/MiniAppWindowContent';
import { openMiniAppWindow } from '@/components/dashboard/openMiniAppWindow';
import { useWindowStore } from '@/store/windowStore';
/**
* M6 — Windows host tests: MiniApps open in floating windows via the
* existing window manager (openMiniAppWindow + MiniAppWindowContent).
*/
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
vi.mock('@/api/miniapps', () => ({
useMiniapps: () => ({
data: {
items: [
{
app_id: 'recent_contacts', name: 'Recent Contacts', icon: 'Users', description: 'desc',
plugin_name: 'contacts', render_schema: {}, permission: '', settings_schema: {},
col_span: 2, row_span: 1, hosts: ['chat', 'dashboard', 'window'],
component: '@/components/dashboard/RecentContactsWidget', order: 10, builtin: true,
},
],
total: 1,
},
isLoading: false,
}),
}));
vi.mock('@/components/dashboard/MiniAppHost', () => ({
MiniAppHost: ({ appId }: { appId: string }) => (
<div data-testid={`miniapp-${appId}`}>host:{appId}</div>
),
}));
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
function renderContent() {
return render(
<QueryClientProvider client={queryClient}>
<MiniAppWindowContent appId="recent_contacts" settings={{ limit: 5 }} />
</QueryClientProvider>
);
}
describe('MiniAppWindowContent', () => {
it('renders the MiniApp host with the given app id', () => {
renderContent();
expect(screen.getByTestId('miniapp-window-recent_contacts')).toBeInTheDocument();
expect(screen.getByTestId('miniapp-recent_contacts')).toBeInTheDocument();
});
it('shows the app description when present', () => {
renderContent();
expect(screen.getByText('desc')).toBeInTheDocument();
});
});
describe('openMiniAppWindow', () => {
it('opens a window with miniapp type and props', () => {
const spy = vi.fn();
// spy on the store's openWindow implementation
const state = useWindowStore.getState();
const origOpen = state.openWindow;
useWindowStore.setState({ openWindow: spy as never });
openMiniAppWindow({
appId: 'recent_contacts',
settings: { limit: 3 },
component: MiniAppWindowContent as never,
});
expect(spy).toHaveBeenCalledTimes(1);
const config = spy.mock.calls[0][0];
expect(config.type).toBe('miniapp-recent_contacts');
expect(config.title).toBe('recent_contacts');
expect(config.componentProps).toEqual({ appId: 'recent_contacts', settings: { limit: 3 } });
// restore
useWindowStore.setState({ openWindow: origOpen as never, windows: [] });
});
it('uses the def name as window title when provided', () => {
const spy = vi.fn();
const state = useWindowStore.getState();
const origOpen = state.openWindow;
useWindowStore.setState({ openWindow: spy as never });
openMiniAppWindow({
appId: 'x',
def: { name: 'Mein Widget' } as never,
component: MiniAppWindowContent as never,
});
expect(spy.mock.calls[0][0].title).toBe('Mein Widget');
useWindowStore.setState({ openWindow: origOpen as never, windows: [] });
});
});