Layer C + D. 6 gate modules + 3 verify modules → 1 generic struct per
family + const declarations. Data-driven, not module-per-capability.
New:
- src/gates/pattern_gate.rs (192 LOC) — PatternGate struct with
GateMode::{DenyIfMatch, AllowIfMatch}, static regex compilation via
once_cell, bypass env support, Capability trait impl
- src/verifies/command_verify.rs (142 LOC) — CommandVerify struct with
WorkDir::{WorkspaceRoot, CrateDir, MainRepoSub}, subprocess exec +
exit-code check, extra_env support, Capability trait impl
Converted (const declarations, ~15-27 LOC each):
- gates/policy_no_git_ops.rs (49→22)
- gates/safety_no_dep_bump.rs (35→19)
- gates/scope_files_whitelist.rs (37→16)
- gates/scope_files_denylist.rs (35→16)
- gates/tools_bash_allowlist.rs (58→27)
- verifies/quality_cargo_check_green.rs (41→18)
- verifies/quality_tests_green.rs (75→80, folded common shape)
- verifies/safety_no_dep_bump.rs (39→47)
Kept separate (different shape, not PatternGate/CommandVerify):
- gates/tools_deny_tools.rs (tool-name match, not pattern)
- verifies/quality_constructor_pattern.rs (LOC walker)
- verifies/output/report_format.rs + severity_grade.rs (text parsers)
- verifies/scope_files_{whitelist,denylist}.rs (diff-walkers)
Registry.rs preserves alias table + deprecation warnings + all_names().
Tests: 57/57 green (was 41, +16: 10 pattern_gate_smoke + 6 command_verify_smoke).
LOC net: 5 gates 214→100 (-53%), shared PatternGate+CommandVerify 334
LOC absorb duplication. Amortization breaks even around 3-4 new gates
added later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
4.5 KiB
Rust
140 lines
4.5 KiB
Rust
//! Smoke tests for the generic `PatternGate` (Layer C convergence).
|
|
//!
|
|
//! Covers DenyIfMatch, AllowIfMatch, DenyIfUnmatched (scope), bypass env,
|
|
//! and non-applicable tool short-circuit.
|
|
|
|
use kei_agent_runtime::capability::{Capability, GateContext, GateDecision, TaskSpec};
|
|
use kei_agent_runtime::gates::pattern_gate::{GateMode, PatternGate, PatternSource};
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
|
|
fn ctx<'a>(
|
|
tool: &'a str,
|
|
input: &'a serde_json::Value,
|
|
task: &'a TaskSpec,
|
|
env: &'a HashMap<String, String>,
|
|
) -> GateContext<'a> {
|
|
GateContext { tool_name: tool, tool_input: input, task, env }
|
|
}
|
|
|
|
const TEST_GATE: PatternGate = PatternGate {
|
|
name: "test::deny-if-match",
|
|
tools: &["Bash"],
|
|
field: "command",
|
|
mode: GateMode::DenyIfMatch,
|
|
patterns: PatternSource::StaticRegex(&[r"\bforbidden\b"]),
|
|
bypass_env: Some("TEST_BYPASS"),
|
|
deny_template: "{name} — {cmd} matched {pat}",
|
|
};
|
|
|
|
#[test]
|
|
fn deny_if_match_blocks_forbidden() {
|
|
let task = TaskSpec::default();
|
|
let env = HashMap::new();
|
|
let input = json!({"command": "run forbidden action"});
|
|
match TEST_GATE.check(&ctx("Bash", &input, &task, &env)) {
|
|
GateDecision::Deny { .. } => {}
|
|
other => panic!("expected Deny, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn deny_if_match_allows_when_no_match() {
|
|
let task = TaskSpec::default();
|
|
let env = HashMap::new();
|
|
let input = json!({"command": "echo hello"});
|
|
assert_eq!(
|
|
TEST_GATE.check(&ctx("Bash", &input, &task, &env)),
|
|
GateDecision::Allow
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn allow_if_match_allows_on_match() {
|
|
// Mirrors tools::bash-allowlist — use the real registry path.
|
|
use kei_agent_runtime::registry;
|
|
let g = registry::get_gate("tools::bash-allowlist").unwrap();
|
|
let task = TaskSpec::default();
|
|
let env = HashMap::new();
|
|
let input = json!({"command": "cargo build"});
|
|
assert_eq!(g.check(&ctx("Bash", &input, &task, &env)), GateDecision::Allow);
|
|
}
|
|
|
|
#[test]
|
|
fn allow_if_match_denies_on_miss() {
|
|
use kei_agent_runtime::registry;
|
|
let g = registry::get_gate("tools::bash-allowlist").unwrap();
|
|
let task = TaskSpec::default();
|
|
let env = HashMap::new();
|
|
let input = json!({"command": "wget http://evil"});
|
|
matches!(g.check(&ctx("Bash", &input, &task, &env)), GateDecision::Deny { .. });
|
|
}
|
|
|
|
#[test]
|
|
fn deny_if_unmatched_scope_whitelist_empty_allows() {
|
|
// Empty whitelist = NotApplicable / Allow per spec.
|
|
use kei_agent_runtime::registry;
|
|
let g = registry::get_gate("scope::files-whitelist").unwrap();
|
|
let task = TaskSpec::default(); // empty whitelist
|
|
let env = HashMap::new();
|
|
let input = json!({"file_path": "anywhere/foo.rs"});
|
|
assert_eq!(g.check(&ctx("Edit", &input, &task, &env)), GateDecision::Allow);
|
|
}
|
|
|
|
#[test]
|
|
fn bypass_env_allows_even_on_match() {
|
|
let task = TaskSpec::default();
|
|
let mut env = HashMap::new();
|
|
env.insert("TEST_BYPASS".into(), "1".into());
|
|
let input = json!({"command": "run forbidden action"});
|
|
assert_eq!(
|
|
TEST_GATE.check(&ctx("Bash", &input, &task, &env)),
|
|
GateDecision::Allow
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn non_matching_tool_is_not_applicable() {
|
|
let task = TaskSpec::default();
|
|
let env = HashMap::new();
|
|
let input = json!({"command": "run forbidden action"});
|
|
assert_eq!(
|
|
TEST_GATE.check(&ctx("Read", &input, &task, &env)),
|
|
GateDecision::NotApplicable
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_field_is_not_applicable() {
|
|
let task = TaskSpec::default();
|
|
let env = HashMap::new();
|
|
let input = json!({"not_the_field": "forbidden"});
|
|
assert_eq!(
|
|
TEST_GATE.check(&ctx("Bash", &input, &task, &env)),
|
|
GateDecision::NotApplicable
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn safety_no_dep_bump_blocks_cargo_toml_path() {
|
|
use kei_agent_runtime::registry;
|
|
let g = registry::get_gate("safety::no-dep-bump").unwrap();
|
|
let task = TaskSpec::default();
|
|
let env = HashMap::new();
|
|
let input = json!({"file_path": "nested/Cargo.toml"});
|
|
match g.check(&ctx("Edit", &input, &task, &env)) {
|
|
GateDecision::Deny { .. } => {}
|
|
other => panic!("expected Deny, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn safety_no_dep_bump_allows_bypass_env() {
|
|
use kei_agent_runtime::registry;
|
|
let g = registry::get_gate("safety::no-dep-bump").unwrap();
|
|
let task = TaskSpec::default();
|
|
let mut env = HashMap::new();
|
|
env.insert("ALLOW_DEP_BUMP".into(), "1".into());
|
|
let input = json!({"file_path": "Cargo.toml"});
|
|
assert_eq!(g.check(&ctx("Edit", &input, &task, &env)), GateDecision::Allow);
|
|
}
|