Skip to content

feat: add headless CLI and MCP server modes - #16

Merged
smartel99 merged 17 commits into
mainfrom
feature/headless
Aug 5, 2026
Merged

feat: add headless CLI and MCP server modes#16
smartel99 merged 17 commits into
mainfrom
feature/headless

Conversation

@smartel99

Copy link
Copy Markdown
Owner

Summary

Add two headless execution modes to Frasy at the framework level:

  • CLI mode (--headless) — run tests from the command line with colored progress output, stdin-based popup interaction, and JSON/human report formats
  • MCP server mode (--mcp-server) — run as a Model Context Protocol tool server over stdio, enabling AI agents (via Kiro) to launch tests, interact with popups, and retrieve results using native tool calls

Key Features

  • ProductProvider interface — applications implement a single setup() method shared between GUI and headless paths (no code duplication)
  • Console popup handler — text-based popup interaction via stdin/stdout with timeout support
  • MCP toolslist_products, run_tests, get_status, get_pending_popup, respond_to_popup, get_results
  • Progress reporter — callback-based, captures every sequence/test/expectation event in real-time
  • Colored output — green for pass, red for fail, white for start events
  • Silent by default — logs suppressed unless --verbose is passed; stdout is exclusively structured output
  • Exit codes — 0 (pass), 1 (fail), 2 (error)

Testing

  • 57 unit tests (25 CLI args + 32 headless components)
  • Live-tested MCP mode via Kiro MCP connection (all tools working)
  • End-to-end headless tests with popup interaction (single + multi-UUT)
  • Demo application refactored to demonstrate the shared ProductProvider pattern

Documentation

Add Frasy::CliArgs struct with static parse() and get() methods.
Supports the following flags:
  --headless, --product, --operator, --serial (repeatable),
  --config, --output-format, --output-dir, --skip-verification,
  --popup-timeout, --help

Validates required flags in headless mode (product, operator, serial)
and exits with code 2 on invalid arguments. Non-headless mode ignores
headless-specific flags gracefully.

Includes 25 unit tests covering all parsing paths, validation errors,
and death tests for std::exit behavior.
…rface

- Add Frasy::Headless::ProductProvider abstract class with:
  - validateSerialNumber() for product-specific serial validation
  - setup() to configure orchestrator and CANopen for a product
  - onTestComplete() optional post-test hook
- Add setProductProvider()/getProductProvider() on Frasy::Interpreter
- Modify Brigerad EntryPoint.h to:
  - Parse CLI args before application creation
  - Branch on --headless after CreateApplication
  - Check for registered ProductProvider (exits with code 2 if missing)
  - Use exitCode variable to avoid returning inside BR_GUARDED_SCOPE
- Add demo_mode/CMakeLists.txt for cmake-based building
- Fix demo_mode to use new getExpectationsMutex/getExpectationsVector API

Tested: headless mode exits with code 2 (no provider), GUI mode runs normally.
- Add 'visible' field to WindowProps (default: true)
- Add GLFW_VISIBLE=FALSE hint in WindowsWindow::Init() when not visible
- Add 'visible' parameter to Brigerad::Application constructor
- Frasy::Interpreter passes !headless as visible to Application base

In headless mode the OpenGL context still initializes (required by
framework internals) but no window appears on screen. Normal GUI
mode is unaffected — visible defaults to true.

Tested: headless runs without any visible window, GUI mode shows
window as before.
Add Frasy::Headless::HeadlessRunner that drives the full headless
test lifecycle:
  1. Discovers products by scanning lua/user/
  2. Validates requested product exists (lists available on error)
  3. Validates serial numbers via ProductProvider
  4. Calls ProductProvider::setup() to configure orchestrator
  5. Validates serial count matches UUT count from environment
  6. Runs the solution asynchronously and polls for completion
  7. Reports results and returns exit code (0=pass, 1=fail, 2=error)

Wire HeadlessRunner into EntryPoint.h replacing the stub.

Add DemoProductProvider in demo_mode that accepts any non-empty serial
and configures orchestrator/CANopen from the IB map.

Add CLI-tests product (simple math/range/string tests with no hardware)
for headless mode validation.

Tested:
  - CLI-tests product runs all 5 tests, all pass, exits 0
  - Invalid product name lists available products, exits 2
  - GUI mode unaffected
