upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/tests/ngit_pr_checkout.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-03-05 19:57:28 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-03-05 21:31:17 +0000
commit83b0886b97e2e90e328f91fcfaeb59726c93308f (patch)
tree7e5f17529608738c98dd45c8c01f61e3d12b1b32 /tests/ngit_pr_checkout.rs
parent84a197700cac6b2ef72cf0723474ac3185c5d1de (diff)
test(pr-checkout): replace broken ngit_list tests with ngit_pr_checkout
tests/ngit_list.rs had 27 tests all failing because the interactive mode they tested has been replaced by a non-interactive implementation. Replace the file with a stub documenting the coverage gaps and add tests/ngit_pr_checkout.rs covering the same proposal branch checkout logic via `ngit pr checkout <id>`, starting with the fresh-checkout case.
Diffstat (limited to 'tests/ngit_pr_checkout.rs')
-rw-r--r--tests/ngit_pr_checkout.rs536
1 files changed, 536 insertions, 0 deletions
diff --git a/tests/ngit_pr_checkout.rs b/tests/ngit_pr_checkout.rs
new file mode 100644
index 0000000..f7d7855
--- /dev/null
+++ b/tests/ngit_pr_checkout.rs
@@ -0,0 +1,536 @@
1use anyhow::Result;
2use futures::join;
3use serial_test::serial;
4use test_utils::{git::GitTestRepo, relay::Relay, *};
5
6/// Run `ngit pr list --json --offline` in `dir` and return the nevent id for
7/// the proposal whose branch-name matches `branch_name_in_event`.
8fn get_proposal_id_for_branch(dir: &std::path::Path, branch_name_in_event: &str) -> Result<String> {
9 let output = std::process::Command::new(assert_cmd::cargo::cargo_bin("ngit"))
10 .env("NGITTEST", "TRUE")
11 .current_dir(dir)
12 .args([
13 "--nsec",
14 TEST_KEY_1_NSEC,
15 "--password",
16 TEST_PASSWORD,
17 "--disable-cli-spinners",
18 "pr",
19 "list",
20 "--json",
21 "--offline",
22 ])
23 .output()?;
24 let stdout = String::from_utf8(output.stdout)?;
25 let proposals: Vec<serde_json::Value> = serde_json::from_str(&stdout)
26 .map_err(|e| anyhow::anyhow!("failed to parse pr list json: {e}\nstdout: {stdout}"))?;
27 let entry = proposals
28 .iter()
29 .find(|p| {
30 p["branch"]
31 .as_str()
32 .map(|b| b.starts_with(&format!("pr/{branch_name_in_event}(")))
33 .unwrap_or(false)
34 })
35 .ok_or_else(|| {
36 anyhow::anyhow!(
37 "no proposal found for branch {branch_name_in_event} in: {stdout}"
38 )
39 })?;
40 Ok(entry["id"].as_str().unwrap_or_default().to_string())
41}
42
43/// Run `ngit pr checkout --offline <id>` (cache must already be populated).
44fn run_pr_checkout(test_repo: &GitTestRepo, branch_name_in_event: &str) -> Result<()> {
45 run_pr_checkout_with_args(test_repo, branch_name_in_event, &["--offline"])
46}
47
48/// Run `ngit pr checkout --force --offline <id>` (cache must already be populated).
49fn run_pr_checkout_force(test_repo: &GitTestRepo, branch_name_in_event: &str) -> Result<()> {
50 run_pr_checkout_with_args(test_repo, branch_name_in_event, &["--force", "--offline"])
51}
52
53fn run_pr_checkout_with_args(
54 test_repo: &GitTestRepo,
55 branch_name_in_event: &str,
56 extra_args: &[&str],
57) -> Result<()> {
58 let proposal_id = get_proposal_id_for_branch(&test_repo.dir, branch_name_in_event)?;
59 let mut args = vec![
60 "--nsec",
61 TEST_KEY_1_NSEC,
62 "--password",
63 TEST_PASSWORD,
64 "--disable-cli-spinners",
65 "pr",
66 "checkout",
67 ];
68 args.extend_from_slice(extra_args);
69 args.push(&proposal_id);
70 // Use std::process::Command directly (not CliTester/rexpect) so that a
71 // non-zero exit code is reliably detected without PTY timeout issues.
72 let status = std::process::Command::new(assert_cmd::cargo::cargo_bin("ngit"))
73 .env("NGITTEST", "TRUE")
74 .current_dir(&test_repo.dir)
75 .args(&args)
76 .status()?;
77 if status.success() {
78 Ok(())
79 } else {
80 anyhow::bail!("ngit pr checkout exited with {status}")
81 }
82}
83
84/// Spin up the standard 5-relay set used by all tests in this file.
85/// Returns the five relay handles in port order (51,52,53,55,56).
86#[allow(clippy::type_complexity)]
87fn make_relays() -> (
88 Relay<'static>,
89 Relay<'static>,
90 Relay<'static>,
91 Relay<'static>,
92 Relay<'static>,
93) {
94 let mut r51 = Relay::new(8051, None, None);
95 let r52 = Relay::new(8052, None, None);
96 let r53 = Relay::new(8053, None, None);
97 let mut r55 = Relay::new(8055, None, None);
98 let r56 = Relay::new(8056, None, None);
99
100 r51.events.push(generate_test_key_1_relay_list_event());
101 r51.events.push(generate_test_key_1_metadata_event("fred"));
102 r51.events.push(generate_repo_ref_event());
103
104 r55.events.push(generate_repo_ref_event());
105 r55.events.push(generate_test_key_1_metadata_event("fred"));
106 r55.events.push(generate_test_key_1_relay_list_event());
107
108 (r51, r52, r53, r55, r56)
109}
110
111fn shutdown_relays() -> Result<()> {
112 for port in [51u64, 52, 53, 55, 56] {
113 relay::shutdown_relay(8000 + port)?;
114 }
115 Ok(())
116}
117
118mod when_proposal_branch_doesnt_exist {
119 use super::*;
120
121 async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> {
122 let (mut r51, mut r52, mut r53, mut r55, mut r56) = make_relays();
123
124 let cli_tester_handle =
125 std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
126 let originating_repo = cli_tester_create_proposals()?;
127
128 let test_repo = GitTestRepo::default();
129 test_repo.populate()?;
130
131 use_ngit_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1)?;
132
133 shutdown_relays()?;
134 Ok((originating_repo, test_repo))
135 });
136
137 let _ = join!(
138 r51.listen_until_close(),
139 r52.listen_until_close(),
140 r53.listen_until_close(),
141 r55.listen_until_close(),
142 r56.listen_until_close(),
143 );
144 cli_tester_handle.join().unwrap()
145 }
146
147 #[tokio::test]
148 #[serial]
149 async fn proposal_branch_created_with_correct_name() -> Result<()> {
150 let (_, test_repo) = prep_and_run().await?;
151 let expected_branch = get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?;
152 assert!(
153 test_repo.get_local_branch_names()?.contains(&expected_branch),
154 "expected branch {expected_branch} to exist"
155 );
156 Ok(())
157 }
158
159 #[tokio::test]
160 #[serial]
161 async fn proposal_branch_checked_out() -> Result<()> {
162 let (_, test_repo) = prep_and_run().await?;
163 assert_eq!(
164 get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?,
165 test_repo.get_checked_out_branch_name()?,
166 );
167 Ok(())
168 }
169
170 #[tokio::test]
171 #[serial]
172 async fn proposal_branch_tip_is_most_recent_patch() -> Result<()> {
173 let (originating_repo, test_repo) = prep_and_run().await?;
174 assert_eq!(
175 originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?,
176 test_repo.get_tip_of_local_branch(
177 &get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?
178 )?,
179 );
180 Ok(())
181 }
182}
183
184mod when_proposal_branch_exists_and_is_up_to_date {
185 use super::*;
186
187 async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> {
188 let (mut r51, mut r52, mut r53, mut r55, mut r56) = make_relays();
189
190 let cli_tester_handle =
191 std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
192 let originating_repo = cli_tester_create_proposals()?;
193
194 let test_repo = GitTestRepo::default();
195 test_repo.populate()?;
196
197 // first checkout creates the branch
198 use_ngit_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1)?;
199 test_repo.checkout("main")?;
200
201 // second checkout: branch already exists and is up to date
202 run_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1)?;
203
204 shutdown_relays()?;
205 Ok((originating_repo, test_repo))
206 });
207
208 let _ = join!(
209 r51.listen_until_close(),
210 r52.listen_until_close(),
211 r53.listen_until_close(),
212 r55.listen_until_close(),
213 r56.listen_until_close(),
214 );
215 cli_tester_handle.join().unwrap()
216 }
217
218 #[tokio::test]
219 #[serial]
220 async fn proposal_branch_checked_out() -> Result<()> {
221 let (_, test_repo) = prep_and_run().await?;
222 assert_eq!(
223 get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?,
224 test_repo.get_checked_out_branch_name()?,
225 );
226 Ok(())
227 }
228
229 #[tokio::test]
230 #[serial]
231 async fn proposal_branch_tip_unchanged() -> Result<()> {
232 let (originating_repo, test_repo) = prep_and_run().await?;
233 assert_eq!(
234 originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?,
235 test_repo.get_tip_of_local_branch(
236 &get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?
237 )?,
238 );
239 Ok(())
240 }
241}
242
243mod when_proposal_branch_exists_and_is_behind {
244 use super::*;
245
246 async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> {
247 let (mut r51, mut r52, mut r53, mut r55, mut r56) = make_relays();
248
249 let cli_tester_handle =
250 std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
251 let originating_repo = cli_tester_create_proposals()?;
252
253 let test_repo = GitTestRepo::default();
254 test_repo.populate()?;
255
256 use_ngit_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1)?;
257
258 // wind the local branch back one commit so it's behind
259 remove_latest_commit_so_proposal_branch_is_behind_and_checkout_main(&test_repo)?;
260
261 // checkout again — should fast-forward to the latest patch
262 run_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1)?;
263
264 shutdown_relays()?;
265 Ok((originating_repo, test_repo))
266 });
267
268 let _ = join!(
269 r51.listen_until_close(),
270 r52.listen_until_close(),
271 r53.listen_until_close(),
272 r55.listen_until_close(),
273 r56.listen_until_close(),
274 );
275 cli_tester_handle.join().unwrap()
276 }
277
278 #[tokio::test]
279 #[serial]
280 async fn proposal_branch_checked_out() -> Result<()> {
281 let (_, test_repo) = prep_and_run().await?;
282 assert_eq!(
283 get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?,
284 test_repo.get_checked_out_branch_name()?,
285 );
286 Ok(())
287 }
288
289 #[tokio::test]
290 #[serial]
291 async fn proposal_branch_tip_is_most_recent_patch() -> Result<()> {
292 let (originating_repo, test_repo) = prep_and_run().await?;
293 assert_eq!(
294 originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?,
295 test_repo.get_tip_of_local_branch(
296 &get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?
297 )?,
298 );
299 Ok(())
300 }
301}
302
303mod when_proposal_branch_has_local_amendments {
304 use super::*;
305
306 async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> {
307 let (mut r51, mut r52, mut r53, mut r55, mut r56) = make_relays();
308
309 let cli_tester_handle =
310 std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
311 let originating_repo = cli_tester_create_proposals()?;
312
313 let test_repo = GitTestRepo::default();
314 test_repo.populate()?;
315
316 use_ngit_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1)?;
317
318 // amend: remove the tip and add a different commit in its place
319 amend_last_commit(&test_repo, "add ammended-commit.md")?;
320 test_repo.checkout("main")?;
321
322 // checkout without --force should bail on diverged branch
323 assert!(
324 run_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1).is_err(),
325 "expected checkout to fail without --force on amended branch"
326 );
327
328 shutdown_relays()?;
329 Ok((originating_repo, test_repo))
330 });
331
332 let _ = join!(
333 r51.listen_until_close(),
334 r52.listen_until_close(),
335 r53.listen_until_close(),
336 r55.listen_until_close(),
337 r56.listen_until_close(),
338 );
339 cli_tester_handle.join().unwrap()
340 }
341
342 #[tokio::test]
343 #[serial]
344 async fn local_unpublished_commits_are_not_overwritten() -> Result<()> {
345 let (originating_repo, test_repo) = prep_and_run().await?;
346 // the local branch tip must differ from the published tip — local work preserved
347 assert_ne!(
348 originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?,
349 test_repo.get_tip_of_local_branch(
350 &get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?
351 )?,
352 );
353 Ok(())
354 }
355}
356
357mod when_proposal_branch_has_local_commits_on_top {
358 use super::*;
359
360 async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> {
361 let (mut r51, mut r52, mut r53, mut r55, mut r56) = make_relays();
362
363 let cli_tester_handle =
364 std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
365 let originating_repo = cli_tester_create_proposals()?;
366
367 let test_repo = GitTestRepo::default();
368 test_repo.populate()?;
369
370 use_ngit_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1)?;
371
372 // add an extra local commit on top of the proposal branch
373 std::fs::write(test_repo.dir.join("local-extra.md"), "local work")?;
374 test_repo.stage_and_commit("add local-extra.md")?;
375 test_repo.checkout("main")?;
376
377 // checkout again — should not discard the extra local commit
378 run_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1)?;
379
380 shutdown_relays()?;
381 Ok((originating_repo, test_repo))
382 });
383
384 let _ = join!(
385 r51.listen_until_close(),
386 r52.listen_until_close(),
387 r53.listen_until_close(),
388 r55.listen_until_close(),
389 r56.listen_until_close(),
390 );
391 cli_tester_handle.join().unwrap()
392 }
393
394 #[tokio::test]
395 #[serial]
396 async fn proposal_branch_checked_out() -> Result<()> {
397 let (_, test_repo) = prep_and_run().await?;
398 assert_eq!(
399 get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?,
400 test_repo.get_checked_out_branch_name()?,
401 );
402 Ok(())
403 }
404
405 #[tokio::test]
406 #[serial]
407 async fn local_commits_are_not_discarded() -> Result<()> {
408 let (originating_repo, test_repo) = prep_and_run().await?;
409 let branch = get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?;
410 let local_tip = test_repo.get_tip_of_local_branch(&branch)?;
411 let published_tip = originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?;
412 // local tip must be ahead of (not equal to) the published tip
413 assert_ne!(local_tip, published_tip, "local commits were discarded");
414 // and the published tip must be an ancestor of the local tip
415 assert!(
416 test_repo
417 .git_repo
418 .graph_descendant_of(local_tip, published_tip)?,
419 "local branch is not descended from published tip"
420 );
421 Ok(())
422 }
423}
424
425mod when_newer_revision_rebases_proposal {
426 use super::*;
427
428 async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> {
429 let (mut r51, mut r52, mut r53, mut r55, mut r56) = make_relays();
430
431 let cli_tester_handle =
432 std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
433 let (new_originating_repo, test_repo) =
434 create_proposals_with_rebased_first_proposal()?;
435
436 // refresh test_repo cache so it sees the new rebased revision
437 let mut p = CliTester::new_from_dir(
438 &test_repo.dir,
439 [
440 "--nsec",
441 TEST_KEY_1_NSEC,
442 "--password",
443 TEST_PASSWORD,
444 "--disable-cli-spinners",
445 "pr",
446 "list",
447 ],
448 );
449 p.expect_end_eventually()?;
450
451 // checkout without --force should bail on diverged branch
452 assert!(
453 run_pr_checkout(&test_repo, FEATURE_BRANCH_NAME_1).is_err(),
454 "expected checkout to fail without --force on diverged branch"
455 );
456 // checkout with --force should update to the new rebased revision
457 // (relays still needed for the fetch inside checkout)
458 run_pr_checkout_force(&test_repo, FEATURE_BRANCH_NAME_1)?;
459
460 shutdown_relays()?;
461 Ok((new_originating_repo, test_repo))
462 });
463
464 let _ = join!(
465 r51.listen_until_close(),
466 r52.listen_until_close(),
467 r53.listen_until_close(),
468 r55.listen_until_close(),
469 r56.listen_until_close(),
470 );
471 cli_tester_handle.join().unwrap()
472 }
473
474 #[tokio::test]
475 #[serial]
476 async fn proposal_branch_checked_out() -> Result<()> {
477 let (_, test_repo) = prep_and_run().await?;
478 assert_eq!(
479 get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?,
480 test_repo.get_checked_out_branch_name()?,
481 );
482 Ok(())
483 }
484
485 #[tokio::test]
486 #[serial]
487 async fn proposal_branch_tip_is_most_recent_revision_tip() -> Result<()> {
488 let (new_originating_repo, test_repo) = prep_and_run().await?;
489 assert_eq!(
490 new_originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?,
491 test_repo.get_tip_of_local_branch(
492 &get_proposal_branch_name(&test_repo, FEATURE_BRANCH_NAME_1)?
493 )?,
494 );
495 Ok(())
496 }
497}
498
499/// Creates 3 proposals, checks out proposal 1 in a test repo, then publishes
500/// a rebased revision of proposal 1 from a second originating repo. Returns
501/// (new_originating_repo, test_repo) with the test repo still on the old branch.
502fn create_proposals_with_rebased_first_proposal(
503) -> Result<(GitTestRepo, GitTestRepo)> {
504 // create the initial 3 proposals and check out proposal 1 in a test repo
505 let (_, test_repo) =
506 create_proposals_and_repo_with_proposal_branch_checked_out(FEATURE_BRANCH_NAME_1)?;
507
508 // get the original proposal id to use as in_reply_to for the rebased revision
509 let original_proposal_id =
510 get_proposal_id_for_branch(&test_repo.dir, FEATURE_BRANCH_NAME_1)?;
511
512 // publish a rebased revision of proposal 1 from a second originating repo
513 let second_originating_repo = GitTestRepo::default();
514 second_originating_repo.populate()?;
515 std::fs::write(
516 second_originating_repo.dir.join("amazing.md"),
517 "some content",
518 )?;
519 second_originating_repo.stage_and_commit("commit for rebasing on top of")?;
520 cli_tester_create_proposal(
521 &second_originating_repo,
522 FEATURE_BRANCH_NAME_1,
523 "a",
524 Some((PROPOSAL_TITLE_1, "proposal a description")),
525 Some(original_proposal_id),
526 )?;
527
528 // simulate the test repo having pulled the updated main branch
529 let branch_name = test_repo.get_checked_out_branch_name()?;
530 test_repo.checkout("main")?;
531 std::fs::write(test_repo.dir.join("amazing.md"), "some content")?;
532 test_repo.stage_and_commit("commit for rebasing on top of")?;
533 test_repo.checkout(&branch_name)?;
534
535 Ok((second_originating_repo, test_repo))
536}