Skip to content

Commit 98f7e08

Browse files
AlexMikhalevclaude
andcommitted
fix(cli): address code review follow-ups for ontology commands
- Remove unused _json parameter from handle_coverage - Restructure coverage exit path: use flag instead of duplicating format+print before process::exit(1), ensuring stdout flush - Add 10 unit tests for schema extraction and coverage in service_tests - Add 8 CLI integration tests for extract and coverage commands - Update help test to verify extract/coverage in command listing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cf73758 commit 98f7e08

3 files changed

Lines changed: 432 additions & 12 deletions

File tree

crates/terraphim_cli/src/main.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ async fn main() -> Result<()> {
323323
.await
324324
.context("Failed to initialize service")?;
325325

326+
// Track whether coverage check failed (exit code 1 after output)
327+
let mut coverage_below_threshold = false;
328+
326329
// Execute command
327330
let result = match cli.command {
328331
Some(Commands::Search { query, role, limit }) => {
@@ -351,24 +354,17 @@ async fn main() -> Result<()> {
351354
text,
352355
schema,
353356
threshold,
354-
json,
357+
json: _,
355358
}) => {
356-
let result = handle_coverage(&service, text, &schema, threshold, json).await;
357-
// Coverage uses exit code 1 when below threshold
359+
let result = handle_coverage(&service, text, &schema, threshold).await;
360+
// Check if coverage is below threshold for exit code
358361
if let Ok(val) = &result {
359362
if let Some(true) = val
360363
.get("signal")
361364
.and_then(|s| s.get("needs_review"))
362365
.and_then(|v| v.as_bool())
363366
{
364-
let formatted = match cli.format {
365-
OutputFormat::Json => serde_json::to_string(val)?,
366-
OutputFormat::JsonPretty => serde_json::to_string_pretty(val)?,
367-
OutputFormat::Text => format_as_text(val)
368-
.unwrap_or_else(|_| serde_json::to_string(val).unwrap()),
369-
};
370-
println!("{}", formatted);
371-
std::process::exit(1);
367+
coverage_below_threshold = true;
372368
}
373369
}
374370
result
@@ -394,6 +390,9 @@ async fn main() -> Result<()> {
394390
.unwrap_or_else(|_| serde_json::to_string(&output).unwrap()),
395391
};
396392
println!("{}", formatted);
393+
if coverage_below_threshold {
394+
std::process::exit(1);
395+
}
397396
Ok(())
398397
}
399398
Err(e) => {
@@ -682,7 +681,6 @@ async fn handle_coverage(
682681
text: String,
683682
schema_path: &str,
684683
threshold: f32,
685-
_json: bool,
686684
) -> Result<serde_json::Value> {
687685
let schema = terraphim_types::OntologySchema::load_from_file(schema_path)
688686
.map_err(|e| anyhow::anyhow!("Failed to load schema '{}': {}", schema_path, e))?;

crates/terraphim_cli/tests/cli_command_tests.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ fn test_cli_help() {
2626
.stdout(predicate::str::contains("graph"))
2727
.stdout(predicate::str::contains("replace"))
2828
.stdout(predicate::str::contains("find"))
29+
.stdout(predicate::str::contains("extract"))
30+
.stdout(predicate::str::contains("coverage"))
2931
.stdout(predicate::str::contains("thesaurus"))
3032
.stdout(predicate::str::contains("completions"));
3133
}
@@ -568,4 +570,218 @@ mod integration {
568570
// This test mainly verifies the flag is accepted
569571
assert!(output.status.success() || stderr.len() < 1000);
570572
}
573+
574+
#[test]
575+
fn test_extract_help() {
576+
cli_command()
577+
.args(["extract", "--help"])
578+
.assert()
579+
.success()
580+
.stdout(predicate::str::contains("text"))
581+
.stdout(predicate::str::contains("--role"))
582+
.stdout(predicate::str::contains("--json"))
583+
.stdout(predicate::str::contains("--schema"));
584+
}
585+
586+
#[test]
587+
fn test_coverage_help() {
588+
cli_command()
589+
.args(["coverage", "--help"])
590+
.assert()
591+
.success()
592+
.stdout(predicate::str::contains("text"))
593+
.stdout(predicate::str::contains("--schema"))
594+
.stdout(predicate::str::contains("--threshold"))
595+
.stdout(predicate::str::contains("--json"));
596+
}
597+
598+
#[test]
599+
#[serial]
600+
fn test_extract_with_schema() {
601+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
602+
let schema_path = std::path::PathBuf::from(&manifest_dir)
603+
.parent()
604+
.and_then(|p| p.parent())
605+
.unwrap()
606+
.join("crates/terraphim_types/test-fixtures/sample_ontology_schema.json");
607+
608+
let output = cli_command()
609+
.args([
610+
"extract",
611+
"This chapter covers the concept of knowledge graphs",
612+
"--schema",
613+
schema_path.to_str().unwrap(),
614+
])
615+
.output()
616+
.expect("Failed to execute command");
617+
618+
if output.status.success() {
619+
let stdout = String::from_utf8_lossy(&output.stdout);
620+
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
621+
assert!(
622+
parsed.is_ok(),
623+
"Extract --schema output should be valid JSON: {}",
624+
stdout
625+
);
626+
627+
if let Ok(json) = parsed {
628+
// SchemaSignal has entities, relationships, confidence
629+
assert!(json.get("entities").is_some(), "Should have entities field");
630+
assert!(
631+
json.get("confidence").is_some(),
632+
"Should have confidence field"
633+
);
634+
}
635+
}
636+
}
637+
638+
#[test]
639+
#[serial]
640+
fn test_coverage_with_full_coverage() {
641+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
642+
let schema_path = std::path::PathBuf::from(&manifest_dir)
643+
.parent()
644+
.and_then(|p| p.parent())
645+
.unwrap()
646+
.join("crates/terraphim_types/test-fixtures/sample_ontology_schema.json");
647+
648+
// Text that contains all 3 entity types: chapter, concept, knowledge graph
649+
let output = cli_command()
650+
.args([
651+
"coverage",
652+
"This chapter covers the concept and the knowledge graph",
653+
"--schema",
654+
schema_path.to_str().unwrap(),
655+
"--threshold",
656+
"0.7",
657+
])
658+
.output()
659+
.expect("Failed to execute command");
660+
661+
// Full coverage should exit 0
662+
let stdout = String::from_utf8_lossy(&output.stdout);
663+
if !stdout.is_empty() {
664+
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
665+
assert!(
666+
parsed.is_ok(),
667+
"Coverage output should be valid JSON: {}",
668+
stdout
669+
);
670+
671+
if let Ok(json) = parsed {
672+
assert!(json.get("signal").is_some(), "Should have signal field");
673+
assert!(
674+
json.get("matched_categories").is_some(),
675+
"Should have matched_categories field"
676+
);
677+
assert!(
678+
json.get("missing_categories").is_some(),
679+
"Should have missing_categories field"
680+
);
681+
assert!(
682+
json.get("schema_name").is_some(),
683+
"Should have schema_name field"
684+
);
685+
}
686+
}
687+
}
688+
689+
#[test]
690+
#[serial]
691+
fn test_coverage_below_threshold_exits_1() {
692+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
693+
let schema_path = std::path::PathBuf::from(&manifest_dir)
694+
.parent()
695+
.and_then(|p| p.parent())
696+
.unwrap()
697+
.join("crates/terraphim_types/test-fixtures/sample_ontology_schema.json");
698+
699+
// Text that matches NONE of the entity types
700+
let output = cli_command()
701+
.args([
702+
"coverage",
703+
"completely unrelated text about cooking recipes",
704+
"--schema",
705+
schema_path.to_str().unwrap(),
706+
"--threshold",
707+
"0.7",
708+
])
709+
.output()
710+
.expect("Failed to execute command");
711+
712+
// Zero coverage should exit with code 1
713+
assert!(
714+
!output.status.success(),
715+
"Coverage below threshold should exit with non-zero code"
716+
);
717+
718+
// But output should still be valid JSON
719+
let stdout = String::from_utf8_lossy(&output.stdout);
720+
if !stdout.is_empty() {
721+
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
722+
assert!(
723+
parsed.is_ok(),
724+
"Coverage output should be valid JSON even on exit 1: {}",
725+
stdout
726+
);
727+
728+
if let Ok(json) = parsed {
729+
let needs_review = json
730+
.get("signal")
731+
.and_then(|s| s.get("needs_review"))
732+
.and_then(|v| v.as_bool());
733+
assert_eq!(
734+
needs_review,
735+
Some(true),
736+
"needs_review should be true when below threshold"
737+
);
738+
}
739+
}
740+
}
741+
742+
#[test]
743+
#[serial]
744+
fn test_extract_command_json_mode() {
745+
let output = cli_command()
746+
.args(["extract", "rust async tokio", "--json"])
747+
.output()
748+
.expect("Failed to execute command");
749+
750+
if output.status.success() {
751+
let stdout = String::from_utf8_lossy(&output.stdout);
752+
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
753+
assert!(
754+
parsed.is_ok(),
755+
"Extract --json output should be valid JSON: {}",
756+
stdout
757+
);
758+
759+
// Should be an array of ExtractedEntity
760+
if let Ok(json) = parsed {
761+
assert!(json.is_array(), "Extract --json should return an array");
762+
}
763+
}
764+
}
765+
766+
#[test]
767+
fn test_coverage_missing_schema_arg() {
768+
// coverage requires --schema
769+
cli_command()
770+
.args(["coverage", "some text"])
771+
.assert()
772+
.failure();
773+
}
774+
775+
#[test]
776+
fn test_extract_with_nonexistent_schema() {
777+
cli_command()
778+
.args([
779+
"extract",
780+
"some text",
781+
"--schema",
782+
"/nonexistent/schema.json",
783+
])
784+
.assert()
785+
.failure();
786+
}
571787
}

0 commit comments

Comments
 (0)