Test Without Credentials
The complete no-credential story — mock model, single-file runs, stub sessions, mock sandboxes, and evals.
Everything on this page runs offline with zero API keys. Reach for a real model only when the behavior under test depends on the model itself.
Mock model from the CLI
--mock injects the deterministic mock provider into fh run and fh dev. Discovery, input/output schema validation, the model loop, and HTTP routing all still execute:
fh run hello --name Preetham --mock
# Mock response: Say hello to Preetham.
fh dev --mock --port 3000
curl http://localhost:3000/jobs/hello \
-H 'content-type: application/json' \
-d '{"name":"Preetham"}'
# {"result":"Mock response: Say hello to Preetham.","runId":"…"}Single-file runs auto-mock
fh run executes any standalone .ts/.js file that exports defineAgent() — no .fabricharness/ workspace, no config.ts, no install step beyond a resolvable @fabric-harness/sdk:
import { defineAgent, schema } from '@fabric-harness/sdk';
export default defineAgent({
name: 'hello',
input: schema.object({ message: schema.string() }),
output: schema.string(),
run: async ({ init, input }) => {
const fabric = await init();
const session = await fabric.session();
return session.prompt(input.message);
},
});fh run ./agent.ts --message "hi"
# [fabric-harness] no model credentials resolved; using the mock model (set FABRIC_MODEL or pass --model for a real provider).
# [fabric-harness] run hello-4edbadf5-…
# Mock response: hiWhen no model credentials resolve (--model, FABRIC_MODEL, or provider keys), the run falls back to the mock model and says so on stderr, so the command round-trips offline. Pass --model (or set FABRIC_MODEL) with a configured provider to run the same file against a real model. See fh run for multi-export files, payload flags, and error behavior.
MockModelProvider in code
@fabric-harness/sdk/testing exports the same provider the CLI uses:
import { MockModelProvider } from '@fabric-harness/sdk/testing';
const provider = new MockModelProvider();
const response = await provider.generate({
messages: [{ role: 'user', content: 'ping' }],
});
// response.message.content === 'Mock response: ping'The mock is deterministic: it echoes the latest user message as Mock response: <text> and summarizes tool results after tool turns. To exercise a tool loop, script tool calls in the prompt with a fabric-tool-calls fenced block:
const response = await provider.generate({
messages: [{
role: 'user',
content: 'run the tool\n```fabric-tool-calls\n[{"tool":"lookup_order","input":{"id":"A-1"}}]\n```',
}],
});
// response.toolCalls === [{ id: 'mock-tool-1', name: 'lookup_order', input: { id: 'A-1' } }]The
/testingentry point is shaped for test ergonomics and is not covered by the runtime SemVer commitment. Production code should not import from it.
Stub sessions for unit tests
StubFabricAgent and StubFabricSession are the in-process implementations behind init(). Construct them directly with the mock model to unit-test agent logic without a workspace, server, or credentials:
import { describe, expect, it } from 'vitest';
import { StubFabricAgent } from '@fabric-harness/sdk/testing';
describe('support agent', () => {
it('answers through the session contract', async () => {
const agent = new StubFabricAgent({ model: 'mock/test-model' });
const session = await agent.session('demo');
const reply = await session.prompt('hello from the test');
expect(reply).toBe('Mock response: hello from the test');
});
});For workspace-level runs, runAgent({ agent, payload, mock: true }) from @fabric-harness/node covers the same ground through the full CLI pipeline — see Testing locally.
Mock sandbox handles
Remote sandbox adapters in @fabric-harness/connectors are structural: they adapt a provider SDK handle to the Fabric SandboxEnv contract. That makes the provider handle mockable — back it with the SDK's in-memory EmptySandboxEnv and the real adapter runs end-to-end offline:
import { EmptySandboxEnv } from '@fabric-harness/sdk';
const inner = new EmptySandboxEnv('/home/daytona');
await inner.writeFile('/home/daytona/welcome.txt', 'hello from the mock daytona sandbox\n');
// expose inner's readFile/writeFile/exec/… through the provider's handle shapeThe canonical pattern lives in examples/remote-coding-agent (connectors/mock-remote.ts adapts EmptySandboxEnv to the RemoteSandboxApi contract), and each provider example ships its own structural mock with the live path gated behind an opt-in flag:
| Example | Mock handle | Live opt-in |
|---|---|---|
with-daytona | connectors/mock-daytona.ts | FABRIC_DAYTONA_LIVE=1 + DAYTONA_API_KEY |
with-e2b | connectors/mock-e2b.ts | FABRIC_E2B_LIVE=1 + E2B_API_KEY |
with-modal | connectors/mock-modal.ts | FABRIC_MODAL_LIVE=1 + Modal credentials |
with-kubernetes | connectors/mock-kubernetes.ts | FABRIC_K8S_LIVE=1 + kubeconfig |
with-vercel-sandbox | connectors/mock-vercel.ts | FABRIC_VERCEL_LIVE=1 + VERCEL_TOKEN |
with-local-shell | local sandbox directly | not needed — runs on the host |
Each example's default pnpm run run executes against the mock with no credentials; the live path fails fast with a clear error when the flag is set without its credentials. See Sandbox connectors for the adapter contracts.
Evals with fh test
Eval suites are TypeScript modules named **/*.eval.ts. Run the agent with mock: true inside the suite runner and the whole suite is deterministic and offline:
import { containsTextScorer, defineEvalSuite } from '@fabric-harness/evals';
import { runAgent } from '@fabric-harness/node';
export default defineEvalSuite({
name: 'hello-quality',
cases: [
{ id: 'named-user', input: { name: 'Ada' }, expected: 'Ada' },
],
runner: async ({ case: evalCase }) => {
const run = await runAgent({ agent: 'hello', payload: evalCase.input, mock: true });
return run.result;
},
scorers: [containsTextScorer()],
passThreshold: 1,
});fh test
# Eval Results
#
# hello-quality PASS 100% 39ms
# PASS named-user contains_text=1.00
#
# Overall: PASS@fabric-harness/evals ships deterministic scorers — exactMatchScorer, containsTextScorer, regexMatchScorer, jsonShapeMatchScorer — plus llmAsJudgeScorer for model-graded checks (the one scorer that needs a real provider). fh test exits non-zero when any suite fails, so the same command gates CI. See Evaluations and fh test.
When to add credentials
Everything above stays green with no keys. Add a provider key in .env.local when you are validating real model behavior, then drop --mock:
echo 'OPENAI_API_KEY=sk-...' > .env.local
fh doctor --live --model openai/gpt-5.5
fh run hello --name PreethamSee Model providers for supported providers and credential resolution, and Live tests for the opt-in environment variable matrix used by the repo's own live suites.