Skip to content

Commit 9ef82be

Browse files
committed
test(copilot): exorcise carryover, no ghosts between turns
- Lock down no message carryover across client, event loop, and UI - Verify EOS clears awaiting state and files are sent each turn Signed-off-by: Jessie Frazelle <[email protected]>
1 parent 9dae23c commit 9ef82be

File tree

3 files changed

+123
-0
lines changed

3 files changed

+123
-0
lines changed

src/ml/copilot/run.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,55 @@ mod tests {
610610
}
611611
}
612612

613+
#[test]
614+
fn client_message_content_does_not_carryover() {
615+
let mut files = std::collections::HashMap::new();
616+
files.insert("main.kcl".to_string(), b"cube(1)".to_vec());
617+
let project_name = Some("proj".to_string());
618+
619+
let (m1, _) = build_user_message("first".into(), &files, &project_name);
620+
let v1 = serde_json::to_value(&m1).unwrap();
621+
assert_eq!(v1.get("content").unwrap().as_str().unwrap(), "first");
622+
623+
let (m2, _) = build_user_message("second".into(), &files, &project_name);
624+
let v2 = serde_json::to_value(&m2).unwrap();
625+
assert_eq!(v2.get("content").unwrap().as_str().unwrap(), "second");
626+
}
627+
628+
#[test]
629+
fn event_loop_two_submits_send_verbatim_and_files() {
630+
let mut app = App::new();
631+
let mut files = std::collections::HashMap::new();
632+
files.insert("main.kcl".to_string(), b"cube(1)".to_vec());
633+
let project_name = Some("proj".to_string());
634+
635+
// First submission
636+
let s1 = app
637+
.try_submit("hi im jess".into(), true)
638+
.expect("first should send now");
639+
let (m1, _len1) = build_user_message(s1, &files, &project_name);
640+
let v1 = serde_json::to_value(&m1).unwrap();
641+
assert_eq!(v1.get("content").unwrap().as_str().unwrap(), "hi im jess");
642+
let files1 = v1.get("current_files").unwrap().as_object().unwrap();
643+
assert!(files1.contains_key("main.kcl"));
644+
645+
// Simulate end-of-stream to clear awaiting state
646+
assert!(app.on_end_of_stream(true).is_none());
647+
648+
// Second submission
649+
let s2 = app
650+
.try_submit("can you edit the kcl code to make the button blue".into(), true)
651+
.expect("second should send now");
652+
let (m2, _len2) = build_user_message(s2, &files, &project_name);
653+
let v2 = serde_json::to_value(&m2).unwrap();
654+
assert_eq!(
655+
v2.get("content").unwrap().as_str().unwrap(),
656+
"can you edit the kcl code to make the button blue"
657+
);
658+
let files2 = v2.get("current_files").unwrap().as_object().unwrap();
659+
assert!(files2.contains_key("main.kcl"));
660+
}
661+
613662
#[test]
614663
fn tool_output_error_displays_error() {
615664
let mut app = App::new();

src/ml/copilot/state.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,26 @@ mod tests {
368368
let _ = app.handle_key_action(key(KeyCode::PageUp, KeyModifiers::NONE));
369369
assert!(app.msg_scroll <= 10);
370370
}
371+
372+
#[test]
373+
fn lifecycle_two_submits_no_carryover() {
374+
let mut app = App::new();
375+
// Files not ready yet; first submit queues
376+
assert!(app.try_submit("first".into(), false).is_none());
377+
assert_eq!(app.queue.len(), 1);
378+
// Scan done → send first
379+
let s1 = app.on_scan_done();
380+
assert_eq!(s1.as_deref(), Some("first"));
381+
assert!(app.awaiting_response);
382+
383+
// EOS → allow next
384+
let s_none = app.on_end_of_stream(true);
385+
assert!(s_none.is_none());
386+
assert!(!app.awaiting_response);
387+
388+
// Second submit should return exactly "second"
389+
let s2 = app.try_submit("second".into(), true);
390+
assert_eq!(s2.as_deref(), Some("second"));
391+
assert!(app.queue.is_empty());
392+
}
371393
}

src/ml/copilot/ui.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,56 @@ mod tests {
346346
}
347347
assert!(!row0b.contains("Proposed Changes"));
348348
}
349+
350+
#[test]
351+
fn two_turns_no_prepend() {
352+
let backend = TestBackend::new(60, 12);
353+
let mut terminal = Terminal::new(backend).unwrap();
354+
let mut app = App::new();
355+
// Turn 1
356+
app.events.push(ChatEvent::User("first".into()));
357+
app.events
358+
.push(ChatEvent::Server(kittycad::types::MlCopilotServerMessage::Delta {
359+
delta: "Hello".into(),
360+
}));
361+
app.events
362+
.push(ChatEvent::Server(kittycad::types::MlCopilotServerMessage::Delta {
363+
delta: ", Jess".into(),
364+
}));
365+
app.events.push(ChatEvent::Server(
366+
kittycad::types::MlCopilotServerMessage::EndOfStream { whole_response: None },
367+
));
368+
// Turn 2
369+
app.events.push(ChatEvent::User("second".into()));
370+
app.events
371+
.push(ChatEvent::Server(kittycad::types::MlCopilotServerMessage::Delta {
372+
delta: "Hi again".into(),
373+
}));
374+
app.events.push(ChatEvent::Server(
375+
kittycad::types::MlCopilotServerMessage::EndOfStream { whole_response: None },
376+
));
377+
378+
terminal.draw(|f| draw(f, &app)).unwrap();
379+
let buf = terminal.backend().buffer();
380+
let area = buf.area;
381+
let mut content = String::new();
382+
for y in 0..area.height {
383+
for x in 0..area.width {
384+
content.push(buf.get(x, y).symbol().chars().next().unwrap_or(' '));
385+
}
386+
content.push('\n');
387+
}
388+
389+
// Ensure ordering and no prepend of first into second
390+
let p1 = content.find("You> first").expect("missing first user line");
391+
let p2 = content.find("You> second").expect("missing second user line");
392+
assert!(p2 > p1);
393+
assert!(
394+
!content.contains("firstYou> second"),
395+
"first message prepended to second user line"
396+
);
397+
// Assistant segments present separately
398+
assert!(content.contains("ML-ephant> Hello, Jess"));
399+
assert!(content.contains("ML-ephant> Hi again"));
400+
}
349401
}

0 commit comments

Comments
 (0)