Files
openhuman/tests/memory_sources_readers_round21_raw_coverage_e2e.rs
T

308 lines
10 KiB
Rust

use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::memory_sources::readers::SourceReader;
use openhuman_core::openhuman::memory_sources::{ContentType, MemorySourceEntry, SourceKind};
use tempfile::{Builder, TempDir};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
struct EnvGuard {
key: &'static str,
old: Option<String>,
}
impl EnvGuard {
fn set_path(key: &'static str, value: &Path) -> Self {
let old = std::env::var(key).ok();
unsafe { std::env::set_var(key, value.as_os_str()) };
Self { key, old }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match &self.old {
Some(value) => unsafe { std::env::set_var(self.key, value) },
None => unsafe { std::env::remove_var(self.key) },
}
}
}
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn tempdir() -> TempDir {
std::fs::create_dir_all("target").expect("target dir");
Builder::new()
.prefix("memory-sources-readers-round21-")
.tempdir_in("target")
.expect("tempdir")
}
fn config(tmp: &TempDir) -> Config {
let mut config = Config::default();
config.workspace_dir = tmp.path().join("workspace");
config.action_dir = tmp.path().join("workspace");
config.config_path = tmp.path().join("config.toml");
config
}
fn source_entry(id: &str, kind: SourceKind) -> MemorySourceEntry {
MemorySourceEntry {
id: id.to_string(),
kind,
label: format!("{id} label"),
enabled: true,
toolkit: None,
connection_id: None,
path: None,
glob: None,
url: None,
branch: None,
paths: Vec::new(),
query: None,
since_days: None,
max_items: None,
max_commits: None,
max_issues: None,
max_prs: None,
selector: None,
max_tokens_per_sync: None,
max_cost_per_sync_usd: None,
sync_depth_days: None,
}
}
async fn one_response_server(
status: &'static str,
headers: &'static str,
body: Vec<u8>,
) -> (String, tokio::task::JoinHandle<()>) {
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
.await
.expect("bind fixture");
let url = format!("http://{}", listener.local_addr().expect("addr"));
let task = tokio::spawn(async move {
if let Ok((mut stream, _)) = listener.accept().await {
let mut req = [0_u8; 1024];
let _ = stream.read(&mut req).await;
let has_content_length = headers
.lines()
.any(|line| line.to_ascii_lowercase().starts_with("content-length:"));
let mut response = format!("HTTP/1.1 {status}\r\n");
if !has_content_length {
response.push_str(&format!("content-length: {}\r\n", body.len()));
}
if !headers.is_empty() {
response.push_str(headers);
response.push_str("\r\n");
}
response.push_str("\r\n");
let _ = stream.write_all(response.as_bytes()).await;
let _ = stream.write_all(&body).await;
let _ = stream.shutdown().await;
}
});
(url, task)
}
#[tokio::test]
async fn round21_rss_reader_covers_http_body_guards_and_invalid_utf8() {
let _lock = env_lock();
let tmp = tempdir();
let config = config(&tmp);
let reader = openhuman_core::openhuman::memory_sources::readers::rss::RssReader;
let (status_url, status_server) =
one_response_server("503 Service Unavailable", "", b"down".to_vec()).await;
let status_err = reader
.list_items(
&MemorySourceEntry {
url: Some(status_url),
..source_entry("rss-status", SourceKind::RssFeed)
},
&config,
)
.await
.expect_err("non-success status rejected");
assert!(status_err.contains("feed returned 503"));
let _ = status_server.await;
let huge_len = 5 * 1024 * 1024 + 1;
let (large_url, large_server) = one_response_server(
"200 OK",
"content-type: application/rss+xml\r\ncontent-length: 5242881",
b"<rss/>".to_vec(),
)
.await;
let large_err = reader
.list_items(
&MemorySourceEntry {
url: Some(large_url),
..source_entry("rss-large", SourceKind::RssFeed)
},
&config,
)
.await
.expect_err("large feed rejected from content-length");
assert!(large_err.contains(&format!("feed body too large: {huge_len} bytes")));
let _ = large_server.await;
let (utf8_url, utf8_server) = one_response_server(
"200 OK",
"content-type: application/rss+xml",
vec![0xff, 0xfe],
)
.await;
let utf8_err = reader
.list_items(
&MemorySourceEntry {
url: Some(utf8_url),
..source_entry("rss-utf8", SourceKind::RssFeed)
},
&config,
)
.await
.expect_err("invalid utf8 feed rejected");
assert!(utf8_err.contains("feed body is not valid UTF-8"));
let _ = utf8_server.await;
}
#[tokio::test]
async fn round21_github_reader_covers_commit_issue_comments_and_error_paths() {
let _lock = env_lock();
let tmp = tempdir();
let config = config(&tmp);
let bin = tmp.path().join("bin");
std::fs::create_dir_all(&bin).expect("bin dir");
write_fake_gh(&bin.join("gh"));
write_fake_git(&bin.join("git"));
let old_path = std::env::var("PATH").unwrap_or_default();
let _path = EnvGuard::set_path("PATH", Path::new(&format!("{}:{old_path}", bin.display())));
let reader = openhuman_core::openhuman::memory_sources::readers::github::GithubReader;
let entry = MemorySourceEntry {
url: Some("https://github.com/tinyhumansai/openhuman".to_string()),
..source_entry("github-round21", SourceKind::GithubRepo)
};
let items = reader
.list_items(&entry, &config)
.await
.expect("list items");
assert!(items.iter().any(|item| item.id == "commit:abc123"));
assert!(items.iter().any(|item| item.id == "issue:42"));
assert!(items.iter().any(|item| item.id == "pr:43"));
let commit = reader
.read_item(&entry, "commit:abc123", &config)
.await
.expect("read commit");
assert_eq!(commit.content_type, ContentType::Markdown);
assert!(commit.body.contains("Round21 commit subject"));
let issue = reader
.read_item(&entry, "issue:42", &config)
.await
.expect("read issue with comments");
assert!(issue.body.contains("## Comments"));
assert!(issue.body.contains("Looks good from the fixture"));
assert_eq!(
issue
.metadata
.get("labels")
.and_then(|value| value.as_array())
.map(Vec::len),
Some(0)
);
let bad_pr = reader
.read_item(&entry, "pr:not-a-number", &config)
.await
.expect_err("bad pr number rejected");
assert!(bad_pr.contains("invalid PR number"));
}
fn write_fake_gh(path: &PathBuf) {
let script = r#"#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" == "--version" ]]; then
echo "gh version 2.0.0"
exit 0
fi
if [[ "${1:-}" != "api" ]]; then
echo "unsupported gh command" >&2
exit 2
fi
arg="${2:-}"
# Match on the base resource path, ignoring per_page/page/state params.
case "$arg" in
repos/tinyhumansai/openhuman/commits\?*)
cat <<'JSON'
[{"sha":"abc123","commit":{"message":"Round21 commit subject\n\nBody line","author":{"name":"Ada","email":"ada@example.test","date":"2026-05-30T00:00:00Z"},"committer":{"name":"Ada","email":"ada@example.test","date":"2026-05-30T00:00:00Z"}}}]
JSON
;;
repos/tinyhumansai/openhuman/issues\?*)
cat <<'JSON'
[{"number":42,"title":"Round21 issue","body":"Issue body","state":"open","user":{"login":"octo"},"labels":[],"created_at":"2026-05-30T00:00:00Z","updated_at":"2026-05-30T00:01:00Z","pull_request":null}]
JSON
;;
repos/tinyhumansai/openhuman/pulls\?*)
cat <<'JSON'
[{"number":43,"title":"Round21 PR","body":"PR body","state":"open","user":{"login":"octo"},"labels":[],"created_at":"2026-05-30T00:00:00Z","updated_at":"2026-05-30T00:02:00Z","merged_at":null,"comments":1}]
JSON
;;
repos/tinyhumansai/openhuman/commits/abc123)
cat <<'JSON'
{"sha":"abc123","commit":{"message":"Round21 commit subject\n\nBody line","author":{"name":"Ada","email":"ada@example.test","date":"2026-05-30T00:00:00Z"},"committer":{"name":"Grace","email":"grace@example.test","date":"2026-05-30T00:03:00Z"}}}
JSON
;;
repos/tinyhumansai/openhuman/issues/42)
cat <<'JSON'
{"number":42,"title":"Round21 issue","body":"Issue body","state":"open","user":{"login":"octo"},"labels":[],"created_at":"2026-05-30T00:00:00Z","updated_at":"2026-05-30T00:01:00Z","pull_request":null}
JSON
;;
repos/tinyhumansai/openhuman/issues/42/comments\?*)
cat <<'JSON'
[{"user":{"login":"reviewer"},"body":"Looks good from the fixture","created_at":"2026-05-30T00:04:00Z"}]
JSON
;;
*)
echo "unexpected gh api path: $arg" >&2
exit 3
;;
esac
"#;
std::fs::write(path, script).expect("write fake gh");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path).expect("metadata").permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms).expect("chmod fake gh");
}
}
fn write_fake_git(path: &PathBuf) {
let script = r#"#!/usr/bin/env bash
set -euo pipefail
echo "git disabled for github reader fixture" >&2
exit 42
"#;
std::fs::write(path, script).expect("write fake git");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path).expect("metadata").permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms).expect("chmod fake git");
}
}