chore(chat): harden pipeline and deploy flow
This commit is contained in:
parent
4ab97acab7
commit
b4f68725ef
1
.gitignore
vendored
1
.gitignore
vendored
@ -29,3 +29,4 @@ mobile/package-lock.json
|
||||
|
||||
# Claude Code session metadata
|
||||
.claude/
|
||||
.tmp/
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"description": "ByteLyst Trading backend and execution control service",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "npm run check:websocket-contract && npm run check:session-rule-normalization && npm run check:api-contract && npm run check:audit-repository && npm run check:market-data-endpoints && npm run check:fmp-cache && npm run check:backtest-strategy-safety",
|
||||
"test": "npm run check:websocket-contract && npm run check:session-rule-normalization && npm run check:api-contract && npm run check:audit-repository && npm run check:market-data-endpoints && npm run check:chat-copilot-contract && npm run check:chat-copilot-fallbacks && npm run check:fmp-cache && npm run check:backtest-strategy-safety",
|
||||
"dev": "node --import tsx src/bootstrap.ts",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
@ -31,6 +31,7 @@
|
||||
"check:audit-repository": "node --import tsx verifyAuditRepository.ts",
|
||||
"check:market-data-endpoints": "node --import tsx verifyMarketDataEndpoints.ts",
|
||||
"check:chat-copilot-contract": "node --import tsx verifyChatCopilotContract.ts",
|
||||
"check:chat-copilot-fallbacks": "node --import tsx testChatCopilotFallbacks.ts",
|
||||
"check:fmp-cache": "node --import tsx testFmpCache.ts",
|
||||
"check:backtest-strategy-safety": "node --import tsx testBacktestStrategySafety.ts",
|
||||
"check:websocket-contract": "node --import tsx src/scripts/verifyWebsocketContract.ts",
|
||||
|
||||
153
backend/testChatCopilotFallbacks.ts
Normal file
153
backend/testChatCopilotFallbacks.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { ApiServer } from './src/services/apiServer.js';
|
||||
|
||||
type ChatContext = Parameters<any>[1];
|
||||
|
||||
function createServerHarness() {
|
||||
return Object.create(ApiServer.prototype) as any;
|
||||
}
|
||||
|
||||
function buildContext(): ChatContext {
|
||||
return {
|
||||
profiles: [
|
||||
{
|
||||
id: 'p1',
|
||||
name: 'High Risk Scalper',
|
||||
allocated_capital: 1000,
|
||||
risk_per_trade_percent: 1.25,
|
||||
symbols: 'BTC/USDT',
|
||||
is_active: true,
|
||||
strategy_config: {
|
||||
rules: [{ ruleId: 'TrendBiasRule', enabled: true }],
|
||||
riskLimits: { maxDailyLossUsd: 50, maxOpenTrades: 2, maxConsecutiveLosses: 2 },
|
||||
execution: { orderType: 'market', cooldownMinutes: 20, entryMode: 'both' }
|
||||
}
|
||||
}
|
||||
],
|
||||
runtime: {
|
||||
positions: [
|
||||
{
|
||||
symbol: 'BTC/USDT',
|
||||
side: 'BUY',
|
||||
entryPrice: 60000,
|
||||
currentPrice: 61500,
|
||||
unrealizedPnl: 375,
|
||||
unrealizedPnlPercent: 2.5,
|
||||
size: 0.25,
|
||||
profileId: 'p1',
|
||||
profileName: 'High Risk Scalper',
|
||||
tradeId: 'trade-1',
|
||||
takeProfit: 63000,
|
||||
stopLoss: 58500,
|
||||
}
|
||||
],
|
||||
signalContexts: [
|
||||
{
|
||||
symbol: 'BTC/USDT',
|
||||
profileId: 'p1',
|
||||
profileName: 'High Risk Scalper',
|
||||
signal: 'BUY',
|
||||
passed: false,
|
||||
reason: 'Waiting for stronger rule alignment.',
|
||||
executionStatus: 'SKIPPED',
|
||||
executionCode: 'rule_ratio_not_met',
|
||||
executionReason: 'Only 2 of 4 voting rules passed.',
|
||||
orderId: 'ord-1',
|
||||
}
|
||||
],
|
||||
recentOrders: [],
|
||||
recentHistory: [
|
||||
{ symbol: 'BTC/USDT', side: 'BUY', pnl: 125, reason: 'Simple target hit', tradeId: 'h1' },
|
||||
{ symbol: 'ETH/USDT', side: 'BUY', pnl: -40, reason: 'Stop loss', tradeId: 'h2' },
|
||||
],
|
||||
orderFailures: [
|
||||
{
|
||||
symbol: 'BTC/USDT',
|
||||
side: 'BUY',
|
||||
qty: 0.1,
|
||||
reason: 'Duplicate entry request blocked',
|
||||
tradeId: 'trade-1',
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
],
|
||||
operationalEvents: [
|
||||
{
|
||||
id: 'evt-1',
|
||||
type: 'RECONCILIATION_ALERT',
|
||||
severity: 'WARN',
|
||||
message: 'Reconciliation mismatch detected for BTC/USDT',
|
||||
symbol: 'BTC/USDT',
|
||||
profileId: 'p1',
|
||||
tradeId: 'trade-1',
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
],
|
||||
accountSnapshot: null,
|
||||
health: {
|
||||
tradingLoopHealthy: true,
|
||||
orderSyncHealthy: true,
|
||||
reconciliationLoopHealthy: false,
|
||||
reconciliationMismatchCount: 2,
|
||||
reconciliationMissingFromExchange: 1,
|
||||
reconciliationMissingInDb: 0,
|
||||
reconciliationNoGoTrades: 1,
|
||||
reconciliationParityQuarantinedTrades: 1,
|
||||
reconciliationParityAutoClosedTrades: 0,
|
||||
reconciliationIntegrityWatchdogTriggered: false,
|
||||
lockContentionCount: 0,
|
||||
reconciliationLockContentionCount: 0,
|
||||
},
|
||||
settings: {
|
||||
executionMode: 'paper',
|
||||
totalCapital: 1000,
|
||||
riskPerTrade: 1.25,
|
||||
maxOpenTrades: 2,
|
||||
isAlgoEnabled: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function testTradePlanRecommendation() {
|
||||
const server = createServerHarness();
|
||||
const response = server.buildLocalChatFallback('Recommend a trade plan for my BTC position', buildContext());
|
||||
assert.equal(response.action, 'recommend_trade_plan');
|
||||
assert.ok(Array.isArray(response.insights) && response.insights.length > 0, 'trade-plan recommendation must include insights');
|
||||
assert.ok(response.quickLinks?.some((link: any) => link.kind === 'plans'), 'trade-plan recommendation must include a plans link');
|
||||
console.log('[PASS] chat fallback builds trade-plan recommendation.');
|
||||
}
|
||||
|
||||
function testReconciliationFollowup() {
|
||||
const server = createServerHarness();
|
||||
const response = server.buildLocalChatFallback('What should I do about reconciliation right now?', buildContext());
|
||||
assert.equal(response.action, 'recommend_reconciliation_followup');
|
||||
assert.ok(response.quickLinks?.some((link: any) => link.kind === 'settings'), 'reconciliation follow-up must include settings/admin quick link');
|
||||
assert.ok(response.insights?.some((entry: string) => entry.includes('Mismatch count')), 'reconciliation follow-up must include mismatch insights');
|
||||
console.log('[PASS] chat fallback builds reconciliation follow-up.');
|
||||
}
|
||||
|
||||
function testRecentTradeReview() {
|
||||
const server = createServerHarness();
|
||||
const response = server.buildLocalChatFallback('Review my recent trades', buildContext());
|
||||
assert.equal(response.action, 'review_recent_trades');
|
||||
assert.ok(response.insights?.some((entry: string) => entry.includes('BTC/USDT')), 'recent trade review must include trade evidence');
|
||||
console.log('[PASS] chat fallback builds recent-trade review.');
|
||||
}
|
||||
|
||||
function testWaitingExplanation() {
|
||||
const server = createServerHarness();
|
||||
const response = server.buildLocalChatFallback('Why did no trade fire for BTC?', buildContext());
|
||||
assert.equal(response.action, 'explain_waiting');
|
||||
assert.ok(response.insights?.some((entry: string) => entry.includes('Execution code')), 'waiting explanation must include execution insight');
|
||||
console.log('[PASS] chat fallback builds waiting explanation.');
|
||||
}
|
||||
|
||||
function main() {
|
||||
testTradePlanRecommendation();
|
||||
testReconciliationFollowup();
|
||||
testRecentTradeReview();
|
||||
testWaitingExplanation();
|
||||
console.log('Chat copilot fallback checks passed');
|
||||
}
|
||||
|
||||
main();
|
||||
28
scripts/deploy-hotcopy.sh
Executable file
28
scripts/deploy-hotcopy.sh
Executable file
@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
echo "Building web and backend artifacts..."
|
||||
pnpm --filter @bytelyst/trading-web build
|
||||
pnpm --filter @bytelyst/trading-backend build
|
||||
|
||||
echo "Copying frontend assets into invttrdg-web..."
|
||||
docker cp web/dist/. invttrdg-web:/usr/share/nginx/html
|
||||
|
||||
echo "Copying backend apiServer bundle into invttrdg-backend..."
|
||||
docker cp backend/dist/backend/src/services/apiServer.js invttrdg-backend:/app/backend/dist/backend/src/services/apiServer.js
|
||||
|
||||
echo "Restarting invttrdg-backend..."
|
||||
docker restart invttrdg-backend >/dev/null
|
||||
|
||||
echo "Waiting for backend readiness..."
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||
READY_JSON="$(curl -s http://127.0.0.1:4025/health/ready || true)"
|
||||
echo "$READY_JSON" | grep -q '"status":"healthy"' && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Public asset summary:"
|
||||
curl -s https://invttrdg.bytelyst.com | grep -o 'index-[^"[:space:]]*\.js\|index-[^"[:space:]]*\.css' | head
|
||||
|
||||
echo "Backend readiness:"
|
||||
curl -s http://127.0.0.1:4025/health/ready
|
||||
86
web/src/components/ChatCopilotRoutes.dom.test.tsx
Normal file
86
web/src/components/ChatCopilotRoutes.dom.test.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
// @vitest-environment jsdom
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { ChatControl } from './ChatControl';
|
||||
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
|
||||
import type { BotState } from '../hooks/useWebSocket';
|
||||
|
||||
const { getPlatformAccessTokenMock } = vi.hoisted(() => ({
|
||||
getPlatformAccessTokenMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../lib/authSession', () => ({
|
||||
getPlatformAccessToken: getPlatformAccessTokenMock,
|
||||
}));
|
||||
|
||||
const botStateFixture: BotState = {
|
||||
...DEFAULT_BOT_STATE,
|
||||
positions: [
|
||||
{
|
||||
id: 'pos-1',
|
||||
symbol: 'BTC/USDT',
|
||||
side: 'BUY' as const,
|
||||
size: 0.25,
|
||||
entryPrice: 60000,
|
||||
currentPrice: 61500,
|
||||
stopLoss: 58500,
|
||||
takeProfit: 63000,
|
||||
unrealizedPnl: 375,
|
||||
unrealizedPnlPercent: 2.5,
|
||||
marketValue: 15375,
|
||||
profileId: 'p1',
|
||||
profileName: 'High Risk Scalper',
|
||||
tradeId: 'trade-1',
|
||||
}
|
||||
],
|
||||
symbols: {},
|
||||
health: {
|
||||
...DEFAULT_BOT_STATE.health!,
|
||||
reconciliationMismatchCount: 2,
|
||||
},
|
||||
};
|
||||
|
||||
describe('Chat copilot route flow', () => {
|
||||
it('navigates from assistant quick links into real app routes', async () => {
|
||||
getPlatformAccessTokenMock.mockResolvedValue('token-route');
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
summary: 'A Trade Plan would help manage BTC/USDT more explicitly from here.',
|
||||
reasoning: 'You already have a live holding on BTC/USDT.',
|
||||
action: 'recommend_trade_plan',
|
||||
quickLinks: [
|
||||
{ kind: 'plans', label: 'Manage in Plans', mode: 'sell', symbol: 'BTC/USDT', tradeId: 'trade-1' }
|
||||
]
|
||||
})
|
||||
} as any));
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<Routes>
|
||||
<Route path="/" element={<ChatControl profiles={[]} botState={botStateFixture} onApplyProfile={vi.fn()} />} />
|
||||
<Route path="/plans" element={<div>Plans route content</div>} />
|
||||
<Route path="/portfolio" element={<div>Portfolio route content</div>} />
|
||||
<Route path="/settings" element={<div>Settings route content</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await user.click(screen.getAllByRole('button')[0]);
|
||||
await user.type(screen.getByPlaceholderText(/Ask for a profile, holding explanation, or reconciliation help/i), 'Recommend a trade plan for BTC/USDT{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /Manage in Plans/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Manage in Plans/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Plans route content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user