Tooling layer adalah jembatan kritis antara model probabilistik (LLM) dan sistem deterministik eksternal seperti database, REST API, atau shell execution. Masalah utama pengujian integrasi tool agent AI adalah non-determinisme LLM dan risiko destructive side-effects pada target environment saat tool dieksekusi secara otonom.

Menguji tooling layer secara efektif memerlukan pemisahan dua domain uji: validasi kepatuhan skema/kontrak (Mocking) dan validasi eksekusi runtime nyata tanpa merusak infrastruktur (Sandbox).

Tantangan Flaky Tests dan Non-Determinisme LLM

Memanggil LLM langsung di dalam pipeline CI/CD menimbulkan tiga masalah utama:

  • Non-deterministic output: LLM dapat menghasilkan pemanggilan fungsi (function calls) dengan urutan argumen atau tipe data yang berfluktuasi meskipun diberi prompt identik.
  • High latency dan cost: Setiap unit test yang memanggil API inference komersial memperlambat build pipeline dan menghabiskan token budget.
  • Flaky assertions: Testing yang bergantung pada respons mentah LLM menyebabkan test suite rapuh terhadap perubahan subtle pada bobot model.

Solusinya: pisahkan verifikasi tool selection oleh LLM dari verifikasi tool contract execution oleh engine backend.

Strategi 1: Functional Mock Layer (Deterministic Schema Validation)

Mocking berada pada level unit test. Tujuannya adalah memvalidasi bahwa payload yang diterima dari model sesuai skema JSON Schema / Pydantic / Zod sebelum logika dieksekusi, serta memverifikasi bahwa tool handler menangani edge case secara deterministik.

Contoh berikut mendefinisikan tool operasi file dan memvalidasinya menggunakan zod di TypeScript tanpa menyentuh file system nyata.

import { z } from "zod";

// 1. Skema definisi kontrak tool
export const WriteFileSchema = z.object({
  path: z.string().regex(/^[a-zA-Z0-9_/.-]+$/, "Invalid path format"),
  content: z.string().max(10_000, "Payload size exceeded limit"),
  overwrite: z.boolean().default(false),
});

export type WriteFileInput = z.infer<typeof WriteFileSchema>;

export interface FileSystemAdapter {
  writeFile(path: string, content: string): Promise<void>;
  exists(path: string): Promise<boolean>;
}

// 2. Tool handler dengan dependency injection
export async function executeWriteFile(
  rawInput: unknown,
  fsAdapter: FileSystemAdapter
): Promise<{ success: boolean; error?: string }> {
  const parseResult = WriteFileSchema.safeParse(rawInput);
  if (!parseResult.success) {
    return { success: false, error: parseResult.error.message };
  }

  const { path, content, overwrite } = parseResult.data;
  const alreadyExists = await fsAdapter.exists(path);

  if (alreadyExists && !overwrite) {
    return { success: false, error: "Target file exists and overwrite is false." };
  }

  await fsAdapter.writeFile(path, content);
  return { success: true };
}

Pengujian unit test deterministik menggunakan mock adapter:

// test/write-file-tool.test.ts
import { executeWriteFile, FileSystemAdapter } from "../src/write-file-tool";

describe("executeWriteFile Tool Contract", () => {
  const mockFs: FileSystemAdapter = {
    writeFile: jest.fn().mockResolvedValue(undefined),
    exists: jest.fn().mockResolvedValue(false),
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  test("gagal validasi jika path mengandung traversal token", async () => {
    const invalidInput = { path: "../../etc/passwd", content: "root:x:0:0" };
    const result = await executeWriteFile(invalidInput, mockFs);

    expect(result.success).toBe(false);
    expect(mockFs.writeFile).not.toHaveBeenCalled();
  });

  test("berhasil mengeksekusi penulisan jika validasi lolos", async () => {
    const validInput = { path: "/tmp/test.txt", content: "ok" };
    const result = await executeWriteFile(validInput, mockFs);

    expect(result.success).toBe(true);
    expect(mockFs.writeFile).toHaveBeenCalledWith("/tmp/test.txt", "ok");
  });
});

Strategi 2: Isolated Container Sandbox (End-to-End Execution)

Mock layer tidak dapat mendeteksi kegagalan OS-level, permissions, atau binary dependency. Untuk tool yang memiliki risiko eksekusi tinggi (seperti Bash executor atau code runner), isolasi sandbox via container (Docker/testcontainers) wajib digunakan.

Pendekatan ini menjamin isolasi jaringan, ephemeral filesystem, dan pembatasan memori/CPU sehingga efek destruktif tertahan di dalam runtime container.

# test_sandbox_tool.py
import pytest
from testcontainers.core.container import DockerContainer
import requests

class PythonCodeRunnerTool:
    def __init__(self, endpoint_url: str):
        self.endpoint_url = endpoint_url

    def run(self, script: str) -> dict:
        response = requests.post(
            f"{self.endpoint_url}/run",
            json={"code": script},
            timeout=5
        )
        return response.json()

@pytest.fixture(scope="module")
def runner_sandbox():
    # Menjalankan isolated runner service di dalam container
    with DockerContainer("python:3.11-slim") \
        .with_command("python -m http.server 8080") \
        .with_exposed_ports(8080) as container:
        host = container.get_container_host_ip()
        port = container.get_exposed_port(8080)
        yield f"http://{host}:{port}"

def test_tool_execution_in_sandbox(runner_sandbox):
    tool = PythonCodeRunnerTool(endpoint_url=runner_sandbox)
    # Verifikasi konektivitas dan isolasi sandbox berjalan
    res = requests.get(runner_sandbox)
    assert res.status_code == 200

Matriks Komparasi: Mocking vs Sandbox

MetrikMock LayerContainer Sandbox
Kecepatan EksekusiSangat Cepat (< 10ms per test)Lambat (detik per test setup)
Infrastruktur CI/CDRingan (Node.js/Python runtime standar)Butuh akses Docker daemon / nested virt
Cakupan ValidasiKepatuhan schema, boundary, error handlerOS system calls, runtime dependencies, timeout
Proteksi KerusakanTinggi (tidak ada eksekusi nyata)Tinggi (lingkup blast radius terisolasi)
FidelityRendah (tergantung akurasi mock)Tinggi (lingkungan produksi identik)

Rekomendasi Arsitektur Testing Tooling Agent

Terapkan pendekatan hibrida untuk menjaga efisiensi pipeline:

  1. Gunakan Mock Layer untuk 80% test coverage: verifikasi input schema sanitization, missing parameter, invalid types, dan mapping return schema yang akan dibaca kembali oleh context agent.
  2. Gunakan Sandbox Layer untuk 20% integration critical path: eksekusi script generatif, command shell, database migration tool, atau manipulasi filesystem kompleks.
  3. Simulasikan tool call dengan data JSON statis (Golden Master / Recorded Payloads) daripada memanggil endpoint model secara langsung pada pipeline integrasi harian.