From b4f68725ef9911583068851bfae3638f74f3a8a5 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 7 May 2026 08:24:44 +0000 Subject: [PATCH] chore(chat): harden pipeline and deploy flow --- .gitignore | 1 + backend/package.json | 3 +- backend/testChatCopilotFallbacks.ts | 153 ++++++++++++++++++ scripts/deploy-hotcopy.sh | 28 ++++ .../components/ChatCopilotRoutes.dom.test.tsx | 86 ++++++++++ 5 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 backend/testChatCopilotFallbacks.ts create mode 100755 scripts/deploy-hotcopy.sh create mode 100644 web/src/components/ChatCopilotRoutes.dom.test.tsx diff --git a/.gitignore b/.gitignore index ff78163..9643b09 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ mobile/package-lock.json # Claude Code session metadata .claude/ +.tmp/ diff --git a/backend/package.json b/backend/package.json index ec2f11d..96e9610 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/testChatCopilotFallbacks.ts b/backend/testChatCopilotFallbacks.ts new file mode 100644 index 0000000..9291538 --- /dev/null +++ b/backend/testChatCopilotFallbacks.ts @@ -0,0 +1,153 @@ +import assert from 'node:assert/strict'; +import { ApiServer } from './src/services/apiServer.js'; + +type ChatContext = Parameters[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(); diff --git a/scripts/deploy-hotcopy.sh b/scripts/deploy-hotcopy.sh new file mode 100755 index 0000000..9cae147 --- /dev/null +++ b/scripts/deploy-hotcopy.sh @@ -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 diff --git a/web/src/components/ChatCopilotRoutes.dom.test.tsx b/web/src/components/ChatCopilotRoutes.dom.test.tsx new file mode 100644 index 0000000..e33053b --- /dev/null +++ b/web/src/components/ChatCopilotRoutes.dom.test.tsx @@ -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( + + + } /> + Plans route content} /> + Portfolio route content} /> + Settings route content} /> + + + ); + + 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(); + }); + }); +});