ambient acceptance criteria

By: Lars Wirzenius

2025-12-19 04:17

Table of Contents

1 Introduction

ambient runs CI for a project in a VM.

2 Acceptance criteria for ambient

2.1 Smoke test

Want: The ambient tool can show it's runtime configuration.

Why: If this doesn't work, there's no hope of anything else working, either.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 when I run ambient config
4 then stdout contains ""executor": "ambient-execute-plan""
state: ambient_state
executor: ambient-execute-plan
image_store: image-store
uefi: true

2.2 Reports its version

Want: ambient can be queried for its version.

Why: This is useful for diagnosing problems, and also acts as a smoke test: if this works, we know the adapter is installed and can be run.

1 given an installed ambient
2 when I run ambient --version
3 then stdout matches regex ^ambient \d+\.\d+\.\d+@.*$

2.3 Backwards compatible QEMU configuration

Want: The ambient tool understands its own older configuration files, but warns about them.

Why: This make migration to the newer configuration format less harsh.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from old-config.yaml
3 when I run ambient config --output config.json
4 then stderr contains "deprecated"
5 then stderr contains "cpus"
6 then stderr contains "memory"
7 then JSON file config.json contains qemu.cpus=8
8 then JSON file config.json contains qemu.memory=16 GB
cpus: 8
memory: "16 GB"

2.4 Allows specifying a configuration file

Want: The ambient tool allows the user to specify which configuration file to use.

Why: This is very convenient when one wants to try things and temporarily not use the default configuration file. This happens both for experimentation and for testing.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from special.yaml
3 given file other.yaml
4 when I run env ambient --config other.yaml config
5 then stdout contains "xyzzy"
6 then stdout contains ""artifacts_max_size": "1 TB""
7 when I run env ambient --no-config --config other.yaml config
8 then stdout doesn't contain "xyzzy"
9 then stdout contains ""artifacts_max_size": "1 TB""
executor: xyzzy
artifacts_max_size: 1TB

2.5 Notices a source directory that is not a directory

Want: The ambient tool notices if a projects file refers to a source directory that is not a directory.

Why: It's better to catch errors early than let tar notice them later.

Who: Lars

1 given an installed ambient
2 given an Ambient VM image ambient.qcow2
3 given file .config/ambient/config.yaml from config.yaml
4 given file bad-source.yaml
5 when I try to run env ambient projects --projects bad-source.yaml
6 then command fails
7 then stderr contains "bad-source.yaml"
8 then stderr contains "directory"
9 then stderr contains "/etc/passwd"
projects:
  bad_source:
    image: ambient.qcow2
    source: /etc/passwd
    plan:
    - action: shell
      shell: |
        true

2.6 Obeys TMPDIR

Want: The default temporary directory is $TMPDIR, if set, or /tmp otherwise.

Why: This is expected Unix behavior.

Who: Lars.

1 given an installed ambient
2 given file special.yaml
3 when I run env --unset TMPDIR ambient config
4 then stdout contains ""tmpdir": "/tmp""
5 when I run env TMPDIR=/xyzzy ambient config
6 then stdout contains ""tmpdir": "/xyzzy""

2.7 Sets environment variables

Want: Ambient sets specific environment variables when running actions in the VM. It may set others, but at least these.

Why: This is expected Unix behavior.

Who: Lars.

1 given an installed ambient
2 given an Ambient VM image ambient.qcow2
3 given file .config/ambient/config.yaml from config.yaml
4 given file env.yaml
5 given a directory srcdir
6 given a directory ambient-state
7 when I run ambient run --projects env.yaml
8 when I run ambient log env
9 then stdout contains " HOME="
10 then stdout contains " PATH="
11 then stdout contains " CARGO_TARGET_DIR="
12 then stdout contains " CARGO_HOME="
projects:
  env:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        env

2.8 Configures Git user

Want: Ambient configures the Git user in the VM.

