agentidem 1.0.0__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.
- agentidem/__init__.py +33 -0
- agentidem/cli/__init__.py +6 -0
- agentidem/cli/app.py +43 -0
- agentidem/cli/commands/__init__.py +0 -0
- agentidem/cli/commands/record.py +100 -0
- agentidem/cli/commands/replay.py +134 -0
- agentidem/cli/commands/test.py +110 -0
- agentidem/cli/commands/version.py +12 -0
- agentidem/cli/constants.py +1 -0
- agentidem/cli/loader.py +52 -0
- agentidem/cli/output/__init__.py +12 -0
- agentidem/cli/output/console.py +97 -0
- agentidem/detection/__init__.py +19 -0
- agentidem/detection/duplicates.py +99 -0
- agentidem/detection/models.py +19 -0
- agentidem/detection/normalization.py +86 -0
- agentidem/errors.py +41 -0
- agentidem/faults/__init__.py +40 -0
- agentidem/faults/base.py +23 -0
- agentidem/faults/case.py +14 -0
- agentidem/faults/context.py +42 -0
- agentidem/faults/injector.py +53 -0
- agentidem/faults/plan.py +19 -0
- agentidem/faults/runner.py +193 -0
- agentidem/faults/scenarios/__init__.py +15 -0
- agentidem/faults/scenarios/before_operation_failure.py +32 -0
- agentidem/faults/scenarios/duplicate_delivery.py +26 -0
- agentidem/faults/scenarios/lost_ack.py +30 -0
- agentidem/faults/suite.py +215 -0
- agentidem/invariants/__init__.py +14 -0
- agentidem/invariants/api.py +17 -0
- agentidem/invariants/evaluator.py +45 -0
- agentidem/invariants/models.py +10 -0
- agentidem/runtime/__init__.py +9 -0
- agentidem/runtime/context.py +23 -0
- agentidem/runtime/decorators.py +111 -0
- agentidem/runtime/operations.py +198 -0
- agentidem/runtime/runner.py +76 -0
- agentidem/testing/__init__.py +17 -0
- agentidem/testing/models.py +45 -0
- agentidem/testing/runner.py +156 -0
- agentidem/testing/serialization.py +98 -0
- agentidem/trace/__init__.py +19 -0
- agentidem/trace/models.py +56 -0
- agentidem/trace/recorder.py +24 -0
- agentidem/trace/serialization.py +22 -0
- agentidem-1.0.0.dist-info/METADATA +272 -0
- agentidem-1.0.0.dist-info/RECORD +52 -0
- agentidem-1.0.0.dist-info/WHEEL +5 -0
- agentidem-1.0.0.dist-info/entry_points.txt +2 -0
- agentidem-1.0.0.dist-info/licenses/LICENSE +51 -0
- agentidem-1.0.0.dist-info/top_level.txt +1 -0
agentidem/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from agentidem.invariants import invariant
|
|
2
|
+
from agentidem.runtime import (
|
|
3
|
+
read,
|
|
4
|
+
run_traced,
|
|
5
|
+
run_traced_async,
|
|
6
|
+
write,
|
|
7
|
+
)
|
|
8
|
+
from agentidem.testing import TestReport, test_agent, test_agent_async
|
|
9
|
+
from agentidem.trace import (
|
|
10
|
+
ObservationStatus,
|
|
11
|
+
OperationKind,
|
|
12
|
+
OperationStatus,
|
|
13
|
+
Trace,
|
|
14
|
+
TraceOperation,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "1.0.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ObservationStatus",
|
|
21
|
+
"OperationKind",
|
|
22
|
+
"OperationStatus",
|
|
23
|
+
"TestReport",
|
|
24
|
+
"Trace",
|
|
25
|
+
"TraceOperation",
|
|
26
|
+
"invariant",
|
|
27
|
+
"read",
|
|
28
|
+
"run_traced",
|
|
29
|
+
"run_traced_async",
|
|
30
|
+
"test_agent",
|
|
31
|
+
"test_agent_async",
|
|
32
|
+
"write",
|
|
33
|
+
]
|
agentidem/cli/app.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from agentidem.cli.commands.record import record_command
|
|
6
|
+
from agentidem.cli.commands.test import test_command
|
|
7
|
+
from agentidem.cli.commands.replay import replay_command
|
|
8
|
+
from agentidem.cli.commands.version import version_command
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="agentidem",
|
|
12
|
+
help="Reliability testing for state-changing AI agents.",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
app.command(
|
|
18
|
+
"test",
|
|
19
|
+
help="Test an agent for unsafe side effects.",
|
|
20
|
+
)(test_command)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
app.command(
|
|
24
|
+
"record",
|
|
25
|
+
help="Run an agent once and record its execution trace.",
|
|
26
|
+
)(record_command)
|
|
27
|
+
app.command(
|
|
28
|
+
"version",
|
|
29
|
+
help="Show the AgentIdem CLI version.",
|
|
30
|
+
)(version_command)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
app.command(
|
|
34
|
+
"replay",
|
|
35
|
+
help="Re-run a recorded trace and compare the execution.",
|
|
36
|
+
)(replay_command)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.callback()
|
|
40
|
+
def main() -> None:
|
|
41
|
+
"""
|
|
42
|
+
AgentIdem command-line interface.
|
|
43
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from agentidem.cli.loader import (
|
|
12
|
+
TargetLoadError,
|
|
13
|
+
load_target,
|
|
14
|
+
)
|
|
15
|
+
from agentidem.cli.output import (
|
|
16
|
+
render_target_error,
|
|
17
|
+
)
|
|
18
|
+
from agentidem.errors import TracedExecutionError
|
|
19
|
+
from agentidem.runtime.runner import (
|
|
20
|
+
run_traced,
|
|
21
|
+
run_traced_async,
|
|
22
|
+
)
|
|
23
|
+
from agentidem.trace.models import Trace
|
|
24
|
+
from agentidem.trace.serialization import save_trace
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _record_target(
|
|
28
|
+
target: str,
|
|
29
|
+
func: Callable[..., Any],
|
|
30
|
+
) -> Trace:
|
|
31
|
+
if inspect.iscoroutinefunction(func):
|
|
32
|
+
async_func = cast(
|
|
33
|
+
Callable[..., Awaitable[Any]],
|
|
34
|
+
func,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_, trace = asyncio.run(
|
|
38
|
+
run_traced_async(
|
|
39
|
+
target,
|
|
40
|
+
async_func,
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return trace
|
|
45
|
+
|
|
46
|
+
_, trace = run_traced(
|
|
47
|
+
target,
|
|
48
|
+
func,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return trace
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def record_command(
|
|
55
|
+
target: str = typer.Argument(
|
|
56
|
+
...,
|
|
57
|
+
help="Python target in module.path:function format.",
|
|
58
|
+
),
|
|
59
|
+
out: Path = typer.Option(
|
|
60
|
+
Path("trace.json"),
|
|
61
|
+
"--out",
|
|
62
|
+
help="Write the recorded trace to a JSON file.",
|
|
63
|
+
),
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Run an agent once and record its execution trace.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
func = load_target(target)
|
|
70
|
+
|
|
71
|
+
except TargetLoadError as exc:
|
|
72
|
+
render_target_error(str(exc))
|
|
73
|
+
raise typer.Exit(code=2) from exc
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
trace = _record_target(
|
|
77
|
+
target,
|
|
78
|
+
func,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
except TracedExecutionError as exc:
|
|
82
|
+
save_trace(
|
|
83
|
+
exc.trace,
|
|
84
|
+
out,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
render_target_error(
|
|
88
|
+
f"{type(exc.cause).__name__}: {exc.cause}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
raise typer.Exit(code=2) from exc
|
|
92
|
+
|
|
93
|
+
save_trace(
|
|
94
|
+
trace,
|
|
95
|
+
out,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
typer.echo(
|
|
99
|
+
f"Recorded trace to {out}"
|
|
100
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from agentidem.cli.loader import (
|
|
12
|
+
TargetLoadError,
|
|
13
|
+
load_target,
|
|
14
|
+
)
|
|
15
|
+
from agentidem.cli.output import render_target_error
|
|
16
|
+
from agentidem.errors import TracedExecutionError
|
|
17
|
+
from agentidem.runtime.runner import (
|
|
18
|
+
run_traced,
|
|
19
|
+
run_traced_async,
|
|
20
|
+
)
|
|
21
|
+
from agentidem.trace.models import Trace, TraceOperation
|
|
22
|
+
from agentidem.trace.serialization import load_trace
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _operation_signature(
|
|
26
|
+
operation: TraceOperation,
|
|
27
|
+
) -> tuple[Any, ...]:
|
|
28
|
+
return (
|
|
29
|
+
operation.name,
|
|
30
|
+
operation.kind,
|
|
31
|
+
operation.args,
|
|
32
|
+
operation.identity,
|
|
33
|
+
operation.result,
|
|
34
|
+
operation.error,
|
|
35
|
+
operation.status,
|
|
36
|
+
operation.observation,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _traces_match(
|
|
41
|
+
expected: Trace,
|
|
42
|
+
actual: Trace,
|
|
43
|
+
) -> bool:
|
|
44
|
+
if expected.target != actual.target:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
if len(expected.operations) != len(actual.operations):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
return all(
|
|
51
|
+
_operation_signature(expected_operation)
|
|
52
|
+
== _operation_signature(actual_operation)
|
|
53
|
+
for expected_operation, actual_operation in zip(
|
|
54
|
+
expected.operations,
|
|
55
|
+
actual.operations,
|
|
56
|
+
strict=True,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _run_replay_target(
|
|
62
|
+
target: str,
|
|
63
|
+
func: Callable[..., Any],
|
|
64
|
+
) -> Trace:
|
|
65
|
+
if inspect.iscoroutinefunction(func):
|
|
66
|
+
async_func = cast(
|
|
67
|
+
Callable[..., Awaitable[Any]],
|
|
68
|
+
func,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
_, trace = asyncio.run(
|
|
73
|
+
run_traced_async(
|
|
74
|
+
target,
|
|
75
|
+
async_func,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
except TracedExecutionError as exc:
|
|
79
|
+
return exc.trace
|
|
80
|
+
|
|
81
|
+
return trace
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
_, trace = run_traced(
|
|
85
|
+
target,
|
|
86
|
+
func,
|
|
87
|
+
)
|
|
88
|
+
except TracedExecutionError as exc:
|
|
89
|
+
return exc.trace
|
|
90
|
+
|
|
91
|
+
return trace
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def replay_command(
|
|
95
|
+
trace_path: Path = typer.Argument(
|
|
96
|
+
...,
|
|
97
|
+
help="Path to a recorded AgentIdem trace.",
|
|
98
|
+
),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Re-run a recorded target and compare its execution trace.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
expected_trace = load_trace(trace_path)
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
render_target_error(
|
|
107
|
+
f"Failed to load trace '{trace_path}': {exc}"
|
|
108
|
+
)
|
|
109
|
+
raise typer.Exit(code=2) from exc
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
func = load_target(expected_trace.target)
|
|
113
|
+
except TargetLoadError as exc:
|
|
114
|
+
render_target_error(str(exc))
|
|
115
|
+
raise typer.Exit(code=2) from exc
|
|
116
|
+
|
|
117
|
+
actual_trace = _run_replay_target(
|
|
118
|
+
expected_trace.target,
|
|
119
|
+
func,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if _traces_match(
|
|
123
|
+
expected_trace,
|
|
124
|
+
actual_trace,
|
|
125
|
+
):
|
|
126
|
+
typer.echo(
|
|
127
|
+
f"REPLAY MATCH: {expected_trace.target}"
|
|
128
|
+
)
|
|
129
|
+
raise typer.Exit(code=0)
|
|
130
|
+
|
|
131
|
+
typer.echo(
|
|
132
|
+
f"REPLAY MISMATCH: {expected_trace.target}"
|
|
133
|
+
)
|
|
134
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from agentidem import (
|
|
13
|
+
test_agent,
|
|
14
|
+
test_agent_async,
|
|
15
|
+
)
|
|
16
|
+
from agentidem.cli.loader import (
|
|
17
|
+
TargetLoadError,
|
|
18
|
+
load_target,
|
|
19
|
+
)
|
|
20
|
+
from agentidem.cli.output import (
|
|
21
|
+
render_target_error,
|
|
22
|
+
render_test_report,
|
|
23
|
+
)
|
|
24
|
+
from agentidem.testing.models import TestReport
|
|
25
|
+
from agentidem.testing.serialization import (
|
|
26
|
+
report_to_dict,
|
|
27
|
+
save_report,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _run_target(
|
|
32
|
+
target: str,
|
|
33
|
+
func: Callable[..., Any],
|
|
34
|
+
) -> TestReport:
|
|
35
|
+
if inspect.iscoroutinefunction(func):
|
|
36
|
+
async_func = cast(
|
|
37
|
+
Callable[..., Awaitable[Any]],
|
|
38
|
+
func,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return asyncio.run(
|
|
42
|
+
test_agent_async(
|
|
43
|
+
target,
|
|
44
|
+
async_func,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return test_agent(
|
|
49
|
+
target,
|
|
50
|
+
func,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_command(
|
|
55
|
+
target: str = typer.Argument(
|
|
56
|
+
...,
|
|
57
|
+
help="Python target in module.path:function format.",
|
|
58
|
+
),
|
|
59
|
+
out: Path | None = typer.Option(
|
|
60
|
+
None,
|
|
61
|
+
"--out",
|
|
62
|
+
help="Write the test report to a JSON file.",
|
|
63
|
+
),
|
|
64
|
+
json_output: bool = typer.Option(
|
|
65
|
+
False,
|
|
66
|
+
"--json",
|
|
67
|
+
help="Print the test report as JSON.",
|
|
68
|
+
),
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Test an agent for retry and side effect safety.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
func = load_target(target)
|
|
75
|
+
|
|
76
|
+
except TargetLoadError as exc:
|
|
77
|
+
render_target_error(str(exc))
|
|
78
|
+
raise typer.Exit(code=2) from exc
|
|
79
|
+
|
|
80
|
+
report = _run_target(
|
|
81
|
+
target,
|
|
82
|
+
func,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if out is not None:
|
|
86
|
+
save_report(
|
|
87
|
+
report,
|
|
88
|
+
out,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if json_output:
|
|
92
|
+
typer.echo(
|
|
93
|
+
json.dumps(
|
|
94
|
+
report_to_dict(report),
|
|
95
|
+
indent=2,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
render_test_report(
|
|
100
|
+
target,
|
|
101
|
+
report,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if report.baseline_failure is not None:
|
|
105
|
+
raise typer.Exit(code=2)
|
|
106
|
+
|
|
107
|
+
if report.safe:
|
|
108
|
+
raise typer.Exit(code=0)
|
|
109
|
+
|
|
110
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTIDEM_CLI_VERSION = "v1.0.0"
|
agentidem/cli/loader.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TargetLoadError(RuntimeError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_target(target: str) -> Callable[..., Any]:
|
|
15
|
+
if ":" not in target:
|
|
16
|
+
raise TargetLoadError(
|
|
17
|
+
"Target must use the format 'module.path:function'."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
module_name, object_name = target.split(":", 1)
|
|
21
|
+
|
|
22
|
+
if not module_name:
|
|
23
|
+
raise TargetLoadError("Target module cannot be empty.")
|
|
24
|
+
|
|
25
|
+
if not object_name:
|
|
26
|
+
raise TargetLoadError("Target function cannot be empty.")
|
|
27
|
+
|
|
28
|
+
cwd = str(Path.cwd())
|
|
29
|
+
|
|
30
|
+
if cwd not in sys.path:
|
|
31
|
+
sys.path.insert(0, cwd)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
module = importlib.import_module(module_name)
|
|
35
|
+
except Exception as exc:
|
|
36
|
+
raise TargetLoadError(
|
|
37
|
+
f"Failed to import module '{module_name}': {exc}"
|
|
38
|
+
) from exc
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
obj = getattr(module, object_name)
|
|
42
|
+
except AttributeError as exc:
|
|
43
|
+
raise TargetLoadError(
|
|
44
|
+
f"Module '{module_name}' has no attribute '{object_name}'."
|
|
45
|
+
) from exc
|
|
46
|
+
|
|
47
|
+
if not callable(obj):
|
|
48
|
+
raise TargetLoadError(
|
|
49
|
+
f"Target '{target}' is not callable."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return obj
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from agentidem.testing.models import TestReport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
error_console = Console(stderr=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_test_report(
|
|
14
|
+
target: str,
|
|
15
|
+
report: TestReport,
|
|
16
|
+
) -> None:
|
|
17
|
+
if report.baseline_failure is not None:
|
|
18
|
+
render_baseline_failure(
|
|
19
|
+
target,
|
|
20
|
+
report,
|
|
21
|
+
)
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
status = "SAFE" if report.safe else "UNSAFE"
|
|
25
|
+
|
|
26
|
+
console.print()
|
|
27
|
+
console.print(
|
|
28
|
+
f"[bold]{status}[/bold] {target}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
console.print(
|
|
32
|
+
f"Fault cases: {report.fault_count} "
|
|
33
|
+
f"Unsafe: {report.unsafe_count}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not report.results:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
table = Table(
|
|
40
|
+
title="Fault scenarios",
|
|
41
|
+
show_header=True,
|
|
42
|
+
header_style="bold",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
table.add_column("Scenario")
|
|
46
|
+
table.add_column("Status")
|
|
47
|
+
table.add_column("Execution")
|
|
48
|
+
table.add_column("Findings", justify="right")
|
|
49
|
+
|
|
50
|
+
for result in report.results:
|
|
51
|
+
scenario_status = (
|
|
52
|
+
"SAFE"
|
|
53
|
+
if result.safe
|
|
54
|
+
else "UNSAFE"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
execution_status = (
|
|
58
|
+
"FAILED"
|
|
59
|
+
if result.failed
|
|
60
|
+
else "COMPLETED"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
table.add_row(
|
|
64
|
+
result.case.scenario,
|
|
65
|
+
scenario_status,
|
|
66
|
+
execution_status,
|
|
67
|
+
str(len(result.findings)),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
console.print(table)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def render_baseline_failure(
|
|
74
|
+
target: str,
|
|
75
|
+
report: TestReport,
|
|
76
|
+
) -> None:
|
|
77
|
+
failure = report.baseline_failure
|
|
78
|
+
|
|
79
|
+
if failure is None:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
error_console.print()
|
|
83
|
+
error_console.print(
|
|
84
|
+
f"[bold]BASELINE FAILED[/bold] {target}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
error_console.print(
|
|
88
|
+
f"{failure.error_type}: {failure.message}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def render_target_error(
|
|
93
|
+
message: str,
|
|
94
|
+
) -> None:
|
|
95
|
+
error_console.print(
|
|
96
|
+
f"[bold]ERROR[/bold] {message}"
|
|
97
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from agentidem.detection.duplicates import (
|
|
2
|
+
DuplicateWrite,
|
|
3
|
+
detect_duplicate_write_findings,
|
|
4
|
+
detect_duplicate_writes,
|
|
5
|
+
duplicate_write_to_finding,
|
|
6
|
+
)
|
|
7
|
+
from agentidem.detection.models import (
|
|
8
|
+
Finding,
|
|
9
|
+
FindingSeverity,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DuplicateWrite",
|
|
14
|
+
"Finding",
|
|
15
|
+
"FindingSeverity",
|
|
16
|
+
"detect_duplicate_write_findings",
|
|
17
|
+
"detect_duplicate_writes",
|
|
18
|
+
"duplicate_write_to_finding",
|
|
19
|
+
]
|