//! `GetChannelName` — the joined-channel lookup ([`crate::script::channel`]). //! //! The verb is small; the contract around it is not, and every test here is named after one claim. //! The load-bearing one is [`a_channel_that_is_not_joined_answers_the_number_zero_never_nil`]: the //! reference's own callers and the corpus both compare the first return numerically, so a nil there //! does not degrade — it raises, at four call sites that work today. use super::common::script; /// Two channels joined in order, the way `ui_chat::feed` mirrors them after the server's /// YOU_JOINED. Join ORDER is the whole numbering law, so the fixture is deliberately not sorted. fn joined() -> crate::script::UiScript { let mut s = script(); s.set_joined_channels(vec![Some("World".into()), Some("Trade - City".into())]); s } /// **A joined channel answers its 1-based slot, its name, and an instanceID.** The slot is the /// position in JOIN order — not a DBC id, not an alphabetical rank. #[test] fn a_joined_channel_answers_its_slot_name_and_instance() { let s = joined(); let (id, name, instance): (i64, String, i64) = s.eval("return GetChannelName(2)").unwrap(); assert_eq!(id, 2, "the 1-based slot in join order"); assert_eq!(name, "Trade - City"); assert_eq!( instance, 0, "instanceID is 0 on every vanilla emulator, and a NUMBER so a caller can compare it" ); } /// **The name form resolves, case-insensitively, and yields the same slot as the index form.** /// Half the corpus sites look up by name (`GetChannelName("world")`, `GetChannelName("Trade - /// City")`), and `_LazyPig` passes `"world"` for a channel the server names `"World"`. #[test] fn the_name_form_resolves_case_insensitively_to_the_same_slot() { let s = joined(); let by_name: i64 = s.eval("return GetChannelName('world')").unwrap(); let by_index: i64 = s.eval("return GetChannelName(1)").unwrap(); assert_eq!(by_name, 1); assert_eq!(by_name, by_index, "both directions must agree on the slot"); let name: String = s .eval("local _, n = GetChannelName('TRADE - CITY') return n") .unwrap(); assert_eq!( name, "Trade - City", "the answer is the JOINED spelling, not the caller's" ); } /// **Not joined ⇒ the NUMBER 0. Never nil.** This is the trap, and it is the only way this verb can /// break working code rather than merely fail to help it. /// /// Verified from both sides. The reference compares it numerically at `ChatFrame.lua:2114` /// (`if ( channelNum > 0 )`) and `l.2232` (`if ( channelNum <= 0 ) then return end`); so does the /// corpus at `_LazyPig/LazyPig.lua:1996` (`if id > 0 then`). A nil first return turns every one of /// those into "attempt to compare nil with number" — a loud failure in code that works today. /// /// Asserted by DOING the comparison, not by inspecting the type, because the comparison is the /// thing that must not raise. #[test] fn a_channel_that_is_not_joined_answers_the_number_zero_never_nil() { let s = joined(); let id: i64 = s.eval("return GetChannelName('NoSuchChannel')").unwrap(); assert_eq!(id, 0); // The reference's own guard shape, run for real: it must evaluate, not raise. let guard: bool = s .eval("local id = GetChannelName('NoSuchChannel') return id > 0") .unwrap(); assert!( !guard, "the reference's `if ( channelNum > 0 )` must run and be false" ); let out_of_range: bool = s .eval("local id = GetChannelName(99) return id > 0") .unwrap(); assert!(!out_of_range, "an out-of-range index takes the same branch"); let zero: bool = s .eval("local id = GetChannelName(0) return id > 0") .unwrap(); assert!( !zero, "the client bounds-checks 1 <= n <= count, so 0 is not a slot" ); } /// **A numeric STRING resolves as a number**, because `ChatFrame.lua:2113` hands one straight in: /// it `gsub`s `/1` down to `"1"` and calls `GetChannelName(channel)`. On the real client Lua's own /// coercion makes that work; ours must not treat it as a channel literally named "1". #[test] fn a_numeric_string_resolves_as_an_index_not_as_a_name() { let s = joined(); let id: i64 = s.eval("return GetChannelName('2')").unwrap(); assert_eq!(id, 2, "the `/2` slash-command path depends on this"); } /// **Empty until the server confirms a join.** `ui_chat::feed` appends on the server's YOU_JOINED /// notice, never on the request, so a session that has asked and not been answered has no slots — /// and the verb must still answer 0 rather than raise on an empty list. #[test] fn nothing_is_joined_before_the_server_confirms_it() { let s = script(); let id: i64 = s.eval("return GetChannelName('World')").unwrap(); assert_eq!(id, 0); } /// **`GetChannelList()` is a FLAT vararg of (slot, name) pairs in join order** — not a table, and /// not name-first. /// /// Neither wow-re nor any recorded signature pins the shape; two independent consumers do, and the /// test asserts what each of them relies on. The reference's `FCFDropDown_LoadChannels` walks /// `for i=1, arg.n, 2` and reads `arg[i+1]` as the name, so the pair order is (slot, name) and the /// stride is 2. `ChatLog.lua:424` packs it with `{ GetChannelList() }` and tests /// `type(value) == "number"` to spot an id, so the values must interleave rather than nest. #[test] fn get_channel_list_is_a_flat_slot_name_vararg_in_join_order() { let s = joined(); // Exactly two pairs for two joined channels — the arity `FCFDropDown_LoadChannels` steps over. assert_eq!( s.eval::("return select('#', GetChannelList())") .unwrap(), 4 ); // Pair order and join order, both at once: slot 1 is the FIRST joined, not the alphabetical // first ("Trade - City" would sort ahead of "World"). let (s1, n1, s2, n2) = s .eval::<(i64, String, i64, String)>("return GetChannelList()") .unwrap(); assert_eq!((s1, n1.as_str()), (1, "World")); assert_eq!((s2, n2.as_str()), (2, "Trade - City")); // ChatLog's own walk: packed, the odd entries are numbers and the even ones are names. assert!(s .eval::( "local t = { GetChannelList() } \ return table.getn(t) == 4 and type(t[1]) == 'number' and type(t[2]) == 'string'" ) .unwrap()); // The slot agrees with GetChannelName's, which is the point of sharing the numbering. assert!(s .eval::("local i = GetChannelName('Trade - City') return i == 2") .unwrap()); // No channels joined is ZERO returns, so `{ GetChannelList() }` is an empty table rather than // a table of nils — every consumer above already handles that shape. let empty = crate::script::tests::common::script(); assert_eq!( empty .eval::("return select('#', GetChannelList())") .unwrap(), 0 ); }