Strategi test untuk game AI tidak bisa disamakan dengan testing aplikasi biasa yang seluruh output-nya deterministik. Pada game atau produk interaktif berbasis AI generatif, input yang sama bisa menghasilkan variasi teks, struktur, atau keputusan yang berbeda. Jika test Anda mengharapkan string yang persis sama, hasilnya sering flaky: kadang lolos, kadang gagal, tanpa perubahan kode yang relevan.

Solusi utamanya adalah memisahkan apa yang harus deterministik dari apa yang memang non-deterministik, lalu menguji keduanya dengan strategi berbeda. Artikel ini membahas pola yang bisa diterapkan tim produk AI interaktif: memisahkan domain logic dari panggilan model, menggunakan fake/stub model, golden dataset terkurasi, property-based atau invariant testing, contract test untuk parser prompt-output, toleransi assertion yang tepat, seed dan replay, serta alur verifikasi di CI agar regresi tertangkap tanpa membuat pipeline tidak stabil.

Mengapa test untuk game AI mudah menjadi flaky

Pada game AI, model sering dipakai untuk menghasilkan dialog, deskripsi ruangan, aksi musuh, hint, atau ringkasan state. Masalahnya, model generatif punya sifat berikut:

  • Output tidak selalu identik meski prompt sama.
  • Model provider dapat berubah tanpa perubahan kode aplikasi.
  • Prompt sensitif terhadap konteks, termasuk urutan pesan, formatting, atau data yang disuntikkan.
  • Parser rapuh jika aplikasi mengharapkan format tertentu tetapi model sesekali meleset.
  • Test integrasi mahal dan lambat karena tergantung jaringan, rate limit, dan biaya token.

Akibatnya, satu test yang memanggil model sungguhan lalu membandingkan output mentah hampir pasti menjadi sumber masalah. Tanda umum test yang buruk antara lain:

  • Assertion terhadap string penuh yang dihasilkan model.
  • Test unit yang diam-diam melakukan network call.
  • Parser diuji hanya dengan satu contoh output “ideal”.
  • CI gagal acak, lalu di-rerun sampai hijau tanpa investigasi.

Prinsip dasar: pisahkan logika deterministik dan panggilan model

Arsitektur yang baik adalah fondasi anti-flaky. Jangan biarkan domain logic game bercampur langsung dengan pemanggilan model dan parsing output dalam satu fungsi besar. Pisahkan menjadi beberapa lapisan:

  1. Domain logic deterministik: aturan game, state transition, validasi inventory, damage calculation, scoring, quest progression.
  2. Prompt builder: menyusun input dari state game ke format yang dikirim ke model.
  3. Model client: antarmuka ke provider atau runtime model.
  4. Output parser/normalizer: mengubah respons model ke struktur internal aplikasi.
  5. Orchestrator: menghubungkan semuanya.

Dengan pemisahan ini, sebagian besar test bisa tetap deterministik dan cepat.

Contoh struktur komponen

interface ModelClient {
  generate(prompt: string): Promise<string> 
}

type GameState = {
  roomId: string
  hp: number
  inventory: string[]
}

type AiAction = {
  narration: string
  suggestedActions: string[]
  dangerLevel: number
}

function buildRoomPrompt(state: GameState): string {
  return [
    `Room: ${state.roomId}`,
    `HP: ${state.hp}`,
    `Inventory: ${state.inventory.join(", ") || "empty"}`,
    "Return JSON with narration, suggestedActions, dangerLevel"
  ].join("\n")
}

function parseAiAction(raw: string): AiAction {
  const data = JSON.parse(raw)

  if (typeof data.narration !== "string") throw new Error("invalid narration")
  if (!Array.isArray(data.suggestedActions)) throw new Error("invalid suggestedActions")
  if (typeof data.dangerLevel !== "number") throw new Error("invalid dangerLevel")

  return {
    narration: data.narration.trim(),
    suggestedActions: data.suggestedActions.map(String),
    dangerLevel: Math.max(0, Math.min(10, data.dangerLevel))
  }
}

async function generateRoomAction(state: GameState, model: ModelClient): Promise<AiAction> {
  const prompt = buildRoomPrompt(state)
  const raw = await model.generate(prompt)
  return parseAiAction(raw)
}

