Skip to content

Commit 25d5a5e

Browse files
committed
fix(databases): graft the descent past a paginated fork page too
The review caught a hole in the walk: descent was grafted only when a node's lineage fetch FAILED, not when it succeeded with a truncated fork page. A root with more forks than the page (server clamps to 100, newest-first) could page out the very generation that leads to the queried database — the tree then rendered without its '← this database' row and exited 0, breaking the always-marked contract the skill doc states. Now the Ok arm grafts the known descent child whenever the listed page didn't include it, counting it out of the unlisted tally. Regression test: fork_count 3, page of 1, queried database off the page. Also split the Err arm's diagnostics: a 404 stays quiet (the expected shape of a deleted generation), while any other failure (auth, rate limit, transport) still renders '⋯ forks unknown' but says why on stderr, naming the id — so a failed request no longer masquerades as a deleted-generation artifact.
1 parent 2f46826 commit 25d5a5e

1 file changed

Lines changed: 92 additions & 2 deletions

File tree

‎src/commands/databases.rs‎

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,7 @@ fn walk_lineage_tree(
19021902
};
19031903
match fetched {
19041904
Ok(r) => {
1905-
let unlisted_forks = (r.fork_count - r.forks.len() as i64).max(0);
1905+
let mut unlisted_forks = (r.fork_count - r.forks.len() as i64).max(0);
19061906
let mut forks = Vec::new();
19071907
for f in r.forks {
19081908
if visited.contains(&f.database_id) {
@@ -1917,14 +1917,44 @@ fn walk_lineage_tree(
19171917
visited,
19181918
));
19191919
}
1920+
// A paginated fork list can omit the very generation that leads to
1921+
// the queried database (pages are newest-first and the server
1922+
// clamps their size) — graft that known child so the queried
1923+
// database always appears. It was one of the unlisted forks.
1924+
if let Some(g) = descent.get(&id)
1925+
&& !visited.contains(&g.database_id)
1926+
{
1927+
forks.push(walk_lineage_tree(
1928+
api,
1929+
g.clone(),
1930+
forks_limit,
1931+
descent,
1932+
prefetched,
1933+
visited,
1934+
));
1935+
unlisted_forks = (unlisted_forks - 1).max(0);
1936+
}
19201937
LineageTreeNode {
19211938
entry,
19221939
unlisted_forks,
19231940
forks_unknown: false,
19241941
forks,
19251942
}
19261943
}
1927-
Err(_) => {
1944+
Err(e) => {
1945+
// A 404 is the expected shape of a deleted generation: its record
1946+
// survives in relatives' lineage, but the endpoint no longer
1947+
// answers for it. Anything else is a failed request (auth, rate
1948+
// limit, transport) — the fork list is just as unknown, but say
1949+
// so instead of passing it off as a deleted-generation artifact.
1950+
let not_found = matches!(&e, ApiError::Status { status, .. } if *status == reqwest::StatusCode::NOT_FOUND);
1951+
if !not_found {
1952+
let why = match &e {
1953+
ApiError::Status { status, .. } => format!("HTTP {status}"),
1954+
ApiError::Transport(t) => t.clone(),
1955+
};
1956+
eprintln!("warning: could not list forks of {id}: {why}");
1957+
}
19281958
let forks = descent
19291959
.get(&id)
19301960
.filter(|g| !visited.contains(&g.database_id))
@@ -2916,6 +2946,66 @@ mod tests {
29162946
b_lin.assert();
29172947
}
29182948

2949+
#[test]
2950+
fn lineage_tree_grafts_past_paginated_fork_page() {
2951+
// The root has 3 forks but its page lists only one, and the queried
2952+
// database is off the page. The walk must graft the known descent so
2953+
// the queried database still appears, counting it out of the unlisted
2954+
// tally.
2955+
let mut server = mockito::Server::new();
2956+
let q_lin = server
2957+
.mock("GET", "/v1/databases/db_q/lineage")
2958+
.with_status(200)
2959+
.with_header("content-type", "application/json")
2960+
.with_body(lineage_body(
2961+
"db_q",
2962+
"db_root",
2963+
r#"{"database_id":"db_root","name":"prod","exists":true}"#,
2964+
"",
2965+
0,
2966+
))
2967+
.expect(1)
2968+
.create();
2969+
let root_lin = server
2970+
.mock("GET", "/v1/databases/db_root/lineage")
2971+
.with_status(200)
2972+
.with_header("content-type", "application/json")
2973+
.with_body(lineage_body(
2974+
"db_root",
2975+
"db_root",
2976+
"",
2977+
r#"{"database_id":"db_other","exists":true}"#,
2978+
3,
2979+
))
2980+
.create();
2981+
let other_lin = server
2982+
.mock("GET", "/v1/databases/db_other/lineage")
2983+
.with_status(200)
2984+
.with_header("content-type", "application/json")
2985+
.with_body(lineage_body("db_other", "db_root", "", "", 0))
2986+
.create();
2987+
2988+
let api = Api::test_new(&server.url(), "k", Some("ws"));
2989+
let lin = fetch_lineage(&api, "db_q", None).unwrap();
2990+
let t = build_lineage_tree(&api, None, lin, None);
2991+
2992+
assert!(tree_contains(&t.tree, "db_q"), "queried db must appear");
2993+
// 3 total − 1 listed − 1 grafted = 1 still unlisted.
2994+
assert_eq!(t.tree.unlisted_forks, 1);
2995+
assert!(!t.tree.forks_unknown);
2996+
let ids: Vec<&str> = t
2997+
.tree
2998+
.forks
2999+
.iter()
3000+
.map(|f| f.entry.database_id.as_str())
3001+
.collect();
3002+
assert_eq!(ids, ["db_other", "db_q"]);
3003+
3004+
q_lin.assert();
3005+
root_lin.assert();
3006+
other_lin.assert();
3007+
}
3008+
29193009
#[test]
29203010
fn lineage_tree_render_rails_and_markers() {
29213011
let entry = |id: &str, name: Option<&str>| LineageEntry {

0 commit comments

Comments
 (0)