test-environment.yaml Reference

test-environment.yaml is the manifest that tells the GAIA Test Execution Bridge how to run your project's tests. It declares one or more runners (test commands), assigns each runner to a tier (unit, integration, or end-to-end), and maps tiers to review gates. When the bridge is enabled, every test-execution step in the GAIA lifecycle consults this file to decide what command to invoke, what environment is required, and which gate to credit on success.

This page is the complete reference for the file: where it lives, every field in the schema, the template sentinel, the Layer 0 readiness gates, and copy-ready examples for the seven supported stacks.

Overview

A minimal valid manifest declares a schema version and at least one runner. Everything else is optional. Here is the smallest file the bridge will accept:

version: 2

runners:
  - name: unit
    command: "npm test"
    tier: 1

A real-world manifest typically adds a primary runner, a tier-to-gate map, environment requirements, and a CI workflow reference. The full template — with every field, default value, and inline comment — ships with the plugin at plugins/gaia/templates/test-environment.yaml.example and is the source of truth for the schema.

File location

The canonical path is:

config/test-environment.yaml

This sits next to .gaia/config/project-config.yaml under the project root. By convention, all runtime configuration lives under config/.

Legacy path migration

Prior to v1.157.0 the manifest lived at .gaia/artifacts/test-artifacts/test-environment.yaml. If your project still has a file at that legacy path, the next invocation of /gaia-bridge-enable detects it and moves it to the canonical location automatically. The move is idempotent and emits a one-time deprecation warning. No manual action is required — but you should update any project-local scripts or documentation that referenced the legacy path.

The template sentinel

A freshly-generated manifest may contain a single comment line that acts as a tripwire:

# GAIA-MANIFEST-TEMPLATE: edit this file before enabling the bridge -- bridge will fail Layer 0 readiness check until this line is removed

This is the GAIA-MANIFEST-TEMPLATE sentinel. Its presence is a contract between the generator and the bridge: as long as the sentinel is in the file, the bridge refuses to run tests against the manifest, because the runners are known to be placeholders that have not been reviewed by a human.

The sentinel is added in two specific cases:

  • No stack was detected. The auto-generator could not infer a language or build tool from your project, so it emitted a generic make test placeholder. Customise the runners, then delete the sentinel line.
  • You used option [b] (“copy the schema example template”) at the /gaia-bridge-enable prompt. The shipped .example file carries the sentinel because its sample runners are illustrative, not project-specific.

When the generator detects a stack and emits stack-specific runners, the sentinel is not included — the manifest is treated as presumed-customised for that stack and Layer 0 passes immediately.

To remove the sentinel, open config/test-environment.yaml, customise the runners for your project, and delete the entire # GAIA-MANIFEST-TEMPLATE: … line. Save and re-run /gaia-bridge-enable (or any downstream test command) to re-evaluate the readiness gate.

Schema reference

The schema is versioned via the top-level version integer. version: 2 is current; version: 1 is still accepted but lacks the per-runner promotion_chain_env_id field.

Top-level fields

Field Type Required Description
version integer yes Schema version. Use 2 for new manifests. 1 is accepted for backward compatibility but cannot use the per-runner promotion_chain_env_id field.
runners list yes One or more runner objects (see below). At least one runner must be present.
primary_runner string no Name of the default runner. Must match one of the runners[].name values. Used when a test step does not specify a tier.
tiers map no Maps tier numbers (1, 2, 3) to a list of review gates that require execution evidence from that tier. See Tiers block.
stack_hints map no Per-stack tier hints consumed by stack adapters (pytest_markers, gradle_tasks, go_build_tags, flutter_suites). See Stack hints.
env_requirements list of strings no Free-form system-level prerequisites. Each entry is human documentation (e.g. "node >= 20", "docker >= 24 (for tier 2+)"). The bridge does not enforce these — they are advisory.
ci_workflow string no Filename of the GitHub Actions workflow the bridge should trigger in CI mode (e.g. "ci.yml"). Resolved against .github/workflows/.

