@andrian.yablonskyy/thub-common
Shared code for TestHub — a self-hosted job network that lets CI/CD pipelines and individual developers run firmware tests on real hardware or emulators in a private lab. This package has no server and no CLI of its own; it's the small, dependency-light core that the Agent, Coordinator and Client all depend on, so the three speak exactly the same protocol.
Install
npm install @andrian.yablonskyy/thub-common
What's in it
Job spec schema and validation
An Ajv JSON schema for the job specification the Agent sends to the Coordinator, and a thin wrapper that validates a spec and fills in its defaults.
const { validateJobSpec } = require('@andrian.yablonskyy/thub-common');
const { valid, spec, errors } = validateJobSpec({
target: { type: 'hw', labels: ['board:nucleo-f401re'] },
command: './ci/test.sh',
downloads: [{ url: 'https://artifactory.example.com/app.bin' }],
git: { url: 'https://github.com/yourorg/firmware-tests.git', ref: 'v1.4.0' }
});
A job is a task: command (required — the shell command the Client runs as its entry point, args as "$@"), with optional inputs downloads ([{ url }]), git (url, optional ref — branch, tag or commit — and depth, default 1) and, for SW jobs, image (a Docker image to run as the DUT). Other top-level fields: target (type, labels, optional group/client), suite, timeoutSec, priority, source (ci/cli), user (a free-text job-owner label), meta (arbitrary key/value metadata), and dryRun. A spec in the old shape (firmware/tests, from an Agent older than --command) is refused with a message to update the Agent.
State enums and exit codes
const { RESOURCE_STATES, JOB_STATES, ACTIVE_JOB_STATES, TERMINAL_JOB_STATES, BUSY_SOURCES, EXIT_CODES, exitCodeForJobState } = require('@andrian.yablonskyy/thub-common');
RESOURCE_STATES:REGISTERED,IDLE,BUSY,OUT_OF_SERVICE,MAINTENANCE.JOB_STATES:QUEUED,ASSIGNED,PREPARING,RUNNING,PASSED,FAILED,ERROR,TIMEOUT,CANCELED,LOST.ACTIVE_JOB_STATES/TERMINAL_JOB_STATESare the two natural partitions of that set.BUSY_SOURCES:ci,cli,local— why a resource is currently busy.EXIT_CODES/exitCodeForJobState(state): the Agent CLI's process exit codes (PASSED→ 0,FAILED→ 1,ERROR/TIMEOUT/LOST→ 2,CANCELED→ 3, usage/auth error → 4, detached with Ctrl-C → 130), so a CI step can branch on$?without parsing output.
API client
A minimal fetch-based HTTP client for the Coordinator's /api/v1 surface, with a hand-rolled Server-Sent-Events parser (no external SSE dependency) — used by both the Agent and the Client so they speak the exact same protocol.
const { ApiClient } = require('@andrian.yablonskyy/thub-common');
// userAgent is optional; the Coordinator records "thub-agent/<v>" / "thub-client/<v>" versions for its dashboard
const client = new ApiClient({ baseUrl: 'https://thub.example.com', token: 'agt_...', userAgent: 'thub-agent/1.0.8' });
const job = await client.post('/jobs', spec);
await client.streamEvents(`/jobs/${job.jobId}/logs/stream`, {
onEvent: ({ event, id, data }) => console.log(event, data)
});
request()/get()/post() handle JSON bodies, query strings and non-2xx errors (throwing with .status/.body set); streamEvents() reconnects the caller is expected to drive (it just parses one connection's worth of events and resolves when the stream ends) and honors Last-Event-ID for resuming after a drop.
Reserved resource remote channels
The Agent, Coordinator and Client share a WebSocket channel protocol for reserved resources. Besides UART, SWD, RTT and video channels, a holder can request a named service channel with a Client port. RTT opens an OpenOCD session over the selected ST-Link, searches a configured target RAM range for the SEGGER control block, and streams one up-channel. The Client connects a service channel only to 127.0.0.1:<port> on its own host; it never exposes a Client-side listener to the network.
Version checks and self-update helpers
PACKAGES (the Coordinator/Agent/Client npm names), fetchLatestVersion(pkg, { registry }) (the latest dist-tag from the npm registry), compareVersions/isNewer/isValidVersion (strict X.Y.Z[-pre]), npmBin() (the npm next to the running node) and npmInstallGlobal(pkg, version) (only for the three TestHub packages, only a valid version).
Development
npm install
npm test # node --test test/*.test.js
npm run lint
License
See LICENSE.md.