Connector testkit

harn_vm::connectors::testkit is the shared fixture surface for Harn core and connector package tests. It keeps connector tests deterministic without live provider credentials, external services, or wall-clock sleeps.

Connector crates should add harn-vm as a dev-dependency and import only the testkit pieces they need:

use harn_vm::connectors::testkit::{
    ConnectorTestkit, HttpMockGuard, HttpMockResponse, MemorySecretProvider,
    github_ping_fixture, notion_page_content_updated_fixture, scoped_secret_id,
};

Runtime context

ConnectorTestkit::new(start) builds a complete ConnectorCtx backed by:

  • a memory event log
  • an inbox index and metrics registry
  • a versioned in-memory SecretProvider
  • a rate limiter
  • a mock clock that can be installed for the current test thread
let kit = ConnectorTestkit::new(start).await;
let _clock = kit.install_clock();
connector.init(kit.ctx()).await?;

Use kit.clock.advance_std(...) or advance_until(...) to drive deadlines, retry backoff, cron ticks, and cancellation logic without sleeping.

Secrets

MemorySecretProvider supports latest and exact versions. The scoped_secret_id(namespace, tenant, binding, name) helper gives connector tests a consistent tenant/binding naming convention:

let secret_id = scoped_secret_id("github", "tenant-a", "binding-a", "token");
let secrets = MemorySecretProvider::new("github").with_secret(secret_id, "token-v1");

Use this for webhook signing keys, outbound tokens, and token-refresh tests.

HTTP

HttpMockGuard drives the same mock registry used by Harn http_request and the http_mock(...) builtins. Script-level and Rust-level assertions therefore observe the same calls:

let http = HttpMockGuard::new();
http.push(
    "GET",
    "https://api.example.com/*",
    vec![HttpMockResponse::new(200, r#"{"ok":true}"#)],
);

let calls = http.calls();

The guard clears HTTP mock state on creation and drop.

Streams and webhooks

mock_stream() returns a handle and reader for deterministic stream tests. Send JSON or bytes through the handle, then call cancel() to prove stream shutdown without a network socket.

Webhook fixtures cover common first-party shapes:

  • github_ping_fixture(secret, received_at)
  • slack_message_fixture(secret, timestamp, received_at)
  • linear_issue_update_fixture(secret, received_at)
  • notion_page_content_updated_fixture(secret, received_at)

They return a WebhookFixture containing the signed RawInbound and original body bytes. Use .with_binding(...) and .with_tenant(...) to attach runtime metadata before normalization.

Temp packages

TempPackageWorkspace creates a disposable package root and writes common manifest markers for package-manager and conformance tests:

let workspace = TempPackageWorkspace::new("connector-contract")?;
workspace.write_harn_package("demo-connector")?;
workspace.write_file("src/main.harn", "pipeline main(task) {}")?;

The directory is removed when the workspace value drops.