Runner fields

Each entry under runners is an object with the following fields:

Field Type Required Description
name string yes Unique identifier for the runner. Conventionally unit, integration, or e2e, but any string is valid.
command string yes Shell command the bridge will execute. Quote the string; pipes and redirects are allowed.
tier integer yes 1 = unit (no environment required), 2 = integration (service containers required), 3 = end-to-end (deployed application required).
test_pattern string no Glob pattern locating the test files this runner owns. Used for change-impact analysis and traceability.
timeout_seconds integer no Maximum execution time in seconds. Default 300.
promotion_chain_env_id string (v2 only) no Optional cross-reference to a ci_cd.promotion_chain[].id entry in config/global.yaml. When set, the bridge enriches its context with the linked environment's CI provider, branch, and ci_checks. See Linking runners to environments.

Tiers block

The tiers map declares which review gates require execution evidence from each tier. The default mapping is:

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

Read this as: “Before any of qa-tests, test-automate, or test-review can pass, the bridge must have a green run from a tier-1 runner.” If your project does not run integration tests, leave tier 2 with an empty gate list rather than removing it.

Layer 0 readiness gates

Before the bridge will dispatch any test, it runs a three-step readiness check. All three gates must pass.

# Gate Failure mode
1 File exists at config/test-environment.yaml The bridge halts and instructs the user to run /gaia-bridge-enable or /gaia-brownfield to generate the manifest.
2 Schema valid — parses as YAML, has version, has a non-empty runners list, every runner has name, command, and tier Bridge surfaces the schema validation errors. The bridge_enabled flag is not rolled back — you fix the file and re-evaluate.
3 Sentinel absent — the # GAIA-MANIFEST-TEMPLATE: … line is not in the file Bridge halts with a message pointing at the sentinel line and the line number. Customise the runners and delete the sentinel line, then re-evaluate.

The sentinel-absent check is implemented by scripts/lib/check-manifest-sentinel.sh and runs on every bridge invocation, not just the first one. This means an accidentally re-introduced sentinel (e.g. by reverting a commit) will block test execution until removed.

How the file is created

You almost never create this file by hand. There are three sanctioned paths:

  1. Auto-generate via /gaia-bridge-enable Step 4 (recommended). When you enable the bridge and the manifest is absent, the command offers option [a]: auto-generate a stack-specific manifest. Under the hood it invokes scripts/lib/test-environment-manifest.sh --target <project-root> --write, which calls detect-signals.sh to identify your stack and emits populated runners for it. If a stack is detected, the resulting file is sentinel-free and Layer 0 passes immediately.
  2. Schema-doc starter via Step 4 option [b] (advanced). Copies the canonical config/test-environment.yaml.example template verbatim. The template carries the sentinel and a full set of inline comments — useful as a reference while authoring, but it requires you to remove the sentinel before the bridge will run.
  3. Auto-generate during /gaia-brownfield Phase 5. When onboarding an existing project, brownfield delegates to the same generator helper. The behaviour is identical to option [a] above. If a manifest is already present, brownfield preserves it byte-identical.

Copy-if-absent semantics

Both auto-generate paths use copy-if-absent semantics: if config/test-environment.yaml already exists, the generator exits successfully without overwriting. Your edits are safe across repeated invocations of /gaia-bridge-enable and /gaia-brownfield.

Stack-specific examples

The auto-generator recognises seven stacks. Each example below is a complete, valid manifest you can copy verbatim and adapt. The auto-generator emits only the version, runners, primary_runner, and tiers blocks; the env_requirements, stack_hints, and ci_workflow fields shown here are recommended hand-edit additions.

Node.js / TypeScript

Detection triggers on package.json presence and dependency signals for React, Vue, Angular, Svelte, plain Node, or TypeScript. Tier 2 is included when an integration npm script is conventional.

