Files
codex/prs/bolinfest/study/PR-1546-study.md
2025-09-02 15:17:45 -07:00

3.8 KiB
Raw Blame History

DOs

  • Use Clear Type Names: Prefer TestCase over ambiguous names like Case.
struct TestCase {
    name: &'static str,
    event: serde_json::Value,
    expected: Expected,
}
  • Run Each Case Independently: Avoid a single looped test; extract a helper and write one test per case for better failure isolation.
use serde_json::json;

async fn assert_event(event: serde_json::Value, expected: Expected) {
    let mut evs = vec![event, json!({"type":"response.completed","response":{"id":"c","output":[]}})];
    let out = run_sse(evs).await;
    assert_eq!(out.len(), expected.len);
    assert!(expected.first.matches(&out[0]));
}

#[tokio::test]
async fn created_emits_created() {
    assert_event(json!({"type":"response.created","response":{}}),
                 Expected { first: Expectation::Created, len: 2 }).await;
}

#[tokio::test]
async fn output_item_done_emits_output() {
    assert_event(json!({"type":"response.output_item.done","item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi"}]}}),
                 Expected { first: Expectation::OutputItemDone, len: 2 }).await;
}
  • Document NonTrivial Tests: Add a concise doc comment explaining what the test validates.
/// Verifies that SSE events map to the correct `ResponseEvent`
/// and that a synthetic `response.completed` ends the stream.
#[tokio::test]
async fn created_emits_created() { /* ... */ }
  • Separate Inputs From Expectations: Use distinct structs for what drives the test vs. what should happen.
struct EventArgs {
    name: &'static str,
    event: serde_json::Value,
}

struct Expected {
    first: Expectation,
    len: usize,
}
  • Prefer Typed Expectations Over Fn Pointers: Use an enum with a matcher method instead of fn(&ResponseEvent) -> bool.
enum Expectation {
    Created,
    OutputItemDone,
    Completed,
}

impl Expectation {
    fn matches(&self, ev: &ResponseEvent) -> bool {
        match self {
            Expectation::Created => matches!(ev, ResponseEvent::Created),
            Expectation::OutputItemDone => matches!(ev, ResponseEvent::OutputItemDone(_)),
            Expectation::Completed => matches!(ev, ResponseEvent::Completed { .. }),
        }
    }
}
  • Remove Unneeded Lint Suppressions: Drop #[allow(dead_code)] once helpers are used.
// Before:
// #[allow(dead_code)]
// pub fn load_sse_fixture(path: impl AsRef<Path>) -> String { ... }

// After:
pub fn load_sse_fixture(path: impl AsRef<std::path::Path>) -> String { /* used in tests */ }

DONTs

  • Dont Use Ambiguous Names: Avoid struct Case; its unclear and collides with other languages keywords.
// ❌
struct Case { /* ... */ }

// ✅
struct TestCase { /* ... */ }
  • Dont Pack Many Cases Into One Test: A failing early case hides later failures.
// ❌ Single test with loop:
#[tokio::test]
async fn event_kinds_in_one_go() {
    for c in cases {
        /* first failure stops here */
    }
}
  • Dont Assert With Bare Function Pointers: Theyre less expressive and harder to extend.
// ❌
expect_first: fn(&ResponseEvent) -> bool

// ✅
expected: Expected { first: Expectation::Created, len: 2 }
  • Dont Leave Dead-Code Allows Behind: If a helper is referenced, remove #[allow(dead_code)].
// ❌
#[allow(dead_code)]
pub fn load_sse_fixture_with_id(...) -> String { /* actually used */ }

// ✅
pub fn load_sse_fixture_with_id(...) -> String { /* used */ }
  • Dont Skip Test Docs When Behavior Is Subtle: Undocumented tests slow reviews and regressions hunts.
// ❌
#[tokio::test]
async fn table_driven_event_kinds() { /* ... */ }

// ✅
/** Explains mapping and termination behavior. */
#[tokio::test]
async fn table_driven_event_kinds() { /* ... */ }