Skip to main content

Testing your manifests

Telo tests are themselves Telo manifests: a Run.Sequence (or a top-level Assert.Events / Assert.Manifest) drives the test, and the Telo.Application's targets: list invokes it. Because a test runs on the same kernel you deploy, the behaviour you assert in development is the behaviour you get in production.

Run a single test manifest with the telo CLI:

telo ./tests/add-two-numbers.yaml

Where to put tests​

Keep tests next to the manifest they exercise — a tests/ directory beside your application or library is the convention. Shared inputs (sample manifests a test loads via source: or include:) go in a __fixtures__/ subdirectory so a suite can skip them during discovery.

Anatomy of a test manifest​

A test is a Telo.Application that imports the stdlib modules it needs, then defines the resource named in targets:. Pin each import to an exact published version (@<version>):

kind: Telo.Application
metadata:
name: AddTwoNumbers
version: 1.0.0
imports:
Run: oci://ghcr.io/telorun/run@0.28.0
JavaScript: oci://ghcr.io/telorun/javascript@0.14.0
Assert: oci://ghcr.io/telorun/assert@0.21.1
targets:
- !ref TestAdd
---
kind: Run.Sequence
metadata:
name: TestAdd
steps:
- name: AddNumbers
inputs:
a: 5
b: 3
invoke:
kind: JavaScript.Script
outputType:
kind: Telo.JsonSchema
schema:
type: object
properties:
sum: { type: integer }
required: [sum]
code: |
function main({ a, b }) {
return { sum: a + b }
}
- name: VerifySum
inputs:
actual: !cel "steps.AddNumbers.result.sum"
expected: 8
invoke:
kind: Assert.Equals

Prefer Assert.Equals over Assert.Schema for checking an output: it reads as a plain expected value and compares the whole result at once. Declaring outputType on the script is what makes steps.AddNumbers.result.sum type-check rather than fall back to a permissive shape.

Running a whole suite​

Test.Suite (from the test module) discovers test manifests by glob, runs each in its own isolated kernel, and reports pass/fail. Write a suite application once and point telo at it:

kind: Telo.Application
metadata:
name: TestSuite
version: 1.0.0
variables:
include:
type: array
items: { type: string }
arg: include
default: ["**/tests/*.yaml"]
filter:
type: string
arg: { position: 0 }
default: ""
imports:
Test: oci://ghcr.io/telorun/test@0.13.0
targets:
- !ref RunAll
---
kind: Test.Suite
metadata:
name: RunAll
include: !cel "variables.include"
exclude:
- "**/__fixtures__/**"
filter: !cel "variables.filter"

The suite's command line is whatever the application declares with arg: — here a repeatable --include and a positional filter:

telo ./test-suite.yaml                                   # run everything
telo ./test-suite.yaml add # filter by substring (matches "add-two-numbers.yaml")
telo ./test-suite.yaml --include tests/add-two-numbers.yaml # exactly this manifest
telo ./test-suite.yaml --help # what the suite accepts

See the Test.Suite reference on the hub for the full field list.

Step shapes​

A test drives its assertions with the ordinary Run.Sequence grammar — invoke, if/then/else, while/do, switch/cases, try/catch/finally, throw, and the when: guard. All of it is documented once, in Composing behaviour; none of it is test-specific.

Two shapes come up constantly in tests:

# Assert on an earlier step's result — actual/expected are call inputs
- name: VerifySum
inputs:
actual: !cel "steps.AddNumbers.result.sum"
expected: 8
invoke:
kind: Assert.Equals

# Assert that something fails, and how
- name: Attempt
try:
- name: Boom
throw:
code: UNAUTHORIZED
message: "bad token"
catch:
- name: Inspect
inputs:
actual: !cel "error.code"
expected: UNAUTHORIZED
invoke:
kind: Assert.Equals

error.step inside a catch: names the enclosing step that failed (Attempt above), not the inner one.

Assertion kinds​

All exported by the assert stdlib (Assert: oci://ghcr.io/telorun/assert@0.21.1):

KindUse forWhere it goes
Assert.SchemaJSON Schema validation on inputsStep (invoke.kind)
Assert.EqualsDeep equalityStep
Assert.MatchesRegex match on a stringStep
Assert.ContainsSubstring / element / property containmentStep
Assert.EventsAsserts an ordered subsequence of kernel eventsTop-level resource
Assert.ManifestAsserts the analyzer emits specific diagnostic codes for a fixtureTop-level resource

Assert.Events​

Watches the kernel event stream and asserts an ordered subsequence:

kind: Assert.Events
metadata: { name: ExpectEvents }
filter:
- type: "*"
expect:
- event: JavaScript.Script.*.Invoked
payload:
outputs: { sum: 8 }
- event: Assert.Schema.*.Invoked
payload:
outputs: true

Assert.Manifest​

Asserts the analyzer's diagnostics on a fixture without running it:

kind: Assert.Manifest
metadata: { name: TestExtendsMalformed }
source: ./__fixtures__/extends-malformed.yaml
expect:
errors:
- code: EXTENDS_MALFORMED

Negative-path patterns​

Two shapes:

  1. Static-analysis errors — use Assert.Manifest with a fixture under __fixtures__/ and an expected errors[].code.
  2. Runtime errors — wrap the failing step in try/catch and assert against !cel "error.code", !cel "error.message", !cel "error.step", !cel "error.data.*".

See also​