diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog
+
+## 2026-10-25 — Reliable codec oracle checks
+
+- `crates/codecs/src/tests.rs`: explicitly encode the 16 kHz-source VoIP fixture at the 48 kHz Opus clock to avoid contradictory CodecDelay metadata from older external muxers. Use a fresh cache name and validate CodecDelay against OpusHead pre-skip before waveform comparisons. Keep the original 40 dB sequential and seek SNR assertions.
+- `crates/vp9/tests/it/{common/mod.rs,conformance.rs}`: build the draft-mode fixture from key/inter pairs and explicitly clear inter-frame refresh masks, then decode the rewritten stream with the external oracle. Preserve the draft coverage and unflagged-frame exactness assertions independently of encoder temporal-layer heuristics.
+- `docs/testing.md`: explain the fixture contracts and retained accuracy assertions.
diff --git a/crates/codecs/src/tests.rs b/crates/codecs/src/tests.rs
--- a/crates/codecs/src/tests.rs
+++ b/crates/codecs/src/tests.rs
@@ -752,6 +752,15 @@ fn check_opus(name: &str, channels: usize, expr: &str, extra: &[&str], container
         eprintln!("libopus decoder unavailable; skipping {name}");
         return;
     };
+    if container == "Matroska" {
+        let reader = crate::Src(Arc::new(filmcraft_media::reader::MemReader(b.clone())));
+        let file = filmcraft_matroska::open(&reader).unwrap();
+        let track = file.tracks.iter().find(|t| t.kind == filmcraft_matroska::TrackKind::Audio).unwrap();
+        let filmcraft_matroska::Codec::Opus { head } = &track.codec else { panic!("expected Opus fixture") };
+        let head = filmcraft_opus::OpusHead::parse(head).unwrap();
+        let delay = (track.codec_delay_ns as u128 * 48_000 + 500_000_000) / 1_000_000_000;
+        assert_eq!(delay, head.pre_skip as u128, "oracle fixture CodecDelay must agree with the 48 kHz OpusHead pre-skip");
+    }
     let src = crate::open_bytes(name, b).expect("open");
     let info = src.info().clone();
     assert_eq!(info.container, container);
