Skip to content

Testing & Metadata

Overview

The App Engine provides two systems that improve developer experience:

Testing     = Mocks, TestEngine, assertion helpers — test handlers without a full engine
Metadata    = Version, PayloadSchema — describe and version command/task/plugin definitions

Part 1: Testing Infrastructure

Why Mocks?

Without mocks, testing a CommandHandler requires: - Creating a full AppEngine via Bootstrap - Registering the handler - Constructing EngineRef - Executing the command

With mocks, you can: - Create a MockCommandExecutor that records executions - Create a MockEventBus that records publications - Verify specific commands were executed or events were published

MockCommandExecutor

Records executed commands and provides assertion methods:

use app_shell::app_engine::{MockCommandExecutor, CommandDefinition, Command, CommandContext, ExecutionId, CommandInput, EngineRef, CommandHandler, CommandResult, CommandError};

struct EchoHandler;
impl CommandHandler for EchoHandler {
    fn execute(&self, _: &CommandInput, _: &CommandContext, _: &EngineRef) -> Result<CommandResult, CommandError> {
        Ok(CommandResult::success())
    }
}

// Create a TestEngine for EngineRef
let test_engine = app_shell::app_engine::TestEngine::new();

// Create mock executor
let mut exec = MockCommandExecutor::new();
exec.register(
    CommandDefinition::new("app.echo", "Echo", "Echo"),
    Box::new(EchoHandler),
)?;

// Execute a command
let cmd = Command::with_empty_input("app.echo", CommandContext::new(ExecutionId::new()));
let engine_ref = test_engine.engine_ref();
exec.execute(&cmd, &engine_ref)?;

// Assert it was executed
exec.assert_executed("app.echo");
assert_eq!(exec.executed_count(), 1);

// Assert something was NOT executed
exec.assert_not_executed("app.missing");

MockEventBus

Records published events:

use app_shell::app_engine::MockEventBus;
use app_shell::app_engine::Event;

let bus = MockEventBus::new();

bus.publish(&Event::new("task.completed"));
bus.publish(&Event::new("document.changed"));

// Assert events were published
bus.assert_published("task.completed");
bus.assert_published("document.changed");

// Assert an event was NOT published
bus.assert_not_published("task.failed");

// Count
assert_eq!(bus.published_count(), 2);

// Get all published event types
let events = bus.published_events();
assert!(events.contains(&"task.completed".to_string()));

MockSignalBus

Records emitted signals:

use app_shell::app_engine::{MockSignalBus, Signal};

let bus = MockSignalBus::new();

bus.emit(&Signal::new("task.progress_changed"));
bus.emit(&Signal::new("state.changed"));

// Assert signals were emitted
bus.assert_emitted("task.progress_changed");
bus.assert_emitted("state.changed");

// Count
assert_eq!(bus.emitted_count(), 2);

TestEngine

A pre-configured, lightweight engine for fast test setup:

use app_shell::app_engine::TestEngine;

let engine = TestEngine::new();

// Provides real (empty) subsystems
let engine_ref = engine.engine_ref();

// All systems accessible
let _ = engine_ref.event_bus;
let _ = engine_ref.signal_bus;
let _ = engine_ref.resource_manager;
let _ = engine_ref.config_store;
let _ = engine_ref.preferences;
let _ = engine_ref.state_manager;
let _ = engine_ref.command_executor;
let _ = engine_ref.scheduler;
let _ = engine_ref.history_store;

TestEngine with Real Handlers

let mut engine = TestEngine::new();

// Register a real handler
engine.command_executor.register(
    CommandDefinition::new("test.echo", "Test", "Test"),
    Box::new(EchoHandler),
)?;

// Execute
let cmd = Command::with_empty_input("test.echo", CommandContext::new(ExecutionId::new()));
let engine_ref = engine.engine_ref();
let result = engine.command_executor.execute(&cmd, &engine_ref).unwrap();
assert!(result.is_success());

Assertion Helpers

Convenience functions for common assertions:

use app_shell::app_engine::{
    assert_command_executed,
    assert_command_not_executed,
    assert_event_published,
    assert_event_not_published,
    assert_signal_emitted,
};

// With MockCommandExecutor
assert_command_executed(&exec, "app.echo");
assert_command_not_executed(&exec, "app.missing");

// With MockEventBus
assert_event_published(&bus, "task.completed");
assert_event_not_published(&bus, "task.failed");

// With MockSignalBus
assert_signal_emitted(&bus, "task.progress_changed");

Test Pattern: Verify Handler Publishes Events

use app_shell::app_engine::*;
use std::sync::{Arc, Mutex};

struct GreetHandler {
    received: Arc<Mutex<bool>>,
}

impl CommandHandler for GreetHandler {
    fn execute(&self, _: &CommandInput, _: &CommandContext, _: &EngineRef) -> Result<CommandResult, CommandError> {
        // In a real handler, this would publish via EngineRef.event_bus
        Ok(CommandResult::success())
    }
}