# test-environment.yaml — Test Execution Bridge Manifest
# detected-stack: node
# Reference: architecture.md Section 10.20.5

version: 2

runners:
  - name: unit
    command: "npm test"
    tier: 1
    test_pattern: "test/unit/**/*.test.js"
    timeout_seconds: 120
  - name: integration
    command: "npm run test:integration"
    tier: 2
    test_pattern: "test/integration/**/*.test.js"
    timeout_seconds: 300

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

env_requirements:
  - "node >= 20"
  - "docker >= 24 (for tier 2+)"

ci_workflow: "ci.yml"

Python (pytest)

Detection triggers on pyproject.toml, setup.py, requirements.txt, or pytest.ini.

# test-environment.yaml — Test Execution Bridge Manifest
# detected-stack: python
# Reference: architecture.md Section 10.20.5

version: 2

runners:
  - name: unit
    command: "pytest tests/unit"
    tier: 1
    test_pattern: "tests/unit/**/test_*.py"
    timeout_seconds: 120
  - name: integration
    command: "pytest tests/integration"
    tier: 2
    test_pattern: "tests/integration/**/test_*.py"
    timeout_seconds: 300

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

stack_hints:
  pytest_markers: ["slow", "integration"]

env_requirements:
  - "python >= 3.11"

Go

Detection triggers on a go.mod file at the project root. The generator emits a single recursive runner because go test ./... traverses the whole module tree.

# test-environment.yaml — Test Execution Bridge Manifest
# detected-stack: go
# Reference: architecture.md Section 10.20.5

version: 2

runners:
  - name: unit
    command: "go test ./..."
    tier: 1
    test_pattern: "**/*_test.go"
    timeout_seconds: 120

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

stack_hints:
  go_build_tags: ["integration", "e2e"]

env_requirements:
  - "go >= 1.22"

Java (Maven)

Detection triggers on pom.xml (Maven) or build.gradle / build.gradle.kts (Gradle). Kotlin projects are detected as java. The example below is Maven; the Gradle variant is shown after it.

# test-environment.yaml — Test Execution Bridge Manifest
# detected-stack: java
# Reference: architecture.md Section 10.20.5

version: 2

runners:
  - name: unit
    command: "mvn test"
    tier: 1
    test_pattern: "src/test/java/**/*Test.java"
    timeout_seconds: 300

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

env_requirements:
  - "java >= 17"
  - "maven >= 3.9"

Java (Gradle) variant

For Gradle, replace the runners block and add gradle_tasks under stack_hints so the Gradle stack adapter knows how to map tiers to tasks:

version: 2

runners:
  - name: unit
    command: "./gradlew test"
    tier: 1
    test_pattern: "src/test/**/*Test.{java,kt}"
    timeout_seconds: 300
  - name: integration
    command: "./gradlew integrationTest"
    tier: 2
    test_pattern: "src/integrationTest/**/*Test.{java,kt}"
    timeout_seconds: 600

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

stack_hints:
  gradle_tasks:
    unit: test
    integration: integrationTest
    e2e: e2eTest

env_requirements:
  - "java >= 17"
  - "gradle wrapper present"

Flutter / Dart

Detection triggers on pubspec.yaml. Flutter projects get both a unit runner (flutter test) and an integration runner (flutter test integration_test).

# test-environment.yaml — Test Execution Bridge Manifest
# detected-stack: flutter
# Reference: architecture.md Section 10.20.5

version: 2

runners:
  - name: unit
    command: "flutter test"
    tier: 1
    test_pattern: "test/**/*_test.dart"
    timeout_seconds: 300
  - name: integration
    command: "flutter test integration_test"
    tier: 2
    test_pattern: "integration_test/**/*_test.dart"
    timeout_seconds: 600

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

stack_hints:
  flutter_suites:
    unit: test/
    integration: integration_test/
    e2e: integration_test/e2e/

env_requirements:
  - "flutter >= 3.22"
  - "dart >= 3.4"

Bash / bats

