Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci-lite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ jobs:
openhuman/agent/harness/session/tests.rs
openhuman/agent/harness/subagent_runner/tool_prep.rs
openhuman/agent_registry/agents/loader.rs
openhuman/composio/action_tool.rs
openhuman/inference/local/mod.rs
openhuman/tool_registry/ops_tests.rs
openhuman/tool_registry/schemas.rs
Expand Down
49 changes: 40 additions & 9 deletions src/openhuman/learning/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,20 @@ mod tests {
}

#[tokio::test]
async fn learning_subscriber_fires_with_no_channel_configured() {
init_global(DEFAULT_CAPACITY);
let (tmp, _client) = test_client();
// Make the memory client ready so the full Platform wiring runs — no
// channel runtime is ever constructed in this test.
let _ = crate::openhuman::memory::global::init(tmp.path().join("workspace"));
register_learning_subscribers(tmp.path().to_path_buf());
async fn learning_subscriber_fires_on_document_event() {
// The email-signature producer reacts to `DocumentCanonicalized` and pushes
// Identity candidates into the global candidate buffer. Driving this through
// the process-global event bus (as it originally did) is flaky under the full
// parallel suite: that singleton is shared by every test, so its receiver
// starves under load and drops this test's event. Exercise the subscriber on
// an ISOLATED bus instead — the repo's canonical test-isolation pattern (see
// `core::event_bus::bus`) — so delivery is deterministic. The #5003
// regression (the subscriber registers on the channel-independent boot path)
// is guarded separately by
// `email_signature_subscriber_registers_on_channel_independent_path` below.
use crate::core::event_bus::EventBus;
use crate::openhuman::learning::extract::signature::EmailSignatureSubscriber;
use std::sync::Arc;

let source_id = unique_source_id("e2e");
let body = signature_body();
Expand All @@ -286,12 +293,36 @@ mod tests {
"signature body must yield at least one identity candidate"
);

publish_email_doc(&source_id, &body);
let bus = EventBus::create(64);
let _handle = bus.subscribe(Arc::new(EmailSignatureSubscriber));
bus.publish(DomainEvent::DocumentCanonicalized {
source_id: source_id.clone(),
source_kind: "email".to_string(),
chunks_written: 1,
chunk_ids: vec![format!("{source_id}-c1")],
canonicalized_at: 0.0,
body_preview: Some(body),
});

let got = wait_for_candidates(&source_id, expected).await;
assert_eq!(
got, expected,
"email-signature subscriber must push the parsed identity candidates \
with no channel configured anywhere (#5003)"
from a DocumentCanonicalized event"
);
}

/// #5003 regression guard (channel-independent boot path): the email-signature
/// subscriber must register even when no memory client / channel is configured.
/// Synchronous (no bus delivery), so it is not subject to the shared-bus flake.
#[tokio::test]
async fn email_signature_subscriber_registers_on_channel_independent_path() {
init_global(DEFAULT_CAPACITY);
assert!(
crate::openhuman::learning::extract::signature::register_email_signature_subscriber()
.is_some(),
"email-signature subscriber must register on the global bus once it is \
initialised, independent of any channel configuration (#5003)"
);
}

Expand Down
29 changes: 23 additions & 6 deletions src/openhuman/web_chat/event_bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,27 @@ mod tests {
/// - `args.reason` and `args.source` echoed from the domain event
#[tokio::test]
async fn automation_halt_subscriber_handle_publishes_correct_payload() {
// The web-channel bus is a process-shared broadcast; under the parallel test
// suite other tests publish to it too, so take THIS subscriber's next
// `automation_halt` event rather than whatever happens to be first in the
// buffer (which flaked as an "event name mismatch").
fn next_automation_halt(
rx: &mut tokio::sync::broadcast::Receiver<WebChannelEvent>,
) -> WebChannelEvent {
use tokio::sync::broadcast::error::TryRecvError;
loop {
match rx.try_recv() {
Ok(ev) if ev.event == "automation_halt" => return ev,
Ok(_) => {} // foreign event from a concurrent test
Err(TryRecvError::Lagged(_)) => {} // skipped some; keep draining
Err(TryRecvError::Empty) => {
panic!("expected an automation_halt WebChannelEvent but none was published")
}
Err(TryRecvError::Closed) => panic!("web-channel bus closed"),
}
}
}

// Subscribe BEFORE calling handle so the broadcast receiver is created
// before any event is sent (broadcast channels only buffer messages
// sent AFTER the receiver subscribed).
Expand All @@ -519,9 +540,7 @@ mod tests {
})
.await;

let halted = rx
.try_recv()
.expect("AutomationHalted must publish a WebChannelEvent");
let halted = next_automation_halt(&mut rx);
assert_eq!(
halted.event, "automation_halt",
"event name mismatch for halted"
Expand Down Expand Up @@ -553,9 +572,7 @@ mod tests {
})
.await;

let resumed = rx
.try_recv()
.expect("AutomationResumed must publish a WebChannelEvent");
let resumed = next_automation_halt(&mut rx);
assert_eq!(
resumed.event, "automation_halt",
"event name mismatch for resumed"
Expand Down
Loading