refactor(acl): shared command census module for build.rs and tests

Moves the generate_handler! parser out of the lib.rs test into
src/command_census.rs and adds the capability rules (one allow-* grant per
command, in the file its name says, no deny-*) with unit tests. No
behaviour change yet: build.rs does not use it until the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 22:15:25 -07:00
co-authored by Claude Opus 5.5
parent 7c8ad62da4
commit ed6596436e
2 changed files with 398 additions and 38 deletions
+11 -38
View File
@@ -1,5 +1,7 @@
mod auth_bridge;
mod browser_view;
#[cfg(test)]
mod command_census;
mod commands;
mod docker;
pub mod file_viewer;
@@ -844,30 +846,11 @@ mod tests {
&mut defined,
);
// The registration list, read from this file rather than from a macro
// expansion so the test does not depend on `generate_handler!`'s shape.
let this = include_str!("lib.rs");
let handler = this
.split_once("generate_handler![")
.and_then(|(_, rest)| rest.split_once("])"))
.map(|(inside, _)| inside)
// The registration list, read from this file by the same parser `build.rs` uses to
// declare the AppManifest — so if this test can see a command, the ACL can too.
let ordered = crate::command_census::registered_commands(include_str!("lib.rs"))
.expect("lib.rs should contain a generate_handler! list");
// Line-based, not `split(',')`: the list is grouped under `// Docker`
// style comments, and splitting on commas glues each comment to the
// command that follows it. A `starts_with("//")` filter then drops that
// command — silently, and once per group.
let registered: BTreeSet<String> = handler
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with("//"))
.filter_map(|l| {
l.trim_end_matches(',')
.rsplit("::")
.next()
.map(|n| n.trim().to_string())
})
.filter(|n| !n.is_empty())
.collect();
let registered: BTreeSet<String> = ordered.iter().cloned().collect();
assert!(
!defined.is_empty() && !registered.is_empty(),
@@ -896,21 +879,11 @@ mod tests {
// passed here.
let mut seen: Vec<&str> = Vec::new();
let mut duplicated: Vec<&str> = Vec::new();
for line in handler
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with("//"))
{
if let Some(name) = line.trim_end_matches(',').rsplit("::").next() {
let name = name.trim();
if name.is_empty() {
continue;
}
if seen.contains(&name) {
duplicated.push(name);
} else {
seen.push(name);
}
for name in &ordered {
if seen.contains(&name.as_str()) {
duplicated.push(name);
} else {
seen.push(name);
}
}
assert!(