Files
goodgo-platform/docs/audits/MCP_MODULE_EXPLORATION.md
Ho Ngoc Hai 11f2bf26e6
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
chore: update project documentation, audit reports, and initialize IDE configuration files
2026-04-19 03:12:54 +07:00

22 KiB

MCP Module Exploration - GoodGo Platform

1. CẤU TRÚC MODULE & CÁC TỆP NGUỒN

Cấu trúc thư mục

apps/api/src/modules/mcp/
├── index.ts
├── mcp.module.ts
└── presentation/
    ├── mcp-transport.controller.ts
    └── __tests__/
        └── mcp-transport.controller.spec.ts

Tất cả tệp nguồn (4 tệp)

1. apps/api/src/modules/mcp/index.ts

  • Loại: Điểm vào của module (xuất khẩu)
  • Mục đích: Xuất khẩu McpIntegrationModule
  • Xuất khẩu: { McpIntegrationModule }

2. apps/api/src/modules/mcp/mcp.module.ts

  • Loại: Cấu hình NestJS Module (22 dòng)
  • Lớp chính: McpIntegrationModule implements OnModuleInit
  • Trách nhiệm:
    • Thiết lập module MCP cốt lõi với cấu hình
    • Khởi tạo TypesenseClient cho registry MCP
    • Ghi log tên các server đã khởi tạo khi module được khởi động
  • Phụ thuộc được tiêm:
    • TypesenseClientService (từ SearchModule)
    • McpRegistryService (từ @goodgo/mcp-servers)
    • LoggerService (từ SharedModule)
  • Nhập khẩu:
    • SearchModule
    • AuthModule
    • McpCoreModule.forRoot() với cấu hình
  • Controllers: McpTransportController
  • Vòng đời: Triển khai onModuleInit()

3. apps/api/src/modules/mcp/presentation/mcp-transport.controller.ts

  • Loại: NestJS Controller (102 dòng)
  • Lớp chính: McpTransportController
  • Trách nhiệm: Lớp truyền tải HTTP cho các kết nối MCP SSE
  • Decorator được áp dụng:
    • @ApiTags('mcp')
    • @ApiBearerAuth('JWT')
    • @Controller('mcp')
    • @UseGuards(JwtAuthGuard) - bảo vệ tất cả các endpoint
  • Thuộc tính:
    • transports: Map<string, SSEServerTransport> - quản lý phiên hoạt động
  • Phụ thuộc được tiêm:
    • registry: McpRegistryService

Các endpoint:

  1. GET /mcp/servers (dòng 27-34)

    • Tóm tắt: Liệt kê các MCP server khả dụng
    • Throttle: 30 yêu cầu mỗi 60 giây
    • Trả về: { servers: string[] }
    • Trạng thái: 200 (thành công), 401 (không được phép)
  2. GET /mcp/:serverName/sse (dòng 36-68)

    • Tóm tắt: Mở kết nối SSE đến MCP server
    • Throttle: 5 yêu cầu mỗi 60 giây (chặt chẽ hơn)
    • Tham số: serverName
    • Trả về: Luồng SSE
    • Xử lý phản hồi:
      • Tạo instance SSEServerTransport
      • Lưu vào map với sessionId
      • Kết nối đến server qua server.connect(transport)
      • Dọn dẹp khi yêu cầu đóng
    • Trạng thái: 200 (luồng), 404 (không tìm thấy server), 401 (không được phép)
  3. POST /mcp/:serverName/messages (dòng 70-102)

    • Tóm tắt: Gửi tin nhắn đến phiên MCP server
    • Throttle: 30 yêu cầu mỗi 60 giây
    • Tham số: serverName
    • Query: sessionId (bắt buộc)
    • Body: Dữ liệu tin nhắn được truyền đến transport
    • Xử lý phản hồi:
      • Xác thực sessionId tồn tại
      • Ủy quyền cho transport.handlePostMessage(req, res)
    • Trạng thái: 200 (thành công), 400 (thiếu sessionId), 404 (không tìm thấy phiên), 401 (không được phép)

2. CÁC TỆP TEST

