Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-forge/tests/comments_no_paging.rs
atlas 3d81266483 hive-forge: comments --since --limit returns N, not at most 49
The issue-comments endpoint takes no page or limit, so `comments
--since` cut its window client-side from a whole-list response. The
leftover over-fetch-by-one request (`.page_size(limit + 1)`) was a
no-op and the `--limit` clamp to MAX_LIMIT (49) guarded a truncation
check that no longer needs one.

Drop both: `--since T --limit N` now returns N comments. `comments
--json` still emits `since_limit_clamped`, always `false`. `timeline`
is untouched; its endpoint pages and its clamp stays.
2026-10-09 21:46:14 +02:00

159 lines
5.8 KiB
Rust

//! `view` and `comments` against a mock forge whose issue-comments
//! endpoint behaves like Forgejo's: it takes no page or limit and returns
//! the whole thread on every request. Drives the compiled binary, since
//! the request code is inline in each verb's `run()`.
use std::io::{BufRead as _, BufReader, Write as _};
use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::process::Command;
/// More than one 50-row page, so a loop that pages until a short page
/// never sees one.
const THREAD_LEN: usize = 60;
/// The issue the mock forge serves; any other route gets an empty list.
const NUMBER: &str = "1";
/// A unique scratch `HYPERHIVE_STATE_DIR` holding a `forge-token` file,
/// same manual-tmpdir pattern as `credential_helper.rs`.
fn scratch_state_dir(tag: &str) -> PathBuf {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let dir = std::env::temp_dir().join(format!("hive-forge-comments-test-{tag}-{ts}"));
std::fs::create_dir_all(&dir).expect("create scratch state dir");
std::fs::write(dir.join("forge-token"), "test-token\n").expect("seed forge-token");
dir
}
/// Answer one request. The comments route ignores the query string, as
/// the real endpoint does; any route the verbs only read best-effort
/// (notifications, reactions, PR reviews) gets an empty list.
fn serve(mut stream: TcpStream) {
let mut reader = BufReader::new(stream.try_clone().expect("clone stream"));
let mut request_line = String::new();
reader
.read_line(&mut request_line)
.expect("read request line");
loop {
let mut line = String::new();
if reader.read_line(&mut line).expect("read header") == 0 || line == "\r\n" {
break;
}
}
let target = request_line.split_whitespace().nth(1).unwrap_or("");
let path = target.split('?').next().unwrap_or("");
let issue = format!("/api/v1/repos/o/r/issues/{NUMBER}");
let body = match path {
p if p == format!("{issue}/comments") => serde_json::Value::Array(
(0..THREAD_LEN)
.map(|i| {
serde_json::json!({
"id": i + 1,
"body": format!("comment {i}"),
"user": {"login": "someone"},
"created_at": format!("2026-10-09T00:{:02}:00Z", i % 60),
})
})
.collect(),
),
p if p == issue => serde_json::json!({
"number": NUMBER.parse::<u64>().expect("numeric NUMBER"),
"title": "a long thread",
"body": "",
"state": "open",
"user": {"login": "someone"},
}),
_ => serde_json::json!([]),
}
.to_string();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nX-Total-Count: {THREAD_LEN}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
stream
.write_all(response.as_bytes())
.expect("write response");
}
/// Start the mock forge on a loopback port; returns its base URL.
fn mock_forge() -> String {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock forge");
let addr = listener.local_addr().expect("mock forge addr");
std::thread::spawn(move || {
for stream in listener.incoming() {
serve(stream.expect("accept"));
}
});
format!("http://{addr}")
}
/// Run `hive-forge -r o/r <args>` against a fresh mock forge; returns
/// stdout, asserting the run succeeded.
fn hive_forge(tag: &str, args: &[&str]) -> String {
let out = Command::new(env!("CARGO_BIN_EXE_hive-forge"))
.args(["-r", "o/r"])
.args(args)
.env("HIVE_FORGE_URL", mock_forge())
.env("HYPERHIVE_STATE_DIR", scratch_state_dir(tag))
.env_remove("HIVE_FORGE_TOKEN_FILE")
.env_remove("HIVE_LABEL")
.output()
.expect("run hive-forge");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
out.status.success(),
"hive-forge {args:?} failed\nstdout:\n{stdout}\nstderr:\n{}",
String::from_utf8_lossy(&out.stderr)
);
stdout
}
/// Rendered comments: both verbs start each one with a bold `**` author.
fn comment_count(stdout: &str) -> usize {
stdout.lines().filter(|l| l.starts_with("**")).count()
}
#[test]
fn view_prints_each_comment_once() {
let stdout = hive_forge("view", &["view", NUMBER]);
assert_eq!(comment_count(&stdout), THREAD_LEN, "{stdout}");
assert!(stdout.contains(&format!("## Comments ({THREAD_LEN})")));
}
#[test]
fn comments_limit_returns_exactly_limit() {
let stdout = hive_forge("limit", &["comments", NUMBER, "--limit", "5"]);
assert_eq!(comment_count(&stdout), 5, "{stdout}");
assert!(stdout.contains(&format!("({} more comment(s)", THREAD_LEN - 5)));
}
#[test]
fn comments_tail_returns_the_last_n() {
let stdout = hive_forge("tail", &["comments", NUMBER, "--tail", "3"]);
assert_eq!(comment_count(&stdout), 3, "{stdout}");
assert!(stdout.contains(&format!("comment {}", THREAD_LEN - 1)));
assert!(!stdout.contains(&format!("comment {}", THREAD_LEN - 4)));
assert!(stdout.contains(&format!("({} earlier comment(s)", THREAD_LEN - 3)));
}
#[test]
fn comments_since_limit_above_a_page_returns_exactly_limit() {
let stdout = hive_forge(
"since-limit",
&[
"--json",
"comments",
NUMBER,
"--since",
"2026-10-09T00:00:00Z",
"--limit",
"55",
],
);
let out: serde_json::Value = serde_json::from_str(&stdout).expect("comments --json output");
assert_eq!(out["comments"].as_array().map(Vec::len), Some(55));
assert_eq!(out["since_more"], true);
assert_eq!(out["since_limit_clamped"], false);
}