Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7dc4be4
feat(gl): add status-checked read_json helper; fix MCP repo reads (#123)
Jul 11, 2026
3383410
fix(gl): status-check the CLI repo & PR read arms (#123)
Jul 11, 2026
e922246
fix(gl): status-check remaining MCP + CLI gated read arms (#123)
Jul 11, 2026
ad80430
fix(gl): sanitize cmd_info error (INV-6); surface gated denials in gl…
Jul 11, 2026
9a23d71
fix(gl): status-check gl cert list (#123)
Jul 11, 2026
b0f7a0c
fix(review): status-check gated repo bounties read; add missing denia…
Jul 11, 2026
4f91764
test(gl): make gl status section denial-surfacing testable + tested (…
Jul 11, 2026
ee00bdf
test(gl): close remaining #123 coverage gaps
Jul 11, 2026
6d356e0
style(gl): apply rustfmt to #123 test additions
Jul 11, 2026
4266803
test(gl): assert the gated denial mocks are actually hit so a non-mat…
Jul 11, 2026
bf1841c
test(gl): assert mock hits across the remaining gated-denial and serv…
Jul 11, 2026
63abe2f
test(gl): assert mock hits on unwrap_err-style denial/error tests (sy…
Jul 11, 2026
3787a36
test(gl): assert mock hits on remaining unwrap_err denial tests (boun…
Jul 11, 2026
5b943e9
fix(gl): status-check the gl task read/write surfaces so a node denia…
Jul 11, 2026
a829597
fix(gl): status-check the MCP tool surfaces so a node denial isn't re…
Jul 11, 2026
58f2ed1
fix(gl): status-check the sync-status and peer read surfaces so a nod…
Jul 11, 2026
b37a054
fix(gl): route the remaining resolve/list read helpers through read_j…
Jul 11, 2026
975a5be
test(gl): execute-vet the owner-resolution read_json conversions with…
Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/gl/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,14 @@ mod tests {
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"not found"}"#)
.expect(1)
.create_async()
.await;

let err = cmd_list(server.url(), None).await.unwrap_err();
assert!(err.to_string().contains("agents API"));
// Prove the mocked route was actually requested; a non-matching request (mockito's 501, also non-2xx) would otherwise satisfy the error assertion vacuously.
_m.assert_async().await;
}

#[tokio::test]
Expand All @@ -230,11 +233,14 @@ mod tests {
.with_status(500)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"internal error"}"#)
.expect(1)
.create_async()
.await;

let err = cmd_list(server.url(), None).await.unwrap_err();
assert!(err.to_string().contains("list agents failed"));
// Prove the mocked route was actually requested; a non-matching request (mockito's 501, also non-2xx) would otherwise satisfy the error assertion vacuously.
_m.assert_async().await;
}

#[tokio::test]
Expand All @@ -261,13 +267,16 @@ mod tests {
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"agent not found"}"#)
.expect(1)
.create_async()
.await;

let err = cmd_show("did:key:z6MkMissing".to_string(), server.url())
.await
.unwrap_err();
assert!(err.to_string().contains("agents API") || err.to_string().contains("not found"));
// Prove the mocked route was actually requested; a non-matching request (mockito's 501, also non-2xx) would otherwise satisfy the error assertion vacuously.
_m.assert_async().await;
}

#[tokio::test]
Expand Down
42 changes: 37 additions & 5 deletions crates/gl/src/bounty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,14 @@ async fn cmd_list(
u
};

let resp = client
.get_authed(&url)
.await
.context("failed to connect to node")?;
let body: Value = resp.json().await.unwrap_or_default();
let body = crate::http::read_json(
client
.get_authed(&url)
.await
.context("failed to connect to node")?,
"bounties",
)
.await?;

let bounties = body["bounties"].as_array();
if let Some(arr) = bounties {
Expand Down Expand Up @@ -521,13 +524,16 @@ mod tests {
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"not found"}"#)
.expect(1)
.create_async()
.await;

let err = cmd_show("nonexistent".to_string(), server.url(), None)
.await
.unwrap_err();
assert!(err.to_string().contains("not found"));
// Prove the mocked route was actually requested; a non-matching request (mockito's 501, also non-2xx) would otherwise satisfy the error assertion vacuously.
_m.assert_async().await;
}

#[tokio::test]
Expand All @@ -544,4 +550,30 @@ mod tests {

cmd_stats(server.url()).await.unwrap();
}

#[tokio::test]
async fn cmd_list_repo_scoped_surfaces_denial_not_empty() {
// `gl bounty list --repo owner/name` hits the gated /repos/{o}/{n}/bounties;
// a 404 must Err, not silently print nothing (INV-8).
let mut server = mockito::Server::new_async().await;
let _m = server
.mock(
"GET",
mockito::Matcher::Regex(r"/repos/alice/secret/bounties".to_string()),
)
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"repository 'alice/secret' not found"}"#)
.expect(1)
.create_async()
.await;
let result = cmd_list(Some("alice/secret".to_string()), None, server.url(), None).await;
assert!(
result.is_err(),
"bounty list --repo must Err on a gated 404"
);
// Prove the gated repo-scoped path was actually requested: without this,
// an unmatched route (mockito's 501, also non-2xx) would satisfy is_err().
_m.assert_async().await;
}
}
63 changes: 51 additions & 12 deletions crates/gl/src/cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,7 @@ async fn resolve_repo(
did.split(':').next_back().unwrap_or(&did).to_string()
} else {
let client = signed_client(node, dir);
let info: Value = client
.get_authed("/")
.await?
.json()
.await
.context("failed to fetch node info")?;
let info = crate::http::read_json(client.get_authed("/").await?, "node info").await?;
let did = info["did"].as_str().context("node info missing 'did'")?;
did.split(':').next_back().unwrap_or(did).to_string()
};
Expand All @@ -88,12 +83,7 @@ async fn cmd_list(repo: String, node: String, dir: Option<PathBuf>) -> Result<()

let client = signed_client(&node, dir.as_deref());
let path = format!("/api/v1/repos/{owner}/{name}/certs");
let resp: Value = client
.get_authed(&path)
.await?
.json()
.await
.context("failed to list certificates")?;
let resp = crate::http::read_json(client.get_authed(&path).await?, "certificates").await?;

let certs = resp["certificates"].as_array().cloned().unwrap_or_default();

Expand Down Expand Up @@ -209,3 +199,52 @@ async fn resolve_cert_id(client: &NodeClient, owner: &str, name: &str, id: &str)
_ => anyhow::bail!("certificate prefix {id} matches multiple certificates"),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn cmd_list_surfaces_denial_not_empty() {
// A gated 404 on the repo-scoped certs read must Err, not print "No certificates".
let mut server = mockito::Server::new_async().await;
let _m = server
.mock(
"GET",
mockito::Matcher::Regex(r"^/api/v1/repos/alice/secret/certs$".to_string()),
)
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"repository 'alice/secret' not found"}"#)
.expect(1)
.create_async()
.await;
let result = cmd_list("alice/secret".to_string(), server.url(), None).await;
assert!(result.is_err(), "cert list must Err on a gated 404");
// Prove the gated certs path was actually requested: without this, an
// unmatched route (mockito's 501, also non-2xx) would satisfy is_err().
_m.assert_async().await;
}

#[tokio::test]
async fn resolve_repo_surfaces_denial() {
// A slash-free repo with an empty identity dir forces the GET / node-info
// fetch. A gated 404 there must Err (surfacing the status), proving the
// read_json conversion is load-bearing rather than silently ignored.
let mut server = mockito::Server::new_async().await;
let dir = tempfile::TempDir::new().unwrap(); // empty, no identity.pem, forces the GET / branch
let _m = server
.mock("GET", "/")
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"denied"}"#)
.expect(1)
.create_async()
.await;
let err = resolve_repo("noslash", &server.url(), Some(dir.path()))
.await
.unwrap_err();
assert!(err.to_string().contains("404"), "got: {err}");
_m.assert_async().await;
}
}
37 changes: 31 additions & 6 deletions crates/gl/src/changelog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,7 @@ pub async fn run(args: ChangelogArgs) -> Result<()> {
did.split(':').next_back().unwrap_or(&did).to_string()
} else {
let client = NodeClient::new(&args.node, None);
let info: Value = client
.get("/")
.await?
.json()
.await
.context("failed to fetch node info")?;
let info = crate::http::read_json(client.get("/").await?, "node info").await?;
let did = info["did"].as_str().context("node missing DID")?;
did.split(':').next_back().unwrap_or(did).to_string()
};
Expand Down Expand Up @@ -212,6 +207,7 @@ mod tests {
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"repo not found"}"#)
.expect(1)
.create_async()
.await;

