thirdshift 0.13.0

Turn a GitHub issue into a ready-for-review pull request, unattended
use super::*;
use crate::issue::IssueUrl;
use crate::logs::effects::OnMachine;

const STAMP: &str = "20261003T120000-0400";

#[test]
fn real_files_keep_interleaved_command_output_and_records_separate() {
    let root = tempfile::tempdir().unwrap();
    let issue = IssueUrl::parse("https://github.com/acme/widgets/issues/7").unwrap();
    let mut files = OnMachine;
    let first = allocate(&mut files, root.path(), Work::Run(&issue), STAMP).unwrap();
    let first_path = first.owner.log_path().unwrap().to_path_buf();
    let mut first_writer = first.writer;
    first_writer.write_all(b"first before\n").unwrap();
    let second = allocate(&mut files, root.path(), Work::Run(&issue), STAMP).unwrap();
    let mut second_writer = second.writer;
    second_writer.write_all(b"second before\n").unwrap();
    first_writer.write_all(b"first after\n").unwrap();
    second_writer.write_all(b"second after\n").unwrap();
    assert_ne!(first_path, second.owner.log_path().unwrap());
    assert_ne!(
        first.owner.record_path().unwrap(),
        second.owner.record_path().unwrap()
    );
    assert_eq!(
        fs::read(first_path).unwrap(),
        b"first before\nfirst after\n"
    );
    assert_eq!(
        fs::read(second.owner.log_path().unwrap()).unwrap(),
        b"second before\nsecond after\n"
    );
    for owner in [&first.owner, &second.owner] {
        assert_eq!(
            record_log(&owner.record_path().unwrap()),
            owner.log_path().unwrap()
        );
        let decoded = decode(owner.log_path().unwrap()).unwrap();
        assert_eq!(decoded.identity.stamp, STAMP);
        assert!(decoded.identity.token.is_some());
        assert_eq!(decoded.subject, "7");
    }
}

#[test]
fn a_forced_collision_preserves_the_existing_file_and_retries_with_a_fresh_token() {
    struct Colliding {
        occupied: Option<PathBuf>,
    }
    impl Files for Colliding {
        type CommandLog = File;
        fn create_command_log(&mut self, path: &Path) -> io::Result<File> {
            if self.occupied.is_none() {
                fs::create_dir_all(path.parent().unwrap())?;
                fs::write(path, b"another Command's output\n")?;
                self.occupied = Some(path.to_path_buf());
            }
            create(path)
        }
    }
    let root = tempfile::tempdir().unwrap();
    let issue = IssueUrl::parse("https://github.com/acme/widgets/issues/7").unwrap();
    let mut files = Colliding { occupied: None };
    let mut allocated = allocate(&mut files, root.path(), Work::Run(&issue), STAMP).unwrap();
    allocated.writer.write_all(b"new output\n").unwrap();
    let occupied = files.occupied.unwrap();
    assert_ne!(occupied, allocated.owner.log_path().unwrap());
    assert_eq!(fs::read(occupied).unwrap(), b"another Command's output\n");
    assert_eq!(
        fs::read(allocated.owner.log_path().unwrap()).unwrap(),
        b"new output\n"
    );
}

#[test]
fn legacy_and_owned_artifacts_share_one_decoder() {
    for (name, subject, token, kind, log) in [
        (
            "7-20261003T120000-0400.log",
            "7",
            None,
            None,
            "7-20261003T120000-0400.log",
        ),
        (
            "7-20261003T120000-0400.123-start-1-9.command.json",
            "7",
            None,
            None,
            "7-20261003T120000-0400.log",
        ),
        (
            "7-20261003T120000-0400~ownerA.command.json",
            "7",
            Some("ownerA"),
            None,
            "7-20261003T120000-0400~ownerA.log",
        ),
        (
            "20261003T120000-0400~ownerB.log",
            "",
            Some("ownerB"),
            None,
            "20261003T120000-0400~ownerB.log",
        ),
        (
            "7-20261003T120000-0400-implement.jsonl",
            "7",
            None,
            Some("implement"),
            "7-20261003T120000-0400.log",
        ),
        (
            "7-20261003T120000-0400~ownerA-repair-1-resume.jsonl",
            "7",
            Some("ownerA"),
            Some("repair-1-resume"),
            "7-20261003T120000-0400~ownerA.log",
        ),
        (
            "architect-20261003T120000-0400~ownerB-architecture-review.jsonl",
            "architect",
            Some("ownerB"),
            Some("architecture-review"),
            "20261003T120000-0400~ownerB.log",
        ),
        (
            "secure-20261003T120000+0530~ownerC-security-review.jsonl",
            "secure",
            Some("ownerC"),
            Some("security-review"),
            "20261003T120000+0530~ownerC.log",
        ),
    ] {
        let path = Path::new(name);
        let decoded = decode(path).unwrap_or_else(|| panic!("{name}"));
        assert_eq!(decoded.subject, subject);
        assert_eq!(decoded.identity.token, token);
        assert_eq!(decoded.session_kind, kind);
        assert_eq!(decoded.command_log(path), Path::new(log));
    }
    for name in [
        "7-bad.log",
        "7-20261003T120000-0400~.log",
        "7-20261003T120000-0400~owner-.log",
        "7-20261003T120000-0400~owner.jsonl",
        "7-20261003T120000-0400-.jsonl",
    ] {
        assert!(decode(Path::new(name)).is_none(), "{name}");
    }
}