|
1 | 1 | use std::fs; |
| 2 | +use std::os::unix::fs::PermissionsExt; |
| 3 | +use std::os::unix::process::CommandExt as _; |
2 | 4 | use std::path::Path; |
3 | 5 | use std::process::Command; |
4 | 6 |
|
@@ -550,6 +552,266 @@ fn byte_identical_tracked_target_is_allowed_without_modification() { |
550 | 552 | assert_eq!(before, after, "identical target was needlessly rewritten"); |
551 | 553 | } |
552 | 554 |
|
| 555 | +#[test] |
| 556 | +fn executable_copy_repairs_a_byte_identical_destination_mode() { |
| 557 | + let tmp = tempfile::tempdir().unwrap(); |
| 558 | + let catalog = tmp.path().join("catalog"); |
| 559 | + let workspace = tmp.path().join("workspace"); |
| 560 | + fs::create_dir_all(&workspace).unwrap(); |
| 561 | + write(&catalog.join("_templates/wrapper"), "#!/bin/sh\nexit 0\n"); |
| 562 | + let destination = workspace.join("bin/wrapper"); |
| 563 | + write(&destination, "#!/bin/sh\nexit 0\n"); |
| 564 | + fs::set_permissions(&destination, fs::Permissions::from_mode(0o644)).unwrap(); |
| 565 | + write( |
| 566 | + &catalog.join("agents/Silber/cos/agent.kdl"), |
| 567 | + agent_kdl( |
| 568 | + &workspace, |
| 569 | + r#" copy "_templates/wrapper" "bin/wrapper" executable=#true"#, |
| 570 | + ), |
| 571 | + ); |
| 572 | + |
| 573 | + let found = discover(&catalog); |
| 574 | + assert!(found.errors.is_empty(), "{:?}", found.errors); |
| 575 | + let report = materialize_catalog(&catalog, &found.specs, "Silber"); |
| 576 | + assert!(report.is_clean(), "{:?}", report.errors); |
| 577 | + assert_eq!( |
| 578 | + fs::metadata(destination).unwrap().permissions().mode() & 0o777, |
| 579 | + 0o755 |
| 580 | + ); |
| 581 | +} |
| 582 | + |
| 583 | +#[test] |
| 584 | +fn regular_copy_repairs_a_byte_identical_executable_mode() { |
| 585 | + let tmp = tempfile::tempdir().unwrap(); |
| 586 | + let catalog = tmp.path().join("catalog"); |
| 587 | + let workspace = tmp.path().join("workspace"); |
| 588 | + fs::create_dir_all(&workspace).unwrap(); |
| 589 | + write(&catalog.join("_templates/plain"), "same\n"); |
| 590 | + let destination = workspace.join("plain"); |
| 591 | + write(&destination, "same\n"); |
| 592 | + fs::set_permissions(&destination, fs::Permissions::from_mode(0o777)).unwrap(); |
| 593 | + write( |
| 594 | + &catalog.join("agents/Silber/cos/agent.kdl"), |
| 595 | + agent_kdl(&workspace, r#" copy "_templates/plain" "plain""#), |
| 596 | + ); |
| 597 | + |
| 598 | + let found = discover(&catalog); |
| 599 | + let report = materialize_catalog(&catalog, &found.specs, "Silber"); |
| 600 | + |
| 601 | + assert!(report.is_clean(), "{:?}", report.errors); |
| 602 | + assert_eq!( |
| 603 | + fs::metadata(destination).unwrap().permissions().mode() & 0o777, |
| 604 | + 0o644 |
| 605 | + ); |
| 606 | +} |
| 607 | + |
| 608 | +#[test] |
| 609 | +fn content_directives_create_exact_modes_under_a_restrictive_umask() { |
| 610 | + let tmp = tempfile::tempdir().unwrap(); |
| 611 | + let catalog = tmp.path().join("catalog"); |
| 612 | + let workspace = tmp.path().join("workspace"); |
| 613 | + fs::create_dir_all(&workspace).unwrap(); |
| 614 | + let template = catalog.join("_templates/tool"); |
| 615 | + write(&template, "#!/bin/sh\nexit 0\n"); |
| 616 | + fs::set_permissions(&template, fs::Permissions::from_mode(0o755)).unwrap(); |
| 617 | + write( |
| 618 | + &catalog.join("agents/Silber/cos/agent.kdl"), |
| 619 | + agent_kdl( |
| 620 | + &workspace, |
| 621 | + r##" copy "_templates/tool" "bin/copied" executable=#true |
| 622 | + copy "_templates/tool" "plain-copy" |
| 623 | + file "bin/written" "#!/bin/sh\nexit 0\n" executable=#true |
| 624 | + json-upsert "bin/data.json" #"{"value":true}"# executable=#true |
| 625 | + ensure-line "bin/lines" "line" executable=#true |
| 626 | + file "plain" "plain" |
| 627 | +"##, |
| 628 | + ), |
| 629 | + ); |
| 630 | + |
| 631 | + let mut process = Command::new(env!("CARGO_BIN_EXE_st2")); |
| 632 | + process.args(["up", "--catalog"]).arg(&catalog).args([ |
| 633 | + "--host", |
| 634 | + "Silber", |
| 635 | + "--materialize-only", |
| 636 | + ]); |
| 637 | + unsafe { |
| 638 | + process.pre_exec(|| { |
| 639 | + libc::umask(0o077); |
| 640 | + Ok(()) |
| 641 | + }); |
| 642 | + } |
| 643 | + let output = process.output().unwrap(); |
| 644 | + assert!( |
| 645 | + output.status.success(), |
| 646 | + "stdout:\n{}\nstderr:\n{}", |
| 647 | + String::from_utf8_lossy(&output.stdout), |
| 648 | + String::from_utf8_lossy(&output.stderr) |
| 649 | + ); |
| 650 | + for path in ["bin/copied", "bin/written", "bin/data.json", "bin/lines"] { |
| 651 | + assert_eq!( |
| 652 | + fs::metadata(workspace.join(path)) |
| 653 | + .unwrap() |
| 654 | + .permissions() |
| 655 | + .mode() |
| 656 | + & 0o777, |
| 657 | + 0o755, |
| 658 | + "{path}" |
| 659 | + ); |
| 660 | + } |
| 661 | + for path in ["plain", "plain-copy"] { |
| 662 | + assert_eq!( |
| 663 | + fs::metadata(workspace.join(path)) |
| 664 | + .unwrap() |
| 665 | + .permissions() |
| 666 | + .mode() |
| 667 | + & 0o777, |
| 668 | + 0o644, |
| 669 | + "{path}" |
| 670 | + ); |
| 671 | + } |
| 672 | + assert!( |
| 673 | + Command::new(workspace.join("bin/copied")) |
| 674 | + .status() |
| 675 | + .unwrap() |
| 676 | + .success() |
| 677 | + ); |
| 678 | +} |
| 679 | + |
| 680 | +#[test] |
| 681 | +fn inline_executable_content_is_exact_runnable_and_idempotent() { |
| 682 | + let tmp = tempfile::tempdir().unwrap(); |
| 683 | + let catalog = tmp.path().join("catalog"); |
| 684 | + let workspace = tmp.path().join("workspace"); |
| 685 | + fs::create_dir_all(&workspace).unwrap(); |
| 686 | + write( |
| 687 | + &catalog.join("agents/Silber/cos/agent.kdl"), |
| 688 | + agent_kdl( |
| 689 | + &workspace, |
| 690 | + r#####" file "bin/probe" executable=#true { |
| 691 | + content #""" |
| 692 | +#!/bin/sh |
| 693 | +printf 'hello:%s\n' "${ST_AGENT:-unknown}" |
| 694 | +
|
| 695 | +"""# |
| 696 | + } |
| 697 | + file "empty" "" |
| 698 | + file "quote-sequence" { |
| 699 | + content ##""" |
| 700 | +before |
| 701 | +"""# remains content |
| 702 | +
|
| 703 | +"""## |
| 704 | + } |
| 705 | +"#####, |
| 706 | + ), |
| 707 | + ); |
| 708 | + let expected_probe = b"#!/bin/sh\nprintf 'hello:%s\\n' \"${ST_AGENT:-unknown}\"\n".as_slice(); |
| 709 | + let expected_quote = b"before\n\"\"\"# remains content\n".as_slice(); |
| 710 | + |
| 711 | + let found = discover(&catalog); |
| 712 | + let first = materialize_catalog(&catalog, &found.specs, "Silber"); |
| 713 | + assert!(first.is_clean(), "{:?}", first.errors); |
| 714 | + assert_eq!(first.materialized.len(), 3, "{:?}", first.materialized); |
| 715 | + |
| 716 | + let probe = workspace.join("bin/probe"); |
| 717 | + assert_eq!(fs::read(&probe).unwrap(), expected_probe); |
| 718 | + assert_eq!(fs::read(workspace.join("empty")).unwrap(), b""); |
| 719 | + assert_eq!( |
| 720 | + fs::read(workspace.join("quote-sequence")).unwrap(), |
| 721 | + expected_quote |
| 722 | + ); |
| 723 | + assert_eq!( |
| 724 | + fs::metadata(&probe).unwrap().permissions().mode() & 0o777, |
| 725 | + 0o755 |
| 726 | + ); |
| 727 | + let executed = Command::new(&probe) |
| 728 | + .env("ST_AGENT", "Silber.inline-proof") |
| 729 | + .output() |
| 730 | + .unwrap(); |
| 731 | + assert!( |
| 732 | + executed.status.success(), |
| 733 | + "{}", |
| 734 | + String::from_utf8_lossy(&executed.stderr) |
| 735 | + ); |
| 736 | + assert_eq!(executed.stdout, b"hello:Silber.inline-proof\n"); |
| 737 | + |
| 738 | + let second = materialize_catalog(&catalog, &found.specs, "Silber"); |
| 739 | + assert!(second.is_clean(), "{:?}", second.errors); |
| 740 | + assert!( |
| 741 | + second.materialized.is_empty(), |
| 742 | + "unchanged operations were reported as materialized: {:?}", |
| 743 | + second.materialized |
| 744 | + ); |
| 745 | + assert_eq!(fs::read(probe).unwrap(), expected_probe); |
| 746 | +} |
| 747 | + |
| 748 | +#[test] |
| 749 | +fn one_inline_target_repairs_mode_toggles_in_both_directions() { |
| 750 | + let tmp = tempfile::tempdir().unwrap(); |
| 751 | + let catalog = tmp.path().join("catalog"); |
| 752 | + let workspace = tmp.path().join("workspace"); |
| 753 | + fs::create_dir_all(&workspace).unwrap(); |
| 754 | + let declaration = |executable: bool| { |
| 755 | + agent_kdl( |
| 756 | + &workspace, |
| 757 | + &format!( |
| 758 | + " file \"target\" \"same\" executable=#{}", |
| 759 | + if executable { "true" } else { "false" } |
| 760 | + ), |
| 761 | + ) |
| 762 | + }; |
| 763 | + |
| 764 | + for (executable, expected_mode) in [(true, 0o755), (false, 0o644), (true, 0o755)] { |
| 765 | + write( |
| 766 | + &catalog.join("agents/Silber/cos/agent.kdl"), |
| 767 | + declaration(executable), |
| 768 | + ); |
| 769 | + let found = discover(&catalog); |
| 770 | + let report = materialize_catalog(&catalog, &found.specs, "Silber"); |
| 771 | + assert!(report.is_clean(), "{:?}", report.errors); |
| 772 | + assert_eq!(report.materialized.len(), 1, "{:?}", report.materialized); |
| 773 | + assert_eq!( |
| 774 | + fs::metadata(workspace.join("target")) |
| 775 | + .unwrap() |
| 776 | + .permissions() |
| 777 | + .mode() |
| 778 | + & 0o777, |
| 779 | + expected_mode |
| 780 | + ); |
| 781 | + } |
| 782 | +} |
| 783 | + |
| 784 | +#[test] |
| 785 | +fn invalid_render_mode_properties_fail_validation() { |
| 786 | + for (directive, expected) in [ |
| 787 | + ( |
| 788 | + r#"copy "_templates/source" "target" executable="yes""#, |
| 789 | + "property 'executable' must be a boolean", |
| 790 | + ), |
| 791 | + ( |
| 792 | + r#"file "target" "content" mode=#true"#, |
| 793 | + "unknown property 'mode'", |
| 794 | + ), |
| 795 | + ( |
| 796 | + r#"git-exclude "target" executable=#true"#, |
| 797 | + "unknown property 'executable'", |
| 798 | + ), |
| 799 | + ] { |
| 800 | + let tmp = tempfile::tempdir().unwrap(); |
| 801 | + let catalog = tmp.path().join("catalog"); |
| 802 | + let workspace = tmp.path().join("workspace"); |
| 803 | + fs::create_dir_all(&workspace).unwrap(); |
| 804 | + write(&catalog.join("_templates/source"), "source\n"); |
| 805 | + write( |
| 806 | + &catalog.join("agents/Silber/cos/agent.kdl"), |
| 807 | + agent_kdl(&workspace, &format!(" {directive}")), |
| 808 | + ); |
| 809 | + let found = discover(&catalog); |
| 810 | + let error = parse_plan(&found.specs[0]).unwrap_err().to_string(); |
| 811 | + assert!(error.contains(expected), "{error}"); |
| 812 | + } |
| 813 | +} |
| 814 | + |
553 | 815 | #[test] |
554 | 816 | fn untracked_and_non_git_targets_remain_materializable() { |
555 | 817 | for git in [true, false] { |
|
0 commit comments