Implement text-based popup interaction via stdin/stdout:
- Present popups with text, numbered inputs, and button labels
- Accept N=value to set inputs, button label to consume
- Call button action callbacks with input values (matching GUI behavior)
- Support human and JSON output formats
- Timeout support via --popup-timeout flag (auto-cancel on expiry)
- EOF handling: auto-cancel gracefully if stdin closes
- Trim trailing whitespace from stdin (Windows pipe compat)

Multi-UUT popup serialization:
- Hold ioMutex for entire popup interaction (present + read + consume)
- Ensures one popup completes before the next presents
- Prevents interleaving from concurrent UUT threads

Redirect logs to stderr in headless mode:
- Add useStderr parameter to Brigerad::Log::Init()
- In headless mode, stdout is exclusively for structured output
- Logs go to stderr, eliminating interleaving with popup display

Add setPopupImport() to Orchestrator:
- Allows HeadlessRunner to install alternative popup handling
- Falls back to default GUI importPopup if not set

Add test products:
- CLI-tests/popup_test.lua: confirm popup, single input, multi-input
- CLI-tests-multi: 2-UUT product validating popup serialization

Tested: single-UUT popups with input validation, multi-UUT
serialization, clean stdout/stderr separation.
Add plan for --mcp-server mode where Frasy acts as an MCP stdio
tool server (JSON-RPC). Enables AI agents to launch tests, interact
with popups, and retrieve results via Kiro's native MCP integration.

Tools: list_products, run_tests, get_status, get_pending_popup,
respond_to_popup, get_results, abort.

Shifted Progress Reporter to Task 7, Report Summary to Task 8,
Demo Refactor to Task 9, Documentation to Task 10.
Implement --mcp-server flag that runs Frasy as a Model Context Protocol
(MCP) stdio server. AI agents (via Kiro) can launch tests, interact
with popups, and retrieve results using native MCP tool calls.

MCP protocol implementation (mcp_server.h/.cpp):
- JSON-RPC over stdin/stdout (newline-delimited, protocol v2024-11-05)
- Handles initialize handshake, tools/list, tools/call, ping
- Tool registration with JSON Schema input definitions

MCP tools (mcp_runner.h/.cpp):
- list_products: discovers available products in lua/user/
- run_tests: validates serials, sets up orchestrator, launches async
- get_status: returns running/passed/failed/error + per-UUT states
- get_results: reads report JSONs, returns test counts and duration
- get_pending_popup: returns next popup awaiting interaction
- respond_to_popup: sends inputs and button press to unblock popup

MCP popup handler (mcp_popup_handler.h/.cpp):
- Installs into Lua state as alternative popup import
- Queues popups with condition variable for blocking
- Agent polls get_pending_popup, responds via respond_to_popup
- Button action callbacks invoked with input values

Additional changes:
- --mcp-server and --headless are mutually exclusive
- Window hidden and logs to stderr in MCP mode
- Graceful shutdown: waits for running orchestrator before exit
- Add CLI-tests-nopopup product for no-popup MCP testing

Tested live via Kiro MCP connection:
- list_products returns all products
- run_tests + get_status + get_results lifecycle works
- Popup interaction (confirm, single input, multi-input) all pass
- 8/8 tests passed on CLI-tests product with agent-driven popups
Replace polling approach with event-driven progress reporting:

Orchestrator changes:
- Add setProgressCallback() method
- Install __progress.report() Lua binding in initLua for execution stage
- No-op binding for generation/validation stages

Lua framework changes (orchestrator.lua):
- Call __progress.report at sequence start/end
- Call __progress.report at test start/end
- Call __progress.report at each expectation evaluation
- Events carry type, UUT, name, parent context, and pass/fail

ProgressReporter (headless):
- Receives events via callback (no polling thread)
- Human format: indented tree with >> sequence, > test, [PASS]/[FAIL]
- JSON format: structured JSON-lines per event

HeadlessRunner integration:
- Installs progress callback before runSolution
- Maps string type to ProgressEvent enum
- Forwards to ProgressReporter::onEvent()

Every sequence, test, and expectation is now reported in real-time
regardless of execution speed.
In headless and MCP server modes, console logs are now suppressed
by default. Only structured progress output appears on stdout.
Logs still go to the rotating file sink (logs/frasy/log.json).

Add --verbose flag to re-enable stderr logging when needed for
debugging. GUI mode is unaffected (always logs to stdout).

