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 source_ref: Option<serde_json::Value>,
213 pub provenance: Option<String>,
214}
215
216pub fn append(repo: &Path, d: Decision) -> Result<Tick, String> {
222 for field in std::iter::once(d.decision.clone())
223 .chain(std::iter::once(d.observe.clone()))
224 .chain(t_grounds_text(&d.grounds))
225 {
226 for verb in crate::lint::r3_self_evolve(&field) {
227 eprintln!("warning: \"{verb}\" should take a human subject, not the system (best-effort lint; a re-wording evades it)");
228 }
229 }
230 let store = Store::at(repo);
231 if !store.exists() {
232 return Err("no .evolving/ store here — run `ev init` first".into());
233 }
234 let parent_id = store
235 .read_head()
236 .map_err(|e| format!("reading HEAD: {e}"))?;
237 let held_since = time::OffsetDateTime::now_utc()
238 .format(&time::format_description::well_known::Rfc3339)
239 .map_err(|e| format!("timestamp: {e}"))?;
240 let mut t = Tick {
241 id: String::new(),
242 parent_id,
243 observe: d.observe,
244 decision: d.decision,
245 grounds: d.grounds,
246 status: "live".into(),
247 held_since,
248 blame: d.blame,
249 authority: d.authority,
250 jurisdiction: d.jurisdiction,
251 source_ref: d.source_ref,
252 provenance: d.provenance,
253 };
254 t.id = compute_id(&t);
255 store
256 .write_tick(&t)
257 .map_err(|e| format!("writing tick: {e}"))?;
258 Ok(t)
259}
260
261fn build_ground(
262 repo: &Path,
263 d: DraftGround,
264 sha_override: &Option<String>,
265) -> Result<Ground, String> {
266 use crate::tick::Liveness;
267 if d.claim.is_empty() {
268 return Err("ground claim is empty".into());
269 }
270 if d.supports.starts_with("rejected:") && (d.test_ref.is_some() || d.revisit.is_some()) {
271 return Err("a road-not-taken (rejected) ground cannot carry a check".into());
272 }
273 if d.revisit.is_some() && d.test_ref.is_some() {
274 return Err("a ground cannot be both --revisit and --assume-test (R2)".into());
275 }
276 let has_test_fields = d.counter_test.is_some()
277 || !d.platforms.is_empty()
278 || !d.triggered_by.is_empty()
279 || !d.surfaces.is_empty();
280 let check = match (d.test_ref, d.revisit) {
281 (Some(reference), _) => {
282 let counter_test = d
283 .counter_test
284 .ok_or("a test binding requires --counter-test (no vacuous binding)".to_string())?;
285 if counter_test.trim().is_empty() {
286 return Err("a test binding requires --counter-test (no vacuous binding)".into());
289 }
290 if d.platforms.is_empty() || d.triggered_by.is_empty() || d.surfaces.is_empty() {
291 return Err("a test binding requires at least one --on-platform, --triggered-by, and --surface".into());
292 }
293 let verified_at_sha = resolve_sha(repo, sha_override)?;
294 Some(Check::Test {
295 reference,
296 verified_at_sha,
297 counter_test: Some(counter_test),
298 liveness: Liveness {
299 platforms: d.platforms,
300 triggered_by: d.triggered_by,
301 surfaces: d.surfaces,
302 },
303 })
304 }
305 (None, Some(when)) => {
306 if has_test_fields {
307 return Err(
308 "--counter-test/--on-platform/--triggered-by/--surface require --assume-test"
309 .into(),
310 );
311 }
312 Some(Check::Person { reference: when })
313 }
314 (None, None) => {
315 if has_test_fields {
316 return Err(
317 "--counter-test/--on-platform/--triggered-by/--surface require --assume-test"
318 .into(),
319 );
320 }
321 None
322 }
323 };
324 Ok(Ground {
325 claim: d.claim,
326 supports: d.supports,
327 check,
328 })
329}
330
331pub fn run(repo: &Path, decision: Option<&str>, args: &[String]) -> Result<Tick, String> {
332 let mut observe = String::new();
333 let mut blame_override: Option<String> = None;
334 let mut sha_override: Option<String> = None;
335 let mut authority: Option<String> = None;
336 let mut jurisdiction: Option<String> = None;
337 let mut source_ref: Option<serde_json::Value> = None;
338 let mut from_git: Option<String> = None;
339 let mut drafts: Vec<DraftGround> = Vec::new();
340 let mut i = 0;
341 while i < args.len() {
342 let flag = args[i].clone();
343 match flag.as_str() {
344 "--from-git" => {
345 from_git = Some(need(args, i, &flag)?);
346 }
347 "--observe" => {
348 observe = need(args, i, &flag)?;
349 }
350 "--blame" => {
351 blame_override = Some(need(args, i, &flag)?);
352 }
353 "--verified-at-sha" => {
354 sha_override = Some(need(args, i, &flag)?);
355 }
356 "--authority" => {
357 let v = need(args, i, &flag)?;
358 validate_authority(&v)?;
359 authority = Some(v);
360 }
361 "--jurisdiction" => {
362 let v = need(args, i, &flag)?;
363 crate::tick::validate_jurisdiction(&v)?;
364 jurisdiction = Some(v);
365 }
366 "--source-ref" => {
367 let v = need(args, i, &flag)?;
370 if v.is_empty() {
371 return Err("--source-ref needs a non-empty value".into());
372 }
373 source_ref = Some(serde_json::Value::String(v));
374 }
375 "--reject" => {
376 let v = need(args, i, &flag)?;
377 let (opt, why) = v
378 .split_once(':')
379 .ok_or("--reject expects \"<option>: <why>\"".to_string())?;
380 let (opt, why) = (opt.trim(), why.trim());
381 if opt.is_empty() || why.is_empty() {
382 return Err("--reject needs non-empty <option> and <why>".into());
383 }
384 drafts.push(DraftGround {
385 claim: why.into(),
386 supports: format!("rejected:{opt}"),
387 ..Default::default()
388 });
389 }
390 "--assume" => {
391 let claim = need(args, i, &flag)?;
392 drafts.push(DraftGround {
393 claim,
394 supports: "chosen".into(),
395 ..Default::default()
396 });
397 }
398 "--revisit" => {
399 last(&mut drafts, &flag)?.revisit = Some(need(args, i, &flag)?);
400 }
401 "--assume-test" => {
402 last(&mut drafts, &flag)?.test_ref = Some(need(args, i, &flag)?);
403 }
404 "--counter-test" => {
405 last(&mut drafts, &flag)?.counter_test = Some(need(args, i, &flag)?);
406 }
407 "--on-platform" => {
408 let v = need(args, i, &flag)?;
409 last(&mut drafts, &flag)?.platforms.push(v);
410 }
411 "--triggered-by" => {
412 let v = need(args, i, &flag)?;
413 last(&mut drafts, &flag)?.triggered_by.push(v);
414 }
415 "--surface" => {
416 let v = need(args, i, &flag)?;
417 last(&mut drafts, &flag)?.surfaces.push(v);
418 }
419 other => return Err(format!("decide: unknown flag {other}")),
420 }
421 i += 2;
422 }
423
424 let (decision, observe) = match (decision, &from_git) {
428 (Some(_), Some(_)) => {
429 return Err("decide: decision given twice (positional and --from-git)".into())
430 }
431 (None, None) => return Err("decide: needs a decision (positional) or --from-git".into()),
432 (Some(d), None) => (d.to_string(), observe),
433 (None, Some(commit)) => {
434 let env = read_envelope(repo, commit)?;
435 if blame_override.is_none() {
438 blame_override = Some(match subject_role(&env.subject) {
439 Some(role) => role.to_string(),
440 None => env.author,
441 });
442 }
443 let observe = std::iter::once(observe)
445 .chain(subject_refs(&env.subject))
446 .chain(env.refs)
447 .filter(|s| !s.is_empty())
448 .collect::<Vec<_>>()
449 .join(" ");
450 (env.subject, observe)
451 }
452 };
453 if decision.trim().is_empty() {
454 return Err("decision text is empty".into());
455 }
456 let blame = resolve_blame(repo, blame_override)?;
457 let mut grounds = Vec::new();
458 for d in drafts {
459 grounds.push(build_ground(repo, d, &sha_override)?);
460 }
461 append(
464 repo,
465 Decision {
466 observe,
467 decision: decision.to_string(),
468 grounds,
469 blame,
470 authority,
471 jurisdiction,
472 source_ref,
473 provenance: None,
476 },
477 )
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use crate::tick::Check;
484
485 fn repo() -> std::path::PathBuf {
486 use std::sync::atomic::{AtomicU64, Ordering};
487 static N: AtomicU64 = AtomicU64::new(0);
488 let p = std::env::temp_dir().join(format!(
489 "ev-capture-{}-{}",
490 std::process::id(),
491 N.fetch_add(1, Ordering::Relaxed)
492 ));
493 let _ = std::fs::remove_dir_all(&p);
494 std::fs::create_dir_all(&p).unwrap();
495 Store::at(&p).init().unwrap();
496 p
497 }
498 fn s(v: &[&str]) -> Vec<String> {
499 v.iter().map(|x| x.to_string()).collect()
500 }
501
502 #[test]
503 fn decide_should_record_a_chosen_a_revisit_and_a_rejected_road_when_all_are_passed() {
504 let r = repo();
506
507 let t = run(
509 &r,
510 Some("build our own retrieval; reject pgvector"),
511 &s(&[
512 "--observe",
513 "evaluating backend",
514 "--assume",
515 "team has bandwidth long-term",
516 "--revisit",
517 "Q3 review",
518 "--reject",
519 "pgvector: would lock our schema",
520 "--blame",
521 "Wang Yu",
522 ]),
523 )
524 .expect("ok");
525
526 assert_eq!(t.grounds.len(), 2);
528 assert!(matches!(t.grounds[0].check, Some(Check::Person { .. })));
529 assert_eq!(t.grounds[1].supports, "rejected:pgvector");
530 assert_eq!(t.blame, "Wang Yu");
531 assert_eq!(Store::at(&r).read_head().unwrap(), t.id);
532 }
533
534 #[test]
535 fn decide_should_stamp_held_since_with_a_nonempty_rfc3339_time_when_recording() {
536 let r = repo();
538
539 run(&r, Some("ship it"), &s(&["--blame", "Wang Yu"])).expect("ok");
541
542 let head = Store::at(&r).read_head().unwrap();
544 let tick = Store::at(&r).read_tick(&head).unwrap().unwrap();
545 assert!(!tick.held_since.is_empty());
546 time::OffsetDateTime::parse(
547 &tick.held_since,
548 &time::format_description::well_known::Rfc3339,
549 )
550 .expect("held_since parses as RFC 3339");
551 }
552
553 #[test]
554 fn decide_should_store_a_trimmed_blame_when_the_blame_is_padded() {
555 let r = repo();
557
558 let t = run(
560 &r,
561 Some("d"),
562 &s(&["--assume", "c", "--blame", " Wang Yu "]),
563 )
564 .expect("ok");
565
566 assert_eq!(t.blame, "Wang Yu");
568 }
569
570 #[test]
571 fn decide_should_refuse_the_ground_when_it_is_both_revisit_and_assume_test() {
572 let r = repo();
574
575 let e = run(
577 &r,
578 Some("d"),
579 &s(&[
580 "--assume",
581 "c",
582 "--revisit",
583 "Q3",
584 "--assume-test",
585 "pytest x",
586 "--blame",
587 "Wang Yu",
588 ]),
589 );
590
591 assert!(e.is_err());
593 }
594
595 #[test]
596 fn decide_should_refuse_a_check_when_the_ground_is_a_rejected_road() {
597 let r = repo();
599
600 let e = run(
602 &r,
603 Some("d"),
604 &s(&[
605 "--reject",
606 "pgvector: would lock our schema",
607 "--assume-test",
608 "pytest x",
609 "--counter-test",
610 "ct",
611 "--on-platform",
612 "linux-ci",
613 "--triggered-by",
614 "f",
615 "--surface",
616 "s",
617 "--verified-at-sha",
618 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
619 "--blame",
620 "Wang Yu",
621 ]),
622 );
623
624 assert!(e.is_err());
626 }
627
628 #[test]
629 fn decide_should_error_when_there_is_no_store() {
630 let p = std::env::temp_dir().join(format!("ev-nostore-{}", std::process::id()));
632 let _ = std::fs::remove_dir_all(&p);
633 std::fs::create_dir_all(&p).unwrap();
634
635 let e = run(&p, Some("d"), &s(&["--blame", "x"]));
637
638 assert!(e.is_err());
640 }
641
642 #[test]
643 fn decide_should_build_a_self_verifying_test_binding_when_all_test_fields_are_present() {
644 let r = repo();
646
647 let t = run(
649 &r,
650 Some("restore-safety counter DB-backed; reject Redis"),
651 &s(&[
652 "--assume",
653 "Argus introduces no Redis; multi-pod coord via existing DB",
654 "--assume-test",
655 "pytest tests/test_redis_absent.py",
656 "--counter-test",
657 "pytest tests/test_redis_absent.py::test_redis_injection_flips_red",
658 "--on-platform",
659 "linux-ci",
660 "--triggered-by",
661 "pyproject.toml",
662 "--surface",
663 "pyproject-deps",
664 "--verified-at-sha",
665 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
666 "--reject",
667 "Redis: a new infra dependency",
668 "--blame",
669 "Wang Yu",
670 ]),
671 )
672 .expect("ok");
673
674 match &t.grounds[0].check {
676 Some(Check::Test {
677 reference,
678 counter_test,
679 liveness,
680 verified_at_sha,
681 }) => {
682 assert_eq!(reference, "pytest tests/test_redis_absent.py");
683 assert!(counter_test
684 .as_deref()
685 .is_some_and(|c| c.contains("flips_red")));
686 assert_eq!(liveness.platforms, vec!["linux-ci".to_string()]);
687 assert_eq!(verified_at_sha.len(), 40);
688 }
689 _ => panic!("expected a test check"),
690 }
691 }
692
693 #[test]
694 fn decide_should_reject_a_test_binding_when_there_is_no_counter_test() {
695 let r = repo();
697
698 let e = run(
700 &r,
701 Some("d"),
702 &s(&[
703 "--assume",
704 "c",
705 "--assume-test",
706 "pytest x",
707 "--on-platform",
708 "linux-ci",
709 "--triggered-by",
710 "f",
711 "--surface",
712 "s",
713 "--verified-at-sha",
714 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
715 "--blame",
716 "Wang Yu",
717 ]),
718 );
719
720 assert!(e.is_err());
722 }
723
724 #[test]
725 fn decide_should_reject_a_test_binding_when_the_counter_test_is_empty() {
726 let r = repo();
728
729 let e = run(
731 &r,
732 Some("d"),
733 &s(&[
734 "--assume",
735 "c",
736 "--assume-test",
737 "pytest x",
738 "--counter-test",
739 "",
740 "--on-platform",
741 "linux-ci",
742 "--triggered-by",
743 "f",
744 "--surface",
745 "s",
746 "--verified-at-sha",
747 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
748 "--blame",
749 "Wang Yu",
750 ]),
751 );
752
753 assert!(e.is_err());
755 }
756
757 #[test]
758 fn decide_should_reject_a_test_binding_when_there_is_no_verified_at_sha_and_no_git() {
759 let r = repo();
761
762 let e = run(
764 &r,
765 Some("d"),
766 &s(&[
767 "--assume",
768 "c",
769 "--assume-test",
770 "pytest x",
771 "--counter-test",
772 "ct",
773 "--on-platform",
774 "linux-ci",
775 "--triggered-by",
776 "f",
777 "--surface",
778 "s",
779 "--blame",
780 "Wang Yu",
781 ]),
782 );
783
784 assert!(e.is_err());
786 }
787
788 #[test]
789 fn migrate_bind_should_build_a_harvested_test_check_when_no_counter_test() {
790 let check = harvested_test_check(
793 "pytest tests/test_invariant_no_redis.py".into(),
794 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
795 vec!["linux-ci".into()],
796 vec!["pyproject.toml".into()],
797 vec!["pyproject-deps".into()],
798 )
799 .expect("the full liveness is present, so the harvested binding is well-formed");
800
801 match check {
803 Check::Test {
804 reference,
805 counter_test,
806 liveness,
807 verified_at_sha,
808 } => {
809 assert_eq!(reference, "pytest tests/test_invariant_no_redis.py");
810 assert_eq!(counter_test, None); assert_eq!(liveness.platforms, vec!["linux-ci".to_string()]);
812 assert_eq!(liveness.triggered_by, vec!["pyproject.toml".to_string()]);
813 assert_eq!(liveness.surfaces, vec!["pyproject-deps".to_string()]);
814 assert_eq!(verified_at_sha.len(), 40);
815 }
816 _ => panic!("expected a harvested test check"),
817 }
818 }
819
820 #[test]
821 fn migrate_bind_should_reject_a_harvested_binding_when_a_liveness_key_is_missing() {
822 let e = harvested_test_check(
824 "pytest x".into(),
825 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
826 vec!["linux-ci".into()],
827 vec!["pyproject.toml".into()],
828 vec![], );
830
831 assert!(e.is_err());
833 }
834
835 #[test]
836 fn decide_should_still_error_without_a_counter_test() {
837 let r = repo();
841
842 let e = run(
844 &r,
845 Some("d"),
846 &s(&[
847 "--assume",
848 "c",
849 "--assume-test",
850 "pytest x",
851 "--on-platform",
852 "linux-ci",
853 "--triggered-by",
854 "f",
855 "--surface",
856 "s",
857 "--verified-at-sha",
858 "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
859 "--blame",
860 "Wang Yu",
861 ]),
862 );
863
864 assert!(e.is_err());
866 }
867
868 #[test]
869 fn append_should_compute_the_frozen_genesis_id_when_given_the_genesis_decision() {
870 let r = repo();
873 let d = Decision {
874 observe: "evaluating retrieval backend".into(),
875 decision: "freeze the retrieval schema for v2".into(),
876 grounds: vec![
877 Ground {
878 claim: "team still wants a frozen schema".into(),
879 supports: "chosen".into(),
880 check: Some(Check::Person {
881 reference: "Q3 infra review".into(),
882 }),
883 },
884 Ground {
885 claim: "pgvector would lock our schema".into(),
886 supports: "rejected:pgvector".into(),
887 check: None,
888 },
889 ],
890 blame: "Wang Yu".into(),
891 authority: None,
892 jurisdiction: None,
893 source_ref: None,
894 provenance: None,
895 };
896
897 let t = append(&r, d).expect("ok");
899
900 assert_eq!(t.id, "e2b337f53a1f");
903 assert_eq!(t.parent_id, "");
904 assert_eq!(Store::at(&r).read_head().unwrap(), t.id);
905 }
906
907 #[test]
908 fn decide_should_take_blame_from_git_config_when_no_blame_flag_is_given() {
909 let r = repo();
911 for a in [
912 ["init"].as_slice(),
913 ["config", "user.name", "Ada Lovelace"].as_slice(),
914 ] {
915 std::process::Command::new("git")
916 .args(a)
917 .current_dir(&r)
918 .output()
919 .unwrap();
920 }
921
922 let t = run(&r, Some("d"), &s(&["--assume", "c"])).expect("ok");
924
925 assert_eq!(t.blame, "Ada Lovelace");
927 }
928}