Introduction

The OpenTAP Runner allows for remote starting and control of OpenTAP sessions via NATS. These sessions can create, load, and execute test plans, modify test plan and test step properties, adjust OpenTAP settings, and provide logs and results.

When to use the OpenTAP Runner?

You should consider the OpenTAP Runner if:

  • Your solution requires remote interactions with Test Automation
  • Your solution is implemented in another language than C#
  • You want to integrate with KS8500

Concepts

OpenTAP Runner Communication

The OpenTAP Runner communicates using NATS.io, a high-performance messaging system that provides a simple yet powerful API for building distributed systems and applications.

Alt text

The NATS Server can be an instance that you control, or you can install the NATS Server TapPackage to let OpenTAP Runner start the NATS Server, which is the recommended approach.

To install the NATS Server package, use the following command: tap package install "NATS Server"

After installation, the runner can be started with the command: tap runner start

The Runner can start Runner Sessions. A Runner Session is a process that also communicates on NATS and exposes the APIs to conduct the same functionality as KS8400A PathWave Test Automation. This includes creating, loading, modifying, and executing test plans.

There are two ways to start a Runner Session through the Runner: using an Image or just a simple copy of the Runner installation. An Image is a set of OpenTAP Packages and a set of OpenTAP Repositories from where to fetch the packages.

The simple case where we ask the Runner to start a Runner Session not based on an Image looks like this:

Alt text

The more remote-friendly and flexible solution is to use Images. If you require a Session with a different set of packages than the current Runner installation provides, create an Image in the Runner and then use that Image to start a Runner Session:

Alt text

Getting started

This guide provides a quick tutorial on setting up and running an OpenTAP project using the OpenTAP Runner and NATS Server. The tutorial will walk you through installing necessary packages, setting up a basic C# Console project, and interacting with the Runner.

Compatibility Runner Client 3.x targets the asynchronous API and is wire-compatible with Runner 2.x. Use Runner Client 2.x only when talking to Runner 1.x installations.

Prerequisites

  • An OpenTAP installation

Starting the Runner

In an OpenTAP installation, install the two OpenTAP Packages, "Runner" and "NATS Server":

./tap image install "Runner,NATS Server" --merge

Then start the Runner to start the OpenTAP Runner and the NATS Server:

./tap runner start

The NATS Server process will by default start on nats://127.0.0.1:20111 and the Runner will listen on the OpenTap.Runner.{MachineName} subject.

Docker images

OpenTAP Runner publishes two Linux images to GHCR for Keysight teams:

  • Static (version-tagged) image: ghcr.io/opentap/runner:<version>
  • Updatable image: ghcr.io/opentap/runner:updatable (and :updatable-beta / :updatable-rc for prereleases)

Static images keep the OpenTAP installation inside the image and only expose data folders as volumes. Updatable images mount the full /opt/tap installation so in-container updates persist across restarts.

Example: run a static image and persist data

docker run --rm -p 20111:20111 -p 20116:20116 \
  -v runner-storage:/opt/tap/Storage \
  -v runner-logs:/opt/tap/SessionLogs \
  -v runner-settings:/opt/tap/Settings \
  ghcr.io/opentap/runner:<version>

Example: run the updatable image

docker run --rm -p 20111:20111 -p 20116:20116 \
  -v runner-installation:/opt/tap \
  ghcr.io/opentap/runner:updatable

These images are currently only available to Keysight teams. If you are outside Keysight and are interested in access, reach out via the contact information on the contact page.

Creating a C# Console Project

  1. Create a new C# Console project:
mkdir MyApp
cd MyApp
dotnet new console
  1. Add the Runner Client package:
dotnet add package OpenTAP.Runner.Client --version 3.*
  1. Modify Program.cs to interact with the Runner. The Runner Client 3.x surface is asynchronous, so we use top-level await (available in recent .NET templates):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runnerClient = new RunnerClient(connection);
SessionClient sessionClient = await runnerClient.StartSessionAsync();

sessionClient.ConnectSessionLogs(handleSessionLogs);

static Task handleSessionLogs(LogList? logList)
{
    if (logList?.Logs == null)
        return Task.CompletedTask;

    foreach (var log in logList.Logs)
        Console.WriteLine($"{log.Source,-12}: {log.Message}");

    return Task.CompletedTask;
}

// Get the available step types (Similar to "Add new step" dialog)
List<TestStepType> stepTypes = await sessionClient.GetStepTypesAsync();

// Get the current test plan from the session
TestPlan testPlan = await sessionClient.GetTestPlanAsync(null);

// Add a delay step to the test plan
testPlan.ChildTestSteps.Add(stepTypes.First(step => step.TypeName.Contains("Delay")));

// Update the test plan on the session
testPlan = await sessionClient.SetTestPlanAsync(testPlan);

// Give the test plan a name
await sessionClient.SetTestPlanNameAsync("My Testplan");

// Start the test plan
RunStatus runStatus = await sessionClient.RunTestPlanAsync(new List<Parameter>());

// Wait for the test plan to finish
while (runStatus.SessionState != SessionState.Idle)
{
    await Task.Delay(250);
    runStatus = await sessionClient.GetStatusAsync();
}

// Shutdown the session
await sessionClient.ShutdownAsync();
await runnerClient.ShutdownSessionAsync(sessionClient.Id);

This code connects, starts a Session, connects to session logs, creates a test plan and executes it. After execution finishes, the session is closed.

  1. Run the project:
dotnet run

The expected output of dotnet run:

TestPlan    : -----------------------------------------------------------------
TestPlan    : Starting TestPlan 'My Testplan' on 04/05/2023 14:36:25, 1 of 1 TestSteps enabled.
TestPlan    : Saved Test Plan XML [1.66 ms]
Settings    : No settings file exists for TestPlanRunPackageParameterMonitor. A new instance with default values has been created. [200 us]
TestPlan    : PrePlanRun Methods completed [2.46 ms]
TestPlan    : "Delay" started.
TestPlan    : "Delay" completed. [107 ms]
TestPlan    : Test step runs finished. [119 ms]
Summary     : ----- Summary of test plan started 04/05/2023 14:36:25 -----
Summary     :  Delay                                             107 ms
Summary     : ------------------------------------------------------------
Summary     : -------- Test plan completed successfully in 199 ms --------

With these steps, you have successfully set up and run an application that remotely executed a testplan using the OpenTAP Runner and NATS Server.

We can now stop the OpenTAP Runner (and NATS Server) by sending CTRL+C in the command prompt with the Runner.

Code Examples

Compatibility Runner Client 3.x exposes asynchronous APIs and is the recommended client for Runner 2.x servers.

  1. Working with Images and starting Sessions based on Images
  2. Simple session creation based on Runner installation
  3. Create and execute a test plan
  4. Editing step and test plan settings
  5. Load and execute a test plan
  6. Receiving results, logs and events from a Session
  7. How to work with component settings
  8. Configuring a Runner for default execution and using stored test plans on the Runner
  9. Copy, paste, undo, and redo test steps
  10. Working with UserInputs
  11. Debugging: Breakpoints, pause, and jump-to-step
  12. Session watchdog configuration
  13. Test plan validation errors
  14. Runner status and health monitoring
  15. Moving and deleting test steps
  16. Querying and searching session logs

Image creation

For this example we have a Runner started which has the ID: HKZ6QN3

Alt text

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);

Image image = new Image
{
    Packages = new List<PackageSpecifier>
    {
        new PackageSpecifier { Name = "Demonstration" },
        new PackageSpecifier { Name = "HTTP", Version = "1.0.0" },

        // We can even specify a different runner version for the session
        new PackageSpecifier { Name = "Runner", Version = "2" }
    },
    Repositories = new List<string> { "https://packages.opentap.io" }
};

Image? resolvedImage = await runner.ResolveImageAsync(new List<Image> { image });
if (resolvedImage is null)
    throw new InvalidOperationException("Image resolution failed.");

SessionClient newSession = await runner.StartImageSessionAsync(resolvedImage);

// Use the session for test automation

await runner.ShutdownSessionAsync(newSession.Id);

In this example, we successfully resolved an image, started and stopped a session based on the image. This can be verified in the Runner logs:

Alt text

Session creation

Although the Image approach offers flexibility, some use-cases can be kept simple by just using a Runner Session based on the Runner installation.

using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);

SessionClient newSession = await runner.StartSessionAsync();

// Use the session for test automation. Plugins available in this session
// come from the packages installed in the Runner.

await runner.ShutdownSessionAsync(newSession.Id);

Create and Run Test Plan

In this example, we use the simple session from the Session creation example above, but before shutting down the session, we setup and run a simple test plan.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();

List<TestStepType> types = await newSession.GetStepTypesAsync();
TestPlan plan = await newSession.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.First(step => step.TypeName.Contains("Delay")));
plan = await newSession.SetTestPlanAsync(plan);

Settings delayStepSettings = await newSession.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (delayStepSettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
    textBox.StringValue = "3 s";
delayStepSettings = await newSession.SetSettingsAsync(plan.ChildTestSteps[0].Id, delayStepSettings);

RunStatus status = await newSession.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
    await Task.Delay(1000);
    status = await newSession.GetStatusAsync();
}

Console.WriteLine(status.Verdict);

await runner.ShutdownSessionAsync(newSession.Id);

In this example, we retrieved the available test step types using GetStepTypes and chose a Delay step and inserted into the Test Plan using SetTestPlan.

Afterwards, we retrieved the Delay step settings and modified the Time Delay to be 3 s.

Lastly, we ran the Test Plan and continually asked for the status until the SessionState turned back to Idle. The Verdict of the Test Plan was logged and the output of this program would be NotSet.

Editing Step and TestPlan Settings

In this example, we add two steps to a test plan and demonstrate how to read and modify settings on both steps and the plan itself.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();

try
{
    // Add a DelayStep and a LogStep to the test plan
    List<TestStepType> types = await session.GetStepTypesAsync();
    TestStepType delayType = types.First(step => step.TypeName.Contains("Delay"));
    TestStepType logType = types.First(step => step.TypeName.Contains("LogStep"));

    TestPlan plan = await session.GetTestPlanAsync(null);
    plan.ChildTestSteps.Clear();
    plan.ChildTestSteps.Add(delayType);
    plan.ChildTestSteps.Add(logType);
    plan = await session.SetTestPlanAsync(plan);

    Guid delayStepId = plan.ChildTestSteps[0].Id;
    Guid logStepId = plan.ChildTestSteps[1].Id;

    // Read and modify a TextBoxControl on the Delay step
    Settings delaySettings = await session.GetSettingsAsync(delayStepId);
    if (delaySettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
    {
        Console.WriteLine($"Current delay: {textBox.StringValue}");
        textBox.StringValue = "5 s";
    }
    delaySettings = await session.SetSettingsAsync(delayStepId, delaySettings);

    // Read a CheckBoxControl on the Delay step
    if (delaySettings.FirstOrDefault(s => s.Display.Name == "Enabled") is CheckBoxControl checkBox)
    {
        Console.WriteLine($"Enabled: {checkBox.BoolValue}");
        checkBox.BoolValue = !checkBox.BoolValue; // toggle the value
    }
    delaySettings = await session.SetSettingsAsync(delayStepId, delaySettings);

    // Get plan-level settings using the test plan ID
    Settings planSettings = await session.GetSettingsAsync(plan.Id);
    foreach (Setting setting in planSettings)
        Console.WriteLine($"Plan setting: {setting.Display.Name} ({setting.GetType().Name})");

    // Set the test plan name
    await session.SetTestPlanNameAsync("My Custom Plan");
}
finally
{
    await session.ShutdownAsync();
    await runner.ShutdownSessionAsync(session.Id);
}

GetSettingsAsync and SetSettingsAsync accept a Guid context identifier that can refer to either a test step or the test plan itself. Pass plan.ChildTestSteps[0].Id to configure a step, or plan.Id to inspect and modify plan-level properties.

The settings collection contains polymorphic controls such as TextBoxControl, CheckBoxControl, and DropdownControl. Use C# pattern matching (is TextBoxControl textBox) to handle each control type and access its specific properties — StringValue for text boxes, BoolValue for check boxes, and SelectedIndex / AvailableValues for dropdowns.

Settings are round-tripped: retrieve the current values with GetSettingsAsync, modify the returned control instances in place, then pass the entire collection back to SetSettingsAsync to apply the changes. The returned Settings object reflects the server-side state after the update.

Serialize and Deserialize Settings JSON

If you need to serialize or deserialize raw settings payloads outside request/response methods, use RunnerSerialization.SerializerOptions so polymorphic controls deserialize reliably.

using System.Text.Json;
using OpenTap.Runner.Client;

string rawSettingsJson = """
[
  {
    "StringValue": "5 s",
    "PropertyName": "DelaySecs",
    "ControlType": "TextBoxControl"
  }
]
""";

Settings? parsed = JsonSerializer.Deserialize<Settings>(rawSettingsJson, RunnerSerialization.SerializerOptions);
if (parsed is { Count: > 0 } && parsed[0] is TextBoxControl delay)
    delay.StringValue = "10 s";

JsonSerializerOptions writeOptions = RunnerSerialization.SerializerOptions;
writeOptions.WriteIndented = true;
string updatedJson = parsed == null ? "[]" : JsonSerializer.Serialize(parsed, writeOptions);

Load and Run Test Plan

In this example, we use the simple session from the Session creation example above, but before shutting down the session, we load and run a Test Plan locally stored on the disk.

using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();

await newSession.SetTestPlanXMLAsync(await File.ReadAllTextAsync("MyTestPlan.TapPlan"));

RunStatus status = await newSession.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
    await Task.Delay(1000);
    status = await newSession.GetStatusAsync();
}

Console.WriteLine(status.Verdict);

await runner.ShutdownSessionAsync(newSession.Id);

Results, Logs and Events

In this example, we use the simple session from the Create and Run Test Plan section above, but with results, logs and events logged.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();

newSession.ConnectSessionLogs(LogHandler);
Task LogHandler(LogList? list)
{
    if (list?.Logs != null)
        foreach (var log in list.Logs)
            Console.WriteLine($"{log.Source,-12}: {log.Message}");
    return Task.CompletedTask;
}

newSession.ConnectSessionResults(ResultHandler, RunHandler);
Task ResultHandler(Result? result)
{
    if (result != null)
        Console.WriteLine($"Result: {result.Status}");
    return Task.CompletedTask;
}
Task RunHandler(TestRun? run)
{
    if (run != null)
        Console.WriteLine($"Run: {run.Status}");
    return Task.CompletedTask;
}

newSession.ConnectSessionEvents(EventHandler);
Task EventHandler(SessionEvent? evt)
{
    if (evt != null)
        Console.WriteLine($"Event: {evt.GetType().Name}");
    return Task.CompletedTask;
}

List<TestStepType> types = await newSession.GetStepTypesAsync();
TestPlan plan = await newSession.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.First(step => step.TypeName.Contains("Delay")));
plan = await newSession.SetTestPlanAsync(plan);

Settings delayStepSettings = await newSession.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (delayStepSettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
    textBox.StringValue = "3 s";
delayStepSettings = await newSession.SetSettingsAsync(plan.ChildTestSteps[0].Id, delayStepSettings);

RunStatus status = await newSession.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
    await Task.Delay(1000);
    status = await newSession.GetStatusAsync();
}

Console.WriteLine(status.Verdict);

await runner.ShutdownSessionAsync(newSession.Id);

This code should produce the following output, where events, runs and results are prepended with "Event: ", "Run: ", and "Result: ".

Alt text

Working with ComponentSettings

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();

