use super::*;
#[test]
fn test_search_params_exclude_glob_schema_documents_brace_expansion() {
let schema = schemars::schema_for!(SearchParams);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["exclude_glob"]["description"]
.as_str()
.expect("`exclude_glob` property must have a `description` field in the generated schema");
assert!(
description.contains("brace expansion"),
"`exclude_glob` schema description must mention brace expansion so agents know \
they can use `{{**/*.spec.*,**/*.test.*}}` syntax.\nGot description: {description:?}"
);
assert!(
description.contains('{') && description.contains('}'),
"`exclude_glob` schema description must include a brace expansion example.\n\
Got description: {description:?}"
);
}
#[test]
fn test_read_params_max_lines_per_file_schema_documents_truncation() {
let schema = schemars::schema_for!(ReadParams);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["max_lines_per_file"]["description"]
.as_str()
.expect(
"`max_lines_per_file` property must have a `description` field in the generated schema",
);
assert!(
description.contains("batch mode and config/raw files"),
"`max_lines_per_file` schema description must document that it applies to batch mode and config/raw files.\nGot description: {description:?}"
);
}
#[test]
fn test_find_callers_callees_metadata_incoming_schema_documents_null_vs_empty() {
let schema = schemars::schema_for!(FindCallersCalleesMetadata);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["incoming"]["description"]
.as_str()
.expect("`incoming` property must have a `description` field in the generated schema");
assert!(
description.contains("null") || description.contains("`null`"),
"`incoming` schema description must explicitly mention `null` so agents know \
the field is absent when degraded, not just empty.\nGot: {description:?}"
);
assert!(
description.contains("[]") || description.contains("empty array"),
"`incoming` schema description must distinguish `[]` (confirmed zero) from `null` \
(unknown/degraded).\nGot: {description:?}"
);
assert!(
description.contains("degraded"),
"`incoming` schema description must reference the `degraded` field to explain \
when null is returned.\nGot: {description:?}"
);
}
#[test]
fn test_find_callers_callees_metadata_outgoing_schema_documents_null_vs_empty() {
let schema = schemars::schema_for!(FindCallersCalleesMetadata);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["outgoing"]["description"]
.as_str()
.expect("`outgoing` property must have a `description` field in the generated schema");
assert!(
description.contains("null") || description.contains("`null`"),
"`outgoing` schema description must mention `null`.\nGot: {description:?}"
);
assert!(
description.contains("[]") || description.contains("empty array"),
"`outgoing` schema description must distinguish `[]` from `null`.\nGot: {description:?}"
);
}
#[test]
fn test_find_all_references_metadata_references_schema_documents_null_vs_empty() {
let schema = schemars::schema_for!(FindAllReferencesMetadata);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["references"]["description"]
.as_str()
.expect("`references` property must have a `description` field in the generated schema");
assert!(
description.contains("null") || description.contains("`null`"),
"`references` schema description must mention `null`.\nGot: {description:?}"
);
assert!(
description.contains("[]") || description.contains("empty array"),
"`references` schema description must distinguish `[]` from `null`.\nGot: {description:?}"
);
}
#[test]
fn test_impact_summary_incoming_schema_documents_absent_semantics() {
let schema = schemars::schema_for!(ImpactSummary);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["incoming"]["description"]
.as_str()
.expect("`incoming` property must have a `description` field");
assert!(
description.contains("absent") || description.contains("omitted"),
"`ImpactSummary.incoming` uses `skip_serializing_if` — doc must say \
the field is absent (not null) when unavailable.\nGot: {description:?}"
);
}
#[test]
fn test_impact_summary_outgoing_schema_documents_absent_semantics() {
let schema = schemars::schema_for!(ImpactSummary);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["outgoing"]["description"]
.as_str()
.expect("`outgoing` property must have a `description` field");
assert!(
description.contains("absent") || description.contains("omitted"),
"`ImpactSummary.outgoing` uses `skip_serializing_if` — doc must say \
the field is absent (not null) when unavailable.\nGot: {description:?}"
);
}
#[test]
fn test_symbol_overview_response_references_schema_documents_absent_semantics() {
let schema = schemars::schema_for!(SymbolOverviewResponse);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["references"]["description"]
.as_str()
.expect("`references` property must have a `description` field");
assert!(
description.contains("absent") || description.contains("omitted"),
"`SymbolOverviewResponse.references` uses `skip_serializing_if` — doc \
must say the field is absent when unavailable.\nGot: {description:?}"
);
assert!(
description.contains("[]") || description.contains("empty array"),
"`SymbolOverviewResponse.references` must document that `[]` means \
LSP confirmed zero references.\nGot: {description:?}"
);
}
#[test]
fn test_find_callers_callees_degraded_doc_is_accurate() {
let schema = schemars::schema_for!(FindCallersCalleesMetadata);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["degraded"]["description"]
.as_str()
.expect("`degraded` property must have a `description` field");
assert!(
!description.contains("are `null` (not empty arrays)"),
"`degraded` doc must not claim incoming/outgoing are always null when degraded — \
grep fallback can produce heuristic results while degraded=true.\nGot: {description:?}"
);
}
#[test]
fn test_visibility_degraded_doc_matches_code() {
let schema = schemars::schema_for!(GetRepoMapMetadata);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
if let Some(description) =
schema_json["properties"]["visibility_degraded"]["description"].as_str()
{
assert!(
!description.contains("Always `true`") && !description.contains("always true"),
"`visibility_degraded` doc says 'always true' but code always sets None.\n\
Got: {description:?}"
);
}
}
#[test]
fn test_find_callers_callees_incoming_serialization_null_vs_empty() {
let meta_none = FindCallersCalleesMetadata {
incoming: None,
..Default::default()
};
let json = serde_json::to_value(&meta_none).expect("serialize");
assert!(
json["incoming"].is_null(),
"`incoming: None` must serialize as JSON `null`, got: {}",
json["incoming"]
);
let meta_empty = FindCallersCalleesMetadata {
incoming: Some(vec![]),
..Default::default()
};
let json = serde_json::to_value(&meta_empty).expect("serialize");
assert!(
json["incoming"].is_array() && json["incoming"].as_array().unwrap().is_empty(),
"`incoming: Some(vec![])` must serialize as JSON `[]`, got: {}",
json["incoming"]
);
}
#[test]
fn test_impact_summary_incoming_serialization_absent_vs_empty() {
let summary = ImpactSummary {
incoming: None,
outgoing: None,
degraded: true,
};
let json = serde_json::to_value(&summary).expect("serialize");
let obj = json.as_object().expect("object");
assert!(
!obj.contains_key("incoming"),
"`ImpactSummary.incoming: None` must be ABSENT from JSON, \
but it was present: {json}"
);
let summary_empty = ImpactSummary {
incoming: Some(vec![]),
outgoing: None,
degraded: false,
};
let json = serde_json::to_value(&summary_empty).expect("serialize");
assert!(
json["incoming"].is_array() && json["incoming"].as_array().unwrap().is_empty(),
"`ImpactSummary.incoming: Some(vec![])` must serialize as `[]`, got: {}",
json["incoming"]
);
}
#[test]
fn test_symbol_overview_response_references_serialization_absent_vs_empty() {
let resp = SymbolOverviewResponse {
references: None,
source: None,
impact: None,
files_referenced: 0,
degraded: true,
impact_degraded: false,
references_degraded: false,
degraded_reason: None,
actionable_guidance: None,
lsp_readiness: None,
warm_start_in_progress: None,
};
let json = serde_json::to_value(&resp).expect("serialize");
let obj = json.as_object().expect("object");
assert!(
!obj.contains_key("references"),
"`SymbolOverviewResponse.references: None` must be ABSENT from JSON, \
but it was present: {json}"
);
}
#[test]
fn test_default_value_helpers() {
assert_eq!(default_path_glob(), "**/*");
assert_eq!(default_max_results(), 50);
assert_eq!(default_context_lines(), 2);
assert_eq!(default_repo_map_path(), ".");
assert_eq!(default_max_tokens(), 16_000);
assert_eq!(default_max_tokens_per_file(), 2_000);
assert_eq!(default_max_depth(), 3);
assert_eq!(default_max_references(), 50);
assert_eq!(default_max_dependencies(), 50);
assert_eq!(default_start_line(), 1);
assert_eq!(default_max_lines(), 500);
assert_eq!(default_detail_level(), "compact");
assert!(default_group_by_file());
assert_eq!(default_filter_mode(), FilterMode::CodeOnly);
}
#[test]
fn test_filepath_alias_deserialization() {
let json_data = serde_json::json!({
"path": "src/lib.rs",
"start_line": 10,
});
let read_params: ReadParams = serde_json::from_value(json_data).unwrap();
assert_eq!(read_params.filepath, Some("src/lib.rs".to_string()));
}
#[test]
fn test_degraded_reason_serde_roundtrip() {
use pathfinder_common::types::DegradedReason;
let variants = vec![
(DegradedReason::NoLsp, "\"no_lsp\""),
(
DegradedReason::LspWarmupEmptyUnverified,
"\"lsp_warmup_empty_unverified\"",
),
(
DegradedReason::LspWarmupGrepFallback,
"\"lsp_warmup_grep_fallback\"",
),
(
DegradedReason::LspTimeoutGrepFallback,
"\"lsp_timeout_grep_fallback\"",
),
(
DegradedReason::LspErrorGrepFallback,
"\"lsp_error_grep_fallback\"",
),
(
DegradedReason::NoLspGrepFallback,
"\"no_lsp_grep_fallback\"",
),
(
DegradedReason::GrepFallbackFileScoped,
"\"grep_fallback_file_scoped\"",
),
(
DegradedReason::GrepFallbackImplScoped,
"\"grep_fallback_impl_scoped\"",
),
(
DegradedReason::GrepFallbackGlobal,
"\"grep_fallback_global\"",
),
(
DegradedReason::GrepFallbackDependencies,
"\"grep_fallback_dependencies\"",
),
(
DegradedReason::UnsupportedLanguageFilterBypassed,
"\"unsupported_language_filter_bypassed\"",
),
(
DegradedReason::UnsupportedLanguage,
"\"unsupported_language\"",
),
(DegradedReason::GitError, "\"git_error\""),
];
for (variant, expected_json) in variants {
let serialized = serde_json::to_string(&variant).expect("serialize DegradedReason");
assert_eq!(
serialized, expected_json,
"serialize mismatch for {variant:?}"
);
let deserialized: DegradedReason =
serde_json::from_str(&serialized).expect("deserialize DegradedReason");
assert_eq!(deserialized, variant, "roundtrip mismatch for {variant:?}");
}
}
#[test]
fn test_detail_enum_serde() {
let cases = vec![
("\"structure\"", "structure"),
("\"files\"", "files"),
("\"symbols\"", "symbols"),
];
for (json_str, label) in cases {
let detail: Detail = serde_json::from_str(json_str)
.unwrap_or_else(|e| panic!("failed to deserialize Detail from {json_str}: {e}"));
match (label, &detail) {
("structure", Detail::Structure)
| ("files", Detail::Files)
| ("symbols", Detail::Symbols) => {}
_ => panic!("unexpected Detail variant for {label}: {detail:?}"),
}
}
}
#[test]
fn test_search_mode_serde() {
let cases = vec![
("\"text\"", "text"),
("\"symbol\"", "symbol"),
("\"regex\"", "regex"),
];
for (json_str, label) in cases {
let mode: SearchMode = serde_json::from_str(json_str)
.unwrap_or_else(|e| panic!("failed to deserialize SearchMode from {json_str}: {e}"));
match (label, &mode) {
("text", SearchMode::Text)
| ("symbol", SearchMode::Symbol)
| ("regex", SearchMode::Regex) => {}
_ => panic!("unexpected SearchMode variant for {label}: {mode:?}"),
}
}
}
#[test]
fn test_trace_scope_serde() {
let cases = vec![
("\"callers\"", "callers"),
("\"references\"", "references"),
("\"overview\"", "overview"),
];
for (json_str, label) in cases {
let scope: TraceScope = serde_json::from_str(json_str)
.unwrap_or_else(|e| panic!("failed to deserialize TraceScope from {json_str}: {e}"));
match (label, &scope) {
("callers", TraceScope::Callers)
| ("references", TraceScope::References)
| ("overview", TraceScope::Overview) => {}
_ => panic!("unexpected TraceScope variant for {label}: {scope:?}"),
}
}
}
#[test]
fn test_search_params_default() {
let params = SearchParams::default();
assert_eq!(params.query, "");
assert!(matches!(params.mode, SearchMode::Text));
assert_eq!(params.path_glob, "**/*");
assert_eq!(params.max_results, 50);
assert_eq!(params.context_lines, 2);
assert!(params.known_files.is_empty());
assert_eq!(params.exclude_glob, ExcludeGlob::default());
assert_eq!(params.offset, 0);
assert!(params.kind.is_none());
assert!(params.group_by_file);
assert!(matches!(params.filter_mode, FilterMode::CodeOnly));
}
#[test]
fn test_inspect_params_default() {
let params = InspectParams::default();
assert_eq!(params.semantic_path, None);
assert_eq!(params.semantic_paths, None); assert!(!params.include_dependencies);
assert_eq!(params.max_dependencies, 50);
assert!(!params.include_imports);
}
#[test]
fn test_trace_params_default() {
let params = TraceParams::default();
assert_eq!(params.semantic_path, "");
assert!(matches!(params.scope, TraceScope::Callers));
assert_eq!(params.max_depth, 3);
assert_eq!(params.max_references, 50);
assert_eq!(params.offset, 0);
}
#[test]
fn test_explore_depth_default() {
assert_eq!(default_explore_depth(), 3);
}
#[test]
fn test_is_false_true_value() {
assert!(!is_false(&true));
}
#[test]
fn test_is_false_false_value() {
assert!(is_false(&false));
}
#[test]
fn test_symbol_overview_response_skip_empty_fields() {
let resp = SymbolOverviewResponse {
source: None,
impact: None,
references: None,
files_referenced: 0,
degraded: false,
impact_degraded: false,
references_degraded: false,
degraded_reason: None,
actionable_guidance: None,
lsp_readiness: None,
warm_start_in_progress: None,
};
let json = serde_json::to_value(&resp).expect("serialize SymbolOverviewResponse");
let obj = json.as_object().expect("should be JSON object");
assert!(
!obj.contains_key("source"),
"source should be absent when None"
);
assert!(
!obj.contains_key("impact"),
"impact should be absent when None"
);
assert!(
!obj.contains_key("references"),
"references should be absent when None"
);
assert!(
!obj.contains_key("degraded_reason"),
"degraded_reason should be absent when None"
);
assert!(
!obj.contains_key("actionable_guidance"),
"actionable_guidance should be absent when None"
);
assert!(
!obj.contains_key("lsp_readiness"),
"lsp_readiness should be absent when None"
);
assert!(
!obj.contains_key("warm_start_in_progress"),
"warm_start_in_progress should be absent when None"
);
assert!(
!obj.contains_key("impact_degraded"),
"impact_degraded should be absent when false"
);
assert!(
!obj.contains_key("references_degraded"),
"references_degraded should be absent when false"
);
assert!(obj.contains_key("files_referenced"));
assert!(obj.contains_key("degraded"));
}
#[test]
fn test_find_all_references_metadata_default_roundtrip() {
let meta = FindAllReferencesMetadata::default();
let json = serde_json::to_value(&meta).expect("serialize FindAllReferencesMetadata");
let obj = json.as_object().expect("should be JSON object");
assert!(
obj.contains_key("references"),
"references should be present (defaults to None/null)"
);
assert!(obj.contains_key("files_referenced"));
assert!(obj.contains_key("degraded"));
assert!(
!obj.contains_key("truncated"),
"truncated should be absent when false (default)"
);
assert!(
!obj.contains_key("degraded_reason"),
"degraded_reason should be absent when None"
);
assert!(!obj.contains_key("actionable_guidance"));
assert!(!obj.contains_key("lsp_readiness"));
assert!(!obj.contains_key("duration_ms"));
assert!(!obj.contains_key("resolution_strategy"));
assert!(!obj.contains_key("hint"));
}
#[test]
fn test_source_symbol_serde_missing_children() {
let json_str = r#"{"name":"foo","semantic_path":"foo::bar","kind":"function","start_line":10,"end_line":20}"#;
let symbol: SourceSymbol = serde_json::from_str(json_str).expect("deserialize SourceSymbol");
assert_eq!(symbol.name, "foo");
assert_eq!(symbol.semantic_path, "foo::bar");
assert_eq!(symbol.kind, "function");
assert_eq!(symbol.start_line, 10);
assert_eq!(symbol.end_line, 20);
assert!(symbol.children.is_empty());
let serialized = serde_json::to_string(&symbol).expect("serialize SourceSymbol");
assert!(!serialized.contains("children"));
}
#[test]
fn test_explore_params_visibility_schema_description() {
let schema = schemars::schema_for!(ExploreParams);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["visibility"]["description"]
.as_str()
.expect("`visibility` property must have a `description` field");
assert!(
description.contains("public") || description.contains("all"),
"`visibility` schema description must document the possible options.\nGot: {description:?}"
);
}
#[test]
fn test_explore_params_extensions_schema_descriptions() {
let schema = schemars::schema_for!(ExploreParams);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let include_desc = schema_json["properties"]["include_extensions"]["description"]
.as_str()
.expect("`include_extensions` must have a description");
assert!(
include_desc.contains("\"rs\"") && include_desc.contains("\".rs\""),
"`include_extensions` description must contrast raw extensions (e.g. \"rs\") with dotted ones.\nGot: {include_desc:?}"
);
assert!(
include_desc.to_lowercase().contains("mutually exclusive") && include_desc.contains("error"),
"`include_extensions` description must explain mutual exclusion and error behavior.\nGot: {include_desc:?}"
);
let exclude_desc = schema_json["properties"]["exclude_extensions"]["description"]
.as_str()
.expect("`exclude_extensions` must have a description");
assert!(
exclude_desc.contains("\"rs\"") && exclude_desc.contains("\".rs\""),
"`exclude_extensions` description must contrast raw extensions (e.g. \"rs\") with dotted ones.\nGot: {exclude_desc:?}"
);
assert!(
exclude_desc.to_lowercase().contains("mutually exclusive") && exclude_desc.contains("error"),
"`exclude_extensions` description must explain mutual exclusion and error behavior.\nGot: {exclude_desc:?}"
);
}
#[test]
fn test_search_params_path_glob_schema_documents_brace_expansion() {
let schema = schemars::schema_for!(SearchParams);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["path_glob"]["description"]
.as_str()
.expect("`path_glob` must have a description");
assert!(
description.contains("brace expansion"),
"`path_glob` description must mention brace expansion.\nGot: {description:?}"
);
assert!(
description.contains('{') && description.contains('}'),
"`path_glob` description must include a brace expansion example.\nGot: {description:?}"
);
}
#[test]
fn test_explore_params_changed_since_schema_documents_empty_string_behavior() {
let schema = schemars::schema_for!(ExploreParams);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["changed_since"]["description"]
.as_str()
.expect("`changed_since` must have a description");
assert!(
description.contains("empty") && description.contains("entire"),
"`changed_since` description must document empty string / full repository mapping behavior.\nGot: {description:?}"
);
}
#[test]
fn test_find_callers_callees_test_callers_schema_documents_absent_vs_empty() {
let schema = schemars::schema_for!(FindCallersCalleesMetadata);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["test_callers"]["description"]
.as_str()
.expect("`test_callers` must have a description");
assert!(
description.contains("absent") || description.contains("omitted"),
"`test_callers` description must mention absent/omitted semantics.\nGot: {description:?}"
);
assert!(
description.contains("[]") || description.contains("empty array"),
"`test_callers` description must mention `[]`/empty array semantics.\nGot: {description:?}"
);
}
#[test]
fn test_get_semantic_path_result_serialization_absent_when_none() {
let result = GetSemanticPathResult {
semantic_path: None,
symbol: None,
file: "src/lib.rs".to_string(),
line: 42,
};
let json = serde_json::to_value(&result).expect("serialize");
let obj = json.as_object().expect("object");
assert!(
!obj.contains_key("semantic_path"),
"`semantic_path: None` must be ABSENT from JSON, but it was present: {json}"
);
assert!(
!obj.contains_key("symbol"),
"`symbol: None` must be ABSENT from JSON, but it was present: {json}"
);
assert_eq!(json["file"], "src/lib.rs");
assert_eq!(json["line"], 42);
}
#[test]
fn test_get_semantic_path_result_serialization_present_when_some() {
let result = GetSemanticPathResult {
semantic_path: Some("src/lib.rs::MyStruct.method".to_string()),
symbol: Some("MyStruct.method".to_string()),
file: "src/lib.rs".to_string(),
line: 42,
};
let json = serde_json::to_value(&result).expect("serialize");
assert_eq!(
json["semantic_path"], "src/lib.rs::MyStruct.method",
"`semantic_path` must be present when Some"
);
assert_eq!(
json["symbol"], "MyStruct.method",
"`symbol` must be present when Some"
);
}
#[test]
fn test_get_semantic_path_result_schema_documents_absent_semantics() {
let schema = schemars::schema_for!(GetSemanticPathResult);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let sp_desc = schema_json["properties"]["semantic_path"]["description"]
.as_str()
.expect("`semantic_path` must have a description");
assert!(
sp_desc.contains("absent") || sp_desc.contains("omitted"),
"`semantic_path` doc must say absent/omitted (not null) since it uses \
skip_serializing_if.\nGot: {sp_desc:?}"
);
let sym_desc = schema_json["properties"]["symbol"]["description"]
.as_str()
.expect("`symbol` must have a description");
assert!(
sym_desc.contains("absent") || sym_desc.contains("omitted"),
"`symbol` doc must say absent/omitted (not null) since it uses \
skip_serializing_if.\nGot: {sym_desc:?}"
);
}
#[test]
fn test_file_result_version_hash_schema_describes_short_format() {
let schema = schemars::schema_for!(FileResult);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["version_hash"]["description"]
.as_str()
.expect("`version_hash` must have a description");
assert!(
!description.contains("SHA-256 hash"),
"`FileResult.version_hash` doc must NOT say 'SHA-256 hash' — the actual \
value is a 7-char truncated hex. Got: {description:?}"
);
assert!(
description.contains("7-char") || description.contains("short") || description.contains("fingerprint"),
"`FileResult.version_hash` doc must describe the short/fingerprint format. Got: {description:?}"
);
}
#[test]
fn test_search_result_group_version_hash_schema_describes_short_format() {
let schema = schemars::schema_for!(SearchResultGroup);
let schema_json = serde_json::to_value(&schema).expect("schema to JSON");
let description = schema_json["properties"]["version_hash"]["description"]
.as_str()
.expect("`version_hash` must have a description");
assert!(
!description.contains("SHA-256 hash"),
"`SearchResultGroup.version_hash` doc must NOT say 'SHA-256 hash' — the actual \
value is a 7-char truncated hex. Got: {description:?}"
);
assert!(
description.contains("7-char") || description.contains("short") || description.contains("fingerprint"),
"`SearchResultGroup.version_hash` doc must describe the short/fingerprint format. Got: {description:?}"
);
}