Coverage for little_loops / cli / learning_tests.py: 9%

106 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-29 00:55 -0500

1"""ll-learning-tests: CLI for querying and managing the learning test registry.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6import sys 

7 

8from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context 

9 

10__all__ = ["main_learning_tests"] 

11 

12 

13def cmd_check(args: argparse.Namespace) -> int: 

14 from little_loops.cli.output import print_json 

15 from little_loops.learning_tests import check_learning_test 

16 

17 record = check_learning_test(args.target) 

18 if record is None: 

19 print(f"Error: no record found for {args.target!r}", file=sys.stderr) 

20 return 1 

21 

22 print_json(record.to_dict()) 

23 

24 if getattr(args, "stale_aware", False): 

25 import json as _json 

26 from pathlib import Path 

27 

28 from little_loops.config.core import resolve_config_path 

29 from little_loops.config.features import LearningTestsConfig 

30 from little_loops.learning_tests.gate import is_record_stale 

31 

32 config_path = resolve_config_path(Path.cwd()) 

33 lt_config = LearningTestsConfig() 

34 if config_path is not None: 

35 try: 

36 data = _json.loads(config_path.read_text(encoding="utf-8")) 

37 if isinstance(data, dict): 

38 lt_config = LearningTestsConfig.from_dict(data.get("learning_tests", {})) 

39 except (OSError, _json.JSONDecodeError): 

40 pass 

41 

42 if record.status != "proven" or is_record_stale(record, lt_config.stale_after_days): 

43 return 1 

44 

45 return 0 

46 

47 

48def cmd_list(_args: argparse.Namespace) -> int: 

49 from little_loops.cli.output import print_json 

50 from little_loops.learning_tests import list_records 

51 

52 records = list_records() 

53 print_json([r.to_dict() for r in records]) 

54 return 0 

55 

56 

57def cmd_mark_stale(args: argparse.Namespace) -> int: 

58 from little_loops.issue_parser import slugify 

59 from little_loops.learning_tests import check_learning_test, mark_stale 

60 

61 record = check_learning_test(args.target) 

62 if record is None: 

63 print(f"Error: no record found for {args.target!r}", file=sys.stderr) 

64 return 1 

65 mark_stale(slugify(args.target)) 

66 return 0 

67 

68 

69def cmd_orphans(args: argparse.Namespace) -> int: 

70 import json as _json 

71 from pathlib import Path 

72 

73 from little_loops.config.core import resolve_config_path 

74 from little_loops.issue_parser import slugify 

75 from little_loops.learning_tests import list_records, mark_stale 

76 from little_loops.learning_tests.import_scan import get_imported_packages 

77 

78 source_dirs: list[Path] 

79 if args.scope: 

80 source_dirs = [Path(d.strip()) for d in args.scope.split(",")] 

81 else: 

82 resolved: list[Path] | None = None 

83 config_path = resolve_config_path(Path.cwd()) 

84 if config_path is not None: 

85 try: 

86 data = _json.loads(config_path.read_text(encoding="utf-8")) 

87 if isinstance(data, dict): 

88 raw = data.get("learning_tests", {}).get("scan_dirs") 

89 if raw: 

90 resolved = [Path(d) for d in raw] 

91 except (OSError, _json.JSONDecodeError): 

92 pass 

93 source_dirs = resolved if resolved is not None else [Path("scripts/")] 

94 

95 imported = get_imported_packages(source_dirs) 

96 

97 records = list_records() 

98 orphans = [r for r in records if r.target.split()[0].lower() not in imported] 

99 

100 if not orphans: 

101 print("No orphaned records found.") 

102 return 0 

103 

104 for record in orphans: 

105 print(f"{record.target} (status: {record.status}, date: {record.date})") 

106 

107 if args.mark_stale: 

108 for record in orphans: 

109 mark_stale(slugify(record.target)) 

110 print(f"\nMarked {len(orphans)} record(s) stale.") 

111 return 0 

112 

113 return 1 

114 

115 

116def main_learning_tests() -> int: 

117 """CLI handler for ll-learning-tests subcommands.""" 

118 with cli_event_context(DEFAULT_DB_PATH, "ll-learning-tests", sys.argv[1:]): 

119 parser = argparse.ArgumentParser( 

120 prog="ll-learning-tests", 

121 description="Query and manage the little-loops learning test registry", 

122 formatter_class=argparse.RawDescriptionHelpFormatter, 

123 epilog=""" 

124Examples: 

125 ll-learning-tests check "Anthropic SDK streaming" 

126 ll-learning-tests list 

127 ll-learning-tests mark-stale "Anthropic SDK streaming" 

128""", 

129 ) 

130 

131 subparsers = parser.add_subparsers(dest="command", metavar="COMMAND") 

132 subparsers.required = True 

133 

134 check_parser = subparsers.add_parser( 

135 "check", 

136 help="Print a record as JSON; exit 1 if not found", 

137 description="Look up a learning test record by target name and print as JSON", 

138 ) 

139 check_parser.add_argument("target", help="Target name (e.g. 'Anthropic SDK streaming')") 

140 check_parser.add_argument( 

141 "--stale-aware", 

142 action="store_true", 

143 default=False, 

144 dest="stale_aware", 

145 help=( 

146 "Exit 1 if the record is absent or date-stale " 

147 "(even if status=proven); exit 0 only if proven and within stale_after_days threshold" 

148 ), 

149 ) 

150 

151 subparsers.add_parser( 

152 "list", 

153 help="Print all records as a JSON array", 

154 description="List all learning test records in the registry", 

155 ) 

156 

157 stale_parser = subparsers.add_parser( 

158 "mark-stale", 

159 help="Mark a record as stale; exit 1 if not found", 

160 description="Set status=stale on a learning test record", 

161 ) 

162 stale_parser.add_argument("target", help="Target name (e.g. 'Anthropic SDK streaming')") 

163 

164 orphans_parser = subparsers.add_parser( 

165 "orphans", 

166 help="List records for packages no longer imported; exit 1 if any found", 

167 description=( 

168 "Detect learning test records whose target package is no longer imported " 

169 "anywhere in the configured source directories." 

170 ), 

171 ) 

172 orphans_parser.add_argument( 

173 "--mark-stale", 

174 action="store_true", 

175 default=False, 

176 dest="mark_stale", 

177 help="Atomically mark all orphaned records stale and exit 0", 

178 ) 

179 orphans_parser.add_argument( 

180 "--scope", 

181 default=None, 

182 metavar="DIRS", 

183 help=( 

184 "Comma-separated list of directories to scan for imports " 

185 "(default: learning_tests.scan_dirs config key, fallback 'scripts/')" 

186 ), 

187 ) 

188 

189 parsed = parser.parse_args() 

190 

191 if parsed.command == "check": 

192 return cmd_check(parsed) 

193 elif parsed.command == "list": 

194 return cmd_list(parsed) 

195 elif parsed.command == "mark-stale": 

196 return cmd_mark_stale(parsed) 

197 elif parsed.command == "orphans": 

198 return cmd_orphans(parsed) 

199 else: 

200 parser.print_help() 

201 return 1