try
{
    List<ComponentSettingsIdentifier> componentSettings = await newSession.GetComponentSettingsOverviewAsync();
    foreach (var componentSetting in componentSettings)
        Console.WriteLine($"Component Setting: {componentSetting.Name} ({componentSetting.GroupName})");

    // Outputs:
    // Component Setting: DUTs(Bench)
    // Component Setting: Engine(-)
    // Component Setting: Instruments(Bench)
    // Component Setting: Connections(Bench)
    // Component Setting: Results(-)
    // Component Setting: Editor(-)
    // Component Setting: Cloud Drive(-)

    ComponentSettingsIdentifier instrumentIdentifier = componentSettings
        .First(s => s.Name == "Instruments" && s.GroupName == "Bench");

    List<ListItemType> instrumentTypes = await newSession.GetComponentSettingsListAvailableTypesAsync(
        instrumentIdentifier.GroupName,
        instrumentIdentifier.Name);
    foreach (var instrumentType in instrumentTypes)
        Console.WriteLine($"Instrument Type: {string.Join("/", instrumentType.TypeDisplay.Group)}/{instrumentType.TypeDisplay.Name}");

    // Outputs:
    // Instrument Type: / Generic SCPI Instrument
    // Instrument Type: Demo / Battery Test / Power Analyzer
    // Instrument Type: Demo / Battery Test / Temperature Chamber
    // Instrument Type: Demo / Results And Timing/ Timing Instrument

    ComponentSettingsBase instruments = await newSession.GetComponentSettingsAsync(
        instrumentIdentifier.GroupName,
        instrumentIdentifier.Name);
    if (instruments is ComponentSettingsList list)
        foreach (var instrument in list.Items)
            Console.WriteLine($"Instrument: {instrument.Name}");

    // Outputs:
    // Instrument: SCPI
    // Instrument: PSU
    // Instrument: TEMP

    ComponentSettingsBase updated = await newSession.AddComponentSettingsListItemAsync(
        instrumentIdentifier.GroupName,
        instrumentIdentifier.Name,
        instrumentTypes.First(type => type.TypeDisplay.Name == "Power Analyzer").TypeName);

    if (updated is ComponentSettingsList listWithNewItem)
    {
        foreach (var instrument in listWithNewItem.Items)
            Console.WriteLine($"Instrument: {instrument.Name}");

        // Outputs:
        // Instrument: SCPI
        // Instrument: PSU
        // Instrument: TEMP
        // Instrument: PSU (1)

        foreach (var setting in listWithNewItem.Items[^1].Settings)
        {
            Console.WriteLine($"Setting: {setting.Display.Name}");
            if (setting is TextBoxControl textBox)
                Console.WriteLine(textBox.StringValue);
        }
    }

    // Outputs:
    // Setting: Cell Size Factor
    // 0.005
}
finally
{
    await runner.ShutdownSessionAsync(newSession.Id);
}

Default Endpoints

Default endpoints are endpoints that affect each other. They are:

  • SetDefaultImage: Sets a default image in the Runner environment. The image includes essential packages.
  • GetDefaultImage: Retrieves the currently set default image.
  • SetDefaultSettings: Applies default settings for the Runner. Settings might have various package dependencies.
  • GetDefaultSettings: Retrieves the current settings from the Runner.
  • StartDefaultSession: Starts a session using the default settings and image.
  • StartDefaultSessionOverrideImage: Begins a session with an overridden image for specific use cases.

A Session's plugin configuration depends on:

  • The dependencies of the TestPlan.
  • The dependencies of the Settings.
  • The packages defined in a "default image".
  • Optionally, the packages defined in an "image override".

The following example demonstrates how to use the RunnerClient for managing test plans in a Runner session.

public static async Task DefaultTestPlanShowcaseAsync(RunnerClient runnerClient)
{
    Image baseImage = new()
    {
        Packages = new List<PackageSpecifier>
        {
            new PackageSpecifier { Name = "Runner", Version = "1.7.0" }
        },
        Repositories = new List<string>
        {
            "https://packages.opentap.io"
        }
    };
    await runnerClient.Default.SetImageAsync(baseImage); // Apply the base image configuration

    RepositoryPackageReference settingsPackageReference = new()
    {
        Repository = "https://test-automation.pw.keysight.com/api/packages",
        Path = "/users/dennis.rasmussen@keysight.com/BatterySettings.TapPackage",
        Version = "0.2.0"
    };
    await runnerClient.Default.SetSettingsAsync(settingsPackageReference); // Apply settings package

    RepositoryPackageReference testPlanReference = new()
    {
        Repository = "https://test-automation.pw.keysight.com/api/packages",
        Path = "/users/dennis.rasmussen@keysight.com/BatteryPlan.TapPlan",
        Version = "0.1.0-Draft.5"
    };

    SessionClient session = await runnerClient.Default.StartSessionAsync(testPlanReference);

    try
    {
        RunStatus status = await session.RunTestPlanAsync(new List<Parameter>());
        while (status.SessionState != SessionState.Idle)
        {
            await Task.Delay(1000);
            status = await session.GetStatusAsync();
        }

        Console.WriteLine($"Verdict: {status.Verdict}");
    }
    finally
    {
        await session.ShutdownAsync();
        await runnerClient.ShutdownSessionAsync(session.Id);
    }
}

Copy, paste, undo, and redo test steps

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);

SessionClient session = await runner.StartSessionAsync();

try
{
    List<TestStepType> stepTypes = await session.GetStepTypesAsync();
    TestStepType delay = stepTypes.First(step => step.TypeName == "OpenTap.Plugins.BasicSteps.DelayStep");

    TestPlan plan = await session.GetTestPlanAsync(null);
    plan.ChildTestSteps.Clear();
    plan = await session.SetTestPlanAsync(plan);

    plan.ChildTestSteps.Add(delay);
    plan = await session.SetTestPlanAsync(plan);
    Guid firstStepId = plan.ChildTestSteps[0].Id;

    plan.ChildTestSteps.Add(delay);
    plan = await session.SetTestPlanAsync(plan);
    Guid targetStepId = plan.ChildTestSteps[1].Id;

    string clipboard = await session.CopyStepsAsync(new HashSet<Guid> { firstStepId });
    await session.PasteStepsAsync(targetStepId, clipboard);

    await session.TestPlanUndoAsync(); // removes the pasted copy
    await session.TestPlanRedoAsync(); // reapplies the paste
}
finally
{
    await session.ShutdownAsync();
    await runner.ShutdownSessionAsync(session.Id);
}

Working with UserInputs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();

try
{
    // Used to wait for the test plan to finish executing
    var readyEvent = new ManualResetEventSlim(false);
    bool planStarted = false;

    // Subscribe to session events to handle user input requests
    session.ConnectSessionEvents(async eve =>
    {
        if (eve == null)
            return;

        Console.WriteLine($"Event: {eve.GetType().Name}");

        // When a test step (e.g. DialogStep) requires operator input,
        // the session raises a UserInputRequestedEvent
        if (eve is UserInputRequestedEvent userInputRequest)
        {
            // Retrieve the full Interaction model for this request
            Interaction interaction = await session.GetUserInputAsync(userInputRequest.RequestId);
            Console.WriteLine($"UserInput requested: {interaction.Title}");

            // Find the visible ButtonsControl in the interaction settings
            if (interaction.Settings.OfType<ButtonsControl>().FirstOrDefault(s => s.VisualStatus.IsVisible) is
                ButtonsControl buttons)
            {
                // Select the first button and invoke it
                buttons.SelectedIndex = 0;
                buttons.InvokeMethod = true;

                // Submit the response — the session resumes execution
                await session.SetUserInputAsync(interaction);
            }
        }

        // Signal when the session returns to Idle after the plan has started
        if (eve is SessionStateChangedEvent executionChange)
        {
            if (executionChange.RunStatus.SessionState == SessionState.Executing)
                planStarted = true;
            if (planStarted && executionChange.RunStatus.SessionState == SessionState.Idle)
                readyEvent.Set();
        }
    });

    // Build a test plan with a DialogStep that will trigger a user input
    List<TestStepType> stepTypes = await session.GetStepTypesAsync();
    TestPlan plan = await session.GetTestPlanAsync(null);
    plan.ChildTestSteps.Add(stepTypes.First(step => step.TypeName.Contains("Dialog")));
    plan = await session.SetTestPlanAsync(plan);

    // Run the test plan — execution blocks until the DialogStep's user input is answered
    await session.RunTestPlanAsync(new List<Parameter>());

    // Wait for the session to return to Idle
    if (!readyEvent.Wait(TimeSpan.FromSeconds(30)))
        Console.WriteLine("Test plan did not finish within 30 seconds.");

    RunStatus status = await session.GetStatusAsync();
    Console.WriteLine($"Verdict: {status.Verdict}");
}
finally
{
    await session.ShutdownAsync();
    await runner.ShutdownSessionAsync(session.Id);
}

User inputs are raised by test steps (such as DialogStep) that require operator interaction during execution. When a step requests input, the session publishes a UserInputRequestedEvent containing a RequestId. Use GetUserInputAsync with this ID to retrieve the full Interaction model.

The Interaction contains a Title, Modal flag, Timeout, and Settings. The settings collection uses the same control types as step settings — TextBoxControl, ButtonsControl, and others — so the same patterns for reading and modifying controls apply here. While the session is waiting for a response, its state is WaitingForUserInput.

To submit a response, set InvokeMethod = true on a ButtonsControl (optionally choosing a button via SelectedIndex) and call SetUserInputAsync. The session resumes execution after the input is answered.

Debugging: Breakpoints, pause, and jump-to-step

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();

try
{
    // Track break events so we know which step the session stopped on
    var breakHit = new ManualResetEventSlim(false);
    Guid breakStepId = Guid.Empty;
    bool planStarted = false;
    var planFinished = new ManualResetEventSlim(false);

    session.ConnectSessionEvents(evt =>
    {
        if (evt is BreakEvent breakEvent)
        {
            breakStepId = breakEvent.StepId;
            breakHit.Set();
        }
        if (evt is SessionStateChangedEvent stateChange)
        {
            if (stateChange.RunStatus.SessionState == SessionState.Executing)
                planStarted = true;
            if (planStarted && stateChange.RunStatus.SessionState == SessionState.Idle)
                planFinished.Set();
        }
        return Task.CompletedTask;
    });

    // Build a plan with three Delay steps
    List<TestStepType> types = await session.GetStepTypesAsync();
    TestStepType delayType = types.First(step => step.TypeName.Contains("Delay"));

    TestPlan plan = await session.GetTestPlanAsync(null);
    plan.ChildTestSteps.Clear();
    plan.ChildTestSteps.Add(delayType);
    plan.ChildTestSteps.Add(delayType);
    plan.ChildTestSteps.Add(delayType);
    plan = await session.SetTestPlanAsync(plan);

    Guid step0 = plan.ChildTestSteps[0].Id;
    Guid step1 = plan.ChildTestSteps[1].Id;
    Guid step2 = plan.ChildTestSteps[2].Id;

    // Set each delay to 0.5 seconds
    foreach (Guid stepId in new[] { step0, step1, step2 })
    {
        Settings s = await session.GetSettingsAsync(stepId);
        if (s.FirstOrDefault(x => x.PropertyName == "DelaySecs") is TextBoxControl tb)
            tb.StringValue = "0.5 s";
        await session.SetSettingsAsync(stepId, s);
    }

    // --- Set breakpoints on the first two steps ---
    await session.SetBreakpointsAsync(new BreakPoints { TestSteps = new List<Guid> { step0, step1 } });

    BreakPoints currentBp = await session.GetBreakpointsAsync();
    Console.WriteLine($"Breakpoints set on {currentBp.TestSteps.Count} steps");

    // Run the plan — it will break on step0
    await session.RunTestPlanAsync(new List<Parameter>());
    breakHit.Wait(TimeSpan.FromSeconds(10));
    Console.WriteLine($"Broke on step: {(breakStepId == step0 ? "step0" : breakStepId.ToString())}");

    // --- Resume execution — it will break on step1 ---
    breakHit.Reset();
    await session.RunTestPlanAsync(new List<Parameter>());
    breakHit.Wait(TimeSpan.FromSeconds(10));
    Console.WriteLine($"Broke on step: {(breakStepId == step1 ? "step1" : breakStepId.ToString())}");

    // --- Use SetPauseNext to single-step to the next step ---
    // SetPauseNext resumes execution and breaks on the very next step
    breakHit.Reset();
    await session.SetPauseNextAsync();
    breakHit.Wait(TimeSpan.FromSeconds(10));
    Console.WriteLine($"PauseNext broke on step: {(breakStepId == step2 ? "step2" : breakStepId.ToString())}");

    // --- Jump back to step0 while in Breaking state ---
    // Clear breakpoints first so step0 doesn't trigger another break
    await session.SetBreakpointsAsync(new BreakPoints { TestSteps = new List<Guid>() });
    await session.SetJumpToStepAsync(step0);

    // --- Abort the run ---
    await session.AbortTestPlanAsync();

    planFinished.Wait(TimeSpan.FromSeconds(10));
    RunStatus status = await session.GetStatusAsync();
    Console.WriteLine($"Final verdict: {status.Verdict}");
}
finally
{
    await session.ShutdownAsync();
    await runner.ShutdownSessionAsync(session.Id);
}

Breakpoints pause execution just before a step runs. Set them with SetBreakpointsAsync by passing a BreakPoints object containing the target step IDs. When a breakpoint triggers, the session enters the Breaking state and publishes a BreakEvent with the StepId that caused the break.

While in Breaking state you can:

  • Resume — call RunTestPlanAsync to continue execution until the next breakpoint or completion.
  • Step — call SetPauseNextAsync to resume and break on the very next step regardless of breakpoints.
  • Jump — call SetJumpToStepAsync to redirect execution to a different step. This only works while in the Breaking state.
  • Abort — call AbortTestPlanAsync to cancel the run entirely.

Clear all breakpoints by passing an empty list to SetBreakpointsAsync.

Session watchdog configuration

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();

try
{
    // Read the current watchdog configuration
    WatchDog watchdog = await session.GetWatchDogAsync();
    Console.WriteLine($"Current timeout: {watchdog.TerminationTimeout}s, inactive: {watchdog.InactiveSeconds}s");

    // Set the watchdog to terminate the session after 120 seconds of inactivity
    watchdog.TerminationTimeout = 120;
    watchdog = await session.SetWatchDogAsync(watchdog);
    Console.WriteLine($"Updated timeout: {watchdog.TerminationTimeout}s");

    // InactiveSeconds only counts while the session is Idle and
    // resets whenever a NATS request is received. After a brief wait
    // the counter reflects the time since the last API call.
    await Task.Delay(3000);
    watchdog = await session.GetWatchDogAsync();
    Console.WriteLine($"After 3s idle: {watchdog.InactiveSeconds:F1}s inactive");

    // Disable the watchdog by setting TerminationTimeout to 0
    watchdog.TerminationTimeout = 0;
    watchdog = await session.SetWatchDogAsync(watchdog);
    Console.WriteLine($"Watchdog disabled (timeout={watchdog.TerminationTimeout})");
}
finally
{
    await session.ShutdownAsync();
    await runner.ShutdownSessionAsync(session.Id);
}

The session watchdog monitors idle time and terminates unattended sessions. InactiveSeconds tracks how long the session has been idle — it only increments when the session state is Idle and resets whenever a NATS request is received or a test plan runs.

Set TerminationTimeout to the maximum number of idle seconds before the session is automatically shut down. Setting it to 0 disables the watchdog entirely.

Test plan validation errors

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();

