Fibric. Docs fibric.io →
v1.0.0 · stable
Build

Testing connectors

Fibric's architecture makes connector testing unusually tractable: proposals are data, disposition is deterministic, and the two are separable. You can replay a recorded envelope through an operator, assert on the exact ExecutionPlan it proposes, and simulate the trust policy's verdict on every action, all without a single side effect reaching a real system. This page covers the local harness, the fixture format, plan assertions, trust-tier simulation, and a CI recipe.

The local harness: fibric dev

fibric dev runs your connector or operator against a local kernel: an in-process event bus, an in-memory executor, and a file-backed secret stub. Nothing leaves your machine unless you explicitly create a sandbox connection. The harness watches your source, reloads on change, and prints every envelope, plan, and disposition as it happens.

terminal
$ fibric dev
  fibric dev 0.9  ·  local kernel  ·  tenant t_local
  loaded connector cn-brightdesk@1.0.0 (2 tools, 3 events)
  loaded operator  order-risk (requires orders.read, orders.hold, notify.send)
  executor: in-memory, policy from ./policy/trust.yaml
  watching ./connectors ./operators ./fixtures

  [14:02:11] envelope  order.created  src=fixture  corr=9f31
  [14:02:12] plan      order-risk     2 actions    (proposed, not disposed)
  [14:02:12] dispose   order.hold     ALLOW        key=order-risk:SO-10884:hold
  [14:02:12] dispose   notify.send    ALLOW        key=order-risk:SO-10884:notify
  [14:02:12] receipt   2 written -> .fibric/dev/receipts.jsonl

Side-effecting handlers do not fire in dev mode unless you opt a connection in. By default the executor runs the full pipeline, validation, policy, single-flight, dedup, and records the disposition, but replaces the handler call with a stub that echoes the args. That means dev-mode receipts are structurally identical to production receipts, which is what makes them assertable.

Fixture format

A fixture is one or more recorded event envelopes, stored as JSONL under ./fixtures. The shape is exactly the kernel's EventEnvelope, no test-only fields:

fixtures/order-created.jsonl
{"event_id":"6a1e0c2f-6c1a-4f0e-9f31-b7d02f6e8c11",
 "reseller_id":null,
 "tenant_id":"t_local",
 "workspace_id":null,
 "source":"magento",
 "event_type":"order.created",
 "correlation_id":"9f31c4d8-2b7e-4a55-8d20-1f0a9e3b6c77",
 "payload":{"order_id":"SO-10884","promise_date":"2026-07-04","total":412.50},
 "agent_id":null,
 "session_id":null}
FieldTypeNotes for fixtures
event_idstringAny UUID. Replay does not dedupe on it; the executor dedupes on action idempotency keys.
reseller_idstring | nullnull for Fibric-direct. Present on every envelope, fixtures included.
tenant_idstringUse t_local in dev. The harness refuses fixtures whose tenant does not match the local tenant, for the same reason production does.
source / event_typestringWhat the router matches operator triggers against, for example order.*.
correlation_idstringTies the envelope, the plan, and the receipts together in output.
payloadRecord<string, unknown>The event body your connector emitted, or a hand-written equivalent.

The best fixtures are recorded, not written. Any envelope that has flowed through a workspace can be exported and dropped into ./fixtures: fibric receipts export --envelopes --since 7d emits replayable JSONL with payloads intact and tenant identifiers rewritten to t_local.

Replaying envelopes

Replay pushes fixture envelopes through the local router, which matches them to operator triggers exactly as production would, glob semantics included:

terminal
# replay one fixture file through whatever operators match
$ fibric dev replay fixtures/order-created.jsonl
  1 envelope  ·  matched: order-risk (trigger order.*)
  plan proposed: 2 actions  ·  written to .fibric/dev/plans/9f31.json

# replay a whole directory, propose-only (nothing disposed)
$ fibric dev replay fixtures/ --propose-only
  14 envelopes  ·  9 plans proposed  ·  0 disposed

With --propose-only the run stops at the plan. This is the mode most tests want: the model (or a stubbed reasoner) proposes, the plan lands on disk as JSON, and your test asserts on it. Nothing was validated against policy, nothing acquired a lock, nothing acted.