Expand All @@ -223,6 +219,8 @@ mod tests {
};
let err = run(args).await.unwrap_err();
assert!(err.to_string().contains("changelog failed"));
// Prove the mocked route was actually requested; a non-matching request (mockito's 501, also non-2xx) would otherwise satisfy the error assertion vacuously.
_m.assert_async().await;
}

#[tokio::test]
Expand Down Expand Up @@ -270,4 +268,31 @@ mod tests {
let err = rt.block_on(run(args)).unwrap_err();
assert!(err.to_string().contains("no repo specified"));
}

#[tokio::test]
async fn resolve_via_run_surfaces_denial() {
// A slash-free repo with an empty identity dir forces the inline GET /
// node-info fetch during resolution. A gated 404 there must Err (surfacing
// the status), proving the read_json conversion is load-bearing.
let mut server = mockito::Server::new_async().await;
let dir = tempfile::TempDir::new().unwrap(); // empty, no identity.pem, forces the GET / branch
let _m = server
.mock("GET", "/")
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"denied"}"#)
.expect(1)
.create_async()
.await;
let err = run(ChangelogArgs {
repo: Some("noslash".to_string()),
limit: 20,
node: server.url(),
dir: Some(dir.path().to_path_buf()),
})
.await
.unwrap_err();
assert!(err.to_string().contains("404"), "got: {err}");
_m.assert_async().await;
}
}
Loading
Loading