mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
refactor(openhuman): codify canonical module shape, tidy offenders (#2986)
This commit is contained in:
@@ -317,7 +317,7 @@ Deep link plugin is registered where supported; behavior is platform-specific (s
|
||||
- **Controller schema contract**: Shared controller metadata types live in `src/core/types.rs` / `src/core/mod.rs` (`ControllerSchema`, `FieldSchema`, `TypeSchema`) and are consumed by adapters (RPC/CLI).
|
||||
- **Domain schema files**: For each domain, define controller schema metadata in a dedicated module inside the domain folder (example: `src/openhuman/cron/schemas.rs`) and export from the domain `mod.rs`.
|
||||
- **Controller-only exposure rule**: Expose domain functionality to **CLI and JSON-RPC through the controller registry** (`schemas.rs` + registered handlers wired into `src/core/all.rs`). Do **not** add domain-specific branches in `src/core/cli.rs` or `src/core/jsonrpc.rs`.
|
||||
- **Light `mod.rs` rule**: Keep domain `mod.rs` files light and export-focused. Put operational code in sibling files (`ops.rs`, `store.rs`, `schedule.rs`, `types.rs`, `bus.rs`).
|
||||
- **Light `mod.rs` rule**: Keep domain `mod.rs` files light and export-focused. Put operational code in sibling files (`ops.rs`, `store.rs`, `schedule.rs`, `types.rs`, `bus.rs`). See the **Canonical module shape** table in [`CLAUDE.md`](CLAUDE.md) for the full per-file contract (it is the single source of truth for module structure).
|
||||
- **`src/core/`** — Transport only: Axum/HTTP, JSON-RPC envelope, CLI parsing, **dispatch** (`src/core/dispatch.rs`), auth, observability, event bus. **No** heavy business logic here. (Older docs that say `core_server` mean this directory; there is no `src/core_server/`.)
|
||||
- **Layering**: Implementation in `openhuman::<domain>/`, controllers in `openhuman::<domain>/rpc.rs`, routes/dispatch in `src/core/`.
|
||||
|
||||
|
||||
@@ -225,9 +225,28 @@ Watch out for Tauri plugins that inject JS by default. `tauri-plugin-opener` shi
|
||||
- **Controller schema contract**: shared types in `src/core/types.rs` / `src/core/mod.rs` (`ControllerSchema`, `FieldSchema`, `TypeSchema`).
|
||||
- **Domain schema files**: per-domain `schemas.rs` (e.g. `src/openhuman/cron/schemas.rs`), exported from domain `mod.rs`.
|
||||
- **Controller-only exposure**: expose features to CLI and JSON-RPC via the controller registry. **Do not** add domain branches in `src/core/cli.rs` / `src/core/jsonrpc.rs`.
|
||||
- **Light `mod.rs`**: keep domain `mod.rs` export-focused. Operational code in `ops.rs`, `store.rs`, `types.rs`, etc.
|
||||
- **Light `mod.rs`**: keep domain `mod.rs` export-focused. Operational code in `ops.rs`, `store.rs`, `types.rs`, etc. See **Canonical module shape** below for the full per-file contract.
|
||||
- **`src/core/`** — Transport only. Modules: `all`, `all_tests`, `auth`, `autocomplete_cli_adapter`, `cli`, `cli_tests`, `dispatch`, `event_bus/`, `jsonrpc`, `jsonrpc_tests`, `legacy_aliases`, `logging`, `memory_cli`, `observability`, `rpc_log`, `shutdown`, `socketio`, `types`, plus `agent_cli`. No heavy domain logic here. (There is no `src/core_server/` — older docs that reference `core_server` mean `src/core/`.)
|
||||
|
||||
### Canonical module shape
|
||||
|
||||
Each high-level domain under `src/openhuman/<domain>/` should follow this file contract. Only `mod.rs` and tests are universal; the rest exist **only when applicable** — do not create empty placeholder files (e.g. a stateless domain has no `store.rs`, a domain that exposes no agent tools has no `tools.rs`).
|
||||
|
||||
| File | When | Role |
|
||||
| --- | --- | --- |
|
||||
| `mod.rs` | always | Export-focused **only**: module docstring + `mod`/`pub mod` decls + `pub use` re-exports, plus the `all_<domain>_controller_schemas` / `all_<domain>_registered_controllers` pair when RPC-facing. **No business logic, no domain-state statics, no domain `impl` blocks.** |
|
||||
| `types.rs` | domain has its own types | Serde domain types. |
|
||||
| `store.rs` | domain persists state | Persistence layer. |
|
||||
| `ops.rs` | domain has logic / handlers | Business logic + entry points returning `RpcOutcome<T>`. **Canonical handler file** (`ops.rs` is the majority convention; `rpc.rs` is legal only where a domain separates a pure-domain API from ops, e.g. `cron` does `pub use ops as rpc`). |
|
||||
| `schemas.rs` | RPC-facing | Controller schemas + `handle_*` fns delegating to `ops.rs` (see **Controller migration checklist**). |
|
||||
| `tools.rs` | domain owns agent tools | Domain-owned tool impls live here (+ optional `tools/` submodules), re-exported via `src/openhuman/tools/mod.rs` (see AGENTS.md "Tool ownership rule"). Only genuinely cross-cutting tool families (filesystem, browser/computer, generic system/network) stay in `src/openhuman/tools/impl/`. |
|
||||
| `bus.rs` | domain has event subscribers | `EventHandler` impls (see **Event bus**). |
|
||||
| tests | new/changed behavior | Inline `#[cfg(test)] mod tests` (small modules) **or** a sibling `<file>_tests.rs` via `#[cfg(test)] #[path = "<file>_tests.rs"] mod tests;` (large suites). Both are legal. |
|
||||
|
||||
Two clarifications:
|
||||
- **Inline tests do not count against "light `mod.rs`".** A `mod.rs` whose non-test body is pure re-exports is already compliant; moving a large inline suite into a sibling `mod_tests.rs` is tidiness, not a correctness requirement.
|
||||
- **Narrow thin-facade exception:** pure dispatch forwarders (pick an implementation and forward — no domain state, no I/O of their own) MAY stay in `mod.rs` when being that facade is the module's whole purpose (e.g. `cwd_jail::spawn` / `spawn_with` / `default_backend`). Justify it in the module docstring.
|
||||
|
||||
### Controller migration checklist
|
||||
|
||||
- `src/openhuman/<domain>/mod.rs`: add `mod schemas;`, re-export `all_controller_schemas as all_<domain>_controller_schemas` and `all_registered_controllers as all_<domain>_registered_controllers`.
|
||||
|
||||
@@ -72,6 +72,19 @@ impl Jail {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Best-effort canonicalize that swallows errors and logs them. Most
|
||||
/// callers should use the validating [`Jail::canonicalize`] path that
|
||||
/// [`crate::openhuman::cwd_jail::spawn`] runs automatically.
|
||||
pub fn canonicalize_or_log(&mut self) {
|
||||
if let Err(e) = self.canonicalize() {
|
||||
log::warn!(
|
||||
"[cwd_jail] failed to canonicalize jail root {}: {}",
|
||||
self.root.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OS-specific enforcement of a [`Jail`].
|
||||
|
||||
@@ -92,21 +92,6 @@ pub fn spawn_with(backend: &dyn JailBackend, jail: &Jail, cmd: Command) -> std::
|
||||
backend.spawn(&jail, cmd)
|
||||
}
|
||||
|
||||
impl Jail {
|
||||
/// Best-effort canonicalize that swallows errors and logs them. Most
|
||||
/// callers should use the validating [`Jail::canonicalize`] path that
|
||||
/// [`jail`] runs automatically.
|
||||
pub fn canonicalize_or_log(&mut self) {
|
||||
if let Err(e) = self.canonicalize() {
|
||||
log::warn!(
|
||||
"[cwd_jail] failed to canonicalize jail root {}: {}",
|
||||
self.root.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -49,229 +49,5 @@ pub use schemas::{
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── Trait default method ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn noop_name_and_dims() {
|
||||
let p = NoopEmbedding;
|
||||
assert_eq!(p.name(), "none");
|
||||
assert_eq!(p.model_id(), "none");
|
||||
assert_eq!(p.dimensions(), 0);
|
||||
assert_eq!(p.signature(), "provider=none;model=none;dims=0");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_embed_returns_empty() {
|
||||
let p = NoopEmbedding;
|
||||
let result = p.embed(&["hello"]).await.unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_embed_one_returns_error() {
|
||||
// embed returns empty vec → pop() returns None → error from default impl
|
||||
let p = NoopEmbedding;
|
||||
let err = p.embed_one("hello").await.unwrap_err();
|
||||
assert!(err.to_string().contains("Empty embedding result"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_embed_empty_batch() {
|
||||
let p = NoopEmbedding;
|
||||
let result = p.embed(&[]).await.unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
// ── Factory — success ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn factory_ollama() {
|
||||
let p = create_embedding_provider("ollama", DEFAULT_OLLAMA_MODEL, 768).unwrap();
|
||||
assert_eq!(p.name(), "ollama");
|
||||
assert_eq!(p.model_id(), DEFAULT_OLLAMA_MODEL);
|
||||
assert_eq!(p.dimensions(), 768);
|
||||
assert_eq!(p.signature(), "provider=ollama;model=bge-m3;dims=768");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_openai() {
|
||||
let p = create_embedding_provider("openai", "text-embedding-3-small", 1536).unwrap();
|
||||
assert_eq!(p.name(), "openai");
|
||||
assert_eq!(p.model_id(), "text-embedding-3-small");
|
||||
assert_eq!(p.dimensions(), 1536);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_custom_url() {
|
||||
let p = create_embedding_provider("custom:http://localhost:1234", "model", 768).unwrap();
|
||||
assert_eq!(p.name(), "openai"); // OpenAI-compatible under the hood
|
||||
assert_eq!(p.dimensions(), 768);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_custom_empty_url() {
|
||||
let p = create_embedding_provider("custom:", "model", 768).unwrap();
|
||||
assert_eq!(p.name(), "openai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_none() {
|
||||
let p = create_embedding_provider("none", "", 0).unwrap();
|
||||
assert_eq!(p.name(), "none");
|
||||
assert_eq!(p.dimensions(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_voyage() {
|
||||
let p = create_embedding_provider("voyage", "voyage-3-large", 1024).unwrap();
|
||||
assert_eq!(p.name(), "voyage");
|
||||
assert_eq!(p.dimensions(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_cohere() {
|
||||
let p = create_embedding_provider("cohere", "embed-english-v3.0", 1024).unwrap();
|
||||
assert_eq!(p.name(), "cohere");
|
||||
assert_eq!(p.dimensions(), 1024);
|
||||
}
|
||||
|
||||
// ── Factory — errors ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn factory_unknown_provider_errors() {
|
||||
let result = create_embedding_provider("deepseek", "model", 1536);
|
||||
let msg = result.err().expect("should be an error").to_string();
|
||||
assert!(
|
||||
msg.contains("deepseek"),
|
||||
"should include provider name: {msg}"
|
||||
);
|
||||
assert!(msg.contains("unknown"), "should say unknown: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_empty_string_errors() {
|
||||
let result = create_embedding_provider("", "model", 1536);
|
||||
assert!(result
|
||||
.err()
|
||||
.expect("should error")
|
||||
.to_string()
|
||||
.contains("unknown"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_fastembed_errors() {
|
||||
let result = create_embedding_provider("fastembed", "BGESmallENV15", 384);
|
||||
assert!(result
|
||||
.err()
|
||||
.expect("should error")
|
||||
.to_string()
|
||||
.contains("fastembed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_cloud() {
|
||||
let p = create_embedding_provider(
|
||||
"cloud",
|
||||
DEFAULT_CLOUD_EMBEDDING_MODEL,
|
||||
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(p.name(), "cloud");
|
||||
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_managed() {
|
||||
let p = create_embedding_provider(
|
||||
"managed",
|
||||
DEFAULT_CLOUD_EMBEDDING_MODEL,
|
||||
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(p.name(), "cloud");
|
||||
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
|
||||
}
|
||||
|
||||
// ── Default provider ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_provider_uses_cloud() {
|
||||
let p = default_embedding_provider();
|
||||
assert_eq!(p.name(), "cloud");
|
||||
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_local_provider_uses_ollama() {
|
||||
let p = default_local_embedding_provider();
|
||||
assert_eq!(p.name(), "ollama");
|
||||
assert_eq!(p.dimensions(), DEFAULT_OLLAMA_DIMENSIONS);
|
||||
}
|
||||
|
||||
// ── create_embedding_provider_with_credentials ───────────
|
||||
|
||||
#[test]
|
||||
fn factory_with_credentials_voyage() {
|
||||
let p = factory::create_embedding_provider_with_credentials(
|
||||
"voyage",
|
||||
"voyage-3-large",
|
||||
1024,
|
||||
"voyage-test-key",
|
||||
None,
|
||||
)
|
||||
.expect("voyage with key");
|
||||
assert_eq!(p.name(), "voyage");
|
||||
assert_eq!(p.model_id(), "voyage-3-large");
|
||||
assert_eq!(p.dimensions(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_with_credentials_cohere() {
|
||||
let p = factory::create_embedding_provider_with_credentials(
|
||||
"cohere",
|
||||
"embed-english-v3.0",
|
||||
1024,
|
||||
"cohere-test-key",
|
||||
None,
|
||||
)
|
||||
.expect("cohere with key");
|
||||
assert_eq!(p.name(), "cohere");
|
||||
assert_eq!(p.model_id(), "embed-english-v3.0");
|
||||
assert_eq!(p.dimensions(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_with_credentials_custom() {
|
||||
let p = factory::create_embedding_provider_with_credentials(
|
||||
"custom",
|
||||
"custom-model",
|
||||
768,
|
||||
"custom-key",
|
||||
Some("http://localhost:9999"),
|
||||
)
|
||||
.expect("custom provider with endpoint");
|
||||
// Custom is backed by OpenAiEmbedding
|
||||
assert_eq!(p.name(), "openai");
|
||||
assert_eq!(p.dimensions(), 768);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_with_credentials_managed_ignores_key() {
|
||||
// Managed/cloud provider does not use the API key — it routes through
|
||||
// the OpenHuman backend. Creating it with an arbitrary key must succeed
|
||||
// and produce the cloud provider.
|
||||
let p = factory::create_embedding_provider_with_credentials(
|
||||
"managed",
|
||||
DEFAULT_CLOUD_EMBEDDING_MODEL,
|
||||
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
|
||||
"should-be-ignored",
|
||||
None,
|
||||
)
|
||||
.expect("managed ignores key");
|
||||
assert_eq!(p.name(), "cloud");
|
||||
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
|
||||
}
|
||||
}
|
||||
#[path = "mod_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Tests for the embeddings module root (provider factory + trait defaults).
|
||||
//!
|
||||
//! Split out of `mod.rs` to keep the module root export-focused. Declared via
|
||||
//! `#[path = "mod_tests.rs"] mod tests;` so `super::*` still resolves to the
|
||||
//! `embeddings` module.
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Trait default method ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn noop_name_and_dims() {
|
||||
let p = NoopEmbedding;
|
||||
assert_eq!(p.name(), "none");
|
||||
assert_eq!(p.model_id(), "none");
|
||||
assert_eq!(p.dimensions(), 0);
|
||||
assert_eq!(p.signature(), "provider=none;model=none;dims=0");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_embed_returns_empty() {
|
||||
let p = NoopEmbedding;
|
||||
let result = p.embed(&["hello"]).await.unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_embed_one_returns_error() {
|
||||
// embed returns empty vec → pop() returns None → error from default impl
|
||||
let p = NoopEmbedding;
|
||||
let err = p.embed_one("hello").await.unwrap_err();
|
||||
assert!(err.to_string().contains("Empty embedding result"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_embed_empty_batch() {
|
||||
let p = NoopEmbedding;
|
||||
let result = p.embed(&[]).await.unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
// ── Factory — success ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn factory_ollama() {
|
||||
let p = create_embedding_provider("ollama", DEFAULT_OLLAMA_MODEL, 768).unwrap();
|
||||
assert_eq!(p.name(), "ollama");
|
||||
assert_eq!(p.model_id(), DEFAULT_OLLAMA_MODEL);
|
||||
assert_eq!(p.dimensions(), 768);
|
||||
assert_eq!(p.signature(), "provider=ollama;model=bge-m3;dims=768");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_openai() {
|
||||
let p = create_embedding_provider("openai", "text-embedding-3-small", 1536).unwrap();
|
||||
assert_eq!(p.name(), "openai");
|
||||
assert_eq!(p.model_id(), "text-embedding-3-small");
|
||||
assert_eq!(p.dimensions(), 1536);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_custom_url() {
|
||||
let p = create_embedding_provider("custom:http://localhost:1234", "model", 768).unwrap();
|
||||
assert_eq!(p.name(), "openai"); // OpenAI-compatible under the hood
|
||||
assert_eq!(p.dimensions(), 768);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_custom_empty_url() {
|
||||
let p = create_embedding_provider("custom:", "model", 768).unwrap();
|
||||
assert_eq!(p.name(), "openai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_none() {
|
||||
let p = create_embedding_provider("none", "", 0).unwrap();
|
||||
assert_eq!(p.name(), "none");
|
||||
assert_eq!(p.dimensions(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_voyage() {
|
||||
let p = create_embedding_provider("voyage", "voyage-3-large", 1024).unwrap();
|
||||
assert_eq!(p.name(), "voyage");
|
||||
assert_eq!(p.dimensions(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_cohere() {
|
||||
let p = create_embedding_provider("cohere", "embed-english-v3.0", 1024).unwrap();
|
||||
assert_eq!(p.name(), "cohere");
|
||||
assert_eq!(p.dimensions(), 1024);
|
||||
}
|
||||
|
||||
// ── Factory — errors ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn factory_unknown_provider_errors() {
|
||||
let result = create_embedding_provider("deepseek", "model", 1536);
|
||||
let msg = result.err().expect("should be an error").to_string();
|
||||
assert!(
|
||||
msg.contains("deepseek"),
|
||||
"should include provider name: {msg}"
|
||||
);
|
||||
assert!(msg.contains("unknown"), "should say unknown: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_empty_string_errors() {
|
||||
let result = create_embedding_provider("", "model", 1536);
|
||||
assert!(result
|
||||
.err()
|
||||
.expect("should error")
|
||||
.to_string()
|
||||
.contains("unknown"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_fastembed_errors() {
|
||||
let result = create_embedding_provider("fastembed", "BGESmallENV15", 384);
|
||||
assert!(result
|
||||
.err()
|
||||
.expect("should error")
|
||||
.to_string()
|
||||
.contains("fastembed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_cloud() {
|
||||
let p = create_embedding_provider(
|
||||
"cloud",
|
||||
DEFAULT_CLOUD_EMBEDDING_MODEL,
|
||||
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(p.name(), "cloud");
|
||||
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_managed() {
|
||||
let p = create_embedding_provider(
|
||||
"managed",
|
||||
DEFAULT_CLOUD_EMBEDDING_MODEL,
|
||||
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(p.name(), "cloud");
|
||||
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
|
||||
}
|
||||
|
||||
// ── Default provider ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_provider_uses_cloud() {
|
||||
let p = default_embedding_provider();
|
||||
assert_eq!(p.name(), "cloud");
|
||||
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_local_provider_uses_ollama() {
|
||||
let p = default_local_embedding_provider();
|
||||
assert_eq!(p.name(), "ollama");
|
||||
assert_eq!(p.dimensions(), DEFAULT_OLLAMA_DIMENSIONS);
|
||||
}
|
||||
|
||||
// ── create_embedding_provider_with_credentials ───────────
|
||||
|
||||
#[test]
|
||||
fn factory_with_credentials_voyage() {
|
||||
let p = factory::create_embedding_provider_with_credentials(
|
||||
"voyage",
|
||||
"voyage-3-large",
|
||||
1024,
|
||||
"voyage-test-key",
|
||||
None,
|
||||
)
|
||||
.expect("voyage with key");
|
||||
assert_eq!(p.name(), "voyage");
|
||||
assert_eq!(p.model_id(), "voyage-3-large");
|
||||
assert_eq!(p.dimensions(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_with_credentials_cohere() {
|
||||
let p = factory::create_embedding_provider_with_credentials(
|
||||
"cohere",
|
||||
"embed-english-v3.0",
|
||||
1024,
|
||||
"cohere-test-key",
|
||||
None,
|
||||
)
|
||||
.expect("cohere with key");
|
||||
assert_eq!(p.name(), "cohere");
|
||||
assert_eq!(p.model_id(), "embed-english-v3.0");
|
||||
assert_eq!(p.dimensions(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_with_credentials_custom() {
|
||||
let p = factory::create_embedding_provider_with_credentials(
|
||||
"custom",
|
||||
"custom-model",
|
||||
768,
|
||||
"custom-key",
|
||||
Some("http://localhost:9999"),
|
||||
)
|
||||
.expect("custom provider with endpoint");
|
||||
// Custom is backed by OpenAiEmbedding
|
||||
assert_eq!(p.name(), "openai");
|
||||
assert_eq!(p.dimensions(), 768);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_with_credentials_managed_ignores_key() {
|
||||
// Managed/cloud provider does not use the API key — it routes through
|
||||
// the OpenHuman backend. Creating it with an arbitrary key must succeed
|
||||
// and produce the cloud provider.
|
||||
let p = factory::create_embedding_provider_with_credentials(
|
||||
"managed",
|
||||
DEFAULT_CLOUD_EMBEDDING_MODEL,
|
||||
DEFAULT_CLOUD_EMBEDDING_DIMENSIONS,
|
||||
"should-be-ignored",
|
||||
None,
|
||||
)
|
||||
.expect("managed ignores key");
|
||||
assert_eq!(p.name(), "cloud");
|
||||
assert_eq!(p.dimensions(), DEFAULT_CLOUD_EMBEDDING_DIMENSIONS);
|
||||
}
|
||||
@@ -16,51 +16,5 @@ pub use types::{
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tool_scope_equality() {
|
||||
assert_eq!(ToolScope::All, ToolScope::All);
|
||||
assert_ne!(ToolScope::All, ToolScope::CliRpcOnly);
|
||||
assert_ne!(ToolScope::AgentOnly, ToolScope::CliRpcOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_response_deserializes() {
|
||||
let json = r#"{"success": true, "data": {"foo": 42}}"#;
|
||||
let resp: BackendResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
|
||||
assert!(resp.success);
|
||||
assert_eq!(resp.data.unwrap()["foo"], 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_response_without_data() {
|
||||
let json = r#"{"success": true}"#;
|
||||
let resp: BackendResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
|
||||
assert!(resp.success);
|
||||
assert!(resp.data.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_pricing_defaults_on_missing_fields() {
|
||||
let json = r#"{"integrations": {}}"#;
|
||||
let pricing: IntegrationPricing = serde_json::from_str(json).unwrap();
|
||||
assert!(pricing.integrations.apify.is_none());
|
||||
assert!(pricing.integrations.twilio.is_none());
|
||||
assert!(pricing.integrations.google_places.is_none());
|
||||
assert!(pricing.integrations.parallel.is_none());
|
||||
assert!(pricing.integrations.tinyfish.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_client_returns_none_when_no_auth_token() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let config = crate::openhuman::config::Config {
|
||||
workspace_dir: tmp.path().join("workspace"),
|
||||
config_path: tmp.path().join("config.toml"),
|
||||
..crate::openhuman::config::Config::default()
|
||||
};
|
||||
assert!(build_client(&config).is_none());
|
||||
}
|
||||
}
|
||||
#[path = "mod_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
//! Tests for the integrations module root (types + client construction).
|
||||
//!
|
||||
//! Split out of `mod.rs` to keep the module root export-focused. Declared via
|
||||
//! `#[path = "mod_tests.rs"] mod tests;` so `super::*` still resolves to the
|
||||
//! `integrations` module.
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tool_scope_equality() {
|
||||
assert_eq!(ToolScope::All, ToolScope::All);
|
||||
assert_ne!(ToolScope::All, ToolScope::CliRpcOnly);
|
||||
assert_ne!(ToolScope::AgentOnly, ToolScope::CliRpcOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_response_deserializes() {
|
||||
let json = r#"{"success": true, "data": {"foo": 42}}"#;
|
||||
let resp: BackendResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
|
||||
assert!(resp.success);
|
||||
assert_eq!(resp.data.unwrap()["foo"], 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_response_without_data() {
|
||||
let json = r#"{"success": true}"#;
|
||||
let resp: BackendResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
|
||||
assert!(resp.success);
|
||||
assert!(resp.data.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_pricing_defaults_on_missing_fields() {
|
||||
let json = r#"{"integrations": {}}"#;
|
||||
let pricing: IntegrationPricing = serde_json::from_str(json).unwrap();
|
||||
assert!(pricing.integrations.apify.is_none());
|
||||
assert!(pricing.integrations.twilio.is_none());
|
||||
assert!(pricing.integrations.google_places.is_none());
|
||||
assert!(pricing.integrations.parallel.is_none());
|
||||
assert!(pricing.integrations.tinyfish.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_client_returns_none_when_no_auth_token() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let config = crate::openhuman::config::Config {
|
||||
workspace_dir: tmp.path().join("workspace"),
|
||||
config_path: tmp.path().join("config.toml"),
|
||||
..crate::openhuman::config::Config::default()
|
||||
};
|
||||
assert!(build_client(&config).is_none());
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
//! `openhuman::memory` re-exports it as `memory::jobs` during the migration.
|
||||
|
||||
pub(crate) mod handlers;
|
||||
mod ops;
|
||||
mod redact;
|
||||
pub mod scheduler;
|
||||
pub mod store;
|
||||
@@ -38,82 +39,7 @@ pub mod testing;
|
||||
pub mod types;
|
||||
pub(crate) mod worker;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// #1574 §6 / #1365: set while a re-embed backfill chain has work pending.
|
||||
///
|
||||
/// Read by the first-person / subconscious retrieval layer so an empty
|
||||
/// vector-search result during the backfill window is interpreted as
|
||||
/// "not searched yet" rather than "no such memory" — preventing the agent
|
||||
/// from confidently asserting false self-ignorance mid-re-embed. Set true
|
||||
/// when a backfill is enqueued / still has rows; cleared when the chain
|
||||
/// drains. Process-global (resets to false on restart; the worker re-sets
|
||||
/// it on the next backfill tick — acceptable for v1).
|
||||
static BACKFILL_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Mark whether a re-embed backfill currently has pending work.
|
||||
pub fn set_backfill_in_progress(v: bool) {
|
||||
BACKFILL_IN_PROGRESS.store(v, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// True while a re-embed backfill chain still has rows to process. The
|
||||
/// #1365 absence-reasoning consumer checks this before treating an empty
|
||||
/// semantic-recall result as "no memory exists".
|
||||
pub fn backfill_in_progress() -> bool {
|
||||
BACKFILL_IN_PROGRESS.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// #1574 §4: ensure a re-embed backfill chain exists for the **current**
|
||||
/// active signature, if (and only if) there is uncovered work.
|
||||
///
|
||||
/// This is the switch-path trigger: call it after the embedder config
|
||||
/// changes (a new signature → every prior row is missing at it). The §7
|
||||
/// migration is one-shot (`user_version`-gated) so it does NOT fire on a
|
||||
/// later model switch — without this, switching silently blinds prior
|
||||
/// memory. Standalone (own connection); the §7 migration keeps its own
|
||||
/// in-tx enqueue (atomic with the copy). Idempotent + non-fatal: the
|
||||
/// per-signature dedupe key means at most one chain per space, and a
|
||||
/// covered space enqueues nothing. Errors are logged, never propagated —
|
||||
/// a failed enqueue must not fail the user's settings save.
|
||||
pub fn ensure_reembed_backfill(config: &crate::openhuman::config::Config) {
|
||||
let sig = crate::openhuman::memory_store::chunks::store::tree_active_signature(config);
|
||||
let result = crate::openhuman::memory_store::chunks::store::with_connection(config, |conn| {
|
||||
Ok(crate::openhuman::memory_store::chunks::store::has_uncovered_reembed_work(conn, &sig)?)
|
||||
});
|
||||
match result {
|
||||
Ok(true) => {
|
||||
let job = match types::NewJob::reembed_backfill(&types::ReembedBackfillPayload {
|
||||
signature: sig.clone(),
|
||||
}) {
|
||||
Ok(j) => j,
|
||||
Err(e) => {
|
||||
log::warn!("[memory::jobs] ensure_reembed_backfill: build job failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match store::enqueue(config, &job) {
|
||||
Ok(_) => {
|
||||
set_backfill_in_progress(true);
|
||||
log::info!(
|
||||
"[memory::jobs] ensure_reembed_backfill: enqueued chain for sig={sig}"
|
||||
);
|
||||
}
|
||||
Err(e) => log::warn!(
|
||||
"[memory::jobs] ensure_reembed_backfill: enqueue failed for sig={sig}: {e}"
|
||||
),
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log::debug!(
|
||||
"[memory::jobs] ensure_reembed_backfill: sig={sig} fully covered; nothing to do"
|
||||
);
|
||||
}
|
||||
Err(e) => log::warn!(
|
||||
"[memory::jobs] ensure_reembed_backfill: coverage probe failed for sig={sig}: {e}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub use ops::{backfill_in_progress, ensure_reembed_backfill, set_backfill_in_progress};
|
||||
pub use scheduler::{backfill_missing_digests, trigger_digest};
|
||||
pub use store::{
|
||||
claim_next, count_by_status, count_total, enqueue, enqueue_tx, get_job, mark_deferred,
|
||||
@@ -125,20 +51,3 @@ pub use types::{
|
||||
Job, JobKind, JobOutcome, JobStatus, NewJob, NodeRef, SealPayload, TopicRoutePayload,
|
||||
};
|
||||
pub use worker::{start, wake_workers};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn backfill_flag_roundtrip() {
|
||||
set_backfill_in_progress(false);
|
||||
assert!(!backfill_in_progress());
|
||||
|
||||
set_backfill_in_progress(true);
|
||||
assert!(backfill_in_progress());
|
||||
|
||||
set_backfill_in_progress(false);
|
||||
assert!(!backfill_in_progress());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//! Memory-queue operations: backfill-progress signalling and the re-embed
|
||||
//! backfill switch-path trigger.
|
||||
//!
|
||||
//! Split out of `mod.rs` so the module root stays export-focused. Public paths
|
||||
//! are preserved via re-exports in [`super`], so callers keep using
|
||||
//! `crate::openhuman::memory_queue::<fn>`.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use super::{store, types};
|
||||
|
||||
/// #1574 §6 / #1365: set while a re-embed backfill chain has work pending.
|
||||
///
|
||||
/// Read by the first-person / subconscious retrieval layer so an empty
|
||||
/// vector-search result during the backfill window is interpreted as
|
||||
/// "not searched yet" rather than "no such memory" — preventing the agent
|
||||
/// from confidently asserting false self-ignorance mid-re-embed. Set true
|
||||
/// when a backfill is enqueued / still has rows; cleared when the chain
|
||||
/// drains. Process-global (resets to false on restart; the worker re-sets
|
||||
/// it on the next backfill tick — acceptable for v1).
|
||||
static BACKFILL_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Mark whether a re-embed backfill currently has pending work.
|
||||
pub fn set_backfill_in_progress(v: bool) {
|
||||
BACKFILL_IN_PROGRESS.store(v, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// True while a re-embed backfill chain still has rows to process. The
|
||||
/// #1365 absence-reasoning consumer checks this before treating an empty
|
||||
/// semantic-recall result as "no memory exists".
|
||||
pub fn backfill_in_progress() -> bool {
|
||||
BACKFILL_IN_PROGRESS.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// #1574 §4: ensure a re-embed backfill chain exists for the **current**
|
||||
/// active signature, if (and only if) there is uncovered work.
|
||||
///
|
||||
/// This is the switch-path trigger: call it after the embedder config
|
||||
/// changes (a new signature → every prior row is missing at it). The §7
|
||||
/// migration is one-shot (`user_version`-gated) so it does NOT fire on a
|
||||
/// later model switch — without this, switching silently blinds prior
|
||||
/// memory. Standalone (own connection); the §7 migration keeps its own
|
||||
/// in-tx enqueue (atomic with the copy). Idempotent + non-fatal: the
|
||||
/// per-signature dedupe key means at most one chain per space, and a
|
||||
/// covered space enqueues nothing. Errors are logged, never propagated —
|
||||
/// a failed enqueue must not fail the user's settings save.
|
||||
pub fn ensure_reembed_backfill(config: &crate::openhuman::config::Config) {
|
||||
let sig = crate::openhuman::memory_store::chunks::store::tree_active_signature(config);
|
||||
let result = crate::openhuman::memory_store::chunks::store::with_connection(config, |conn| {
|
||||
Ok(crate::openhuman::memory_store::chunks::store::has_uncovered_reembed_work(conn, &sig)?)
|
||||
});
|
||||
match result {
|
||||
Ok(true) => {
|
||||
let job = match types::NewJob::reembed_backfill(&types::ReembedBackfillPayload {
|
||||
signature: sig.clone(),
|
||||
}) {
|
||||
Ok(j) => j,
|
||||
Err(e) => {
|
||||
log::warn!("[memory::jobs] ensure_reembed_backfill: build job failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match store::enqueue(config, &job) {
|
||||
Ok(_) => {
|
||||
set_backfill_in_progress(true);
|
||||
log::info!(
|
||||
"[memory::jobs] ensure_reembed_backfill: enqueued chain for sig={sig}"
|
||||
);
|
||||
}
|
||||
Err(e) => log::warn!(
|
||||
"[memory::jobs] ensure_reembed_backfill: enqueue failed for sig={sig}: {e}"
|
||||
),
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log::debug!(
|
||||
"[memory::jobs] ensure_reembed_backfill: sig={sig} fully covered; nothing to do"
|
||||
);
|
||||
}
|
||||
Err(e) => log::warn!(
|
||||
"[memory::jobs] ensure_reembed_backfill: coverage probe failed for sig={sig}: {e}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn backfill_flag_roundtrip() {
|
||||
set_backfill_in_progress(false);
|
||||
assert!(!backfill_in_progress());
|
||||
|
||||
set_backfill_in_progress(true);
|
||||
assert!(backfill_in_progress());
|
||||
|
||||
set_backfill_in_progress(false);
|
||||
assert!(!backfill_in_progress());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user