|
3 | 3 | //! |
4 | 4 | //! Exercises only the public surface of the `tinymemory` facade. |
5 | 5 | //! |
6 | | -//! # Scope note |
| 6 | +//! # Selection |
7 | 7 | //! |
8 | | -//! Issue #18 §E3 describes this file as also asserting that "the bound |
9 | | -//! provider's `driver_id()` matches" the configured id. That step needs |
10 | | -//! `MemoryHostConfig::memory_provider()` to actually select an engine, which is |
11 | | -//! §A5 and does not exist yet — `create_memory_client_with_local_ai` still |
12 | | -//! constructs TinyCortex unconditionally. The issue's own sequencing says to |
13 | | -//! write these tests "against the *current* behaviour first", so this file |
14 | | -//! pins what admission does today. The binding half joins it when §A5 lands, |
15 | | -//! and this file is where it goes. |
| 8 | +//! Configuration now chooses the engine (§A5). One correction to the issue is |
| 9 | +//! worth recording here, because it would otherwise have wired the wrong thing: |
| 10 | +//! §A5 names `MemoryHostConfig::memory_provider()` as the selector, but that |
| 11 | +//! method is a `provider:model` routing string for the memory *workload* — |
| 12 | +//! which language model summarises — not the engine the memory lives in. |
| 13 | +//! Selection reads `memory_driver()` instead, added for the purpose. |
| 14 | +//! |
| 15 | +//! What still does not exist is the last clause of §A5, that `create_memory_*` |
| 16 | +//! return a bound `Arc<dyn MemoryProvider>`. It cannot, and the reason is |
| 17 | +//! structural rather than unfinished: `adapters/tinycortex` depends on |
| 18 | +//! `tinymemory-core` since §C3, so a core factory returning a constructed |
| 19 | +//! adapter provider would be a dependency cycle. Selection resolves the |
| 20 | +//! *decision*; the host constructs — which is what `src/registry`'s own module |
| 21 | +//! docs have always said. |
16 | 22 |
|
17 | 23 | // A failing assertion in a test *is* a panic; the crate-wide `expect_used` / |
18 | 24 | // `unwrap_used` / `panic` lints exist to keep the library from panicking, not |
19 | 25 | // the tests. Same allowance, and same reasoning, as `src/registry/test.rs`. |
20 | 26 | #![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] |
21 | 27 |
|
| 28 | +use tinymemory::api::host::test_support::TestHostConfig; |
22 | 29 | use tinymemory::registry::{ |
23 | 30 | ConfigLabels, DriverClass, DriverEntry, DriverRegistry, COGNEE_DRIVER_ID, MEM0_DRIVER_ID, |
24 | 31 | SUPERMEMORY_DRIVER_ID, TINYCORTEX_DRIVER_ID, TRUSTED, |
@@ -159,3 +166,65 @@ fn a_config_class_typo_is_echoed_back_to_the_operator() { |
159 | 166 | reason.reason |
160 | 167 | ); |
161 | 168 | } |
| 169 | + |
| 170 | +// ── The selection half, through the public facade ──────────────────────────── |
| 171 | + |
| 172 | +fn config_naming(driver: Option<&str>) -> TestHostConfig { |
| 173 | + let mut config = TestHostConfig::default(); |
| 174 | + config.memory_driver = driver.map(str::to_owned); |
| 175 | + config |
| 176 | +} |
| 177 | + |
| 178 | +#[test] |
| 179 | +fn configuration_chooses_the_engine_and_admission_gates_it() { |
| 180 | + let admission = DriverRegistry::builtin() |
| 181 | + .select( |
| 182 | + &config_naming(Some(COGNEE_DRIVER_ID)), |
| 183 | + Some(trusted_external()), |
| 184 | + labels(), |
| 185 | + ) |
| 186 | + .expect("a configured, trusted external engine binds"); |
| 187 | + assert_eq!(admission.id, COGNEE_DRIVER_ID); |
| 188 | + assert_eq!(admission.class, DriverClass::External); |
| 189 | +} |
| 190 | + |
| 191 | +#[test] |
| 192 | +fn an_unconfigured_host_still_binds_the_embedded_default() { |
| 193 | + // The property that matters most operationally: adding engine selection |
| 194 | + // must not turn "I configured nothing" into a host that fails to start. |
| 195 | + let admission = DriverRegistry::builtin() |
| 196 | + .select(&config_naming(None), None, labels()) |
| 197 | + .expect("an unconfigured host binds the embedded default"); |
| 198 | + assert_eq!(admission.id, TINYCORTEX_DRIVER_ID); |
| 199 | + assert_eq!(admission.class, DriverClass::Embedded); |
| 200 | +} |
| 201 | + |
| 202 | +#[test] |
| 203 | +fn selection_does_not_loosen_the_fail_closed_external_gate() { |
| 204 | + // Going through `select` rather than `admit` must not become a way around |
| 205 | + // the trust requirement. |
| 206 | + let untrusted = DriverEntry { |
| 207 | + class: None, |
| 208 | + trust_state: "untrusted", |
| 209 | + }; |
| 210 | + let reason = DriverRegistry::builtin() |
| 211 | + .select( |
| 212 | + &config_naming(Some(MEM0_DRIVER_ID)), |
| 213 | + Some(untrusted), |
| 214 | + labels(), |
| 215 | + ) |
| 216 | + .expect_err("an untrusted external engine is refused however it was chosen"); |
| 217 | + assert!(reason.reason.contains(TRUSTED), "{}", reason.reason); |
| 218 | +} |
| 219 | + |
| 220 | +#[test] |
| 221 | +fn the_model_routing_field_cannot_repoint_the_store() { |
| 222 | + // `memory_provider` chooses a language model; `memory_driver` chooses the |
| 223 | + // store. Conflating them would let a model change move a company's memory. |
| 224 | + let mut config = TestHostConfig::default(); |
| 225 | + config.memory_provider = Some("ollama:llama3".to_owned()); |
| 226 | + let admission = DriverRegistry::builtin() |
| 227 | + .select(&config, None, labels()) |
| 228 | + .expect("model routing leaves engine selection alone"); |
| 229 | + assert_eq!(admission.id, TINYCORTEX_DRIVER_ID); |
| 230 | +} |
0 commit comments