Run deletion called the driver's `provider.delete(id)` under the run's `petri.run` scope, a delete of Fabro's own over a sandbox whose lease record Petri owns. It now goes the way `petri sandbox prune` goes: `fabro_petri::prune` builds the run's Petri runtime over the server's store (the run key, the run directory, the sandbox backend) and calls Petri's prune, which opens the run for writing, checks each lease's provider fingerprint, writes the delete intent and the tombstone beside the run's other records, and lets each provider remove its managed workspace, a host workspace included. A run a live process holds answers 409 unless the delete is forced; a lease Petri could not prune answers 409 with the problem text, or is warned and skipped under force or a delete that already started. The server drops the worker's handles before the prune, on the store instance the prune opens, so the lease a stopped worker held is released first. The projection reads only the coordinator and execution logs, so the resource records change nothing it reports. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
29 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build and test commands
Rust
cargo build --workspace— build all cratescargo nextest run --workspace— run all unit testscargo nextest run -p fabro-server— test a single cratecargo nextest run -p fabro-workflow -- test_name— run a single testset -a && source .env && set +a && cargo nextest run --workspace --profile e2e --run-ignored only— run all E2E live tests (requires credentials in.env, see.env.example)set -a && source .env && set +a && cargo nextest run -p fabro-llm --profile e2e --run-ignored only— run E2E tests for a single cratecargo +nightly-2026-04-14 fmt --check --all— check formatting (pinned nightly required for rustfmt config; CI uses the same date)cargo +nightly-2026-04-14 fmt --all— auto-formatcargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings— lint (CI runs nightly clippy to match; install withrustup toolchain install nightly-2026-04-14 --profile minimal --component clippy,rustfmt)
macOS note: if cargo nextest run fails with Too many open files (os error 24) / EMFILE, raise the shell's soft FD limit before running tests, for example ulimit -n 4096 && cargo nextest run --workspace. Some terminals and inherited agent sessions start with ulimit -n 256, which is too low for the shared CLI test daemon under parallel nextest load.
TypeScript (fabro-web)
cd apps/fabro-web && bun run dev— rebuild web assets on change for the Rust server; refresh the browser manuallycd apps/fabro-web && bun test— run testscd apps/fabro-web && bun run typecheck— type checkcd apps/fabro-web && bun run build— production build (writes toapps/fabro-web/dist/only; does NOT update the bundled SPA that ships in the Rust binary)cargo dev build [-- <cargo args>]— refreshes the embedded SPA assets from the production build, verifies SPA asset budgets, and then runscargo buildwith forwarded args. The embedded assets are gitignored except for.gitkeep; use this when building a Rust binary that should include a populated SPA bundle.bun run devfor local development is unchanged because debug builds preferapps/fabro-web/dist/on disk via the server fallback.
Docker image
cargo dev docker-build— builds the local Docker image from the current tree using the release pipeline's cargo-zigbuild approach. Honors--arch amd64|arm64,--tag <name>(defaultfabro-sh/fabro),--compile-only(stagestmp/docker-context/<arch>/fabrowithoutdocker build), and--dry-run(prints the Docker commands without running them). Prefer this over writing a throwaway Dockerfile; the release pipeline,Dockerfile, and this command share the same binary layout.
Docker sandbox provider
- Docker is the default runtime sandbox provider from
defaults.toml. The Fabro process must have a working Docker client environment (DOCKER_HOST, socket access, Docker Desktop behavior, TLS settings, groups/permissions, and any remote daemon policy are operator responsibilities). - The packaged compose service mounts
/var/run/docker.sockso the server can create sibling run containers on the host daemon. This is host-root-equivalent under Docker's security model; only use it in the trusted, single-tenant deployment model described by the sandbox code/docs. - Fabro no longer clones a repository into a sandbox: the engine prepares
every run's checkout.
CloneRequeststill travels beside the sandbox spec so the run record names the origin and branch; fabro validates it (a pin needs a branch, a non-GitHub origin needsskip_clone) and refuses a request that asks for a clone. Preflight andfabro execinitialize sandboxes withCloneRequest::none(), which creates an empty workspace.
Release automation
cargo dev release— creates the next stable release tag. Usecargo dev release --nightlyfor a nightly prerelease. Use--dry-runto print planned commands without mutating git or running Cargo,--skip-testsonly after running the release-mode smoke yourself, and--release-date YYYY-MM-DDorFABRO_RELEASE_DATEfor deterministic version computation.
Marketing site (apps/marketing)
cd apps/marketing && bun run dev— start Astro dev servercd apps/marketing && bun run build— production buildcd apps/marketing && bunx vercel --prod— deploy to Vercel (project: website, domain: fabro.sh)
Dev servers
fabro server start— starts the Rust API server (demo mode is per-request viaX-Fabro-Demo: 1header)cd apps/fabro-web && bun run dev— rebuilds web assets on change; refresh the browser manually- Mintlify docs dev server (requires Docker —
mintlify devneeds Node LTS which may not match the host):
Then open http://localhost:3333. Stop withdocker run --rm -d -p 3333:3333 -v $(pwd)/docs/public:/docs -w /docs --name mintlify-dev node:22-slim \ bash -c "npx mintlify dev --host 0.0.0.0 --port 3333"docker stop mintlify-dev.
API workflow
The OpenAPI spec at docs/public/api-reference/fabro-api.yaml is the source of truth for the fabro-api HTTP interface.
- Edit
docs/public/api-reference/fabro-api.yaml cargo build -p fabro-api— build.rs regenerates Rust types and client via progenitor- Write/update handler in
lib/apps/fabro-server/src/server.rs, add route tobuild_router() cargo nextest run -p fabro-server— conformance test catches spec/router driftcd lib/packages/fabro-api-client && bun run generate— regenerates TypeScript Axios client
API type ownership
- Treat OpenAPI as the source of truth for the wire contract, not as the automatic owner of Rust types.
- Before adding or keeping a generated schema type, search the workspace for an existing hand-written Rust type with the same product meaning.
- If the schema and an existing Rust type have the same semantics and serde shape, reuse the existing type via
lib/foundation/fabro-api/build.rswith_replacement(...)instead of generating a parallel API type. - If two types are close but not identical, prefer proposing changes that align them into one canonical type rather than accepting small drift. It is usually better to iterate the API now than to create permanently split Rust/API types.
- Keep a separate API DTO only when the API is intentionally a projection, summary, or presentation-specific view of internal state. In that case, give it a distinct API-facing name instead of reusing the internal concept name.
- Treat
ApiFooaliases andfoo_to_api/foo_from_apiadapters as a smell unless they represent a real semantic boundary. They should not exist only to bridge accidental duplicate types. - If a type is shared across crates and is part of the core product vocabulary, move it to a shared crate first, then make
fabro-apireuse it. - For every new
with_replacement(...), add afabro-apitest that proves type identity and JSON parity with the OpenAPI schema.
Test support boundaries
Test-only helpers, fixture constructors, fake credentials, in-memory stores, panic-heavy setup code, and test environment shims must not be exposed from production modules or linked into normal builds.
Put shared test helpers in a dedicated test_support module gated behind tests or an explicit feature:
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
If another crate's tests need those helpers, enable the feature only through a dev-dependency using Cargo's dual-listing pattern:
[dependencies]
fabro-server = { path = "../fabro-server" }
[dev-dependencies]
fabro-server = { path = "../fabro-server", features = ["test-support"] }
Do not enable test-support in default features, production dependencies, release builds, or binaries.
Use names that make the boundary obvious: test_app_state, test_store_bundle, test_auth_mode, and similar. Avoid production-looking names such as create_app_state for test fixtures. #[doc(hidden)] is not a substitute for feature-gating; hidden public APIs still compile, link, and can be used accidentally.
Before merging changes that add or move shared test helpers, verify:
cargo build --workspacesucceeds withouttest-support- relevant tests compile and run with
test-support rg -n "create_app_state|test-only-name"does not show production call sites- release/debug artifacts do not contain fake secrets, fixture tokens, or test helper symbols when built without
test-support
Architecture
Fabro is an AI-powered workflow orchestration platform. Workflows are defined as Graphviz graphs, where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.). Petri, the workflow engine, admits a workflow at create and executes every run. Two crates import it: fabro-petri (the engine adapters) and fabro-dot (Petri's DOT parser, for reading a graph's shape and file references).
Rust crates (lib/apps/, lib/components/, and lib/foundation/)
- fabro-cli — CLI entry point. Commands:
run,exec,serve,validate,parse,cp,model,doctor,install,ps,system prune - fabro-workflow — Fabro's platform half of a run: creates a run around Petri's admission (the run's display graph is read off the admitted graph), archives, forks and retries runs, and holds the run tools and the pull request pipeline. Compilation and execution are Petri's, through
fabro-petri - fabro-dot — The workflow graph as written, read through Petri's DOT parser: its name, goal, node and edge counts, and the files it references (
import,stack.child_workflow,@fileprompts, the goal). The bundler and the workflow-version store walk references through it;fabro-graphvizre-emits Fabro DOT for Graphviz through it - fabro-graphviz — SVG rendering of workflow graphs through the vendored Graphviz (
graphviz-sys) - fabro-pebble-sandbox — A
sandbox-driverhandle as theEnvironmentpebble's coding agent runs its tools through (PebbleSandbox), with Fabro's exec policy, port routes, and secret redactor. Petri creates and owns every run sandbox through the sandbox driver; Fabro attaches to one for Ask Fabro, andfabro execcreates a host sandbox of its own. Agent stages, Ask Fabro, hook evaluators, andfabro execall run on thepebble-coding-agentcrate (pinned by rev in the workspaceCargo.toml). Docker is the default runtime provider and runs the operator's Docker daemon; daemon access is host-root-equivalent and assumes trusted callers/payloads. - fabro-petri — Fabro's adapters over Petri, the workflow engine: the one crate that imports the Petri packages (pinned by rev in the workspace
Cargo.toml), holding the run store over SQLite and the platform adapters - fabro-server — Axum HTTP server. Routes for runs, sessions, models, completions, usage. SSE event streaming. Demo mode via header
- fabro-llm — Unified LLM client with providers: Anthropic, OpenAI, Gemini, OpenAI-compatible, plus retry/middleware/streaming
- fabro-api — Auto-generated Rust types and reqwest HTTP client from OpenAPI spec (build.rs + progenitor)
- fabro-github — GitHub App auth (JWT signing, installation tokens, PR creation)
- fabro-mcp-server — Fabro's own MCP server (
fabro mcp): the run tools for external agents - fabro-slack — Slack integration (socket mode, blocks API)
- fabro-checkpoint — Git checkpoint author identity and commit trailers
- fabro-telemetry — CLI analytics (Segment) and crash reporting (Sentry), with anonymous IDs, command sanitization, and detached subprocess delivery
- fabro-util — Shared utilities (redaction, terminal formatting)
TypeScript (fabro-web)
cd apps/fabro-web && bun run dev— rebuild web assets on change for the Rust server; refresh the browser manuallycd apps/fabro-web && bun test— run testscd apps/fabro-web && bun run typecheck— type checkcd apps/fabro-web && bun run build— production build (writes toapps/fabro-web/dist/only; does NOT update the bundled SPA that ships in the Rust binary)cargo dev build [-- <cargo args>]— refreshes the embedded SPA assets from the production build, verifies SPA asset budgets, and then runscargo buildwith forwarded args. The embedded assets are gitignored except for.gitkeep; use this when building a Rust binary that should include a populated SPA bundle.bun run devfor local development is unchanged because debug builds preferapps/fabro-web/dist/on disk via the server fallback.
Docker image
cargo dev docker-build— builds the local Docker image from the current tree using the release pipeline's cargo-zigbuild approach. Honors--arch amd64|arm64,--tag <name>(defaultfabro-sh/fabro),--compile-only(stagestmp/docker-context/<arch>/fabrowithoutdocker build), and--dry-run(prints the Docker commands without running them). Prefer this over writing a throwaway Dockerfile; the release pipeline,Dockerfile, and this command share the same binary layout.
Docker sandbox provider
- Docker is the default runtime sandbox provider from
defaults.toml. The Fabro process must have a working Docker client environment (DOCKER_HOST, socket access, Docker Desktop behavior, TLS settings, groups/permissions, and any remote daemon policy are operator responsibilities). - The packaged compose service mounts
/var/run/docker.sockso the server can create sibling run containers on the host daemon. This is host-root-equivalent under Docker's security model; only use it in the trusted, single-tenant deployment model described by the sandbox code/docs. - Fabro no longer clones a repository into a sandbox: the engine prepares
every run's checkout.
CloneRequeststill travels beside the sandbox spec so the run record names the origin and branch; fabro validates it (a pin needs a branch, a non-GitHub origin needsskip_clone) and refuses a request that asks for a clone. Preflight andfabro execinitialize sandboxes withCloneRequest::none(), which creates an empty workspace.
Release automation
cargo dev release— creates the next stable release tag. Usecargo dev release --nightlyfor a nightly prerelease. Use--dry-runto print planned commands without mutating git or running Cargo,--skip-testsonly after running the release-mode smoke yourself, and--release-date YYYY-MM-DDorFABRO_RELEASE_DATEfor deterministic version computation.
Marketing site (apps/marketing)
cd apps/marketing && bun run dev— start Astro dev servercd apps/marketing && bun run build— production buildcd apps/marketing && bunx vercel --prod— deploy to Vercel (project: website, domain: fabro.sh)
Dev servers
fabro server start— starts the Rust API server (demo mode is per-request viaX-Fabro-Demo: 1header)cd apps/fabro-web && bun run dev— rebuilds web assets on change; refresh the browser manually- Mintlify docs dev server (requires Docker —
mintlify devneeds Node LTS which may not match the host):
Then open http://localhost:3333. Stop withdocker run --rm -d -p 3333:3333 -v $(pwd)/docs/public:/docs -w /docs --name mintlify-dev node:22-slim \ bash -c "npx mintlify dev --host 0.0.0.0 --port 3333"docker stop mintlify-dev.
API workflow
The OpenAPI spec at docs/public/api-reference/fabro-api.yaml is the source of truth for the fabro-api HTTP interface.
- Edit
docs/public/api-reference/fabro-api.yaml cargo build -p fabro-api— build.rs regenerates Rust types and client via progenitor- Write/update handler in
lib/apps/fabro-server/src/server.rs, add route tobuild_router() cargo nextest run -p fabro-server— conformance test catches spec/router driftcd lib/packages/fabro-api-client && bun run generate— regenerates TypeScript Axios client
API type ownership
- Treat OpenAPI as the source of truth for the wire contract, not as the automatic owner of Rust types.
- Before adding or keeping a generated schema type, search the workspace for an existing hand-written Rust type with the same product meaning.
- If the schema and an existing Rust type have the same semantics and serde shape, reuse the existing type via
lib/foundation/fabro-api/build.rswith_replacement(...)instead of generating a parallel API type. - If two types are close but not identical, prefer proposing changes that align them into one canonical type rather than accepting small drift. It is usually better to iterate the API now than to create permanently split Rust/API types.
- Keep a separate API DTO only when the API is intentionally a projection, summary, or presentation-specific view of internal state. In that case, give it a distinct API-facing name instead of reusing the internal concept name.
- Treat
ApiFooaliases andfoo_to_api/foo_from_apiadapters as a smell unless they represent a real semantic boundary. They should not exist only to bridge accidental duplicate types. - If a type is shared across crates and is part of the core product vocabulary, move it to a shared crate first, then make
fabro-apireuse it. - For every new
with_replacement(...), add afabro-apitest that proves type identity and JSON parity with the OpenAPI schema.
Test support boundaries
Test-only helpers, fixture constructors, fake credentials, in-memory stores, panic-heavy setup code, and test environment shims must not be exposed from production modules or linked into normal builds.
Put shared test helpers in a dedicated test_support module gated behind tests or an explicit feature:
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
If another crate's tests need those helpers, enable the feature only through a dev-dependency using Cargo's dual-listing pattern:
[dependencies]
fabro-server = { path = "../fabro-server" }
[dev-dependencies]
fabro-server = { path = "../fabro-server", features = ["test-support"] }
Do not enable test-support in default features, production dependencies, release builds, or binaries.
Use names that make the boundary obvious: test_app_state, test_store_bundle, test_auth_mode, and similar. Avoid production-looking names such as create_app_state for test fixtures. #[doc(hidden)] is not a substitute for feature-gating; hidden public APIs still compile, link, and can be used accidentally.
Before merging changes that add or move shared test helpers, verify:
cargo build --workspacesucceeds withouttest-support- relevant tests compile and run with
test-support rg -n "create_app_state|test-only-name"does not show production call sites- release/debug artifacts do not contain fake secrets, fixture tokens, or test helper symbols when built without
test-support
Architecture
Fabro is an AI-powered workflow orchestration platform. Workflows are defined as Graphviz graphs, where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.). Petri, the workflow engine, admits a workflow at create and executes every run. Two crates import it: fabro-petri (the engine adapters) and fabro-dot (Petri's DOT parser, for reading a graph's shape and file references).
Rust crates (lib/apps/, lib/components/, and lib/foundation/)
- fabro-cli — CLI entry point. Commands:
run,exec,serve,validate,parse,cp,model,doctor,install,ps,system prune - fabro-workflow — Fabro's platform half of a run: creates a run around Petri's admission (the run's display graph is read off the admitted graph), archives, forks and retries runs, and holds the run tools and the pull request pipeline. Compilation and execution are Petri's, through
fabro-petri - fabro-dot — The workflow graph as written, read through Petri's DOT parser: its name, goal, node and edge counts, and the files it references (
import,stack.child_workflow,@fileprompts, the goal). The bundler and the workflow-version store walk references through it;fabro-graphvizre-emits Fabro DOT for Graphviz through it - fabro-graphviz — SVG rendering of workflow graphs through the vendored Graphviz (
graphviz-sys) - fabro-pebble-sandbox — A
sandbox-driverhandle as theEnvironmentpebble's coding agent runs its tools through (PebbleSandbox), with Fabro's exec policy, port routes, and secret redactor. Petri creates and owns every run sandbox through the sandbox driver; Fabro attaches to one for Ask Fabro, andfabro execcreates a host sandbox of its own. Agent stages, Ask Fabro, hook evaluators, andfabro execall run on thepebble-coding-agentcrate (pinned by rev in the workspaceCargo.toml). Docker is the default runtime provider and runs the operator's Docker daemon; daemon access is host-root-equivalent and assumes trusted callers/payloads. - fabro-petri — Fabro's adapters over Petri, the workflow engine: the one crate that imports the Petri packages (pinned by rev in the workspace
Cargo.toml), holding the run store over SQLite and the platform adapters - fabro-server — Axum HTTP server. Routes for runs, sessions, models, completions, usage. SSE event streaming. Demo mode via header
- fabro-llm — Unified LLM client with providers: Anthropic, OpenAI, Gemini, OpenAI-compatible, plus retry/middleware/streaming
- fabro-api — Auto-generated Rust types and reqwest HTTP client from OpenAPI spec (build.rs + progenitor)
- fabro-github — GitHub App auth (JWT signing, installation tokens, PR creation)
- fabro-mcp-server — Fabro's own MCP server (
fabro mcp): the run tools for external agents - fabro-slack — Slack integration (socket mode, blocks API)
- fabro-checkpoint — Git checkpoint author identity and commit trailers
- fabro-telemetry — CLI analytics (Segment) and crash reporting (Sentry), with anonymous IDs, command sanitization, and detached subprocess delivery
- fabro-util — Shared utilities (redaction, terminal formatting)
TypeScript (apps/ and lib/packages/)
- apps/fabro-web — React 19 + React Router + Tailwind CSS frontend, bundled by a custom Bun script (
apps/fabro-web/scripts/build.ts), not Vite - lib/packages/fabro-api-client — Auto-generated TypeScript Axios client from OpenAPI spec
Key design patterns
- Direct sandbox access — Petri creates every run sandbox through the sandbox driver and records its provider, id and working directory on the run (
RunSandboxInstance); every Docker and Daytona sandbox carries thepetri.runlabel. The server reaches a run's sandbox (the sandbox tab, Run Files, terminal, SSH, preview URLs, VNC,fabro cp, Ask Fabro) throughfabro-server/src/sandbox_access.rs: it connects the record's provider itself, keys ownership onpetri.run, and works on the driver'sArc<dyn Sandbox>facets (exec, filesystem, search, git, pty). Deleting a run deletes its sandboxes through Petri's lease ledger (fabro_petri::prune, whatpetri sandbox prunedoes), not through a provider call of Fabro's own. There is no fabro-side sandbox trait; tests usefabro_pebble_sandbox::test_support::MockSandboxover the driver's scripted doubles. - Graphviz graph workflows — Stages and transitions defined as Graphviz graph attributes
- OpenAPI-first —
fabro-api.yamldrives Rust type + client generation (progenitor) and TypeScript client generation (openapi-generator) - Checkpoint/resume — Workflows can be paused, checkpointed, and resumed
Strategy docs
When working in an area covered by a strategy doc, read the relevant document before making changes:
docs/internal/logging-strategy.md— read when addingtracingcalls (info!,debug!,warn!,error!), working on error handling paths, or adding new operations that should be observabledocs/internal/events-strategy.md— read when adding a platform record kind, changing the projection fold or the run stream, or writing a consumer that matches on stream itemsdocs/internal/testing-strategy.md— read when adding or reorganizing tests, choosing between unit vstests/it, deciding whether a test belongs incmdvsworkflowvsscenario, or deciding how to structure snapshots and fixturesdocs/internal/server-secrets-strategy.md— read when adding or changing server-level secrets, startup validation, install-time secret persistence, or subprocess env inheritance/scrubbingdocs/internal/migrations-strategy.md— read when adding or changing temporary compatibility migrations, startup/file rewrites, migration runners, backups, or removal deadlinesdocs/internal/error-handling-strategy.md— read when changing error types, usinganyhow/thiserror, adding.map_err(...), converting errors toString, changing API error responses, or touching CLI/miette/log/telemetry error renderingdocs/internal/react-effects-policy.md— read when adding or refactoring React effects inapps/fabro-web; directuseEffectcalls should be avoided in component code
Shell quoting in sandbox code
When interpolating values into shell command strings (in fabro-workflow), always use the shell_quote() helper (backed by shlex::try_quote). Never use manual replace('\'', "'\\''") or unquoted interpolation. This applies to file paths, branch names, URLs, env vars, image names, glob patterns, and any other user-controlled input assembled into a shell script.
Rust import style
- Types (structs, enums, traits): import by name —
use crate::outcome::Outcome; - Functions: import the parent module, call as
module::function()—use fabro_workflow::operations; operations::create(...) - No glob imports in production code (
use foo::*). Globs are acceptable in test modules and preludes. Enforced by clippywildcard_importslint.
Enum string/int conversions (strum)
For any enum where a variant maps to a fixed string or integer, derive it with strum instead of hand-writing impl Display, impl FromStr, as_str(), fn all(), or const ALL: &[Self]. Hand-written variant→string maps drift across the three impls on every rename.
strum::Displayreplaces hand-writtenimpl fmt::Displaywhose body is a match over string literals.strum::EnumStringreplaces hand-writtenimpl FromStr. TheErrtype becomesstrum::ParseError— adjust callers that assumedErr = String.strum::IntoStaticStrreplacesimpl From<E> for &'static str. When an existingas_str(self) -> &'static stris on the public API, keep it as a one-line wrapper:pub fn as_str(self) -> &'static str { self.into() }.strum::EnumIter,strum::VariantArray,strum::VariantNamesreplace hand-writtenfn all()/const ALL/&[&'static str]arrays. Do NOT use these if the hand-written list intentionally excludes variants (e.g.Provider::ALLskipsOpenAiCompatible).strum::FromReprreplacesfn from_u8/from_i32. Note: it returnsOption<Self>, so don't adopt it when the existing conversion has a_ => defaultfallback — that's a behavior change, not a cleanup.
Align strum with serde. When the enum also derives Serialize/Deserialize with #[serde(rename_all = "...")], add the matching #[strum(serialize_all = "...")]. For variant aliases, use #[strum(to_string = "canonical", serialize = "alias")] — strum picks the last serialize for Display/IntoStaticStr otherwise, so to_string is needed to pin the canonical form.
Skip strum when parsing is fuzzy (URL/path detection, structured IDs, multi-token formats), when a variant carries a String catch-all, or when Display does dynamic formatting.
Snapshot tests (insta)
Many CLI tests use insta inline snapshots. When a snapshot needs updating:
- Run
cargo insta pending-snapshotsto list what changed - Verify each pending snapshot is expected
- Run
cargo insta acceptto accept all, orcargo insta accept --snapshot <path>for a specific one
Never run cargo insta accept without first checking what's pending — it accepts all pending snapshots, which may include unrelated changes.
Testing workflows
fabro run <name>— run a workflow by name (resolves.fabro/workflows/<name>/workflow.toml), e.g.fabro run repl#[e2e_test(twin, live("VAR"))]— dual-mode test that runs against twin-openai or real API.#[e2e_test(twin)]for twin-only tests (e.g., scripted failures).#[e2e_test(live("VAR"))]for live-only tests requiring secrets.#[e2e_test()]for sandbox tests with no API deps. Behavior is controlled byFABRO_TEST_MODE(live,strict; default istwin), andcargo nextest run --profile e2e ...impliesstrict. Usefabro_test::e2e_openai!()in twin/dual-mode tests to get(base_url, api_key).- Local test HTTP clients must use
.no_proxy(). Prefer shared helpers likefabro_test::test_http_client()or crate-local equivalents instead ofreqwest::Client::new(), bareClient::builder().build(), orreqwest::get(...). - This is not cosmetic: macOS proxy discovery adds hidden startup overhead to repeated localhost reqwest clients and can surface as misleading nextest timeouts.