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 Code session metadata
|
||||||
.claude/
|
.claude/
|
||||||
|
.tmp/
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"description": "ByteLyst Trading backend and execution control service",
|
"description": "ByteLyst Trading backend and execution control service",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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",
|
"dev": "node --import tsx src/bootstrap.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
@ -31,6 +31,7 @@
|
|||||||
"check:audit-repository": "node --import tsx verifyAuditRepository.ts",
|
"check:audit-repository": "node --import tsx verifyAuditRepository.ts",
|
||||||
"check:market-data-endpoints": "node --import tsx verifyMarketDataEndpoints.ts",
|
"check:market-data-endpoints": "node --import tsx verifyMarketDataEndpoints.ts",
|
||||||
"check:chat-copilot-contract": "node --import tsx verifyChatCopilotContract.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:fmp-cache": "node --import tsx testFmpCache.ts",
|
||||||
"check:backtest-strategy-safety": "node --import tsx testBacktestStrategySafety.ts",
|
"check:backtest-strategy-safety": "node --import tsx testBacktestStrategySafety.ts",
|
||||||
"check:websocket-contract": "node --import tsx src/scripts/verifyWebsocketContract.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