Detection triggers when no package.json is present but the project contains *.bats test files within three directory levels of the root. Bridge timeout is generous because bats suites often shell out to slow tools.

# test-environment.yaml — Test Execution Bridge Manifest
# detected-stack: bash
# Reference: architecture.md Section 10.20.5

version: 2

runners:
  - name: unit
    command: "bats tests/"
    tier: 1
    test_pattern: "tests/**/*.bats"
    timeout_seconds: 600

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

env_requirements:
  - "bats-core >= 1.10"
  - "bash >= 5"

Rust (cargo)

Detection triggers on Cargo.toml. The generator emits a single cargo test runner; add integration and e2e runners manually if your project separates them (e.g. cargo test --test integration).

# test-environment.yaml — Test Execution Bridge Manifest
# detected-stack: rust
# Reference: architecture.md Section 10.20.5

version: 2

runners:
  - name: unit
    command: "cargo test"
    tier: 1
    test_pattern: "src/**/*.rs"
    timeout_seconds: 300

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

env_requirements:
  - "rust >= 1.78 (stable channel)"

Generic placeholder (no stack detected)

If the auto-generator cannot identify a stack, it emits this file. Note the sentinel line — Layer 0 will refuse to run until you customise the runners and remove it.

# test-environment.yaml — Test Execution Bridge Manifest
# Auto-generated by /gaia-bridge-enable.
# No stack detected — generic placeholder runners. CUSTOMIZE for your project.
#
# detected-stack: generic
# Reference: architecture.md Section 10.20.5

# GAIA-MANIFEST-TEMPLATE: edit this file before enabling the bridge -- bridge will fail Layer 0 readiness check until this line is removed

version: 2

runners:
  - name: unit
    command: "make test"
    tier: 1
    test_pattern: ""
    timeout_seconds: 120

primary_runner: unit

tiers:
  1:
    gates: [qa-tests, test-automate, test-review]
  2:
    gates: [review-perf]
  3:
    gates: []

Linking runners to environments

Schema version 2 introduced an optional per-runner field, promotion_chain_env_id, that links a runner to a specific entry in the ci_cd.promotion_chain block of config/global.yaml. When set, the bridge enriches the runner's execution context with the linked environment's CI provider, target branch, and required ci_checks. This is how a tier-2 integration runner gets pointed at the staging environment, and a tier-3 e2e runner at production.

runners:
  - name: unit
    command: "npm test"
    tier: 1
    # No promotion_chain_env_id: tier-local config only

  - name: integration
    command: "npm run test:integration"
    tier: 2
    promotion_chain_env_id: "staging"   # links to ci_cd.promotion_chain[id=staging]

  - name: e2e
    command: "npm run test:e2e"
    tier: 3
    promotion_chain_env_id: "prod"      # links to ci_cd.promotion_chain[id=prod]

The field is silently ignored when config/global.yaml has no ci_cd block, preserving backward compatibility for projects that have not yet adopted promotion chains. Run /gaia-ci-edit to inspect or modify the promotion chain itself.

Stack hints

The optional stack_hints block lets you fine-tune how a stack adapter resolves tiers when the default mapping is insufficient. Each key under stack_hints is consumed by exactly one adapter, and an unknown adapter's hints are silently ignored — so a multi-stack monorepo can include hints for every stack without conflict.

Hint key Adapter Value shape Purpose
pytest_markers python list of strings Marker names that classify tests into integration / slow tiers. The python adapter uses pytest -m to select them.
gradle_tasks java (Gradle) map: tier → task name Maps tier numbers to Gradle task names. Required when your build defines non-default integration or e2e tasks.
go_build_tags go list of strings Build-tag names that gate integration / e2e tests (go test -tags ...).
flutter_suites flutter map: tier → directory Maps tier numbers to test directories. Useful when e2e tests live in a sub-directory of integration_test/.

