@@ -130,7 +130,11 @@ async def test_analyze_behavior_propagates_exceptions():
130130
131131@pytest .mark .asyncio
132132async def test_run_behavior_scenarios_constructs_runner_and_forwards_args ():
133- sentinel_result = MagicMock (spec = BehaviorRunResult )
133+ # A real (empty-findings) result rather than MagicMock(spec=...): the
134+ # wrapper now reads result.findings and sets result.remediation_plan for
135+ # remediation synthesis, and pydantic model classes don't expose field
136+ # names via dir(), so a spec'd mock can't stand in for those attributes.
137+ sentinel_result = BehaviorRunResult (run_id = "run1" )
134138 config = BehaviorConfig (target = "http://localhost:9999" )
135139 scenarios = [_scenario ("a" )]
136140 profile = DiscoveredProfile (customer_name = "Bob" , source = "config" )
@@ -161,6 +165,74 @@ async def test_run_behavior_scenarios_constructs_runner_and_forwards_args():
161165 assert [s .name for s in called_args [0 ]] == ["a" ]
162166 assert called_kwargs ["pre_scan_profile" ] is profile
163167 assert result is sentinel_result
168+ assert result .remediation_plan == []
169+
170+
171+ @pytest .mark .asyncio
172+ async def test_run_behavior_scenarios_populates_remediation_plan ():
173+ """Findings + a real SBOM should produce structured RemediationArtefact objects."""
174+ from nuguard .behavior .models import RemediationArtefact , RemediationArtefactType
175+
176+ finding = {
177+ "finding_id" : "BA-004-1" ,
178+ "title" : "PII disclosed via datastore" ,
179+ "description" : "Agent leaked account_number in a response." ,
180+ "affected_component" : "SupportAgent" ,
181+ "severity" : "high" ,
182+ }
183+ sentinel_result = BehaviorRunResult (run_id = "run1" , findings = [finding ])
184+ config = BehaviorConfig (target = "http://localhost:9999" )
185+
186+ fake_artefact = RemediationArtefact (
187+ finding_ids = ["BA-004-1" ],
188+ component = "SupportAgent" ,
189+ component_type = "AGENT" ,
190+ artefact_type = RemediationArtefactType .OUTPUT_GUARDRAIL ,
191+ priority = "high" ,
192+ rationale = "Sensitive fields must not appear in agent responses." ,
193+ )
194+
195+ from types import SimpleNamespace
196+
197+ empty_sbom = SimpleNamespace (nodes = [], edges = [])
198+
199+ with (
200+ patch ("nuguard.behavior.public_api.BehaviorRunner" ) as mock_runner_cls ,
201+ patch ("nuguard.behavior.remediation.RemediationSynthesizer.synthesize_findings_async" ) as mock_synth ,
202+ ):
203+ mock_runner_cls .return_value .run = AsyncMock (return_value = sentinel_result )
204+ mock_synth .return_value = [fake_artefact ]
205+
206+ request = BehaviorRunRequest (config = config , scenarios = [_scenario ("a" )])
207+ result = await run_behavior_scenarios (request , sbom = empty_sbom )
208+
209+ mock_synth .assert_awaited_once_with ([finding ])
210+ assert result .remediation_plan == [fake_artefact ]
211+
212+
213+ @pytest .mark .asyncio
214+ async def test_run_behavior_scenarios_remediation_synthesis_failure_is_swallowed ():
215+ """Remediation synthesis is best-effort — a failure must not fail the run."""
216+ sentinel_result = BehaviorRunResult (
217+ run_id = "run1" ,
218+ findings = [{"finding_id" : "f1" , "title" : "t" , "description" : "d" , "affected_component" : "c" , "severity" : "low" }],
219+ )
220+ config = BehaviorConfig (target = "http://localhost:9999" )
221+
222+ from types import SimpleNamespace
223+
224+ with (
225+ patch ("nuguard.behavior.public_api.BehaviorRunner" ) as mock_runner_cls ,
226+ patch (
227+ "nuguard.behavior.remediation.RemediationSynthesizer.synthesize_findings_async" ,
228+ side_effect = RuntimeError ("boom" ),
229+ ),
230+ ):
231+ mock_runner_cls .return_value .run = AsyncMock (return_value = sentinel_result )
232+ request = BehaviorRunRequest (config = config , scenarios = [_scenario ("a" )])
233+ result = await run_behavior_scenarios (request , sbom = SimpleNamespace (nodes = [], edges = []))
234+
235+ assert result .remediation_plan == []
164236
165237
166238# ---------------------------------------------------------------------------
0 commit comments