test(chat): add copilot contract coverage
This commit is contained in:
parent
8adc27004d
commit
8fd5fbae3c
@ -30,6 +30,7 @@
|
||||
"check:api-contract": "node --import tsx verifyApiContract.ts",
|
||||
"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: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",
|
||||
|
||||
105
backend/verifyChatCopilotContract.ts
Normal file
105
backend/verifyChatCopilotContract.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* verifyChatCopilotContract.ts
|
||||
*
|
||||
* Static contract checks for the chat copilot surface.
|
||||
* Verifies the supported action set, runtime-context prompt contract,
|
||||
* and safe quick-link guidance without starting the backend server.
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const apiServerSource = readFileSync(join(__dirname, 'src/services/apiServer.ts'), 'utf8');
|
||||
|
||||
function assertSourceIncludes(fragment: string, message: string) {
|
||||
assert.ok(apiServerSource.includes(fragment), message);
|
||||
}
|
||||
|
||||
function assertSourceMatches(pattern: RegExp, message: string) {
|
||||
assert.match(apiServerSource, pattern, message);
|
||||
}
|
||||
|
||||
function testChatRouteRequiresAuth() {
|
||||
assertSourceMatches(
|
||||
/this\.app\.post\('\/api\/chat',\s*this\.requireAuth,/,
|
||||
'/api/chat must be protected by requireAuth',
|
||||
);
|
||||
console.log('[PASS] /api/chat requires authenticated requests.');
|
||||
}
|
||||
|
||||
function testChatActionContract() {
|
||||
for (const action of [
|
||||
'create_profile',
|
||||
'update_profile',
|
||||
'recommend_profile_change',
|
||||
'recommend_trade_plan',
|
||||
'recommend_reconciliation_followup',
|
||||
'review_recent_trades',
|
||||
'explain',
|
||||
'explain_position',
|
||||
'explain_waiting',
|
||||
'explain_blocker',
|
||||
'summarize_reconciliation',
|
||||
]) {
|
||||
assertSourceIncludes(`'${action}'`, `chat copilot must support ${action}`);
|
||||
}
|
||||
|
||||
assertSourceIncludes(
|
||||
'"action": "create_profile" | "update_profile" | "recommend_profile_change" | "recommend_trade_plan" | "recommend_reconciliation_followup" | "review_recent_trades" | "explain" | "explain_position" | "explain_waiting" | "explain_blocker" | "summarize_reconciliation"',
|
||||
'chat prompt contract must document the full action enum',
|
||||
);
|
||||
console.log('[PASS] chat action contract is documented and supported.');
|
||||
}
|
||||
|
||||
function testQuickLinkContract() {
|
||||
assertSourceIncludes(
|
||||
`"quickLinks": [{ kind: "portfolio" | "plans" | "settings", ... }] (optional safe in-app destinations)`,
|
||||
'chat prompt contract must document quickLinks',
|
||||
);
|
||||
assertSourceIncludes(
|
||||
'Include safe "nextActions" and "quickLinks" whenever they would help the user move to the right app surface.',
|
||||
'chat prompt rules must require safe nextActions/quickLinks guidance',
|
||||
);
|
||||
assertSourceIncludes(
|
||||
"{ kind: 'plans', label: 'Manage in Plans'",
|
||||
'chat copilot must be able to deep-link into Trade Plans',
|
||||
);
|
||||
assertSourceIncludes(
|
||||
"{ kind: 'settings', label: 'Open Admin Panel', section: 'Admin Panel' }",
|
||||
'chat copilot must be able to deep-link into the admin panel',
|
||||
);
|
||||
assertSourceIncludes(
|
||||
"{ kind: 'portfolio', label: 'Open Portfolio'",
|
||||
'chat copilot must be able to deep-link into Portfolio',
|
||||
);
|
||||
console.log('[PASS] quick-link contract is covered.');
|
||||
}
|
||||
|
||||
function testRuntimeCopilotFallbacks() {
|
||||
assertSourceIncludes(
|
||||
'this.buildTradePlanRecommendation(message, context)',
|
||||
'local fallback must support trade-plan recommendations',
|
||||
);
|
||||
assertSourceIncludes(
|
||||
'return this.buildReconciliationFollowup(context);',
|
||||
'local fallback must support reconciliation follow-up guidance',
|
||||
);
|
||||
assertSourceIncludes(
|
||||
'this.buildRecentTradeReview(context)',
|
||||
'local fallback must support recent-trade review guidance',
|
||||
);
|
||||
console.log('[PASS] runtime copilot fallback intents are covered.');
|
||||
}
|
||||
|
||||
function main() {
|
||||
testChatRouteRequiresAuth();
|
||||
testChatActionContract();
|
||||
testQuickLinkContract();
|
||||
testRuntimeCopilotFallbacks();
|
||||
console.log('Chat copilot contract checks passed');
|
||||
}
|
||||
|
||||
main();
|
||||
@ -2,7 +2,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { ChatControl } from './ChatControl';
|
||||
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
|
||||
import type { BotState } from '../hooks/useWebSocket';
|
||||
@ -75,9 +75,15 @@ const botStateFixture: BotState = {
|
||||
};
|
||||
|
||||
describe('ChatControl DOM flow', () => {
|
||||
const LocationProbe = () => {
|
||||
const location = useLocation();
|
||||
return <div data-testid="chat-location">{`${location.pathname}${location.search}`}</div>;
|
||||
};
|
||||
|
||||
const renderControl = (onApplyProfile = vi.fn(async () => ({ success: true }))) =>
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<LocationProbe />
|
||||
<ChatControl profiles={profilesFixture} botState={botStateFixture} onApplyProfile={onApplyProfile} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
@ -346,4 +352,40 @@ describe('ChatControl DOM flow', () => {
|
||||
expect(screen.getByRole('button', { name: /Open Portfolio/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates through safe quick links into plans and settings routes', async () => {
|
||||
getPlatformAccessTokenMock.mockResolvedValue('token-6');
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
summary: 'Reconciliation needs follow-up before more execution.',
|
||||
reasoning: 'Current mismatch counts show the admin panel is the safest next stop.',
|
||||
action: 'recommend_reconciliation_followup',
|
||||
quickLinks: [
|
||||
{ kind: 'settings', label: 'Open Admin Panel', section: 'Admin Panel' },
|
||||
{ kind: 'plans', label: 'Manage in Plans', mode: 'sell', symbol: 'BTC/USDT', tradeId: 'trade-1' }
|
||||
]
|
||||
})
|
||||
} as any);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderControl();
|
||||
await user.click(screen.getAllByRole('button')[0]);
|
||||
await user.type(screen.getByPlaceholderText(/Ask for a profile, holding explanation, or reconciliation help/i), 'What should I do about this reconciliation mismatch?{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /Open Admin Panel/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Open Admin Panel/i }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('chat-location')).toHaveTextContent('/settings?section=Admin+Panel');
|
||||
});
|
||||
|
||||
await user.click(screen.getAllByRole('button')[0]);
|
||||
await user.click(screen.getByRole('button', { name: /Manage in Plans/i }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('chat-location')).toHaveTextContent('/plans?mode=sell&symbol=BTC%2FUSDT&tradeId=trade-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user