try
{
    // Start with a valid plan — no validation errors expected
    List<TestStepType> types = await session.GetStepTypesAsync();
    TestPlan plan = await session.GetTestPlanAsync(null);
    plan.ChildTestSteps.Clear();
    plan.ChildTestSteps.Add(types.First(step => step.TypeName.Contains("Delay")));
    plan = await session.SetTestPlanAsync(plan);

    TestPlanValidationErrors errors = await session.GetValidationErrorsAsync();
    Console.WriteLine($"Validation errors (valid plan): {errors.Count}");

    // Disable the step — disabled steps are excluded from validation
    Guid delayStepId = plan.ChildTestSteps[0].Id;
    Settings settings = await session.GetSettingsAsync(delayStepId);
    if (settings.FirstOrDefault(s => s.Display.Name == "Enabled") is CheckBoxControl enabledBox)
        enabledBox.BoolValue = false;
    await session.SetSettingsAsync(delayStepId, settings);

    errors = await session.GetValidationErrorsAsync();
    Console.WriteLine($"Validation errors (disabled step): {errors.Count}");

    // Print any validation errors found
    foreach (TestStepValidationError stepError in errors)
    {
        Console.WriteLine($"  Step {stepError.StepId}:");
        foreach (ValidationError ve in stepError.ValidationErrors)
            Console.WriteLine($"    {ve.PropertyName}: {ve.Error}");
    }
}
finally
{
    await session.ShutdownAsync();
    await runner.ShutdownSessionAsync(session.Id);
}

GetValidationErrorsAsync returns a TestPlanValidationErrors collection containing one TestStepValidationError per step that has issues. Each entry has a StepId (string GUID) and a list of ValidationError objects with PropertyName and Error message.

Only enabled steps are validated — disabling a step removes it from the validation results. This is useful for temporarily bypassing steps with missing resource dependencies during plan development.

Runner status and health monitoring

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);

// --- Runner-level status ---
RunnerStatus status = await runner.GetStatusAsync();
Console.WriteLine($"Runner version:   {status.Version}");
Console.WriteLine($"Hostname:         {status.Hostname}");
Console.WriteLine($"IP address:       {status.IpAddress}");
Console.WriteLine($"Running as service: {status.RunningAsService}");
Console.WriteLine($"Running in container: {status.RunningInContainer}");

// NATS connection info
if (status.RemoteConnection is RemoteConnection rc)
{
    Console.WriteLine($"NATS endpoint:    {rc.Endpoint}");
    Console.WriteLine($"NATS connected:   {rc.Connected}");
    Console.WriteLine($"NATS RTT:         {rc.Rtt}");
}

// Storage and stream health
if (status.StreamInfo is StreamInfoStatus si)
{
    Console.WriteLine($"Runs max bytes:   {si.RunsMaxBytes}");
    Console.WriteLine($"Runs usage:       {si.RunsUsage}");
    Console.WriteLine($"Disk free:        {si.DiskFree}");
}

// --- Active sessions ---
List<Session> sessions = await runner.GetSessionsAsync();
Console.WriteLine($"\nActive sessions: {sessions.Count}");
foreach (Session s in sessions)
{
    Console.WriteLine($"  {s.Id} — State: {s.SessionState}, StartedBy: {s.StartedBy}");
    foreach (var kv in s.Metadata)
        Console.WriteLine($"    {kv.Key}: {kv.Value}");
}

GetStatusAsync on the RunnerClient returns a RunnerStatus with version, hostname, IP address, and flags indicating whether the Runner is running as a service or in a container. It also includes RemoteConnection (NATS connectivity) and StreamInfoStatus (JetStream storage usage and free disk space) for health monitoring.

GetSessionsAsync lists all active sessions with their state, the user who started them, and any attached metadata. This is useful for building dashboards or monitoring tools that need to track Runner utilization.

Moving and deleting test steps

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();

try
{
    List<TestStepType> types = await session.GetStepTypesAsync();
    TestStepType delayType = types.First(step => step.TypeName.Contains("Delay"));
    TestStepType repeatType = types.First(step => step.TypeName.Contains("RepeatStep"));

    // Build a plan: [Delay A, Delay B, Repeat C]
    TestPlan plan = await session.GetTestPlanAsync(null);
    plan.ChildTestSteps.Clear();
    plan.ChildTestSteps.Add(delayType);
    plan.ChildTestSteps.Add(delayType);
    plan.ChildTestSteps.Add(repeatType);
    plan = await session.SetTestPlanAsync(plan);

    Guid stepA = plan.ChildTestSteps[0].Id;
    Guid stepB = plan.ChildTestSteps[1].Id;
    Guid stepC = plan.ChildTestSteps[2].Id;

    Console.WriteLine("Initial order:");
    plan = await session.GetTestPlanAsync(null);
    foreach (var step in plan.ChildTestSteps)
        Console.WriteLine($"  {step.TypeName}  {step.Id}");

    // --- Reorder: move A and B after C → [C, A, B] ---
    await session.MoveStepsAsync(new HashSet<Guid> { stepA, stepB }, stepC);
    plan = await session.GetTestPlanAsync(null);
    Console.WriteLine("\nAfter MoveSteps (A,B after C):");
    foreach (var step in plan.ChildTestSteps)
        Console.WriteLine($"  {step.TypeName}  {step.Id}");

    // --- Nest: move A and B as children of C ---
    await session.MoveStepsAsChildrenAsync(new HashSet<Guid> { stepA, stepB }, stepC);
    plan = await session.GetTestPlanAsync(null);
    Console.WriteLine("\nAfter MoveStepsAsChildren (A,B into C):");
    Console.WriteLine($"  Root steps: {plan.ChildTestSteps.Count}");
    Console.WriteLine($"  Children of C: {plan.ChildTestSteps[0].ChildTestSteps.Count}");

    // --- Delete step B ---
    await session.DeleteStepsAsync(new HashSet<Guid> { stepB });
    plan = await session.GetTestPlanAsync(null);
    Console.WriteLine("\nAfter DeleteSteps (B):");
    Console.WriteLine($"  Root steps: {plan.ChildTestSteps.Count}");
    Console.WriteLine($"  Children of C: {plan.ChildTestSteps[0].ChildTestSteps.Count}");
}
finally
{
    await session.ShutdownAsync();
    await runner.ShutdownSessionAsync(session.Id);
}

Three operations are available for reorganising steps in a test plan:

  • MoveStepsAsync(stepIds, targetStepId) — Moves the specified steps so they appear immediately after the target step at the same nesting level. This is a reorder operation.
  • MoveStepsAsChildrenAsync(stepIds, targetStepId) — Moves the specified steps so they become children of the target step. The target must support child steps (e.g. RepeatStep, SequenceStep).
  • DeleteStepsAsync(stepIds) — Removes the specified steps from the plan entirely.

All three accept a HashSet<Guid> of step IDs. Moving a step to itself throws an ArgumentException, and moving a parent into one of its own descendants throws an InvalidOperationException.

Querying and searching session logs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();

try
{
    // Run a short plan to generate some log entries
    List<TestStepType> types = await session.GetStepTypesAsync();
    TestPlan plan = await session.GetTestPlanAsync(null);
    plan.ChildTestSteps.Clear();
    plan.ChildTestSteps.Add(types.First(step => step.TypeName.Contains("Delay")));
    plan = await session.SetTestPlanAsync(plan);

    Settings s = await session.GetSettingsAsync(plan.ChildTestSteps[0].Id);
    if (s.FirstOrDefault(x => x.PropertyName == "DelaySecs") is TextBoxControl tb)
        tb.StringValue = "1 s";
    await session.SetSettingsAsync(plan.ChildTestSteps[0].Id, s);

    RunStatus status = await session.RunTestPlanAsync(new List<Parameter>());
    while (status.SessionState != SessionState.Idle)
    {
        await Task.Delay(500);
        status = await session.GetStatusAsync();
    }

    // --- Discover available log levels ---
    Dictionary<int, string> levels = await session.LogLevelsAsync();
    Console.WriteLine("Log levels:");
    foreach (var kv in levels)
        Console.WriteLine($"  {kv.Key}: {kv.Value}");

    // --- Discover log sources ---
    List<string> sources = await session.SessionLogSourcesAsync();
    Console.WriteLine($"\nLog sources: {string.Join(", ", sources)}");

    // --- Get per-level log counts ---
    Dictionary<int, int> counts = await session.SessionLogCountsAsync();
    Console.WriteLine("\nLog counts by level:");
    foreach (var kv in counts)
        Console.WriteLine($"  {(levels.TryGetValue(kv.Key, out string? name) ? name : kv.Key.ToString())}: {kv.Value}");

    // --- Fetch paginated log entries ---
    List<int> allLevels = levels.Keys.ToList();
    LogList? logs = await session.GetSessionLogsAsync(
        level: allLevels,
        excludedSource: new List<string>(),
        filterText: "",
        offset: 0,
        limit: 20
    );

    if (logs != null)
    {
        Console.WriteLine($"\nFirst page: {logs.Logs.Count} entries (offset={logs.Offset}, filtered={logs.FilteredCount})");
        foreach (LogEntry entry in logs.Logs.Take(5))
            Console.WriteLine($"  [{entry.Source}] {entry.Message}");
    }

    // --- Search logs for a keyword ---
    List<int> matchIndexes = await session.SessionLogSearchAsync(
        level: allLevels,
        excludedSource: new List<string>(),
        filterText: "",
        searchText: "Delay"
    );
    Console.WriteLine($"\nSearch hits for 'Delay': {matchIndexes.Count} matching entries");
}
finally
{
    await session.ShutdownAsync();
    await runner.ShutdownSessionAsync(session.Id);
}

Session logs can be queried historically using several complementary APIs:

  • LogLevelsAsync — Returns a Dictionary<int, string> mapping numeric level IDs to names (e.g. 10 → Debug, 20 → Information, 30 → Warning, 40 → Error).
  • SessionLogSourcesAsync — Returns the list of source names that have emitted log entries in the current session.
  • SessionLogCountsAsync — Returns per-level entry counts as Dictionary<int, int>.
  • GetSessionLogsAsync — Fetches a page of log entries filtered by level, excluded sources, and filter text. The returned LogList includes the entries, the current Offset, FilteredCount, and TotalCount per level.
  • SessionLogSearchAsync — Searches log entries matching the filters and returns a list of matching entry indexes. These indexes correspond to positions in the filtered log sequence.

The LogEntry model contains Source, Message, Level (numeric), Timestamp (Unix epoch ticks), and DurationNS.

Future code examples:

  • Load from and save to Repository

API Reference

All Runner and Session communication uses NATS request-reply messaging. This document defines the complete set of endpoints, their request/response types, and the data transfer objects (DTOs) used on the wire.

Wire Format

All messages are JSON-serialized. Understanding how different types are encoded on the wire is essential for correct usage:

  • Object types (e.g., Image, SetSettingsRequest) are sent as JSON objects: {"Name": "...", "Packages": [...]}
  • String values are sent as JSON string literals. For example, SetTestPlanXML expects a JSON-encoded string containing the XML content. The XML must be wrapped in double quotes with proper JSON escaping:
    "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TestPlan type=\"OpenTap.TestPlan\">..."
    
  • Guid values are sent as JSON string literals in standard format: "a8a9baa6-d3b0-446d-a3e2-07bbf9310a71"
  • NoInput is sent as an empty JSON object: {}
  • NoResponse is returned as an empty JSON object: {}
  • Errors from the server are returned as NATS error responses with an OpenTapNatsError header. Fatal errors (e.g., invalid input, server exceptions) are communicated this way rather than in the response body.

Runner APIs

The {base} is in the form of: OpenTap.Runner.{RunnerId}.Request

Subject Request Type Response Type Available since
.Status NoInput RunnerStatus v1.0
.Shutdown NoInput NoResponse v1.0
.GetImages NoInput List of Image v1.0
.GetImage String Image v1.0
.ResolveImage List of Image Image v1.0
.ResolveImageDryRun List of Image Image v1.0
.GetSessionManagerImage NoInput Image v1.0
.GetSessions NoInput List of Session v1.0
.StartSession NoInput Session v1.0
.StartImageSession Image Session v1.0
.StartDefaultSession RepositoryPackageReference Session v1.6
.StartDefaultSessionOverrideImage DefaultSessionRequest Session v1.6
.NewSession NewSessionRequest NewSessionResponse v2.2
.SetSession Session Session v1.0
.ShutdownSession Guid NoResponse v1.0
.RegisterSession RegisterSessionRequest RegisterSessionResponse v1.0
.SetDefaultSettings RepositoryPackageReference NoResponse v1.0
.GetDefaultSettings NoInput RepositoryPackageReference v1.0
.SaveDefaultSettingsInRepository SaveDefaultSettings NoResponse v1.0
.GetDefaultImage NoInput Image v1.5
.SetDefaultImage Image Image v1.5
.UpdateRunner RunnerUpdateRequest RunnerUpdateResponse v1.4
.LogsZip NoInput Byte[] v2.0
.GetComponentSettingsOverview NoInput List of ComponentSettingsIdentifier v1.0
.SetComponentSettings SetComponentSettingsRequest ComponentSettingsBase v1.0
.GetComponentSettings GetComponentSettingsRequest ComponentSettingsBase v1.0
.GetComponentSettingsListItem GetComponentSettingsListItemRequest ComponentSettingsListItem v1.0
.SetComponentSettingsListItem SetComponentSettingsListItemRequest ComponentSettingsListItem v1.0
.GetComponentSettingDataGrid GetComponentSettingDataGridRequest DataGridControl v1.0
.SetComponentSettingDataGrid SetComponentSettingDataGridRequest DataGridControl v1.0
.AddComponentSettingDataGridItemType AddComponentSettingDataGridItemTypeRequest DataGridControl v1.0
.AddComponentSettingDataGridItem GetComponentSettingDataGridRequest DataGridControl v1.0
.GetComponentSettingDataGridTypes GetComponentSettingDataGridRequest List of ListItemType v1.0
.SetComponentSettingsProfiles List of ProfileGroup List of ProfileGroup v1.0
.GetComponentSettingsProfiles NoInput List of ProfileGroup v1.0
.UploadComponentSettings UploadFileRequest List of String v1.0
.DownloadComponentSettings DownloadTapSettingsRequest Byte[] v1.0
.LoadComponentSettingsFromRepository RepositoryPackageReference List of ErrorResponse v1.0
.SaveComponentSettingsToRepository RepositorySettingsPackageDefinition NoResponse v1.0
.GetComponentSettingsListAvailableTypes GetComponentSettingsRequest List of ListItemType v1.0
.AddComponentSettingsListItem AddComponentSettingsListItemRequest ComponentSettingsBase v1.0
.GetSettingsPackageFiles NoInput List of String v1.0
.GetSettingsTypes NoInput List of String v1.0

Session APIs

The {base} is in the form of: OpenTap.Runner.{RunnerId}.Session.{SessionId}.Request

