mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* feat(ux): complete UX overhaul — design system, boot screen, onboarding, web polish Shared design system: CSS custom properties for spacing, typography, transitions, and color tokens used across web UI and boot screen. Boot screen: compact feature-tags line showing enabled subsystems (db, tools, routines, heartbeat, skills, sandbox, embeddings) at a glance. Downgrade startup info logs (libSQL, webhook, workspace seed) to debug level since the boot screen now covers this. Onboarding wizard: model picker with live API fetch, provider-aware auth flow, improved error recovery and progress display. Web UI: ARIA attributes, welcome card, streaming debounce, connection status banner, skeleton loaders, send cooldown. CLI: doctor command enhancements, status command cleanup, REPL banner consolidation, shared fmt module. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(ux): Apple-level design refinements — spring physics, glass morphism, chat polish Merge staging theme support (dark/light/system toggle) and layer UX polish on top: spring-physics motion, glass morphism depth, chat experience improvements, and responsive mobile refinements. Design system: - Restore and extend design token system (spacing, typography, timing, easing) with legacy aliases for theme compatibility - Add shadow tiers, accent glow, glass morphism, spring easing tokens - Tokens defined in both dark (:root) and light ([data-theme="light"]) Micro-interactions (Phase 2): - Spring-overshoot message entry animation (slideUp) - Spring-scale button press on all interactive buttons - Tab crossfade animation, tool card smooth accordion (max-height) - Modal scale(0.95) + blur(8px) entry, toast spring slide - Sidebar width crossfade, card hover lift Visual depth (Phase 3): - Tab bar glass morphism + surface highlight + sliding indicator - Active tab accent background pill - Assistant message accent left border, user message bubble tail - Floating input area (rounded + shadow + margin) Chat polish (Phase 4): - Smooth streaming cursor (cursorPulse), message hover timestamps - Time separators (Today/Yesterday/date) - Textarea smooth auto-expand, send button glow Settings & forms (Phase 5): - iOS-style toggle switches for boolean settings - Input focus glow, save feedback spring animation - Welcome card with gradient background + proper spacing - Sticky settings group headers with glass backdrop Accessibility & mobile (Phase 6): - Animated focus ring, prefers-reduced-motion global kill-switch - Touch target audit (44px min), mobile bottom-sheet modals - Mobile bottom tab bar, toast redesign (icon + border + countdown) - Thread hover translateX, badge in_progress pulse Bug fixes: - Gateway/TEE popover z-index (tab-bar z-index: 200, popovers 500) - Connection lost banner as fixed top bar instead of flex child - Sidebar collapse keeps toggle + new thread buttons visible - Downgrade noisy startup logs (db, webhook, vector) to debug - Remove green dot pulse animation on connected status - Deduplicate confirm-modal in HTML, add tab-indicator div Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(web): mobile layout improvements — sidebar toggle, settings drill-down, tab bar polish - Fix mobile sidebar toggle: use expanded-mobile class instead of collapsed, add backdrop overlay, auto-close on thread select, outside-click dismiss - Settings: replace cramped horizontal tabs with drill-down navigation (category list → detail view → back button) - Bottom tab bar: add glass morphism, hide theme toggle, flip tab indicator to top edge - Keep thread toggle button visible in collapsed 36px sidebar strip Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(repl): interactive approval selector and transient status lines - Replace ASCII-art approval box with clean horizontal rule card - Add inquire-based interactive selector for tool approvals (↑↓ + Enter) - Selector runs directly from send_status via spawn_blocking, with stdin_locked flag to prevent readline from competing for stdin - Transient thinking/tool-started lines: each replaces the previous, all erased before final output (no clutter left in scrollback) - Esc in selector sends denial so agent never gets stuck Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: widen TurnCost token fields to u64 and remove unused variable - Change input_tokens/output_tokens from u32 to u64 in StatusUpdate::TurnCost, SseEvent::TurnCost, and the thread_ops emit site to avoid truncation on large conversations - Remove unused _routine_engine_for_loop binding in agent_loop.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore: reduce startup log noise — demote info to debug Demote routine startup messages (builder, WASM tools, tunnel, WASM channels) from info to debug so the default log output stays clean. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(web): allow CDN scripts in CSP connect-src directive Add cdn.jsdelivr.net and cdnjs.cloudflare.com to connect-src so the browser can fetch marked.js and DOMPurify without CSP violations. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: fix cargo fmt in repl.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(web): gate turn_cost SSE handler on current thread Prevents cost badge from attaching to the wrong message when switching threads or receiving events from background threads. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * ci: retrigger CI * fix: add missing extension_manager to webhook EngineContext The webhook trigger path added in #736 was missing the extension_manager field introduced by #1453. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore: ignore RUSTSEC-2026-0049 rustls-webpki CRL advisory Low impact — requires compromised CA to exploit. Tracked for upstream rustls-webpki upgrade. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(routines): use fields.join for cron normalization Use split_whitespace fields instead of re-trimming the original string to avoid preserving extra internal whitespace in cron expressions. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(repl): Apple-style approval card — clean vertical flow - Drop verbose tool description (the command IS the decision surface) - Unified vertical pipe layout: ◆ header → │ params → │ selector - Selector options show keyboard shortcuts inline: Approve (y) - Compact help message, answered state uses └ to close the flow - No horizontal rules, no blank-line padding — just breathing room Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(repl): replace inquire with crossterm for approval selector Drop the inquire dependency (which pulled in crossterm 0.25, duplicating the existing 0.28). The 3-option approval selector is now built directly with crossterm raw mode — same UX, zero new dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore(deps): upgrade crossterm 0.28 → 0.29, eliminate duplication termimad (via crokey) uses crossterm 0.29. Upgrading our direct dependency from 0.28 to 0.29 collapses to a single crossterm version in the dependency tree. Also migrated termimad::crossterm:: references to the direct crossterm import. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review comments — box_top off-by-one, smart_truncate overflow, mobile theme toggle - Fix box_top() fill calculation: was off-by-one, producing boxes 1 char too wide (fmt.rs) - Fix smart_truncate(): account for "..." in the budget so output never exceeds max_chars (repl.rs) - Move theme toggle to settings sidebar on mobile instead of display:none, so mobile users can still switch themes (style.css, index.html, app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: cargo fmt repl.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review — retry duplication, CSP connect-src, deny color - Remove failed message before retry to prevent duplicate user messages - Revert connect-src to 'self' — CDN hosts only need script-src - Use red for Deny confirmation in REPL approval selector Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
471 lines
17 KiB
Rust
471 lines
17 KiB
Rust
//! WASM tool runtime for managing compiled components.
|
|
//!
|
|
//! Follows the principle: compile once at registration, instantiate fresh per execution.
|
|
//! This matches NEAR blockchain patterns for deterministic, isolated execution.
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use tokio::sync::RwLock;
|
|
use wasmtime::{Config, Engine, OptLevel};
|
|
|
|
use crate::tools::wasm::error::WasmError;
|
|
use crate::tools::wasm::limits::{FuelConfig, ResourceLimits};
|
|
|
|
/// Default epoch tick interval. Each tick increments the engine's epoch counter,
|
|
/// which causes any store with an expired epoch deadline to trap.
|
|
pub const EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(500);
|
|
|
|
/// Enable wasmtime's persistent compilation cache for a [`Config`].
|
|
///
|
|
/// On Unix, this delegates to `cache_config_load_default()` which uses a
|
|
/// shared cache directory. On Windows, each engine gets its own subdirectory
|
|
/// (keyed by `label`) to avoid OS error 33 (`ERROR_LOCK_VIOLATION`) when
|
|
/// multiple engines memory-map files in the same cache directory. See #448.
|
|
///
|
|
/// If `explicit_dir` is `Some`, it is used as the cache directory on all
|
|
/// platforms, bypassing the default.
|
|
pub fn enable_compilation_cache(
|
|
wasmtime_config: &mut Config,
|
|
label: &str,
|
|
explicit_dir: Option<&Path>,
|
|
) -> anyhow::Result<()> {
|
|
// If the caller provided an explicit directory, or we're on Windows and
|
|
// need per-engine isolation, write a TOML config with a custom directory.
|
|
let custom_dir = match explicit_dir {
|
|
Some(dir) => Some(dir.to_path_buf()),
|
|
#[cfg(windows)]
|
|
None => {
|
|
let base = dirs::cache_dir()
|
|
.unwrap_or_else(std::env::temp_dir)
|
|
.join("ironclaw");
|
|
Some(base.join(format!("wasmtime-{}", label)))
|
|
}
|
|
#[cfg(not(windows))]
|
|
None => {
|
|
let _ = label;
|
|
None
|
|
}
|
|
};
|
|
|
|
match custom_dir {
|
|
Some(dir) => {
|
|
std::fs::create_dir_all(&dir)?;
|
|
let toml_path = dir.join("wasmtime-cache.toml");
|
|
let escaped = dir
|
|
.to_string_lossy()
|
|
.replace('\\', "\\\\")
|
|
.replace('"', "\\\"");
|
|
let toml_content = format!("[cache]\nenabled = true\ndirectory = \"{}\"\n", escaped);
|
|
std::fs::write(&toml_path, toml_content)?;
|
|
wasmtime_config.cache_config_load(&toml_path)?;
|
|
Ok(())
|
|
}
|
|
None => {
|
|
wasmtime_config.cache_config_load_default()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration for the WASM runtime.
|
|
#[derive(Debug, Clone)]
|
|
pub struct WasmRuntimeConfig {
|
|
/// Default resource limits for tools.
|
|
pub default_limits: ResourceLimits,
|
|
/// Fuel configuration.
|
|
pub fuel_config: FuelConfig,
|
|
/// Whether to cache compiled modules.
|
|
pub cache_compiled: bool,
|
|
/// Directory for compiled module cache.
|
|
pub cache_dir: Option<PathBuf>,
|
|
/// Cranelift optimization level.
|
|
pub optimization_level: OptLevel,
|
|
}
|
|
|
|
impl Default for WasmRuntimeConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
default_limits: ResourceLimits::default(),
|
|
fuel_config: FuelConfig::default(),
|
|
cache_compiled: true,
|
|
cache_dir: None,
|
|
optimization_level: OptLevel::Speed,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WasmRuntimeConfig {
|
|
/// Create a minimal config for testing.
|
|
pub fn for_testing() -> Self {
|
|
Self {
|
|
default_limits: ResourceLimits::default()
|
|
.with_memory(1024 * 1024) // 1 MB
|
|
.with_fuel(100_000)
|
|
.with_timeout(Duration::from_secs(5)),
|
|
fuel_config: FuelConfig::with_limit(100_000),
|
|
cache_compiled: false,
|
|
cache_dir: None,
|
|
optimization_level: OptLevel::None, // Faster compilation for tests
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A compiled WASM component ready for instantiation.
|
|
///
|
|
/// Contains the pre-compiled component plus cached metadata extracted
|
|
/// from the component during preparation. Stores the compiled `Component`
|
|
/// directly so instantiation doesn't require recompilation.
|
|
pub struct PreparedModule {
|
|
/// Tool name.
|
|
pub name: String,
|
|
/// Tool description (cached from component).
|
|
pub description: String,
|
|
/// Full parameter schema JSON extracted from the component.
|
|
/// Used for discovery and coercion, not necessarily for the compact
|
|
/// schema advertised in the main tools array.
|
|
pub schema: serde_json::Value,
|
|
/// Pre-compiled component (cheaply cloneable via internal Arc).
|
|
component: wasmtime::component::Component,
|
|
/// Resource limits for this tool.
|
|
pub limits: ResourceLimits,
|
|
}
|
|
|
|
impl PreparedModule {
|
|
/// Get the pre-compiled component for instantiation.
|
|
pub fn component(&self) -> &wasmtime::component::Component {
|
|
&self.component
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for PreparedModule {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("PreparedModule")
|
|
.field("name", &self.name)
|
|
.field("description", &self.description)
|
|
.field("limits", &self.limits)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
/// WASM tool runtime.
|
|
///
|
|
/// Manages the Wasmtime engine and a cache of prepared modules.
|
|
pub struct WasmToolRuntime {
|
|
/// Wasmtime engine with configured settings.
|
|
engine: Engine,
|
|
/// Runtime configuration.
|
|
config: WasmRuntimeConfig,
|
|
/// Cache of prepared modules by name.
|
|
modules: RwLock<HashMap<String, Arc<PreparedModule>>>,
|
|
}
|
|
|
|
impl WasmToolRuntime {
|
|
/// Create a new runtime with the given configuration.
|
|
pub fn new(config: WasmRuntimeConfig) -> Result<Self, WasmError> {
|
|
let mut wasmtime_config = Config::new();
|
|
|
|
// Enable fuel consumption for CPU limiting
|
|
if config.fuel_config.enabled {
|
|
wasmtime_config.consume_fuel(true);
|
|
}
|
|
|
|
// Enable epoch interruption as a backup timeout mechanism
|
|
wasmtime_config.epoch_interruption(true);
|
|
|
|
// Enable component model (WASI Preview 2)
|
|
wasmtime_config.wasm_component_model(true);
|
|
|
|
// Disable threads (simplifies security model)
|
|
wasmtime_config.wasm_threads(false);
|
|
|
|
// Set optimization level
|
|
wasmtime_config.cranelift_opt_level(config.optimization_level);
|
|
|
|
// Disable debug info in production for smaller modules
|
|
wasmtime_config.debug_info(false);
|
|
|
|
// Enable persistent compilation cache. Wasmtime serializes compiled native
|
|
// code to disk (~/.cache/wasmtime by default), so subsequent startups
|
|
// deserialize instead of recompiling — typically 10-50x faster.
|
|
//
|
|
// On Windows, each Engine gets its own cache subdirectory to avoid
|
|
// OS error 33 (ERROR_LOCK_VIOLATION) when multiple engines share the
|
|
// default cache and Windows holds exclusive locks on memory-mapped
|
|
// files. See #448.
|
|
if let Err(e) =
|
|
enable_compilation_cache(&mut wasmtime_config, "tools", config.cache_dir.as_deref())
|
|
{
|
|
tracing::warn!("Failed to enable wasmtime compilation cache: {}", e);
|
|
}
|
|
|
|
let engine = Engine::new(&wasmtime_config).map_err(|e| {
|
|
WasmError::EngineCreationFailed(format!("Failed to create Wasmtime engine: {}", e))
|
|
})?;
|
|
|
|
// Spawn a background thread that periodically increments the engine's
|
|
// epoch counter. Without this, epoch_deadline_trap() never fires and
|
|
// WASM modules can spin indefinitely even with a deadline set.
|
|
let ticker_engine = engine.clone();
|
|
std::thread::Builder::new()
|
|
.name("wasm-epoch-ticker".into())
|
|
.spawn(move || {
|
|
loop {
|
|
std::thread::sleep(EPOCH_TICK_INTERVAL);
|
|
ticker_engine.increment_epoch();
|
|
}
|
|
})
|
|
.map_err(|e| {
|
|
WasmError::EngineCreationFailed(format!(
|
|
"Failed to spawn epoch ticker thread: {}",
|
|
e
|
|
))
|
|
})?;
|
|
|
|
Ok(Self {
|
|
engine,
|
|
config,
|
|
modules: RwLock::new(HashMap::new()),
|
|
})
|
|
}
|
|
|
|
/// Get the Wasmtime engine.
|
|
pub fn engine(&self) -> &Engine {
|
|
&self.engine
|
|
}
|
|
|
|
/// Get the runtime configuration.
|
|
pub fn config(&self) -> &WasmRuntimeConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Prepare a WASM component for execution.
|
|
///
|
|
/// This validates and compiles the component, extracting metadata.
|
|
/// The compiled component is cached for fast instantiation.
|
|
pub async fn prepare(
|
|
&self,
|
|
name: &str,
|
|
wasm_bytes: &[u8],
|
|
limits: Option<ResourceLimits>,
|
|
) -> Result<Arc<PreparedModule>, WasmError> {
|
|
// Check if already prepared
|
|
if let Some(module) = self.modules.read().await.get(name) {
|
|
return Ok(Arc::clone(module));
|
|
}
|
|
|
|
let name = name.to_string();
|
|
let wasm_bytes = wasm_bytes.to_vec();
|
|
let engine = self.engine.clone();
|
|
let default_limits = self.config.default_limits.clone();
|
|
|
|
// Compile in blocking task (Wasmtime compilation is synchronous)
|
|
let prepared = tokio::task::spawn_blocking(move || {
|
|
// Validate and compile the component
|
|
let component = wasmtime::component::Component::new(&engine, &wasm_bytes)
|
|
.map_err(|e| WasmError::CompilationFailed(e.to_string()))?;
|
|
|
|
// Briefly instantiate to extract metadata (description + schema)
|
|
// from the tool's exports, analogous to MCP's list_tools().
|
|
let effective_limits = limits.clone().unwrap_or(default_limits.clone());
|
|
let (description, schema) = crate::tools::wasm::wrapper::extract_wasm_metadata(
|
|
&engine,
|
|
&component,
|
|
&effective_limits,
|
|
)
|
|
.unwrap_or_else(|e| {
|
|
tracing::warn!(
|
|
name = %name,
|
|
error = %e,
|
|
"WASM metadata extraction failed, using fallbacks"
|
|
);
|
|
(
|
|
"WASM sandboxed tool".to_string(),
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {},
|
|
"additionalProperties": true
|
|
}),
|
|
)
|
|
});
|
|
|
|
Ok::<_, WasmError>(PreparedModule {
|
|
name: name.clone(),
|
|
description,
|
|
schema,
|
|
component,
|
|
limits: limits.unwrap_or(default_limits),
|
|
})
|
|
})
|
|
.await
|
|
.map_err(|e| WasmError::ExecutionPanicked(format!("Preparation task panicked: {}", e)))??;
|
|
|
|
let prepared = Arc::new(prepared);
|
|
|
|
// Cache the prepared module
|
|
if self.config.cache_compiled {
|
|
self.modules
|
|
.write()
|
|
.await
|
|
.insert(prepared.name.clone(), Arc::clone(&prepared));
|
|
}
|
|
|
|
tracing::debug!(
|
|
name = %prepared.name,
|
|
"Prepared WASM tool for execution"
|
|
);
|
|
|
|
Ok(prepared)
|
|
}
|
|
|
|
/// Get a prepared module by name.
|
|
pub async fn get(&self, name: &str) -> Option<Arc<PreparedModule>> {
|
|
self.modules.read().await.get(name).cloned()
|
|
}
|
|
|
|
/// Remove a prepared module from the cache.
|
|
pub async fn remove(&self, name: &str) -> Option<Arc<PreparedModule>> {
|
|
self.modules.write().await.remove(name)
|
|
}
|
|
|
|
/// List all prepared module names.
|
|
pub async fn list(&self) -> Vec<String> {
|
|
self.modules.read().await.keys().cloned().collect()
|
|
}
|
|
|
|
/// Clear all cached modules.
|
|
pub async fn clear(&self) {
|
|
self.modules.write().await.clear();
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for WasmToolRuntime {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("WasmToolRuntime")
|
|
.field("config", &self.config)
|
|
.field("modules", &"<RwLock<HashMap>>")
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::tools::wasm::limits::ResourceLimits;
|
|
use crate::tools::wasm::runtime::{WasmRuntimeConfig, WasmToolRuntime};
|
|
|
|
#[test]
|
|
fn test_runtime_config_default() {
|
|
let config = WasmRuntimeConfig::default();
|
|
assert!(config.cache_compiled);
|
|
assert!(config.fuel_config.enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn test_runtime_config_for_testing() {
|
|
let config = WasmRuntimeConfig::for_testing();
|
|
assert!(!config.cache_compiled);
|
|
assert_eq!(config.default_limits.memory_bytes, 1024 * 1024);
|
|
}
|
|
|
|
#[test]
|
|
fn test_runtime_creation() {
|
|
let config = WasmRuntimeConfig::for_testing();
|
|
let runtime = WasmToolRuntime::new(config).unwrap();
|
|
// Engine was created successfully, which validates the config
|
|
assert!(runtime.config().fuel_config.enabled);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_module_cache_operations() {
|
|
let config = WasmRuntimeConfig::for_testing();
|
|
let runtime = WasmToolRuntime::new(config).unwrap();
|
|
|
|
// Initially empty
|
|
assert!(runtime.list().await.is_empty());
|
|
assert!(runtime.get("test").await.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_prepared_module_limits() {
|
|
let limits = ResourceLimits::default()
|
|
.with_memory(5 * 1024 * 1024)
|
|
.with_fuel(500_000);
|
|
|
|
assert_eq!(limits.memory_bytes, 5 * 1024 * 1024);
|
|
assert_eq!(limits.fuel, 500_000);
|
|
}
|
|
|
|
/// Per-engine cache directories must work correctly to avoid file lock
|
|
/// conflicts on Windows where multiple engines sharing a single cache
|
|
/// directory triggers OS error 33 (ERROR_LOCK_VIOLATION). Regression test
|
|
/// for #448: `enable_compilation_cache` must create a subdirectory and
|
|
/// produce a valid TOML config that wasmtime can load.
|
|
#[test]
|
|
fn test_enable_compilation_cache_with_explicit_dir() {
|
|
use crate::tools::wasm::runtime::enable_compilation_cache;
|
|
|
|
let tmp = tempfile::tempdir().expect("failed to create temp dir");
|
|
let cache_dir = tmp.path().join("custom-cache");
|
|
|
|
let mut config = wasmtime::Config::new();
|
|
enable_compilation_cache(&mut config, "test-engine", Some(cache_dir.as_path()))
|
|
.expect("enable_compilation_cache should succeed with explicit dir");
|
|
|
|
// The cache directory should have been created.
|
|
assert!(cache_dir.exists(), "cache directory should be created");
|
|
|
|
// A TOML config file should have been written inside.
|
|
let toml_path = cache_dir.join("wasmtime-cache.toml");
|
|
assert!(toml_path.exists(), "TOML config should be written");
|
|
|
|
let content = std::fs::read_to_string(&toml_path).unwrap();
|
|
assert!(
|
|
content.contains("[cache]"),
|
|
"TOML must contain [cache] section"
|
|
);
|
|
assert!(content.contains("enabled = true"), "cache must be enabled");
|
|
}
|
|
|
|
/// Two engines with different labels must get independent cache directories
|
|
/// so that their file locks do not conflict. Regression test for #448.
|
|
#[test]
|
|
fn test_enable_compilation_cache_label_isolation() {
|
|
use crate::tools::wasm::runtime::enable_compilation_cache;
|
|
|
|
let tmp = tempfile::tempdir().expect("failed to create temp dir");
|
|
let base = tmp.path().join("isolation");
|
|
|
|
let dir_a = base.join("engine-a");
|
|
let dir_b = base.join("engine-b");
|
|
|
|
let mut config_a = wasmtime::Config::new();
|
|
enable_compilation_cache(&mut config_a, "a", Some(dir_a.as_path()))
|
|
.expect("cache A should succeed");
|
|
|
|
let mut config_b = wasmtime::Config::new();
|
|
enable_compilation_cache(&mut config_b, "b", Some(dir_b.as_path()))
|
|
.expect("cache B should succeed");
|
|
|
|
// Both directories must exist and be distinct.
|
|
assert!(dir_a.exists());
|
|
assert!(dir_b.exists());
|
|
assert_ne!(dir_a, dir_b);
|
|
}
|
|
|
|
/// The WASM runtime (Wasmtime engine) must initialise successfully even
|
|
/// when no tools directory exists on disk. The engine only configures the
|
|
/// compiler and epoch ticker — loading modules from a directory is a
|
|
/// separate step. Regression test for a bug where the runtime was gated
|
|
/// on `tools_dir.exists()`, causing extensions installed after startup
|
|
/// (e.g. via the web UI) to fail with "WASM runtime not available".
|
|
#[test]
|
|
fn test_runtime_creation_without_tools_dir() {
|
|
let config = WasmRuntimeConfig::for_testing();
|
|
// Runtime should succeed even though no tools directory exists.
|
|
let runtime = WasmToolRuntime::new(config).expect("runtime should init without tools dir");
|
|
assert!(runtime.config().fuel_config.enabled);
|
|
}
|
|
}
|