chore(chat): harden pipeline and deploy flow

This commit is contained in:
root 2026-05-07 08:24:44 +00:00
parent 4ab97acab7
commit b4f68725ef
5 changed files with 270 additions and 1 deletions

1
.gitignore vendored
View File

@ -29,3 +29,4 @@ mobile/package-lock.json
# Claude Code session metadata
.claude/
.tmp/

View File

@ -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",

View 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
View 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

View 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();
});
});
});