mirror of
https://github.com/Twigpine/node.git
synced 2026-11-08 07:38:18 -10:00
docs(gl): describe the client-contract rules in plain English
The comments carried internal shorthand identifiers that resolve to nothing for a reader outside this repo. Replace each with the property it names: a denial must surface as an error rather than render as an empty or fabricated result, and an error body must be read under a cap and sanitized before it reaches the terminal. Comment-only; no behavior change. Verified after the edit because the completeness gate reads source text to decide which handlers it scans: reverting a converted handler to a bare .json().await still turns it red. gl: 412 unit tests green, fmt and clippy clean, cargo check --workspace --locked clean.
This commit is contained in:
1 parent
d48ae1ea43
commit
21ba974c54
9 files changed
+23
-22
No files matched your search
@@ -576,7 +576,7 @@ mod tests {
|
||||
#[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).
|
||||
// a 404 must Err, not silently print nothing.
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let _m = server
|
||||
.mock(
|
||||
|
||||
@@ -206,7 +206,7 @@ async fn obtain_proof(cfg: IcaptchaCfg) -> Result<String> {
|
||||
|
||||
/// Read at most `cap` bytes of a response body. Bounds the allocation from a
|
||||
/// hostile or broken node returning a huge error body — the display is capped
|
||||
/// separately, but the read itself must not be unbounded (INV-6, read half).
|
||||
/// separately, but the read itself must not be unbounded either.
|
||||
pub(crate) async fn read_body_capped(mut resp: reqwest::Response, cap: usize) -> String {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
while buf.len() < cap {
|
||||
@@ -227,7 +227,7 @@ pub(crate) async fn read_body_capped(mut resp: reqwest::Response, cap: usize) ->
|
||||
/// Strip terminal-dangerous characters from (and cap the length of) a
|
||||
/// node-supplied error string before surfacing it. The node a caller talks to
|
||||
/// could be hostile and embed escape sequences in its error body; those must not
|
||||
/// reach the terminal verbatim (INV-6). We drop the C0/C1 control bytes (which
|
||||
/// reach the terminal verbatim. We drop the C0/C1 control bytes (which
|
||||
/// defangs ANSI/OSC escapes) AND the Unicode bidi/format controls (which
|
||||
/// `char::is_control` does not cover — they can reorder the displayed line).
|
||||
pub(crate) fn sanitize_node_msg(s: &str) -> String {
|
||||
@@ -239,13 +239,13 @@ pub(crate) fn sanitize_node_msg(s: &str) -> String {
|
||||
|
||||
/// Read a JSON response, surfacing a node denial/error instead of parsing it as
|
||||
/// the requested resource. On a non-2xx status it returns an `Err` carrying the
|
||||
/// node's sanitized `message` (INV-6) plus the status; on success it parses the
|
||||
/// node's sanitized `message` plus the status; on success it parses the
|
||||
/// body and propagates a parse error, so a truncated/garbage 2xx body is an error
|
||||
/// rather than a silently-empty success (the denial-as-success bug #123 fixes).
|
||||
/// `what` names the resource for the error text (e.g. "repo", "commits").
|
||||
///
|
||||
/// Callers must route gated reads through this rather than `resp.json().await?`:
|
||||
/// the bare parse renders a gated 404/5xx body back as the resource (INV-8).
|
||||
/// the bare parse renders a gated 404/5xx body back as the resource.
|
||||
/// Cap on the error body `read_json` reads before parsing a node's `message`.
|
||||
/// Matches the capped-read bound used on the sync error path; large enough for any
|
||||
/// legitimate error, small enough that a hostile node cannot exhaust memory.
|
||||
@@ -707,7 +707,7 @@ mod tests {
|
||||
assert_eq!(out, "ok \u{0627}\u{200D}b");
|
||||
}
|
||||
|
||||
// ── read_json (status-checked read; #123 / INV-8 / INV-6) ────────────
|
||||
// ── read_json (status-checked, bounded, sanitized read; #123) ────────
|
||||
|
||||
/// Drive a real `reqwest::Response` off a mockito mock so `read_json` sees an
|
||||
/// actual HTTP status + body, the same shape the gated read arms produce.
|
||||
@@ -775,7 +775,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_json_sanitizes_control_and_bidi_in_message() {
|
||||
// INV-6: a hostile node embeds ESC, BEL, and a right-to-left override in
|
||||
// A hostile node embeds ESC, BEL, and a right-to-left override in
|
||||
// the error message; none may reach the terminal verbatim.
|
||||
let mut server = Server::new_async().await;
|
||||
let resp = response_for(
|
||||
|
||||
@@ -2139,7 +2139,7 @@ mod tests {
|
||||
assert_eq!(count, 40, "expected 40 tools, got {count}");
|
||||
}
|
||||
|
||||
// ── Gated read arms surface node denials, not fabricated results (#123 / INV-8) ──
|
||||
// ── Gated read arms surface node denials, not fabricated results (#123) ──
|
||||
|
||||
#[tokio::test]
|
||||
async fn repo_get_surfaces_denial_not_fabricated_repo() {
|
||||
@@ -2359,7 +2359,7 @@ mod tests {
|
||||
_m.assert_async().await;
|
||||
}
|
||||
|
||||
// ── INV-8 across the MCP twin: every tool that renders a node response must
|
||||
// ── Across the MCP twin: every tool that renders a node response must
|
||||
// surface a denial as an Err, not print the 4xx/5xx body as a success. ──
|
||||
|
||||
/// Drive one MCP tool against a node returning a 404 and assert it Errs
|
||||
|
||||
+1
-1
@@ -853,7 +853,7 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// ── Gated PR reads surface node denials, not empty/stub renders (#123 / INV-8) ──
|
||||
// ── Gated PR reads surface node denials, not empty/stub renders (#123) ──
|
||||
|
||||
#[tokio::test]
|
||||
async fn cmd_list_surfaces_denial_not_empty() {
|
||||
|
||||
@@ -344,7 +344,7 @@ async fn cmd_info(repo: String, node: String, dir: Option<PathBuf>) -> Result<()
|
||||
anyhow::bail!("repository '{owner}/{name}' not found");
|
||||
}
|
||||
// Every other status routes through read_json: a 2xx yields the parsed body,
|
||||
// any other non-2xx yields the node's capped + sanitized message (INV-6), and
|
||||
// any other non-2xx yields the node's capped + sanitized message, and
|
||||
// the error read is bounded rather than buffering the whole hostile body.
|
||||
let r = crate::http::read_json(resp, "repo info").await?;
|
||||
|
||||
@@ -893,7 +893,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn cmd_list_repos_surfaces_denial() {
|
||||
// A node error must Err with the status, not trip the vague "expected
|
||||
// array" fallback that hides the node's actual response (INV-8).
|
||||
// array" fallback that hides the node's actual response.
|
||||
let dir = TempDir::new().unwrap();
|
||||
write_identity(&dir);
|
||||
|
||||
@@ -1423,7 +1423,7 @@ mod tests {
|
||||
_repo.assert_async().await;
|
||||
}
|
||||
|
||||
// ── Gated CLI reads surface node denials, not empty renders (#123 / INV-8) ──
|
||||
// ── Gated CLI reads surface node denials, not empty renders (#123) ──
|
||||
|
||||
#[tokio::test]
|
||||
async fn cmd_commits_surfaces_denial_not_empty() {
|
||||
@@ -1717,7 +1717,7 @@ mod vet_merge_deny_arms {
|
||||
}
|
||||
|
||||
// An authenticated caller DENIED with a 403 must surface an error, never
|
||||
// render a repo card (deny-as-success is the INV-8 client-contract break).
|
||||
// render a repo card (deny-as-success is the client-contract break).
|
||||
#[tokio::test]
|
||||
async fn cmd_info_403_denied_surfaces_error_not_card() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
@@ -133,7 +133,7 @@ pub async fn run(args: StatusArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
/// The status line for a repo section (PRs / issues) whose gated read returned a
|
||||
/// non-2xx: the denial must surface, never render as "no open ..." (INV-8).
|
||||
/// non-2xx: the denial must surface, never render as "no open ...".
|
||||
/// Returns `None` for a success (the caller renders the body) or a transport error
|
||||
/// (the section degrades silently — R5 — so the multi-section `gl status` never
|
||||
/// hard-fails on one denied read).
|
||||
@@ -149,7 +149,7 @@ fn section_unavailable_line(label: &str, resp: &Result<reqwest::Response>) -> Op
|
||||
/// The `trust` status line for the caller's own identity. A genuine 404 means the
|
||||
/// identity is not registered; any OTHER non-2xx (403/429/5xx) is a failed lookup
|
||||
/// and must surface as unavailable rather than fabricating an unregistered state
|
||||
/// (INV-8). A transport error degrades silently — the node-unreachable line below
|
||||
/// A transport error degrades silently — the node-unreachable line below
|
||||
/// already surfaces it — matching `section_unavailable_line`.
|
||||
async fn trust_line(resp: Result<reqwest::Response>) -> Option<String> {
|
||||
match resp {
|
||||
@@ -407,7 +407,7 @@ mod tests {
|
||||
assert_eq!(trust_bar(0.25), "█░░░");
|
||||
}
|
||||
|
||||
// ── gl status section denial surfacing (#123 / INV-8, R5) ────────────
|
||||
// ── gl status section denial surfacing (#123, R5) ────────────────────
|
||||
|
||||
async fn get_response(
|
||||
server: &mut mockito::Server,
|
||||
@@ -426,7 +426,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn gated_section_surfaces_unavailable_not_empty() {
|
||||
// A gated 404 on a status section must surface "unavailable", never be
|
||||
// treated as "no open PRs" (INV-8).
|
||||
// treated as "no open PRs".
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let resp = get_response(&mut server, 404).await;
|
||||
assert_eq!(
|
||||
@@ -452,7 +452,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// ── trust_line: a 404 means unregistered; any OTHER failure must surface as
|
||||
// unavailable, never fabricate an unregistered verdict (INV-8). ──────────
|
||||
// unavailable, never fabricate an unregistered verdict. ───────────────
|
||||
async fn agents_resp(
|
||||
server: &mut mockito::Server,
|
||||
status: usize,
|
||||
|
||||
@@ -45,7 +45,7 @@ pub async fn run(args: SyncArgs) -> Result<()> {
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
// Bound the read: a hostile or broken node must not force an
|
||||
// unbounded allocation just to surface a denial (INV-6, read half).
|
||||
// unbounded allocation just to surface a denial.
|
||||
let raw = read_body_capped(resp, 8 * 1024).await;
|
||||
let msg = serde_json::from_str::<serde_json::Value>(&raw)
|
||||
.ok()
|
||||
|
||||
@@ -613,7 +613,7 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// ── denial surfaces (INV-8): a node 4xx/5xx must Err, not render as success ──
|
||||
// ── denial surfaces: a node 4xx/5xx must Err, not render as success ──
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_tasks_surfaces_denial() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! INV-21 completeness gate for #186.
|
||||
//! Completeness gate for #186, itself proven load-bearing by reverting a
|
||||
//! converted handler and watching this go red.
|
||||
//!
|
||||
//! The client handlers converted in #186 must read node responses through
|
||||
//! `crate::http::read_json` — status-first, capped error read, sanitized message.
|
||||
|
||||
Reference in new issue
Block a user