//! Integration tests for M3.7.8 symptom projection //! //! Validates that user queries normalize deterministically and can match //! extracted failure signatures from M3.7.7. use mem_core::project_symptom; #[test] fn a1_same_symptom_same_hash() { // Multiple query formulations should normalize to the same sym_sha let q1 = "npm ERR! ERESOLVE unable to resolve typescript"; let q2 = "npm error: eresolve dependency tree"; let q3 = "cannot resolve typescript (npm)"; let sym1 = project_symptom("npm", q1); let sym2 = project_symptom("npm", q2); let sym3 = project_symptom("npm", q3); // All should produce identical hashes assert_eq!(sym1.sym_sha, sym2.sym_sha, "q1 and q2 should hash identically"); assert_eq!(sym2.sym_sha, sym3.sym_sha, "q2 and q3 should hash identically"); // Verify they're not empty assert!(!sym1.sym_sha.is_empty()); assert_eq!(sym1.sym_sha.len(), 64, "SHA256 should be 64 hex chars"); } #[test] fn a2_abbrev_expansion() { // ERESOLVE, ERR, OOM, EACCES should expand correctly per tool let cases = vec![ ("npm", "npm ERESOLVE error", "resolve"), ("npm", "npm ERR unable to resolve", "error"), ("cargo", "error E0599 no method", "e0599"), ("cargo", "E0308 type mismatch", "e0308"), ("kubectl", "error CRD not found", "custom"), ]; for (tool, query, expected_substring) in cases { let symptom = project_symptom(tool, query); assert!( symptom.normalised.contains(expected_substring), "Query '{}' (tool {}) should contain '{}' after expansion, got: {}", query, tool, expected_substring, symptom.normalised ); } } #[test] fn a3_stop_word_removal() { // Stop words (able, to, the, of, etc.) should be removed let query = "npm is unable to resolve the typescript dependency tree"; let symptom = project_symptom("npm", query); // These specific stop words should NOT appear let stop_words_to_check = vec!["is", "unable", "to", "the", "of"]; for stop in stop_words_to_check { // Check that the stop word is NOT in the normalized form let has_stop = symptom.normalised.split_whitespace().any(|w| w == stop); assert!(!has_stop, "Stop word '{}' should be removed, got: {}", stop, symptom.normalised); } // Key terms should remain assert!( symptom.normalised.contains("resolve"), "Should contain 'resolve'" ); assert!( symptom.normalised.contains("dependency"), "Should contain 'dependency'" ); assert!( symptom.normalised.contains("typescript"), "Should contain 'typescript'" ); } #[test] fn a4_tool_consistency() { // Same error text under different tools should produce different hashes let query = "error 1234 module not found"; let sym_npm = project_symptom("npm", query); let sym_cargo = project_symptom("cargo", query); let sym_kubectl = project_symptom("kubectl", query); // All should have different hashes (tool name is part of identity) assert_ne!( sym_npm.sym_sha, sym_cargo.sym_sha, "npm and cargo should have different signatures" ); assert_ne!( sym_cargo.sym_sha, sym_kubectl.sym_sha, "cargo and kubectl should have different signatures" ); assert_ne!( sym_npm.sym_sha, sym_kubectl.sym_sha, "npm and kubectl should have different signatures" ); // Verify tool field assert_eq!(sym_npm.tool, "npm"); assert_eq!(sym_cargo.tool, "cargo"); assert_eq!(sym_kubectl.tool, "kubectl"); } #[test] fn a5_case_insensitive() { // "NPM ERROR" should hash identically to "npm error" let q1 = project_symptom("npm", "NPM ERROR ERESOLVE"); let q2 = project_symptom("npm", "npm error eresolve"); let q3 = project_symptom("npm", "NpM eRrOr ErEsOlVe"); assert_eq!(q1.sym_sha, q2.sym_sha, "Uppercase and lowercase should hash identically"); assert_eq!(q2.sym_sha, q3.sym_sha, "Mixed case should also hash identically"); } #[test] fn a6_keyword_order_irrelevant() { // "error npm resolve" should hash same as "npm resolve error" // (keywords are sorted alphabetically before hashing) let q1 = project_symptom("npm", "error npm resolve typescript"); let q2 = project_symptom("npm", "npm typescript resolve error"); let q3 = project_symptom("npm", "typescript error npm resolve"); assert_eq!( q1.sym_sha, q2.sym_sha, "Different keyword order should hash identically" ); assert_eq!( q2.sym_sha, q3.sym_sha, "All permutations should hash identically" ); // But the normalised form should be sorted let expected_sorted = "error npm resolve typescript"; assert_eq!(q1.normalised, expected_sorted, "Should be sorted alphabetically"); } #[test] fn test_matches_signature() { // SymptomVector::matches_signature should compare sym_sha let symptom = project_symptom("npm", "npm error resolve dependency"); assert!( symptom.matches_signature(&symptom.sym_sha), "Should match its own signature" ); assert!( !symptom.matches_signature("different_hash_value"), "Should not match different hash" ); assert!( !symptom.matches_signature(&symptom.sym_sha[0..32]), "Should not match partial hash" ); } #[test] fn test_npm_real_world_scenarios() { // Real-world npm error scenarios let queries = vec![ "npm ERR! code ERESOLVE", "npm ERR! ERESOLVE unable to resolve dependency tree", "npm error: cannot find module @types/node", "npm E404 not found", "permission denied (EACCES) npm ERR!", ]; for query in &queries { let symptom = project_symptom("npm", query); assert!(!symptom.sym_sha.is_empty(), "Query '{}' produced empty hash", query); assert_eq!(symptom.tool, "npm"); assert!(!symptom.normalised.is_empty()); assert!(symptom.confidence > 0.0); } } #[test] fn test_cargo_real_world_scenarios() { // Real-world cargo error scenarios let queries = vec![ "error E0599: no method named 'foo' found for type 'Bar'", "error E0308: mismatched types", "error E0433: cannot find function 'println'", "cargo error: failed to compile", ]; for query in &queries { let symptom = project_symptom("cargo", query); assert!(!symptom.sym_sha.is_empty(), "Query '{}' produced empty hash", query); assert_eq!(symptom.tool, "cargo"); assert!(!symptom.normalised.is_empty()); } } #[test] fn test_kubectl_real_world_scenarios() { // Real-world kubectl error scenarios let queries = vec![ "error: error validating YAML", "error: connection refused", "RBAC authorization error", "CRD not found custom resource definition", ]; for query in &queries { let symptom = project_symptom("kubectl", query); assert!(!symptom.sym_sha.is_empty(), "Query '{}' produced empty hash", query); assert_eq!(symptom.tool, "kubectl"); assert!(!symptom.normalised.is_empty()); } } #[test] fn test_confidence_scoring() { // More specific queries should have higher confidence let low_conf = project_symptom("npm", "error"); let medium_conf = project_symptom("npm", "npm error cannot resolve"); let high_conf = project_symptom("npm", "npm error ERESOLVE cannot resolve typescript module"); // Confidence should increase with specificity println!("low_conf: {}", low_conf.confidence); println!("medium_conf: {}", medium_conf.confidence); println!("high_conf: {}", high_conf.confidence); assert!( medium_conf.confidence >= low_conf.confidence, "More keywords should have equal or higher confidence" ); assert!( high_conf.confidence >= medium_conf.confidence, "Even more keywords should have equal or higher confidence" ); } #[test] fn test_raw_query_preserved() { // Original query should be preserved for logging let original = "npm ERR! ERESOLVE unable to resolve typescript"; let symptom = project_symptom("npm", original); assert_eq!(symptom.raw_query, original); } #[test] fn test_keywords_vector_populated() { // Keywords should be extracted and deduplicated let symptom = project_symptom("npm", "npm error resolve typescript dependency"); assert!(!symptom.keywords.is_empty(), "Keywords should not be empty"); // Check for specific keywords assert!( symptom.keywords.contains(&"npm".to_string()), "Should contain 'npm' keyword" ); assert!( symptom.keywords.contains(&"error".to_string()), "Should contain 'error' keyword" ); // Keywords should be sorted let mut sorted_keywords = symptom.keywords.clone(); sorted_keywords.sort(); assert_eq!( symptom.keywords, sorted_keywords, "Keywords should be sorted" ); } #[test] fn test_deterministic_across_multiple_calls() { // Calling project_symptom multiple times with same input should always produce same output let query = "npm unable to resolve typescript dependency tree"; let mut hashes = Vec::new(); for _ in 0..10 { let symptom = project_symptom("npm", query); hashes.push(symptom.sym_sha); } // All hashes should be identical for hash in &hashes[1..] { assert_eq!(hash, &hashes[0], "All calls should produce identical hash"); } } #[test] fn test_special_characters_handled() { // Queries with special characters should be tokenized properly let queries = vec![ "npm ERR! code:ERESOLVE!", "npm (error) [ERESOLVE]", "npm error \"cannot resolve\"", "npm error: can't resolve 'module'", ]; for query in queries { let symptom = project_symptom("npm", query); assert!(!symptom.sym_sha.is_empty()); assert!(!symptom.normalised.is_empty()); } } #[test] fn test_error_code_extraction() { // Error codes like E0599, EACCES should be extracted let symptom = project_symptom("cargo", "error E0599 no method found"); assert!( symptom.normalised.contains("e0599"), "Error code E0599 should be extracted" ); }