Tổng số tệp test: 1

apps/api/src/modules/mcp/presentation/tests/mcp-transport.controller.spec.ts (174 dòng)

  • Framework kiểm thử: Vitest
  • Đối tượng kiểm thử: McpTransportController
  • Cấu trúc test: Các khối Describe + thiết lập beforeEach

Bộ test (4 khối describe):

  1. security decorators (4 test)

    • Xác minh JwtAuthGuard được áp dụng
    • Kiểm tra metadata Throttle trên các endpoint
    • Xác nhận giới hạn throttle (30, 5, 30)
  2. listServers (2 test)

    • Trả về danh sách server từ registry
    • Xử lý danh sách server rỗng
  3. handleSse (3 test)

    • Ném NOT_FOUND khi server không tồn tại
    • Tạo transport và kết nối đến server
    • Dọn dẹp khi đóng kết nối (xóa transport)
  4. handleMessage (2 test)

    • Ném BAD_REQUEST khi thiếu sessionId
    • Ném NOT_FOUND khi phiên không tồn tại

Các mẫu Mock được sử dụng:

  • vi.mock() cho các module ngoài (SSEServerTransport)
  • vi.fn() cho mock phương thức service
  • mockReturnValue(), mockResolvedValue(), mockRejectValue()
  • Đối tượng mock thủ công cho request/response
  • Reflection API để kiểm tra metadata (Reflect.getMetadata())

3. CẤU TRÚC LỚP DDD

Trạng thái hiện tại: Module MCP có cấu trúc ĐƠN GIẢN HÓA (chưa áp dụng DDD đầy đủ):

Những gì ĐÃ TỒN TẠI:

  • Lớp Presentation
    • presentation/mcp-transport.controller.ts
    • presentation/__tests__/mcp-transport.controller.spec.ts
    • Định nghĩa HTTP endpoint
    • Decorator Guard (xác thực)
    • Decorator Throttle
    • Decorator Swagger

Những gì CHƯA TỒN TẠI:

  • Lớp Domain

    • Không có thư mục domain/
    • Không có entity, value object hay domain event
    • Không có logic nghiệp vụ domain
  • Lớp Application

    • Không có thư mục application/
    • Không có command/handler (chưa dùng mẫu CQRS)
    • Không có query
    • Không có DTO
  • Lớp Infrastructure

    • Không có thư mục infrastructure/
    • Không có repository
    • Không có adapter dịch vụ ngoài
    • Không có truy cập cơ sở dữ liệu

Ghi chú kiến trúc:

  • Module MCP đóng vai trò là một lớp tích hợp bọc ngoài
  • Ủy quyền cho thư viện @goodgo/mcp-servers (phụ thuộc ngoài)
  • Cách tiếp cận controller chỉ có presentation đơn giản
  • Tập trung vào cơ chế truyền tải HTTP (kết nối SSE)
  • Quản lý phiên qua Map trong bộ nhớ

4. CÁC LỚP/HANDLER CHÍNH CẦN ĐƯỢC KIỂM THỬ

Triển khai hiện tại:

  1. McpIntegrationModule (mcp.module.ts)

    • Trạng thái: Được kiểm thử một phần (logic khởi tạo)
    • Các phương thức cần kiểm thử:
      • constructor() - tiêm phụ thuộc
      • onModuleInit() - luồng khởi tạo
    • Trọng tâm kiểm thử: Thiết lập module, tích hợp service, ghi log
  2. McpTransportController (mcp-transport.controller.ts)

    • Trạng thái: Được kiểm thử tốt (174 dòng test)
    • Các phương thức được kiểm thử:
      • listServers() - trả về các server khả dụng
      • handleSse() - thiết lập kết nối SSE
      • handleMessage() - định tuyến tin nhắn đến phiên
    • Độ phủ kiểm thử: Happy path + trường hợp lỗi + decorator bảo mật

5. CÁC MẪU KIỂM THỬ TỪ CÁC MODULE KHÁC

Mẫu 1: Module Auth - Kiểm thử Handler (Đơn giản)

