//! Integration tests for `jsonl` cube.
//!
//! Constructor Pattern: one scenario per test. We mount `jsonl.rs` via
//! `#[path]` (same pattern as `integration.rs`) so no library crate
//! surface is required. Fixtures are written to `tempfile::TempDir` —
//! nothing persists after the test.
#[path = "../src/jsonl.rs"]
mod jsonl;
use jsonl::parse_user_lines;
use std::fs;
use tempfile::TempDir;
// ---------------------------------------------------------------
// 1. mixed_shapes — 5 lines: 2 user-string, 1 user-array-blocks,
// 1 assistant, 1 local-command echo. Expect 3 user lines.
// ---------------------------------------------------------------
#[test]
fn mixed_shapes() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("session.jsonl");
let body = [
// 1. top-level type=user, content=string
r#"{"type":"user","role":"user","content":"посмотри генезис опять"}"#,
// 2. nested message.role=user, content=string
r#"{"type":"user","message":{"role":"user","content":"нет, делай так — хватит уже"},"timestamp":"2026-04-22T03:14:15Z"}"#,
// 3. nested message.role=user, content=array of text blocks
r#"{"type":"user","message":{"role":"user","content":[{"type":"text","text":"стоп. почему ты опять полез не туда?"}]}}"#,
// 4. assistant — must be ignored
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"let me reconsider."}]}}"#,
// 5. local-command echo — must be filtered
r#"{"type":"user","role":"user","content":"you ran /effort"}"#,
]
.join("\n");
fs::write(&path, body).unwrap();
let out = parse_user_lines(&path).unwrap();
assert_eq!(out.len(), 3, "expected 3 user lines, got {}: {:#?}", out.len(), out);
assert!(out[0].text.contains("генезис"), "line 1: {}", out[0].text);
assert_eq!(out[0].line_no, 1);
assert!(out[0].timestamp.is_none());
assert!(out[1].text.contains("хватит"), "line 2: {}", out[1].text);
assert_eq!(out[1].line_no, 2);
assert_eq!(
out[1].timestamp.as_deref(),
Some("2026-04-22T03:14:15Z"),
"timestamp passthrough"
);
assert!(out[2].text.contains("почему ты опять"), "line 3: {}", out[2].text);
assert_eq!(out[2].line_no, 3);
let joined = out.iter().map(|l| l.text.as_str()).collect::>().join("|");
assert!(!joined.contains("reconsider"), "assistant leaked: {joined}");
assert!(!joined.contains("local-command-caveat"), "echo leaked: {joined}");
}
// ---------------------------------------------------------------
// 2. malformed_line_skipped — one bad JSON line in the middle must
// NOT abort parsing; parser returns what it could extract.
// ---------------------------------------------------------------
#[test]
fn malformed_line_skipped() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("malformed.jsonl");
let body = [
r#"{"type":"user","content":"я же уже просил — не трогай это"}"#,
r#"{this is not valid json at all"#, // line 2 — malformed
r#"{"type":"user","content":"опять куда ты полез"}"#,
]
.join("\n");
fs::write(&path, body).unwrap();
let out = parse_user_lines(&path).unwrap();
assert_eq!(out.len(), 2, "malformed line must not abort: {out:?}");
assert!(out[0].text.contains("уже просил"));
assert_eq!(out[0].line_no, 1);
assert!(out[1].text.contains("опять"));
assert_eq!(out[1].line_no, 3, "line_no must reflect true file position");
}
// ---------------------------------------------------------------
// 3. empty_file_yields_empty_vec
// ---------------------------------------------------------------
#[test]
fn empty_file_yields_empty_vec() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("empty.jsonl");
fs::write(&path, "").unwrap();
let out = parse_user_lines(&path).unwrap();
assert!(out.is_empty(), "empty file should yield empty Vec, got {out:?}");
}
// ---------------------------------------------------------------
// 4. assistant_only_yields_empty_vec
// ---------------------------------------------------------------
#[test]
fn assistant_only_yields_empty_vec() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("assistant-only.jsonl");
let body = [
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hello"}]}}"#,
r#"{"type":"assistant","message":{"role":"assistant","content":"plain string"}}"#,
r#"{"type":"user","content":"ignore me"}"#,
]
.join("\n");
fs::write(&path, body).unwrap();
let out = parse_user_lines(&path).unwrap();
assert!(
out.is_empty(),
"no real user messages → empty Vec, got {out:?}"
);
}