Menghubungkan test suite integrasi langsung ke API provider LLM publik (seperti OpenAI atau Anthropic) pada pipeline CI/CD memicu tiga masalah utama: latensi eksekusi yang tinggi (berkisar antara 2 hingga 15 detik per request), pemborosan credit token berbayar untuk assertion statis, serta kegagalan acak akibat rate limiting (HTTP 429) dan response non-deterministik.

Solusi yang tepat untuk pipeline pull request (PR) adalah network-level stubbing. Pendekatan ini mengintersepsi outgoing HTTP request sebelum keluar dari runner CI, memvalidasi skema payload, dan mengembalikan respons deterministik dalam hitungan milidetik tanpa menyentuh jaringan eksternal.

Arsitektur Mocking: Level Jaringan vs. SDK Monkey Patching

Membuat mock pada level kode aplikasi (seperti membungkus wrapper method SDK) sering menyembunyikan bug integrasi riil, seperti serialisasi header otentikasi yang salah, URL endpoint keliru, atau kegagalan parsing format response SDK resmi. Mengintersepsi pada transport layer HTTP (menggunakan pustaka seperti Mock Service Worker / MSW di Node.js atau WireMock di ekosistem JVM) memverifikasi seluruh siklus komunikasi HTTP internal aplikasi.

Simulasi Respons JSON dan Server-Sent Events (SSE) Streaming

Aplikasi LLM modern umumnya mengonsumsi endpoint completions via dua cara: standard JSON payload untuk kalkulasi batch atau Server-Sent Events (SSE) untuk text streaming UI.

Berikut implementasi mock interceptor menggunakan MSW (Node.js) yang mendukung kedua mode response tersebut serta memvalidasi struktur payload request:

import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

export const handlers = [
  http.post('https://api.openai.com/v1/chat/completions', async ({ request }) => {
    const authHeader = request.headers.get('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return new HttpResponse(null, { status: 401, statusText: 'Unauthorized' });
    }

    const payload = await request.json() as Record<string, any>;

    // Validasi skema payload minimum
    if (!payload.model || !Array.isArray(payload.messages)) {
      return HttpResponse.json(
        { error: { message: 'Invalid payload structure', type: 'invalid_request_error' } },
        { status: 400 }
      );
    }

    // Skenario 1: Streaming Response (Server-Sent Events)
    if (payload.stream === true) {
      const encoder = new TextEncoder();
      const chunks = ['Halo', ' dunia,', ' ini', ' respons', ' mock.'];

      const stream = new ReadableStream({
        async start(controller) {
          for (const chunk of chunks) {
            const sseData = {
              id: 'chatcmpl-mock-123',
              object: 'chat.completion.chunk',
              created: Math.floor(Date.now() / 1000),
              model: payload.model,
              choices: [{ index: 0, delta: { content: chunk }, finish_reason: null }]
            };
            controller.enqueue(encoder.encode(`data: ${JSON.stringify(sseData)}

`));
          }
          controller.enqueue(encoder.encode('data: [DONE]

'));
          controller.close();
        }
      });

      return new HttpResponse(stream, {
        headers: {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          'Connection': 'keep-alive',
        }
      });
    }

    // Skenario 2: Standard JSON Response
    return HttpResponse.json({
      id: 'chatcmpl-mock-123',
      object: 'chat.completion',
      created: Math.floor(Date.now() / 1000),
      model: payload.model,
      choices: [{
        index: 0,
        message: { role: 'assistant', content: 'Halo dunia, ini respons mock statis.' },
        finish_reason: 'stop'
      }],
      usage: { prompt_tokens: 10, completion_tokens: 8, total_tokens: 18 }
    });
  })
];

export const server = setupServer(...handlers);
Catatan: Inisialisasi server.listen({ onUnhandledRequest: 'error' }) pada setup file test framework (seperti Vitest atau Jest). Hal ini menjamin test suite langsung gagal bila ada panggilan endpoint tidak terdaftar yang mencoba menembus koneksi internet riil.

Konfigurasi GitHub Actions Workflow Terisolasi

Eksekusi test integrasi di CI wajib menerapkan isolasi penuh dan caching dependensi agar feedback cycle developer berlangsung singkat. Variabel environment dummy wajib di-inject ke runner agar SDK LLM tidak melempar inisialisasi error sebelum mencapai mock transport.

name: Integration Tests

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  test:
    name: Run Test Suite
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Setup Node.js Runtime
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

      - name: Run Tests with Mock Interceptor
        env:
          NODE_ENV: test
          # Kunci palsu valid format agar SDK client tidak abort saat inisialisasi
          OPENAI_API_KEY: sk-mock-dummy-token-for-ci-pipeline-assertions
          ANTHROPIC_API_KEY: mock-anthropic-ci-key
        run: npm run test:ci

Trade-Off: Mock Fidelity vs. Execution Speed

Memilih strategi testing LLM bergantung pada trade-off antara kecepatan verifikasi logika software dengan akurasi respons generatif:

  • Mock Static/SSE (Fast Feedback Loop): Latensi eksekusi < 20ms per test. Memvalidasi state handling, callback streaming, parsing skema UI, dan error-handling fallback. Kelemahan: tidak memvalidasi perubahan akurasi prompt atau evaluasi output faktual LLM. Cocok untuk gate merge PR.
  • Canary Live Model Evaluation (High Fidelity): Latensi tinggi (menit), biaya token aktif. Memvalidasi drift performa prompt engineering menggunakan framework evaluasi (misal: Ragas atau DeepEval). Cocok dijadwalkan secara periodik (nightly build) atau pra-release milestone, bukan pada setiap commit branch.

Terapkan mock server berbasis network level pada pipeline harian untuk menjaga stabilitas CI, meniadakan flakiness akibat rate limit 429, dan menjaga biaya operasional API LLM tetap terkendali.