use super::*;
fn db() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
conn.execute_batch(crate::desk::SCHEMA).unwrap();
conn.execute_batch(crate::desk::POS_COLUMN).unwrap();
conn.execute_batch(crate::desk::SENT_BY_COLUMN).unwrap();
conn.execute_batch(THREAD_COLUMN).unwrap();
conn.execute_batch(SCHEMA).unwrap();
conn.execute_batch(CMD_COLUMN).unwrap();
conn
}
fn desk(conn: &mut Connection) -> (i64, Vec<i64>) {
let d = crate::desk::create(conn, "/w", None, 0).unwrap().id;
let notes = ["one", "two", "three"]
.iter()
.map(|t| crate::desk::add_note(conn, d, t, 0).unwrap().unwrap().id)
.collect();
(d, notes)
}
fn start_as(conn: &mut Connection, d: i64, name: &str, pane: &str, notes: &[i64]) -> Started {
start(
conn,
d,
&Start {
name: name.into(),
notes: notes.to_vec(),
pane: pane.into(),
by: "claude-code".into(),
..Start::default()
},
10,
)
.unwrap()
}
#[test]
fn a_thread_groups_notes_and_its_name_again_is_the_same_thread() {
let mut conn = db();
let (d, n) = desk(&mut conn);
let Started::New(t) = start_as(&mut conn, d, "Home + friends", "p1", &n[..2]) else {
panic!()
};
assert_eq!(t.stage, "planned");
assert_eq!(t.notes, n[..2]);
let Started::Again(again) = start_as(&mut conn, d, "home + FRIENDS", "p2", &n[2..]) else {
panic!()
};
assert_eq!(again.id, t.id);
assert_eq!(again.notes, n);
assert_eq!(again.pane, "p2");
let notes = crate::desk::notes(&conn, d).unwrap();
assert!(notes.iter().all(|x| x.thread == t.id));
}
#[test]
fn a_note_moves_to_the_thread_it_was_last_put_in() {
let mut conn = db();
let (d, n) = desk(&mut conn);
let Started::New(a) = start_as(&mut conn, d, "A", "p1", &n) else {
panic!()
};
let Started::New(b) = start_as(&mut conn, d, "B", "p1", &n[..1]) else {
panic!()
};
assert_eq!(get(&conn, d, a.id).unwrap().unwrap().notes, n[1..]);
assert_eq!(get(&conn, d, b.id).unwrap().unwrap().notes, n[..1]);
}
#[test]
fn moving_the_panes_thread_parks_and_ships_it() {
let mut conn = db();
let (d, _) = desk(&mut conn);
start_as(&mut conn, d, "A", "p1", &[]);
let mv = |stage: &str, next: &str| Move {
stage: stage.into(),
next: next.into(),
pane: "p1".into(),
..Move::default()
};
let Moved::Thread(t) =
move_thread(&mut conn, d, None, &mv("parked", "rebase first"), 20).unwrap()
else {
panic!()
};
assert_eq!(
(t.stage.as_str(), t.next.as_str()),
("parked", "rebase first")
);
let Moved::Thread(t) = move_thread(&mut conn, d, None, &mv("building", ""), 30).unwrap() else {
panic!()
};
assert_eq!(
(t.stage.as_str(), t.next.as_str(), t.shipped_at),
("building", "", 0)
);
let Moved::Thread(t) = move_thread(&mut conn, d, None, &mv("shipped", ""), 40).unwrap() else {
panic!()
};
assert_eq!(t.shipped_at, 40);
let other = Move {
stage: "review".into(),
pane: "p9".into(),
..Move::default()
};
assert_eq!(
move_thread(&mut conn, d, None, &other, 50).unwrap(),
Moved::NoThread
);
assert_eq!(
move_thread(&mut conn, d, None, &mv("done", ""), 50).unwrap(),
Moved::BadStage
);
}
#[test]
fn what_the_mod_saw_is_filed_on_the_panes_thread() {
let mut conn = db();
let (d, _) = desk(&mut conn);
assert!(seen(&mut conn, d, "p1", &Seen::default(), 1)
.unwrap()
.is_none());
start_as(&mut conn, d, "A", "p1", &[]);
let s = |branch: &str, commits: i64| Seen {
branch: branch.into(),
commits,
..Seen::default()
};
seen(&mut conn, d, "p1", &s("claude/a", 0), 2).unwrap();
seen(&mut conn, d, "p1", &s("", 1), 3).unwrap();
let t = seen(&mut conn, d, "p1", &s("claude/a", 2), 4)
.unwrap()
.unwrap();
assert_eq!(
(t.branch.as_str(), t.commits, t.seen),
("claude/a", 3, true)
);
let t = seen(&mut conn, d, "p1", &s("claude/b", 1), 5)
.unwrap()
.unwrap();
assert_eq!(t.commits, 1);
let m = Seen {
pr: "https://github.com/o/r/pull/57".into(),
merged: "7E1C0A2".into(),
..Seen::default()
};
let t = seen(&mut conn, d, "p1", &m, 6).unwrap().unwrap();
assert_eq!(
(t.pr.as_str(), t.merged.as_str(), t.merged_at),
("57", "7e1c0a2", 6)
);
let t = seen(&mut conn, d, "p1", &m, 7).unwrap().unwrap();
assert_eq!(t.merged_at, 6);
let t = seen(&mut conn, d, "p1", &s("a b", 0), 8).unwrap().unwrap();
assert_eq!(t.branch, "claude/b");
}
#[test]
fn a_question_is_answered_once_and_told_once() {
let mut conn = db();
let (d, _) = desk(&mut conn);
let q = |options: &[&str]| Ask {
kind: "decide".into(),
text: "Search box after how many docs?".into(),
options: options.iter().map(|s| s.to_string()).collect(),
recommended: 1,
pane: "p1".into(),
..Ask::default()
};
assert_eq!(
ask(&mut conn, d, &q(&["10"]), 1).unwrap(),
Asked::BadOptions
);
let Asked::Turn(t) = ask(&mut conn, d, &q(&["After 5", "After 10"]), 1).unwrap() else {
panic!()
};
assert_eq!(t.recommended, 1);
let a = answer(&conn, d, t.id, "After 10", "snyvi", 2)
.unwrap()
.unwrap();
assert_eq!(a.answered_in, "snyvi");
assert!(answer(&conn, d, t.id, "After 5", "panel", 3)
.unwrap()
.is_none());
let live = vec!["p1".to_string(), "p2".to_string()];
assert!(take_untold(&conn, d, "p2", &live, 4).unwrap().is_empty());
assert_eq!(take_untold(&conn, d, "p1", &live, 4).unwrap().len(), 1);
assert!(take_untold(&conn, d, "p1", &live, 5).unwrap().is_empty());
}
#[test]
fn an_answer_outlives_its_panel_and_a_dialog_is_not_retold() {
let mut conn = db();
let (d, _) = desk(&mut conn);
let h = |via: &str| Ask {
kind: "try".into(),
text: "Try it on 7871".into(),
via: via.into(),
pane: "gone".into(),
..Ask::default()
};
let Asked::Turn(a) = ask(&mut conn, d, &h("ask"), 1).unwrap() else {
panic!()
};
let Asked::Turn(b) = ask(&mut conn, d, &h("dialog"), 1).unwrap() else {
panic!()
};
answer(&conn, d, a.id, "Looks good", "snyvi", 2).unwrap();
answer(&conn, d, b.id, "Looks good", "snyvi", 2).unwrap();
let told = take_untold(&conn, d, "p2", &["p2".into()], 3).unwrap();
assert_eq!(told.iter().map(|t| t.id).collect::<Vec<_>>(), [a.id]);
}
#[test]
fn threads_are_capped_by_the_panels_still_open() {
let mut conn = db();
let (d, _) = desk(&mut conn);
conn.execute(
"INSERT INTO panes(id, desk_id, slot, cwd, created_at) VALUES ('live', ?1, 1, '/w', 0)",
params![d],
)
.unwrap();
for i in 0..THREADS_PER_DESK {
assert!(matches!(
start_as(&mut conn, d, &format!("gone {i}"), "closed", &[]),
Started::New(_)
));
}
for i in 0..THREADS_PER_DESK {
assert!(matches!(
start_as(&mut conn, d, &format!("live {i}"), "live", &[]),
Started::New(_)
));
}
assert!(matches!(
start_as(&mut conn, d, "one more", "live", &[]),
Started::Full
));
assert!(matches!(
start_as(&mut conn, d, "gone 0", "live", &[]),
Started::Again(_)
));
}
#[test]
fn turns_are_capped_and_join_the_panes_thread() {
let mut conn = db();
let (d, _) = desk(&mut conn);
let Started::New(th) = start_as(&mut conn, d, "A", "p1", &[]) else {
panic!()
};
let h = Ask {
kind: "merge".into(),
text: "Merge PR 57".into(),
pane: "p1".into(),
..Ask::default()
};
for i in 0..TURNS_PER_DESK {
let Asked::Turn(t) = ask(&mut conn, d, &h, i).unwrap() else {
panic!()
};
assert_eq!(t.thread_id, th.id);
}
assert_eq!(ask(&mut conn, d, &h, 9).unwrap(), Asked::Full);
assert_eq!(waiting(&conn).unwrap().len() as i64, TURNS_PER_DESK);
}
#[test]
fn a_run_turn_carries_one_line_it_can_type() {
let mut conn = db();
let (d, _) = desk(&mut conn);
let run = |cmd: &str| Ask {
kind: "run".into(),
text: "Upload the reel and its cover".into(),
cmd: cmd.into(),
pane: "p1".into(),
..Ask::default()
};
let cmd = r#"cd ~/Studio/reel && curl -s -F "file=@$f" https://tmpfiles.org/api/v1/upload"#;
let Asked::Turn(t) = ask(&mut conn, d, &run(&format!(" {cmd}\t ")), 1).unwrap() else {
panic!()
};
assert_eq!((t.kind.as_str(), t.cmd.as_str()), ("run", cmd));
assert_eq!(turn(&conn, d, t.id).unwrap().unwrap().cmd, cmd);
for bad in [
"",
" ",
"echo one\necho two",
"echo hi\rmore",
"echo \u{1b}[201~ typed as keys",
"echo \u{9b}",
&"x".repeat(CMD_BYTES + 1),
] {
assert_eq!(
ask(&mut conn, d, &run(bad), 2).unwrap(),
Asked::BadCmd,
"{bad:?}"
);
}
assert!(matches!(
ask(&mut conn, d, &run(&"x".repeat(CMD_BYTES)), 3).unwrap(),
Asked::Turn(_)
));
let merge = Ask {
kind: "merge".into(),
cmd: "rm -rf ~".into(),
..run("")
};
let Asked::Turn(m) = ask(&mut conn, d, &merge, 4).unwrap() else {
panic!()
};
assert_eq!(m.cmd, "");
}
#[test]
fn suggestions_wait_settle_and_are_told_once() {
let mut conn = db();
let (d, _) = desk(&mut conn);
let desk_card = Suggest {
kind: "desk".into(),
folder: "/w/".into(),
why: "its own project".into(),
..Suggest::default()
};
assert_eq!(
suggest(&mut conn, d, &desk_card, 1).unwrap(),
Suggested::HasDesk(d)
);
let panel = Suggest {
kind: "panel".into(),
name: "test window".into(),
cmd: "snyvi serve --port 7871".into(),
why: "to try it".into(),
pane: "p1".into(),
..Suggest::default()
};
let mut ids = vec![];
for _ in 0..SUGGESTIONS_PER_DESK {
let Suggested::Card(c) = suggest(&mut conn, d, &panel, 1).unwrap() else {
panic!()
};
ids.push(c.id);
}
assert_eq!(suggest(&mut conn, d, &panel, 1).unwrap(), Suggested::Full);
settle(&conn, d, ids[0], "dismissed", 2).unwrap().unwrap();
assert!(unsettle(&conn, d, ids[0]).unwrap());
settle(&conn, d, ids[1], "opened", 3).unwrap().unwrap();
assert!(!unsettle(&conn, d, ids[1]).unwrap());
assert!(settle(&conn, d, ids[1], "dismissed", 4).unwrap().is_none());
assert_eq!(take_opened(&conn, d, "p1", 5).unwrap().len(), 1);
assert!(take_opened(&conn, d, "p1", 6).unwrap().is_empty());
}
#[test]
fn a_pr_is_a_number() {
assert_eq!(pr_number("#57").as_deref(), Some("57"));
assert_eq!(
pr_number("https://github.com/o/r/pull/57/").as_deref(),
Some("57")
);
assert_eq!(pr_number("57; rm -rf"), None);
assert!(branch_ok("claude/threads"));
assert!(!branch_ok("a..b"));
assert!(!branch_ok("-x"));
}