aperion_shield/
context.rs1use std::path::{Path, PathBuf};
12
13use crate::engine::Policy;
14
15#[derive(Debug, Clone)]
16pub struct WorkspaceContext {
17 pub root: PathBuf,
18 pub is_prod: bool,
19 pub matched_signals: Vec<String>,
20}
21
22impl WorkspaceContext {
23 pub fn probe(policy: &Policy) -> Self {
25 let root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
26 Self::probe_at(policy, &root)
27 }
28
29 pub fn probe_at(policy: &Policy, root: &Path) -> Self {
33 let root = root.to_path_buf();
34 if !policy.workspace_probe.enabled {
35 return Self { root, is_prod: false, matched_signals: vec![] };
36 }
37 let mut matched = Vec::new();
38 for sig in &policy.workspace_probe.prod_signals {
39 if signal_present(&root, sig) {
40 matched.push(sig.clone());
41 }
42 }
43 let is_prod = !matched.is_empty();
44 Self { root, is_prod, matched_signals: matched }
45 }
46}
47
48fn signal_present(root: &Path, signal: &str) -> bool {
49 if let Some(dir) = signal.strip_suffix('/') {
50 let p = root.join(dir);
51 return p.is_dir();
52 }
53 if root.join(signal).exists() {
57 return true;
58 }
59 for sub in ["config", "deploy", "ops", "infra"] {
60 if root.join(sub).join(signal).exists() {
61 return true;
62 }
63 }
64 false
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use crate::engine::WorkspaceProbeCfg;
71 use std::fs;
72 use tempfile::TempDir;
73
74 fn policy_with_signals(signals: &[&str]) -> Policy {
75 let mut p = Policy::default();
76 p.workspace_probe = WorkspaceProbeCfg {
77 enabled: true,
78 prod_signals: signals.iter().map(|s| s.to_string()).collect(),
79 severity_bump: 1,
80 };
81 p
82 }
83
84 #[test]
85 fn no_signals_means_not_prod() {
86 let tmp = TempDir::new().unwrap();
87 let ctx = WorkspaceContext::probe_at(
88 &policy_with_signals(&[".env.production", "prod/"]),
89 tmp.path(),
90 );
91 assert!(!ctx.is_prod);
92 assert!(ctx.matched_signals.is_empty());
93 }
94
95 #[test]
96 fn file_signal_at_cwd_root() {
97 let tmp = TempDir::new().unwrap();
98 fs::write(tmp.path().join(".env.production"), "DB=prod").unwrap();
99 let ctx = WorkspaceContext::probe_at(
100 &policy_with_signals(&[".env.production"]),
101 tmp.path(),
102 );
103 assert!(ctx.is_prod);
104 assert_eq!(ctx.matched_signals, vec![".env.production".to_string()]);
105 }
106
107 #[test]
108 fn dir_signal() {
109 let tmp = TempDir::new().unwrap();
110 fs::create_dir(tmp.path().join("prod")).unwrap();
111 let ctx = WorkspaceContext::probe_at(&policy_with_signals(&["prod/"]), tmp.path());
112 assert!(ctx.is_prod);
113 }
114
115 #[test]
116 fn nested_config_dir() {
117 let tmp = TempDir::new().unwrap();
118 fs::create_dir(tmp.path().join("config")).unwrap();
119 fs::write(tmp.path().join("config").join("production.yml"), "x: 1").unwrap();
120 let ctx = WorkspaceContext::probe_at(&policy_with_signals(&["production.yml"]), tmp.path());
121 assert!(ctx.is_prod);
122 }
123
124 #[test]
125 fn disabled_probe_short_circuits() {
126 let tmp = TempDir::new().unwrap();
127 fs::write(tmp.path().join(".env.production"), "x").unwrap();
128 let mut p = policy_with_signals(&[".env.production"]);
129 p.workspace_probe.enabled = false;
130 let ctx = WorkspaceContext::probe_at(&p, tmp.path());
131 assert!(!ctx.is_prod);
132 }
133}