Unknown keys at the top of stack_hints are rejected by the schema validator with a loud error listing the accepted keys. Partial blocks are valid — any tier you leave unset falls back to the adapter's built-in default.

Editing workflow

The recommended workflow when you need to change the manifest:

  1. Open config/test-environment.yaml in your editor.
  2. Edit Adjust runner commands, timeouts, tier assignments, or promotion-chain links. Keep the structure compliant with the schema reference above.
  3. Remove the sentinel If the # GAIA-MANIFEST-TEMPLATE: … line is still present, delete it.
  4. Validate Re-run /gaia-bridge-enable (idempotent; reports the bridge state and re-evaluates Layer 0) or /gaia-config-validate for a standalone schema check.
  5. Run tests With Layer 0 green, downstream test commands — /gaia-atdd, /gaia-test-run, review gates — will dispatch through the bridge using your runners.

The file is intended to be version-controlled alongside your source code. Treat changes to it as you would any other configuration change: PR review, CI green, merge.

Troubleshooting

Manifest still at the legacy path

Symptom: The file exists at .gaia/artifacts/test-artifacts/test-environment.yaml but downstream commands report it as missing.

Cause: The canonical path moved to config/test-environment.yaml in v1.157.0.

Fix: Run /gaia-bridge-enable; its first step invokes a one-shot migration helper that moves the legacy file to the canonical path. The move is idempotent and emits a one-time deprecation warning. If you prefer to move it by hand, simply mv .gaia/artifacts/test-artifacts/test-environment.yaml config/test-environment.yaml and commit the change.

Sentinel still present

Symptom: The bridge halts with Layer 0 readiness check failed: GAIA-MANIFEST-TEMPLATE sentinel is still in the manifest.

Cause: The auto-generator either could not detect your stack and emitted a generic placeholder, or you chose option [b] (schema-doc starter) when prompted. Either way, the sentinel is the file's own “please review me” flag.

Fix: Open config/test-environment.yaml, replace the placeholder runner(s) with your project's real test commands (use the stack-specific examples above as a template), and delete the entire # GAIA-MANIFEST-TEMPLATE: … line. Save and re-run the bridge command.

No stack detected — generic placeholder shipped

Symptom: The auto-generator log says detected-stack=generic and the file contains a single make test runner with the sentinel.

Cause: Your project does not have any of the marker files the brownfield detection-signals registry looks for — no package.json, pyproject.toml, go.mod, pom.xml, build.gradle*, pubspec.yaml, Cargo.toml, or *.bats files in the top three directory levels.

Fix: Find the closest match in stack-specific examples and copy its runners block over the placeholder. If your stack genuinely is not on the list, write runners by hand using the runner-fields reference. Remove the sentinel when done.

Schema validation failed

Symptom: Bridge surfaces messages like runners[0].tier: expected integer, got string or missing required field: runners.

Cause: The file does not conform to the schema described in Schema reference above. Common causes: a tier quoted as a string (tier: "1" instead of tier: 1), a runner without a command, or YAML indentation that nested fields under the wrong parent.

Fix: Compare your file to the closest stack example. Pay particular attention to: version is an integer (not a string), each runner's tier is an integer 1, 2, or 3, and tiers: top-level keys are unquoted integers. The bridge does not roll back the bridge_enabled flag on schema failure — you can re-edit and re-validate as many times as you need.

Generator failed and the bridge fell back to template-copy

Symptom: The bridge log contains generator failed, falling back to schema-doc template-copy and the resulting manifest is the shipped .example file with the sentinel.

Cause: The auto-generator helper exited non-zero (most commonly: detect-signals.sh is missing or jq is not installed). The bridge protects you by falling back to a known-good schema-doc copy.

Fix: Install the missing prerequisite (jq for the detector) or run /gaia-init to materialise the .example source if you got an “.example missing” error. Then delete the generated manifest and re-run /gaia-bridge-enable — with copy-if-absent semantics the generator will only act when the file is absent, so you must remove the fallback file to retry.