Changes:
- Add 'silent' parameter to Brigerad::Log::Init()
- When silent, no console sink is added (file sink only)
- Add --verbose to CliArgs and help text
- EntryPoint passes silent=true when headless/MCP without --verbose

Tested: without --verbose stderr is empty, with --verbose stderr
shows all logs.
Use ANSI color codes in human-readable progress format:
- White (reset): SequenceStart, TestStart entries
- Green: entries with pass=true (SequenceEnd, TestEnd, Expectation)
- Red: entries with pass=false

JSON format is unaffected (no color codes in structured output).
Replace the simple log-based result reporting with a full summary:

Human format:
- Colored bordered table with per-UUT results
- Test counts (passed/total), duration, report file paths
- Green [PASS] / Red [FAIL] with overall result

JSON format:
- Structured run_end object with overall_pass, product, and
  per-UUT array (serial, state, tests_passed, tests_total,
  duration, report path)

Exit codes:
- 0: all enabled UUTs passed
- 1: one or more UUTs failed (expectation failures)
- 2: error (orchestrator crash, missing reports, setup failure)

Reads logs/last/{uut}.json for detailed test counts and timing.

Tested: all-pass returns 0, popup-timeout failure returns 1.
Refactor MyMainApplicationLayer::makeOrchestrator to delegate
orchestrator and CANopen setup to ProductProvider::setup(), establishing
the shared-logic pattern between GUI and headless paths.

Before: makeOrchestrator duplicated loadUserFiles, CANopen node
configuration, and start logic inline.

After: makeOrchestrator calls provider->setup() for all orchestrator/
CANopen work, then handles GUI-specific post-setup only (serial fields
resize, active product assignment).

This demonstrates the pattern for other applications: implement
ProductProvider::setup() once, call it from both GUI makeOrchestrator
and the headless/MCP runner.

Tested: GUI mode launches normally, headless mode passes all tests.
Add getting-started/headless-mode.md:
- Full CLI flag reference (--headless, --mcp-server, --product, etc.)
- Exit code meanings (0=pass, 1=fail, 2=error)
- Human and JSON output format examples
- Popup interaction protocol (stdin commands + JSON response)
- MCP server mode: tool descriptions, workflow, configuration

Add developer-guide/headless-mode.md:
- ProductProvider interface and method responsibilities
- Registration in Interpreter constructor
- makeOrchestrator refactoring pattern (before/after)
- Product routing examples
- Testing instructions for all modes

Update architecture/overview.md:
- Add headless/MCP to Key Design Decisions with links

Update mkdocs.yml:
- Add both pages to Getting Started and Developer Guide nav

Verified: mkdocs build --strict passes.
Add FrasyTest_Headless module with 32 tests covering:

MCP server (6 tests):
- Tool registration
- JSON-RPC message format validation (initialize, tools/list,
  tools/call result, error responses)

Console popup parsing (14 tests):
- parseHumanLine: button-only, input assignment (N=value),
  values with equals, non-numeric equals, empty values
- parseJsonLine: button-only, button+inputs, inputs-only,
  invalid JSON, empty object, multiple inputs

Progress reporter (9 tests):
- Human format: sequence start/end, test start, expectation
- JSON format: sequence_start, test_end, expectation with all
  fields, timestamp presence

Product discovery (3 tests):
- Directory structure with environment.lua detection
- Directories without environment.lua excluded
- Empty user directory returns no products

Refactored parseHumanLine/parseJsonLine from anonymous namespace
to Frasy::Headless (declared in header) for testability.

Total test count: 57 (25 cli_args + 32 headless), all passing.
@smartel99 smartel99 added the enhancement New feature or request label Aug 5, 2026
@smartel99
smartel99 marked this pull request as ready for review August 5, 2026 20:47
The orchestrator.lua now calls __progress.report() during execution,
but the test fixture never defined this global, causing all
ExpectationClass tests to crash with 'attempt to index a nil value'.

Adds a no-op __progress.report mock matching the stub that
orchestrator.cpp provides for non-callback scenarios.
CTest runs each test as a separate process in parallel. All three
ProductDiscovery tests were using the same fixed temp path, so one
test's remove_all would delete directories another test just created.

Use a randomly-suffixed unique directory per test invocation to
eliminate the race condition.
@smartel99
smartel99 merged commit 9077e1a into main Aug 5, 2026
1 check passed
@smartel99
smartel99 deleted the feature/headless branch August 5, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant