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

@ -27,8 +27,14 @@ import { StrategyWizard } from '../components/StrategyWizard';
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
import { useBacktestFeatureGate } from '../backtest/useBacktestFeatureGate';
import { deleteTradeProfile, fetchTradeProfiles, setTradeProfileActive } from '../lib/profileApi';
const ActiveStrategyCard: React.FC<{
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;
tier: string;
@ -39,9 +45,10 @@ const ActiveStrategyCard: React.FC<{
isExpanded: boolean;
onToggleExpand: (id: string) => void;
}> = ({ profile, botState, tier, onToggle, onEdit, onBacktest, onDelete, isExpanded, onToggleExpand }) => {
const config = profile.strategy_config;
const isAggressive = config?.execution?.minRulePassRatio < 0.9;
const isSafe = config?.execution?.minRulePassRatio >= 1.0;
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);
@ -115,12 +122,12 @@ const ActiveStrategyCard: React.FC<{
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '12px' }}>
<span style={{ background: 'rgba(0,255,136,0.05)', color: '#00ff88', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid rgba(0,255,136,0.1)', textTransform: 'uppercase' }}>
{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
</span>
</div>
</div>
</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' }}>
{strategyKindLabel}
</span>
</div>
</div>
{/* 4. Operational DNA (Specs) */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px', marginBottom: '28px' }}>