gaard-api 0.2.17__tar.gz → 0.2.18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gaard_api-0.2.17/src/gaard_api.egg-info → gaard_api-0.2.18}/PKG-INFO +1 -1
- {gaard_api-0.2.17 → gaard_api-0.2.18}/pyproject.toml +1 -1
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/services.py +12 -3
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/analysis_findings.py +40 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api/v1/analysis.py +33 -11
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/main.py +1 -1
- {gaard_api-0.2.17 → gaard_api-0.2.18/src/gaard_api.egg-info}/PKG-INFO +1 -1
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_analysis_api.py +140 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/MANIFEST.in +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/README.md +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/setup.cfg +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/__init__.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/__init__.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/database.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/database_initial.sql +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/database_legacy_updates.sql +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/database_updates.sql +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/defaults.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/metadata.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/migration_runner.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/models.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/prompt_runtime.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin/security.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin-web/assets/getgaard.svg +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin-web/assets/identity.js +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin-web/assets/main.js +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin-web/assets/styles.css +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack-all.js +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack.min.css +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin-web/index.html +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/admin-web/package.json +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api/__init__.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api/v1/__init__.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api/v1/admin.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api/v1/dashboards.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api/v1/prompts.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api/v1/query.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api/v1/schema.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/api_registry.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/auth_dependencies.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/auth_hooks.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/cli.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/cli_commands.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/conversations.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/core/__init__.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/core/error_handlers.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/core/schema_cache.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/core/settings.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/example_data/__init__.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/example_database.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/extension_services.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/extensions.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/license.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/package_updates.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/py.typed +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/query_hooks.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/server_cli.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/siem.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api/tls_http.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api.egg-info/SOURCES.txt +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api.egg-info/dependency_links.txt +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api.egg-info/entry_points.txt +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api.egg-info/requires.txt +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/src/gaard_api.egg-info/top_level.txt +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_admin_api.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_api_cli.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_api_registry.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_conversation_api.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_database_migrations.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_error_handlers.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_license_api.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_license_service.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_prompt_runtime.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_query_error_messages.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_server_cli.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_settings.py +0 -0
- {gaard_api-0.2.17 → gaard_api-0.2.18}/tests/test_tls_http.py +0 -0
|
@@ -1839,10 +1839,19 @@ def upsert_analysis_business_logic_suggestion(
|
|
|
1839
1839
|
session.flush()
|
|
1840
1840
|
return existing
|
|
1841
1841
|
|
|
1842
|
+
should_enable = auto_enable or existing.enabled
|
|
1842
1843
|
existing.source_audit_id = source_audit_id
|
|
1843
|
-
existing.status =
|
|
1844
|
-
|
|
1845
|
-
|
|
1844
|
+
existing.status = (
|
|
1845
|
+
BUSINESS_LOGIC_STATUS_ACTIVE
|
|
1846
|
+
if should_enable
|
|
1847
|
+
else BUSINESS_LOGIC_STATUS_PENDING
|
|
1848
|
+
)
|
|
1849
|
+
existing.safety = (
|
|
1850
|
+
BUSINESS_LOGIC_SAFETY_SAFE
|
|
1851
|
+
if should_enable
|
|
1852
|
+
else BUSINESS_LOGIC_SAFETY_REVIEW
|
|
1853
|
+
)
|
|
1854
|
+
existing.enabled = should_enable
|
|
1846
1855
|
existing.error_category = normalized_type
|
|
1847
1856
|
existing.title = normalized_title
|
|
1848
1857
|
existing.rule_text = normalized_rule
|
|
@@ -186,6 +186,46 @@ def create_analysis_finding(
|
|
|
186
186
|
return finding
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
def get_analysis_finding_for_business_logic_suggestion(
|
|
190
|
+
session: Session,
|
|
191
|
+
*,
|
|
192
|
+
investigation_id: str,
|
|
193
|
+
owner_user_id: str,
|
|
194
|
+
business_logic_suggestion_id: int,
|
|
195
|
+
) -> AnalysisFinding | None:
|
|
196
|
+
return session.scalar(
|
|
197
|
+
select(AnalysisFinding)
|
|
198
|
+
.where(
|
|
199
|
+
AnalysisFinding.investigation_id == investigation_id,
|
|
200
|
+
AnalysisFinding.owner_user_id == owner_user_id,
|
|
201
|
+
AnalysisFinding.business_logic_suggestion_id == business_logic_suggestion_id,
|
|
202
|
+
)
|
|
203
|
+
.order_by(AnalysisFinding.id.asc())
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def refresh_analysis_finding_observation(
|
|
208
|
+
finding: AnalysisFinding,
|
|
209
|
+
*,
|
|
210
|
+
statement: str,
|
|
211
|
+
finding_type: str,
|
|
212
|
+
confidence: float,
|
|
213
|
+
critique: str,
|
|
214
|
+
scope: dict[str, Any],
|
|
215
|
+
evidence_refs: list[str],
|
|
216
|
+
) -> AnalysisFinding:
|
|
217
|
+
finding.statement = remove_thinking_blocks(statement).strip()[:4_000]
|
|
218
|
+
finding.finding_type = (finding_type.strip() or "finding")[:100]
|
|
219
|
+
finding.confidence = max(0.0, min(1.0, float(confidence)))
|
|
220
|
+
finding.critique = remove_thinking_blocks(critique).strip()[:4_000]
|
|
221
|
+
finding.scope_json = json_dumps(scope)
|
|
222
|
+
finding.evidence_refs_json = json_dumps(
|
|
223
|
+
unique_texts(text_list(finding.evidence_refs_json) + evidence_refs)
|
|
224
|
+
)
|
|
225
|
+
finding.contract_version = FINDING_CONTRACT_VERSION
|
|
226
|
+
return finding
|
|
227
|
+
|
|
228
|
+
|
|
189
229
|
def get_owned_analysis_finding(
|
|
190
230
|
session: Session,
|
|
191
231
|
*,
|
|
@@ -45,10 +45,12 @@ from gaard_api.analysis_findings import (
|
|
|
45
45
|
create_analysis_finding,
|
|
46
46
|
create_radar_finding_decision,
|
|
47
47
|
format_working_knowledge,
|
|
48
|
+
get_analysis_finding_for_business_logic_suggestion,
|
|
48
49
|
get_owned_analysis_finding,
|
|
49
50
|
list_active_analysis_findings,
|
|
50
51
|
list_owned_analysis_findings,
|
|
51
52
|
record_finding_usage,
|
|
53
|
+
refresh_analysis_finding_observation,
|
|
52
54
|
serialize_analysis_finding,
|
|
53
55
|
serialize_finding_decision,
|
|
54
56
|
serialize_working_knowledge_item,
|
|
@@ -1221,22 +1223,42 @@ def save_business_logic_finding(
|
|
|
1221
1223
|
auto_enable=runtime_config.analysis_auto_enable_business_logic,
|
|
1222
1224
|
actor=f"analysis:{session_id}",
|
|
1223
1225
|
)
|
|
1224
|
-
|
|
1226
|
+
critique = first_non_empty(
|
|
1227
|
+
finding.critique,
|
|
1228
|
+
"Confirmed only by evidence from this investigation and datasource.",
|
|
1229
|
+
)
|
|
1230
|
+
scope = finding_scope(finding, connector.connector_key, context)
|
|
1231
|
+
evidence_refs = finding_evidence_refs(finding, context)
|
|
1232
|
+
finding_record = get_analysis_finding_for_business_logic_suggestion(
|
|
1225
1233
|
session,
|
|
1226
1234
|
investigation_id=session_id,
|
|
1227
1235
|
owner_user_id=record.user_id,
|
|
1228
|
-
connector_id=connector.id,
|
|
1229
1236
|
business_logic_suggestion_id=suggestion.id,
|
|
1230
|
-
statement=statement,
|
|
1231
|
-
finding_type=finding.knowledge_type,
|
|
1232
|
-
confidence=finding.confidence,
|
|
1233
|
-
critique=first_non_empty(
|
|
1234
|
-
finding.critique,
|
|
1235
|
-
"Confirmed only by evidence from this investigation and datasource.",
|
|
1236
|
-
),
|
|
1237
|
-
scope=finding_scope(finding, connector.connector_key, context),
|
|
1238
|
-
evidence_refs=finding_evidence_refs(finding, context),
|
|
1239
1237
|
)
|
|
1238
|
+
if finding_record is None:
|
|
1239
|
+
finding_record = create_analysis_finding(
|
|
1240
|
+
session,
|
|
1241
|
+
investigation_id=session_id,
|
|
1242
|
+
owner_user_id=record.user_id,
|
|
1243
|
+
connector_id=connector.id,
|
|
1244
|
+
business_logic_suggestion_id=suggestion.id,
|
|
1245
|
+
statement=statement,
|
|
1246
|
+
finding_type=finding.knowledge_type,
|
|
1247
|
+
confidence=finding.confidence,
|
|
1248
|
+
critique=critique,
|
|
1249
|
+
scope=scope,
|
|
1250
|
+
evidence_refs=evidence_refs,
|
|
1251
|
+
)
|
|
1252
|
+
else:
|
|
1253
|
+
refresh_analysis_finding_observation(
|
|
1254
|
+
finding_record,
|
|
1255
|
+
statement=statement,
|
|
1256
|
+
finding_type=finding.knowledge_type,
|
|
1257
|
+
confidence=finding.confidence,
|
|
1258
|
+
critique=critique,
|
|
1259
|
+
scope=scope,
|
|
1260
|
+
evidence_refs=evidence_refs,
|
|
1261
|
+
)
|
|
1240
1262
|
session.commit()
|
|
1241
1263
|
finding_payload = serialize_analysis_finding(finding_record)
|
|
1242
1264
|
return {
|
|
@@ -40,7 +40,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
|
40
40
|
|
|
41
41
|
app = FastAPI(
|
|
42
42
|
title="GAARD API",
|
|
43
|
-
version="0.2.
|
|
43
|
+
version="0.2.18",
|
|
44
44
|
description="Self-hosted AI SQL Gateway for governed natural-language access to relational data.",
|
|
45
45
|
lifespan=lifespan,
|
|
46
46
|
)
|
|
@@ -376,6 +376,146 @@ def test_analysis_database_step_can_record_business_logic_suggestion(
|
|
|
376
376
|
assert suggestions[0].error_category == "analysis.dictionary_value"
|
|
377
377
|
|
|
378
378
|
|
|
379
|
+
def test_repeated_analysis_finding_preserves_review_decisions(
|
|
380
|
+
analysis_client: TestClient,
|
|
381
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
382
|
+
) -> None:
|
|
383
|
+
headers = auth_headers(analysis_client)
|
|
384
|
+
connector = create_active_default_datasource()
|
|
385
|
+
|
|
386
|
+
def repeated_finding() -> analysis_module.AnalysisBusinessLogicFinding:
|
|
387
|
+
return analysis_module.AnalysisBusinessLogicFinding(
|
|
388
|
+
create_suggestion=True,
|
|
389
|
+
knowledge_type="semantic_mapping",
|
|
390
|
+
title="Cardiology dictionary mapping",
|
|
391
|
+
rule_text="The dictionary value for cardiology is cardiology.",
|
|
392
|
+
statement="The dictionary value for cardiology is cardiology.",
|
|
393
|
+
confidence=0.94,
|
|
394
|
+
critique="The mapping is confirmed only in the current datasource.",
|
|
395
|
+
scope={"entity": "specialization", "field": "specialization_name"},
|
|
396
|
+
evidence_refs=["query:dictionary-check"],
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
class RepeatedFindingPlanner:
|
|
400
|
+
def __init__(self) -> None:
|
|
401
|
+
self.calls = 0
|
|
402
|
+
self.working_knowledge_seen: list[list[dict[str, Any]]] = []
|
|
403
|
+
|
|
404
|
+
def decide(
|
|
405
|
+
self, request: Any, datasource_context: Any, context: dict[str, Any]
|
|
406
|
+
) -> analysis_module.AnalysisPlannerDecision:
|
|
407
|
+
self.calls += 1
|
|
408
|
+
self.working_knowledge_seen.append(list(context.get("working_knowledge") or []))
|
|
409
|
+
return analysis_module.AnalysisPlannerDecision(
|
|
410
|
+
action=analysis_module.AnalysisAction.ASK_USER,
|
|
411
|
+
visible_question=f"Review mapping attempt {self.calls}.",
|
|
412
|
+
visible_reasoning="The same semantic mapping was observed again.",
|
|
413
|
+
user_question="Continue after review.",
|
|
414
|
+
business_logic=repeated_finding(),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
planner = RepeatedFindingPlanner()
|
|
418
|
+
monkeypatch.setattr(analysis_module, "create_analysis_planner", lambda: planner)
|
|
419
|
+
|
|
420
|
+
response = analysis_client.post(
|
|
421
|
+
"/api/v1/analysis/stream",
|
|
422
|
+
headers=headers,
|
|
423
|
+
json={"question": "Find the cardiology dictionary value.", "user_id": "spoofed"},
|
|
424
|
+
)
|
|
425
|
+
assert response.status_code == 200
|
|
426
|
+
events = parse_ndjson(response.text)
|
|
427
|
+
session_id = events[0]["session_id"]
|
|
428
|
+
first_suggestion = next(
|
|
429
|
+
item["business_logic_suggestion"]
|
|
430
|
+
for item in events
|
|
431
|
+
if item["event"] == "business_logic_suggestion"
|
|
432
|
+
)
|
|
433
|
+
finding_id = first_suggestion["finding_id"]
|
|
434
|
+
|
|
435
|
+
radar_decision_payload = {
|
|
436
|
+
"finding_id": finding_id,
|
|
437
|
+
"decision": "accept_for_investigation",
|
|
438
|
+
"confidence": 0.93,
|
|
439
|
+
"verdict": "The observed values explain the terminology lookup.",
|
|
440
|
+
"scope": {
|
|
441
|
+
"investigation_id": session_id,
|
|
442
|
+
"radar_run_id": "radar-repeat-1",
|
|
443
|
+
},
|
|
444
|
+
"evidence_refs": ["query:dictionary-check"],
|
|
445
|
+
}
|
|
446
|
+
accepted_response = analysis_client.post(
|
|
447
|
+
f"/api/v1/analysis/{session_id}/finding-decisions",
|
|
448
|
+
headers=headers,
|
|
449
|
+
json=radar_decision_payload,
|
|
450
|
+
)
|
|
451
|
+
assert accepted_response.status_code == 200
|
|
452
|
+
assert accepted_response.json()["accepted"] is True
|
|
453
|
+
|
|
454
|
+
repeat_response = analysis_client.post(
|
|
455
|
+
f"/api/v1/analysis/{session_id}/messages/stream",
|
|
456
|
+
headers=headers,
|
|
457
|
+
json={"message": "Continue."},
|
|
458
|
+
)
|
|
459
|
+
assert repeat_response.status_code == 200
|
|
460
|
+
repeat_events = parse_ndjson(repeat_response.text)
|
|
461
|
+
repeated_suggestion = next(
|
|
462
|
+
item["business_logic_suggestion"]
|
|
463
|
+
for item in repeat_events
|
|
464
|
+
if item["event"] == "business_logic_suggestion"
|
|
465
|
+
)
|
|
466
|
+
assert repeated_suggestion["finding_id"] == finding_id
|
|
467
|
+
assert repeated_suggestion["finding"]["status"] == "accepted_for_investigation"
|
|
468
|
+
assert planner.working_knowledge_seen[-1][0]["finding_id"] == finding_id
|
|
469
|
+
|
|
470
|
+
persistent_response = analysis_client.put(
|
|
471
|
+
f"/api/v1/analysis/{session_id}/findings/{finding_id}/decision",
|
|
472
|
+
headers=headers,
|
|
473
|
+
json={
|
|
474
|
+
"finding_id": finding_id,
|
|
475
|
+
"decision": "accept_as_persistent_business_logic",
|
|
476
|
+
"confidence": 0.93,
|
|
477
|
+
"verdict": "An administrator approved this as durable logic.",
|
|
478
|
+
"scope": {"investigation_id": session_id},
|
|
479
|
+
"evidence_refs": ["query:dictionary-check"],
|
|
480
|
+
},
|
|
481
|
+
)
|
|
482
|
+
assert persistent_response.status_code == 200
|
|
483
|
+
|
|
484
|
+
repeat_after_persistent_response = analysis_client.post(
|
|
485
|
+
f"/api/v1/analysis/{session_id}/messages/stream",
|
|
486
|
+
headers=headers,
|
|
487
|
+
json={"message": "Continue again."},
|
|
488
|
+
)
|
|
489
|
+
assert repeat_after_persistent_response.status_code == 200
|
|
490
|
+
repeat_after_persistent_events = parse_ndjson(repeat_after_persistent_response.text)
|
|
491
|
+
repeated_persistent_suggestion = next(
|
|
492
|
+
item["business_logic_suggestion"]
|
|
493
|
+
for item in repeat_after_persistent_events
|
|
494
|
+
if item["event"] == "business_logic_suggestion"
|
|
495
|
+
)
|
|
496
|
+
assert repeated_persistent_suggestion["finding_id"] == finding_id
|
|
497
|
+
assert repeated_persistent_suggestion["finding"]["status"] == (
|
|
498
|
+
"accepted_as_persistent_business_logic"
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
with create_session() as session:
|
|
502
|
+
findings = list(
|
|
503
|
+
session.scalars(
|
|
504
|
+
select(AnalysisFinding).where(
|
|
505
|
+
AnalysisFinding.investigation_id == session_id
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
assert len(findings) == 1
|
|
510
|
+
assert findings[0].finding_id == finding_id
|
|
511
|
+
assert findings[0].decision == "accept_as_persistent_business_logic"
|
|
512
|
+
assert findings[0].status == "accepted_as_persistent_business_logic"
|
|
513
|
+
suggestions = list_business_logic_suggestions(session, connector.id)
|
|
514
|
+
assert len(suggestions) == 1
|
|
515
|
+
assert suggestions[0].enabled is True
|
|
516
|
+
assert suggestions[0].status == "active"
|
|
517
|
+
|
|
518
|
+
|
|
379
519
|
def test_investigation_finding_review_lifecycle_and_scoped_working_knowledge(
|
|
380
520
|
analysis_client: TestClient,
|
|
381
521
|
monkeypatch: pytest.MonkeyPatch,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|