Why: This is common in test suites.

Who: Lars.

1 given an installed ambient
2 given an Ambient VM image ambient.qcow2
3 given file .config/ambient/config.yaml from config.yaml
4 given file git.yaml
5 given a directory srcdir
6 given a directory ambient-state
7 when I run ambient run --projects git.yaml
8 when I run ambient log git
9 then stdout contains "GITUSER: Ambient CI //"
10 then stdout contains "GITEMAIL: ambient@example.com //"
projects:
  git:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        echo "GITUSER: $(git config --global user.name) //"
        echo "GITEMAIL: $(git config --global user.email) //"

2.9 Allows setting environment variables

Want: Ambient allows setting environment variables for later actions.

Why: This is common in test suites.

Who: Lars.

1 given an installed ambient
2 given an Ambient VM image ambient.qcow2
3 given file .config/ambient/config.yaml from config.yaml
4 given file setenv.yaml
5 given a directory srcdir
6 when I run ambient run --projects setenv.yaml
7 when I run ambient log setenv
8 then stdout contains "foo=bar/"
projects:
  setenv:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: setenv
      set:
        FOO: bar
    - action: shell
      shell: |
        env
        echo "foo=$FOO/"

2.10 Capture build log when running

Want: The stdout/stderr output of the commands executed by the CI run must be captured.

Why: This is the primary way for the user to know what happened during a run.

Who: Lars.

Note that we can't use . for the current directory as the source field in the project file due to how Subplot sets up a temporary data directory for each scenario, and sets TMPDIR to that directory. This would mean that . as source would create a tar archive in the temporary directory that tries to add itself, which creates an infinitely large tar archive.

1 given an installed ambient
2 given an Ambient VM image ambient.qcow2
3 given file .config/ambient/config.yaml from config.yaml
4 given file hello.yaml
5 given a directory srcdir
6 when I run env AMBIENT_LOG=trace ambient run --projects hello.yaml
7 then command is successful
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        echo well hello there, friend

2.11 Run fails if an action fails

Want: If an action fails, the whole CI run fails.

Why: This is fundemental to how a CI engine needs to work.

Who: Lars

1 given an installed ambient
2 given an Ambient VM image ambient.qcow2
3 given file .config/ambient/config.yaml from config.yaml
4 given file hello-fails.yaml
5 given a directory srcdir
6 when I try to run env AMBIENT_LOG=trace ambient run --projects hello-fails.yaml
7 then command fails
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        false

2.12 Store run log in project directory

Want: The run log should be stored persistently in the project state directory, and there is a way to retrieve it later.

Why: Run logs are often needed long after the run has happened.

Who: Lars.

1 given an installed ambient
2 given an Ambient VM image ambient.qcow2
3 given file .config/ambient/config.yaml from config.yaml
4 given file hello.yaml
5 given a directory srcdir
6 when I run env AMBIENT_LOG=debug ambient run --projects hello.yaml
7 when I run ambient log hello
8 then stdout contains "hello there, friend"

2.13 Relative filenames in project files

Want: When a project file has a relative filename, it's relative to the directory containing the project file.

Why: The user can then run ambient run from anywhere, not just the source directory.

Who: Lars.

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given a directory path/to/project/srcdir
4 given an Ambient VM image ambient.qcow2
5 when I run mv ambient.qcow2 path/to/project/
6 given file path/to/project/hello.yaml from hello.yaml
7 when I run env AMBIENT_LOG=trace ambient run --projects path/to/project/hello.yaml
8 then command is successful

2.14 Working directory in pre- and post-plan actions

Want: When a pre- or post-plan action is executed, the current working directory should be the source directory.

Why: Many actions can only usefully be executed in the source directory.

Who: Lars.

