feat: implement M3.7.8 symptom projection (250 LOC) + 22 tests (10 unit + 12 integration)
IMPLEMENTATION:
- crates/mem-core/src/symptom_projection.rs (250 LOC)
- project_symptom(tool, query) → SymptomVector
- Three-stage normalization:
- Stage 1: Extract keywords
- Stage 2: Normalize (stop words, abbreviations)
- Stage 3: Generate deterministic SHA256 hash
- Tool-specific abbreviation mappings (npm, cargo, kubectl, docker, go)
- Stop words list (30+ common words)
- Confidence scoring based on keyword specificity
TEST COVERAGE: 22 tests passing
- 10 unit tests in lib (determinism, abbreviations, stop words, tools, case, order)
- 12 integration tests (a1-a6 assertions from design doc)
- Real-world scenario tests (npm, cargo, kubectl)
- 100% deterministic hashing verified
INTEGRATION:
- Module exported in crates/mem-core/src/lib.rs
- All 43 existing mem-core tests still passing
- Ready for M3.7.4 context endpoint integration
DESIGN ASSERTIONS (all passing):
✅ a1: Same symptom = same hash (deterministic)
✅ a2: Abbreviation expansion (ERESOLVE → error resolve)
✅ a3: Stop word removal (is, unable, to, the)
✅ a4: Tool consistency (npm ≠ cargo for same error)
✅ a5: Case insensitive (NPM = npm)
✅ a6: Keyword order irrelevant (sorted before hash)
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
//! Integration tests for M3.7.8 symptom projection
|
||||
//! Tests the six main assertions from the design doc
|
||||
|
||||
use mem_core::project_symptom;
|
||||
|
||||
#[test]
|
||||
fn a1_same_symptom_same_hash() {
|
||||
// ASSERTION 1: Same query always produces identical hash (deterministic)
|
||||
let query = "npm ERR! ERESOLVE unable to resolve dependency tree";
|
||||
|
||||
let sym1 = project_symptom("npm", query);
|
||||
let sym2 = project_symptom("npm", query);
|
||||
let sym3 = project_symptom("npm", query);
|
||||
|
||||
assert_eq!(sym1.sym_sha, sym2.sym_sha);
|
||||
assert_eq!(sym2.sym_sha, sym3.sym_sha);
|
||||
assert_eq!(sym1.sym_sha.len(), 64); // SHA256 = 64 hex chars
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a2_abbrev_expansion() {
|
||||
// ASSERTION 2: Tool-specific abbreviations expand correctly
|
||||
// npm: ERESOLVE → error resolve
|
||||
let npm_query = "npm ERESOLVE error";
|
||||
let npm_sym = project_symptom("npm", npm_query);
|
||||
assert!(npm_sym.normalised.contains("resolve"), "ERESOLVE should expand to 'resolve'");
|
||||
|
||||
// cargo: E0599 → error 0599
|
||||
let cargo_query = "cargo E0599 method";
|
||||
let cargo_sym = project_symptom("cargo", cargo_query);
|
||||
assert!(cargo_sym.normalised.contains("0599"), "E0599 should expand");
|
||||
|
||||
// kubectl: CRD → custom resource definition
|
||||
let kubectl_query = "error CRD";
|
||||
let kubectl_sym = project_symptom("kubectl", kubectl_query);
|
||||
assert!(kubectl_sym.normalised.contains("custom"), "CRD should expand to custom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a3_stop_word_removal() {
|
||||
// ASSERTION 3: Stop words removed (is, unable, to, the, of, etc.)
|
||||
let query = "npm is unable to resolve the dependency";
|
||||
let symptom = project_symptom("npm", query);
|
||||
|
||||
// Stop words should not appear
|
||||
assert!(!symptom.normalised.split_whitespace().any(|w| w == "is"));
|
||||
assert!(!symptom.normalised.split_whitespace().any(|w| w == "unable"));
|
||||
assert!(!symptom.normalised.split_whitespace().any(|w| w == "to"));
|
||||
assert!(!symptom.normalised.split_whitespace().any(|w| w == "the"));
|
||||
|
||||
// Content should remain
|
||||
assert!(symptom.normalised.contains("npm"));
|
||||
assert!(symptom.normalised.contains("resolve"));
|
||||
assert!(symptom.normalised.contains("dependency"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a4_tool_consistency() {
|
||||
// ASSERTION 4: Tool name is part of identity
|
||||
// Same query, different tools = different hashes
|
||||
let query = "error 1234 module";
|
||||
|
||||
let npm_sym = project_symptom("npm", query);
|
||||
let cargo_sym = project_symptom("cargo", query);
|
||||
let kubectl_sym = project_symptom("kubectl", query);
|
||||
|
||||
assert_ne!(npm_sym.sym_sha, cargo_sym.sym_sha);
|
||||
assert_ne!(cargo_sym.sym_sha, kubectl_sym.sym_sha);
|
||||
assert_ne!(npm_sym.sym_sha, kubectl_sym.sym_sha);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a5_case_insensitive() {
|
||||
// ASSERTION 5: Case doesn't affect hash
|
||||
// (all normalized to lowercase)
|
||||
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);
|
||||
assert_eq!(q2.sym_sha, q3.sym_sha);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a6_keyword_order_irrelevant() {
|
||||
// ASSERTION 6: Keyword order irrelevant
|
||||
// (keywords 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 order should hash the same");
|
||||
assert_eq!(q2.sym_sha, q3.sym_sha, "Different order should hash the same");
|
||||
|
||||
// Verify keywords are sorted
|
||||
let mut words: Vec<_> = q1.normalised.split_whitespace().collect();
|
||||
words.sort();
|
||||
let normalised_words: Vec<_> = q1.normalised.split_whitespace().collect();
|
||||
assert_eq!(words, normalised_words, "Keywords should be sorted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_across_calls() {
|
||||
// Multiple calls with same query = identical results
|
||||
let query = "npm unable to resolve typescript dependency";
|
||||
let mut hashes = Vec::new();
|
||||
|
||||
for _ in 0..10 {
|
||||
let sym = project_symptom("npm", query);
|
||||
hashes.push(sym.sym_sha);
|
||||
}
|
||||
|
||||
for hash in &hashes[1..] {
|
||||
assert_eq!(hash, &hashes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_real_world_npm() {
|
||||
// Real npm error scenarios
|
||||
let queries = vec![
|
||||
"npm ERR! code ERESOLVE",
|
||||
"npm ERR! ERESOLVE unable to resolve dependency tree",
|
||||
"npm error cannot find module typescript",
|
||||
"npm E404 not found",
|
||||
];
|
||||
|
||||
for query in queries {
|
||||
let sym = project_symptom("npm", query);
|
||||
assert!(!sym.sym_sha.is_empty());
|
||||
assert_eq!(sym.tool, "npm");
|
||||
assert!(sym.confidence > 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_real_world_cargo() {
|
||||
// Real cargo error scenarios
|
||||
let queries = vec![
|
||||
"error E0599 no method found",
|
||||
"error E0308 mismatched types",
|
||||
"cargo error failed to compile",
|
||||
];
|
||||
|
||||
for query in queries {
|
||||
let sym = project_symptom("cargo", query);
|
||||
assert!(!sym.sym_sha.is_empty());
|
||||
assert_eq!(sym.tool, "cargo");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matches_signature() {
|
||||
// SymptomVector::matches_signature works correctly
|
||||
let sym = project_symptom("npm", "npm error resolve dependency");
|
||||
|
||||
assert!(sym.matches_signature(&sym.sym_sha));
|
||||
assert!(!sym.matches_signature("different_hash"));
|
||||
assert!(!sym.matches_signature(&sym.sym_sha[0..32])); // partial hash
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_query_preserved() {
|
||||
// Original query should be preserved for logging
|
||||
let original = "npm ERR! ERESOLVE unable to resolve typescript";
|
||||
let sym = project_symptom("npm", original);
|
||||
|
||||
assert_eq!(sym.raw_query, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_confidence_scoring() {
|
||||
// More specific queries = higher confidence
|
||||
let low = project_symptom("npm", "error");
|
||||
let high = project_symptom("npm", "npm error ERESOLVE cannot resolve typescript module");
|
||||
|
||||
assert!(high.confidence >= low.confidence);
|
||||
}
|
||||
Reference in New Issue
Block a user