git-ents.gitmain
⌘K
foforge
issue.rs198 lines · 6.9 KB · rusthistorycomment on this file
1//! Integration coverage for `git ents issue` against a real local
2//! composition root (`roots.local`): `new` via an explicit `--title`
3//! (a direct library call), `new` via a fake `$EDITOR` script run through
4//! the actual `git-ents` binary (the interactive-composition path — a
5//! subprocess so `$EDITOR` is scoped to the child rather than mutating
6//! this test process's own environment), and `edit` round-tripping
7//! state/assignees/labels.
8
9#![allow(clippy::expect_used, reason = "integration test")]
10
11mod common;
12
13use std::process::Command;
14
15use ents_model::MemberId;
16use git_ents::commands::issue;
17use git_ents::root::LocalRoot;
18
19use common::write_fake_editor;
20
21/// `git ents issue new --title ...`: no editor needed, the title and body
22/// round-trip exactly.
23// @relation(model.issue, roots.local, scope=function, role=Verifies)
24#[test]
25fn issue_new_with_an_explicit_title_skips_the_editor() {
26 let fixture = common::Fixture::new(1);
27 let root = LocalRoot::open(fixture.path()).expect("opens");
28
29 let id = issue::new(
30 &root,
31 "gate rejects a valid signature".to_owned(),
32 "steps to reproduce...".to_owned(),
33 "open".to_owned(),
34 vec!["bug".to_owned()],
35 vec!["jdc".to_owned()],
36 Some(fixture.key_path.clone()),
37 )
38 .expect("creates");
39
40 let found = issue::show(&root, &id).expect("shows");
41 assert_eq!(found.title, "gate rejects a valid signature");
42 assert_eq!(found.body, "steps to reproduce...");
43 assert_eq!(found.state, "open");
44 assert_eq!(found.labels, vec!["bug".to_owned()]);
45 assert_eq!(found.assignees, vec![MemberId::new("jdc")]);
46}
47
48/// `git ents issue new` with no `--title`, run as the real binary with a
49/// fake `$EDITOR`: composes the title and body from the scratch file —
50/// first line title, remaining lines body, `#` lines stripped.
51// @relation(model.issue, roots.local, scope=function, role=Verifies)
52#[test]
53fn issue_new_composes_title_and_body_from_a_fake_editor() {
54 let fixture = common::Fixture::new(2);
55 let editor_path = fixture.path().join("fake-editor.sh");
56 write_fake_editor(
57 &editor_path,
58 "issue title from the editor\nfirst body line\nsecond body line\n# a stray comment line",
59 );
60
61 let output = Command::new(common::bin_path())
62 .current_dir(fixture.path())
63 .args(["issue", "new", "--state", "open", "--key"])
64 .arg(&fixture.key_path)
65 .env("GIT_EDITOR", &editor_path)
66 .env("EDITOR", &editor_path)
67 .output()
68 .expect("runs");
69 assert!(output.status.success(), "{output:?}");
70 let stdout = String::from_utf8(output.stdout).expect("utf8");
71 let id = stdout
72 .trim()
73 .strip_prefix("opened ")
74 .expect("prints \"opened <id>\"")
75 .to_owned();
76
77 let root = LocalRoot::open(fixture.path()).expect("opens");
78 let found = issue::show(&root, &id).expect("shows");
79 assert_eq!(found.title, "issue title from the editor");
80 assert_eq!(found.body, "first body line\nsecond body line");
81}
82
83/// An empty editor message (blank title after stripping `#` lines) aborts
84/// the command with a failing exit status, mirroring `git commit`'s own
85/// empty-message abort.
86// @relation(model.issue, roots.local, scope=function, role=Verifies)
87#[test]
88fn issue_new_aborts_on_an_empty_editor_message() {
89 let fixture = common::Fixture::new(3);
90 let editor_path = fixture.path().join("fake-editor.sh");
91 write_fake_editor(&editor_path, "# only a comment, no title");
92
93 let output = Command::new(common::bin_path())
94 .current_dir(fixture.path())
95 .args(["issue", "new", "--state", "open", "--key"])
96 .arg(&fixture.key_path)
97 .env("GIT_EDITOR", &editor_path)
98 .env("EDITOR", &editor_path)
99 .output()
100 .expect("runs");
101 assert!(
102 !output.status.success(),
103 "an empty title must abort issue creation: {output:?}"
104 );
105}
106
107/// `git ents issue list --porcelain` emits the stable record grammar
108/// (`lens.parity`, `model.issue`): full ids on a space-separated head
109/// line, `title`/`assignees`/`labels` keyed lines (empties omitted), the
110/// body tab-prefixed line by line, records blank-line separated.
111// @relation(lens.porcelain, lens.parity, model.issue, roots.local, scope=function, role=Verifies)
112#[test]
113fn issue_list_porcelain_emits_full_id_records() {
114 let fixture = common::Fixture::new(5);
115 let root = LocalRoot::open(fixture.path()).expect("opens");
116
117 let first = issue::new(
118 &root,
119 "gate rejects a valid signature".to_owned(),
120 "first body line\n\nthird body line".to_owned(),
121 "open".to_owned(),
122 vec!["bug".to_owned(), "gate".to_owned()],
123 vec!["jdc".to_owned()],
124 Some(fixture.key_path.clone()),
125 )
126 .expect("creates");
127 let second = issue::new(
128 &root,
129 "unlabeled".to_owned(),
130 "short".to_owned(),
131 "triaged".to_owned(),
132 vec![],
133 vec![],
134 Some(fixture.key_path.clone()),
135 )
136 .expect("creates");
137
138 let output = Command::new(common::bin_path())
139 .current_dir(fixture.path())
140 .args(["issue", "list", "--porcelain"])
141 .output()
142 .expect("runs");
143 assert!(output.status.success(), "{output:?}");
144 let stdout = String::from_utf8(output.stdout).expect("utf8");
145
146 let first_record = format!(
147 "{first} open\ntitle gate rejects a valid signature\nassignees jdc\nlabels bug, gate\n\tfirst body line\n\t\n\tthird body line\n"
148 );
149 let second_record = format!("{second} triaged\ntitle unlabeled\n\tshort\n");
150 // Listing order follows the refs' own (id-sorted) order.
151 let expected = if first < second {
152 format!("{first_record}\n{second_record}")
153 } else {
154 format!("{second_record}\n{first_record}")
155 };
156 assert_eq!(stdout, expected);
157}
158
159/// `git ents issue edit`: state, assignees, and labels round-trip through
160/// an edit on top of the issue's existing tip.
161// @relation(model.issue, roots.local, scope=function, role=Verifies)
162#[test]
163fn issue_edit_round_trips_state_assignees_and_labels() {
164 let fixture = common::Fixture::new(4);
165 let root = LocalRoot::open(fixture.path()).expect("opens");
166
167 let id = issue::new(
168 &root,
169 "title".to_owned(),
170 "body".to_owned(),
171 "open".to_owned(),
172 vec![],
173 vec![],
174 Some(fixture.key_path.clone()),
175 )
176 .expect("creates");
177
178 issue::edit(
179 &root,
180 &id,
181 Some("triaged".to_owned()),
182 vec!["bug".to_owned(), "gate".to_owned()],
183 vec!["jdc".to_owned(), "ci-worker".to_owned()],
184 Some(fixture.key_path.clone()),
185 )
186 .expect("edits");
187
188 let found = issue::show(&root, &id).expect("shows");
189 assert_eq!(found.state, "triaged");
190 assert_eq!(found.labels, vec!["bug".to_owned(), "gate".to_owned()]);
191 assert_eq!(
192 found.assignees,
193 vec![MemberId::new("jdc"), MemberId::new("ci-worker")]
194 );
195 // Title and body are untouched by an edit that names neither.
196 assert_eq!(found.title, "title");
197 assert_eq!(found.body, "body");
198}