test: raise coverage across keydb, keydb_format, online, lib sources

Adds targeted unit tests for previously-uncovered branches: keydb load
failure classification (invalid-UTF-8 / unreadable path), zip extraction
success/no-cfg-member paths, write_atomic create/rename failure modes,
keydb_format hex-digit-parsing edge cases, iter_disc_entries, to_keydb_cfg
HC2 emission and empty-title/comment-field combinations, MultiSource::label,
and the online key-service client's SSRF-guard malformed-authority and
unresolvable-host paths, UK-as-string and VUK-derivation reply handling,
and get_fmts_indexes. Local line coverage moves from ~90.3% to ~92.1%.
This commit is contained in:
Matthew Jackson
2026-08-25 22:11:01 -07:00
parent 4ef4d5dfd6
commit e83b7a3489
4 changed files with 523 additions and 0 deletions
+148
View File
@@ -809,6 +809,17 @@ mod tests {
const HASH: &str = "0xaabb";
/// `MockCtx` implements the full `ResolveCtx` trait so it can stand in for
/// a real disc, but `KeydbSource` itself never calls `title()`/`samples()`
/// (those matter to the online source, not the local keydb). Exercise
/// them directly so the mock's trait surface is proven correct too.
#[test]
fn mock_ctx_title_and_samples_are_stubbed_correctly() {
let c = ctx(HASH, Vec::new(), None);
assert_eq!(c.title(), None);
assert_eq!(c.samples(4).unwrap(), Vec::<Vec<u8>>::new());
}
// ── KAT (a): disc with terminal Unit Keys, no enc_title_keys ──────────────
/// Stored terminal unit keys are returned with their CPS numbering preserved.
/// Here `enc_title_keys` is empty, so the VUK can't derive anything — only the
@@ -1637,6 +1648,52 @@ mod tests {
assert!(matches!(src.save(garbage), Err(Error::KeydbInvalid)));
}
/// A keydb.cfg that is over the size cap or not valid UTF-8
/// (`std::io::ErrorKind::InvalidData`) must surface as a SOURCE FAILURE
/// (`Error::KeydbInvalid`), never as a silent empty result — a corrupt
/// keydb is not the same thing as "this disc has no key".
#[test]
fn get_unit_keys_reports_invalid_utf8_keydb_as_a_failure() {
let dir = scratch("load-failure-invalid");
let path = dir.join("keydb.cfg");
std::fs::write(&path, [0xffu8, 0xfe, 0x00, 0x01]).unwrap();
let src = KeydbSource::new(&path);
let err = src
.get_unit_keys(&ctx(HASH, Vec::new(), None))
.expect_err("non-UTF-8 keydb must be a failure, not an empty miss");
assert!(matches!(err, Error::KeydbInvalid));
}
/// Any OTHER read failure (not "missing", not "invalid data") — e.g. the
/// configured path is a directory, not a file — is also a source failure,
/// distinct from `KeydbInvalid`.
#[test]
fn get_unit_keys_reports_unreadable_keydb_path_as_a_failure() {
let dir = scratch("load-failure-unreadable");
// Point the source AT a directory: `File::open` fails with an io::Error
// whose kind is neither NotFound nor InvalidData.
let src = KeydbSource::new(&dir);
let err = src
.get_unit_keys(&ctx(HASH, Vec::new(), None))
.expect_err("an unreadable keydb path must be a failure, not an empty miss");
assert!(matches!(err, Error::KeydbLoad { .. }));
}
/// `host_certs` (the `Vec`-returning trait method) hits the exact same
/// `load_failure` branches, just via a swallowed log instead of `Err` —
/// prove it returns empty rather than panicking on a corrupt keydb.
#[test]
fn host_certs_swallows_a_corrupt_keydb_into_an_empty_list() {
let dir = scratch("load-failure-hostcerts");
let path = dir.join("keydb.cfg");
std::fs::write(&path, [0xffu8, 0xfe]).unwrap();
let src = KeydbSource::new(&path);
assert!(
src.host_certs().is_empty(),
"a corrupt keydb must yield no host certs, not panic"
);
}
/// `save` recognises gzip magic (0x1f 0x8b) and routes to the gz decoder; a
/// truncated gzip is a parse/invalid error, never a plain-text UTF-8 error.
#[test]
@@ -1663,6 +1720,97 @@ mod tests {
}
}
/// `extract_zip` on a WELL-FORMED zip that carries no `*.cfg`/`*.CFG`
/// member must fall through the whole member loop and return
/// `Error::KeydbInvalid` — never a parse error, and never silently pick a
/// non-keydb member.
#[test]
fn save_rejects_a_valid_zip_with_no_cfg_member() {
use std::io::Write as _;
let mut buf = Vec::new();
{
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let opts: zip::write::SimpleFileOptions = zip::write::SimpleFileOptions::default();
writer.start_file("readme.txt", opts).unwrap();
writer.write_all(b"not a keydb").unwrap();
writer.finish().unwrap();
}
assert!(buf.starts_with(b"PK\x03\x04"), "must be a real zip");
let dir = scratch("save-zip-no-cfg");
let src = KeydbSource::new(dir.join("k.cfg"));
assert!(matches!(src.save(&buf), Err(Error::KeydbInvalid)));
}
/// `save` on a WELL-FORMED zip that DOES carry a `*.cfg` member routes
/// through the success arm of `extract_zip` — proven distinct from the
/// "no member found" and "truncated/corrupt zip" cases above.
#[test]
fn save_extracts_the_cfg_member_from_a_valid_zip() {
use std::io::Write as _;
let mut buf = Vec::new();
{
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let opts: zip::write::SimpleFileOptions = zip::write::SimpleFileOptions::default();
writer.start_file("ignored.txt", opts).unwrap();
writer.write_all(b"not a keydb").unwrap();
writer.start_file("KEYDB.cfg", opts).unwrap();
writer
.write_all(b"0xAABBCCDDAABBCCDDAABBCCDDAABBCCDD = Test\n")
.unwrap();
writer.finish().unwrap();
}
let dir = scratch("save-zip-with-cfg");
let src = KeydbSource::new(dir.join("k.cfg"));
let result = src
.save(&buf)
.expect("a zip carrying a .cfg member must extract");
assert_eq!(result.entries, 1);
}
/// `write_atomic` when the temp file cannot be CREATED (permission
/// denied on an existing, unwritable parent directory) must clean up and
/// surface `Error::KeydbWrite`, distinct from the create_dir_all failure
/// already pinned by `write_atomic_failure_preserves_prior_keydb`.
#[cfg(unix)]
#[test]
fn write_atomic_failure_when_temp_file_cannot_be_created() {
use std::os::unix::fs::PermissionsExt;
let dir = scratch("write-atomic-unwritable");
let target = dir.join("keydb.cfg");
// Parent dir exists (so create_dir_all is a no-op) but is read-only,
// so `File::create(&tmp)` fails with permission denied.
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o555)).unwrap();
let result = write_atomic(&target, "0xAAAA = x\n");
// Restore write permission so the scratch dir can be cleaned up by a
// later test run regardless of outcome here.
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(matches!(result, Err(Error::KeydbWrite { .. })));
}
/// `write_atomic` when the RENAME step fails (the temp file wrote fine,
/// but the destination is an existing directory, so `rename` refuses)
/// must surface `Error::KeydbWrite` and remove the orphaned temp file.
#[test]
fn write_atomic_failure_when_rename_target_is_a_directory() {
let dir = scratch("write-atomic-rename-fail");
// The "keydb path" is itself an existing directory — the temp file
// writes fine (same parent), but renaming a file onto a directory
// always fails.
let target = dir.join("keydb.cfg");
std::fs::create_dir_all(&target).unwrap();
let result = write_atomic(&target, "0xAAAA = x\n");
assert!(matches!(result, Err(Error::KeydbWrite { .. })));
// No stray temp file left behind.
let leftovers: Vec<_> = std::fs::read_dir(&dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.contains(".tmp."))
.collect();
assert!(leftovers.is_empty(), "stray temp files: {leftovers:?}");
}
/// `read_capped_to_string` rejects input over the cap (decompression-bomb
/// guard) and accepts exactly at the cap.
#[test]
+109
View File
@@ -1922,6 +1922,15 @@ mod tests {
assert_eq!(pk, [0xABu8; 16]);
}
/// A `| PK |` row with fewer than 3 `|`-delimited parts (no key field at
/// all) has nothing to parse and must return `None`, not panic on an
/// out-of-range index.
#[test]
fn parse_processing_key_rejects_row_with_too_few_parts() {
assert!(KeyDb::parse_processing_key("| PK |").is_none());
assert!(KeyDb::parse_processing_key("just text, no pipes at all").is_none());
}
// ── Disc-entry comment metadata: MKBv / VolumeSize / UHD ────────────────
#[test]
@@ -2084,6 +2093,106 @@ mod tests {
assert_eq!(uks[0].1, vec![(1, [0x03u8; 16])]);
}
/// `parse_digits_after` must return `None`, not panic or misparse, when
/// the marker is present but no digit immediately follows it (Path 3 of
/// the MKBv/VolumeSize comment grammar: marker-with-no-value).
#[test]
fn parse_digits_after_marker_with_no_trailing_digits_is_none() {
assert_eq!(parse_digits_after::<u32>("MKBv", "MKBv"), None);
assert_eq!(parse_digits_after::<u32>("MKBv/BEE", "MKBv"), None);
assert_eq!(parse_digits_after::<u32>("no marker here", "MKBv"), None);
}
/// `iter_disc_entries` yields every parsed disc, independent of hash
/// lookup — Path 3's full-scan use.
#[test]
fn iter_disc_entries_yields_every_disc() {
let k = "01".repeat(16);
let db = KeyDb::parse(&format!(
"0xAAAA = A | U | 1-0x{k}\n0xBBBB = B | U | 1-0x{k}\n"
));
let mut hashes: Vec<&str> = db.iter_disc_entries().map(|e| &*e.disc_hash).collect();
hashes.sort();
assert_eq!(hashes, vec!["0xaaaa", "0xbbbb"]);
}
/// `to_keydb_cfg` emits the sibling `| HC2 |` line for a host cert that
/// carries AACS 2.0 credentials, and a `parse` of the output recovers
/// them. Also: an empty title serializes as the literal `Unknown` (the
/// inverse of the parser's own convention).
#[test]
fn to_keydb_cfg_emits_hc2_and_unknown_for_empty_title() {
let h = |b: u8, n: usize| format!("{b:02x}").repeat(n);
let k = h(0x09, 16);
let src = format!(
"| HC | HOST_PRIV_KEY 0x{priv1} | HOST_CERT 0x{cert1}\n\
| HC2 | HOST_PRIV_KEY 0x{priv2} | HOST_CERT 0x{cert2}\n\
0xAAAA = | U | 1-0x{k}\n",
priv1 = h(0x01, 20),
cert1 = h(0x02, 92),
priv2 = h(0x03, 32),
cert2 = h(0x04, 132),
);
let db = KeyDb::parse(&src);
assert_eq!(db.host_certs.len(), 1);
assert!(db.host_certs[0].cert.certificate_v2.is_some());
// Empty title round-trips through "Unknown".
let entry = db.disc_entries.values().next().unwrap();
assert_eq!(entry.title, "");
let out = db.to_keydb_cfg();
assert!(
out.contains("| HC2 | HOST_PRIV_KEY"),
"HC2 must be emitted: {out}"
);
assert!(
out.contains("Unknown"),
"empty title must serialize as Unknown: {out}"
);
// And it round-trips: re-parsing sees the HC2 material again.
let db2 = KeyDb::parse(&out);
assert_eq!(db2.host_certs.len(), 1);
assert!(db2.host_certs[0].cert.certificate_v2.is_some());
assert_eq!(
db2.host_certs[0].cert.private_key_v2,
db.host_certs[0].cert.private_key_v2
);
}
/// `to_keydb_cfg`'s comment clause is written piecewise (MKBv, then
/// VolumeSize, then UHD) — exercise each optional sub-field independently
/// (not just all-three-true) so every combination of the trailing
/// `; comment` assembly is proven, not just the "everything present" case.
#[test]
fn to_keydb_cfg_comment_fields_are_independent() {
let k = "0a".repeat(16);
// Only VolumeSize, no MKBv, not UHD.
let a = KeyDb::parse(&format!("0xAAAA = A | U | 1-0x{k} ; VolumeSize: 12345\n"));
let out_a = a.to_keydb_cfg();
assert!(out_a.contains("VolumeSize: 12345"), "{out_a}");
assert!(!out_a.contains("MKBv"), "{out_a}");
assert!(!out_a.contains("(UHD)"), "{out_a}");
let a2 = KeyDb::parse(&out_a);
let ea2 = a2.disc_entries.values().next().unwrap();
assert_eq!(ea2.volume_size, Some(12345));
assert_eq!(ea2.mkb_version, None);
assert!(!ea2.is_uhd);
// Only the UHD flag, no MKBv, no VolumeSize.
let b = KeyDb::parse(&format!("0xBBBB = B | U | 1-0x{k} ; (UHD)\n"));
let out_b = b.to_keydb_cfg();
assert!(out_b.contains("(UHD)"), "{out_b}");
assert!(!out_b.contains("MKBv"), "{out_b}");
assert!(!out_b.contains("VolumeSize"), "{out_b}");
let b2 = KeyDb::parse(&out_b);
let eb2 = b2.disc_entries.values().next().unwrap();
assert!(eb2.is_uhd);
assert_eq!(eb2.mkb_version, None);
assert_eq!(eb2.volume_size, None);
}
// ════════════════════════════════════════════════════════════════════
// KEYDB-parser integration tests relocated from libfreemkv.
//
+257
View File
@@ -907,6 +907,52 @@ mod tests {
assert!(resolve_and_guard("").is_err());
}
/// Malformed-authority edges that `resolve_and_guard` must reject BEFORE
/// ever touching DNS: an empty host after the scheme (`https://`), an
/// unterminated IPv6 literal (missing `]`), and a bare `host:` with an
/// empty host part.
#[test]
fn resolve_and_guard_rejects_malformed_authorities() {
// Scheme with nothing after it at all.
assert!(resolve_and_guard("https://").is_err());
// Scheme immediately followed by a path — empty authority.
assert!(resolve_and_guard("https:///keys").is_err());
// Bracketed IPv6 host missing its closing `]`.
assert!(resolve_and_guard("http://[::1/keys").is_err());
// `host:port` split with an empty host before the colon.
assert!(resolve_and_guard("https://:8080/keys").is_err());
}
/// A host that genuinely does not resolve (RFC 6761 reserves `.test`
/// to never resolve in the public DNS) must be reported as
/// `GuardFail::Unreachable` — "the service is down", not
/// `GuardFail::Config` ("fix the URL"). No real network traffic beyond a
/// local DNS lookup that is guaranteed to fail.
#[test]
fn resolve_and_guard_reports_unreachable_for_a_host_that_never_resolves() {
let (kind, msg) = resolve_and_guard("https://this-host-does-not-exist.test/keys")
.expect_err(
"a .test host must never resolve — if this passes, treat it as a fixture bug",
);
assert_eq!(kind, GuardFail::Unreachable);
assert!(msg.contains("resolve"), "message should explain: {msg}");
}
/// `PinnedResolver` with an EMPTY address set — the state `agent_for`
/// would build if `resolve_and_guard` somehow returned no addresses —
/// must fail the resolve step with `HostNotFound` rather than silently
/// falling back to live DNS.
#[test]
fn pinned_resolver_with_no_addresses_fails_the_connection() {
let result = hardened_agent(Vec::new())
.post("http://keyserver.test/keys")
.send("{}");
assert!(
result.is_err(),
"an empty pin must fail the connection, not silently resolve some other way"
);
}
#[test]
fn resolve_and_guard_accepts_public_literal() {
// Public numeric hosts resolve without DNS — must be accepted.
@@ -1199,6 +1245,110 @@ mod tests {
assert_eq!(keys[1].key[0], 0x0f);
}
/// Backward-compatible form: `"UK"` as a bare hex STRING (not an array)
/// is still the single base Unit Key at index 0.
#[test]
fn uk_as_a_bare_string_is_still_accepted() {
let keys = interpret_reply(
Ok(reply(200, r#"{"UK":"000102030405060708090a0b0c0d0e0f"}"#)),
&BareCtx,
1,
)
.expect("a string UK must still resolve");
assert_eq!(keys.len(), 1);
assert_eq!(keys[0].idx, 0);
assert_eq!(keys[0].key[0], 0x00);
}
/// A malformed `"UK"` STRING (not the array form) parses to nothing and
/// falls through to the genuine-miss path — proven distinct from the
/// array form's "reject the whole reply" behaviour, since a bad scalar
/// string has no partial set to protect.
#[test]
fn uk_as_an_unparseable_string_falls_through_to_a_miss() {
let keys = interpret_reply(Ok(reply(200, r#"{"UK":"not hex"}"#)), &BareCtx, 1)
.expect("an unparseable scalar UK is a miss, not a transport failure");
assert!(keys.is_empty());
}
/// A `"VUK"` reply is derived LOCALLY into terminal unit keys via the
/// disc's encrypted title keys — the service never sees or returns them
/// directly.
#[test]
fn vuk_reply_is_derived_locally_into_unit_keys() {
struct EncCtx;
impl ResolveCtx for EncCtx {
fn disc_hash(&self) -> &str {
"0x422EB"
}
fn title(&self) -> Option<&str> {
None
}
fn vid(&self) -> Option<libfreemkv::aacs::types::Vid> {
None
}
fn mkb(&self) -> Result<&[u8], Error> {
Ok(&[])
}
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error> {
Ok(&[[0x11u8; 16], [0x22u8; 16]])
}
fn samples(&self, _n: usize) -> Result<Vec<Vec<u8>>, Error> {
Ok(Vec::new())
}
}
let vuk_hex = "0f0e0d0c0b0a09080706050403020100";
let keys = interpret_reply(
Ok(reply(200, &format!(r#"{{"VUK":"{vuk_hex}"}}"#))),
&EncCtx,
1,
)
.expect("a VUK reply must resolve");
assert_eq!(
keys.len(),
2,
"one derived unit key per encrypted title key"
);
assert_eq!(keys[0].idx, 0);
assert_eq!(keys[1].idx, 1);
}
/// The service answered correctly with a VUK, but the DISC's encrypted
/// title keys could not be read — that is a disc-side condition, not a
/// service failure, so it is `Ok(empty)`, never `Err`.
#[test]
fn vuk_reply_with_unreadable_enc_title_keys_is_an_empty_ok_not_an_error() {
struct BrokenEncCtx;
impl ResolveCtx for BrokenEncCtx {
fn disc_hash(&self) -> &str {
"0x422EB"
}
fn title(&self) -> Option<&str> {
None
}
fn vid(&self) -> Option<libfreemkv::aacs::types::Vid> {
None
}
fn mkb(&self) -> Result<&[u8], Error> {
Ok(&[])
}
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error> {
Err(Error::KeydbInvalid)
}
fn samples(&self, _n: usize) -> Result<Vec<Vec<u8>>, Error> {
Ok(Vec::new())
}
}
let vuk_hex = "0f0e0d0c0b0a09080706050403020100";
let keys = interpret_reply(
Ok(reply(200, &format!(r#"{{"VUK":"{vuk_hex}"}}"#))),
&BrokenEncCtx,
1,
)
.expect("an unreadable disc-side input must not be a source failure");
assert!(keys.is_empty());
}
/// A URL that fails the ADDRESS guard is operator configuration, not the
/// service being down — the two must stay separable, since only one of them
/// is worth retrying.
@@ -1443,6 +1593,113 @@ mod tests {
);
}
/// `get_fmts_indexes` shares `query` with `get_unit_keys` — never
/// separately exercised. Same guard-blocked, no-network path as the
/// `get_unit_keys` tests above proves it is wired up.
#[test]
fn get_fmts_indexes_shares_the_same_query_path() {
let src = OnlineSource::new("https://127.0.0.1/keys", "s3cr3t");
let ctx = GuardCtx {
mkb: Vec::new(),
samples: MIN_SAMPLE_UNITS,
};
assert_eq!(
src.get_fmts_indexes(&ctx)
.expect_err("a blocked address means the service was never asked")
.code(),
libfreemkv::error::E_KEY_SERVICE_UNAVAILABLE,
);
}
/// A host that genuinely does not resolve travels `query`'s OWN
/// `GuardFail::Unreachable` arm (distinct from the `Config` arm covered
/// above) — still `Err`, since "did not resolve" is what a key-service
/// outage looks like from here, never a genuine miss.
#[test]
fn query_against_an_unresolvable_host_reports_unreachable_as_a_failure() {
let src = OnlineSource::new("https://this-host-does-not-exist.test/keys", "s3cr3t");
let ctx = GuardCtx {
mkb: Vec::new(),
samples: MIN_SAMPLE_UNITS,
};
assert_eq!(
src.get_unit_keys(&ctx)
.expect_err("an unresolvable host means the service was never asked")
.code(),
libfreemkv::error::E_KEY_SERVICE_UNAVAILABLE,
);
}
/// The request body's `vid_b64` and `title` fields are assembled BEFORE
/// the address guard runs (see `query`'s ordering), so a ctx that
/// supplies a VID and a title still exercises that assembly even when
/// the guard then rejects the address — proven through the same
/// deterministic guard-blocked path the tests above use (no network).
#[test]
fn query_assembles_vid_and_title_before_the_address_guard_runs() {
struct VidTitleCtx;
impl ResolveCtx for VidTitleCtx {
fn disc_hash(&self) -> &str {
"0x422EB"
}
fn title(&self) -> Option<&str> {
Some(" My Disc Title ")
}
fn vid(&self) -> Option<libfreemkv::aacs::types::Vid> {
Some(libfreemkv::aacs::types::Vid([0x42u8; 16]))
}
fn mkb(&self) -> Result<&[u8], Error> {
Ok(&[])
}
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error> {
Ok(&[])
}
fn samples(&self, _n: usize) -> Result<Vec<Vec<u8>>, Error> {
Ok(vec![vec![0u8; 16]; MIN_SAMPLE_UNITS])
}
}
// 127.0.0.1 needs no DNS and is unconditionally rejected — the guard
// fires AFTER the body (incl. vid/title) is already built.
let src = OnlineSource::new("https://127.0.0.1/keys", "s3cr3t");
assert_eq!(
src.get_unit_keys(&VidTitleCtx)
.expect_err("a blocked address means the service was never asked")
.code(),
libfreemkv::error::E_KEY_SERVICE_UNAVAILABLE,
);
}
/// A title that is present but ALL WHITESPACE must not be sent — `query`
/// trims then checks `is_empty()` before adding the `title` field.
#[test]
fn query_skips_a_whitespace_only_title() {
struct WhitespaceTitleCtx;
impl ResolveCtx for WhitespaceTitleCtx {
fn disc_hash(&self) -> &str {
"0x422EB"
}
fn title(&self) -> Option<&str> {
Some(" ")
}
fn vid(&self) -> Option<libfreemkv::aacs::types::Vid> {
None
}
fn mkb(&self) -> Result<&[u8], Error> {
Ok(&[])
}
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error> {
Ok(&[])
}
fn samples(&self, _n: usize) -> Result<Vec<Vec<u8>>, Error> {
Ok(vec![vec![0u8; 16]; MIN_SAMPLE_UNITS])
}
}
let src = OnlineSource::new("https://127.0.0.1/keys", "s3cr3t");
// Reaches the same guard-blocked failure either way; this test's
// value is in exercising the whitespace-title branch without panic.
assert!(src.get_unit_keys(&WhitespaceTitleCtx).is_err());
}
// ── One agent per address set, not one per query ────────────────────────
/// A round-robin keyserver returns the SAME addresses in a different order
+9
View File
@@ -270,6 +270,15 @@ fn uk(b: u8) -> UnitKey {
UnitKey::new(0, [b; 16])
}
/// `MultiSource` itself reports a fixed label, distinct from any inner
/// source's own label — this is what a rip's source-attribution log shows
/// when the composition, not one of its members, answered.
#[test]
fn multi_source_label_is_multi() {
let multi = MultiSource::new(vec![Box::new(ScriptedSource::new("A", Vec::new()))]);
assert_eq!(multi.label(), "multi");
}
#[test]
fn multi_source_first_non_empty_wins_in_caller_order() {
// Caller supplies [A, B]; A is non-empty so A's keys win.