Subject Request Type Response Type Available since
.GetStatus NoInput RunStatus v1.0
.GetEditStatus NoInput EditStatus v2.0
.GetTestPlanXML NoInput String v1.0
.SetTestPlanXML String List of String v1.0
.SetTestPlanName String NoResponse v1.0
.GetTestPlanReference NoInput GetTestPlanReferenceResponse v1.0
.LoadTestPlanFromRepository RepositoryPackageReference List of String v1.0
.SaveTestPlanToRepository RepositoryPackageDefinition SaveTestPlanToRepositoryResponse v1.0
.ResourcesOpen NoInput TestPlan v1.0
.ResourcesClose NoInput TestPlan v1.0
.GetSettings GetSettingsRequest Settings v1.0
.SetSettings SetSettingsRequest Settings v1.0
.GetTestPlan String[] TestPlan v1.0
.SetTestPlan TestPlan TestPlan v1.0
.MoveSteps MoveRequest NoResponse v2.0
.MoveStepsAsChildren MoveRequest NoResponse v2.0
.DeleteSteps DeleteRequest NoResponse v2.0
.GetValidationErrors NoInput TestPlanValidationErrors v1.0
.CommonStepSettings CommonSettings CommonSettings v1.0
.CommonStepSettingsContextMenu CommonStepSettingsContext CommonContext v1.0
.GetUserInputs NoInput List of Guid v1.0
.GetUserInput Guid Interaction v1.0
.SetUserInput Interaction Interaction v1.0
.GetContextMenu PropertyReferenceRequest List of Setting v1.0
.SetContextMenu SetContextMenuRequest List of Setting v1.0
.GetDataGrid PropertyReferenceRequest DataGridControl v1.0
.SetDataGrid SetDataGridRequest DataGridControl v1.0
.AddDataGridItemType AddDataGridItemTypeRequest DataGridControl v1.0
.AddDataGridItem PropertyReferenceRequest DataGridControl v1.0
.GetDataGridTypes PropertyReferenceRequest List of ListItemType v1.0
.GetStepTypes NoInput List of TestStepType v1.0
.CopySteps CopyRequest String v2.0
.PasteSteps PasteRequest NoResponse v2.0
.PasteStepsAsChildren PasteRequest NoResponse v2.0
.RunTestPlan List of Parameter RunStatus v1.0
.SetPauseNext NoInput NoResponse v1.0
.GetBreakpoints NoInput BreakPoints v1.0
.SetBreakpoints BreakPoints BreakPoints v1.0
.SetJumpToStep Guid NoResponse v1.0
.AbortTestPlan NoInput NoResponse v1.0
.TestPlanUndo NoInput NoResponse v2.0
.TestPlanRedo NoInput NoResponse v2.0
.GetSessionLogs GetSessionLogsRequest LogList v1.0
.SessionLogSearch GetSessionSearchRequest List of Int32 v1.0
.SessionLogSources NoInput List of String v1.0
.SessionLogCounts NoInput Dictionary of Int32 to Int32 v1.0
.LogLevels NoInput Dictionary of Int32 to String v1.0
.SetupMetricsPolling EnableMetricsPollingRequest EnableMetricsPollingResponse v2.0
.DiscoverAssets NoInput AssetDiscoveryResponse v2.0
.GetComponentSettingsOverview NoInput List of ComponentSettingsIdentifier v1.0
.SetComponentSettings SetComponentSettingsRequest ComponentSettingsBase v1.0
.GetComponentSettings GetComponentSettingsRequest ComponentSettingsBase v1.0
.GetComponentSettingsListItem GetComponentSettingsListItemRequest ComponentSettingsListItem v1.0
.SetComponentSettingsListItem SetComponentSettingsListItemRequest ComponentSettingsListItem v1.0
.GetComponentSettingDataGrid GetComponentSettingDataGridRequest DataGridControl v1.0
.SetComponentSettingDataGrid SetComponentSettingDataGridRequest DataGridControl v1.0
.AddComponentSettingDataGridItemType AddComponentSettingDataGridItemTypeRequest DataGridControl v1.0
.AddComponentSettingDataGridItem GetComponentSettingDataGridRequest DataGridControl v1.0
.GetComponentSettingDataGridTypes GetComponentSettingDataGridRequest List of ListItemType v1.0
.SetComponentSettingsProfiles List of ProfileGroup List of ProfileGroup v1.0
.GetComponentSettingsProfiles NoInput List of ProfileGroup v1.0
.UploadComponentSettings UploadFileRequest List of String v1.0
.DownloadComponentSettings DownloadTapSettingsRequest Byte[] v1.0
.LoadComponentSettingsFromRepository RepositoryPackageReference List of ErrorResponse v1.0
.SaveComponentSettingsToRepository RepositorySettingsPackageDefinition NoResponse v1.0
.GetComponentSettingsListAvailableTypes GetComponentSettingsRequest List of ListItemType v1.0
.AddComponentSettingsListItem AddComponentSettingsListItemRequest ComponentSettingsBase v1.0
.GetSettingsPackageFiles NoInput List of String v1.0
.GetSettingsTypes NoInput List of String v1.0
.Shutdown NoInput NoResponse v1.0
.GetImage NoInput Image v1.0
.SetWatchDog WatchDog WatchDog v1.0
.GetWatchDog NoInput WatchDog v1.0

Endpoint Details

SetTestPlanXML

The String request for SetTestPlanXML must contain valid OpenTAP test plan XML (.TapPlan format). The XML content is JSON-encoded as a string literal on the wire (see Wire Format).

The List of String response contains non-fatal deserialization warnings from the OpenTAP serializer. For example, a missing plugin type or a package version mismatch will appear as a string in this list. An empty list means the test plan loaded without issues.

Fatal errors (e.g., invalid XML, corrupt data, or a test plan already executing) are returned as NATS error responses, not in the response body.

Example using the NATS CLI:

nats req OpenTap.Runner.{RunnerId}.Session.{SessionId}.Request.SetTestPlanXML '"<?xml version=\"1.0\" encoding=\"utf-8\"?><TestPlan type=\"OpenTap.TestPlan\"><Steps /></TestPlan>"'

Data Types

NoInput

Empty JSON object {}.

For example, the Session endpoint to abort a test plan takes a NoInput:

nats req OpenTap.Runner.{RunnerId}.Session.{SessionId}.Request.AbortTestPlan "{}"

NoResponse

Empty JSON object {}.

For example, the Session endpoint to abort a test plan returns a NoResponse:

nats req OpenTap.Runner.{RunnerId}.Session.{SessionId}.Request.AbortTestPlan "{}" returns {}.

RunnerStatus

Property Type
RemoteConnection RemoteConnection
Version String
RunningAsService Boolean
RunningInContainer Boolean
AutoUpdateDisabled Boolean
AllowParallelRuns Boolean
Hostname String
IpAddress String
StreamInfo StreamInfoStatus

RemoteConnection

Property Type
Endpoint String
Connected Boolean
Rtt String

StreamInfoStatus

Property Type
RunsMaxBytes Int64
RunsUsage Int64
DiskFree Int64

Image

Property Type
Name String
Packages List of PackageSpecifier
Repositories List of String
Id String

Session

Property Type
Subject String
Id Guid
ImageId String
SessionState SessionState
TestPlanRunId String
StartedBy String
Metadata Dictionary of String to String

SessionState

Enum with the following values:

  • Idle
  • Loading
  • Executing
  • Breaking
  • Aborting
  • WaitingForUserInput

RunStatus

Property Type
SessionId Guid
Verdict Verdict
TestPlanRunId Guid (nullable)
FailedToStart Boolean
SessionState SessionState
ExecutingSteps List of Guid

EditStatus

Property Type
TestPlanDirty Boolean
UndoBufferSize Int32
RedoBufferSize Int32

ErrorResponse

Returned by endpoints such as LoadComponentSettingsFromRepository. Represents a structured error with optional nested inner errors.

Property Type
Type String
Message String
Parameter String (nullable)
Source String (nullable)
StackTrace String (nullable)
Inner ErrorResponse (nullable)

NewSessionRequest

Property Type
Image Image
UseDefaults Boolean
ImageOverride Image
TestPlanReference RepositoryPackageReference
Settings Settings
RunTestPlan Boolean
TestPlanParameters List of Parameter
ShutdownOnComplete Boolean
Metadata Dictionary of String to String

NewSessionResponse

Property Type
Session Session

DefaultSessionRequest

Property Type
ImageOverride Image
TestPlanReference RepositoryPackageReference

RegisterSessionRequest

Property Type
SessionId Guid
ProcessId Int32
ExpectedRunnerVersion String
Operator String
Metadata Dictionary of String to String

RegisterSessionResponse

Property Type
Success Boolean
Message String
Url String
AccessToken String
OperatorId String
SettingsFileLocation String
TestPlan RepositoryPackageReference
Settings Settings
RunTestPlan Boolean
Parameters List of Parameter
ShutdownOnComplete Boolean

SaveDefaultSettings

Property Type
SessionId Guid
Repository String

Note: Repository is accepted by the client library for forward-compatibility but is currently ignored by the server.

RunnerUpdateRequest

Property Type
UpdateTo String

RunnerUpdateResponse

Property Type
Success Boolean
ErrorMessage String

GetSettingsRequest

Property Type
ContextId Guid

GetTestPlanReferenceResponse

Property Type
TestPlanReference RepositoryPackageReference

SaveTestPlanToRepositoryResponse

Property Type
TestPlanReference RepositoryPackageReference

MoveRequest

Property Type
StepIds List of Guid
TargetStepId Guid

DeleteRequest

Property Type
StepIds List of Guid

Parameter

Property Type
Group String
Name String
Value String
TypeCode TypeCode

EnableMetricsPollingRequest

Property Type
Enabled Boolean

EnableMetricsPollingResponse

Property Type
JetStreamName String
Enabled Boolean

AssetDiscoveryResponse

Property Type
AssetProviders List of AssetDiscoveryResult
LastSeen DateTime

AssetDiscoveryResult

Property Type
DiscoveredAssets List of DiscoveredAsset
Priority Double
Name String
IsSuccess Boolean
Error String

DiscoveredAsset

Property Type
Manufacturer String
Model String
AssetIdentifier String

SetComponentSettingsRequest

Property Type
ReturnedSettings ComponentSettingsBase
GroupName String
Name String

ComponentSettingsBase

Property Type
Name String
GroupName String

ComponentSettingsIdentifier

Extends ComponentSettingsBase. Returned by GetComponentSettingsOverview to identify available component settings without loading their full configuration.

Property Type
Name String
GroupName String

GetComponentSettingsRequest

Property Type
GroupName String
Name String

GetComponentSettingsListItemRequest

Property Type
Index Int32
GroupName String
Name String

ComponentSettingsListItem

Property Type
Settings Settings
Name String
EnabledResource Boolean (nullable)
VisualStatus VisualStatus
ValueType String
UnitAttribute UnitAttribute
Display DisplayAttribute
MetaData MetaData
ExternalParameter ExternalParameter

SetComponentSettingsListItemRequest

Property Type
Item ComponentSettingsListItem
Index Int32
GroupName String
Name String

GetComponentSettingDataGridRequest

Property Type
PropertyName String
Index Int32
GroupName String
Name String

DataGridControl

Property Type
Items List of List of Setting
FixedSize Boolean
Errors List of String
Layout Layout
ColumnDisplayName ColumnDisplayName
ValueDescription String
PropertyName String
Icons List of Icon
Submit Boolean
VisualStatus VisualStatus
ValueType String
UnitAttribute UnitAttribute
Display DisplayAttribute
MetaData MetaData
ExternalParameter ExternalParameter

SetComponentSettingDataGridRequest

Property Type
DataGridControl DataGridControl
PropertyName String
Index Int32
GroupName String
Name String

AddComponentSettingDataGridItemTypeRequest

Property Type
TypeName String
PropertyName String
Index Int32
GroupName String
Name String

UploadFileRequest

Property Type
File Byte[]
FileName String

DownloadTapSettingsRequest

Property Type
GroupName String

RepositoryPackageReference

Property Type
Version String
Path String
PathId String

RepositorySettingsPackageDefinition

Property Type
Name String
Tags List of String
Version String
Path String
PathId String

AddComponentSettingsListItemRequest

Property Type
TypeName String
GroupName String
Name String

CopyRequest

Property Type
StepIds List of Guid

PasteRequest

Property Type
TargetStepId Guid
Clipboard String

PackageSpecifier

Property Type
Name String
Version String
Architecture String
OS String

Settings

A JSON array of Setting objects. Serialized as [{...}, {...}, ...].

VisualStatus

Property Type
IsReadOnly Boolean
IsVisible Boolean
IsEnabled Boolean

UnitAttribute

Property Type
Unit String
PreScaling Double
StringFormat String
UseRanges Boolean
UseEngineeringPrefix Boolean

DisplayAttribute

Property Type
Description String
Group List of String
Name String
Order Double
Collapsed Boolean

MetaData

Property Type
Name String
MacroName String
Group String
Frozen Boolean

ExternalParameter

Property Type
Name String

Layout

Property Type
Mode LayoutMode
RowHeight Int32
MaxRowHeight Int32

ListItemType

Represents a selectable type in a component settings list or data grid.

Property Type
TypeName String
TypeDisplay DisplayAttribute

ColumnDisplayName

Property Type
ColumnName String
Order Double
IsReadOnly Boolean

Icon

Property Type
IconName String
Invoke Boolean (nullable)
StepReference Guid (nullable)
PropertyReference String

Setting

Property Type
Errors List of String
Layout Layout
ColumnDisplayName ColumnDisplayName
ValueDescription String
PropertyName String
Icons List of Icon
Submit Boolean
VisualStatus VisualStatus
ValueType String
UnitAttribute UnitAttribute
Display DisplayAttribute
MetaData MetaData
ExternalParameter ExternalParameter

RepositoryPackageDefinition

Property Type
Name String
Tags List of String
Version String
Path String
PathId String

TestPlan

Property Type
ChildTestSteps List of TestStep
Settings Settings
IsOpen Boolean
PropertiesToInclude List of String
Id Guid

SetSettingsRequest

Property Type
ContextId Guid
Settings Settings

TestPlanValidationErrors

A JSON array of TestStepValidationError objects.

CommonSettings

Property Type
Step TestStep
StepIds List of Guid

CommonStepSettingsContext

Property Type
CommonContext CommonContext
PropertyName String

CommonContext

Property Type
ContextItems List of Setting
StepIds List of Guid

Interaction

Property Type
Timeout String
Title String
Modal Boolean
Settings Settings
Id Guid

PropertyReferenceRequest

Property Type
ContextId Guid
PropertyName String

ProfileGroup

Represents a group of component settings profiles.

Property Type
Profiles List of String
CurrentProfile String
GroupName String

SetContextMenuRequest

Property Type
ContextId Guid
PropertyName String
ContextMenu List of Setting

SetDataGridRequest

Property Type
ContextId Guid
PropertyName String
DataGridControl DataGridControl

AddDataGridItemTypeRequest

Property Type
ContextId Guid
PropertyName String
TypeName String

BreakPoints

Property Type
TestSteps List of Guid

GetSessionLogsRequest

Property Type
Levels List of Int32
ExcludedSources List of String
FilterText String
Offset Int32
Limit Int32

LogList

Property Type
Logs List of LogEntry
Offset Int32
FilteredCount Int32
TotalCount Dictionary of String to Int32

GetSessionSearchRequest

Property Type
Levels List of Int32
ExcludedSources List of String
FilterText String
SearchText String

WatchDog

Property Type
InactiveSeconds Double
TerminationTimeout Int32

TestStep

Property Type
Id Guid
ChildTestSteps List of TestStep
IsChildTestStepsReadOnly Boolean
IsReadOnly Boolean
Settings Settings
TypeName String
TypeDisplay DisplayAttribute
Name String
ExpandedName String

TestStepType

Extends TestStep. Returned by GetStepTypes to describe an available test step type and its allowed children.

Property Type
Id Guid
ChildTestSteps List of TestStep
IsChildTestStepsReadOnly Boolean
IsReadOnly Boolean
Settings Settings
TypeName String
TypeDisplay DisplayAttribute
Name String
ExpandedName String
AvailableChildrenTypes List of String