Dari struktur di atas, Anda bisa menguji buildRoomPrompt dan parseAiAction secara deterministik tanpa memanggil model sama sekali. Hanya lapisan ModelClient atau test E2E tertentu yang perlu menyentuh sistem eksternal.

Gunakan fake atau stub model untuk mayoritas test

Untuk test unit dan sebagian integration test, gunakan fake model atau stub model. Tujuannya bukan meniru kecerdasan model, tetapi memberi input yang terkontrol untuk memverifikasi perilaku sistem Anda.

Kapan memakai fake, stub, dan mock

  • Fake: implementasi sederhana yang mengembalikan respons berdasarkan aturan atau fixture. Cocok untuk test skenario.
  • Stub: mengembalikan respons tetap yang sudah ditentukan test.
  • Mock: memverifikasi interaksi, misalnya memastikan prompt tertentu dikirim. Gunakan seperlunya agar test tidak terlalu rapuh.

Contoh fake model

class FakeModel implements ModelClient {
  constructor(private readonly responses: Record<string, string>) {}

  async generate(prompt: string): Promise<string> {
    if (!(prompt in this.responses)) {
      throw new Error(`No fake response for prompt: ${prompt}`)
    }
    return this.responses[prompt]
  }
}

// test
const state = { roomId: "dungeon-1", hp: 8, inventory: ["torch"] }
const prompt = buildRoomPrompt(state)
const model = new FakeModel({
  [prompt]: JSON.stringify({
    narration: "Lorong gelap terbuka di depanmu.",
    suggestedActions: ["maju", "periksa dinding"],
    dangerLevel: 4
  })
})

const result = await generateRoomAction(state, model)
// assert result.dangerLevel === 4

Keuntungan pendekatan ini:

  • Test cepat dan stabil.
  • Tidak ada biaya API.
  • Tidak tergantung jaringan atau rate limit.
  • Mudah mereproduksi bug.

Kelemahannya, fake model tidak menangkap perubahan perilaku provider atau kelemahan prompt di dunia nyata. Karena itu, fake model bukan pengganti penuh untuk evaluasi integrasi; ia hanya fondasi utama untuk test otomatis yang andal.

Golden dataset terkurasi untuk mencegah regresi

Selain test unit, tim AI interaktif biasanya butuh golden dataset: kumpulan input dan ekspektasi yang dikurasi untuk skenario penting. Ini berguna untuk mendeteksi regresi setelah perubahan prompt, parser, kebijakan normalisasi, atau aturan game.

Isi golden dataset yang berguna

  • Input state game atau percakapan.
  • Prompt yang dihasilkan bila perlu diaudit.
  • Contoh respons model atau beberapa varian respons realistis.
  • Expected normalized output dalam format internal aplikasi.
  • Label atau invariant, misalnya “harus menawarkan minimal 2 aksi”, “dangerLevel 0-10”, atau “tidak boleh menyebut item yang tidak ada di inventory”.

Contoh format dataset

{
  "name": "room-low-hp-warning",
  "state": {
    "roomId": "crypt-entrance",
    "hp": 2,
    "inventory": ["key", "torch"]
  },
  "modelOutput": "{\"narration\":\"Napasmu berat...\",\"suggestedActions\":[\"mundur\",\"gunakan torch\"],\"dangerLevel\":7}",
  "expected": {
    "dangerLevel": 7,
    "minSuggestedActions": 2,
    "mustMentionLowHealth": true
  }
}

Golden dataset tidak harus selalu memeriksa string penuh. Untuk sistem non-deterministik, lebih aman menyimpan ekspektasi terstruktur atau aturan kualitas minimum daripada memaksa teks identik.

Tips kurasi dataset

  • Pilih skenario yang benar-benar mewakili risiko produk: state langka, input kotor, konteks panjang, inventory kosong, HP kritis, prompt injeksi dari user, dan output parser yang dulu pernah rusak.
  • Tambahkan contoh gagal, bukan hanya contoh bagus.
  • Jangan membuat dataset terlalu besar di awal. Lebih baik 20 kasus penting yang dipelihara baik daripada 500 kasus yang tidak pernah direview.
  • Versioning dataset bersama kode agar perubahan perilaku dapat ditinjau lewat pull request.

Property-based dan invariant testing: jangan menguji teks mentah saja

