Coverage for lintro / models / core / tool_result.py: 90%

30 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Models for core tool execution results. 

2 

3This module defines the canonical result object returned by all tools. It 

4supports both check and fix flows and includes standardized fields to report 

5fixed vs remaining counts for fix-capable tools. 

6""" 

7 

8from __future__ import annotations 

9 

10from collections.abc import Sequence 

11from dataclasses import dataclass, field 

12from typing import TYPE_CHECKING, Any 

13 

14if TYPE_CHECKING: 

15 from lintro.parsers.base_issue import BaseIssue 

16 

17 

18@dataclass 

19class ToolResult: 

20 """Result of running a tool. 

21 

22 For check operations: 

23 - ``issues_count`` represents the number of issues found. 

24 

25 For fix/format operations: 

26 - ``initial_issues_count`` is the number of issues detected before fixes 

27 - ``fixed_issues_count`` is the number of issues the tool auto-fixed 

28 - ``remaining_issues_count`` is the number of issues still remaining 

29 - ``issues_count`` should mirror ``remaining_issues_count`` for 

30 backward compatibility in format-mode summaries 

31 

32 The ``issues`` field can contain parsed issue objects (tool-specific) to 

33 support unified table formatting. 

34 

35 Convention: for FIX action, remaining issues occupy the tail of the 

36 ``issues`` list. Tools append all detected issues in order, so the 

37 last ``remaining_issues_count`` entries are the ones still unfixed. 

38 """ 

39 

40 name: str = field(default="") 

41 success: bool = field(default=False) 

42 output: str | None = field(default=None) 

43 issues_count: int = field(default=0) 

44 formatted_output: str | None = field(default=None) 

45 issues: Sequence[BaseIssue] | None = field(default=None) 

46 

47 # Optional standardized counts for fix-capable tools 

48 initial_issues_count: int | None = field(default=None) 

49 fixed_issues_count: int | None = field(default=None) 

50 remaining_issues_count: int | None = field(default=None) 

51 

52 # Pre-fix issues detected before applying fixes (for displaying what was fixed) 

53 initial_issues: Sequence[BaseIssue] | None = field(default=None) 

54 

55 # Optional pytest-specific summary data for display 

56 pytest_summary: dict[str, Any] | None = field(default=None) 

57 

58 # Optional AI-generated metadata (explanations, fix suggestions). 

59 # Expected keys (all optional): 

60 # "fix_suggestions": list[AIFixSuggestionPayload] (serialized) 

61 # "fixed_count": int 

62 # "verified_count": int 

63 # "unverified_count": int 

64 # "telemetry": dict with api_calls, tokens, cost, latency 

65 # Built incrementally via helpers in lintro.ai.metadata. 

66 ai_metadata: dict[str, Any] | None = field(default=None) 

67 

68 # Working directory used during tool execution (for resolving relative 

69 # issue file paths in AI fix generation) 

70 cwd: str | None = field(default=None) 

71 

72 # Skip tracking for tools that didn't execute 

73 skipped: bool = field(default=False) 

74 skip_reason: str | None = field(default=None) 

75 

76 def __post_init__(self) -> None: 

77 """Validate that the issue counts and skip state are consistent. 

78 

79 Raises: 

80 ValueError: If issue counts are inconsistent or skip state is invalid. 

81 """ 

82 # Skipped tools are not failures — they just didn't run 

83 if self.skipped: 

84 self.success = True 

85 if not self.skip_reason: 

86 raise ValueError( 

87 "skip_reason is required when skipped=True", 

88 ) 

89 

90 if self.skip_reason and not self.skipped: 

91 raise ValueError( 

92 "skip_reason can only be set when skipped=True", 

93 ) 

94 

95 if ( 

96 self.initial_issues_count is not None 

97 and self.fixed_issues_count is not None 

98 and self.remaining_issues_count is not None 

99 and self.initial_issues_count 

100 != self.fixed_issues_count + self.remaining_issues_count 

101 ): 

102 raise ValueError( 

103 f"Inconsistent issue counts: " 

104 f"initial={self.initial_issues_count}, " 

105 f"fixed={self.fixed_issues_count}, " 

106 f"remaining={self.remaining_issues_count}. " 

107 f"Expected: initial = fixed + remaining", 

108 )