@@ -798,8 +807,17 @@ fn opus_webm_stereo() {
 
 #[test]
 fn opus_mkv_mono_speechlike_16k() {
-    // Low bitrate VoIP mode exercises SILK/hybrid; the input rate is 16 kHz but Opus still outputs 48 kHz.
-    check_opus("tones_opus_voip.mkv", 1, "aevalsrc=0.4*sin(2*PI*220*t)*(0.6+0.4*sin(2*PI*3*t)):s=16000", &["-b:a", "16k", "-application", "voip"], "Matroska");
+    // Keep the 16 kHz input and low-bitrate SILK/hybrid case, but explicitly feed the encoder
+    // at Opus's 48 kHz clock. Older ffmpeg muxers otherwise write CodecDelay at the input
+    // clock while OpusHead pre-skip is at 48 kHz (104 vs 312 samples), making the oracle
+    // contradict the container. A fresh cache name avoids reusing that malformed fixture.
+    check_opus(
+        "tones_opus_voip_48k_clock.mkv",
+        1,
+        "aevalsrc=0.4*sin(2*PI*220*t)*(0.6+0.4*sin(2*PI*3*t)):s=16000",
+        &["-b:a", "16k", "-application", "voip", "-ar", "48000"],
+        "Matroska",
+    );
 }
 
 #[test]
diff --git a/crates/vp9/tests/it/common/mod.rs b/crates/vp9/tests/it/common/mod.rs
--- a/crates/vp9/tests/it/common/mod.rs
+++ b/crates/vp9/tests/it/common/mod.rs
@@ -114,7 +114,38 @@ pub const FIXTURES: &[Fixture] = &[
     fx!("sharpness", "testsrc2", 352, 288, 8, NOISE, P420, ["-b:v", "400k", "-sharpness", "5", "-frame-parallel", "0"]),
     fx!("fade_intra_heavy", "testsrc2", 352, 288, 20, "fade=in:0:15", P420, ["-b:v", "400k", "-g", "8", "-frame-parallel", "0"]),
     fx!("hd_1080p", "testsrc2", 1920, 1080, 3, NOISE, P420, ["-b:v", "4M", "-speed", "5", "-deadline", "realtime"]),
-    // Two temporal layers: every other frame refreshes no reference slot (draft mode test).
+    // Each inter frame is followed by a key frame, so making it disposable cannot change
+    // any later prediction. The draft test rewrites the refresh mask, not encoder heuristics.
+    fx!(
+        "draft_pairs",
+        "testsrc2",
+        352,
+        288,
+        20,
+        NOISE,
+        P420,
+        [
+            "-g",
+            "2",
+            "-keyint_min",
+            "2",
+            "-lag-in-frames",
+            "0",
+            "-auto-alt-ref",
+            "0",
+            "-error-resilient",
+            "1",
+            "-speed",
+            "8",
+            "-deadline",
+            "realtime",
+            "-crf",
+            "40",
+            "-b:v",
+            "0"
+        ]
+    ),
+    // Encoder-requested temporal layers; disposable-frame choices depend on libvpx's version.
     fx!(
         "temporal_layers",
         "testsrc2",
diff --git a/crates/vp9/tests/it/conformance.rs b/crates/vp9/tests/it/conformance.rs
--- a/crates/vp9/tests/it/conformance.rs
+++ b/crates/vp9/tests/it/conformance.rs
@@ -69,8 +69,22 @@ fixture_tests!(
 /// other picture stays bit-exact, single-threaded and with frame threads.
 #[test]
 fn draft_mode_changes_only_flagged_non_reference_frames() {
-    let f = common::fixture("temporal_layers");
-    let Some((ivf, yuv)) = common::ensure(f) else { return };
+    let f = common::fixture("draft_pairs");
+    let Some((seed, _)) = common::ensure(f) else { return };
+    let seed = std::fs::read(seed).unwrap();
+    let mut frames: Vec<Vec<u8>> = common::ivf_frames(&seed).into_iter().map(<[u8]>::to_vec).collect();
+    for (i, frame) in frames.iter_mut().enumerate() {
+        // VP9 profile 0, shown frame, error resilient: the eight refresh flags of an
+        // inter frame start at byte 1. The following key frame resets all references.
+        assert_eq!(frame[0], if i % 2 == 0 { 0x83 } else { 0x87 }, "unexpected fixture header at frame {i}");
+        if i % 2 == 1 {
+            frame[1] = 0;
+        }
+    }
+    let ivf = common::fixtures_dir().join("draft_disposable_pairs.ivf");
+    let yuv = common::fixtures_dir().join("draft_disposable_pairs.yuv");
+    common::write_ivf(&ivf, f.width as u16, f.height as u16, &frames);
+    assert!(common::reference_decode(&ivf, &yuv, f.pix_fmt));
     let reference = std::fs::read(&yuv).unwrap();
     let data = std::fs::read(&ivf).unwrap();
     let fsize = common::raw_size(f.width as usize, f.height as usize, f.pix_fmt);
diff --git a/docs/testing.md b/docs/testing.md
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -84,6 +84,11 @@ are never linked or shipped ([AGENTS.md](../AGENTS.md) §2).
   without a Direct3D 11 video device or the HEVC / VP9 / AV1 codec extensions of the Microsoft Store. The VP9 and
   AV1 fixtures (libvpx-vp9, libaom-av1; 360p, 1080p, 2160p, hidden alt-ref frames, two GOPs) need an ffmpeg with those encoders.
 
+Two oracle fixtures explicitly control properties that external encoder versions do not guarantee:
+
+- The low-bitrate Opus/Matroska test keeps its 16 kHz source, but feeds the encoder at 48 kHz. This avoids old muxers writing a `CodecDelay` at the wrong sample clock. The test validates delay metadata against OpusHead pre-skip and retains the 40 dB sequential and random-access SNR requirements.
+- The VP9 draft test rewrites the refresh masks of shown inter frames in key/inter pairs. Each following key frame resets references, so the rewritten stream is valid independently of temporal-layer encoder heuristics. FFmpeg decodes the rewritten stream as the oracle; draft coverage, changed filtered pixels and exact unflagged pictures remain required.
+
 The NVENC tests (`crates/platform/tests/it/nvenc.rs` and `nvenc_export.rs` for H.264, `nvenc_rgba_input.rs` for
 the GPU's RGB → 4:2:0 conversion, `nvenc_hevc.rs`,
 `nvenc_hevc_export.rs`, `nvenc_hevc_probe.rs` and `nvenc_hevc_warm.rs` for H.265, `nvenc_hevc_main10.rs` and
