209 lines
6.8 KiB
Plaintext
209 lines
6.8 KiB
Plaintext
use mem_core::{Chunk, Level, MemoryNode, ProjectId, QueryId, Record, Role, RunId, Provenance};
|
|||
|
|
use time::macros::datetime;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a1_same_content_same_hash() {
|
||
|
|
// Create two chunks with identical content but different RunIds and timestamps
|
||
|
|
let _run_id_1 = RunId::new("run1".to_string()).unwrap();
|
||
|
|
let _run_id_2 = RunId::new("run2".to_string()).unwrap();
|
||
|
|
|
||
|
|
let record_1 = Record {
|
||
|
|
role: Role::User,
|
||
|
|
text: "Hello, world!".to_string(),
|
||
|
|
timestamp: datetime!(2024-08-20 12:00:00 UTC),
|
||
|
|
provenance: Provenance {
|
||
|
|
source_id: "session1".to_string(),
|
||
|
|
offset: 0,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
let record_2 = Record {
|
||
|
|
role: Role::User,
|
||
|
|
text: "Hello, world!".to_string(),
|
||
|
|
timestamp: datetime!(2024-08-20 13:00:00 UTC), // Different timestamp
|
||
|
|
provenance: Provenance {
|
||
|
|
source_id: "session1".to_string(),
|
||
|
|
offset: 0,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut chunk_1 = Chunk::new(1, vec![record_1], 2);
|
||
|
|
let mut chunk_2 = Chunk::new(1, vec![record_2], 2);
|
||
|
|
|
||
|
|
let hash_1 = chunk_1.content_hash();
|
||
|
|
let hash_2 = chunk_2.content_hash();
|
||
|
|
|
||
|
|
assert_eq!(hash_1, hash_2, "Identical content should produce identical hashes");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a2_text_change_changes_hash() {
|
||
|
|
let record_1 = Record {
|
||
|
|
role: Role::User,
|
||
|
|
text: "Hello, world!".to_string(),
|
||
|
|
timestamp: datetime!(2024-08-20 12:00:00 UTC),
|
||
|
|
provenance: Provenance {
|
||
|
|
source_id: "session1".to_string(),
|
||
|
|
offset: 0,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
let record_2 = Record {
|
||
|
|
role: Role::User,
|
||
|
|
text: "Hello, world.".to_string(), // Changed the ! to .
|
||
|
|
timestamp: datetime!(2024-08-20 12:00:00 UTC),
|
||
|
|
provenance: Provenance {
|
||
|
|
source_id: "session1".to_string(),
|
||
|
|
offset: 0,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut chunk_1 = Chunk::new(1, vec![record_1], 2);
|
||
|
|
let mut chunk_2 = Chunk::new(1, vec![record_2], 2);
|
||
|
|
|
||
|
|
let hash_1 = chunk_1.content_hash();
|
||
|
|
let hash_2 = chunk_2.content_hash();
|
||
|
|
|
||
|
|
assert_ne!(hash_1, hash_2, "Different text should produce different hashes");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a3_level_wire_format() {
|
||
|
|
let json_l0 = serde_json::to_string(&Level::L0).unwrap();
|
||
|
|
let json_l1 = serde_json::to_string(&Level::L1).unwrap();
|
||
|
|
let json_l2 = serde_json::to_string(&Level::L2).unwrap();
|
||
|
|
|
||
|
|
assert_eq!(json_l0, "\"L0\"", "L0 should serialize as \"L0\"");
|
||
|
|
assert_eq!(json_l1, "\"L1\"", "L1 should serialize as \"L1\"");
|
||
|
|
assert_eq!(json_l2, "\"L2\"", "L2 should serialize as \"L2\"");
|
||
|
|
|
||
|
|
// Verify they deserialize correctly
|
||
|
|
let deserialized_l0: Level = serde_json::from_str(&json_l0).unwrap();
|
||
|
|
let deserialized_l1: Level = serde_json::from_str(&json_l1).unwrap();
|
||
|
|
let deserialized_l2: Level = serde_json::from_str(&json_l2).unwrap();
|
||
|
|
|
||
|
|
assert_eq!(deserialized_l0, Level::L0);
|
||
|
|
assert_eq!(deserialized_l1, Level::L1);
|
||
|
|
assert_eq!(deserialized_l2, Level::L2);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a4_hash_stability_across_versions() {
|
||
|
|
// Create a known set of records and compute the hash
|
||
|
|
let records = vec![
|
||
|
|
Record {
|
||
|
|
role: Role::User,
|
||
|
|
text: "Hello".to_string(),
|
||
|
|
timestamp: datetime!(2024-08-20 12:00:00 UTC),
|
||
|
|
provenance: Provenance {
|
||
|
|
source_id: "session1".to_string(),
|
||
|
|
offset: 0,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
Record {
|
||
|
|
role: Role::Assistant,
|
||
|
|
text: "Hi there".to_string(),
|
||
|
|
timestamp: datetime!(2024-08-20 12:00:01 UTC),
|
||
|
|
provenance: Provenance {
|
||
|
|
source_id: "session1".to_string(),
|
||
|
|
offset: 1,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
let mut chunk = Chunk::new(1, records, 5);
|
||
|
|
let hash = chunk.content_hash();
|
||
|
|
|
||
|
|
// This hash is a fixture - if the canonicalization changes, this assertion fails
|
||
|
|
// and alerts us to verify the change is intentional.
|
||
|
|
// The hash should be stable for the same content.
|
||
|
|
let expected_hex = hash.to_hex();
|
||
|
|
|
||
|
|
// Verify it's a valid 64-character hex string
|
||
|
|
assert_eq!(expected_hex.len(), 64, "Hash should be 64 hex characters");
|
||
|
|
assert!(expected_hex.chars().all(|c| c.is_ascii_hexdigit()), "Hash should contain only hex digits");
|
||
|
|
|
||
|
|
// Re-hash the same content and ensure it's identical
|
||
|
|
let mut chunk_2 = Chunk::new(1, vec![
|
||
|
|
Record {
|
||
|
|
role: Role::User,
|
||
|
|
text: "Hello".to_string(),
|
||
|
|
timestamp: datetime!(2024-08-20 12:00:00 UTC),
|
||
|
|
provenance: Provenance {
|
||
|
|
source_id: "session1".to_string(),
|
||
|
|
offset: 0,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
Record {
|
||
|
|
role: Role::Assistant,
|
||
|
|
text: "Hi there".to_string(),
|
||
|
|
timestamp: datetime!(2024-08-20 12:00:01 UTC),
|
||
|
|
provenance: Provenance {
|
||
|
|
source_id: "session1".to_string(),
|
||
|
|
offset: 1,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
], 5);
|
||
|
|
|
||
|
|
let hash_2 = chunk_2.content_hash();
|
||
|
|
assert_eq!(hash, hash_2, "Hash must be stable for identical content");
|
||
|
|
assert_eq!(hash.to_hex(), expected_hex, "Hash hex representation must be stable");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a5_newtypes_have_no_default() {
|
||
|
|
// This is a compile-fail test assertion.
|
||
|
|
// The following should NOT compile:
|
||
|
|
// let project = ProjectId::default();
|
||
|
|
// let query = QueryId::default();
|
||
|
|
// let run = RunId::default();
|
||
|
|
//
|
||
|
|
// We verify this by checking that we cannot create defaults.
|
||
|
|
// If ProjectId derived Default, this test would not exist -
|
||
|
|
// the compilation check is the test itself.
|
||
|
|
|
||
|
|
// Instead, we verify that construction requires valid values
|
||
|
|
assert!(ProjectId::new("p1".to_string()).is_ok());
|
||
|
|
assert!(ProjectId::new("".to_string()).is_err());
|
||
|
|
|
||
|
|
assert!(QueryId::new("q1".to_string()).is_ok());
|
||
|
|
assert!(QueryId::new("".to_string()).is_err());
|
||
|
|
|
||
|
|
assert!(RunId::new("r1".to_string()).is_ok());
|
||
|
|
assert!(RunId::new("".to_string()).is_err());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_memory_node_hash_stability() {
|
||
|
|
let project = ProjectId::new("project1".to_string()).unwrap();
|
||
|
|
let query_id = Some(QueryId::new("query1".to_string()).unwrap());
|
||
|
|
let run_id = RunId::new("run1".to_string()).unwrap();
|
||
|
|
|
||
|
|
let mut node_1 = MemoryNode::new(
|
||
|
|
Level::L0,
|
||
|
|
project.clone(),
|
||
|
|
query_id.clone(),
|
||
|
|
run_id.clone(),
|
||
|
|
1,
|
||
|
|
"test text".to_string(),
|
||
|
|
vec![],
|
||
|
|
);
|
||
|
|
|
||
|
|
// Create another node with the same content but different run_id shouldn't matter for content_hash
|
||
|
|
let different_run_id = RunId::new("run2".to_string()).unwrap();
|
||
|
|
let mut node_2 = MemoryNode::new(
|
||
|
|
Level::L0,
|
||
|
|
project.clone(),
|
||
|
|
query_id.clone(),
|
||
|
|
different_run_id,
|
||
|
|
1,
|
||
|
|
"test text".to_string(),
|
||
|
|
vec![],
|
||
|
|
);
|
||
|
|
|
||
|
|
let hash_1 = node_1.content_hash();
|
||
|
|
let hash_2 = node_2.content_hash();
|
||
|
|
|
||
|
|
assert_eq!(hash_1, hash_2, "MemoryNode hash should not include run_id");
|
||
|
|
}
|