Untuk output generatif, assertion paling tahan lama biasanya berbentuk invariant, bukan string literal. Invariant adalah properti yang harus selalu benar walau phrasing berubah.

Contoh invariant pada game AI

  • dangerLevel selalu dalam rentang 0-10 setelah parsing.
  • suggestedActions tidak kosong.
  • Aksi yang dihasilkan tidak melanggar aturan game saat dieksekusi.
  • Output tidak boleh merujuk item yang tidak ada di state, jika sistem mengharuskan grounding ketat.
  • Narasi tidak boleh kosong setelah normalisasi.
  • Parser harus gagal dengan error yang jelas jika format tidak valid.

Mengapa ini efektif

Jika Anda menguji “teks harus persis A”, sedikit variasi gaya bahasa akan mematahkan test walau perilaku produk tetap baik. Jika Anda menguji “harus mengembalikan JSON valid dengan 2-4 aksi yang legal”, Anda menangkap kegagalan yang penting tanpa sensitif berlebihan terhadap variasi alami model.

Contoh pseudo-test invariant

for (const fixture of fixtures) {
  const action = parseAiAction(fixture.raw)

  assert(action.narration.length > 0)
  assert(action.suggestedActions.length > 0)
  assert(action.dangerLevel >= 0 && action.dangerLevel <= 10)

  for (const a of action.suggestedActions) {
    assert(isAllowedAction(a))
  }
}

Jika tim Anda memakai property-based testing, Anda juga bisa menghasilkan input state acak dalam batas domain yang valid lalu memastikan sistem tetap menjaga invariant. Ini sangat berguna untuk parser, validator, dan state transition.

Catatan: property-based testing paling efektif untuk bagian deterministik seperti parser, normalizer, dan engine aturan. Untuk model generatif sungguhan, fokuskan pada verifikasi output terstruktur dan replay, bukan pada “keacakan” model itu sendiri.

Contract test untuk prompt-output parser

Banyak bug pada aplikasi AI bukan karena model “bodoh”, melainkan karena ada kontrak implisit antara prompt dan parser yang tidak pernah diuji dengan baik. Misalnya prompt meminta JSON tertentu, tetapi parser mengasumsikan field selalu hadir, atau model sesekali menambahkan teks sebelum blok JSON.

Apa yang perlu diuji

  • Prompt benar-benar meminta format yang bisa diparsing.
  • Parser menerima output valid yang realistis, bukan hanya contoh sempurna.
  • Parser menolak output invalid dengan error yang bisa didiagnosis.
  • Normalizer menangani variasi minor yang masih bisa diterima.

Contoh test contract

const validOutputs = [
  '{"narration":"Pintu berderit.","suggestedActions":["buka","dengar"],"dangerLevel":3}',
  '{"narration":"Pintu berderit. ","suggestedActions":["buka"],"dangerLevel":11}'
]

for (const raw of validOutputs) {
  const parsed = parseAiAction(raw)
  assert(typeof parsed.narration === "string")
  assert(parsed.dangerLevel >= 0 && parsed.dangerLevel <= 10)
}

const invalidOutputs = [
  'Pintu berderit. Coba buka.',
  '{"narration":null,"suggestedActions":"buka","dangerLevel":"tinggi"}'
]

for (const raw of invalidOutputs) {
  assertThrows(() => parseAiAction(raw))
}

Contract test seperti ini sederhana, tetapi sangat efektif untuk mencegah regresi saat tim mengubah prompt template atau aturan parsing.

Kesalahan umum

  • Parser terlalu permisif sehingga data rusak lolos ke domain layer.
  • Parser terlalu ketat sehingga variasi aman malah dianggap gagal.
  • Prompt meminta format ambigu, misalnya “berikan JSON” tanpa mendefinisikan field secara jelas.

Toleransi assertion yang tepat

Pada sistem AI, assertion harus cukup ketat untuk menangkap bug, tetapi tidak terlalu ketat sampai menghukum variasi yang tidak berbahaya. Ini konsep toleransi assertion.

Assertion yang terlalu ketat

  • Membandingkan seluruh narasi kata demi kata.
  • Mengharapkan urutan aksi selalu sama walau tidak penting.
  • Menganggap sinonim sebagai kegagalan.

