1use crate::canonical::compute_id;
3use crate::store::Store;
4use crate::tick::{Check, Ground, Tick};
5use std::path::Path;
6use std::process::Command;
7
8#[derive(Default)]
9struct DraftGround {
10 claim: String,
11 supports: String, revisit: Option<String>,
13 test_ref: Option<String>,
14 counter_test: Option<String>,
15 platforms: Vec<String>,
16 triggered_by: Vec<String>,
17 surfaces: Vec<String>,
18}
19
20fn need(args: &[String], i: usize, flag: &str) -> Result<String, String> {
21 args.get(i + 1)
22 .cloned()
23 .ok_or(format!("{flag} requires a value"))
24}
25
26fn last<'a>(g: &'a mut [DraftGround], flag: &str) -> Result<&'a mut DraftGround, String> {
27 g.last_mut()
28 .ok_or(format!("{flag} has no preceding --assume/--reject ground"))
29}
30
31pub(crate) fn resolve_blame(repo: &Path, blame_override: Option<String>) -> Result<String, String> {
33 if let Some(b) = blame_override {
34 let b = b.trim();
35 if b.is_empty() {
36 return Err("--blame must be non-empty".into());
37 }
38 return Ok(b.to_string());
39 }
40 let out = Command::new("git")
41 .arg("config")
42 .arg("user.name")
43 .current_dir(repo)
44 .output()
45 .map_err(|e| format!("cannot run git: {e}"))?;
46 let name = String::from_utf8_lossy(&out.stdout).trim().to_string();
47 if name.is_empty() {
48 return Err("no author: pass --blame, or set git config user.name".into());
49 }
50 Ok(name)
51}
52
53pub(crate) fn resolve_sha(repo: &Path, sha_override: &Option<String>) -> Result<String, String> {
54 let sha = match sha_override {
55 Some(s) => s.trim().to_string(),
56 None => {
57 let out = std::process::Command::new("git")
58 .args(["rev-parse", "HEAD"])
59 .current_dir(repo)
60 .output()
61 .map_err(|e| format!("cannot run git: {e}"))?;
62 if !out.status.success() {
63 return Err(
64 "cannot resolve verified_at_sha (not a git repo?) — pass --verified-at-sha"
65 .into(),
66 );
67 }
68 String::from_utf8_lossy(&out.stdout).trim().to_string()
69 }
70 };
71 if !crate::tick::is_40_lower_hex(&sha) {
72 return Err(format!("verified_at_sha must be 40 lowercase hex: {sha}"));
73 }
74 Ok(sha)
75}
76
77fn t_grounds_text(grounds: &[Ground]) -> Vec<String> {
78 grounds.iter().map(|g| g.claim.clone()).collect()
79}
80
81fn git_show(repo: &Path, fmt: &str, commit: &str) -> Result<String, String> {
84 let out = Command::new("git")
85 .args(["show", "-s", fmt, commit])
86 .current_dir(repo)
87 .output()
88 .map_err(|e| format!("cannot run git: {e}"))?;
89 if !out.status.success() {
90 return Err(format!("decide: cannot read commit {commit}"));
91 }
92 Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
93}
94
95struct Envelope {
99 subject: String,
100 author: String,
101 refs: Vec<String>,
102}
103
104const SUBJECT_ROLES: &[&str] = &["Dev", "QA", "Product", "Mac", "User"];
106
107fn subject_role(subject: &str) -> Option<&'static str> {
110 let head = subject.split_whitespace().next()?;
111 let word = head.strip_suffix(':')?;
112 SUBJECT_ROLES
113 .iter()
114 .find(|r| r.eq_ignore_ascii_case(word))
115 .copied()
116}
117
118fn subject_refs(subject: &str) -> Vec<String> {
121 subject
122 .split_whitespace()
123 .filter(|tok| {
124 let rest = tok
125 .strip_prefix('#')
126 .or_else(|| tok.strip_prefix('R'))
127 .or_else(|| tok.strip_prefix('r'));
128 matches!(rest, Some(d) if !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit()))
129 })
130 .map(|t| t.to_string())
131 .collect()
132}
133
134fn read_envelope(repo: &Path, commit: &str) -> Result<Envelope, String> {
135 let subject = git_show(repo, "--format=%s", commit)?;
136 let author = git_show(repo, "--format=%an", commit)?;
137 let body = git_show(repo, "--format=%b", commit)?;
138 let refs = body
139 .lines()
140 .map(str::trim)
141 .filter(|l| l.starts_with("Refs #"))
142 .map(|l| l.to_string())
143 .collect();
144 Ok(Envelope {
145 subject,
146 author,
147 refs,
148 })
149}
150
151pub(crate) fn validate_authority(val: &str) -> Result<(), String> {
153 if val == "user-ruled" || val == "agent-disposable" {
154 Ok(())
155 } else {
156 Err("authority must be user-ruled or agent-disposable".into())
157 }
158}
159
160pub fn harvested_test_check(
168 reference: String,
169 verified_at_sha: String,
170 platforms: Vec<String>,
171 triggered_by: Vec<String>,
172 surfaces: Vec<String>,
173) -> Result<Check, String> {
174 use crate::tick::Liveness;
175 if reference.trim().is_empty() {
176 return Err("a harvested binding requires a non-empty test reference".into());
177 }
178 if !crate::tick::is_40_lower_hex(&verified_at_sha) {
179 return Err(format!(
180 "verified_at_sha must be 40 lowercase hex: {verified_at_sha}"
181 ));
182 }
183 if platforms.is_empty() || triggered_by.is_empty() || surfaces.is_empty() {
184 return Err(
185 "a harvested binding requires at least one platform, triggered-by, and surface (no half-harvest)"
186 .into(),
187 );
188 }
189 Ok(Check::Test {
190 reference,
191 verified_at_sha,
192 counter_test: None, liveness: Liveness {
194 platforms,
195 triggered_by,
196 surfaces,
197 },
198 })
199}
200
201pub struct Decision {
206 pub observe: String,
207 pub decision: String,
208 pub grounds: Vec<Ground>,
209 pub blame: String,
210 pub authority: Option<String>,
211 pub jurisdiction: Option<String>,
212 pub round_id: Option<String>,
213}
214
215pub fn append(repo: &Path, d: Decision) -> Result<Tick, String> {
221 for field in std::iter::once(d.decision.clone())
222 .chain(std::iter::once(d.observe.clone()))
223 .chain(t_grounds_text(&d.grounds))
224 {
225 for verb in crate::lint::r3_self_evolve(&field) {
226 eprintln!("warning: \"{verb}\" should take a human subject, not the system (best-effort lint; a re-wording evades it)");
227 }
228 }
229 let store = Store::at(repo);
230 if !store.exists() {
231 return Err("no .evolving/ store here — run `ev init` first".into());
232 }
233 let parent_id = store
234 .read_head()
235 .map_err(|e| format!("reading HEAD: {e}"))?;
236 let held_since = time::OffsetDateTime::now_utc()
237 .format(&time::format_description::well_known::Rfc3339)
238 .map_err(|e| format!("timestamp: {e}"))?;
239 let mut t = Tick {
240 id: String::new(),
241 parent_id,
242 observe: d.observe,
243 decision: d.decision,
244 grounds: d.grounds,
245 status: "live".into(),
246 held_since,
247 blame: d.blame,
248 authority: d.authority,
249 jurisdiction: d.jurisdiction,
250 round_id: d.round_id,
251 };
252 t.id = compute_id(&t);
253 store
254 .write_tick(&t)
255 .map_err(|e| format!("writing tick: {e}"))?;
256 Ok(t)
257}
258
259fn build_ground(
260 repo: &Path,
261 d: DraftGround,
262 sha_override: &Option<String>,
263) -> Result<Ground, String> {
264 use crate::tick::Liveness;
265 if d.claim.is_empty() {
266 return Err("ground claim is empty".into());
267 }
268 if d.supports.starts_with("rejected:") && (d.test_ref.is_some() || d.revisit.is_some()) {
269 return Err("a road-not-taken (rejected) ground cannot carry a check in 0.1.0 — reserved for a future rejection-rationale liveness feature".into());
270 }
271 if d.revisit.is_some() && d.test_ref.is_some() {
272 return Err("a ground cannot be both --revisit and --assume-test (R2)".into());
273 }
274 let has_test_fields = d.counter_test.is_some()
275 || !d.platforms.is_empty()
276 || !d.triggered_by.is_empty()
277 || !d.surfaces.is_empty();
278 let check = match (d.test_ref, d.revisit) {
279 (Some(reference), _) => {
280 let counter_test = d
281 .counter_test
282 .ok_or("a test binding requires --counter-test (no vacuous binding)".to_string())?;
283 if counter_test.trim().is_empty() {
284 return Err("a test binding requires --counter-test (no vacuous binding)".into());
287 }
288 if d.platforms.is_empty() || d.triggered_by.is_empty() || d.surfaces.is_empty() {
289 return Err("a test binding requires at least one --on-platform, --triggered-by, and --surface".into());
290 }
291 let verified_at_sha = resolve_sha(repo, sha_override)?;
292 Some(Check::Test {
293 reference,
294 verified_at_sha,
295 counter_test: Some(counter_test),
296 liveness: Liveness {
297 platforms: d.platforms,
298 triggered_by: d.triggered_by,
299 surfaces: d.surfaces,
300 },
301 })
302 }
303 (None, Some(when)) => {
304 if has_test_fields {
305 return Err(
306 "--counter-test/--on-platform/--triggered-by/--surface require --assume-test"
307 .into(),
308 );
309 }
310 Some(Check::Person { reference: when })
311 }
312 (None, None) => {
313 if has_test_fields {
314 return Err(
315 "--counter-test/--on-platform/--triggered-by/--surface require --assume-test"
316 .into(),
317 );
318 }
319 None
320 }
321 };
322 Ok(Ground {
323 claim: d.claim,
324 supports: d.supports,
325 check,
326 })
327}
328
329pub fn run(repo: &Path, decision: Option<&str>, args: &[String]) -> Result<Tick, String> {
330 let mut observe = String::new();
331 let mut blame_override: Option<String> = None;
332 let mut sha_override: Option<String> = None;
333 let mut authority: Option<String> = None;
334 let mut jurisdiction: Option<String> = None;
335 let mut round_id: Option<String> = None;
336 let mut from_git: Option<String> = None;
337 let mut drafts: Vec<DraftGround> = Vec::new();
338 let mut i = 0;
339 while i < args.len() {
340 let flag = args[i].clone();
341 match flag.as_str() {
342 "--from-git" => {
343 from_git = Some(need(args, i, &flag)?);
344 }
345 "--observe" => {
346 observe = need(args, i, &flag)?;
347 }
348 "--blame" => {
349 blame_override = Some(need(args, i, &flag)?);
350 }
351 "--verified-at-sha" => {
352 sha_override = Some(need(args, i, &flag)?);
353 }
354 "--authority" => {
355 let v = need(args, i, &flag)?;
356 validate_authority(&v)?;
357 authority = Some(v);
358 }
359 "--jurisdiction" => {
360 let v = need(args, i, &flag)?;
361 crate::tick::validate_jurisdiction(&v)?;
362 jurisdiction = Some(v);
363 }
364 "--round-id" => {
365 let v = need(args, i, &flag)?;
367 if v.is_empty() {
368 return Err("--round-id needs a non-empty value".into());
369 }
370 round_id = Some(v);
371 }
372 "--reject" => {
373 let v = need(args, i, &flag)?;
374 let (opt, why) = v
375 .split_once(':')
376 .ok_or("--reject expects \"<option>: <why>\"".to_string())?;
377 let (opt, why) = (opt.trim(), why.trim());
378 if opt.is_empty() || why.is_empty() {
379 return Err("--reject needs non-empty <option> and <why>".into());
380 }
381 drafts.push(DraftGround {
382 claim: why.into(),
383 supports: format!("rejected:{opt}"),
384 ..Default::default()
385 });
386 }
387 "--assume" => {
388 let claim = need(args, i, &flag)?;
389 drafts.push(DraftGround {
390 claim,
391 supports: "chosen".into(),
392 ..Default::default()
393 });
394 }
395 "--revisit" => {
396 last(&mut drafts, &flag)?.revisit = Some(need(args, i, &flag)?);
397 }
398 "--assume-test" => {
399 last(&mut drafts, &flag)?.test_ref = Some(need(args, i, &flag)?);
400 }
401 "--counter-test" => {
402 last(&mut drafts, &flag)?.counter_test = Some(need(args, i, &flag)?);
403 }
404 "--on-platform" => {
405 let v = need(args, i, &flag)?;
406 last(&mut drafts, &flag)?.platforms.push(v);
407 }
408 "--triggered-by" => {
409 let v = need(args, i, &flag)?;
410 last(&mut drafts, &flag)?.triggered_by.push(v);
411 }
412 "--surface" => {
413 let v = need(args, i, &flag)?;
414 last(&mut drafts, &flag)?.surfaces.push(v);
415 }
416 other => return Err(format!("decide: unknown flag {other}")),
417 }
418 i += 2;
419 }
420
421 let (decision, observe) = match (decision, &from_git) {
425 (Some(_), Some(_)) => {
426 return Err("decide: decision given twice (positional and --from-git)".into())
427 }
428 (None, None) => return Err("decide: needs a decision (positional) or --from-git".into()),
429 (Some(d), None) => (d.to_string(), observe),
430 (None, Some(commit)) => {
431 let env = read_envelope(repo, commit)?;
432 if blame_override.is_none() {
435 blame_override = Some(match subject_role(&env.subject) {
436 Some(role) => role.to_string(),
437 None => env.author,
438 });
439 }
440 let observe = std::iter::once(observe)
442 .chain(subject_refs(&env.subject))
443 .chain(env.refs)
444 .filter(|s| !s.is_empty())
445 .collect::<Vec<_>>()
446 .join(" ");
447 (env.subject, observe)
448 }
449 };
450 if decision.trim().is_empty() {
451 return Err("decision text is empty".into());
452 }
453 let blame = resolve_blame(repo, blame_override)?;
454 let mut grounds = Vec::new();
455 for d in drafts {
456 grounds.push(build_ground(repo, d, &sha_override)?);
457 }
458 append(
461 repo,
462 Decision {
463 observe,
464 decision: decision.to_string(),
465 grounds,
466 blame,
467 authority,
468 jurisdiction,
469 round_id,
470 },
471 )
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use crate::tick::Check;
478
479 fn repo() -> std::path::PathBuf {
480 use std::sync::atomic::{AtomicU64, Ordering};
481 static N: AtomicU64 = AtomicU64::new(0);
482 let p = std::env::temp_dir().join(format!(
483 "ev-capture-{}-{}",
484 std::process::id(),
485 N.fetch_add(1, Ordering::Relaxed)
486 ));
487 let _ = std::fs::remove_dir_all(&p);
488 std::fs::create_dir_all(&p).unwrap();
489 Store::at(&p).init().unwrap();
490 p
491 }
492 fn s(v: &[&str]) -> Vec<String> {
493 v.iter().map(|x| x.to_string()).collect()
494 }
495
496 #[test]
497 fn decide_should_record_a_chosen_a_revisit_and_a_rejected_road_when_all_are_passed() {
498 let r = repo();
500
501 let t = run(
503 &r,
504 Some("build our own retrieval; reject pgvector"),
505 &s(&[
506 "--observe",
507 "evaluating backend",
508 "--assume",
509 "team has bandwidth long-term",
510 "--revisit",
511 "Q3 review",
512 "--reject",
513 "pgvector: would lock our schema",
514 "--blame",
515 "Wang Yu",
516 ]),
517 )
518 .expect("ok");
519
520 assert_eq!(t.grounds.len(), 2);
522 assert!(matches!(t.grounds[0].check, Some(Check::Person { .. })));
523 assert_eq!(t.grounds[1].supports, "rejected:pgvector");
524 assert_eq!(t.blame, "Wang Yu");
525 assert_eq!(Store::at(&r).read_head().unwrap(), t.id);
526 }
527
528 #[test]
529 fn decide_should_stamp_held_since_with_a_nonempty_rfc3339_time_when_recording() {
530 let r = repo();
532
533 run(&r, Some("ship it"), &s(&["--blame", "Wang Yu"])).expect("ok");
535
536 let head = Store::at(&r).read_head().unwrap();
538 let tick = Store::at(&r).read_tick(&head).unwrap().unwrap();
539 assert!(!tick.held_since.is_empty());
540 time::OffsetDateTime::parse(
541 &tick.held_since,
542 &time::format_description::well_known::Rfc3339,
543 )
544 .expect("held_since parses as RFC 3339");
545 }
546
547 #[test]
548 fn decide_should_store_a_trimmed_blame_when_the_blame_is_padded() {
549 let r = repo();
551
552 let t = run(
554 &r,
555 Some("d"),
556 &s(&["--assume", "c", "--blame", " Wang Yu "]),
557 )
558 .expect("ok");
559
560 assert_eq!(t.blame, "Wang Yu");
562 }
563
564 #[test]
565 fn decide_should_refuse_the_ground_when_it_is_both_revisit_and_assume_test() {
566 let r = repo();
568
569 let e = run(
571 &r,
572 Some("d"),
573 &s(&[
574 "--assume",
575 "c",
576 "--revisit",
577 "Q3",
578 "--assume-test",
579 "pytest x",
580 "--blame",
581 "Wang Yu",
582 ]),
583 );
584
585 assert!(e.is_err());
587 }
588
589 #[test]
590 fn decide_should_refuse_a_check_when_the_ground_is_a_rejected_road() {
591 let r = repo();
593
594 let e = run(
596 &r,
597 Some("d"),
598 &s(&[
599 "--reject",
600 "pgvector: would lock our schema",
601 "--assume-test",
602 "pytest x",
603 "--counter-test",
604 "ct",
605 "--on-platform",
606 "linux-ci",
607 "--triggered-by",
608 "f",
609 "--surface",
610 "s",
611 "--verified-at-sha",
612 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
613 "--blame",
614 "Wang Yu",
615 ]),
616 );
617
618 assert!(e.is_err());
620 }
621
622 #[test]
623 fn decide_should_error_when_there_is_no_store() {
624 let p = std::env::temp_dir().join(format!("ev-nostore-{}", std::process::id()));
626 let _ = std::fs::remove_dir_all(&p);
627 std::fs::create_dir_all(&p).unwrap();
628
629 let e = run(&p, Some("d"), &s(&["--blame", "x"]));
631
632 assert!(e.is_err());
634 }
635
636 #[test]
637 fn decide_should_build_a_self_verifying_test_binding_when_all_test_fields_are_present() {
638 let r = repo();
640
641 let t = run(
643 &r,
644 Some("restore-safety counter DB-backed; reject Redis"),
645 &s(&[
646 "--assume",
647 "Argus introduces no Redis; multi-pod coord via existing DB",
648 "--assume-test",
649 "pytest tests/test_redis_absent.py",
650 "--counter-test",
651 "pytest tests/test_redis_absent.py::test_redis_injection_flips_red",
652 "--on-platform",
653 "linux-ci",
654 "--triggered-by",
655 "pyproject.toml",
656 "--surface",
657 "pyproject-deps",
658 "--verified-at-sha",
659 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
660 "--reject",
661 "Redis: a new infra dependency",
662 "--blame",
663 "Wang Yu",
664 ]),
665 )
666 .expect("ok");
667
668 match &t.grounds[0].check {
670 Some(Check::Test {
671 reference,
672 counter_test,
673 liveness,
674 verified_at_sha,
675 }) => {
676 assert_eq!(reference, "pytest tests/test_redis_absent.py");
677 assert!(counter_test
678 .as_deref()
679 .is_some_and(|c| c.contains("flips_red")));
680 assert_eq!(liveness.platforms, vec!["linux-ci".to_string()]);
681 assert_eq!(verified_at_sha.len(), 40);
682 }
683 _ => panic!("expected a test check"),
684 }
685 }
686
687 #[test]
688 fn decide_should_reject_a_test_binding_when_there_is_no_counter_test() {
689 let r = repo();
691
692 let e = run(
694 &r,
695 Some("d"),
696 &s(&[
697 "--assume",
698 "c",
699 "--assume-test",
700 "pytest x",
701 "--on-platform",
702 "linux-ci",
703 "--triggered-by",
704 "f",
705 "--surface",
706 "s",
707 "--verified-at-sha",
708 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
709 "--blame",
710 "Wang Yu",
711 ]),
712 );
713
714 assert!(e.is_err());
716 }
717
718 #[test]
719 fn decide_should_reject_a_test_binding_when_the_counter_test_is_empty() {
720 let r = repo();
722
723 let e = run(
725 &r,
726 Some("d"),
727 &s(&[
728 "--assume",
729 "c",
730 "--assume-test",
731 "pytest x",
732 "--counter-test",
733 "",
734 "--on-platform",
735 "linux-ci",
736 "--triggered-by",
737 "f",
738 "--surface",
739 "s",
740 "--verified-at-sha",
741 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
742 "--blame",
743 "Wang Yu",
744 ]),
745 );
746
747 assert!(e.is_err());
749 }
750
751 #[test]
752 fn decide_should_reject_a_test_binding_when_there_is_no_verified_at_sha_and_no_git() {
753 let r = repo();
755
756 let e = run(
758 &r,
759 Some("d"),
760 &s(&[
761 "--assume",
762 "c",
763 "--assume-test",
764 "pytest x",
765 "--counter-test",
766 "ct",
767 "--on-platform",
768 "linux-ci",
769 "--triggered-by",
770 "f",
771 "--surface",
772 "s",
773 "--blame",
774 "Wang Yu",
775 ]),
776 );
777
778 assert!(e.is_err());
780 }
781
782 #[test]
783 fn migrate_bind_should_build_a_harvested_test_check_when_no_counter_test() {
784 let check = harvested_test_check(
787 "pytest tests/test_invariant_no_redis.py".into(),
788 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
789 vec!["linux-ci".into()],
790 vec!["pyproject.toml".into()],
791 vec!["pyproject-deps".into()],
792 )
793 .expect("the full liveness is present, so the harvested binding is well-formed");
794
795 match check {
797 Check::Test {
798 reference,
799 counter_test,
800 liveness,
801 verified_at_sha,
802 } => {
803 assert_eq!(reference, "pytest tests/test_invariant_no_redis.py");
804 assert_eq!(counter_test, None); assert_eq!(liveness.platforms, vec!["linux-ci".to_string()]);
806 assert_eq!(liveness.triggered_by, vec!["pyproject.toml".to_string()]);
807 assert_eq!(liveness.surfaces, vec!["pyproject-deps".to_string()]);
808 assert_eq!(verified_at_sha.len(), 40);
809 }
810 _ => panic!("expected a harvested test check"),
811 }
812 }
813
814 #[test]
815 fn migrate_bind_should_reject_a_harvested_binding_when_a_liveness_key_is_missing() {
816 let e = harvested_test_check(
818 "pytest x".into(),
819 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
820 vec!["linux-ci".into()],
821 vec!["pyproject.toml".into()],
822 vec![], );
824
825 assert!(e.is_err());
827 }
828
829 #[test]
830 fn decide_should_still_error_without_a_counter_test() {
831 let r = repo();
835
836 let e = run(
838 &r,
839 Some("d"),
840 &s(&[
841 "--assume",
842 "c",
843 "--assume-test",
844 "pytest x",
845 "--on-platform",
846 "linux-ci",
847 "--triggered-by",
848 "f",
849 "--surface",
850 "s",
851 "--verified-at-sha",
852 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
853 "--blame",
854 "Wang Yu",
855 ]),
856 );
857
858 assert!(e.is_err());
860 }
861
862 #[test]
863 fn append_should_compute_the_frozen_genesis_id_when_given_the_genesis_decision() {
864 let r = repo();
867 let d = Decision {
868 observe: "evaluating retrieval backend".into(),
869 decision: "freeze the retrieval schema for v2".into(),
870 grounds: vec![
871 Ground {
872 claim: "team still wants a frozen schema".into(),
873 supports: "chosen".into(),
874 check: Some(Check::Person {
875 reference: "Q3 infra review".into(),
876 }),
877 },
878 Ground {
879 claim: "pgvector would lock our schema".into(),
880 supports: "rejected:pgvector".into(),
881 check: None,
882 },
883 ],
884 blame: "Wang Yu".into(),
885 authority: None,
886 jurisdiction: None,
887 round_id: None,
888 };
889
890 let t = append(&r, d).expect("ok");
892
893 assert_eq!(t.id, "e2b337f53a1f");
896 assert_eq!(t.parent_id, "");
897 assert_eq!(Store::at(&r).read_head().unwrap(), t.id);
898 }
899
900 #[test]
901 fn decide_should_take_blame_from_git_config_when_no_blame_flag_is_given() {
902 let r = repo();
904 for a in [
905 ["init"].as_slice(),
906 ["config", "user.name", "Ada Lovelace"].as_slice(),
907 ] {
908 std::process::Command::new("git")
909 .args(a)
910 .current_dir(&r)
911 .output()
912 .unwrap();
913 }
914
915 let t = run(&r, Some("d"), &s(&["--assume", "c"])).expect("ok");
917
918 assert_eq!(t.blame, "Ada Lovelace");
920 }
921}