mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 23:10:11 +00:00
* refactor: unify WASM artifact resolution into registry/artifacts.rs Consolidate duplicated WASM find/build/install logic from 5+ files into a single src/registry/artifacts.rs module. This fixes two bugs: - registry/installer.rs now respects CARGO_TARGET_DIR (was hardcoded) - channels/wasm/bundled.rs now searches all WASM triples (was wasip2 only) Also includes: extension manager hot-activation for WASM channels, extension guidance in LLM prompts, channel manager hot-add support, webhook router channel lookup, and minor cleanups. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: send approval prompts as messages on WASM channels (Telegram, Slack) WASM channels mapped ApprovalNeeded status to a typing indicator, so users on Telegram never saw tool approval prompts — the agent got stuck in AwaitingApproval and all subsequent messages failed with "Waiting for approval". - Intercept ApprovalNeeded in WasmChannel::handle_status_update and send the prompt as an actual message via call_on_respond, showing tool name, description, parameters, and yes/no/always instructions - Guard against empty LLM responses after clean_response() strips reasoning_content think-tags (defense-in-depth for reasoning models) - Add reasoning_content fallback to NearAiChatProvider::complete() for consistency with complete_with_tools() - Add debug logging when empty responses are suppressed - Improve error logging for channel respond() failures - Register WASM channel webhook routes before credential checks so platforms don't deactivate webhook URLs with 404s Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR #297 review comments - ChannelManager::add: use async write().await instead of try_write() - resolve_target_dir: resolve relative CARGO_TARGET_DIR against crate_dir - install_wasm_files: log warning on capabilities copy failure - refresh_active_channel: load capabilities file for webhook secret name - activate_wasm_channel: validate name against path traversal - Fix cargo fmt formatting in nearai_chat.rs Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: wire up channel runtime for hot-activation and address PR review round 2 - Wire up set_channel_runtime() in main.rs so hot-activation actually works (with_channel_runtime was never called — hot-activation was dead code) - Change ExtensionManager channel runtime fields to RwLock<Option<...>> interior mutability so set_channel_runtime(&self) works after Arc wrapping - Fix artifact tests to use resolve_target_dir() instead of hardcoding "target/" (breaks when CARGO_TARGET_DIR is set) - Fix bundled.rs build hint: cargo component build (not cargo build --target) - Fix wasm_artifact_path doc: binary_name should not include .wasm extension Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: use char-aware truncation to prevent UTF-8 panic in approval prompt &s[..77] panics on multi-byte UTF-8 (CJK, emoji). Use s.chars().take(77) for safe truncation at character boundaries. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
111 lines
4.0 KiB
Rust
111 lines
4.0 KiB
Rust
//! Integration tests for HTML-to-Markdown conversion.
|
|
//!
|
|
//! For each directory in tests/test-pages/, loads source.html, runs the converter,
|
|
//! and optionally verifies against expected.md and metadata.json (contains).
|
|
//! Run with: cargo test --test html_to_markdown -- --nocapture
|
|
|
|
use std::path::Path;
|
|
|
|
#[derive(Debug, Default, serde::Deserialize)]
|
|
#[serde(default)]
|
|
struct PageMetadata {
|
|
/// If false, skip golden-file comparison even when expected.md exists.
|
|
check_expected: Option<bool>,
|
|
/// Strings that must each appear in the converted markdown.
|
|
contains: Option<Vec<String>>,
|
|
/// Base URL for readability. If omitted, use default test-pages URL.
|
|
url: Option<String>,
|
|
}
|
|
|
|
fn normalize(s: &str) -> String {
|
|
let s = s.replace("\r\n", "\n");
|
|
let s = s.trim();
|
|
let lines: Vec<&str> = s.lines().map(|l| l.trim()).collect();
|
|
lines.join("\n").trim_end().to_string()
|
|
}
|
|
|
|
/// Normalize typographic/smart punctuation to ASCII so tests match converter output
|
|
/// regardless of apostrophe/quote variants (e.g. U+2019 ' → U+0027 ').
|
|
fn normalize_smart_punctuation(s: &str) -> String {
|
|
s.replace(['\u{2019}', '\u{2018}'], "'")
|
|
.replace(['\u{201C}', '\u{201D}'], "\"")
|
|
}
|
|
|
|
#[test]
|
|
fn convert_test_pages_to_markdown() {
|
|
let test_pages = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests")
|
|
.join("test-pages");
|
|
|
|
let entries =
|
|
std::fs::read_dir(&test_pages).expect("test-pages directory not found or not readable");
|
|
|
|
let mut converted = 0u32;
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if !path.is_dir() {
|
|
continue;
|
|
}
|
|
let source_html = path.join("source.html");
|
|
if !source_html.is_file() {
|
|
continue;
|
|
}
|
|
let dir_name = path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or("unknown");
|
|
let default_url = format!("https://example.com/test-pages/{}/", dir_name);
|
|
|
|
let metadata: PageMetadata = if path.join("metadata.json").is_file() {
|
|
let raw =
|
|
std::fs::read_to_string(path.join("metadata.json")).expect("read metadata.json");
|
|
serde_json::from_str(&raw).expect("invalid metadata.json")
|
|
} else {
|
|
Default::default()
|
|
};
|
|
|
|
let url = metadata.url.as_deref().unwrap_or(&default_url).to_string();
|
|
|
|
let html = std::fs::read_to_string(&source_html).expect("read source.html");
|
|
let markdown = ironclaw::tools::builtin::convert_html_to_markdown(&html, &url)
|
|
.expect("convert_html_to_markdown failed");
|
|
|
|
let expected_md_path = path.join("expected.md");
|
|
let should_check_expected =
|
|
expected_md_path.is_file() && metadata.check_expected.unwrap_or(true);
|
|
|
|
if should_check_expected {
|
|
let expected = std::fs::read_to_string(&expected_md_path).expect("read expected.md");
|
|
let norm_actual = normalize_smart_punctuation(&normalize(&markdown));
|
|
let norm_expected = normalize_smart_punctuation(&normalize(&expected));
|
|
assert_eq!(
|
|
norm_actual, norm_expected,
|
|
"markdown mismatch for {}:\n--- actual ---\n{}\n--- expected ---\n{}",
|
|
dir_name, norm_actual, norm_expected
|
|
);
|
|
}
|
|
|
|
if let Some(ref contains) = metadata.contains {
|
|
let normalized_md = normalize_smart_punctuation(&markdown);
|
|
for s in contains {
|
|
assert!(
|
|
normalized_md.contains(&normalize_smart_punctuation(s)),
|
|
"{}: markdown missing expected content: {:?}",
|
|
dir_name,
|
|
s
|
|
);
|
|
}
|
|
}
|
|
|
|
if std::env::var("HTML_TO_MD_VERBOSE").is_ok() {
|
|
println!("--- {} ---\n{}\n", dir_name, markdown);
|
|
}
|
|
converted += 1;
|
|
}
|
|
|
|
assert!(
|
|
converted > 0,
|
|
"No test pages found (no directories with source.html in tests/test-pages/)"
|
|
);
|
|
}
|