fix(B9): surface builder strategies in strategies tab

This commit is contained in:
Saravana Achu Mac 2026-05-04 17:21:18 -07:00
parent 81c52479ab
commit 70db4c9a04
4 changed files with 161 additions and 11 deletions

View File

@ -0,0 +1,52 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createTradeProfile } from './profileApi';
vi.mock('./authSession', () => ({
getPlatformAccessToken: vi.fn(async () => 'test-token'),
}));
vi.mock('./runtime', () => ({
tradingRuntime: { tradingApiUrl: 'https://trading.test' },
}));
describe('profileApi profile update notifications', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('dispatches profiles-updated after creating a trade profile', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({
profile: {
id: 'profile-visual-1',
name: 'Visual Strategy',
symbols: 'AAPL',
allocated_capital: 1000,
risk_per_trade_percent: 1,
is_active: false,
strategy_config: { type: 'visual', rules: [] },
},
}),
} as Response);
const listener = vi.fn();
window.addEventListener('profiles-updated', listener);
await createTradeProfile({
name: 'Visual Strategy',
symbols: 'AAPL',
allocated_capital: 1000,
risk_per_trade_percent: 1,
is_active: false,
strategy_config: { type: 'visual', rules: [] },
});
expect(fetchMock).toHaveBeenCalledWith(
'https://trading.test/api/profiles',
expect.objectContaining({ method: 'POST' }),
);
expect(listener).toHaveBeenCalledTimes(1);
window.removeEventListener('profiles-updated', listener);
});
});

View File

@ -55,6 +55,12 @@ async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
return body as T;
}
function notifyProfilesUpdated() {
if (typeof window !== 'undefined') {
window.dispatchEvent(new Event('profiles-updated'));
}
}
export async function fetchCurrentUserProfile(): Promise<CurrentUserProfile> {
const response = await apiRequest<{ profile: CurrentUserProfile }>('/api/me/profile');
return response.profile;
@ -86,6 +92,7 @@ export async function createTradeProfile(payload: TradeProfilePayload): Promise<
method: 'POST',
body: JSON.stringify(payload),
});
notifyProfilesUpdated();
return response.profile;
}
@ -94,6 +101,7 @@ export async function updateTradeProfile(id: string, payload: Partial<TradeProfi
method: 'PUT',
body: JSON.stringify(payload),
});
notifyProfilesUpdated();
return response.profile;
}
@ -102,6 +110,7 @@ export async function setTradeProfileActive(id: string, isActive: boolean): Prom
method: 'PATCH',
body: JSON.stringify({ is_active: isActive }),
});
notifyProfilesUpdated();
return response.profile;
}
@ -109,4 +118,5 @@ export async function deleteTradeProfile(id: string): Promise<void> {
await apiRequest<{ success: boolean }>(`/api/profiles/${id}`, {
method: 'DELETE',
});
notifyProfilesUpdated();
}

View File

@ -0,0 +1,81 @@
// @vitest-environment jsdom
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MyStrategiesTab } from './MyStrategiesTab';
const { fetchTradeProfilesMock } = vi.hoisted(() => ({
fetchTradeProfilesMock: vi.fn(),
}));
vi.mock('../components/AuthContext', () => ({
useAuth: () => ({
user: { id: 'user-1', email: 'trader@example.test' },
profile: { subscription_tier: 'pro' },
}),
}));
vi.mock('../lib/profileApi', () => ({
fetchTradeProfiles: fetchTradeProfilesMock,
setTradeProfileActive: vi.fn(),
deleteTradeProfile: vi.fn(),
}));
vi.mock('../lib/StrategyExplanationService', () => ({
getStrategyExplanation: () => ({ reason: 'Ready for review', recommendation: 'Review sizing.' }),
}));
vi.mock('../backtest/useBacktestFeatureGate', () => ({
useBacktestFeatureGate: () => ({ enabled: false, loading: false }),
}));
vi.mock('../components/StrategyWizard', () => ({
StrategyWizard: () => <div>Strategy wizard</div>,
}));
vi.mock('../backtest/components/BacktestRunnerPanel', () => ({
BacktestRunnerPanel: () => <div>Backtest runner</div>,
}));
const botState = {
connected: true,
symbols: {
AAPL: { change24h: 1.2 },
MSFT: { change24h: -0.4 },
},
};
describe('MyStrategiesTab builder strategy visibility', () => {
beforeEach(() => {
fetchTradeProfilesMock.mockReset();
});
it('shows saved visual and code strategies with their builder type badges', async () => {
fetchTradeProfilesMock.mockResolvedValue([
{
id: 'visual-1',
name: 'Visual Momentum',
symbols: 'AAPL',
allocated_capital: 1000,
risk_per_trade_percent: 1,
is_active: false,
strategy_config: { type: 'visual', rules: [] },
},
{
id: 'code-1',
name: 'Code Mean Reversion',
symbols: 'MSFT',
allocated_capital: 2000,
risk_per_trade_percent: 1,
is_active: false,
strategy_config: { type: 'code', language: 'javascript', code: 'function strategy() {}' },
},
]);
render(<MyStrategiesTab botState={botState} />);
expect(await screen.findByText('Visual Momentum')).toBeInTheDocument();
expect(screen.getByText('Code Mean Reversion')).toBeInTheDocument();
expect(screen.getByText('Visual Builder')).toBeInTheDocument();
expect(screen.getByText('Code Strategy')).toBeInTheDocument();
});
});

View File

@ -28,6 +28,12 @@ import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel'
import { useBacktestFeatureGate } from '../backtest/useBacktestFeatureGate';
import { deleteTradeProfile, fetchTradeProfiles, setTradeProfileActive } from '../lib/profileApi';
function getStrategyKindLabel(config: any) {
if (config?.type === 'visual') return 'Visual Builder';
if (config?.type === 'code') return 'Code Strategy';
return 'V4.0 Core';
}
const ActiveStrategyCard: React.FC<{
profile: any;
botState: any;
@ -42,6 +48,7 @@ const ActiveStrategyCard: React.FC<{
const config = profile.strategy_config;
const isAggressive = config?.execution?.minRulePassRatio < 0.9;
const isSafe = config?.execution?.minRulePassRatio >= 1.0;
const strategyKindLabel = getStrategyKindLabel(config);
const explanation = getStrategyExplanation(profile, botState);
@ -117,7 +124,7 @@ const ActiveStrategyCard: React.FC<{
{profile.symbols}
</span>
<span style={{ background: 'rgba(255,255,255,0.03)', color: '#666', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.05)', textTransform: 'uppercase' }}>
V4.0 Core
{strategyKindLabel}
</span>
</div>
</div>