Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Test IO (nexus.io.test)

Non-interactive IO plugin for automated integration testing. Replaces nexus.io.tui in test configurations to drive sessions programmatically.

Purpose

The test IO plugin feeds scripted inputs into the engine, collects all bus events during execution, and handles approval requests automatically. Used alongside pkg/testharness for Go integration tests.

Configuration

plugins:
  active:
    - nexus.io.test
    # ... other plugins

  nexus.io.test:
    inputs:                          # scripted user messages (in order)
      - "Hello, who are you?"
      - "List files in current directory."
    input_delay: 500ms               # delay between inputs (default: 500ms)
    approval_mode: approve           # approve | deny | per-prompt (default: approve)
    approval_rules:                  # only when approval_mode: per-prompt
      - match: "shell"               # substring match on tool call or description
        action: approve
      - match: "rm -rf"
        action: deny
    ask_responses:                   # canned answers for io.ask events
      - "yes"
    hitl_responses:                  # canned answers for hitl.requested events
      - "staging"
    hitl_auto_respond: true          # answer hitl.requested at all (default: true)
    mock_responses:                  # synthetic LLM responses (no real API calls)
      - content: "Hello! I'm a test assistant."
      - content: "Here are the files: main.go, go.mod"
    timeout: 60s                     # max session duration (default: 60s)

Approval Modes

ModeBehavior
approveAuto-approve all approval requests
denyAuto-deny all approval requests
per-promptMatch against approval_rules, approve if no rule matches

Mock Responses

When mock_responses is configured, the plugin intercepts before:llm.request (at priority 20, after gates at priority 10) and injects synthetic llm.response events instead of letting requests reach the real LLM provider. No API key needed, millisecond execution.

Gates still fire first — a stop words gate can veto a request before the mock ever sees it. Responses are consumed in order; the last one repeats for any remaining requests.

Mock responses can include tool calls for testing tool execution flows:

mock_responses:
  - content: ""
    tool_calls:
      - name: shell
        arguments: '{"command": "ls"}'
  - content: "Done listing files."

Human-in-the-loop Answers

Every hitl.requestedask_user, an approval-policy gate, a remote A2A agent parking at INPUT_REQUIRED — is answered automatically. The answer is the next hitl_responses entry (a bare string is free_text; a {choice_id: ..., free_text: ...} map sets either field), else the request’s own default_choice_id, else an empty answer. The last entry repeats.

Set hitl_auto_respond: false to answer nothing. The question is still collected for assertions, but the plugin that asked it stays blocked, so a test can hand ownership of the answer to something else: another transport, a subscription in the test body, or — as in tests/integration/a2a_loopback_test.go — a second engine’s human. Without it a question can never be observed outstanding, because this plugin settles it inside the same dispatch.

Session Ending

io.session.end is emitted when the last scripted input’s turn completes, when timeout elapses, or when a turn is still in flight three seconds after the last input was sent — the stalled-turn detector, which exists so a permanently vetoed turn does not hang until the global timeout. That three-second cutoff is not configurable, so a test whose turn legitimately waits longer (a delegation parked on a human, say) must arrange for the wait to end inside it.

Input Feeding

Inputs are sent sequentially. The plugin waits for the agent to become idle (turn depth returns to zero) before sending the next input. After all inputs are sent and the final turn completes, the plugin emits io.session.end.

Ask Responses

When the agent emits io.ask events, the plugin responds with canned answers from ask_responses in order. The last response in the list repeats for any remaining asks. If ask_responses is empty, an empty string is returned.

Event Collection

The plugin subscribes to all bus events via SubscribeAll and stores them in an ordered slice. After the session ends, collected events are accessible via the Collected() method for test assertions.

Integration with Test Harness

The test IO plugin is designed to work with pkg/testharness:

h := testharness.New(t, "configs/test-minimal.yaml")
h.Run()                              // boots engine, feeds inputs, waits for completion
h.AssertEventEmitted("io.output")    // check collected events
h.AssertNoSystemOutput()             // no gate vetoes

See the Integration Testing guide for full usage.

Subscriptions

EventPurpose
io.approval.requestAuto-respond per approval config
plan.approval.requestAuto-respond per approval config
io.askRespond with canned answers
hitl.requestedRespond per hitl_responses / hitl_auto_respond
agent.turn.startTrack turn depth for input pacing
agent.turn.endDetect idle state, trigger next input or session end
* (wildcard)Collect all events for assertions

Emissions

EventWhen
io.session.startOn Ready()
io.inputFor each scripted input
io.approval.responseIn response to approval requests
plan.approval.responseIn response to plan approval requests
io.ask.responseIn response to ask events
hitl.respondedIn response to hitl.requested, unless hitl_auto_respond: false
io.session.endAfter all inputs processed or timeout