#[test]
fn handler_executes_and_succeeds() {
    let test_engine = TestEngine::new();
    let mut exec = MockCommandExecutor::new();
    exec.register(
        CommandDefinition::new("app.greet", "Greet", "Greet"),
        Box::new(GreetHandler { received: Arc::new(Mutex::new(false)) }),
    ).unwrap();

    let cmd = Command::with_empty_input("app.greet", CommandContext::new(ExecutionId::new()));
    let engine_ref = test_engine.engine_ref();
    let result = exec.execute(&cmd, &engine_ref).unwrap();

    assert!(result.is_success());
    assert_command_executed(&exec, "app.greet");
}

Test Pattern: Verify Events Are Published

#[test]
fn event_is_published() {
    let bus = MockEventBus::new();

    // Simulate a handler publishing an event
    bus.publish(&Event::new("task.completed").with_source("TestHandler"));

    assert_event_published(&bus, "task.completed");
    assert_event_not_published(&bus, "task.failed");
    assert_eq!(bus.published_count(), 1);
}

Test Pattern: Verify Signals Are Emitted

#[test]
fn signal_is_emitted() {
    let bus = MockSignalBus::new();

    // Simulate a handler emitting a signal
    bus.emit(&Signal::new("task.progress_changed").with_payload(0.5_f32));

    assert_signal_emitted(&bus, "task.progress_changed");
    assert_eq!(bus.emitted_count(), 1);
}

Part 2: Metadata

Version

Semantic versioning for commands, tasks, and plugins:

use app_shell::app_engine::Version;

let v1 = Version::new(1, 2, 3);
assert_eq!(v1.major(), 1);
assert_eq!(v1.minor(), 2);
assert_eq!(v1.patch(), 3);

assert_eq!(v1.to_string(), "1.2.3");

Version Compatibility

Same major version = compatible:

let v1 = Version::new(1, 0, 0);
let v2 = Version::new(1, 5, 3);
let v3 = Version::new(2, 0, 0);

assert!(v1.is_compatible_with(&v2));   // Same major
assert!(!v1.is_compatible_with(&v3));  // Different major

Version Ordering

let v1 = Version::new(1, 0, 0);
let v2 = Version::new(1, 1, 0);
let v3 = Version::new(2, 0, 0);

assert!(v1 < v2);
assert!(v2 < v3);
assert!(v1 < v3);

Version At Least

let v = Version::new(1, 2, 3);
assert!(v.is_at_least(&Version::new(1, 2, 0)));   // Yes — 1.2.3 >= 1.2.0
assert!(!v.is_at_least(&Version::new(1, 3, 0)));  // No — 1.2.3 < 1.3.0

Versioned Trait

use app_shell::app_engine::Versioned;

// Any type that exposes a Version can implement Versioned
pub trait Versioned {
    fn version(&self) -> &Version;
}

PayloadSchema

Describes the expected type of a command/task input or output:

use app_shell::app_engine::PayloadSchema;

let schema = PayloadSchema::of::<String>();
assert!(schema.matches::<String>());
assert!(!schema.matches::<i32>());
assert!(schema.to_string().contains("String"));

PayloadSchema with Description

let schema = PayloadSchema::of_with_description::<String>("The document title");
assert_eq!(schema.description(), "The document title");

PayloadSchema Equality

Two schemas are equal if they describe the same type:

let s1 = PayloadSchema::of::<String>();
let s2 = PayloadSchema::of::<String>();
let s3 = PayloadSchema::of::<i32>();
assert_eq!(s1, s2);
assert_ne!(s1, s3);

Part 3: Versioned Definitions

CommandDefinition with Version and Schema

use app_shell::app_engine::CommandDefinition;

let def = CommandDefinition::new("project.export", "Export Project", "Exports the current project")
    .with_version(Version::new(1, 0, 0))
    .with_input_schema(PayloadSchema::of::<ExportProjectInput>())
    .with_output_schema(PayloadSchema::of::<ExportResult>());

assert_eq!(def.version(), &Version::new(1, 0, 0));
assert!(def.input_schema().is_some());
assert!(def.output_schema().is_some());
assert!(def.input_schema().unwrap().matches::<ExportProjectInput>());
assert!(def.output_schema().unwrap().matches::<ExportResult>());

Compatibility Check

let def = CommandDefinition::new("project.export", "Export", "Export")
    .with_version(Version::new(1, 5, 0));

assert!(def.is_compatible_with(&Version::new(1, 0, 0)));   // Same major
assert!(!def.is_compatible_with(&Version::new(2, 0, 0)));  // Different major

TaskDefinition with Version and Schema

use app_shell::app_engine::TaskDefinition;

let def = TaskDefinition::new("image.render", "Render Image", "Render an image")
    .with_version(Version::new(3, 1, 0))
    .with_input_schema(PayloadSchema::of::<RenderInput>())
    .with_output_schema(PayloadSchema::of::<RenderResult>());

assert_eq!(def.version(), &Version::new(3, 1, 0));
assert!(def.input_schema().is_some());
assert!(def.output_schema().is_some());