Assertion yang lebih tepat

  • Bandingkan struktur hasil parse, bukan respons mentah.
  • Periksa keberadaan kata kunci atau label semantik bila relevan.
  • Gunakan rentang nilai, bukan satu angka absolut, jika memang logikanya toleran.
  • Bandingkan himpunan aksi legal jika urutan tidak berarti.

Namun hati-hati: toleransi yang kelewat longgar dapat menyembunyikan regresi nyata. Misalnya, jika hanya memeriksa “narasi tidak kosong”, test tidak akan menangkap model yang mulai berhalusinasi tentang item yang tidak ada. Karena itu, toleransi harus berbasis risiko produk.

Seed, logging, dan replay untuk investigasi bug

Tidak semua non-determinisme bisa dihilangkan. Karena itu, Anda perlu membuat kegagalan mudah direproduksi. Konsep pentingnya adalah seed dan replay.

Apa yang direkam saat test atau produksi

  • Prompt final yang dikirim ke model.
  • Parameter sampling bila digunakan aplikasi Anda.
  • Versi prompt template.
  • State game yang menjadi konteks.
  • Raw output model.
  • Seed atau identifier request jika runtime mendukung konsep itu.

Bahkan jika provider tidak menjamin determinisme penuh dari seed, menyimpan konteks lengkap tetap sangat membantu untuk replay melalui fake model atau offline fixture.

Alur replay yang praktis

  1. Test atau sesi produksi gagal karena output tak bisa diparse atau melanggar invariant.
  2. Sistem menyimpan prompt, state, dan raw output.
  3. Tim membuat fixture dari kasus itu.
  4. Fixture ditambahkan ke golden dataset.
  5. Bug diperbaiki dan diregresikan secara lokal tanpa memanggil model sungguhan.

Dengan pendekatan ini, kegagalan acak berubah menjadi kasus deterministik yang bisa dipelihara.

Workflow verifikasi di CI yang realistis

Jangan menjalankan semua test AI dengan model sungguhan pada setiap commit. Itu mahal, lambat, dan rawan flaky. Pisahkan level verifikasi di CI.

Layer CI yang direkomendasikan

  1. Fast deterministic tests
    Menjalankan unit test untuk domain logic, prompt builder, parser, normalizer, dan fake-model integration. Ini harus menjadi mayoritas test dan wajib stabil.
  2. Golden dataset regression tests
    Menjalankan skenario terkurasi terhadap parser, orchestrator, dan invariant checker. Tetap tanpa network call jika memungkinkan.
  3. Scheduled live model checks
    Test terbatas terhadap model sungguhan, misalnya harian atau sebelum rilis. Gunakan untuk mendeteksi drift provider, masalah prompt, atau perubahan kualitas.
  4. Manual review gates
    Untuk perubahan prompt besar, lakukan sampling hasil dan review manusia, bukan hanya mengandalkan pass/fail otomatis.

Contoh pembagian suite

tests/
  unit/
    game-rules.test.ts
    prompt-builder.test.ts
    parser.test.ts
  integration/
    ai-orchestrator-with-fake-model.test.ts
  regression/
    golden-dataset.test.ts
  live/
    smoke-with-real-model.test.ts

Prinsip penting di CI

  • Suite default pada pull request harus tanpa network.
  • Test live model diberi label terpisah dan tidak memblokir semua commit jika tujuannya observasi kualitas, bukan validasi deterministik.
  • Simpan artefak kegagalan: prompt, raw output, parsed output, diff invariant.
  • Tetapkan ambang kapan kegagalan live test menjadi blocker, misalnya parser gagal total pada skenario inti.

Contoh struktur test yang bisa diterapkan tim

Berikut contoh pembagian tanggung jawab test yang cukup seimbang untuk game AI interaktif.

1. Test unit domain logic

  • Perubahan HP, inventory, cooldown, state room, scoring.
  • Tidak ada ketergantungan pada AI.

2. Test unit prompt builder

  • State tertentu menghasilkan prompt yang memuat konteks penting.
  • Field wajib tidak hilang.
  • Prompt tidak menyuntikkan data yang tidak perlu atau berbahaya.

3. Test unit parser/normalizer

  • Menerima output valid.
  • Menolak output invalid dengan error jelas.
  • Menormalkan variasi kecil secara konsisten.

