Skip to content

Test and production environments

Reviewed: 2026-09-09. Applies to every integration in the setup index.

Use one application process per environment. The same application runs in either track; the launcher supplies the chosen credentials, config and working directory. No provider credential is obtained by setting mode=test alone.

Test track

Choose one of these two explicit test backends:

Backend Command What it proves
Live test account --mode test Real provider behavior against the account/resources you provisioned
Fixture --mode test --fixture Your application's behavior with explicitly injected test transports/services

Live test accounts can send real mail, publish public posts and incur charges. Where no vendor sandbox is available, use the dedicated test account or isolated resources described in that provider's guide. Never point the test credential at your production newsletter audience just to make a check pass.

Fixture mode supplies no provider environment credentials and uses its own state directory. It does not invent a successful response, implement a universal mock of every API, or block network syscalls. Your application must inject controlled clients/transports, as the supplied offline examples and test suites do. A missing fixture fails normally. Use network-denied CI/container infrastructure when you need an operating-system guarantee of no egress.

Production track

Use --mode production, production credentials, and a separately reviewed production resource configuration. --fixture is rejected with production mode. There is no fallback from a missing test key to a production key, from a missing production key to a test key, or from either namespace to a bare ambient key. For managed LLM services, missing credentials or SDK failures also cannot select another provider or a mock. Production is an explicit launch decision each time.

Install and prepare

Follow the repository quickstart to use final Python 3.14 and an activated environment. Install only the optional extras named in your provider's guide. The base package includes the launcher, HubSpot and Kit clients. From a source checkout, uv pip install -e . installs the current code.

Choose a state root outside the repository. In an activated shell:

export ZEO_ENV_ROOT="$HOME/.local/share/zeocore-environments"
python -m zeo_core.integrations.environments --mode test   --root "$ZEO_ENV_ROOT" --integration kit.marketing --prepare
python -m zeo_core.integrations.environments --mode production   --root "$ZEO_ENV_ROOT" --integration kit.marketing --prepare

--prepare creates directories and an empty config/integrations.yaml, prints only non-secret locations, and makes no provider request. Running it again keeps your existing configuration. It refuses symlinks that join environment directories.

<root>/test/config/integrations.yaml
<root>/test/credentials/
<root>/test/work/                     # app CWD and relative outputs
<root>/test/tmp/
<root>/production/config/integrations.yaml
<root>/production/credentials/
<root>/production/work/
<root>/production/tmp/
<root>/fixtures/test/...             # separate fixture caches and outputs

Google and Bluesky use credential files under the selected credentials/ tree. ZEOconnect uses a distinct Keychain service for each resolved state root and mode. Managed runs do not import old Google tokens, shared user config or the old hosted session automatically. Config and credential path overrides must remain inside the selected state directory, including after resolving symlinks. Copy the correct new client-secret file into that directory; never move a production refresh token into the test track. HOME is preserved, not redirected.

Supply keys without putting them in command history

Each guide tells you exactly where to obtain the credential. The launcher can prompt for it without echoing it:

python -m zeo_core.integrations.environments --mode test   --root "$ZEO_ENV_ROOT" --integration kit.marketing --secret KIT_API_KEY   -- python /absolute/path/check_kit.py

At the prompt paste the test account key. The key goes only to the child process; the launcher does not save it, print it, or modify the parent shell. Use an absolute script path because the child's working directory changes. python -m your_installed_app also works. Commands run without a shell, and the application's exit status is propagated. Inspect a nonzero exit instead of reporting a failed provider check as success.

For unattended runs, use your secret manager/CI environment to supply prefixed variables. --secret prompts only when the selected variable is missing. Do not use interactive prompts in CI; missing credentials should fail. Examples:

Parent variable Child receives in the selected track
ZEO_TEST_KIT_API_KEY KIT_API_KEY, test only
ZEO_PRODUCTION_KIT_API_KEY KIT_API_KEY, production only
ZEO_TEST_HUBSPOT_ACCESS_TOKEN HUBSPOT_ACCESS_TOKEN, test only
ZEO_PRODUCTION_NOTION_TOKEN NOTION_TOKEN, production only

Apply the same prefix to every variable listed in the provider guides. When both namespaces contain the same secret value for a provider variable, launch is refused. For selected live Supabase runs, nonempty identical SUPABASE_URL values in both namespaces are also refused, even when the keys differ: separate keys can address the same project. This compares the exact supplied URLs only when both are present; it does not resolve custom domains or aliases. Identical Bluesky service URLs are allowed because different accounts can share that public service. These checks cannot generally detect two different keys for the same production account. Check account identity and resource access in the provider UI.