TestStepValidationError

Property Type
StepId String
ValidationErrors List of ValidationError

LogEntry

Property Type
Source String
Timestamp Int64
Message String
Level Int32
DurationNS Int64

ValidationError

Property Type
PropertyName String
Error String

Verdict

Outcome of a test plan run. Serialized as a PascalCase string.

Value Integer
NotSet 0
Pass 1
Inconclusive 2
Fail 3
Aborted 4
Error 5

SlimSession

Compact session summary included in runner heartbeat events.

Property Type
SessionId Guid
SessionState SessionState

LayoutMode

Flags enum controlling UI layout of a setting. Values can be combined (bitwise OR).

Value Integer
Normal 1
FullRow 2
FloatBottom 4

TypeCode

Maps to System.TypeCode. Indicates the primitive type of a Parameter value. Serialized as a PascalCase string.

Value Integer
Empty 0
Object 1
DBNull 2
Boolean 3
Char 4
SByte 5
Byte 6
Int16 7
UInt16 8
Int32 9
UInt32 10
Int64 11
UInt64 12
Single 13
Double 14
Decimal 15
DateTime 16
String 18

Implementation Guide

This document describes the wire-level protocol for building an OpenTAP Runner client in any programming language. It covers NATS connection setup, the custom request-reply pattern, chunked transfer, event subscriptions, session lifecycle, JetStream usage, and JSON serialization rules. Read it alongside the API Reference, which lists every endpoint and DTO.

A client built from these two documents alone should be able to connect to a running Runner, create a session, load and execute a test plan, collect results, and shut down cleanly.


1. NATS Connection Setup

Default URL

A locally installed Runner listens on:

nats://127.0.0.1:20111

When connecting to a cloud-hosted Runner, the URL is supplied by the cloud platform alongside JWT credentials (see Authentication below).

Match these options on your NATS client to align with the server's expectations:

Option Value Notes
MaxReconnect Forever / -1 Never stop retrying
ReconnectWait 1000 ms Pause between attempts
AllowReconnect true Required for reconnect to work
PingInterval 45 000 ms Keep-alive interval
ConnectTimeout 5 000 ms Initial connection timeout

Authentication

Local Runner — No credentials are required. Connect without authentication.

Cloud Runner — Authenticate with a JWT and NKey seed pair:

  • On connection, supply the JWT in the NATS user_jwt field.
  • Sign server nonces using the NKey seed (NaCl Ed25519 signature).
  • The specific JWT and seed are provided by the KS8500 cloud platform.

Deriving the RunnerId

The RunnerId is the NATS server's reported name, available immediately after the connection handshake as connection.ServerInfo.Name (or the equivalent field in your NATS client library). All subsequent subject construction requires this value.

C# example:

await using INatsConnection connection = new NatsConnection(new NatsOpts
{
    Url = "nats://127.0.0.1:20111"
});

// ServerInfo is populated after the connection is established.
string runnerId = connection.ServerInfo.Name;
string runnerBase = $"OpenTap.Runner.{runnerId}";

2. Subject Hierarchy

All communication uses NATS subjects derived from two base templates. Substitute {RunnerId} with the value derived in section 1.

Base Templates

Name Pattern
Runner base OpenTap.Runner.{RunnerId}
Session base OpenTap.Runner.{RunnerId}.Session.{SessionId}

{SessionId} is a UUID (lowercase, no braces) returned by session-creation endpoints.

Runner Subjects

Purpose Subject
Runner request OpenTap.Runner.{RunnerId}.Request.{EndpointName}
Runner lifecycle Started OpenTap.Runner.{RunnerId}.Events.Lifetime.Started
Runner lifecycle Stopped OpenTap.Runner.{RunnerId}.Events.Lifetime.Stopped
Runner lifecycle Heartbeat OpenTap.Runner.{RunnerId}.Events.Lifetime.Heartbeat
Runner executing state OpenTap.Runner.{RunnerId}.Events.Running
Runner metadata updated OpenTap.Runner.{RunnerId}.Events.MetadataUpdated

Session Subjects

Purpose Subject
Session request {SessionBase}.Request.{EndpointName}
Session event {SessionBase}.Events.{EventName}
Session logs (streaming) {SessionBase}.SessionLogs
Plan run (start/complete) {SessionBase}.PlanRun.{PlanRunId}
Plan run parameters {SessionBase}.PlanRun.{PlanRunId}.Parameters
Plan run logs {SessionBase}.PlanRun.{PlanRunId}.Logs
Plan run artifact {SessionBase}.PlanRun.{PlanRunId}.Artifact.{Guid}
Step run (start/complete) {SessionBase}.PlanRun.{PlanRunId}.StepRun.{StepRunId}
Step run parameters {SessionBase}.PlanRun.{PlanRunId}.StepRun.{StepRunId}.Parameters
Step run result table {SessionBase}.PlanRun.{PlanRunId}.StepRun.{StepRunId}.Result.{TableName}
Metrics {SessionBase}.Metrics.{Group}.{Name}

{TableName} has NATS-unsafe characters (spaces, dots, wildcards, >) replaced with underscores by the server.

NATS Subscription Wildcards

When subscribing to result streams, use NATS wildcards:

Wildcard Meaning
* Exactly one token
> One or more tokens (only at end)

Typical result subscriptions:

{SessionBase}.PlanRun.*                          → plan run start and complete
{SessionBase}.PlanRun.*.StepRun.*                → step run start and complete
{SessionBase}.PlanRun.*.StepRun.*.Result.>       → all result tables
{SessionBase}.PlanRun.*.Logs                     → plan run log batches

3. Request-Reply Protocol

Overview

The Runner does not use the standard NATS request-reply pattern. Instead, the client implements a custom pattern using an explicit reply subscription:

  1. Client creates a unique reply subject: {requestSubject}.{NewGuid()}
  2. Client subscribes to that reply subject
  3. Client publishes the request to {requestSubject} with replyTo set to the reply subject
  4. Server sends the response (potentially in multiple chunks) to the reply subject
  5. Client reads from the reply subscription until the full response is assembled
  6. Client unsubscribes from the reply subject

This is necessary because the response may be chunked across multiple messages (see section 4), which standard request-reply does not support.

Headers

The following NATS message headers are used in requests:

Header When Present Value
Authorization Endpoints that require a token The access token string
RequestId Multi-chunk request payloads A new UUID per request
ChunkSize Multi-chunk request payloads Chunk size in bytes (integer string)
ChunkNumber Multi-chunk request payloads 1-indexed chunk sequence number

Default Timeout

The default request timeout is 1 minute. There is no built-in retry. If the server does not respond within the timeout, treat the request as failed.

Error Detection

Before deserializing a response, inspect its headers:

  • If the OpenTapNatsError header is present (any value), the response body is an error object:

    { "Message": "Human-readable error description" }
    

    Raise an exception with that message.

  • If the NATS layer reports no subscribers (NatsNoRespondersException or equivalent), the runner or session is not available on that subject.

Pseudocode

function request(subject, payload, token=null, timeout=60s):
    replySubject = subject + "." + newGuid()
    subscription = nats.subscribe(replySubject)
    try:
        headers = {}
        if token != null:
            headers["Authorization"] = token
        encodedPayload = jsonEncode(payload)   // null → empty byte array
        if len(encodedPayload) > chunkSize:
            publishInChunks(subject, encodedPayload, headers, replySubject)
        else:
            nats.publish(subject, encodedPayload, headers, replyTo=replySubject)

        chunks = []
        deadline = now() + timeout
        while now() < deadline:
            msg = subscription.readNext(deadline - now())
            if msg.hasNoResponders:
                raise NoRespondersError
            if "OpenTapNatsError" in msg.headers:
                raise RunnerError(jsonDecode(msg.body).Message)
            if msg.body.length > 0:
                chunks.append(msg.body)
            if "ChunkSize" not in msg.headers:
                break
            chunkSize = int(msg.headers["ChunkSize"])
            if msg.body.length != chunkSize:
                break   // last chunk

        raise TimeoutError if deadline passed
        return jsonDecode(concatenate(chunks))
    finally:
        subscription.unsubscribe()

C# Example

// BaseClient.RequestAsync<T> — simplified excerpt
INatsSub<byte[]> sub = await connection.SubscribeCoreAsync<byte[]>($"{subject}.{Guid.NewGuid()}");
try
{
    var headers = new NatsHeaders();
    if (useToken && Token != null)
        headers.Add("Authorization", Token);

    byte[] payload = JsonSerializer.SerializeToUtf8Bytes(data, RunnerSerialization.DefaultSerializerOptions);
    if (payload.Length > _chunkSize)
        await PublishInChunksAsync(subject, payload, headers, sub.Subject, ct);
    else
        await connection.PublishAsync(subject, payload, headers, replyTo: sub.Subject, cancellationToken: ct);

    List<byte[]> chunks = new();
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
    cts.CancelAfter(timeout);

    while (!cts.Token.IsCancellationRequested)
    {
        NatsMsg<byte[]> msg = await sub.Msgs.ReadAsync(cts.Token);

        if (msg.HasNoResponders)
            throw new NatsNoRespondersException();
        if (msg.Headers?.ContainsKey("OpenTapNatsError") == true)
            throw new RunnerException(JsonSerializer.Deserialize<RunnerError>(msg.Data).Message);
        if (msg.Data?.Length > 0)
            chunks.Add(msg.Data);
        if (msg.Headers == null || !msg.Headers.TryGetValue("ChunkSize", out var cs))
            break;
        if (msg.Data?.Length != int.Parse(cs))
            break;
    }

    return JsonSerializer.Deserialize<T>(CombineChunks(chunks));
}
finally
{
    await sub.UnsubscribeAsync();
}

4. Chunked Transfer Protocol

Large messages — in both directions — are split into chunks. Every endpoint that accepts or returns a payload may involve chunking. Clients must handle chunks even for small payloads, because the server may choose to chunk any response.

Chunk Size

Side Calculation Typical Value
Client (sending) MaxPayload − 10 KB ~1 MB on a default NATS server
Client (fallback) 1 MB Used when MaxPayload is unavailable
Server (publishing results/events) 90 KB (fixed) Aimed at staying under 100 KB per message
Server (request handler) MaxPayload − 50 KB Conservative reservation for headers

MaxPayload is advertised by the NATS server in the connect INFO message (connection.ServerInfo.MaxPayload).

Sending Chunks (Client → Server)

  1. Encode the full payload as UTF-8 JSON bytes.
  2. If the payload fits within one chunk, send it as a single message with no chunk headers.
  3. If the payload exceeds one chunk:
    • Add headers to every message:
      • RequestId: a new UUID string (same value for all chunks of one request)
      • ChunkSize: the chunk size in bytes (integer string)
      • ChunkNumber: 1-indexed position of this chunk (integer string)
    • Send chunks sequentially.
    • Trailing empty message: if the final chunk's data length is exactly equal to ChunkSize (i.e., no partial last chunk), send one additional message with an empty body. This is also required when the total payload is empty (zero bytes). This sentinel tells the receiver that no more chunks are coming.

Receiving Chunks (Client ← Server)

Read messages from the reply subscription in a loop:

chunks = []
while true:
    msg = read_next()
    if msg.body.length > 0:
        chunks.append(msg.body)
    if "ChunkSize" not in msg.headers:
        break   // single-message response (no chunking in use)
    chunkSize = int(msg.headers["ChunkSize"])
    if msg.body.length != chunkSize:
        break   // this message is shorter than a full chunk → it is the last one
// reassemble: concatenate all chunks in order
payload = concatenate(chunks)

The last chunk is detected by its size being strictly less than ChunkSize (or empty). The server never sends a ChunkNumber count in advance; the client discovers the end by size.

Server-Side Reassembly

The server reassembles chunked requests using a RequestHandler keyed on RequestId:

  • Supports out-of-order delivery (chunks are stored by index).
  • Maximum 1000 chunks per request (~1 GB at the default NATS max payload).
  • Incomplete requests expire after 60 seconds of inactivity and are discarded.

C# Example (Sending)

// PublishInChunksAsync — from BaseClient.cs
private async Task PublishInChunksAsync(string subject, ReadOnlyMemory<byte> payload,
    NatsHeaders headers, string replyTo, CancellationToken ct)
{
    headers.Add("RequestId", Guid.NewGuid().ToString());
    headers.Add("ChunkSize", _chunkSize.ToString());

    int sent = 0, chunkNumber = 1;
    while (sent < payload.Length)
    {
        var chunkHeaders = CloneHeaders(headers);
        chunkHeaders["ChunkNumber"] = chunkNumber.ToString();
        int count = Math.Min(payload.Length - sent, _chunkSize);
        await connection.PublishAsync(subject, payload.Slice(sent, count).ToArray(),
            chunkHeaders, replyTo, cancellationToken: ct);
        sent += count;
        chunkNumber++;
    }

    // Sentinel: send empty message if last chunk was exactly full (or payload was empty)
    if (payload.Length % _chunkSize == 0)
    {
        headers["ChunkNumber"] = chunkNumber.ToString();
        await connection.PublishAsync(subject, Array.Empty<byte>(), headers, replyTo, cancellationToken: ct);
    }
}

5. Session Lifecycle

Session States

Sessions progress through a defined set of states. The current state is always available via GetStatus and is also broadcast in SessionStateChanged events.

Value Integer Description
Idle 0 Session is ready; no test plan is executing
Executing 1 Test plan is running
Breaking 2 Execution is paused at a breakpoint
Aborting 3 Abort requested; executing cleanup
WaitingForUserInput 4 Execution paused; awaiting user input
Loading 5 Session is initialising; not yet ready
[Created] → Loading → Idle → Executing → Idle
                                ↕
                            Breaking
                                ↕
                        WaitingForUserInput
                                ↓
                           Aborting → Idle

Creating a Session

Call one of the session-creation endpoints on the Runner:

Endpoint Description
NewSession Full-featured creation: image, defaults, optional auto-run and auto-close
StartSession Simple session using Runner defaults
StartImageSession Session using a specific image

All return a Session object containing the session's UUID (Id). Construct the session base subject immediately:

sessionBase = "OpenTap.Runner.{RunnerId}.Session.{session.Id}"

Waiting for Readiness

A newly created session starts in the Loading state and is not immediately ready to accept commands. Wait for SessionState.Idle before sending requests.

Option A — Poll:

while true:
    status = request(sessionBase + ".Request.GetStatus")
    if status.SessionState == "Idle":
        break
    sleep(250ms)

Option B — Subscribe:

subscribe(sessionBase + ".Events.SessionStateChanged", handler = function(event):
    if event.RunStatus.SessionState == "Idle":
        markReady()
)

Shutdown Sequence

Always shut down sessions explicitly to release resources:

  1. Send Shutdown to the session:
    request(sessionBase + ".Request.Shutdown")
    
  2. Send ShutdownSession to the runner (passing the session UUID):
    request(runnerBase + ".Request.ShutdownSession", sessionId)
    

Watchdog / Heartbeat

Interval Description
Runner heartbeat Every 15 seconds, published to Events.Lifetime.Heartbeat
Session heartbeat Every 5 seconds, published to Events.Heartbeat
Session inactivity timeout Default 1800 seconds (30 minutes); configurable via TerminationTimeout
Runner kill threshold Sessions not seen for 7 seconds in production (1800 s in development mode) are terminated

If your client needs to maintain a long-lived session without activity, monitor the session heartbeat events and ensure the NATS connection stays alive.


6. Event and Subscription Model

Events are published as fire-and-forget core NATS messages (not JetStream). Subscribe before creating a session or starting a test plan to avoid missing early events.

