1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Effective-settings resolution for `tm watch` — CLI flags over config defaults.
//!
//! Why: `tm watch poll|listen <project>` takes several settings (the routing
//! label, the poll interval, the issue state) that may come from a CLI flag OR
//! the `watch:` section of `~/.trusty-tools/trusty-mpm/config.yaml` (#1220). The
//! precedence must be applied in exactly one tested place so `poll` and `listen`
//! cannot diverge, and so "CLI flag overrides config" is an asserted invariant
//! rather than scattered `unwrap_or` calls.
//! What: [`RawWatchArgs`] is the parsed CLI surface (all overrides optional);
//! [`ResolvedWatch`] is the fully-resolved settings the loops consume;
//! [`resolve`] merges a `RawWatchArgs` with a [`WatchConfig`] applying the
//! precedence **CLI flag > config value > built-in default** and resolves the
//! `<project>` token (either a direct `owner/repo` or a name falling back to the
//! configured `repo`). [`DEFAULT_LABEL`] / [`DEFAULT_INTERVAL_SECS`] name the
//! built-in defaults.
//! Test: `resolve_*` in the sibling `tests.rs`.
use parse_github_path;
use WatchConfig;
use IssueState;
/// Built-in default routing label.
///
/// Why: the design fixes `tm-agent` as the default routing label (issues carrying
/// it are picked up); naming it once keeps the CLI default and the docs aligned.
/// What: `"tm-agent"`.
/// Test: `resolve_uses_default_label_when_unset`.
pub const DEFAULT_LABEL: &str = "tm-agent";
/// Built-in default `listen` poll interval, in seconds.
///
/// Why: a sensible cadence that is gentle on the GitHub API yet responsive; the
/// design fixes it at 60s. Named once so the CLI default and config fallback agree.
/// What: `60`.
/// Test: `resolve_uses_default_interval_when_unset`.
pub const DEFAULT_INTERVAL_SECS: u64 = 60;
/// The parsed `tm watch` CLI surface before config merging.
///
/// Why: separating the raw (all-optional) CLI overrides from the resolved
/// settings keeps the precedence logic in [`resolve`] and makes the merge
/// unit-testable without clap.
/// What: the `<project>` token plus the optional `--label` / `--interval-secs` /
/// `--state` overrides. `project` is the verbatim positional (`owner/repo` or a
/// name); `None` overrides mean "fall back to config then default".
/// Test: drives every `resolve_*` test.
pub
/// Fully-resolved `tm watch` settings consumed by the poll/listen loops.
///
/// Why: once precedence is applied the loops want concrete, non-optional values
/// (a resolved `owner/repo`, a label, an interval, a state) so they never re-run
/// the merge or re-resolve the project.
/// What: the resolved `repo` (`owner/repo`), the routing `label`, the listen
/// `interval_secs`, and the `state` selection.
/// Test: produced and asserted by every `resolve_*` test.
pub
/// Resolve the `<project>` token into an `owner/repo` board repository.
///
/// Why: the spec lets `<project>` be a direct `owner/repo` OR a registered
/// project name resolved via config. A direct `owner/repo` must win as-is; any
/// other token (a bare name, or anything that does not parse to two segments)
/// falls back to the configured `watch.repo`. That fallback must NEVER be silent:
/// a typo in the `<project>` token would otherwise quietly target the configured
/// board, so we emit a clear stderr notice naming both the offending token and
/// the repo we are substituting. Surfacing a clear error when neither yields a
/// repo stops the loop from running against a guessed board.
/// What: returns the trimmed `project` when it parses to a clean `owner/repo`
/// (two non-empty segments and no extra path noise); otherwise, when a non-empty
/// `config.repo` exists, prints `note: project token '<tok>' is not 'owner/repo';
/// using configured repo '<config.repo>'` to stderr and returns that repo;
/// otherwise an actionable error (never a guess).
/// Test: `resolve_project_direct_owner_repo`, `resolve_project_name_uses_config`,
/// `resolve_project_unresolvable_errors`.
/// Decide whether a token is a direct `owner/repo` reference.
///
/// Why: `resolve_project` must distinguish a literal `owner/repo` from a bare
/// registered-project name so the former is used verbatim and the latter falls
/// back to config. A `<owner>/<repo>` has exactly two non-empty, slash-separated
/// segments with no host/scheme noise.
/// What: returns true when `parse_github_path` yields a path whose rendered
/// `owner/repo` equals the lower-cased input — i.e. the token is already a clean
/// two-segment identity, not a URL or a single bare name.
/// Test: `looks_like_owner_repo_*`.
/// Merge raw CLI args with config defaults into [`ResolvedWatch`].
///
/// Why: the single place the precedence **CLI flag > config value > built-in
/// default** is applied, so `poll` and `listen` share identical resolution and
/// the rule is asserted once.
/// What: resolves the project to an `owner/repo`, then for each setting takes the
/// CLI override if present, else the config value, else the built-in default.
/// `state` has no config field (it is a per-run choice) so it falls straight
/// through to [`IssueState::Open`].
/// Test: `resolve_cli_overrides_config`, `resolve_config_used_when_no_cli`,
/// `resolve_uses_default_label_when_unset`, `resolve_uses_default_interval_when_unset`.
pub