Coverage for src/pullapprove/presets.py: 100%

20 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-08-17 21:57 -0500

1"""The commands a preset name stands for. 

2 

3A preset is PullApprove's own maintained invocation of a reviewer CLI, named by 

4a word instead of pasted into every config. It lives here, in the library, 

5because it is part of the config contract: `check` resolves the name, so an 

6unknown preset is a config error a person sees at commit time rather than a run 

7that fails an hour later. 

8 

9Nothing here runs anything. These strings are data — the app splices them and 

10executes them, and the execution side is what will smoke whether an invocation 

11is actually right for the CLI's current release. A preset that stops working 

12because its CLI changed is a change to this table. 

13""" 

14 

15from __future__ import annotations 

16 

17import re 

18from dataclasses import dataclass 

19 

20# What may stand in for `{base}`: a commit hash, or a ref made of the safe 

21# subset of ref characters. `{base}` lands inside shell text — a claude 

22# preset's template puts it in a double-quoted argument — and branch names may 

23# legally contain `"`, `$`, backticks and `;`, so a permissive substitution 

24# here would let whoever names a branch choose what runs in the sandbox. 

25_SAFE_BASE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*$") 

26 

27 

28def substitute_base(template: str, *, base: str) -> str: 

29 """`{base}` filled in with the change's base commit. 

30 

31 The one place that substitution happens, so what `{base}` means has a 

32 single answer rather than one per caller — including the refusal below. 

33 """ 

34 if not _SAFE_BASE.match(base): 

35 raise ValueError( 

36 f"base {base!r} is not a commit hash or plain ref name — refusing " 

37 "to splice it into a shell command" 

38 ) 

39 return template.replace("{base}", base) 

40 

41 

42@dataclass(frozen=True) 

43class Preset: 

44 """One named reviewer command, with the base filled in later.""" 

45 

46 # `{base}` is the base commit — the one fact about the change a command 

47 # can't learn from inside the checkout, where HEAD is the change itself. 

48 template: str 

49 

50 def command(self, *, base: str) -> str: 

51 """The command to run, with this base spelled in.""" 

52 return substitute_base(self.template, base=base) 

53 

54 

55PRESETS: dict[str, Preset] = { 

56 # The CLI's own review skill, pointed at the base. Their reviewer, not 

57 # ours: the skill carries its own prompt. 

58 # The range spelling matters: a bare sha reviews that commit itself; 

59 # the review wanted is HEAD against the base. 

60 # The budget cap fails the run at the CLI (exit 1), which reads as a 

61 # FAILED review — the gate holds rather than scoring a partial read. 

62 # Codex has no budget flag; its preset is bounded by the timeout alone. 

63 "claude-code-review": Preset( 

64 template='claude -p "/code-review {base}..HEAD" --max-budget-usd 20' 

65 ), 

66 # Codex's own review task, run headlessly. The sandbox bypass is the 

67 # documented mode for externally-sandboxed environments — the hosted run 

68 # IS the sandbox, and without it Codex's own sandbox fails to start inside 

69 # one and the review reads nothing. 

70 "codex-review": Preset( 

71 template=( 

72 "codex exec review --base {base} --dangerously-bypass-approvals-and-sandbox" 

73 ) 

74 ), 

75} 

76 

77PRESET_NAMES = tuple(PRESETS) 

78 

79 

80def resolve_preset(name: str) -> Preset: 

81 """The preset `name` stands for. 

82 

83 Raises ValueError naming the menu, because the caller is always either 

84 validating a config someone just wrote or compiling one to run. 

85 """ 

86 try: 

87 return PRESETS[name] 

88 except KeyError: 

89 raise ValueError( 

90 f"unknown preset '{name}'. Available presets: {', '.join(PRESET_NAMES)}." 

91 ) from None