PluginManifest with API Version

use app_shell::app_engine::PluginManifest;

let manifest = PluginManifest::new("image-plugin", "Image Plugin")
    .with_engine_api_version(Version::new(1, 0, 0));

assert_eq!(manifest.engine_api_version(), &Version::new(1, 0, 0));
assert!(manifest.is_compatible_with(&Version::new(1, 5, 0)));   // Same major
assert!(!manifest.is_compatible_with(&Version::new(2, 0, 0)));  // Different major

Part 4: Integration — Testing with Metadata

Verify Schema Matches Expected Type

#[test]
fn command_input_schema_matches() {
    let def = CommandDefinition::new("export", "Export", "Export")
        .with_input_schema(PayloadSchema::of::<ExportInput>())
        .with_output_schema(PayloadSchema::of::<ExportResult>());

    assert!(def.input_schema().unwrap().matches::<ExportInput>());
    assert!(def.output_schema().unwrap().matches::<ExportResult>());
    assert!(!def.input_schema().unwrap().matches::<i32>());
}

Test Version Compatibility

#[test]
fn version_compatibility() {
    let def = CommandDefinition::new("export", "Export", "Export")
        .with_version(Version::new(2, 0, 0));

    assert!(def.is_compatible_with(&Version::new(2, 1, 0)));
    assert!(!def.is_compatible_with(&Version::new(3, 0, 0)));
}

Complete Test Example

use app_shell::app_engine::*;

struct CountingHandler {
    order: Arc<Mutex<Vec<usize>>>,
    tag: usize,
}

impl EventHandler for CountingHandler {
    fn handle(&self, _: &Event) {
        self.order.lock().unwrap().push(self.tag);
    }
}

#[test]
fn full_test_with_mocks_and_metadata() {
    // --- Setup ---
    let test_engine = TestEngine::new();
    let engine_ref = test_engine.engine_ref();

    let mut exec = MockCommandExecutor::new();
    let def = CommandDefinition::new("app.test", "Test", "Test command")
        .with_version(Version::new(1, 0, 0))
        .with_input_schema(PayloadSchema::of::<String>());

    exec.register(def, Box::new(EchoHandler)).unwrap();

    // --- Execute ---
    let cmd = Command::with_empty_input("app.test", CommandContext::new(ExecutionId::new()));
    let result = exec.execute(&cmd, &engine_ref).unwrap();

    // --- Assertions ---
    assert!(result.is_success());
    assert_command_executed(&exec, "app.test");
    assert_eq!(exec.executed_count(), 1);

    // --- Verify metadata ---
    let def = exec.list().first().unwrap();
    assert_eq!(def.version(), &Version::new(1, 0, 0));
    assert!(def.input_schema().unwrap().matches::<String>());
}

Quick Reference

Mocks

// MockCommandExecutor
let mut exec = MockCommandExecutor::new();
exec.register(definition, handler)?;
exec.execute(&cmd, &engine_ref)?;
exec.assert_executed("cmd.id");
exec.assert_not_executed("cmd.missing");
exec.executed_count();

// MockEventBus
let bus = MockEventBus::new();
bus.publish(&event);
bus.assert_published("event.type");
bus.assert_not_published("event.missing");
bus.published_count();
bus.published_events();

// MockSignalBus
let bus = MockSignalBus::new();
bus.emit(&signal);
bus.assert_emitted("signal.type");
bus.emitted_count();

TestEngine

let engine = TestEngine::new();
let engine_ref = engine.engine_ref();
// engine.command_executor — real CommandExecutor
// engine.task_manager — real TaskManager
// All other subsystems — real (empty)

Assertion Helpers

assert_command_executed(&exec, "cmd.id");
assert_command_not_executed(&exec, "cmd.missing");
assert_event_published(&bus, "event.type");
assert_event_not_published(&bus, "event.missing");
assert_signal_emitted(&bus, "signal.type");

Version

Version::new(major, minor, patch)
v.major() / v.minor() / v.patch()
v.to_string()                 // "1.2.3"
v.is_compatible_with(&other)  // Same major
v.is_at_least(&other)         // >=
v1 < v2                       // Ordering
Version::default()            // 1.0.0

PayloadSchema

PayloadSchema::of::<T>()                        // Create
PayloadSchema::of_with_description::<T>("desc") // With description
schema.matches::<T>()                           // Type check
schema.type_name()                              // &'static str
schema.description()                            // &str
s1 == s2                                        // Same type

Versioned Definitions

CommandDefinition::new(...)
    .with_version(Version::new(1, 0, 0))
    .with_input_schema(PayloadSchema::of::<InputType>())
    .with_output_schema(PayloadSchema::of::<OutputType>());

TaskDefinition::new(...)
    .with_version(Version::new(1, 0, 0))
    .with_input_schema(PayloadSchema::of::<InputType>())
    .with_output_schema(PayloadSchema::of::<OutputType>());

PluginManifest::new(...)
    .with_engine_api_version(Version::new(1, 0, 0));

def.is_compatible_with(&version);

Next Steps