Repeat --integration for a process using several providers. Only those providers' allowed variables are forwarded. Bare keys, opposite-mode keys, ambient proxies, PYTHONPATH, and unrelated provider variables are not inherited. Ambient netrc lookup is disabled so requests cannot replace an explicit provider token with credentials from ~/.netrc or a parent NETRC override. Local converter settings such as ZEO_TEST_ZEO_PANDOC_OUTPUT_DIR become ZEO_PANDOC_OUTPUT_DIR. Other application settings belong in the selected non-secret config or command arguments. The launcher does not parse .env files or load arbitrary shell files.

Configuration and application code

Edit the selected config/integrations.yaml; keep credentials out of it. It uses the existing provider sections (notion, supabase, llm, integrations.google, etc.) shown in the individual guides. Store resource IDs and expected account labels separately in a mode-specific file such as config/scenario.json, and have your application read it explicitly. Resource IDs do not magically change when a key changes. Keep test and production recipients, repositories, calendars, databases and output paths separate.

The Python launch API is useful to ZeoCreator or another host:

import sys
from pathlib import Path
from zeo_core.integrations.environments import IntegrationEnvironment

environment = IntegrationEnvironment(
    mode="test",
    root=Path.home() / ".local/share/zeocore-environments",
    integrations=("hubspot.marketing", "kit.marketing"),
)
raise SystemExit(environment.run([sys.executable, "/absolute/path/app.py"]))

Inside the child, integration_mode() and integration_backend() from zeo_core.integrations.environment expose the selected track. Include those labels in your own E2E receipts, together with non-secret account/resource identifiers, operation, result and cleanup evidence. Client construction/initialization, provider acceptance and final delivery/render are different facts.

Applications that instantiate integrations directly without this launcher retain the historical environment/config behavior. They must migrate their entry point to the managed launcher to obtain these guarantees. Do not switch modes inside one process or reuse an initialized service across modes. This launcher is an operational boundary for trusted application code, not an OS sandbox: explicitly opening an outside file, using arbitrary subprocesses or hardcoding production credentials/resources can defeat application-level separation.

Third-party SDK default credential chains can also read files under the preserved HOME, including in fixture mode. Dropping GOOGLE_APPLICATION_CREDENTIALS from the child environment does not disable Google Application Default Credentials: on Linux/macOS the SDK can still discover ~/.config/gcloud/application_default_credentials.json, and on cloud hosts it may use an attached service account. See the Google ADC search order. AWS SDKs can automatically load ~/.aws/credentials and ~/.aws/config; see the AWS shared-file locations. The launcher does not relocate or disable those third-party SDK stores. When your application uses such SDKs directly, pass the selected credentials explicitly and avoid default credential discovery. For E2E or fixture runs requiring stronger isolation, use a dedicated host/container with no ambient production credentials or production cloud identity, and deny network access for offline fixtures. Mode selection alone does not provide that isolation.

Offline and E2E checks

From the repository, run make verify for the complete gate. For a simulated marketing workflow under fixture mode, replace /absolute/path/zeocore below:

python -m zeo_core.integrations.environments --mode test --fixture   --root "$ZEO_ENV_ROOT" --integration kit.marketing   -- python /absolute/path/zeocore/examples/kit_usage.py
python -m zeo_core.integrations.environments --mode test --fixture   --root "$ZEO_ENV_ROOT" --integration hubspot.marketing   -- python /absolute/path/zeocore/examples/hubspot_usage.py

The examples explicitly print SIMULATED. Every integration also has provider unit/contract tests under tests/test_integrations; these do not certify a live account. Follow the provider guide for a real account check, then a bounded E2E operation and cleanup. Tests must fail if the expected effect did not occur.

Promotion, troubleshooting and recovery

Promote application code and reviewed templates; provision production credentials and IDs independently. Start with the production guide's harmless check. Review any live mutation using the existing capability confirmation/evidence contract. Never copy token caches or test recipients wholesale into production configuration.

If a key is reported missing, verify the prefix, selected --integration, and the provider's exact variable name. If OAuth opens the wrong account, stop the flow, select the intended browser profile and inspect the selected token path. If config is ignored, verify the printed state directory and section name; a .env file alone has no effect. A 401 requires credential repair; a 403 usually requires scope, resource sharing or account entitlement repair. Neither is fixed by changing mode.

Rotate a key in its provider dashboard, update only that namespace, restart the application and rerun the harmless check. Revoke the old key after validating the replacement. Before revocation, cancel/reconcile queued effects using the provider where applicable; stopping the local process does not undo accepted external work.