Runner Events

Subscribe to these on the runner base subject (OpenTap.Runner.{RunnerId}.{suffix}):

StartedLifetimeEvent

Published when the Runner process has started.

Subject: Events.Lifetime.Started

No payload fields.

StoppedLifetimeEvent

Published when the Runner process is stopping.

Subject: Events.Lifetime.Stopped

No payload fields.

HeartbeatLifetimeEvent

Published every 15 seconds. Contains a snapshot of all currently active sessions.

Subject: Events.Lifetime.Heartbeat

Property Type Description
Sessions List of SlimSession All sessions currently alive on this Runner

RunningEvent

Published when the Runner's overall executing state changes.

Subject: Events.Running

Property Type Description
IsRunning Boolean Whether any session is currently executing a test plan

MetadataUpdatedEvent

Published when Runner-level metadata changes (e.g. after a session is registered or updated).

Subject: Events.MetadataUpdated

Property Type Description
SessionId Guid The session whose metadata changed
Metadata Dictionary of String → String Updated key/value metadata map

Session Events

All session events are published to {SessionBase}.Events.{EventName}. Each event type has its own subject; there is no single wildcard subject that captures all session events. Subscribe to each individually.

TypeCacheInvalidatedEvent

Published when the session's type cache has been invalidated and must be refreshed.

Subject: Events.TypeCacheInvalidated

No payload fields.

TestPlanSettingsChangedEvent

Published when the test plan settings associated with the session have changed.

Subject: Events.TestPlanSettingsChanged

No payload fields.

SessionTimeoutEvent

Published immediately before the session terminates due to watchdog inactivity timeout.

Subject: Events.SessionTimeout

No payload fields.

StartingEvent

Published when a test plan execution is about to begin (sequencer is transitioning into Executing).

Subject: Events.Starting

No payload fields.

StartedEvent

Published after a test plan execution has started and the first step is about to run.

Subject: Events.Started

No payload fields.

HeartbeatEvent

Published every 5 seconds while the session is alive. Use this to monitor session health and watchdog state.

Subject: Events.Heartbeat

Property Type Description
Timestamp Int64 Unix timestamp in seconds
WatchDog WatchDog Watchdog state: inactivity counter and termination threshold
State SessionState Current session state
TestPlanRunID Guid (nullable) Active plan run ID, or null if not executing

WatchDog fields:

Property Type Description
InactiveSeconds Double Seconds elapsed since last activity
TerminationTimeout Int32 Seconds of inactivity before the session is terminated (default 1800)

StoppingEvent

Published when a test plan execution is in the process of stopping (e.g. after abort or normal completion, before teardown is complete).

Subject: Events.Stopping

No payload fields.

StoppedEvent

Published when a test plan execution has fully stopped and the session has returned to Idle.

Subject: Events.Stopped

No payload fields.

TestPlanChangedEvent

Published whenever the in-memory test plan is modified (step added, removed, reordered, or a property changed).

Subject: Events.TestPlanChanged

Property Type Description
EditStatus EditStatus Current dirty/undo/redo state of the test plan editor

EditStatus fields:

Property Type Description
TestPlanDirty Boolean Whether there are unsaved changes
UndoBufferSize Int32 Number of available undo steps
RedoBufferSize Int32 Number of available redo steps

SessionStateChangedEvent

Published whenever the session's execution state changes (e.g. Idle → Executing, Executing → Idle). This is the primary event for tracking execution progress.

Subject: Events.SessionStateChanged

Property Type Description
RunStatus RunStatus Full status snapshot including state, verdict, and active plan run ID

SettingsChangedEvent

Published when a component settings group has been modified.

Subject: Events.SettingsChanged

Property Type Description
Group String Component settings group name
Name String Component settings name within the group

TestStepChangedEvent

Published when a specific test step has been modified.

Subject: Events.TestStepChanged

Property Type Description
StepId Guid ID of the test step that changed

BreakEvent

Published when execution pauses at a breakpoint. The session enters Breaking state.

Subject: Events.Break

Property Type Description
StepId Guid ID of the step where execution broke

UserInputRequestedEvent

Published when a test step requires user input before it can continue. The session enters WaitingForUserInput state.

Subject: Events.UserInputRequested

Property Type Description
RequestId Guid Identifier of the pending input request; use it with GetUserInput and submit via SetUserInput

UserInputRequestCompletedEvent

Published when a pending user input request has been resolved (answered, cancelled, or timed out).

Subject: Events.UserInputRequestCompleted

Property Type Description
RequestId Guid Identifier of the completed input request
Answered Boolean True if the user provided input; false if cancelled
Timeout Boolean True if the request timed out without input

Subscribing to Session Events — C# Example

// SessionClient.ConnectSessionEvents — simplified excerpt
void SubscribeEvent<TEvent>(string eventName) where TEvent : SessionEvent
{
    Task.Run(async () =>
    {
        await foreach (var msg in connection.SubscribeAsync<TEvent>(
            $"{sessionBase}.Events.{eventName}", cancellationToken: cts.Token))
        {
            if (msg.Data != null)
                await handler(msg.Data);
        }
    });
}

SubscribeEvent<TypeCacheInvalidatedEvent>("TypeCacheInvalidated");
SubscribeEvent<TestPlanSettingsChangedEvent>("TestPlanSettingsChanged");
SubscribeEvent<SessionTimeoutEvent>("SessionTimeout");
SubscribeEvent<StartingEvent>("Starting");
SubscribeEvent<StartedEvent>("Started");
SubscribeEvent<HeartbeatEvent>("Heartbeat");
SubscribeEvent<StoppingEvent>("Stopping");
SubscribeEvent<StoppedEvent>("Stopped");
SubscribeEvent<TestPlanChangedEvent>("TestPlanChanged");
SubscribeEvent<SessionStateChangedEvent>("SessionStateChanged");
SubscribeEvent<SettingsChangedEvent>("SettingsChanged");
SubscribeEvent<TestStepChangedEvent>("TestStepChanged");
SubscribeEvent<BreakEvent>("Break");
SubscribeEvent<UserInputRequestedEvent>("UserInputRequested");
SubscribeEvent<UserInputRequestCompletedEvent>("UserInputRequestCompleted");

Session Logs (Streaming)

Real-time log output is published to {SessionBase}.SessionLogs as LogList messages (a list of LogEntry). Subscribe before starting the session if you need complete logs.

Result Streams

Results are published as test plan execution proceeds. Use NATS wildcard subscriptions:

Subject pattern Payload type Trigger
{SessionBase}.PlanRun.* JSON object with Status = "TestPlanRunStart" or "TestPlanRunCompleted" Plan run start / complete
{SessionBase}.PlanRun.*.StepRun.* JSON object with Status = "TestStepRunStart" or "TestStepRunCompleted" Step run start / complete
{SessionBase}.PlanRun.*.StepRun.*.Result.> Result Result table published by a test step
{SessionBase}.PlanRun.*.Logs List<LogEntry> Log batch during plan run; null body signals end of run
{SessionBase}.PlanRun.*.Artifact.* Binary stream Artifact file; Name NATS header contains the filename

Messages on plan run and step run subjects carry a Seq NATS header (unsigned integer string) which can be used to detect gaps or reorder messages. The sequence number is scoped to the session and resets to 0 at the start of each plan run.

Artifacts are published as a raw binary stream in CHUNK_SIZE (90 KB) pieces terminated by a null/empty message. Read them the same way as chunked responses (section 4), but the Name header is only present on the first message.

Metrics

Metrics are published exclusively to JetStream on {SessionBase}.Metrics.{Group}.{Name}. They are stored in the Metric stream with a subject transform to Metric.{Group}.{Name}. Use a JetStream consumer to read historical or live metrics.


7. JetStream vs Core NATS

Core NATS (fire-and-forget pub/sub)

Used for:

  • All request-reply (section 3)
  • All event subscriptions (section 6)
  • Session log streaming

These messages are not persisted. If you are not subscribed when a message is published, it is lost.

JetStream (persistent streams)

Used for:

  • Test plan run results (plan runs, step runs, result tables, parameters)
  • Plan run logs
  • Artifacts
  • Metrics

Runs stream configuration:

Setting Value
Name Runs
Storage File
Retention Interest (kept while at least one consumer is active)
Discard New (new messages dropped when stream is full)
MaxBytes Configurable (default unlimited; monitor disk usage)
Subjects Conditionally: plan runs, step runs, parameters, results, logs, artifacts

Metric stream configuration:

Setting Value
Name Metric
Storage File
Retention Limits
Discard Old
Max age 2 days
Subject transform {SessionBase}.Metrics.>Metric.>

Whether JetStream is available depends on the Runner's configuration. If the Runs stream does not exist, results and logs are published as core NATS messages instead (and may be missed if not subscribed at the right time). In practice, always subscribe to result and log subjects before starting execution regardless of JetStream availability.


8. JSON Serialization

Wire Format

All message bodies are UTF-8 encoded JSON. There is no binary envelope or framing — the raw JSON bytes are the NATS message payload (potentially across multiple chunks).

Null Handling

Both the server and client omit null-valued properties from serialized output. When deserializing, treat absent properties as null. Do not reject messages that are missing optional fields.

Enum Serialization

Enums are serialized as PascalCase strings on the wire, regardless of which side produces them:

  • Server uses Newtonsoft.Json StringEnumConverter (default PascalCase).
  • Client uses System.Text.Json JsonStringEnumConverter with per-property overrides to ensure PascalCase output on properties that carry enums.

Deserialization is case-insensitive. When building a client, always serialize enums as PascalCase strings and accept any casing.

Examples:

{ "SessionState": "Idle" }
{ "Verdict": "Pass" }
{ "LayoutMode": "FullRow" }

Polymorphic Types

Three object hierarchies use explicit discriminator properties to identify concrete types on the wire.

Base type Discriminator property Example values
ComponentSettingsBase ComponentSettingsType ComponentSettings, ComponentSettingsList, ComponentSettingsIdentifier
TestStep TestStepTypeOrInstance TestStep, TestStepType, TestStepCopy
Setting ControlType TextBoxControl, DataGridControl, DropdownControl, ...

When serializing objects of these types, include the discriminator. When deserializing, use the discriminator to select the concrete class, or retain raw JSON when the type is unknown.

AllowOutOfOrderMetadataProperties

The client's System.Text.Json options include AllowOutOfOrderMetadataProperties = true. Do not assume discriminator or metadata properties appear first in the JSON object.

Recommendations for New Client Implementations

  • Serialize enum values as PascalCase strings.
  • Omit null properties from outgoing messages.
  • Use case-insensitive enum deserialization.
  • Accept unknown properties without error (forward compatibility).
  • Accept discriminator / metadata properties at any position in the object.

9. Error Handling

Application-Level Errors

When the Runner or Session encounters an error processing a request, it responds with:

  • NATS header OpenTapNatsError: true
  • Body:
    { "Message": "Human-readable description of the error" }
    

Always check for the OpenTapNatsError header before attempting to deserialize a response as the expected type.

Transport-Level Errors

Condition Cause Action
No responders No subscriber on the request subject The Runner or Session is not running, or the subject is wrong
Timeout No response within 1 minute The Runner is overloaded or the request subject is wrong
Connection closed NATS connection dropped Reconnect (use MaxReconnect=Forever)
Chunk reassembly expiry Request chunks did not arrive within 60 s Retry the full request

JetStream Errors

Error code Meaning
10077 Buffer full — test plan execution pauses until disk space is freed or connectivity is restored
10047 Insufficient storage — the configured MaxBytes exceeds available disk space

10. Complete Workflow Example

This example walks through the most common workflow end-to-end: connect to a Runner, create a session, run a test plan, collect results, and shut down.

Pseudocode

// 1. Connect
connection = nats.connect("nats://127.0.0.1:20111")
runnerId   = connection.serverInfo.name
runnerBase = "OpenTap.Runner." + runnerId

// 2. Create a session
response   = request(runnerBase + ".Request.NewSession", {
    "UseDefaults": true,
    "RunTestPlan": false
})
sessionId   = response.Session.Id
sessionBase = runnerBase + ".Session." + sessionId

// 3. Wait for the session to become ready
while true:
    status = request(sessionBase + ".Request.GetStatus")
    if status.SessionState == "Idle": break
    sleep(250ms)

// 4. Subscribe to events, logs, and results (before loading/running)
subscribe(sessionBase + ".Events.SessionStateChanged", onSessionStateChanged)
subscribe(sessionBase + ".Events.Heartbeat",            onHeartbeat)
subscribe(sessionBase + ".SessionLogs",                 onSessionLog)
subscribe(sessionBase + ".PlanRun.*",                   onTestRun)
subscribe(sessionBase + ".PlanRun.*.StepRun.*",         onTestRun)
subscribe(sessionBase + ".PlanRun.*.StepRun.*.Result.>", onResult)

// 5. Load a test plan XML
warnings = request(sessionBase + ".Request.SetTestPlanXML", testPlanXml)

// 6. Execute (optionally pass parameters)
request(sessionBase + ".Request.RunTestPlan", parameters)

// 7. Wait for execution to finish
while true:
    event = waitFor(onSessionStateChanged)
    if event.RunStatus.SessionState == "Idle": break
    // handle Breaking / WaitingForUserInput / Aborting as needed

// 8. Shutdown
request(sessionBase + ".Request.Shutdown")
request(runnerBase  + ".Request.ShutdownSession", sessionId)

// 9. Disconnect
connection.close()

C# Example

// 1. Connect
await using INatsConnection connection = new NatsConnection(new NatsOpts
{
    Url = RunnerClient.DefaultUrl   // "nats://127.0.0.1:20111"
});

// 2. Create clients
RunnerClient  runner  = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();

// 3. Wait for session to become ready
RunStatus status;
do
{
    await Task.Delay(250);
    status = await session.GetStatusAsync();
} while (status.SessionState != SessionState.Idle);

// 4. Subscribe to events and results
var cts = new CancellationTokenSource();
session.ConnectSessionEvents(async evt =>
{
    if (evt is SessionStateChangedEvent sc)
        Console.WriteLine($"State: {sc.RunStatus.SessionState}");
}, cts.Token);

session.ConnectSessionResults(
    resultHandler  : async result  => Console.WriteLine($"Result: {result?.Name}"),
    testRunHandler : async testRun => Console.WriteLine($"TestRun: {testRun?.Status}"),
    token          : cts.Token
);

session.ConnectSessionLogs(async logs =>
{
    foreach (var entry in logs?.Logs ?? [])
        Console.WriteLine($"[{entry.Source}] {entry.Message}");
}, cts.Token);

// 5. Load and run
List<string> loadWarnings = await session.SetTestPlanXMLAsync(testPlanXml);
foreach (string warning in loadWarnings)
    Console.WriteLine($"Load warning: {warning}");

await session.RunTestPlanAsync(new List<Parameter>());

// 6. Wait for completion
do
{
    await Task.Delay(500);
    status = await session.GetStatusAsync();
} while (status.SessionState != SessionState.Idle);

Console.WriteLine($"Verdict: {status.Verdict}");

// 7. Shutdown
cts.Cancel();
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);

Key Points

  • Always subscribe to events and results before calling RunTestPlan. Events published during the transition from Loading → Idle or Idle → Executing may be missed otherwise.
  • RunTestPlan returns immediately; the test plan runs asynchronously. Poll GetStatus or use SessionStateChanged events to detect completion.
  • After Shutdown returns, the session process has stopped. Any further requests to the session will receive a no-responders error.
  • It is safe to call ShutdownSession even if the session has already exited; the Runner will return success.