Tệp: apps/api/src/modules/auth/application/__tests__/login-user.handler.spec.ts

// CÁC MẪU CHÍNH:
// 1. No explicit imports from vitest (globals enabled)
// 2. beforeEach: Create handler with mocked dependencies
// 3. vi.fn() for service mocks
// 4. mockResolvedValue() for async returns
// 5. expect().toHaveBeenCalledWith() for verification
// 6. Simple test structure for handlers

describe('LoginUserHandler', () => {
  let handler: LoginUserHandler;
  let mockTokenService: { generateTokenPair: ReturnType<typeof vi.fn> };

  const tokenPair = {
    accessToken: 'access-jwt',
    refreshToken: 'family.refresh-hex',
    expiresIn: 900,
  };

  beforeEach(() => {
    mockTokenService = { generateTokenPair: vi.fn().mockResolvedValue(tokenPair) };
    handler = new LoginUserHandler(mockTokenService as any);
  });

  it('generates token pair with correct payload', async () => {
    const command = new LoginUserCommand('user-1', '0912345678', 'BUYER');
    const result = await handler.execute(command);

    expect(result).toEqual(tokenPair);
    expect(mockTokenService.generateTokenPair).toHaveBeenCalledWith({
      sub: 'user-1',
      phone: '0912345678',
      role: 'BUYER',
    });
  });
});

Điểm chú ý chính:

  • Thiết lập tối giản, khởi tạo handler trực tiếp
  • Đối tượng Command làm đầu vào kiểm thử
  • Mock đơn giản với đối tượng có kiểu
  • Tập trung vào xác minh hành vi
  • Dùng as any để ép kiểu

Mẫu 2: Module Payments - Kiểm thử Handler phức tạp (Nâng cao)

Tệp: apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts

// CÁC MẪU CHÍNH:
// 1. Multiple mocked dependencies (repo, factory, gateway, event bus)
// 2. Complex mock setup with multiple methods
// 3. Tests for idempotency handling
// 4. Error case validation
// 5. Event publishing verification

describe('CreatePaymentHandler', () => {
  let handler: CreatePaymentHandler;
  let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
  let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> };
  let mockGateway: { 
    createPaymentUrl: ReturnType<typeof vi.fn>;
    verifyCallback: ReturnType<typeof vi.fn>;
    refund: ReturnType<typeof vi.fn>;
  };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockPaymentRepo = {
      findById: vi.fn(),
      findByProviderTxId: vi.fn(),
      findByIdempotencyKey: vi.fn(),
      findByUserId: vi.fn(),
      save: vi.fn().mockResolvedValue(undefined),
      update: vi.fn(),
      updateIfStatus: vi.fn(),
    };

    mockGateway = {
      createPaymentUrl: vi.fn().mockResolvedValue({
        paymentUrl: 'https://vnpay.vn/pay/123',
        providerTxId: 'vnpay-tx-1',
      }),
      verifyCallback: vi.fn(),
      refund: vi.fn(),
    };

    mockGatewayFactory = {
      getGateway: vi.fn().mockReturnValue(mockGateway),
    };

    mockEventBus = { publish: vi.fn() };
    const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };

    handler = new CreatePaymentHandler(
      mockPaymentRepo as any,
      mockGatewayFactory as any,
      mockEventBus as any,
      mockLogger as any,
    );
  });

  it('creates payment successfully', async () => {
    mockPaymentRepo.findByIdempotencyKey.mockResolvedValue(null);

    const command = new CreatePaymentCommand(
      'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n,
      'Thanh toán gói Pro', 'https://goodgo.vn/return', '127.0.0.1',
      undefined, 'idem-key-1',
    );
    const result = await handler.execute(command);

    expect(result.paymentId).toBeDefined();
    expect(result.paymentUrl).toBe('https://vnpay.vn/pay/123');
    expect(result.providerTxId).toBe('vnpay-tx-1');
    expect(mockPaymentRepo.save).toHaveBeenCalledTimes(1);
    expect(mockEventBus.publish).toHaveBeenCalled();
    expect(mockGatewayFactory.getGateway).toHaveBeenCalledWith('VNPAY');
  });

  it('throws ConflictException for duplicate idempotency key (pending)', async () => {
    mockPaymentRepo.findByIdempotencyKey.mockResolvedValue({ status: 'PENDING' });

    const command = new CreatePaymentCommand(
      'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n,
      'desc', 'https://goodgo.vn/return', '127.0.0.1',
      undefined, 'existing-key',
    );

    await expect(handler.execute(command)).rejects.toThrow(/idempotency/);
  });
});

