use super::common::{hyalo_no_hints, md, write_md};
use tempfile::TempDir;
fn setup() -> TempDir {
let tmp = TempDir::new().unwrap();
write_md(
tmp.path(),
"note.md",
md!(r"
---
title: Test Note
status: draft
tags:
- cli
- rust
---
# Heading One
First paragraph.
## Problem
Problem text line 1.
Problem text line 2.
## Solution
Solution text.
### Details
Nested details.
## Problem
Second problem section.
"),
);
write_md(
tmp.path(),
"no-frontmatter.md",
md!(r"
# Just a file
No frontmatter here.
"),
);
write_md(tmp.path(), "empty.md", "");
tmp
}
#[test]
fn read_full_body_text() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("# Heading One"));
assert!(stdout.contains("First paragraph."));
assert!(stdout.contains("## Problem"));
assert!(stdout.contains("## Solution"));
assert!(!stdout.contains("title: Test Note"));
assert!(!stdout.contains("---"));
}
#[test]
fn read_full_body_json() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--format", "json"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["results"]["file"], "note.md");
let content = json["results"]["content"].as_str().unwrap();
assert!(content.contains("# Heading One"));
assert!(content.contains("Problem text"));
assert!(json["results"].get("frontmatter").is_none());
}
#[test]
fn read_defaults_to_text_format() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(serde_json::from_str::<serde_json::Value>(&stdout).is_err());
assert!(stdout.contains("# Heading One"));
}
#[test]
fn read_section_exact_match() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--section", "Solution"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("## Solution"));
assert!(stdout.contains("Solution text."));
assert!(stdout.contains("### Details"));
assert!(stdout.contains("Nested details."));
assert!(!stdout.contains("Problem text"));
}
#[test]
fn read_section_with_hashes() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--section", "## Solution"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("## Solution"));
assert!(stdout.contains("Solution text."));
}
#[test]
fn read_section_case_insensitive() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--section", "solution"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("## Solution"));
}
#[test]
fn read_section_multiple_matches() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--section", "Problem"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Problem text line 1."));
assert!(stdout.contains("Second problem section."));
}
#[test]
fn read_section_no_match() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--section", "Nonexistent"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("section not found"));
}
#[test]
fn read_section_no_substring_match() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--section", "XYZ"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("section not found"));
}
#[test]
fn read_section_substring_match() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--section", "Prob"])
.output()
.unwrap();
assert!(output.status.success());
let body = String::from_utf8(output.stdout).unwrap();
assert!(body.contains("Problem text line 1"));
}
#[test]
fn read_section_with_count_suffix() {
let tmp = TempDir::new().unwrap();
write_md(
tmp.path(),
"tasks.md",
md!(r"
# My File
## Tasks [4/4]
- [x] Done task
"),
);
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "tasks.md", "--section", "Tasks"])
.output()
.unwrap();
assert!(output.status.success());
let body = String::from_utf8(output.stdout).unwrap();
assert!(body.contains("Done task"));
}
#[test]
fn read_lines_range() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args([
"read", "--file", "note.md", "--lines", "1:3", "--format", "json",
])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let content = json["results"]["content"].as_str().unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines[0], "# Heading One");
assert_eq!(lines[2], "First paragraph.");
assert_eq!(lines.len(), 3);
}
#[test]
fn read_lines_single() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args([
"read", "--file", "note.md", "--lines", "1", "--format", "json",
])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let content = json["results"]["content"].as_str().unwrap();
assert_eq!(content, "# Heading One");
}
#[test]
fn read_lines_open_end() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--lines", "1:"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("# Heading One"));
assert!(stdout.contains("Second problem section."));
}
#[test]
fn read_lines_open_start() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--lines", ":2"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2);
}
#[test]
fn read_lines_invalid() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--lines", "abc"])
.output()
.unwrap();
assert!(!output.status.success());
}
#[test]
fn read_frontmatter_only() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--frontmatter"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("---"));
assert!(stdout.contains("title: Test Note"));
assert!(!stdout.contains("# Heading One"));
}
#[test]
fn read_frontmatter_with_section() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args([
"read",
"--file",
"note.md",
"--frontmatter",
"--section",
"Solution",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("title: Test Note"));
assert!(stdout.contains("## Solution"));
}
#[test]
fn read_frontmatter_json() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args([
"read",
"--file",
"note.md",
"--frontmatter",
"--format",
"json",
])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["results"]["frontmatter"]["title"], "Test Note");
assert_eq!(json["results"]["frontmatter"]["status"], "draft");
}
#[test]
fn read_frontmatter_with_lines() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args([
"read",
"--file",
"note.md",
"--frontmatter",
"--lines",
"1:2",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("title: Test Note"));
assert!(stdout.contains("# Heading One"));
}
#[test]
fn read_file_not_found() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "missing.md"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("file not found"));
}
#[test]
fn read_file_requires_file_flag() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read"])
.output()
.unwrap();
assert!(!output.status.success());
}
#[test]
fn read_no_frontmatter_file() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "no-frontmatter.md"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("# Just a file"));
assert!(stdout.contains("No frontmatter here."));
}
#[test]
fn read_empty_file() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "empty.md"])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
fn read_frontmatter_only_file_returns_empty_body() {
let tmp = TempDir::new().unwrap();
write_md(
tmp.path(),
"fm-only.md",
"---\ntitle: Frontmatter Only\nstatus: draft\n---\n",
);
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "fm-only.md"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.trim().is_empty(),
"expected empty body for frontmatter-only file, got: {stdout:?}"
);
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "fm-only.md", "--format", "json"])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
panic!(
"invalid JSON: {e}\nstdout: {}",
String::from_utf8_lossy(&output.stdout)
)
});
let content = json["results"]["content"]
.as_str()
.expect("field 'content' should be a string");
assert!(
content.trim().is_empty(),
"expected empty content for frontmatter-only file, got: {content:?}"
);
}
#[test]
fn read_with_jq_explicit_json() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args([
"read",
"--file",
"note.md",
"--format",
"json",
"--jq",
".results.file",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert_eq!(stdout.trim(), "note.md");
}
#[test]
fn read_with_jq_auto_promotes_to_json() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "note.md", "--jq", ".results.file"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert_eq!(stdout.trim(), "note.md");
}
#[test]
fn read_section_json_output() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args([
"read",
"--file",
"note.md",
"--section",
"Solution",
"--format",
"json",
])
.output()
.unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let content = json["results"]["content"].as_str().unwrap();
assert!(content.contains("Solution text."));
assert!(content.contains("Nested details."));
}
#[test]
fn read_frontmatter_broken_errors() {
let tmp = TempDir::new().unwrap();
write_md(
tmp.path(),
"broken.md",
"---\ntitle: Unclosed\nNo closing delimiter here\n",
);
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "broken.md", "--frontmatter"])
.output()
.unwrap();
assert_eq!(
output.status.code(),
Some(1),
"expected exit 1 (user error) for broken frontmatter; stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("unclosed frontmatter"),
"expected 'unclosed frontmatter' error; got: {stderr}"
);
}
#[test]
fn read_frontmatter_valid_works() {
let tmp = TempDir::new().unwrap();
write_md(
tmp.path(),
"valid.md",
"---\ntitle: Good File\nstatus: ok\n---\n# Body\n",
);
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["read", "--file", "valid.md", "--frontmatter"])
.output()
.unwrap();
assert!(
output.status.success(),
"expected success for valid frontmatter; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("title: Good File"),
"expected frontmatter in output; got: {stdout}"
);
}
#[test]
fn show_alias_works() {
let tmp = setup();
let output = hyalo_no_hints()
.args(["--dir", tmp.path().to_str().unwrap()])
.args(["show", "--file", "note.md"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("# Heading One"));
assert!(stdout.contains("First paragraph."));
}