g1t/crates/contracts/src/work.rs

571 lines17,391 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Issues and pull requests replace intents and attempts1//! The work service: issues, pull requests, comments and sessions.
Work service in Rust, with RFC 3339 timestamps2//!
3//! Each `*Args` struct is the argument of the method of the same name,
4//! served at `POST /rpc/<method>`.
Issues and pull requests replace intents and attempts5//!
6//! Issues and pull requests share one sequence of numbers per repository,
7//! so `#12` names exactly one of them.
Work service in Rust, with RFC 3339 timestamps8
9use serde::{Deserialize, Serialize};
10
11use crate::repos::RepoPath;
12use crate::{User, Viewer};
13
Issues and pull requests replace intents and attempts14/// Labels every repository starts with. Any other label comes into being
15/// the first time it is put on an issue.
16pub const DEFAULT_LABELS: [&str; 5] = ["bug", "feature", "docs", "chore", "question"];
17
18/// `open` or `closed`: the filter on lists of issues and pull requests. An
19/// open pull request is a draft or one ready for review; a closed one was
20/// merged or closed without merging.
Work service in Rust, with RFC 3339 timestamps21#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
Issues and pull requests replace intents and attempts23pub enum State {
Work service in Rust, with RFC 3339 timestamps24 Open,
Issues and pull requests replace intents and attempts25 Closed,
26}
27
28/// Why an issue was closed.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum IssueReason {
32 /// The work was done. If a pull request did it, `resolved_by` names it.
33 Completed,
34 NotPlanned,
Work service in Rust, with RFC 3339 timestamps35}
36
Issues and pull requests replace intents and attempts37impl IssueReason {
38 pub fn as_str(self) -> &'static str {
39 match self {
40 IssueReason::Completed => "completed",
41 IssueReason::NotPlanned => "not_planned",
42 }
43 }
44}
45
46/// Something that should change in a repository: a bug, a feature, a
47/// question. Opened by a person, an agent or an integration. Pull requests
48/// are made against it; the one that is merged resolves it.
Work service in Rust, with RFC 3339 timestamps49#[derive(Clone, Debug, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
Issues and pull requests replace intents and attempts51pub struct Issue {
Work service in Rust, with RFC 3339 timestamps52 pub id: String,
53 pub repo_id: String,
Issues and pull requests replace intents and attempts54 /// Shown as `#12`.
Work service in Rust, with RFC 3339 timestamps55 pub number: u32,
56 pub title: String,
Issues and pull requests replace intents and attempts57 /// Markdown. Also what an agent is given to work from.
58 pub body: String,
59 pub labels: Vec<String>,
60 /// Commands that must pass for a pull request to be accepted.
Work service in Rust, with RFC 3339 timestamps61 pub checks: Vec<String>,
Issues and pull requests replace intents and attempts62 pub state: State,
63 /// Set when closed.
64 pub reason: Option<IssueReason>,
65 /// The number of the pull request whose merge closed this issue.
66 pub resolved_by: Option<u32>,
Work service in Rust, with RFC 3339 timestamps67 pub author: User,
68 /// RFC 3339.
69 pub created_at: String,
Issues and pull requests replace intents and attempts70 /// RFC 3339.
71 pub updated_at: String,
72 /// RFC 3339.
73 pub closed_at: Option<String>,
74 /// Pull requests made against this issue, in any state.
75 pub pull_count: u32,
76 pub comment_count: u32,
Work service in Rust, with RFC 3339 timestamps77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(rename_all = "lowercase")]
Issues and pull requests replace intents and attempts81pub enum PullStatus {
82 /// Still being worked on.
83 Draft,
84 /// Ready for review.
85 Open,
86 Merged,
87 /// Closed without merging.
88 Closed,
Work service in Rust, with RFC 3339 timestamps89}
90
Issues and pull requests replace intents and attempts91impl PullStatus {
Work service in Rust, with RFC 3339 timestamps92 pub fn as_str(self) -> &'static str {
93 match self {
Issues and pull requests replace intents and attempts94 PullStatus::Draft => "draft",
95 PullStatus::Open => "open",
96 PullStatus::Merged => "merged",
97 PullStatus::Closed => "closed",
Work service in Rust, with RFC 3339 timestamps98 }
99 }
100
Issues and pull requests replace intents and attempts101 /// Whether the pull request can still be changed or merged.
Work service in Rust, with RFC 3339 timestamps102 pub fn is_active(self) -> bool {
Issues and pull requests replace intents and attempts103 matches!(self, PullStatus::Draft | PullStatus::Open)
Work service in Rust, with RFC 3339 timestamps104 }
105}
106
107/// Where the agent runs: on g1t's sandboxes, or in someone's own session.
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "lowercase")]
Issues and pull requests replace intents and attempts110pub enum Runtime {
Work service in Rust, with RFC 3339 timestamps111 Hosted,
112 External,
113}
114
Pull requests from branches115/// A proposed change. It is made either in a fork created for it, which is
116/// how agents work, or on a branch pushed to the repository itself.
Work service in Rust, with RFC 3339 timestamps117#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
Issues and pull requests replace intents and attempts119pub struct Pull {
Work service in Rust, with RFC 3339 timestamps120 pub id: String,
121 pub repo_id: String,
Issues and pull requests replace intents and attempts122 /// Shown as `#12`.
Work service in Rust, with RFC 3339 timestamps123 pub number: u32,
Issues and pull requests replace intents and attempts124 /// The number of the issue this is for, if any.
125 pub issue: Option<u32>,
126 pub title: String,
127 /// Markdown: what changed and why. Set when marked ready.
128 pub body: Option<String>,
Work service in Rust, with RFC 3339 timestamps129 /// A label for the agent doing the work, e.g. `claude-code`.
130 pub agent: String,
Issues and pull requests replace intents and attempts131 pub runtime: Runtime,
132 pub status: PullStatus,
Pull requests from branches133 /// The fork holding the change, unless it is on a branch.
134 pub fork: Option<RepoPath>,
Work service in Rust, with RFC 3339 timestamps135 /// The fork's repository id.
Pull requests from branches136 pub fork_repo_id: Option<String>,
137 /// The branch of the repository holding the change, unless it is in a
138 /// fork.
139 pub branch: Option<String>,
Work service in Rust, with RFC 3339 timestamps140 pub head_commit: Option<String>,
Issues and pull requests replace intents and attempts141 /// For a merged pull request, what the branch pointed to before the
142 /// merge. Comparing against it shows what the pull request changed.
143 pub merge_base: Option<String>,
144 /// Username of whoever merged it.
145 pub merged_by: Option<String>,
146 /// RFC 3339.
147 pub merged_at: Option<String>,
148 /// Set on a pull request closed because another one for the same issue
149 /// was merged: that one's number.
150 pub superseded_by: Option<u32>,
Acceptance checks in sandboxes, line comments and review verdicts151 /// Where the latest run of the issue's acceptance checks stands, if
152 /// there has been one against the current head.
153 pub check_status: Option<CheckStatus>,
Issues and pull requests replace intents and attempts154 pub author: User,
Work service in Rust, with RFC 3339 timestamps155 /// RFC 3339.
156 pub created_at: String,
157 /// RFC 3339.
158 pub updated_at: String,
159}
160
Acceptance checks in sandboxes, line comments and review verdicts161#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "lowercase")]
163pub enum CheckStatus {
164 /// Waiting for a sandbox.
165 Queued,
166 Running,
167 Passed,
168 Failed,
169 /// The checks could not be run at all.
170 Errored,
171}
172
173impl CheckStatus {
174 pub fn as_str(self) -> &'static str {
175 match self {
176 CheckStatus::Queued => "queued",
177 CheckStatus::Running => "running",
178 CheckStatus::Passed => "passed",
179 CheckStatus::Failed => "failed",
180 CheckStatus::Errored => "errored",
181 }
182 }
183}
184
185/// How one acceptance check went.
186#[derive(Clone, Debug, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct CheckResult {
189 pub command: String,
190 pub passed: bool,
191 /// Absent when the command was stopped for taking too long.
192 #[serde(default)]
193 pub exit_code: Option<i32>,
194 /// What the command printed, standard output and error together. The
195 /// end of it, when there was a lot.
196 #[serde(default)]
197 pub output: String,
198 #[serde(default)]
199 pub duration_ms: u64,
200}
201
202/// One run of an issue's acceptance checks against a pull request's head,
203/// in a sandbox that holds nothing but that commit.
204#[derive(Clone, Debug, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct CheckRun {
207 pub id: String,
208 /// The commit that was checked.
209 pub head_commit: String,
210 pub status: CheckStatus,
211 pub results: Vec<CheckResult>,
212 /// Why the checks could not be run, when `status` is `errored`.
213 pub error: Option<String>,
214 /// RFC 3339.
215 pub created_at: String,
216 /// RFC 3339.
217 pub finished_at: Option<String>,
218}
219
220/// A reviewer's decision on a pull request.
221#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(rename_all = "snake_case")]
223pub enum Verdict {
224 Approve,
225 RequestChanges,
226}
227
228impl Verdict {
229 pub fn as_str(self) -> &'static str {
230 match self {
231 Verdict::Approve => "approve",
232 Verdict::RequestChanges => "request_changes",
233 }
234 }
235}
236
237/// A comment on an issue or a pull request. On a pull request it can sit
238/// on one line of the change, and it can carry a reviewer's verdict.
Issues and pull requests replace intents and attempts239#[derive(Clone, Debug, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct Comment {
242 pub id: String,
243 pub author: User,
244 /// Markdown.
245 pub body: String,
Acceptance checks in sandboxes, line comments and review verdicts246 /// The file commented on, for a comment on a line.
247 pub path: Option<String>,
248 /// The line of that file, as numbered after the change.
249 pub line: Option<u32>,
250 pub verdict: Option<Verdict>,
Issues and pull requests replace intents and attempts251 /// RFC 3339.
252 pub created_at: String,
253}
254
Work service in Rust, with RFC 3339 timestamps255#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257pub enum SessionEntryKind {
258 Prompt,
259 Message,
260 ToolCall,
261 ToolResult,
262 Note,
263}
264
Issues and pull requests replace intents and attempts265/// One step of an agent's session: the "why" behind a pull request's commits.
Work service in Rust, with RFC 3339 timestamps266#[derive(Clone, Debug, Serialize, Deserialize)]
267pub struct SessionEntry {
268 pub seq: u32,
269 pub kind: SessionEntryKind,
270 pub text: String,
271 /// For tool calls and results.
272 pub tool: Option<String>,
273 /// The fork's head commit when this entry was recorded, if known.
274 pub commit: Option<String>,
275 /// RFC 3339.
276 pub at: String,
277}
278
279#[derive(Clone, Debug, Serialize, Deserialize)]
280pub struct NewSessionEntry {
281 pub kind: SessionEntryKind,
282 pub text: String,
283 #[serde(default)]
284 pub tool: Option<String>,
285 #[serde(default)]
286 pub commit: Option<String>,
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts290pub struct IssueDetail {
291 pub issue: Issue,
292 /// Every pull request made against it, oldest first.
293 pub pulls: Vec<Pull>,
294 pub comments: Vec<Comment>,
Work service in Rust, with RFC 3339 timestamps295}
296
297#[derive(Clone, Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts298pub struct PullDetail {
299 pub pull: Pull,
300 /// The issue it is for, if any.
301 pub issue: Option<Issue>,
302 pub comments: Vec<Comment>,
Acceptance checks in sandboxes, line comments and review verdicts303 /// The latest run of the issue's acceptance checks.
304 pub checks: Option<CheckRun>,
Work service in Rust, with RFC 3339 timestamps305}
306
Issues and pull requests replace intents and attempts307/// `open_issue`. Returns `Outcome<Issue>`.
Work service in Rust, with RFC 3339 timestamps308#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts309pub struct OpenIssueArgs {
Work service in Rust, with RFC 3339 timestamps310 pub actor: User,
311 pub repo: RepoPath,
312 pub title: String,
313 #[serde(default)]
Issues and pull requests replace intents and attempts314 pub body: String,
315 #[serde(default)]
316 pub labels: Vec<String>,
Work service in Rust, with RFC 3339 timestamps317 #[serde(default)]
318 pub checks: Vec<String>,
319}
320
Issues and pull requests replace intents and attempts321/// `list_issues`, newest first. Returns `Outcome<Vec<Issue>>`.
Work service in Rust, with RFC 3339 timestamps322#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts323pub struct ListIssuesArgs {
Work service in Rust, with RFC 3339 timestamps324 pub repo: RepoPath,
325 pub viewer: Viewer,
326 #[serde(default)]
Issues and pull requests replace intents and attempts327 pub state: Option<State>,
328 /// Only issues carrying this label.
329 #[serde(default)]
330 pub label: Option<String>,
Work service in Rust, with RFC 3339 timestamps331}
332
Issues and pull requests replace intents and attempts333/// `list_pulls`, newest first. Returns `Outcome<Vec<Pull>>`.
Work service in Rust, with RFC 3339 timestamps334#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts335pub struct ListPullsArgs {
336 pub repo: RepoPath,
337 pub viewer: Viewer,
338 #[serde(default)]
339 pub state: Option<State>,
340}
341
342/// `get_issue` (`Outcome<IssueDetail>`), `get_pull` (`Outcome<PullDetail>`),
343/// `read_session` (`Outcome<Vec<SessionEntry>>`), `list_labels`
344/// (`Outcome<Vec<String>>`) and `counts` (`Outcome<Counts>`). The last two
345/// ignore `number`.
346#[derive(Debug, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct ViewArgs {
Work service in Rust, with RFC 3339 timestamps349 pub repo: RepoPath,
Issues and pull requests replace intents and attempts350 #[serde(default)]
Work service in Rust, with RFC 3339 timestamps351 pub number: u32,
352 pub viewer: Viewer,
Issues and pull requests replace intents and attempts353 /// For `read_session`: only entries after this sequence number.
354 #[serde(default)]
355 pub after_seq: u32,
356}
357
358/// How many issues and pull requests are open on a repository.
359#[derive(Debug, Serialize, Deserialize)]
360pub struct Counts {
361 pub issues: u32,
362 pub pulls: u32,
363}
364
365/// `update_issue`: changes whichever fields are given. The author or a
366/// member of the workspace may. Returns `Outcome<Issue>`.
367#[derive(Debug, Serialize, Deserialize)]
368pub struct UpdateIssueArgs {
369 pub actor: User,
370 pub repo: RepoPath,
371 pub number: u32,
372 #[serde(default)]
373 pub title: Option<String>,
374 #[serde(default)]
375 pub body: Option<String>,
376 #[serde(default)]
377 pub labels: Option<Vec<String>>,
Work service in Rust, with RFC 3339 timestamps378}
379
Issues and pull requests replace intents and attempts380/// `close_issue` and `reopen_issue`. Each returns `Outcome<Issue>`.
Work service in Rust, with RFC 3339 timestamps381#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts382pub struct IssueActionArgs {
Work service in Rust, with RFC 3339 timestamps383 pub actor: User,
Issues and pull requests replace intents and attempts384 pub repo: RepoPath,
385 pub number: u32,
386 /// For `close_issue`; `completed` if left out.
387 #[serde(default)]
388 pub reason: Option<IssueReason>,
Work service in Rust, with RFC 3339 timestamps389}
390
Acceptance checks in sandboxes, line comments and review verdicts391/// `add_comment`, on an issue or a pull request. On a pull request it may
392/// name a line of the change, and may carry a verdict; nobody can give a
393/// verdict on their own pull request. Returns `Outcome<Comment>`.
Work service in Rust, with RFC 3339 timestamps394#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts395pub struct AddCommentArgs {
Work service in Rust, with RFC 3339 timestamps396 pub actor: User,
Issues and pull requests replace intents and attempts397 pub repo: RepoPath,
398 pub number: u32,
Acceptance checks in sandboxes, line comments and review verdicts399 /// May be empty when approving.
400 #[serde(default)]
Issues and pull requests replace intents and attempts401 pub body: String,
Acceptance checks in sandboxes, line comments and review verdicts402 #[serde(default)]
403 pub path: Option<String>,
404 #[serde(default)]
405 pub line: Option<u32>,
406 #[serde(default)]
407 pub verdict: Option<Verdict>,
Work service in Rust, with RFC 3339 timestamps408}
409
Pull requests from branches410/// `open_pull`. Without `branch`, forks the repo and returns a draft pull
411/// request to push to. With it, opens a pull request, ready for review,
412/// for a branch already pushed to the repo. Returns `Outcome<Pull>`.
Work service in Rust, with RFC 3339 timestamps413#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts414pub struct OpenPullArgs {
415 pub actor: User,
416 pub repo: RepoPath,
417 /// The issue this is for.
Work service in Rust, with RFC 3339 timestamps418 #[serde(default)]
Issues and pull requests replace intents and attempts419 pub issue: Option<u32>,
420 /// Defaults to the issue's title; required without an issue.
421 #[serde(default)]
422 pub title: String,
Pull requests from branches423 /// What changed and why. Usually set later, when a draft is marked ready.
424 #[serde(default)]
425 pub body: String,
426 /// A branch of the repository that already holds the change.
427 #[serde(default)]
428 pub branch: Option<String>,
Issues and pull requests replace intents and attempts429 #[serde(default)]
430 pub agent: String,
431 pub runtime: Runtime,
Work service in Rust, with RFC 3339 timestamps432}
433
Issues and pull requests replace intents and attempts434/// `ready_pull`, `close_pull` and `merge_pull`. Each returns `Outcome<Pull>`.
Work service in Rust, with RFC 3339 timestamps435#[derive(Debug, Serialize, Deserialize)]
436#[serde(rename_all = "camelCase")]
Issues and pull requests replace intents and attempts437pub struct PullActionArgs {
Work service in Rust, with RFC 3339 timestamps438 pub actor: User,
Issues and pull requests replace intents and attempts439 pub repo: RepoPath,
440 pub number: u32,
441 /// For `ready_pull`: what changed and why.
Work service in Rust, with RFC 3339 timestamps442 #[serde(default)]
443 pub summary: String,
Issues and pull requests replace intents and attempts444 /// For `merge_pull`: leave the issue open and the other pull requests
445 /// for it untouched, because this one is only part of the work.
446 #[serde(default)]
447 pub keep_issue_open: bool,
Acceptance checks in sandboxes, line comments and review verdicts448 /// For `merge_pull`: merge although the acceptance checks have not
449 /// passed.
450 #[serde(default)]
451 pub ignore_checks: bool,
452}
453
454/// `start_checks`: begins a run of the acceptance checks for a pull request
455/// that is ready for review. Called by the runner service, which starts the
456/// sandbox. Returns `Outcome<CheckJob>`.
457#[derive(Debug, Serialize, Deserialize)]
458#[serde(rename_all = "camelCase")]
459pub struct StartChecksArgs {
460 pub pull_id: String,
461}
462
463/// What a sandbox needs to carry out a check run.
464#[derive(Debug, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct CheckJob {
467 pub run_id: String,
468 /// Lets the sandbox, and nothing else, report this run's results.
469 pub token: String,
470 pub commands: Vec<String>,
471 /// The repository holding the commit: the fork, or the repository itself.
472 pub source: RepoPath,
473 pub commit: String,
474 /// Who opened the pull request, and so can read its source.
475 pub author: User,
476 /// Username of whoever wrote the checks: the issue's author.
477 pub requested_by: String,
478 pub repo: RepoPath,
479 pub number: u32,
480}
481
482/// `report_checks`: what a sandbox says about its run. With no results and
483/// no error it has started. `skip` forgets the run, for one that will not
484/// be carried out. Returns `Outcome<CheckRun>`.
485#[derive(Debug, Serialize, Deserialize)]
486#[serde(rename_all = "camelCase")]
487pub struct ReportChecksArgs {
488 pub run_id: String,
489 pub token: String,
490 #[serde(default)]
491 pub results: Vec<CheckResult>,
492 #[serde(default)]
493 pub error: Option<String>,
494 #[serde(default)]
495 pub skip: bool,
Work service in Rust, with RFC 3339 timestamps496}
497
Issues and pull requests replace intents and attempts498/// A pull request in progress, with where it lives.
499#[derive(Debug, Serialize, Deserialize)]
500pub struct ActivePull {
501 pub pull: Pull,
502 pub issue: Option<Issue>,
503}
504
505/// `list_active_pulls`: drafts and open pull requests the viewer started,
506/// most recently active first. Returns `Vec<ActivePull>`.
Work service in Rust, with RFC 3339 timestamps507#[derive(Debug, Serialize, Deserialize)]
508pub struct ViewerArgs {
509 pub viewer: Viewer,
510}
511
512/// `append_session`. Returns `Outcome<Appended>`.
513#[derive(Debug, Serialize, Deserialize)]
514pub struct AppendSessionArgs {
515 pub actor: User,
Issues and pull requests replace intents and attempts516 pub repo: RepoPath,
517 pub number: u32,
Work service in Rust, with RFC 3339 timestamps518 pub entries: Vec<NewSessionEntry>,
519}
520
521#[derive(Debug, Serialize, Deserialize)]
522pub struct Appended {
523 pub count: u32,
524}
Issues and pull requests replace intents and attempts525
526/// Lowercases, trims and de-duplicates labels, dropping empty ones.
527/// Returns `None` if there are too many or one is too long.
528pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> {
529 const MAX_LABELS: usize = 10;
530 const MAX_LABEL_CHARS: usize = 40;
531 let mut normalized: Vec<String> = Vec::new();
532 for label in labels {
533 let label = label
534 .split_whitespace()
535 .collect::<Vec<_>>()
536 .join(" ")
537 .to_lowercase();
538 if label.is_empty() || normalized.contains(&label) {
539 continue;
540 }
541 if label.chars().count() > MAX_LABEL_CHARS {
542 return None;
543 }
544 normalized.push(label);
545 }
546 (normalized.len() <= MAX_LABELS).then_some(normalized)
547}
548
549#[cfg(test)]
550mod tests {
551 use super::normalize_labels;
552
553 fn labels(names: &[&str]) -> Vec<String> {
554 names.iter().map(|name| (*name).to_owned()).collect()
555 }
556
557 #[test]
558 fn labels_are_lowercased_trimmed_and_unique() {
559 assert_eq!(
560 normalize_labels(&labels(&[" Bug ", "bug", "", "Good First Issue"])),
561 Some(labels(&["bug", "good first issue"]))
562 );
563 }
564
565 #[test]
566 fn too_long_or_too_many_labels_are_refused() {
567 assert_eq!(normalize_labels(&["x".repeat(41)]), None);
568 let many: Vec<String> = (0..11).map(|i| format!("label-{i}")).collect();
569 assert_eq!(normalize_labels(&many), None);
570 }
571}