Điểm chú ý chính:

  • Mock nhiều phụ thuộc phức tạp
  • Kiểu ánh xạ cho mock repository [K in keyof IPaymentRepository]
  • Mock từng phương thức riêng lẻ cho mỗi phụ thuộc
  • Kiểm thử quy tắc nghiệp vụ (idempotency)
  • Kiểm thử trường hợp lỗi với khớp regex
  • Bao gồm mock logger
  • Xác minh nhiều lần gọi và tương tác

Mẫu 3: Kiểm thử Entity Domain (Phong cách DDD)

Tệp: apps/api/src/modules/payments/domain/__tests__/payment.entity.spec.ts

// CÁC MẪU CHÍNH:
// 1. Explicit vitest imports at the top
// 2. Complex entity with state transitions
// 3. Tests for domain events
// 4. Tests for value objects
// 5. Tests for error handling (Result types)
// 6. Helper factory function for test setup

import { describe, it, expect } from 'vitest';
import { PaymentEntity } from '../entities/payment.entity';
import { PaymentCompletedEvent } from '../events/payment-completed.event';
import { PaymentCreatedEvent } from '../events/payment-created.event';
import { PaymentFailedEvent } from '../events/payment-failed.event';
import { PaymentRefundedEvent } from '../events/payment-refunded.event';
import { Money } from '../value-objects/money.vo';

describe('PaymentEntity', () => {
  const createPayment = (status?: string) => {
    const money = Money.create(500_000n).unwrap();
    const payment = PaymentEntity.createNew(
      'pay-1',
      'user-1',
      'VNPAY',
      'LISTING_FEE',
      money,
      'txn-1',
      'idem-key-1',
    );
    if (status === 'PROCESSING') {
      payment.markProcessing('vnp-txn-123');
    }
    if (status === 'COMPLETED') {
      payment.markProcessing('vnp-txn-123');
      payment.clearDomainEvents();
      payment.markCompleted({ responseCode: '00' });
    }
    return payment;
  };

  it('should create a new payment with domain events', () => {
    const money = Money.create(500_000n).unwrap();
    const payment = PaymentEntity.createNew(
      'pay-1',
      'user-1',
      'VNPAY',
      'LISTING_FEE',
      money,
      'txn-1',
    );

    expect(payment.id).toBe('pay-1');
    expect(payment.status).toBe('PENDING');
    
    const events = payment.domainEvents;
    expect(events).toHaveLength(1);
    expect(events[0]).toBeInstanceOf(PaymentCreatedEvent);
  });

  it('should mark completed payment as refunded and emit event', () => {
    const payment = createPayment('COMPLETED');
    payment.clearDomainEvents();
    const result = payment.markRefunded();
    expect(result.isOk).toBe(true);
    expect(payment.status).toBe('REFUNDED');

    const events = payment.domainEvents;
    expect(events).toHaveLength(1);
    expect(events[0]).toBeInstanceOf(PaymentRefundedEvent);
  });

  it('should not refund a non-completed payment', () => {
    const payment = createPayment();
    const result = payment.markRefunded();
    expect(result.isErr).toBe(true);
    expect(result.unwrapErr().message).toContain('hoàn tiền');
  });
});

Điểm chú ý chính:

  • Import vitest tường minh (tường minh hơn ngầm định)
  • Phương thức factory trợ giúp cho thiết lập phức tạp
  • Kiểm thử hành vi entity (chuyển đổi trạng thái)
  • Kiểm thử domain event
  • Kiểm thử xử lý lỗi (mẫu Result)
  • Không mock - kiểm thử logic entity thực tế
  • Sử dụng value object (Money.create().unwrap())
  • Xác minh domain event

