~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
252 lines · 9.4 KB Raw History
//! An agent keeping pulls the merge queue turned away moving, on the
//! reference PDS: one that conflicts with its base only where both sides
//! added lines it brings up to date and queues again, one where they
//! changed the same lines it tells its author of, once, and any other it
//! leaves alone.

#[path = "../../knotless/tests/common/mod.rs"]
mod common;

use common::{Sandbox, net, wait_until};
use knotless::forge::Target;
use knotless::queue::Ejected;
use knotless::url::RepoUrl;
use knotless_agent::{Job, Options};
use serde_json::Value;
use std::time::Duration;

/// An appview over the reference PDS, following its firehose.
fn appview(rt: &tokio::runtime::Runtime, s: &Sandbox) -> String {
    let listener = rt
        .block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
        .unwrap();
    let base = format!("http://localhost:{}", listener.local_addr().unwrap().port());
    let index = knotless_serve::index::Index::open(&s.path("index.db")).unwrap();
    let app = knotless_serve::App::build(common::ctx(s), Duration::ZERO, None, Some(index));
    let router = knotless_serve::router(app.clone());
    rt.spawn(async move { axum::serve(listener, router).await.unwrap() });
    rt.spawn(knotless_serve::firehose::watch(
        app,
        net().pds.replace("http://", "ws://"),
    ));
    base
}

#[track_caller]
fn ok(s: &Sandbox, args: &[&str]) -> String {
    let (code, out) = s.cli(args, b"");
    assert_eq!(code, 0, "{args:?}: {out}");
    out
}

/// The comments `who` wrote on `pull`, oldest first.
fn said(who: &Sandbox, pull: &str) -> Vec<String> {
    let mut said: Vec<(String, Value)> = who.records("dev.knotless.comment");
    said.retain(|(_, c)| c["subject"] == pull);
    said.sort_by(|a, b| a.0.cmp(&b.0));
    let bodies = said
        .into_iter()
        .map(|(_, c)| c["body"].as_str().unwrap().to_string());
    bodies.collect()
}

