1use crate::core::archive::authority::ArchiveAuthority;
10use crate::core::archive::bindings::{self, Item};
11use crate::core::context_admission::stored::SourceRef;
12
13use super::SessionState;
14
15impl SessionState {
16 pub fn add_finding_for_file(&mut self, file: Option<&str>, line: Option<u32>, summary: &str) {
22 self.add_finding(file, line, summary);
23 self.bind_last_finding(|project| bindings::sources_at(project, &[file?], "ctx_session"));
24 }
25
26 pub fn add_finding_from_call(
32 &mut self,
33 file: Option<&str>,
34 summary: &str,
35 call_sources: Option<&ArchiveAuthority>,
36 ) {
37 self.add_finding(file, None, summary);
38 self.bind_last_finding(|_| bindings::sources_of_call(call_sources?));
39 }
40
41 fn bind_last_finding(&self, sources: impl FnOnce(&str) -> Option<Vec<SourceRef>>) {
43 let (Some(project), Some(finding)) = (self.project_root.as_deref(), self.findings.last())
44 else {
45 return;
46 };
47 let Some(sources) = sources(project) else {
48 return;
49 };
50 bindings::bind(
51 &Item::SessionFinding {
52 session: &self.id,
53 file: finding.file.as_deref(),
54 },
55 &finding.summary,
56 project,
57 sources,
58 );
59 }
60
61 pub fn finding_served(&self, finding: &super::types::Finding) -> bool {
69 bindings::serve(
70 &Item::SessionFinding {
71 session: &self.id,
72 file: finding.file.as_deref(),
73 },
74 &finding.summary,
75 self.project_root.as_deref(),
76 )
77 .is_ok()
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::core::archive::delivered::LineageRefusal;
85 use crate::core::context_admission::stored::lineage_support::{
86 deliver, governed, project, reusable,
87 };
88 use crate::core::policy::runtime::TestPolicyOverride;
89
90 fn delivered(root: &str, session: &SessionState) -> (String, usize) {
92 let finding = session.findings.last().expect("a finding").clone();
93 let id = session.id.clone();
94 deliver(root, || {
95 let item = Item::SessionFinding {
96 session: &id,
97 file: finding.file.as_deref(),
98 };
99 match bindings::serve(&item, &finding.summary, Some(root)) {
100 Ok(()) => format!("finding: {}", finding.summary),
101 Err(withheld) => withheld.notice(),
102 }
103 })
104 }
105
106 #[test]
110 fn a_finding_is_bound_to_its_file_only_when_the_file_is_admitted() {
111 let _data = governed();
112 let _community = TestPolicyOverride::set(None);
113 let (_directory, root) = project();
114 std::fs::write(format!("{root}/pool.rs"), "pub fn lease() {}\n").expect("source");
115 std::fs::write(format!("{root}/plan.md"), "*** TOP SECRET ***\nlaunch\n").expect("marked");
116 let mut session = SessionState::new();
117 session.project_root = Some(root.clone());
118
119 session.add_finding_for_file(Some("pool.rs"), Some(1), "the pool leaks one lease");
120 let (reply, recorded) = delivered(&root, &session);
121 assert!(reply.contains("leaks one lease"), "{reply}");
122 assert_eq!(recorded, 1);
123 assert_eq!(reusable(&root, &reply), Ok(()));
124
125 for (file, summary) in [
126 (None, "noted without a file"),
127 (Some("missing.rs"), "noted for a missing file"),
128 (Some("plan.md"), "noted for a restricted file"),
129 ] {
130 session.add_finding_for_file(file, None, summary);
131 assert_eq!(
132 session
133 .findings
134 .last()
135 .map(|finding| finding.summary.as_str()),
136 Some(summary),
137 "the finding is recorded either way"
138 );
139 let (unbound, recorded) = delivered(&root, &session);
140 assert_eq!(recorded, 0, "{summary}");
141 assert_eq!(reusable(&root, &unbound), Err(LineageRefusal::Unknown));
142 }
143
144 std::fs::remove_file(format!("{root}/pool.rs")).expect("delete");
145 assert_eq!(
146 reusable(&root, &reply),
147 Err(LineageRefusal::SourceNoLongerAdmitted)
148 );
149 }
150
151 #[test]
155 fn a_call_finding_takes_the_sources_of_its_call() {
156 let _data = governed();
157 let _community = TestPolicyOverride::set(None);
158 let (_directory, root) = project();
159 let path = format!("{root}/build.rs");
160 std::fs::write(&path, "fn main() { panic!(\"linker\") }\n").expect("source");
161 let admitted =
162 crate::core::rooted_read::read_file_for_tool_rooted(&path, &root, "ctx_read")
163 .expect("admitted read");
164 let capture =
165 ArchiveAuthority::file(std::path::Path::new(&root), &path, "ctx_read", &admitted)
166 .expect("protection is active: the read is bound");
167 let mut session = SessionState::new();
168 session.project_root = Some(root.clone());
169
170 session.add_finding_from_call(Some("build.rs"), "build.rs panics", Some(&capture));
171 let (reply, recorded) = delivered(&root, &session);
172 assert_eq!(recorded, 1);
173 assert_eq!(reusable(&root, &reply), Ok(()));
174
175 session.add_finding_from_call(Some("build.rs"), "from an undeclared tool", None);
176 let (_, recorded) = delivered(&root, &session);
177 assert_eq!(recorded, 0);
178
179 let mut old: SessionState =
180 serde_json::from_str(&serde_json::to_string(&session).expect("serialize"))
181 .expect("a session state without bindings loads");
182 old.id = "a-session-written-before-bindings".to_owned();
183 assert_eq!(old.findings.len(), 2);
184 let (_, recorded) = delivered(&root, &old);
185 assert_eq!(recorded, 0, "no register entry: originless");
186 }
187
188 #[test]
194 fn the_renderers_deliver_findings_under_their_current_sources() {
195 let _data = governed();
196 let _community = TestPolicyOverride::set(None);
197 let (_directory, root) = project();
198 std::fs::write(format!("{root}/pool.rs"), "pub fn lease() {}\n").expect("source");
199 let mut session = SessionState::new();
200 session.project_root = Some(root.clone());
201 session.add_finding_for_file(Some("pool.rs"), Some(1), "the pool leaks one lease");
202 let summary =
203 |session: &SessionState| deliver(&root, || session.format_compact_delivered());
204
205 let (reply, recorded) = summary(&session);
206 assert!(reply.contains("the pool leaks one lease"), "{reply}");
207 assert_eq!(recorded, 1);
208 assert_eq!(reusable(&root, &reply), Ok(()));
209
210 let mut mixed = session.clone();
211 mixed.add_finding_for_file(None, None, "noted without a file");
212 let (unknown, recorded) = summary(&mixed);
213 assert!(unknown.contains("noted without a file"), "{unknown}");
214 assert_eq!(recorded, 0, "one finding without a source: unknown");
215 assert_eq!(reusable(&root, &unknown), Err(LineageRefusal::Unknown));
216
217 let mut decided = session.clone();
218 decided.add_decision("lease from the pool only", None);
219 assert_eq!(summary(&decided).1, 0, "a decision has no source: unknown");
220
221 std::fs::write(
222 format!("{root}/pool.rs"),
223 "*** TOP SECRET ***\npub fn lease() {}\n",
224 )
225 .expect("mark");
226 assert_eq!(
227 reusable(&root, &reply),
228 Err(LineageRefusal::SourceNoLongerAdmitted)
229 );
230 for rendered in [
231 session.format_compact(),
232 session.build_compaction_snapshot(),
233 session.build_resume_block(),
234 ] {
235 assert!(!rendered.contains("the pool leaks one lease"), "{rendered}");
236 }
237 assert!(mixed.format_compact().contains("noted without a file"));
239
240 std::fs::remove_file(format!("{root}/pool.rs")).expect("delete");
241 assert_eq!(
242 reusable(&root, &reply),
243 Err(LineageRefusal::SourceNoLongerAdmitted)
244 );
245 let (withheld, recorded) = summary(&session);
246 assert!(!withheld.contains("the pool leaks one lease"), "{withheld}");
247 assert_eq!(recorded, 0);
248 }
249}