Mẫu 4: Kiểm thử Service Infrastructure (Crypto/Bên ngoài)

Tệp: apps/api/src/modules/payments/infrastructure/__tests__/zalopay.service.spec.ts

// CÁC MẪU CHÍNH:
// 1. Explicit vitest imports
// 2. External service simulation (payment gateway)
// 3. Crypto/HMAC signature testing
// 4. Helper function to build test data
// 5. Mocking ConfigService
// 6. Tests for security (tamper detection)

import * as crypto from 'crypto';
import { type ConfigService } from '@nestjs/config';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ZalopayService } from '../services/zalopay.service';

describe('ZalopayService', () => {
  let service: ZalopayService;
  const key2 = 'TESTKEY2ABCDEF1234567890ABCDEF12';

  beforeEach(() => {
    const mockConfig = {
      get: vi.fn((key: string, defaultValue?: string) => {
        const env: Record<string, string> = {
          'ZALOPAY_APP_ID': '2553',
          'ZALOPAY_KEY1': 'TESTKEY1ABCDEF1234567890ABCDEF12',
          'ZALOPAY_KEY2': 'TESTKEY2ABCDEF1234567890ABCDEF12',
        };
        return env[key] ?? defaultValue;
      }),
      getOrThrow: vi.fn((key: string) => {
        // ...
      }),
    } as unknown as ConfigService;
    const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
    service = new ZalopayService(mockConfig, mockLogger as any);
  });

  function buildCallbackData(
    dataPayload: Record<string, unknown> = {},
    tamperMac = false,
  ): Record<string, string> {
    const payload = {
      app_id: 2553,
      app_trans_id: '260408_order-123',
      zp_trans_id: 'ZLP_TX_456',
      ...dataPayload,
    };
    const dataStr = JSON.stringify(payload);
    let mac = crypto
      .createHmac('sha256', key2)
      .update(dataStr)
      .digest('hex');

    if (tamperMac) {
      mac = 'a'.repeat(mac.length);
    }

    return { data: dataStr, mac };
  }

  it('should verify a valid callback with timingSafeEqual', () => {
    const data = buildCallbackData();
    const result = service.verifyCallback(data);

    expect(result.isValid).toBe(true);
    expect(result.isSuccess).toBe(true);
    expect(result.orderId).toBe('260408_order-123');
    expect(result.providerTxId).toBe('ZLP_TX_456');
  });

  it('should reject an invalid MAC', () => {
    const data = buildCallbackData({}, true);
    const result = service.verifyCallback(data);

    expect(result.isValid).toBe(false);
  });

  it('should handle invalid JSON in data field gracefully', () => {
    const mac = crypto
      .createHmac('sha256', key2)
      .update('not-json')
      .digest('hex');

    const result = service.verifyCallback({ data: 'not-json', mac });
    expect(result.isValid).toBe(false);
  });
});

Điểm chú ý chính:

  • Import vitest tường minh (import from 'vitest')
  • Kiểm thử chức năng quan trọng về bảo mật (xác minh HMAC)
  • Mock ConfigService cho biến môi trường
  • Hàm trợ giúp để tạo dữ liệu kiểm thử phức tạp
  • Kiểm thử các điều kiện lỗi (JSON không hợp lệ, MAC sai)
  • Kiểm thử các trường hợp biên về bảo mật (dữ liệu bị giả mạo)
  • Sử dụng module crypto của Node.js trực tiếp
  • Kiểm thử thuật toán thực (SHA256 HMAC)

6. TÓM TẮT QUY ƯỚC KIỂM THỬ

Framework & Thiết lập

  • Framework: Vitest
  • Môi trường: Node.js
  • Globals: Được bật (globals: true trong cấu hình)
  • Mẫu tên tệp: *.spec.ts (không phải .test.ts)
  • Tổ chức test: Thư mục con __tests__/

Phong cách Import

// Phong cách 1: Dùng globals (trong các test đơn giản)
describe('MyClass', () => {
  it('does something', () => {
    expect(true).toBe(true);
  });
});