#[test]
fn an_agent_keeps_turned_away_pulls_moving() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let base = appview(&rt, &Sandbox::new());
    let mut owner = Sandbox::new();
    let handle = owner.account.handle.clone();
    owner.set("KNOTLESS_APPVIEW", &base);
    owner.set("KNOTLESS_IDENTIFIER", &handle);
    let url = owner.url("kept");
    let work = owner.work("w", 1);
    owner.commit_file(&work, "notes", "one\n");
    owner.git(&work, &["push", "-q", &url, "main"]);
    // A runner named, and the queue on for main, so its pulls may be queued.
    ok(&owner, &["runner", "add", &url, &handle]);
    ok(
        &owner,
        &["branch", "protect", &url, "main", "--merges", "--queue"],
    );
    // `branch` as main is now, with `file` holding `text`, pushed.
    let write = |branch: &str, from: &str, file: &str, text: &str| {
        owner.git(&work, &["fetch", "-q", &url, from]);
        owner.git(&work, &["checkout", "-q", "-B", branch, "FETCH_HEAD"]);
        let sha = owner.commit_file(&work, file, text);
        owner.git(&work, &["push", "-q", "-f", &url, branch]);
        sha
    };
    let ctx = common::ctx(&owner);
    let repo = RepoUrl::parse(&url).unwrap();
    // Turns `pull` away from the queue at `head`, as a runner does.
    let eject = |pull: &str, head: &str| {
        let t = rt.block_on(Target::open(&ctx, &repo)).unwrap();
        let ejected = Ejected {
            reason: Some("it conflicts in notes"),
            head: Some(head),
            checks: &[],
        };
        rt.block_on(knotless::queue::eject(&ctx, &t, pull, ejected))
            .unwrap();
    };
    let pull = |branch: &str, title: &str, body: &str| {
        let pull = ok(
            &owner,
            &[
                "pull", "create", &url, "main", &url, branch, title, "-m", body,
            ],
        );
        wait_until(|| ok(&owner, &["pull", "list", &url]).contains(&pull));
        pull
    };
    let shown = |pull: &str| ok(&owner, &["pull", "show", &url, pull]);

    // Pulls cut from main, then main moved on.
    let added = write("added", "main", "notes", "one\nadded\n");
    let changed = write("changed", "main", "notes", "uno\n");
    let apart = write("apart", "main", "apart", "apart\n");
    let off = write("off", "main", "notes", "one\noff\n");
    let moved = write("moved", "main", "notes", "one\nmoved\n");
    let gone = write("gone", "main", "notes", "one\ngone\n");
    // A branch of another of the owner's repositories: not the keeper's
    // to push to.
    let other = owner.url("elsewhere");
    let away = write("away", "main", "notes", "one\naway\n");
    owner.git(&work, &["push", "-q", &other, "away:main", "away"]);
    let from_elsewhere = || {
        let args = [
            "pull",
            "create",
            &url,
            "main",
            &other,
            "away",
            "Away",
            "-m",
            "From elsewhere",
        ];
        let pull = ok(&owner, &args);
        wait_until(|| ok(&owner, &["pull", "list", &url]).contains(&pull));
        pull
    };
    let pulls = [
        (pull("added", "Added", "More notes"), added),
        (pull("changed", "Changed", "Other notes"), changed),
        (pull("apart", "Apart", "Elsewhere"), apart),
        (pull("off", "Off", "Mine to fix\n\nKeeper: off"), off),
        (pull("moved", "Moved", "Pushed to since"), moved),
        (pull("gone", "Gone", "Its branch deleted"), gone),
        (from_elsewhere(), away),
    ];
    write("main", "main", "notes", "one\nmain\n");
    for (pull, head) in &pulls {
        eject(pull, head);
    }
    // Pushed to since it was turned away, it is its author's again.
    write("moved", "moved", "notes", "one\nmoved, again\n");
    ok(&owner, &["branch", "delete", &url, "gone"]);
    for (pull, _) in &pulls {
        wait_until(|| shown(pull).contains("ejected"));
    }
    let added_at = pulls[0].1.clone();
    let [added, changed, apart, off, moved, gone, away] = pulls.map(|(p, _)| p);

    let opts = Options {
        job: Job::Keep,
        command: String::new(),
        all: false,
        rounds: 3,
        timeout: Duration::from_secs(3),
        jetstream: "ws://127.0.0.1:9".into(),
        sweep: Duration::from_secs(600),
        settle: Duration::ZERO,
        once: true,
        repos: vec![format!("{handle}/kept")],
        owners: Vec::new(),
        attempts: 2,
    };
    let once = |opts: &Options| {
        rt.block_on(knotless_agent::run(opts.clone(), common::ctx(&owner)))
            .unwrap()
    };
    once(&opts);

    // Lines both sides added: brought up to date, main's first, and queued.
    owner.git(&work, &["fetch", "-q", &url, "added"]);
    let notes = owner.git(&work, &["show", "FETCH_HEAD:notes"]);
    assert_eq!(notes, "one\nmain\nadded");
    let told = said(&owner, &added);
    assert_eq!(told.len(), 1, "{told:?}");
    assert!(
        told[0].starts_with("Merged main into this pull"),
        "{}",
        told[0]
    );
    assert!(
        told[0].contains("Both sides only added lines in notes"),
        "{}",
        told[0]
    );
    wait_until(|| shown(&added).contains("merge queue: queued"));
    // The same line changed: its author is told where, and it stays.
    let told = said(&owner, &changed);
    assert_eq!(told.len(), 1, "{told:?}");
    assert!(
        told[0].contains("conflicts with main in notes"),
        "{}",
        told[0]
    );
    owner.git(&work, &["fetch", "-q", &url, "changed"]);
    assert_eq!(owner.git(&work, &["show", "FETCH_HEAD:notes"]), "uno");
    // Turned away for another reason, asked to be left, pushed to, its
    // branch gone or elsewhere: nothing.
    for pull in [&apart, &off, &moved, &gone, &away] {
        assert!(said(&owner, pull).is_empty(), "{pull}");
    }

    // Read again, it says nothing twice.
    wait_until(|| shown(&changed).matches("conflicts with main").count() == 1);
    once(&opts);
    assert_eq!(said(&owner, &changed).len(), 1);
    assert_eq!(said(&owner, &added).len(), 1);

    // Left turned away at the head it had before it was brought up to
    // date, as when queueing it failed: queued again, nothing more said.
    // With the queue off for main it is left alone, until it is on again.
    eject(&added, &added_at);
    wait_until(|| shown(&added).contains("merge queue: ejected"));
    ok(&owner, &["branch", "protect", &url, "main", "--undo"]);
    once(&opts);
    assert!(shown(&added).contains("merge queue: ejected"));
    assert_eq!(said(&owner, &added).len(), 1);
    ok(
        &owner,
        &["branch", "protect", &url, "main", "--merges", "--queue"],
    );
    once(&opts);
    wait_until(|| shown(&added).contains("merge queue: queued"));
    assert_eq!(said(&owner, &added).len(), 1);

    // Turned away again once main moved on, it is brought up to date
    // again, but no more often than it may be.
    owner.git(&work, &["fetch", "-q", &url, "added"]);
    let head = owner.git(&work, &["rev-parse", "FETCH_HEAD"]);
    write("main", "main", "notes", "one\nmain\nmain again\n");
    eject(&added, &head);
    wait_until(|| shown(&added).contains("merge queue: ejected"));
    let once_more = Options {
        attempts: 1,
        ..opts.clone()
    };
    once(&once_more);
    assert_eq!(said(&owner, &added).len(), 1);
    once(&opts);
    let told = said(&owner, &added);
    assert_eq!(told.len(), 2, "{told:?}");
    owner.git(&work, &["fetch", "-q", &url, "added"]);
    let notes = owner.git(&work, &["show", "FETCH_HEAD:notes"]);
    assert_eq!(notes, "one\nmain\nmain again\nadded");
    // Its author was told of the head that conflicts once, though main
    // moved on.
    assert_eq!(said(&owner, &changed).len(), 1);
}