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 testplaceholder. Customise the runners, then delete the sentinel line. -
You used option [b] (“copy the schema
example template”) at the
/gaia-bridge-enableprompt. The shipped.examplefile 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:
-
Auto-generate via
/gaia-bridge-enableStep 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 invokesscripts/lib/test-environment-manifest.sh --target <project-root> --write, which callsdetect-signals.shto 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. -
Schema-doc starter via Step 4 option [b]
(advanced).
Copies the canonical
config/test-environment.yaml.exampletemplate 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. -
Auto-generate during
/gaia-brownfieldPhase 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:
-
Open
config/test-environment.yamlin your editor. - Edit Adjust runner commands, timeouts, tier assignments, or promotion-chain links. Keep the structure compliant with the schema reference above.
-
Remove the sentinel
If the
# GAIA-MANIFEST-TEMPLATE: …line is still present, delete it. -
Validate
Re-run
/gaia-bridge-enable(idempotent; reports the bridge state and re-evaluates Layer 0) or/gaia-config-validatefor a standalone schema check. -
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.