CLI Reference

tap.exe runner start

Starts the Runner using the config located at <OpenTapInstallation>/Config/runner.json. See Runner Config section for configuration

Arguments:

--listen           : IP address of the interface and port for the internal NATS server to listen on. Default is 127.0.0.1:20111. When registered to KS8500, only the loopback interface is allowed.

Environment variables:

OPENTAP_RUNNER_TOKEN

If RunnerRegistrationUrl is specified in config, and OPENTAP_RUNNER_TOKEN environment variable is set and the CredentialsFilePath is either not set or does not exist, a registration flow will start, similar to the runner register CLI action by using the RunnerRegistrationUrl and RunnerRegistrationToken specified in the configuration file.

If RunnerRegistrationUrl is specified in config, and OPENTAP_RUNNER_TOKEN environment variable is set and the connection is refused by the server due to an Autorization Violation exception, a registration flow will start and if the registration is successful another connection will be attempted.

tap.exe runner session

Starts a Runner Session that can be connected to via NATS.

Arguments:

--id              : Guid formatted ID. Used as Session ID. Default is a new random GUID.
--nats-server     : NATS Servers to connect to. Multiple entries is supported. Default is nats://localhost:4222.
--log             : Specify log file location. This is a standard OpenTAP option that controls where session logs are written. Default is ./SessionLogs.

Standard OpenTAP options are also available:

-v, --verbose     : Show verbose/debug-level log messages.
-c, --color       : Color messages according to their severity.
-q, --quiet       : Quiet console logging.

The session will have the following NATS subject structure:

OpenTap.Session.{ID}

tap.exe runner register

Sets up a new <OpenTapInstallation>/Config/runner.json config file and registers the runner at the specified --url using the --token as authentication and sets the --name as the display name of the runner.

Arguments:

--config        : File path to config file
--url           : Runners Service Registration Url
--token         : Runners Service Registration Token
--name          : Display name of the runner
--nats-server   : NATS Server to connect to
--capability    : Add capability tags

Environment variables:

OPENTAP_RUNNER_TOKEN

Runner Configuration

{
  // Display name sent to Runner Service as part of registration and also used in the connection name to NATS.
  "Name": "John Runner",
  // Directory to store session logs. Created Sessions will also store their sessionlogs at this location.
  "LogDestination": "./SessionLogs",
  // A performance optimization in image creation. If a previously resolved image is requested again, do not consult repositories, just return last resolved image
  "UseImageCache": true,
  // Runner Service url. If this is set, the Runner will register and store registration details in the runner.json config
  "RunnerRegistrationUrl": "http://runner-url.com",
  // Tags to specify the capabilities of this runner. TestPlans created on this Runner will default request to be loaded in Runners with these tags
  "Capabilities": [
    "PowerAnalyzer",
    "SCPI"
  ],
  // Connect to specified NATS servers. If registering using the "RunnerRegistrationUrl", leave this empty to use servers retrieved by the registration process
  // If "ws://" or "wss://" scheme is used, the Runner will use the "NATS Server" TapPackage and connect as a leaf server. This eliminates firewall issues.
  "NatsUrls": [
    "nats://nats:4222"
  ],
  // Enables JWT Bearer token authentication of incomming requests. The signing keys are retrieved by the runner using <AuthUrl>/.well-known/openid-configuration.
  "AuthUrl": "http://keycloak.ks8500-url.com/auth/realms/Default",
  // Default domain. Client's using SetAccessToken will set their access tokens against this domain.
  "DefaultDomain": "http://ks8500-url.com",
  // Preconfigured NATS Seed. The Runner will register towards RunnerRegistrationUrl using this seed. If not specified, a new seed will be generated
  "NatsSeed": "SUAO4HD23JK4UCLUO2BCQ3FGHNTJ4UOBP3B2G5AS7MABB4ANFSN4RIF27U"
}

Configuration can be overwritten using environment variables. The variables are prefixed with OPENTAP_RUNNER_.

Examples:

  • To change the LogDestination, set OPENTAP_RUNNER_LOGDESTINATION=<DesiredLogDirectory>
  • To change an array, e.g. the NatsUrls, set OPENTAP_RUNNER_NATSURLS__0=<Index 0 Nats URL>

REST-API vs Runner

The OpenTAP Runner represents an evolution from the previous OpenTAP REST-API. Our experience highlighted challenges in supporting cloud scenarios with the OpenTAP REST-API, particularly when attempting to access a PC behind a firewall or within a subnet.

To address these challenges, we integrated NATS technology, retaining the core business logic of the OpenTAP REST-API, but replacing ASP.NET Core with NATS.

Given the nature of the changes involved in this transition, particularly the shift in the underlying transport mechanism, some breaking changes were unavoidable. To signal this significant change and the departure from the REST-API model, we established the OpenTAP Runner.

In the past, we provided an autogenerated C# client for the OpenTAP REST-API (OpenTAP.RestApi.Clients.CSharp). However, in light of our experiences, we elected to manually create a C# client (OpenTAP.Runner.Client) and a TypeScript client (OpenTap Runner Client) for the OpenTAP Runner. These have been designed with improvements derived from our experiences with the REST-API client.

Compatibility Runner Client 3.x aligns with Runner 2.x and replaces the earlier synchronous 2.x client. For Runner 1.x, keep using Runner Client 2.x.

Migrating from REST-API to Runner

Migrating from OpenTAP REST-API to OpenTAP Runner is trivial if the provided C# or TypeScript clients were consumed. If so, the migration consists of two steps:

  1. Exchange the client package

C# Client: Remove the NuGet REST-API client dependency OpenTAP.RestApi.Clients.CSharp and instead add the Runner client dependency OpenTAP.Runner.Client

TypeScript Client: Remove the NPM REST-API client dependency opentap-restapi-clients and instead add the Runner client dependency @opentap/runner-client

  1. Update calling code to use the asynchronous APIs exposed by Runner Client 3.x.

The API surface is largely the same, but methods now return Task and have Async suffixes to better align with asynchronous transports.

The following C# example converts a code snippet which retrieves test step types, adds a delay step to the testplan, modifies the Time Delay property on the delay step to 3s, runs the test plan and waits for the test plan execution to complete.

The REST-API code would look like this:

var types = await sessionClient.GetTestStepTypesAsync();
var plan = await sessionClient.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.FirstOrDefault(s => s.TypeName.Contains("Delay")));
plan = await sessionClient.SetTestPlanAsync(plan);

var delayStepSettings = await sessionClient.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (delayStepSettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
    textBox.StringValue = "3 s";
delayStepSettings = await sessionClient.SetSettingsAsync(plan.ChildTestSteps[0].Id, delayStepSettings);

var status = await sessionClient.RunTestPlanAsync();
while (status.ExecutionState != ExecutionState.Idle)
{
    await Task.Delay(1000);
    status = await sessionClient.GetStatusAsync(0);
}

The migrated Runner code:

var types = await sessionClient.GetStepTypesAsync();
var plan = await sessionClient.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.FirstOrDefault(s => s.TypeName.Contains("Delay")));
plan = await sessionClient.SetTestPlanAsync(plan);

var delayStepSettings = await sessionClient.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (delayStepSettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
    textBox.StringValue = "3 s";
delayStepSettings = await sessionClient.SetSettingsAsync(plan.ChildTestSteps[0].Id, delayStepSettings);

var status = await sessionClient.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
    await Task.Delay(1000);
    status = await sessionClient.GetStatusAsync();
}

Although the majority of the code migration is trivial, the RunnerClient and SessionClient creation is different due to the underlying transport mechanism change from REST to NATS.

The following snippets demonstrates the migration of REST client construction to Runner client construction:

HttpClient httpClient = new HttpClient(handler);
SessionManagerClient sessionManagerClient = new SessionManagerClient(httpClient) { BaseUrl = "https://localhost:20116" };
var session = await sessionManagerClient.StartSessionAsync();
SessionClient sessionClient1 = new SessionClient(httpClient) { BaseUrl = session.Url };

The migrated code:

using NATS.Client.Core;
using OpenTap.Runner.Client;

await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient sessionClient = await runner.StartSessionAsync();

Debugging OpenTAP Plugins in the Runner

When developing an OpenTAP plugin that will run inside the Runner, you can attach a debugger to the session process and step through your plugin code. The recommended approach is to install the Runner into your plugin's build output directory, enable development mode, and then start the Runner from there.

Overview

In normal operation the Runner deploys sessions into isolated image directories under the system temp folder. This means the session process loads plugins from a deployed copy — not from your build output — making it impossible to hit breakpoints in your source code.

Development mode changes this behaviour so that every session runs directly from the Runner's own installation directory. When the Runner is installed into your build output, sessions load your locally built plugin DLLs and you can attach a debugger to the session process.

Prerequisites

  • An OpenTAP installation with the Runner and NATS Server packages installed.
  • Your plugin project builds into (or copies output to) a directory that also contains the OpenTAP installation.

Step-by-step setup

1. Install the Runner in your plugin's build output

Add the Runner and NATS Server packages to the OpenTAP installation located in your build output directory. If you are using the OpenTAP NuGet package, this is typically the bin/Debug/net8.0 (or equivalent) folder where tap.dll lives.

cd <your-plugin-build-output>
./tap package install "Runner"

After this step the build output directory contains both the Runner and your plugin DLLs side by side.

2. Set the development mode environment variable

Before starting the Runner, set OPENTAP_RUNNER_DEVELOPMENT_MODE to 1:

Linux / macOS

export OPENTAP_RUNNER_DEVELOPMENT_MODE=1
./tap runner start

Windows (PowerShell)

$env:OPENTAP_RUNNER_DEVELOPMENT_MODE = "1"
.\tap.exe runner start

Windows (cmd)

set OPENTAP_RUNNER_DEVELOPMENT_MODE=1
tap.exe runner start

On startup the Runner logs a warning to confirm the mode is active:

[WRN] Development mode is enabled!

3. Start a session and attach the debugger

