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
« 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."""
3from __future__ import annotations
5import argparse
6import sys
8from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context
10__all__ = ["main_learning_tests"]
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
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
22 print_json(record.to_dict())
24 if getattr(args, "stale_aware", False):
25 import json as _json
26 from pathlib import Path
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
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
42 if record.status != "proven" or is_record_stale(record, lt_config.stale_after_days):
43 return 1
45 return 0
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
52 records = list_records()
53 print_json([r.to_dict() for r in records])
54 return 0
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
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
69def cmd_orphans(args: argparse.Namespace) -> int:
70 import json as _json
71 from pathlib import Path
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
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/")]
95 imported = get_imported_packages(source_dirs)
97 records = list_records()
98 orphans = [r for r in records if r.target.split()[0].lower() not in imported]
100 if not orphans:
101 print("No orphaned records found.")
102 return 0
104 for record in orphans:
105 print(f"{record.target} (status: {record.status}, date: {record.date})")
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
113 return 1
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 )
131 subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
132 subparsers.required = True
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 )
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 )
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')")
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 )
189 parsed = parser.parse_args()
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