Asserting on proposed plans

A proposed plan is plain data in the kernel's ExecutionPlan shape: an optional reasoning string and an actions array of PlannedAction. Because it is data, you assert on it with your ordinary test runner. The SDK exposes the harness programmatically for exactly this:

tests/order-risk.test.ts
import { test, expect } from 'vitest';
import { devKernel } from '@fibric/connector-sdk/testing';
import orderRisk from '../operators/order-risk';
import fixture from '../fixtures/order-created.json';

test('holds an at-risk order, exactly once, with stable keys', async () => {
  const kernel = devKernel({ operators: [orderRisk] });

  // propose only: the plan is returned, nothing is disposed
  const plan = await kernel.propose(fixture);

  expect(plan.actions).toHaveLength(2);

  const [hold, notify] = plan.actions;
  expect(hold.tool).toBe('order.hold');
  expect(hold.args.order_id).toBe('SO-10884');

  // idempotency and single-flight keys are part of the contract:
  // assert them, so a refactor cannot silently change dedup behavior
  expect(hold.entity_key).toBe('order:SO-10884');
  expect(hold.idempotency_key).toBe('order-risk:SO-10884:hold');
  expect(notify.idempotency_key).toBe('order-risk:SO-10884:notify');
});
+
Treat keys as API

The entity_key and idempotency_key an operator emits are behavioral contracts: change them and yesterday's dedup no longer applies to today's retries. Pin them in tests the way you would pin a wire format.

Trust-tier simulation

The second half of a good test suite exercises disposition. The kernel's evaluate() is a pure function from policies, action, and envelope to a TrustDecision (ALLOW, BLOCK, or ALERT), so simulating your policy against a plan requires no mocks and no network:

tests/policy.test.ts
import { test, expect } from 'vitest';
import { evaluate } from '@fibric/kernel';
import policies from '../policy/trust';   // TrustPolicy[]

const env = { /* fixture envelope */ } as any;

test('refunds above the ceiling are blocked, fail-closed', () => {
  const refund = {
    connector: 'cn-brightdesk', tool: 'order.refund',
    args: { order_id: 'SO-10884' }, value: 250,
    entity_key: 'order:SO-10884',
    idempotency_key: 'order-risk:SO-10884:refund',
  };
  // policy allows order.refund with maxValue: 100
  expect(evaluate(policies, refund, env)).toBe('BLOCK');
});

test('a tool no policy mentions is blocked by default', () => {
  const rogue = { ...baseAction, tool: 'order.delete' };
  expect(evaluate(policies, rogue, env)).toBe('BLOCK');   // no match => BLOCK
});

End-to-end disposition, including single-flight and dedup, is testable the same way through the harness. Run the same plan twice and the second disposition comes back DEDUP with the handler untouched, which is the exact production behavior you are relying on:

terminal
$ fibric dev replay fixtures/order-created.jsonl --twice
  run 1  order.hold   ALLOW   handler stubbed, key recorded
  run 2  order.hold   DEDUP   handler not called
  assertion surface: .fibric/dev/receipts.jsonl (2 records)

CI recipe

A connector repository needs three gates in CI: types compile, plan and policy tests pass, and the def validates against the marketplace contract. None of them require credentials, because nothing in the suite touches a live system.

.github/workflows/connector-ci.yml
name: connector-ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci

      # gate 1: the def and handlers compile
      - run: npx tsc --noEmit

      # gate 2: plan assertions + trust simulation (no credentials needed)
      - run: npx vitest run

      # gate 3: replay the fixture corpus propose-only; fail on any error
      - run: npx fibric dev replay fixtures/ --propose-only --strict

      # gate 4: the def satisfies the marketplace contract
      - run: npx fibric connectors test ./ --contract

Publishing itself stays out of CI for pull requests; it is a release step, covered on the publishing page. If you do publish from CI, the token you mint is tenant-scoped like any other, and a policy veto during a smoke test exits with its own code so the pipeline can distinguish fail-closed from broken.

Keep going