fix(policy): treat git branch listing as read-only operation

Remove 'branch' from requires_write_access() to resolve the
contradiction where branch listing was classified as both read-only
and write-requiring. Branch listing only enumerates local refs and
has no side effects, so it should remain available under ReadOnly
autonomy mode.

Add regression tests:
- branch_is_not_write_gated: verifies classification consistency
- allows_branch_listing_in_readonly_mode: verifies end-to-end
  execution under ReadOnly autonomy
- is_read_only_detection: now explicitly asserts branch is read-only

Resolves zeroclaw-labs/zeroclaw#612

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Alex Gorevski 2026-02-17 13:30:42 -08:00 committed by Chummy
parent b5e1c3a8f5
commit fbc26be7af

View file

@ -53,7 +53,7 @@ impl GitOperationsTool {
fn requires_write_access(&self, operation: &str) -> bool {
matches!(
operation,
"commit" | "add" | "checkout" | "branch" | "stash" | "reset" | "revert"
"commit" | "add" | "checkout" | "stash" | "reset" | "revert"
)
}
@ -666,6 +666,16 @@ mod tests {
assert!(!tool.requires_write_access("log"));
}
#[test]
fn branch_is_not_write_gated() {
let tmp = TempDir::new().unwrap();
let tool = test_tool(tmp.path());
// Branch listing is read-only; it must not require write access
assert!(!tool.requires_write_access("branch"));
assert!(tool.is_read_only("branch"));
}
#[test]
fn is_read_only_detection() {
let tmp = TempDir::new().unwrap();
@ -674,6 +684,7 @@ mod tests {
assert!(tool.is_read_only("status"));
assert!(tool.is_read_only("diff"));
assert!(tool.is_read_only("log"));
assert!(tool.is_read_only("branch"));
assert!(!tool.is_read_only("commit"));
assert!(!tool.is_read_only("add"));
@ -708,6 +719,34 @@ mod tests {
.contains("higher autonomy"));
}
#[tokio::test]
async fn allows_branch_listing_in_readonly_mode() {
let tmp = TempDir::new().unwrap();
// Initialize a git repository so the command can succeed
std::process::Command::new("git")
.args(["init"])
.current_dir(tmp.path())
.output()
.unwrap();
let security = Arc::new(SecurityPolicy {
autonomy: AutonomyLevel::ReadOnly,
..SecurityPolicy::default()
});
let tool = GitOperationsTool::new(security, tmp.path().to_path_buf());
let result = tool
.execute(json!({"operation": "branch"}))
.await
.unwrap();
// Branch listing must not be blocked by read-only autonomy
let error_msg = result.error.as_deref().unwrap_or("");
assert!(
!error_msg.contains("read-only") && !error_msg.contains("higher autonomy"),
"branch listing should not be blocked in read-only mode, got: {error_msg}"
);
}
#[tokio::test]
async fn allows_readonly_ops_in_readonly_mode() {
let tmp = TempDir::new().unwrap();