Note that we can't use . for the current directory as the source field in the project file due to how Subplot sets up a temporary data directory for each scenario, and sets TMPDIR to that directory. This would mean that . as source would create a tar archive in the temporary directory that tries to add itself, which creates an infinitely large tar archive.

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given an Ambient VM image ambient.qcow2
4 given file cwd.yaml
5 given a directory path/to/project/srcdir
6 when I run env AMBIENT_LOG=trace ambient run --projects cwd.yaml
7 then stderr matches regex INFO.*cwd:.*/path/to/project/srcdir
projects:
  pwd:
    image: ambient.qcow2
    source: path/to/project/srcdir
    pre_plan:
    - action: pwd
    post_plan:
    - action: pwd
    plan:
    - action: shell
      shell: |
        pwd

2.15 Run CI only for some projects

Want: When the projects file has more than one project, user can choose only specific ones to run CI for.

Why: User may be only interested in one project right now.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given an Ambient VM image ambient.qcow2
4 given file multi.yaml
5 given a directory foo
6 given a directory bar
7 when I run env AMBIENT_LOG=debug ambient run --dry-run --projects multi.yaml foo
8 then stderr contains "project foo: NOT running CI"
9 then stderr doesn't contain "project bar:"
projects:
  foo:
    image: ambient.qcow2
    source: foo
    plan:
    - action: shell
      shell: |
        echo foo project
  bar:
    image: ambient.qcow2
    source: bar
    plan:
    - action: shell
      shell: |
        echo bar project

2.16 List names of projects

Want: The user can list names of projects in a projects file.

Why: This is handy for checking and scripting.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given an Ambient VM image ambient.qcow2
4 given file multi.yaml
5 given a directory foo
6 given a directory bar
7 when I run ambient projects --oneline --projects multi.yaml
8 then stdout contains "{"projects":["bar","foo"]}"

2.17 List names of actions

Want: The user can list names of projects in a projects file.

Why: This is handy for checking and scripting.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given an Ambient VM image ambient.qcow2
4 when I run ambient actions
5 then stdout contains ""pre_actions": [ "
6 then stdout contains ""actions": [ "
7 then stdout contains ""post_actions": [ "

2.18 Cache persists between CI runs

Want: Cache data is persisted between CI runs.

Why: This allows incrementally building a project after changes.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given an Ambient VM image ambient.qcow2
4 given file cache.yaml
5 given a directory srcdir
 
6 when I run ambient run --projects cache.yaml
7 when I run ambient log hello
8 then stdout contains "counter is now 1."
 
9 when I run ambient run --projects cache.yaml
10 when I run ambient log hello
11 then stdout contains "counter is now 2."
 
12 when I run ambient run --projects cache.yaml
13 when I run ambient log hello
14 then stdout contains "counter is now 3."
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        cache=/workspace/cache
        counter="$cache/counter"
        if [ -e "$counter" ]; then
            n="$(cat "$counter")"
            n=$((n + 1))
            echo "$n" > "$counter"
        else
            echo 1 > "$counter"
        fi
        echo "counter is now $(cat "$counter")."
        find "$cache" -ls

2.19 Publish files via rsync

Want: Artifacts can be published via rsync to a server.

Why: This allows publishing many kinds of things.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given an Ambient VM image ambient.qcow2
4 given file rsync.yaml
5 given a directory srcdir
6 given file srcdir/data.dat from rsync.yaml
 
7 given a directory serverdir
8 when I run env AMBIENT_LOG=trace ambient run --projects rsync.yaml --target serverdir
9 then file serverdir/hello.txt contains "hello, world"
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        echo hello, world > /workspace/artifacts/hello.txt
    post_plan:
    - action: rsync

2.20 Check that http_get doesn't allow duplicate filenames

Want: The http_get action should now allow duplicate filenames.

Why: It's helpful to catch errors early.

Who: Lars

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given an Ambient VM image ambient.qcow2
4 given file http_get_duplicates.yaml
5 given a directory srcdir
 