// Phong cách 2: Import tường minh (trong các test phức tạp/domain)
import { describe, it, expect, beforeEach, vi } from 'vitest';

Các mẫu Mock

  1. Mock Service

    mockService = {
      method: vi.fn().mockResolvedValue(value),
      method2: vi.fn().mockReturnValue(value),
    };
    
  2. Mock Module

    vi.mock('@goodgo/mcp-servers', () => ({
      SSEServerTransport: class MockSSE { /* ... */ },
    }));
    
  3. Mock Cấu hình

    const mockConfig = {
      get: vi.fn((key) => env[key]),
      getOrThrow: vi.fn((key) => env[key] || throw),
    };
    

Cấu trúc Test

describe('ClassName', () => {
  let instance: ClassName;
  let mockDep1: Mock;
  let mockDep2: Mock;

  beforeEach(() => {
    // Thiết lập mock
    mockDep1 = { method: vi.fn() };
    // Khởi tạo với mock
    instance = new ClassName(mockDep1 as any, mockDep2 as any);
  });

  describe('methodName', () => {
    it('happy path scenario', async () => {
      // Arrange
      mockDep1.method.mockResolvedValue(expected);
      
      // Act
      const result = await instance.method();
      
      // Assert
      expect(result).toEqual(expected);
      expect(mockDep1.method).toHaveBeenCalledWith(params);
    });

    it('error case', async () => {
      // Arrange
      mockDep1.method.mockRejectedValue(new Error('fail'));
      
      // Act & Assert
      await expect(instance.method()).rejects.toThrow('fail');
    });
  });
});

Các assertion thường dùng

  • expect(x).toBe(value) - so sánh bằng nghiêm ngặt
  • expect(x).toEqual(object) - so sánh bằng sâu
  • expect(x).toHaveLength(n) - độ dài mảng/chuỗi
  • expect(x).toBeInstanceOf(Class) - kiểm tra instance
  • expect(fn).toHaveBeenCalled() - xác minh đã được gọi
  • expect(fn).toHaveBeenCalledWith(args) - tham số khi gọi
  • expect(fn).toHaveBeenCalledTimes(n) - số lần gọi
  • expect(promise).rejects.toThrow(msg) - kỳ vọng lỗi

Các tính năng đặc trưng của Vitest được dùng

  • vi.fn() - tạo hàm mock
  • vi.mock() - mock module
  • mockResolvedValue() - giá trị trả về bất đồng bộ
  • mockReturnValue() - giá trị trả về đồng bộ
  • mockRejectedValue() - mô phỏng lỗi
  • Reflect.getMetadata() - kiểm tra decorator (NestJS)

7. KHUYẾN NGHỊ CHO KIỂM THỬ MODULE MCP

Trạng thái độ phủ hiện tại

  • Controller (lớp presentation): Được kiểm thử tốt
  • ⚠️ Khởi tạo module: Cơ bản (được mock)
  • Lớp Domain: Chưa được triển khai
  • Lớp Application: Chưa được triển khai
  • Lớp Infrastructure: Chưa được triển khai

Các bước tiếp theo được khuyến nghị

  1. Mở rộng kiểm thử Controller

    • Kiểm thử tích hợp với kết nối SSE được mock
    • Kiểm thử vòng đời phiên (mở → tin nhắn → đóng)
    • Kiểm thử phục hồi lỗi
  2. Thêm kiểm thử Module

    • Kiểm thử McpIntegrationModule.onModuleInit()
    • Kiểm thử khởi tạo registry
    • Kiểm thử tiêm service
  3. Cân nhắc thêm kiểm thử Domain (nếu có logic nghiệp vụ)

    • Kiểm thử entity phiên
    • Quản lý trạng thái kết nối
    • Xử lý sự kiện

Lệnh kiểm thử

# Chạy tất cả test MCP
pnpm test -- src/modules/mcp

# Chạy với coverage
pnpm test -- --coverage src/modules/mcp

# Chế độ watch
pnpm test -- --watch src/modules/mcp