use crate::config::VelesConfig;
use crate::index::hnsw::HnswParams;
use crate::quantization::StorageMode;
use crate::test_fixtures::fixtures::capture_warns;
use crate::{Database, DistanceMetric};
use tempfile::tempdir;
fn config_with_hnsw(m: Option<usize>, ef_construction: Option<usize>) -> VelesConfig {
let mut config = VelesConfig::default();
config.hnsw.m = m;
config.hnsw.ef_construction = ef_construction;
config
}
const DIM: usize = 128;
#[test]
fn test_hnsw_config_applies_to_vector_collection() {
let dir = tempdir().unwrap();
let db = Database::open_with_config(dir.path(), config_with_hnsw(Some(40), Some(500))).unwrap();
db.create_vector_collection("cfg", DIM, DistanceMetric::Cosine)
.unwrap();
let params = db
.get_vector_collection("cfg")
.unwrap()
.config()
.hnsw_params
.expect("a config-resolved collection persists its params");
assert_eq!(params.max_connections, 40);
assert_eq!(params.ef_construction, 500);
}
#[test]
fn test_hnsw_config_applies_to_graph_collection_with_embeddings() {
let dir = tempdir().unwrap();
let db = Database::open_with_config(dir.path(), config_with_hnsw(Some(40), Some(500))).unwrap();
db.create_graph_collection_with_embeddings(
"g",
crate::collection::GraphSchema::default(),
DIM,
DistanceMetric::Cosine,
)
.unwrap();
let params = db
.get_graph_collection("g")
.unwrap()
.inner
.config()
.hnsw_params
.expect("a graph collection with embeddings persists its params");
assert_eq!(params.max_connections, 40);
assert_eq!(params.ef_construction, 500);
}
#[test]
fn test_hnsw_config_fields_resolve_independently() {
let dir = tempdir().unwrap();
let db = Database::open_with_config(dir.path(), config_with_hnsw(Some(40), None)).unwrap();
db.create_vector_collection("partial", DIM, DistanceMetric::Cosine)
.unwrap();
let params = db
.get_vector_collection("partial")
.unwrap()
.config()
.hnsw_params
.unwrap();
assert_eq!(params.max_connections, 40);
assert_eq!(
params.ef_construction,
HnswParams::auto(DIM).ef_construction,
"an unset section field must leave the auto-tuned value alone"
);
}
#[test]
fn test_creation_argument_overrides_hnsw_config() {
let dir = tempdir().unwrap();
let db = Database::open_with_config(dir.path(), config_with_hnsw(Some(40), Some(500))).unwrap();
db.create_vector_collection_with_hnsw(
"explicit",
DIM,
DistanceMetric::Cosine,
StorageMode::Full,
Some(12),
Some(90),
)
.unwrap();
let params = db
.get_vector_collection("explicit")
.unwrap()
.config()
.hnsw_params
.unwrap();
assert_eq!(params.max_connections, 12);
assert_eq!(params.ef_construction, 90);
}
#[test]
fn test_partial_creation_argument_takes_the_other_field_from_config() {
let dir = tempdir().unwrap();
let db = Database::open_with_config(dir.path(), config_with_hnsw(Some(40), Some(500))).unwrap();
db.create_vector_collection_with_hnsw(
"mixed",
DIM,
DistanceMetric::Cosine,
StorageMode::Full,
Some(12),
None,
)
.unwrap();
let params = db
.get_vector_collection("mixed")
.unwrap()
.config()
.hnsw_params
.unwrap();
assert_eq!(params.max_connections, 12);
assert_eq!(params.ef_construction, 500);
}
#[test]
fn test_create_with_params_ignores_hnsw_config_entirely() {
let dir = tempdir().unwrap();
let db = Database::open_with_config(dir.path(), config_with_hnsw(Some(40), Some(500))).unwrap();
let explicit = HnswParams::custom(48, 400, 250_000).with_alpha(1.5);
db.create_vector_collection_with_params(
"full",
DIM,
DistanceMetric::Cosine,
StorageMode::Full,
explicit,
Some(8),
)
.unwrap();
let params = db
.get_vector_collection("full")
.unwrap()
.config()
.hnsw_params
.unwrap();
assert_eq!(params.max_connections, 48);
assert_eq!(params.ef_construction, 400);
assert_eq!(params.max_elements, 250_000);
}
#[test]
fn test_default_hnsw_config_persists_exactly_the_unwired_collection() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_vector_collection("plain", DIM, DistanceMetric::Cosine)
.unwrap();
let cfg = db.get_vector_collection("plain").unwrap().config();
assert_eq!(
cfg.hnsw_params, None,
"an untouched [hnsw] section must not materialize a params snapshot"
);
assert_eq!(cfg.pq_rescore_oversampling, Some(4));
assert_eq!(
HnswParams::from_config(DIM, &crate::config::HnswConfig::default()),
HnswParams {
storage_mode: StorageMode::Full,
..HnswParams::auto(DIM)
}
);
}
#[test]
fn test_graph_collection_without_embeddings_is_unaffected() {
let dir = tempdir().unwrap();
let db = Database::open_with_config(dir.path(), config_with_hnsw(Some(40), Some(500))).unwrap();
db.create_graph_collection("no_emb", crate::collection::GraphSchema::default())
.unwrap();
assert!(db
.get_graph_collection("no_emb")
.unwrap()
.inner
.config()
.hnsw_params
.is_none());
}
#[test]
fn test_params_from_default_config_equals_auto() {
let default = crate::config::HnswConfig::default();
for dim in [64_usize, 128, 384, 768] {
assert_eq!(
HnswParams::from_config(dim, &default),
HnswParams::auto(dim),
"a default [hnsw] section must be indistinguishable from no section at dim {dim}"
);
}
}
#[test]
fn test_params_from_config_leaves_unmapped_fields_alone() {
let hnsw = crate::config::HnswConfig {
m: Some(40),
ef_construction: Some(500),
max_layers: 12,
};
let params = HnswParams::from_config(DIM, &hnsw);
let auto = HnswParams::auto(DIM);
assert_eq!(params.max_connections, 40);
assert_eq!(params.ef_construction, 500);
assert_eq!(params.max_elements, auto.max_elements);
assert_eq!(params.storage_mode, auto.storage_mode);
assert!((params.alpha - auto.alpha).abs() < f32::EPSILON);
}
#[test]
fn test_default_config_reports_nothing_inert() {
assert!(VelesConfig::default().inert_engine_entries().is_empty());
}
#[test]
fn test_configured_hnsw_m_and_ef_construction_are_not_inert() {
assert!(
config_with_hnsw(Some(40), Some(500))
.inert_engine_entries()
.is_empty(),
"the two wired [hnsw] knobs must no longer be reported as inert"
);
}
#[test]
fn test_max_layers_is_still_reported_inert_by_field() {
let mut config = VelesConfig::default();
config.hnsw.max_layers = 12;
assert_eq!(config.inert_engine_entries(), vec!["hnsw.max_layers"]);
}
#[test]
fn test_max_layers_reported_alongside_a_configured_wired_knob() {
let mut config = config_with_hnsw(Some(40), Some(500));
config.hnsw.max_layers = 12;
assert_eq!(config.inert_engine_entries(), vec!["hnsw.max_layers"]);
}
#[test]
fn test_search_and_storage_are_both_reported_by_field() {
let mut config = VelesConfig::default();
config.search.max_results = 42;
config.storage.storage_mode = "memory".to_string();
assert_eq!(
config.inert_engine_entries(),
vec!["search.max_results", "storage.storage_mode"],
"only the knobs that still do nothing may be named"
);
}
#[test]
fn test_a_configured_wired_search_knob_is_not_reported() {
let mut config = VelesConfig::default();
config.search.default_mode = crate::config::SearchMode::Accurate;
config.search.ef_search = Some(321);
assert!(
config.inert_engine_entries().is_empty(),
"default_mode and ef_search reach the engine; naming them would be a false warning"
);
}
#[test]
fn test_default_config_emits_no_deprecation_warning() {
let config = VelesConfig::default();
let ((), warns) = capture_warns(|| {
config.validate().expect("test: default config validates");
});
assert!(
warns.is_empty(),
"a default config must not warn about anything: {warns:?}"
);
}