6 when I try to run ambient projects --projects http_get_duplicates.yaml
7 then command fails
8 then stderr contains "xyzzy"
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    pre_plan:
    - action: http_get
      items:
      - url: http://example.com/foo
        filename: xyzzy
      - url: http://example.com/bar
        filename: xyzzy

2.21 Allow bad plans by turning off linting

Want: The user should allow turning off the linter.

Why: If the linter is wrong for any reason, CI must still run.

Who: Lars

Note that this scenario relies on the http_get_duplicates.yaml embedded file to trigger a lint.

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given file no-linter.yaml
4 given an Ambient VM image ambient.qcow2
5 given file http_get_duplicates.yaml
6 given a directory srcdir
 
7 when I try to run ambient --config no-linter.yaml projects --projects http_get_duplicates.yaml
8 then command succeeds
lint: false

2.22 Verify an image is acceptable to Ambient

2.23 Manage VM images

Want: ambient has an image store and the user can manage images in the image store via the tool.

Why: A user may need to have many images, perhaps with different operating systems or versions of thereof, or with different dependencies installed.

1 given an installed ambient
2 given file .config/ambient/config.yaml from config.yaml
3 given file dummy.qcow2
 
4 when I run ambient image list
5 then stdout is exactly ""
 
6 when I run ambient image import default dummy.qcow2 -d mcdummy -u https://example.com/image.qcow2
7 then file image-store/default.qcow2 exists
8 when I run ambient image list
9 then stdout is exactly "default "
 
10 when I run ambient image show default
11 then stdout contains ""name": "default""
12 then stdout contains ""filename": "default.qcow2""
13 then stdout contains ""sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b""
14 then stdout contains ""description": "mcdummy""
15 then stdout contains ""url": "https://example.com/image.qcow2""
16 then stdout contains ""import_date":"
17 then stdout contains ""uefi": true"
 
18 when I run env AMBIENT_LOG=debug ambient image remove default
19 when I run ambient image list
20 then stdout is exactly ""


Want: When constructing the source tree for the VM, ambient does not follow symbolic links.

Why: We don't want a project to create a link a file outside the source, but we do want to handle broken links. These are both best handled by not following symbolic links, when constructing the tar archive of the source tree.

1 given an installed ambient
2 given an Ambient VM image ambient.qcow2
3 given file .config/ambient/config.yaml from config.yaml
4 given file symlink.yaml
5 given a directory srcdir
6 when I run touch srcdir/README
7 when I run ln -s README srcdir/ok
8 when I run ln -s no-such-file srcdir/broken
9 when I run ln -s /etc/passwd srcdir/passwd
10 when I run env AMBIENT_LOG=trace ambient run --projects symlink.yaml
11 when I run ambient log hello
12 then stdout contains "./ok -> README"
13 then stdout contains "./broken -> no-such-file"
14 then stdout contains "./passwd -> /etc/passwd"
projects:
  hello:
    image: ambient.qcow2
    source: srcdir
    plan:
    - action: shell
      shell: |
        find . -ls

3 Acceptance criteria for ambient-execute-plan

ambient-execute-plan is the program that executes actions inside the virtual machine, specifically actions in the "runnable plan". The scenarios in this chapter verify that it work, without having to run a full virtual machine to do so.

3.1 Default log level is trace

Want: The default log level of ambient-execute-plan is TRACE.

Why: This means we get the all the log output we can, which is useful for remote debugging.

1 given an installed ambient
2 given file plan.yaml from shell-action.yaml
3 when I run ambient-execute-plan
4 then stderr contains "TRACE"

3.2 Fails if an action fails

Want: If an action fails, ambient-execute-plan should fail and not execute any later actions.

Why: Need this so that if something fails, a CI run will fail. This scenario only verifies that the ambient-execute-plan program fails.

We use a shell and an mkdir action for this. One executes an other program (the shell) and the other is implemented by ambient-execute-plan itself. If both work as they should, we assume the rest of the actions are OK too.

1 given an installed ambient
 
