Skip to content

Commit e699757

Browse files
authored
fix(memory,core): eliminate Qdrant upsert timing race and add RuntimeLayer tests (#2413, #2361) (#2430)
- use wait=true on QdrantOps::upsert to block until points are indexed, eliminating scroll_all race in document_integration tests (#2413) - add multi_layer_before_after_tool_ordering test to RuntimeLayer (#2361) - fix float comparison in confusability threshold test (clippy::float_cmp)
1 parent b45de72 commit e699757

File tree

4 files changed

+86
-7
lines changed

4 files changed

+86
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1919

2020
### Fixed
2121

22+
- fix(memory): use `wait=true` on Qdrant upsert to eliminate testcontainer timing race — points are now indexed and queryable immediately after `upsert` returns (closes #2413)
23+
24+
### Added (tests)
25+
26+
- test(core): add `multi_layer_before_after_tool_ordering` — verifies both layers are called in FIFO order for `before_tool` and `after_tool` (#2361)
27+
2228
- fix(classifiers): sha2 0.11 hex formatting — replace `format!("{:x}", ...)` with `hex::encode(...)` in `verify_sha256` and its test helper (#2401)
2329
- fix(deps): bump sha2 0.10→0.11, ordered-float 5.1→5.3, proptest 1.10→1.11, toml 1.0→1.1, uuid 1.22→1.23 (#2401)
2430
- fix(skills): `two_stage_matching` and `confusability_threshold` config fields are now applied at agent startup; `AgentBuilder` gains `with_two_stage_matching` and `with_confusability_threshold` builder methods wired in `runner.rs` and `daemon.rs` (closes #2404)

crates/zeph-core/src/agent/builder.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,14 +1669,14 @@ mod tests {
16691669
);
16701670

16711671
let agent = make_agent().with_confusability_threshold(1.5);
1672-
assert_eq!(
1673-
agent.skill_state.confusability_threshold, 1.0,
1672+
assert!(
1673+
(agent.skill_state.confusability_threshold - 1.0).abs() < f32::EPSILON,
16741674
"with_confusability_threshold must clamp values above 1.0"
16751675
);
16761676

16771677
let agent = make_agent().with_confusability_threshold(-0.1);
1678-
assert_eq!(
1679-
agent.skill_state.confusability_threshold, 0.0,
1678+
assert!(
1679+
agent.skill_state.confusability_threshold.abs() < f32::EPSILON,
16801680
"with_confusability_threshold must clamp values below 0.0"
16811681
);
16821682
}

crates/zeph-core/src/runtime_layer.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,81 @@ mod tests {
344344
);
345345
}
346346

347+
/// Two layers registered in order [A, B]: `before_tool` must fire A then B,
348+
/// and `after_tool` must fire A then B (forward order for both).
349+
#[tokio::test]
350+
async fn multi_layer_before_after_tool_ordering() {
351+
use std::sync::{Arc, Mutex};
352+
353+
struct ToolOrderLayer {
354+
id: u32,
355+
log: Arc<Mutex<Vec<String>>>,
356+
}
357+
impl RuntimeLayer for ToolOrderLayer {
358+
fn before_tool<'a>(
359+
&'a self,
360+
_ctx: &'a LayerContext<'_>,
361+
_call: &'a ToolCall,
362+
) -> Pin<Box<dyn Future<Output = BeforeToolResult> + Send + 'a>> {
363+
self.log
364+
.lock()
365+
.unwrap()
366+
.push(format!("before_tool_{}", self.id));
367+
Box::pin(std::future::ready(None))
368+
}
369+
370+
fn after_tool<'a>(
371+
&'a self,
372+
_ctx: &'a LayerContext<'_>,
373+
_call: &'a ToolCall,
374+
_result: &'a Result<Option<ToolOutput>, ToolError>,
375+
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
376+
self.log
377+
.lock()
378+
.unwrap()
379+
.push(format!("after_tool_{}", self.id));
380+
Box::pin(std::future::ready(()))
381+
}
382+
}
383+
384+
let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
385+
let layer_a = ToolOrderLayer {
386+
id: 1,
387+
log: Arc::clone(&log),
388+
};
389+
let layer_b = ToolOrderLayer {
390+
id: 2,
391+
log: Arc::clone(&log),
392+
};
393+
394+
let ctx = LayerContext {
395+
conversation_id: None,
396+
turn_number: 0,
397+
};
398+
let call = ToolCall {
399+
tool_id: "shell".into(),
400+
params: serde_json::Map::new(),
401+
};
402+
let result: Result<Option<ToolOutput>, ToolError> = Ok(None);
403+
404+
layer_a.before_tool(&ctx, &call).await;
405+
layer_b.before_tool(&ctx, &call).await;
406+
layer_a.after_tool(&ctx, &call, &result).await;
407+
layer_b.after_tool(&ctx, &call, &result).await;
408+
409+
let events = log.lock().unwrap().clone();
410+
assert_eq!(
411+
events,
412+
vec![
413+
"before_tool_1",
414+
"before_tool_2",
415+
"after_tool_1",
416+
"after_tool_2"
417+
],
418+
"tool hooks must fire in registration order"
419+
);
420+
}
421+
347422
/// `NoopLayer` `after_tool` returns `()` without errors.
348423
#[tokio::test]
349424
async fn noop_layer_after_tool_returns_unit() {

crates/zeph-memory/src/qdrant_ops.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,8 @@ impl QdrantOps {
143143
///
144144
/// Returns an error if the upsert fails.
145145
pub async fn upsert(&self, collection: &str, points: Vec<PointStruct>) -> QdrantResult<()> {
146-
// wait(false): fire-and-forget on the hot path — saves 3-15ms per embedding store call.
147-
// Qdrant guarantees eventual consistency; read-your-writes is not required here.
148146
self.client
149-
.upsert_points(UpsertPointsBuilder::new(collection, points).wait(false))
147+
.upsert_points(UpsertPointsBuilder::new(collection, points).wait(true))
150148
.await
151149
.map_err(Box::new)?;
152150
Ok(())

0 commit comments

Comments
 (0)