Use any client (C# console app, KS8500, or the code examples in this documentation) to create a session on the Runner. Because development mode is enabled, the session process starts in the Runner's own directory — your build output — and loads your plugin code.

Find the session's process ID in the Runner logs or with your OS process tools and attach your IDE debugger to it.

Visual Studio: Debug > Attach to Process, filter for dotnet processes, select the session process.

JetBrains Rider: Run > Attach to Process, select the session process. Rider run configurations support AUTO_ATTACH_CHILDREN which can automatically attach to the spawned session process.

VS Code: Use the .NET: Attach launch configuration targeting the session process ID.

Once attached, set breakpoints in your plugin source and run a test plan that exercises your plugin. Execution pauses at your breakpoints as expected.

What development mode changes

Development mode adjusts several Runner behaviours to support an interactive debugging workflow:

Behaviour Normal Development mode
Session working directory Deployed image directory (under system temp) Runner's own installation directory (your build output)
Image on StartImageSession Client-specified image is deployed Image is ignored — session always uses the local directory
Session heartbeat timeout 7 seconds 30 minutes
Watchdog termination timeout Client-controlled Always 30 minutes

The extended timeouts prevent the Runner from killing a session that is paused in a debugger. In normal operation, if a session stops sending heartbeats for more than 7 seconds the Runner terminates it. Development mode raises this to 30 minutes so you can step through code without the session being reaped.

The watchdog — which terminates idle sessions after a configurable period — is similarly overridden to 30 minutes regardless of the value set by the client.

IDE run configuration examples

JetBrains Rider / IntelliJ

Create a .NET Project run configuration that builds and launches the Runner with the environment variable set. Enable Auto Attach Children Processes so that Rider automatically attaches the debugger to session processes spawned by the Runner.

<envs>
    <env name="OPENTAP_RUNNER_DEVELOPMENT_MODE" value="1" />
</envs>

Visual Studio (launchSettings.json)

{
  "profiles": {
    "Runner (Dev)": {
      "commandName": "Project",
      "commandLineArgs": "runner start",
      "environmentVariables": {
        "OPENTAP_RUNNER_DEVELOPMENT_MODE": "1"
      }
    }
  }
}

VS Code (launch.json)

{
  "name": "Runner (Dev)",
  "type": "coreclr",
  "request": "launch",
  "program": "${workspaceFolder}/tap.dll",
  "args": ["runner", "start"],
  "env": {
    "OPENTAP_RUNNER_DEVELOPMENT_MODE": "1"
  }
}

Environment variable reference

Variable Values Description
OPENTAP_RUNNER_DEVELOPMENT_MODE 1 or true Enables development mode

Tips

  • Rebuild and restart: After rebuilding your plugin, restart the Runner so the new DLLs are loaded. Sessions started before the rebuild still reference the old assemblies.
  • Single session: In development mode, image resolution is skipped, so all sessions share the same directory and plugin set. Run one session at a time to avoid conflicts.
  • Breakpoints in test steps: Development mode is about attaching a system debugger to the session process. This is separate from the Runner's built-in test plan breakpoints, which pause test plan execution at specific steps without a debugger. Both can be used together.

Security

The OpenTAP Runner is connected to KS8500 through a secure websocket connection. All traffic is encrypted with TLS between KS8500 and the Runner.

A registered Runner is identified and validated using an ed25519 asymmetric key pair, which is generated uniquely to the specific instance of the Runner. The private key never leaves the local machine, and should never be shared with anyone.

When registering a new runner, the user has to paste in a token, that can be retrieved by logging into KS8500 with valid credentials into a specific Keycloak realm.

Key revocation

When a Runner is removed from KS8500, the key is being revoked immediately. This ensures that no traffic between KS8500 and the Runner occurs after removal. The private key is added to a revocation list kept on the server. The Runner needs to be re-registered again in order to communicate with KS8500.

Runner permissions

Per default the Runner is initially registered to a single user, which becomes the owner of the Runner and granted read/write/owner permissions. The owner of a Runner can, however, be share the instance by granting read/write/owner permissions to other users and/or group of users. The fewer users that have access to the runner, the better. Follow the least privilege principle.

Firewall configuration

As all communication to KS8500 is done through a single secure websocket connection, the Runner expects to be able to reach KS8500 on port 443/tcp. Thus only a single ingress firewall rule is required.

Software updates

Ensure to have installed the latest version of the Runner to be on the latest security level. The latest version of the Runner is available here.

Disk encryption

Consider using disk encryption on the system where you plan to use the Runner. This prevents unattended direct access to the content on the disk.

All data concerning the Runner (Test plans, artifacts, results, logs, etc.) will be stored on the device in an amount of time, until it have been transferred to KS8500.

Changelog

Bold indicates highlights

2.2.0

Added unified NewSession API for single-request session orchestration Added RunnerJson API for stable settings serialization in Runner Client

  • Allow specifying Image when UseDefaults is true in NewSession endpoint
  • Acknowledge all NATS messages to JetStream
  • Prevent multiple Runner instances from running simultaneously
  • Publish large messages in chunks in Runner Client
  • Include run Id in NATS message payload for TestRun DTOs
  • Added validation for websocket port CLI argument
  • Added CLI action to adjust NATS log level output
  • Detect NATS server architecture mismatch and show clear error
  • Improved runner registration license error message
  • Throttle step-complete test plan change checks
  • Added python3 and py3-numpy packages to the Docker image
  • Set UsageCount to not be default enabled
  • Made Update Paths Dialog modal and read-only
  • Fixed issue where plugins adapt the default Newtonsoft JSON serialization settings used in the Runner
  • Fixed session stuck in Aborting state due to race condition
  • Fixed events not being received in certain scenarios in Runner Client
  • Fixed null reference exception when the Errors property is null in Settings
  • Fixed null reference exception when initializing a new test step when a required plugin is missing
  • Fixed Windows service auto-restarting after manual stop
  • Fixed issues with auto-starting Windows service when the runner is shutdown, including during Runner update scenario
  • Fixed Windows service using cached status in CLI actions
  • Fixed CLI command to restart service
  • Fixed capitalization in runner service status command
  • Fixed cloud files not being downloadable while in use
  • Fixed wrong JetStream disk space calculation
  • Fixed log argument being ignored
  • Fixed Publish Runs forced check to use base image instead of current installation
  • Fixed Docker volumes for static image
  • Fixed many SettingsChanged events firing during session startup
  • Updated Runner documentation (code examples, API specifications, CLI reference)

2.1.5

  • Fixed null reference exception when initializing a new test step when a required plugin is missing
  • Added python3, pip, and numpy to the Docker image

2.1.4

  • Fixed issues with auto-starting Windows service when the runner is shutdown, including in Runner update scenario

2.1.3

  • Updated CloudDrive package with fixes for test plans with unset FilePath members
  • Fixed issue causing download to fail if file exists and cannot be opened for writing
  • Fixed issue causing partial file if download fails
  • Fixed Publish Runs forced check to use base image instead of current installation

2.1.2

  • Updated CloudDrive package with fixes for test plan load errors when dependencies do not exist

2.1.1

  • Fixed race condition when removing parameters during test plan serialization

2.1.0

Implemented PasteSteps endpoint Implemented EditStatus functionality to track test plan edit state Introduced a session base class for extensibility

  • Updated Metrics and Assets package
  • Update test plan reference after saving with CloudDrive
  • Optimized SetTestPlanName performance
  • Only set capabilities on an empty TestPlan
  • Moved LoggingMiddleware to NatsSessionBase
  • Updated install script with new arguments and capability option
  • Added quiet option to CLI
  • Use MiB and GiB instead of bytes for JetStream info
  • Rename (or download) a file locally if it was renamed on remote
  • Ensure that XDG_RUNTIME_DIR is defined when running systemctl --user commands
  • Fixed issue when setting a test plan with child steps that do not belong to a namespace
  • Forcefully shut down a session process, but give threads time to complete some work
  • Fixed debug mode SetPauseNext requiring multiple calls on subsequent runs
  • Fixed import Sweep values bug
  • Avoid DefaultSettings to be locked
  • Fixed issue with parent step being moved into its own descendant

2.0.0

Implemented new in-process auto-update of the Runner and added version-locked and updatable Docker images Implemented Allow parallel runs functionality Moved Metrics and Assets NATS endpoints to the runner Implemented test plan step undo/redo and move/delete endpoints

  • Updated NATS server to 2.11.9
  • Implemented new handshake RegisterSession endpoint
  • Implemented a wait until Session is ready in key endpoints
  • Send run parameters in normal RunStart/RunCompleted messages instead of separate messages
  • Improved handshake between KS8500 and the runner
  • Exit immediately if KS8500 registration failed
  • Renamed runner registration argument from --registrationToken to --token
  • Changed Runner heartbeats according to new spec
  • Cleaned up unused run metadata
  • Do not send null NATS messages anymore to signal end of data
  • Removed RunnerListService and utilize AutomationCloud instead
  • Caching test plan when test plan changes
  • Added Runs and Metrics JetStream size metrics
  • Changed SessionEvents and introduced SessionState.Loading and WaitingForUserInput
  • Changed KS8500 API endpoints for registering the runner
  • Removed legacy pre-cached test plans functionality
  • Removed legacy OnTestRun and OnResult messages
  • Removed KS8500 ping request and use RRT from NATS instead
  • Removed methods marked as obsolete
  • Ensured a session run lock is released if session is not executing anymore
  • Fixed issue where non-starting sessions reported wrong error
  • Fixed bug where Source was emitted as a context menu item
  • Fixed NullReferenceException when moving a child test step to the root
  • Fixed NotSupportedException when the status request tried to find a local IPv6 address
  • Fixed race condition in TestPlanRunning logic
  • Fixed bug where sessions could get stuck if the console output buffer gets full
  • Fixed issue with wrong session state being set when waiting for input and a test plan is not running anymore
  • Various fixes for Windows service management
  • Filter system wide packages from session image
  • Fixed issue with browsable attribute
  • Fixed wrong session state when test plan failed to start
  • Fixed problem with waiting for input state change only when executing

1.16.1

  • Fixed issue with wrong sequence number being attached to the NATS message for run results

1.16.0

Added status request as both an API endpoint and as a CLI action Added support for building arm64 Docker image

  • Added support for getting and setting DataGrid from a User Input
  • Improved settings and config cleanup when a runner is removed from remote
  • Added component setting for enabling/disabling logging of NATS debug messages
  • Redesigned the save default settings API
  • Fixed upload package error when path and name do not correspond
  • Moved "Publish Artifact" to "Publish Options" group in runner component settings
  • Fixed bug when deleting a non-existing storage directory
  • Fixed DataGrid issue with CSV import where new rows in CSV file resulted in an error
  • Cleaned up Docker image with reduced size of 73MB
  • Added sequence number to NATS messages during test plan execution

1.15.1

  • Gracefully close sessions if a runner is unregistered from KS8500
  • Ensured that installer does not fail if the tap executable does not exist anymore during uninstall/update

1.15.0

Added support for managing the runner as a service through CLI actions Now uses a new Avalonia based installer

  • Updated NATS Server to 2.11.1
  • Added support for MenuModel items that do not have an IconAttribute
  • Set MachineName as part of Asset ID to ensure asset uniqueness
  • Made the Runner able to adopt existing sessions from same runner
  • Ensured the settings package is always treated as a TapPackage
  • Changed Default ImageOverride to actually override resulting image
  • Added all current packages as dependencies of Settings package
  • Fixed bug where wrong setting could be changed in a DataGrid
  • Fixed a bug in image trimming that could result in unexpected packages in created image
  • Fixed regression with sessions missing subject
  • Fixed permission error when requesting zip logs on Windows

1.14.1

  • Auto-attaching sessions on Windows using the same launch profile for both Linux and Windows
  • Added metric names and log NATS request context on exceptions
  • Log leaf node output in debug level
  • Fixed CultureInfo exception in Docker image
  • Updated Cloud Drive dependency to version 0.6.4

1.14.0

Added new API endpoint for retrieving all logs as a single zip

  • When loading a test plan, do not request for user input
  • Fixed side effects update in data grids
  • Ensured that remote user inputs are not requested from Runner itself
  • NATS requests to sessions are now logged in the Runner log

1.13.0

Added support for ComponentSettingsLayoutAttribute Added support for auto generating Rows in DataGrids

  • Added IgnoreBrowsableAttribute support to GetSettings and GetComponentSettings
  • Added OPENTAP_RUNNER_SESSION_ID environment variable to session process
  • Added ItemTypeDisplayName to the DTO for a ComponentSettingsList
  • Fixed preserving repositories in default image
  • Added logic to allow constructing component settings list elements from non plugin types
  • Fixed Name property handling for ComponentSettingsListItem without a setter
  • Correctly update other properties that might have changed due to a property change with side effects
  • Fixed a null reference timing exception when removing a Test Plan Parameter
  • Use typedata name instead of dotnet type name for ValueType
  • Fixed issue where nested data grid changes were not applied
  • Bumped NATS Server to 2.10.25

1.12.0

Added Runner Shutdown endpoint and CLI command Added support for streaming artifacts over NATS

  • Added Test Plan Settings as Metadata to TestPlan when saving to Repository
  • Added TestPlan/PathId run parameter
  • Fixed datagrid control index bug
  • Fixed User Inputs on Test Plan load
  • Fixed removal of old config files and JetStream storage when unregistering/re-registering a Runner
  • Fixed duplicate capability entries bug
  • Fixed race condition between SetImage and StartSession with an Image
  • Resolve images using only package cache first and fall back to remote Repositories
  • Output the failed Session start logs

1.11.3

  • Improvements to load packages from local cache if possible
  • Skip user input interaction when loading a test plan
  • Use property names instead of indexes when modifying a data grid

1.11.2

  • Fixed use of correct stream name for logs

1.11.1

  • Unregistering a runner now also always removes default settings and image
  • Signal that a metric measurement is disabled by publishing a null data message
  • Toggle between fast and reliable data transmission based on available storage
  • Fixed serialization of non-integer result parameter enum types

1.11.0

Added configuration options for publishing runs, results, and logs

  • NATS leaf node server is now restarted when detecting a permission violation
  • Support of PathID in Cloud Drive
  • Changed default metric lifetime to 1 hour
  • Metrics are published on simpler subject
  • Return the correct metrics configuration
  • Fixes to concurrency in Runner metrics in heartbeat
  • Replaced illegal characters from NATS subject where the subject is dynamically generated
  • Fixed issue with "untitled" test plan name
  • Wait for inactive sessions to shutdown when doing a graceful shutdown of the runner
  • Publish MetadataUpdated event when a session's metadata was updated

1.10.2

  • SSDF Compliance from a CI build perspective

1.10.1

Added support for Offline Runs using JetStream Added support for Metrics

  • Added "SetSession" endpoint on Runner to set Metadata on Session object
  • Added runner settings to modify Runs and Metric streams
  • Upgraded NATS Server to version 2.10.18
  • Made SessionDetails request more tolerant of Settings load error
  • Improved ComponentSettings AddItem error message
  • Validated arguments to help user avoid registering with http instead of https
  • Fixed bug that prevented successful Runner shutdown
  • Cleanup in Runner if a session process does not exist anymore
  • Upgraded dependencies and OpenTAP to ensure SSDF compliance

1.9.2

  • Fixed KS8500 Built-in runner register bug

1.9.1

  • Set Docker health check URL to localhost IP
  • Chunk log messages to chunks of 50 to avoid 'maximum payload exceeded' error
  • Changed to use PATCH metadata
  • Prevent error if a specified CloudDrive file already exists on disk

1.9.0

Provide ping metrics and migrate to .NET 8

  • Migrated Runner Docker image to .NET 8
  • Added settings output in logs
  • Added TestStepName to OnTestStepRun
  • Changed strategy for providing UTC timestamps
  • Implemented ping metrics (round trip time to KS8500)
  • Unbrowsable steps are now shown as available child step types in GetStepTypes
  • Fixed PlanRunLogs DTO bug
  • Included milliseconds in datetime parameters
  • Fixed HistoryLogs timestamp and added integration tests
  • Fixed wrong log count in live logs
  • Canceled HandshakeCancellationToken when interrupting Runner process
  • Fixed race condition bug in GetStatus endpoint

1.8.0

Implemented support for storing TestPlans on the Runner Added granular events to OnTestPlanRun Added TestPlanRunLogs

  • Added DeleteOnExpiration and TimeSpan to UserToken generation
  • When SyncLogsManager initializes, previous logs are added
  • Updated documentation with default endpoints
  • Removed hardcoded DEV URL to fix unregister
  • Improved unregister flow
  • Added verbose logs from LeafNode
  • Fixed error when removing a parameter
  • Fixed bug where "Cancel" would still remove configuration files when unregistering
  • Fixed bug where tokens are not updated in OpenTAP
  • Added granular publishing of test plan run data
  • Ensured Config directory exists when re-registering
  • Added TypeCode to Parameter DTO

1.7.1

  • Fixed Runner unregister issue

1.7.0

  • Added support for packaging Python settings types
  • Added RepositoryPackageReference normalization
  • Bumped Runner Service and NATS Server dependencies
  • Added more messages to the connection status

1.6.3 (Backport)

  • Fixed Runner unregister issue

1.6.2

  • Added workaround for ReadOnly TestSteps not having ReadOnly properties

1.6.1

  • Fixed issue with files being used for image resolution

1.6.0

  • Added RunnerMocker program
  • Added PluginTypeSelector control
  • Fixed issue where not started sessions would shut down due to heartbeat timeout
  • Filtered ContextMenu on IconAnnotation to avoid unwanted entries
  • Added JSON accept header to all repository communication
  • Consider watchdog inactive if TerminationTimeout is set to under 1, by always returning 0 in InactiveSeconds
  • Added ItemType to ComponentSettingsList DTO

1.5.5 (Backport)

  • Fixed Python dependencies issue

1.5.4 (Backport)

  • Fixed issue with files being used for image resolution

1.5.3

  • Fixed heartbeats

1.5.2

  • Threw exception if UserToken can't be created
  • Added context to exception message
  • Added image override repositories to resolver

1.5.1

  • Fixed "downgrade to same version" bug in UpdateRunner request

1.5.0

Added support for uploading Artifacts to KS8500 Added support for starting default session with an image overriding packages

  • Various bug fixes, refactoring, and performance optimizations

1.4.3

  • Fixed error where NatsCredentials was overwritten with null, breaking the KS8500 built-in runners auto-register
  • Always published lifetime started event

1.4.2

  • Added NATS credentials rotation on handshake and removed re-register on startup of Runner
  • Drained connection before close to ensure StoppedLifeTimeEvent is sent

1.4.1

  • Fixed duplicate NATS headers issue
  • Improved performance of GetSettings
  • Made auto-update improvements and removed Runner restart after update
  • Minor UX improvements
  • Switched to always use UserId as StartedBy information

1.4.0

Added local WebSocket listening on the NATS leaf node server Supported storage of base image, default settings, and enabled session creation based on TestPlan reference

  • Added ability to re-register
  • Set Cloud Drive Realm based on handshake
  • Added system account to query server name
  • Fixed issue with DirectoryPathControl
  • Fixed bug with installer making the Runner stay in Registered mode
  • Stopped and started the service in installer
  • Added more descriptive error to failed registration
  • License exception on adding ComponentSettings now produces an error log
  • Fixed the FailedToStart state error when running twice
  • Implemented ResolveImageDryRun
  • Ensured TestPlanChangedEventArgs is emitted always when TestPlan might have changed
  • Modified OpenTAP log messages format for better readability in Loki
  • Enhanced Docker file permissions
  • Added a default value for Capabilities metadata
  • Refactored Session Watchdog check

1.3.1

  • Fixed DirectoryPathControl serialization bug

1.3.0

  • Added --server argument to specify direct NATS URL
  • Added heartbeat support on MacOS
  • Button presses no longer block UI
  • Runner asks to exchange JWT for Repo Token for sessions
  • Bumped OpenTAP dependency to 9.21.1 to support User Tokens
  • Added DirectoryPathControl

1.2.3

  • Added TestPlanJson as run parameter
  • Created a FileWatcher to shut down runner at register/unregister

1.2.2

  • Added default run parameters

1.2.1

  • Added default parameters to RunTestPlan to support local Runner TestPlan runs saved to KS8500

1.2.0

Added support for remotely updating the Runner installation Persisted Runner system logs (Serilog) to disk at the same location as OpenTAP logs

  • Corrected Session TestPlanRunId value
  • Increased session startup timeout to 3 minutes
  • Added "TestPlanName" to Session object
  • Stopped publishing static metrics
  • Fixed parameterized Dialog verdict bug
  • Summed CPU & Memory usage of Runner, NATS, and Sessions
  • Added safeguards to parse partially corrupt logs

1.1.0

Implemented Runner live TestPlan execution updates Supported Hybrid (online/offline) mode

  • Supported DirectoryPathAttribute by introducing a DirectoryPathControl
  • Changed logs to have UTC tick timestamps
  • Changed parameter DateTime types to UTC
  • Various bug fixes and improvements

Contact

OpenTAP Runner Support: Official support channel

Lead developer: Dennis Rasmussen

OpenTAP Manager: Janus Faaborg