hive-forge: fetch issue comments in one request; the endpoint has no paging
The forge's issue-comments endpoint takes no page or limit and always returns the whole thread. #5055 wrapped view's fetch in all_pages, which never saw a short page on a thread of 50+ comments and fetched the same list 50 times: `issue view 3782` printed 3000 comments instead of 60. `comments --limit K` sent K as a page size the forge ignored, so it returned the whole thread. view and pr status's last_comment now send one request. comments fetches the thread once and cuts the --limit / --tail window client-side; the X-Total-Count probe and the computed-page tail walk are gone, and the count of comments outside the window is the length of the list. --since is unchanged. tests/comments_no_paging.rs runs the binary against a loopback mock forge whose comments route ignores page and limit, as Forgejo's does.
This commit is contained in:
parent
6f8a6ab8e0
commit
1d56a74aad
6 changed files with 215 additions and 262 deletions
139
hive-forge/tests/comments_no_paging.rs
Normal file
139
hive-forge/tests/comments_no_paging.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
//! `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)));
|
||||
}
|
||||
Loading…
Reference in a new issue