nat-engine 1__py3-none-any.whl
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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Adaptive Test Generation Controller.
|
|
7
|
+
|
|
8
|
+
Extends the base ε-greedy :class:`~mannf.core.testing.adaptive_controller.AdaptiveController`
|
|
9
|
+
with smarter test-generation strategies guided by BDI belief signals:
|
|
10
|
+
|
|
11
|
+
* **Boundary value analysis** — focus on inputs at boundaries where the neural
|
|
12
|
+
network predicts highest fault likelihood.
|
|
13
|
+
* **Mutation-based fuzzing** — mutate existing test cases more aggressively
|
|
14
|
+
for endpoints the agents believe are fault-prone.
|
|
15
|
+
* **Sequence testing** — identify and test multi-step API flows (e.g.
|
|
16
|
+
create → read → update → delete).
|
|
17
|
+
|
|
18
|
+
Integration with :class:`~mannf.core.prioritization.belief_prioritizer.BeliefPrioritizer`
|
|
19
|
+
allocates test-generation effort proportionally to endpoint risk.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import copy
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import random
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
from mannf.core.agents.belief_state import BeliefState
|
|
33
|
+
from mannf.core.neural import NeuralNetwork
|
|
34
|
+
from mannf.core.prioritization.belief_prioritizer import BeliefPrioritizer
|
|
35
|
+
from mannf.core.testing.models import TestCase, TestResult
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from mannf.llm.base import LLMProvider
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
_FEATURE_DIM = 8
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AdaptiveController:
|
|
46
|
+
"""Adaptive test-generation controller with BDI-guided strategies.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
initial_epsilon:
|
|
51
|
+
Initial exploration probability (0–1).
|
|
52
|
+
min_epsilon:
|
|
53
|
+
Minimum exploration probability after decay.
|
|
54
|
+
epsilon_decay:
|
|
55
|
+
Multiplicative decay applied after each :meth:`update` call.
|
|
56
|
+
learning_rate:
|
|
57
|
+
Step size for the value-network update.
|
|
58
|
+
prioritizer:
|
|
59
|
+
Optional :class:`BeliefPrioritizer` to allocate generation effort.
|
|
60
|
+
mutation_intensity:
|
|
61
|
+
Base mutation intensity (0–1). Higher values apply larger random
|
|
62
|
+
perturbations to mutated inputs.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# Common CRUD sequence patterns used by the sequence tester
|
|
66
|
+
_CRUD_SEQUENCE: List[str] = ["create", "read", "update", "delete"]
|
|
67
|
+
_CRUD_METHODS: Dict[str, str] = {
|
|
68
|
+
"create": "POST",
|
|
69
|
+
"read": "GET",
|
|
70
|
+
"update": "PUT",
|
|
71
|
+
"delete": "DELETE",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
initial_epsilon: float = 0.9,
|
|
77
|
+
min_epsilon: float = 0.05,
|
|
78
|
+
epsilon_decay: float = 0.995,
|
|
79
|
+
learning_rate: float = 0.005,
|
|
80
|
+
prioritizer: Optional[BeliefPrioritizer] = None,
|
|
81
|
+
mutation_intensity: float = 0.3,
|
|
82
|
+
llm_provider: Optional["LLMProvider"] = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
self.epsilon = initial_epsilon
|
|
85
|
+
self.min_epsilon = min_epsilon
|
|
86
|
+
self.epsilon_decay = epsilon_decay
|
|
87
|
+
self.mutation_intensity = mutation_intensity
|
|
88
|
+
self.prioritizer = prioritizer
|
|
89
|
+
self.llm_provider = llm_provider
|
|
90
|
+
|
|
91
|
+
self.value_net = NeuralNetwork(
|
|
92
|
+
layer_sizes=[_FEATURE_DIM, 16, 8, 1],
|
|
93
|
+
hidden_activation="relu",
|
|
94
|
+
learning_rate=learning_rate,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self._step_count = 0
|
|
98
|
+
self._cumulative_reward = 0.0
|
|
99
|
+
|
|
100
|
+
# Sequence memory: stores observed (endpoint, method) pairs to infer flows
|
|
101
|
+
self._sequence_memory: List[Tuple[str, str]] = []
|
|
102
|
+
self._max_sequence_memory = 200
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
# Selection (ε-greedy)
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def select(self, candidates: List[TestCase]) -> Optional[TestCase]:
|
|
109
|
+
"""Pick the next test case using ε-greedy selection.
|
|
110
|
+
|
|
111
|
+
If a *prioritizer* is set, candidates are pre-ordered by risk before
|
|
112
|
+
the greedy selection is applied.
|
|
113
|
+
"""
|
|
114
|
+
if not candidates:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
ordered = candidates
|
|
118
|
+
if self.prioritizer is not None:
|
|
119
|
+
ordered = self.prioritizer.prioritize(candidates)
|
|
120
|
+
|
|
121
|
+
if np.random.random() < self.epsilon:
|
|
122
|
+
choice = ordered[int(np.random.randint(len(ordered)))]
|
|
123
|
+
logger.debug("AdaptiveController: exploring → %s", choice.id[:8])
|
|
124
|
+
else:
|
|
125
|
+
scores = [
|
|
126
|
+
float(self.value_net.predict(c.feature_vector(_FEATURE_DIM))[0])
|
|
127
|
+
for c in ordered
|
|
128
|
+
]
|
|
129
|
+
best_idx = int(np.argmax(scores))
|
|
130
|
+
choice = ordered[best_idx]
|
|
131
|
+
logger.debug(
|
|
132
|
+
"AdaptiveController: exploiting → %s (score=%.4f)",
|
|
133
|
+
choice.id[:8],
|
|
134
|
+
scores[best_idx],
|
|
135
|
+
)
|
|
136
|
+
return choice
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
# Learning
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def update(self, test_case: TestCase, result: TestResult) -> float:
|
|
143
|
+
"""Update the value network with the observed reward signal.
|
|
144
|
+
|
|
145
|
+
Returns the MSE training loss for the current step.
|
|
146
|
+
"""
|
|
147
|
+
reward = result.reward_signal()
|
|
148
|
+
self._cumulative_reward += reward
|
|
149
|
+
|
|
150
|
+
x = test_case.feature_vector(_FEATURE_DIM)
|
|
151
|
+
target = np.array([reward])
|
|
152
|
+
loss = self.value_net.train_step(x, target)
|
|
153
|
+
|
|
154
|
+
# Record for sequence inference
|
|
155
|
+
method = test_case.metadata.get("method", "GET")
|
|
156
|
+
self._record_sequence(test_case.target, method)
|
|
157
|
+
|
|
158
|
+
# Update prioritizer if available
|
|
159
|
+
if self.prioritizer is not None:
|
|
160
|
+
self.prioritizer.record_result(result, test_case.target)
|
|
161
|
+
|
|
162
|
+
self.epsilon = max(self.min_epsilon, self.epsilon * self.epsilon_decay)
|
|
163
|
+
self._step_count += 1
|
|
164
|
+
|
|
165
|
+
logger.debug(
|
|
166
|
+
"AdaptiveController update: reward=%.3f loss=%.6f ε=%.4f",
|
|
167
|
+
reward,
|
|
168
|
+
loss,
|
|
169
|
+
self.epsilon,
|
|
170
|
+
)
|
|
171
|
+
return loss
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
# Boundary value analysis
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def generate_boundary_variants(
|
|
178
|
+
self,
|
|
179
|
+
base_case: TestCase,
|
|
180
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
181
|
+
) -> List[TestCase]:
|
|
182
|
+
"""Generate boundary-value variants of *base_case*.
|
|
183
|
+
|
|
184
|
+
For each numeric input parameter, creates test cases at:
|
|
185
|
+
* value = 0
|
|
186
|
+
* value = -1 (underflow)
|
|
187
|
+
* value = maximum observed + 1 (overflow)
|
|
188
|
+
* value = ``None`` / missing (null injection)
|
|
189
|
+
|
|
190
|
+
The intensity of boundary analysis is scaled by the endpoint's belief
|
|
191
|
+
signal — high-risk endpoints produce more variants.
|
|
192
|
+
"""
|
|
193
|
+
risk_multiplier = self._endpoint_risk(base_case.target, belief_states)
|
|
194
|
+
# Number of variants scales with risk (1–4)
|
|
195
|
+
n_variants = max(1, round(risk_multiplier * 4))
|
|
196
|
+
|
|
197
|
+
variants: List[TestCase] = []
|
|
198
|
+
numeric_keys = [
|
|
199
|
+
k for k, v in base_case.inputs.items() if isinstance(v, (int, float))
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
boundary_values: List[Any] = [0, -1, 2**31 - 1, None]
|
|
203
|
+
|
|
204
|
+
for key in numeric_keys:
|
|
205
|
+
original_val = base_case.inputs[key]
|
|
206
|
+
for bv in boundary_values[: n_variants]:
|
|
207
|
+
new_inputs = dict(base_case.inputs)
|
|
208
|
+
new_inputs[key] = bv
|
|
209
|
+
variant = TestCase(
|
|
210
|
+
target=base_case.target,
|
|
211
|
+
inputs=new_inputs,
|
|
212
|
+
expected_behavior=(
|
|
213
|
+
f"Boundary value {bv!r} for '{key}' — expect graceful handling"
|
|
214
|
+
),
|
|
215
|
+
priority=min(1.0, base_case.priority + 0.1 * risk_multiplier),
|
|
216
|
+
metadata={
|
|
217
|
+
**base_case.metadata,
|
|
218
|
+
"generation_strategy": "boundary_value_analysis",
|
|
219
|
+
"boundary_key": key,
|
|
220
|
+
"boundary_value": str(bv),
|
|
221
|
+
"source_case_id": base_case.id,
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
variants.append(variant)
|
|
225
|
+
|
|
226
|
+
if not variants:
|
|
227
|
+
# No numeric params: inject empty input as boundary
|
|
228
|
+
variant = TestCase(
|
|
229
|
+
target=base_case.target,
|
|
230
|
+
inputs={},
|
|
231
|
+
expected_behavior="Empty inputs — expect graceful handling",
|
|
232
|
+
priority=base_case.priority,
|
|
233
|
+
metadata={
|
|
234
|
+
**base_case.metadata,
|
|
235
|
+
"generation_strategy": "boundary_value_analysis",
|
|
236
|
+
"boundary_key": "__empty__",
|
|
237
|
+
"source_case_id": base_case.id,
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
variants.append(variant)
|
|
241
|
+
|
|
242
|
+
return variants
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
# Mutation-based fuzzing
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def mutate(
|
|
249
|
+
self,
|
|
250
|
+
base_case: TestCase,
|
|
251
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
252
|
+
n_mutations: int = 3,
|
|
253
|
+
) -> List[TestCase]:
|
|
254
|
+
"""Generate mutated variants of *base_case*.
|
|
255
|
+
|
|
256
|
+
Mutation intensity scales with the endpoint's BDI risk signal:
|
|
257
|
+
high-risk endpoints receive more aggressive perturbations.
|
|
258
|
+
|
|
259
|
+
Mutation operations (applied randomly):
|
|
260
|
+
* Numeric values: add Gaussian noise scaled by mutation intensity.
|
|
261
|
+
* String values: random character substitution or length change.
|
|
262
|
+
* Boolean values: flip.
|
|
263
|
+
* Keys: randomly drop one key (missing-parameter test).
|
|
264
|
+
"""
|
|
265
|
+
risk = self._endpoint_risk(base_case.target, belief_states)
|
|
266
|
+
intensity = self.mutation_intensity * (0.5 + risk)
|
|
267
|
+
|
|
268
|
+
mutations: List[TestCase] = []
|
|
269
|
+
for i in range(n_mutations):
|
|
270
|
+
mutated_inputs = self._apply_mutation(base_case.inputs, intensity)
|
|
271
|
+
mutated = TestCase(
|
|
272
|
+
target=base_case.target,
|
|
273
|
+
inputs=mutated_inputs,
|
|
274
|
+
expected_behavior=f"Mutation #{i + 1} of '{base_case.id[:8]}' — fault injection",
|
|
275
|
+
priority=min(1.0, base_case.priority + 0.05 * risk),
|
|
276
|
+
metadata={
|
|
277
|
+
**base_case.metadata,
|
|
278
|
+
"generation_strategy": "mutation_fuzzing",
|
|
279
|
+
"mutation_index": i,
|
|
280
|
+
"mutation_intensity": round(intensity, 3),
|
|
281
|
+
"source_case_id": base_case.id,
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
mutations.append(mutated)
|
|
285
|
+
return mutations
|
|
286
|
+
|
|
287
|
+
async def mutate_with_llm(
|
|
288
|
+
self,
|
|
289
|
+
base_case: TestCase,
|
|
290
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
291
|
+
n_mutations: int = 3,
|
|
292
|
+
) -> List[TestCase]:
|
|
293
|
+
"""Generate mutated variants, adding one LLM-guided smart mutation when available.
|
|
294
|
+
|
|
295
|
+
The LLM mutation is appended to the random mutations produced by
|
|
296
|
+
:meth:`mutate` and tagged with ``generation_strategy = "llm_mutation"``.
|
|
297
|
+
Falls back to :meth:`mutate` if the LLM is unavailable or fails.
|
|
298
|
+
"""
|
|
299
|
+
mutations = self.mutate(base_case, belief_states=belief_states, n_mutations=n_mutations)
|
|
300
|
+
|
|
301
|
+
if self.llm_provider is None or not self.llm_provider.is_available():
|
|
302
|
+
return mutations
|
|
303
|
+
|
|
304
|
+
from mannf.llm.prompts import MUTATION_PROMPT # noqa: PLC0415
|
|
305
|
+
|
|
306
|
+
prompt = MUTATION_PROMPT.format(
|
|
307
|
+
n=1,
|
|
308
|
+
target=base_case.target,
|
|
309
|
+
inputs=json.dumps(base_case.inputs, indent=2),
|
|
310
|
+
expected_behavior=base_case.expected_behavior,
|
|
311
|
+
)
|
|
312
|
+
try:
|
|
313
|
+
raw = await self.llm_provider.generate(prompt)
|
|
314
|
+
raw_cases = self.llm_provider._parse_test_cases(raw)
|
|
315
|
+
for raw_case in raw_cases[:1]:
|
|
316
|
+
if not isinstance(raw_case, dict):
|
|
317
|
+
continue
|
|
318
|
+
inputs = raw_case.get("inputs", {})
|
|
319
|
+
if not isinstance(inputs, dict):
|
|
320
|
+
inputs = copy.deepcopy(base_case.inputs)
|
|
321
|
+
mutations.append(
|
|
322
|
+
TestCase(
|
|
323
|
+
target=base_case.target,
|
|
324
|
+
inputs=inputs,
|
|
325
|
+
expected_behavior=raw_case.get(
|
|
326
|
+
"expected_behavior",
|
|
327
|
+
f"LLM mutation of '{base_case.id[:8]}'",
|
|
328
|
+
),
|
|
329
|
+
priority=min(1.0, base_case.priority + 0.1),
|
|
330
|
+
metadata={
|
|
331
|
+
**base_case.metadata,
|
|
332
|
+
"generation_strategy": "llm_mutation",
|
|
333
|
+
"generator": "llm",
|
|
334
|
+
"source_case_id": base_case.id,
|
|
335
|
+
"mutation_rationale": raw_case.get("mutation_rationale", ""),
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
except Exception as exc: # noqa: BLE001
|
|
340
|
+
logger.warning(
|
|
341
|
+
"AdaptiveController: LLM mutation failed for %s: %s",
|
|
342
|
+
base_case.target, exc,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return mutations
|
|
346
|
+
|
|
347
|
+
def _apply_mutation(self, inputs: Dict[str, Any], intensity: float) -> Dict[str, Any]:
|
|
348
|
+
mutated = copy.deepcopy(inputs)
|
|
349
|
+
if not mutated:
|
|
350
|
+
return mutated
|
|
351
|
+
|
|
352
|
+
keys = list(mutated.keys())
|
|
353
|
+
op = random.choice(["perturb", "drop", "flip", "null"])
|
|
354
|
+
|
|
355
|
+
if op == "drop" and len(keys) > 1:
|
|
356
|
+
drop_key = random.choice(keys)
|
|
357
|
+
del mutated[drop_key]
|
|
358
|
+
elif op == "null":
|
|
359
|
+
null_key = random.choice(keys)
|
|
360
|
+
mutated[null_key] = None
|
|
361
|
+
elif op == "flip":
|
|
362
|
+
bool_keys = [k for k, v in mutated.items() if isinstance(v, bool)]
|
|
363
|
+
if bool_keys:
|
|
364
|
+
flip_key = random.choice(bool_keys)
|
|
365
|
+
mutated[flip_key] = not mutated[flip_key]
|
|
366
|
+
else: # perturb numerics / strings
|
|
367
|
+
for key in keys:
|
|
368
|
+
val = mutated[key]
|
|
369
|
+
if isinstance(val, (int, float)):
|
|
370
|
+
noise = random.gauss(0, intensity * max(1.0, abs(val)))
|
|
371
|
+
mutated[key] = type(val)(val + noise) if isinstance(val, float) else int(val + noise)
|
|
372
|
+
elif isinstance(val, str):
|
|
373
|
+
if val and random.random() < intensity:
|
|
374
|
+
idx = random.randrange(len(val))
|
|
375
|
+
mutated[key] = val[:idx] + random.choice("abcdefXYZ!@#") + val[idx + 1:]
|
|
376
|
+
|
|
377
|
+
return mutated
|
|
378
|
+
|
|
379
|
+
def generate_sequence_tests(
|
|
380
|
+
self,
|
|
381
|
+
endpoints: List[str],
|
|
382
|
+
base_inputs: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
383
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
384
|
+
) -> List[List[TestCase]]:
|
|
385
|
+
"""Generate multi-step API flow test sequences.
|
|
386
|
+
|
|
387
|
+
Infers CRUD sequences from the observed endpoint list and optionally
|
|
388
|
+
from the planner agent's sequence memory. Returns a list of test-case
|
|
389
|
+
*sequences*, each representing a complete API flow.
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
endpoints:
|
|
394
|
+
Known endpoint targets (e.g. ``["/users", "/users/{id}"]``).
|
|
395
|
+
base_inputs:
|
|
396
|
+
Mapping of endpoint → default inputs. Defaults to empty dicts.
|
|
397
|
+
belief_states:
|
|
398
|
+
Optional belief states to prioritize high-risk sequences.
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
list of list of TestCase
|
|
403
|
+
Each inner list is an ordered sequence of test cases forming a flow.
|
|
404
|
+
"""
|
|
405
|
+
base_inputs = base_inputs or {}
|
|
406
|
+
sequences: List[List[TestCase]] = []
|
|
407
|
+
|
|
408
|
+
inferred = self._infer_sequences(endpoints)
|
|
409
|
+
for seq_endpoints in inferred:
|
|
410
|
+
seq = []
|
|
411
|
+
for i, (ep, method) in enumerate(seq_endpoints):
|
|
412
|
+
tc = TestCase(
|
|
413
|
+
target=ep,
|
|
414
|
+
inputs=dict(base_inputs.get(ep, {})),
|
|
415
|
+
expected_behavior=f"Sequence step {i + 1}/{len(seq_endpoints)}: {method} {ep}",
|
|
416
|
+
priority=self._endpoint_risk(ep, belief_states),
|
|
417
|
+
metadata={
|
|
418
|
+
"generation_strategy": "sequence_testing",
|
|
419
|
+
"sequence_step": i,
|
|
420
|
+
"sequence_length": len(seq_endpoints),
|
|
421
|
+
"method": method,
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
seq.append(tc)
|
|
425
|
+
if seq:
|
|
426
|
+
sequences.append(seq)
|
|
427
|
+
|
|
428
|
+
return sequences
|
|
429
|
+
|
|
430
|
+
def _infer_sequences(
|
|
431
|
+
self, endpoints: List[str]
|
|
432
|
+
) -> List[List[Tuple[str, str]]]:
|
|
433
|
+
"""Heuristically group endpoints into CRUD-like sequences."""
|
|
434
|
+
# Group endpoints by common resource root
|
|
435
|
+
# e.g. /users, /users/{id} → one group
|
|
436
|
+
groups: Dict[str, List[str]] = {}
|
|
437
|
+
for ep in endpoints:
|
|
438
|
+
root = ep.split("{")[0].rstrip("/")
|
|
439
|
+
if root not in groups:
|
|
440
|
+
groups[root] = []
|
|
441
|
+
groups[root].append(ep)
|
|
442
|
+
|
|
443
|
+
sequences = []
|
|
444
|
+
for root, eps in groups.items():
|
|
445
|
+
# Assign CRUD methods heuristically
|
|
446
|
+
seq: List[Tuple[str, str]] = []
|
|
447
|
+
has_id = any("{" in ep for ep in eps)
|
|
448
|
+
base_ep = min(eps, key=len) # shortest = collection endpoint
|
|
449
|
+
detail_ep = max(eps, key=len) if has_id else base_ep
|
|
450
|
+
|
|
451
|
+
seq.append((base_ep, "POST")) # create
|
|
452
|
+
seq.append((detail_ep, "GET")) # read
|
|
453
|
+
if has_id:
|
|
454
|
+
seq.append((detail_ep, "PUT")) # update
|
|
455
|
+
seq.append((detail_ep, "DELETE")) # delete
|
|
456
|
+
|
|
457
|
+
if len(seq) >= 2:
|
|
458
|
+
sequences.append(seq)
|
|
459
|
+
|
|
460
|
+
# Also add any patterns observed in sequence memory
|
|
461
|
+
if len(self._sequence_memory) >= 3:
|
|
462
|
+
# Extract unique transitions
|
|
463
|
+
mem_seq: List[Tuple[str, str]] = []
|
|
464
|
+
seen: set = set()
|
|
465
|
+
for item in self._sequence_memory[-10:]:
|
|
466
|
+
if item not in seen:
|
|
467
|
+
seen.add(item)
|
|
468
|
+
mem_seq.append(item)
|
|
469
|
+
if len(mem_seq) >= 2:
|
|
470
|
+
sequences.append(mem_seq)
|
|
471
|
+
|
|
472
|
+
return sequences
|
|
473
|
+
|
|
474
|
+
def _record_sequence(self, endpoint: str, method: str) -> None:
|
|
475
|
+
self._sequence_memory.append((endpoint, method))
|
|
476
|
+
if len(self._sequence_memory) > self._max_sequence_memory:
|
|
477
|
+
self._sequence_memory.pop(0)
|
|
478
|
+
|
|
479
|
+
# ------------------------------------------------------------------
|
|
480
|
+
# Helpers
|
|
481
|
+
# ------------------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
def _endpoint_risk(
|
|
484
|
+
self,
|
|
485
|
+
endpoint: str,
|
|
486
|
+
belief_states: Optional[List[BeliefState]],
|
|
487
|
+
) -> float:
|
|
488
|
+
if not belief_states:
|
|
489
|
+
return 0.5
|
|
490
|
+
signals = [bs.fault_likelihood.get(endpoint, 0.5) for bs in belief_states]
|
|
491
|
+
return sum(signals) / len(signals)
|
|
492
|
+
|
|
493
|
+
# ------------------------------------------------------------------
|
|
494
|
+
# Diagnostics
|
|
495
|
+
# ------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def step_count(self) -> int:
|
|
499
|
+
return self._step_count
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def average_reward(self) -> float:
|
|
503
|
+
if self._step_count == 0:
|
|
504
|
+
return 0.0
|
|
505
|
+
return self._cumulative_reward / self._step_count
|
|
506
|
+
|
|
507
|
+
def score(self, test_case: TestCase) -> float:
|
|
508
|
+
"""Return the current predicted value for a test case."""
|
|
509
|
+
return float(self.value_net.predict(test_case.feature_vector(_FEATURE_DIM))[0])
|