4. Integration test dengan fake model

  • Orchestrator dari state -> prompt -> fake model -> parser -> domain action.
  • Verifikasi fallback saat output invalid.

5. Regression test berbasis golden dataset

  • Skenario penting dan historis.
  • Assertion berbasis invariant atau output terstruktur.

6. Live smoke test terbatas

  • Beberapa prompt inti ke model sungguhan.
  • Tujuan: mendeteksi drift besar, bukan memverifikasi string pasti.

Checklist anti-flaky untuk game AI

  • Apakah mayoritas test berjalan tanpa memanggil model sungguhan?
  • Apakah domain logic sudah dipisahkan dari model client?
  • Apakah parser dan normalizer punya contract test sendiri?
  • Apakah assertion memeriksa invariant, bukan teks mentah semata?
  • Apakah ada golden dataset untuk skenario berisiko tinggi?
  • Apakah setiap kegagalan live test bisa direplay dari fixture?
  • Apakah CI memisahkan deterministic suite dan live suite?
  • Apakah artefak kegagalan disimpan untuk debugging?
  • Apakah perubahan prompt besar ditinjau manusia, bukan hanya pass CI?
  • Apakah test yang flaky ditangani akar masalahnya, bukan sekadar di-retry?

Trade-off biaya, kecepatan, dan cakupan

Tidak ada strategi tunggal yang sempurna. Anda perlu menyeimbangkan beberapa trade-off berikut.

Fake/stub model

  • Pro: cepat, murah, stabil, mudah direproduksi.
  • Kontra: tidak menangkap drift model nyata atau kelemahan prompt di provider aktual.

Golden dataset

  • Pro: bagus untuk regresi, mudah direview, konkret.
  • Kontra: perlu kurasi manual dan bisa usang jika produk berubah cepat.

Invariant/property-based testing

  • Pro: tahan terhadap variasi bahasa, kuat untuk validasi logika.
  • Kontra: bisa terlalu longgar jika invariant yang dipilih lemah.

Live model test

  • Pro: satu-satunya cara melihat perilaku end-to-end yang realistis.
  • Kontra: lambat, berbiaya, dan berpotensi flaky.

Pola yang umumnya sehat adalah 80-90% coverage otomatis berasal dari komponen deterministik dan fixture-based, sedangkan test live model dipakai sebagai sinyal tambahan, bukan satu-satunya pagar kualitas.

Kapan evaluasi manual tetap diperlukan

Beberapa aspek produk AI interaktif sulit direduksi menjadi assertion otomatis, misalnya kualitas narasi, tone, konsistensi karakter, humor, ketegangan, atau apakah petunjuk terasa terlalu eksplisit. Untuk area seperti ini, evaluasi manual tetap penting.

Gunakan evaluasi manual ketika:

  • Tim mengubah prompt atau persona secara signifikan.
  • Anda merilis fitur narasi baru yang dampaknya lebih kualitatif.
  • Golden dataset lolos, tetapi pengalaman bermain terasa turun.
  • Ada laporan pemain tentang respons yang “aneh” namun tidak melanggar parser atau invariant.

Agar review manual tidak berantakan, tentukan rubrik sederhana: akurasi terhadap state game, konsistensi tone, utilitas aksi, keamanan konten, dan keberulangan kesalahan. Simpan contoh sebelum-sesudah agar keputusan tidak murni subjektif.

Penutup

Strategi test untuk game AI yang efektif bukan mencoba memaksa output model menjadi sepenuhnya deterministik, melainkan mendesain sistem dan test agar variasi yang sehat tidak dianggap bug, sementara regresi nyata tetap tertangkap. Caranya adalah memisahkan logika deterministik dari panggilan model, mengandalkan fake/stub model untuk sebagian besar test, memelihara golden dataset, menguji invariant dan kontrak parser, mencatat konteks untuk replay, dan membangun pipeline CI yang membedakan suite stabil dari evaluasi live.

Jika tim Anda baru mulai, urutan implementasi yang paling praktis adalah: pisahkan model client dari domain logic, tambahkan parser contract test, buat fake model untuk integration test, kurasi 10-20 golden cases, lalu tambahkan live smoke test terjadwal. Dengan langkah ini, Anda bisa menurunkan flaky test secara signifikan tanpa kehilangan visibilitas terhadap kualitas perilaku AI di produk nyata.