2 given file plan.yaml from shell-fails.yaml
3 when I try to run ambient-execute-plan
4 then command fails
5 then stdout contains "shell-action A"
6 then stdout doesn't contain "shell-action B"
 
7 given file plan.yaml from mkdir-fails.yaml
8 when I try to run ambient-execute-plan
9 then command fails
10 then stdout doesn't contain "shell-action B"
steps:
- action: shell
  shell: |
    echo shell-action A
    exit 1
- action: shell
  shell: |
    echo shell-action B
    exit 1
source_dir: .
deps_dir: .
artifacts_dir: .
steps:
- action: mkdir
  pathname: /not-allowed
- action: shell
  shell: |
    echo shell-action B
    exit 1
source_dir: .
deps_dir: .
artifacts_dir: .

3.3 Executes shell action

Want: action-execute-plan can execute a shell action with a shell script snippet.

1 given an installed ambient
2 given file plan.yaml from shell-action.yaml
3 when I run ambient-execute-plan
4 then stdout contains "hello, world"
steps:
- action: shell
  shell: |
    echo hello, world
source_dir: .
deps_dir: .
artifacts_dir: .

3.4 Executes pwd action

Want: action-execute-plan can execute a pwd action.

1 given an installed ambient
2 given file plan.yaml from pwd-action.yaml
3 when I run ambient-execute-plan
4 then stderr contains "cwd: /tmp"
steps:
- action: pwd
source_dir: .
deps_dir: .
artifacts_dir: .

3.5 Executes mkdir action

Want: action-execute-plan can execute a mkdir action.

Why: This is needed to set up the VM environment for running CI.

1 given an installed ambient
2 given file plan.yaml from mkdir-action.yaml
3 when I run ambient-execute-plan
4 then stdout contains "./xyzzy "
steps:
- action: mkdir
  pathname: xyzzy
- action: shell
  shell: |
    find -type d
source_dir: .
deps_dir: .
artifacts_dir: .

3.6 Executes tar_create and tar_extract actions

Want: action-execute-plan can execute a tar_create action and its complement tar_extract.

Why: This is needed to set up the VM environment for running CI.

1 given an installed ambient
2 given file plan.yaml from tar-create-action.yaml
3 given a directory data
4 when I write "" to file data/foo.txt
5 when I run ambient-execute-plan
6 then file data2/foo.txt exists
7 when I run tar tvf tar.tar
8 then stdout contains "./foo.txt"
steps:
- action: tar_create
  archive: tar.tar
  directory: data
- action: tar_extract
  archive: tar.tar
  directory: data2
source_dir: .
deps_dir: .
artifacts_dir: .

3.7 Executes a custom action

Want: action-execute-plan can execute a custom action provided in the source tree under test.

Why: This allows actions not built into Ambient.

1 given an installed ambient
2 given file plan.yaml from custom-action.yaml
3 given file .ambient/hello from hello-action.sh
4 when I run chmod +x .ambient/hello
5 when I run ambient-execute-plan
6 then stdout contains "hey there"
steps:
- action: custom
  name: hello
  args:
    greeting: hey
    whom: there
source_dir: .
deps_dir: .
artifacts_dir: .
#!/bin/sh
args="$(cat)"
greeting="$(echo -n "$args" | jq -r .greeting)"
whom="$(echo -n "$args" | jq -r .whom)"
echo "$greeting $whom"

3.8 Fails if a custom action fails

Want: action-execute-plan fails if a script for the custom action fails.

1 given an installed ambient
2 given file plan.yaml from custom-action-fails.yaml
3 given file .ambient/hello from hello-action-fails.sh
4 when I run chmod +x .ambient/hello
5 when I try to run ambient-execute-plan
6 then command fails
steps:
- action: custom
  name: hello
  args:
    greeting: hey
    whom: there
source_dir: .
deps_dir: .
artifacts_dir: .
#!/bin/sh
exit 1
~~