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.
$ 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:
{"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}
| Field | Type | Notes for fixtures |
|---|---|---|
event_id | string | Any UUID. Replay does not dedupe on it; the executor dedupes on action idempotency keys. |
reseller_id | string | null | null for Fibric-direct. Present on every envelope, fixtures included. |
tenant_id | string | Use t_local in dev. The harness refuses fixtures whose tenant does not match the local tenant, for the same reason production does. |
source / event_type | string | What the router matches operator triggers against, for example order.*. |
correlation_id | string | Ties the envelope, the plan, and the receipts together in output. |
payload | Record<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:
# 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:
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');
});
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:
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:
$ 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.
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
- Tools & auth: sandbox vs live connections, and why dev-mode stubs are safe by default.
- The event envelope: the full field reference behind the fixture format.
- Trust tiers: the policy semantics
evaluate()implements. - Publishing to the marketplace: what review checks beyond your own CI.