Zum Hauptinhalt springen
Connic
Test

Assertions

Assertions prüfen Agenten-Ausgaben, Tool-Aufrufe, Trigger-Payloads, das Verhalten untergeordneter Agenten und Abläufe mit menschlichen Freigaben.

Zuletzt aktualisiert

Expression DSL

Testausdrücke verwenden denselben sicheren Evaluator wie Tool Conditions und Approval Rules.

Expression-Syntax

Python-ähnliche Syntax: and, or, not; Vergleiche == != > < >= <=; Zugehörigkeit mit in und not in; Klammern zur Gruppierung; String-Literale in einfachen oder doppelten Anführungszeichen. Verschachtelte Objekte sind über Pfade mit Punktnotation wie context.user.role erreichbar. Ein einzelner Pfad wie context.active prüft, ob der Wert gesetzt und weder leer noch null oder false ist. Fehlt ein Feld, ist die Bedingung nicht erfüllt; es wird kein Fehler ausgelöst.

expected_result

output
Die Antwort des Agenten: Gültiges JSON wird automatisch geparst, andere Antworten bleiben als String erhalten. Je nach Antwortformat lässt sich zum Beispiel output.id == 10 oder "hi" in output prüfen.
error
Die Fehlermeldung des Runs oder None.
status
Einer der Werte "completed", "failed", "cancelled", "blocked" oder "awaiting_approval".
context.<key>
Das Dictionary context enthält die Werte, die der Builder in build() gespeichert hat. Bei Tests ohne Builder ist es leer. Die Werte lassen sich mit der Agentenantwort vergleichen, zum Beispiel über output.id == context.row_uuid. Siehe Dynamische Payload Builder.

expected_tool_calls

invocations
Anzahl der Aufrufe dieses Tools, deren Parameter den Filter erfüllen. Ohne Filter werden alle Aufrufe gezählt.
params.<key>
Argumente eines einzelnen Tool-Aufrufs. Damit lassen sich die Aufrufe nach bestimmten Argumentwerten filtern.
context.<key>
Dasselbe Builder-Dictionary wie oben. Zusammen mit params und invocations lässt sich damit prüfen, ob ein Tool die vom Builder erzeugte ID erhalten hat, zum Beispiel mit params.uuid == context.test_uuid.

approval_decisions.params

params.<key>
Parameter des Tool-Aufrufs, der auf eine Freigabe wartet. Damit lässt sich festlegen, für welchen Aufruf die Entscheidung gilt.
context.<key>
Werte aus dem Builder-Kontext. Damit lassen sich die Aufrufparameter in params mit den von build() erzeugten Testdaten vergleichen und die passende Entscheidung auswählen.
Bei einer Verknüpfung mit and auf oberster Ebene werden zwei Dinge getrennt geprüft: params.* filtert einzelne Tool-Aufrufe nach ihren Argumenten; invocations prüft, wie viele Aufrufe diesen Filter erfüllen. Beide Prüfungen können auf context.* zugreifen. Enthält der Ausdruck nur Bedingungen für params.*, muss mindestens ein Aufruf passen. Das entspricht invocations >= 1. Mehrere Einträge für dasselbe Tool prüfen unterschiedliche Argumentwerte unabhängig voneinander. Als Tool-Name ist der lokale Funktionsname oder die vollständige Referenz mit Modulpfad zulässig.

expected_result Beispiele

examples
tests:
  # Status prüfen (der häufigste Fall)
  - name: completes_cleanly
    payload: "ping"
    expected_result: status == "completed"

  # JSON Output über Attributzugriff
  - name: returns_id_10
    payload: '{"a": 4, "b": 6}'
    expected_result: output.id == 10

  # Substring-Match in einer Plain-Text-Antwort
  - name: greets_user
    payload: "hallo"
    expected_result: '"hallo" in output'

  # Numerischer Vergleich und boolesche Verknüpfung
  - name: high_confidence_only
    payload: "klassifiziere das"
    expected_result: output.confidence >= 0.8 and output.label != "unknown"

  # Negativfall: Ein Fehler ist das erwartete Ergebnis
  - name: rejects_invalid_input
    payload: '{"vendor": ""}'
    expected_result: status == "failed" and "missing vendor" in error

expected_tool_calls Beispiele

examples
tests:
  # Einfacher Name: Das Tool muss mindestens einmal aufgerufen werden
  - name: uses_calculator
    payload: '{"a": 4, "b": 6}'
    expected_tool_calls:
      - math.calculator.add

  # Mapping-Form: Expression für invocations
  - name: calls_add_at_least_five_times
    payload: '{"sum_many": [1,2,3,4,5,6]}'
    expected_tool_calls:
      - math.calculator.add: invocations >= 5

  # Genau einen Aufruf erzwingen
  - name: calls_send_exactly_once
    payload: "sende eine Zusammenfassung"
    expected_tool_calls:
      - notifications.send: invocations == 1

  # Über params.* nach Call-Argumenten filtern: Prüft, ob der Agent
  # wirklich die Operanden aus der Payload verwendet und keine erfunden hat.
  # Fehlt invocations, gilt implizit "mindestens ein passender Call".
  - name: calls_add_with_payload_args
    payload: '{"a": 4, "b": 6}'
    expected_tool_calls:
      - math.calculator.add: params.a == 4 and params.b == 6

  # Dasselbe Tool wiederholen, um jedes Argument-Set unabhängig zu prüfen.
  # Jeder Eintrag ist eine eigene Assertion. Der Test besteht, wenn der Agent
  # add(4, ...) und add(7, ...) jeweils einmal aufruft, unabhängig von der Reihenfolge.
  - name: calls_add_for_each_pair
    payload: "berechne 4+6 und 7+8 getrennt"
    expected_tool_calls:
      - math.calculator.add: invocations == 1 and params.a == 4
      - math.calculator.add: invocations == 1 and params.a == 7

  # Assertion zur relativen Reihenfolge: Diese Tools müssen im Trace
  # in dieser Reihenfolge erscheinen. Dazwischen dürfen andere Tool Calls liegen.
  - name: fetches_then_sends
    payload: "suche die Bestellung und benachrichtige den Kunden"
    expected_tool_call_order:
      - orders.lookup
      - notifications.send

  # Params mit dem Builder Context abgleichen. Der Builder fügt eine Zeile ein,
  # speichert ihre UUID in context["test_uuid"] und übergibt sie dem Agenten im Prompt.
  # Die Assertion schlägt fehl, wenn der Agent eine andere als die vom Builder
  # bereitgestellte Zeile abruft.
  - name: fetches_the_row_we_just_inserted
    builder: insert_then_query
    expected_result: output.row.id == context.test_uuid
    expected_tool_calls:
      - db.fetch_row: params.uuid == context.test_uuid and invocations == 1

  # Negative Assertion: Das Tool darf NICHT aufgerufen werden
  - name: plain_chat_no_tools
    payload: "sage hallo"
    expected_no_tool_calls:
      - math.calculator.add
      - notifications.send

Assertions für ausgelöste Agenten

Wenn der getestete Agent trigger_agent aufruft, siehe trigger_agent, kann expected_child_agents den Input, Output, die Tool-Aufrufe und weitere Trigger des untergeordneten Agenten prüfen.

Die Prüfung lässt sich verschachteln: Jeder Eintrag verwendet den Namen des aufgerufenen Agenten als Schlüssel. Über eigene expected_child_agents lassen sich auch die Agenten prüfen, die dieser Agent wiederum aufruft.

examples
tests:
  # Der Dispatcher Agent ruft trigger_agent("summarizer", ...) mit
  # wait_for_response=True auf. Dadurch stehen Output und Tool Calls
  # für die Child-Agent Assertions zur Verfügung.
  - name: dispatches_to_summarizer
    payload: '{"text": "..."}'
    expected_child_agents:
      summarizer:
        expected_payload: payload.text != ""
        expected_result: output.summary != ""
        expected_tool_calls:
          - llm.complete: invocations >= 1
        expected_no_tool_calls:
          - email.send

  # Trigger Payload mit dem Builder Context abgleichen, damit der Test fehlschlägt,
  # wenn der Agent statt der erhaltenen Fixture ID eine falsche weitergibt.
  # Funktioniert sowohl bei einem Dict (payload.field) als auch bei einem
  # String (Substring über payload_raw) des Parent-Agenten.
  - name: forwards_charge_id_unchanged
    builder: create_charge_then_refund
    builder_args:
      amount_cents: 4200
    expected_child_agents:
      billing-refunder:
        expected_payload: payload.charge_id == context.charge_id

  # Rekursiv: Einen Grandchild prüfen, den summarizer seinerseits auslöst.
  # Auf jeder Ebene wiederholt sich dieselbe Struktur: Agent-Namen als Keys,
  # dieselben Assertion-Felder und eigene expected_child_agents.
  - name: dispatches_summarizer_then_publisher
    payload: '{"text": "..."}'
    expected_child_agents:
      summarizer:
        expected_result: output.summary != ""
        expected_child_agents:
          publisher:
            expected_tool_calls:
              - kafka.publish: params.topic == "summaries"

  # Bei Fire-and-Forget Triggers (wait_for_response=False) kann das Ergebnis
  # nicht geprüft werden. Die Payload wird aber beim Call erfasst, deshalb
  # gilt expected_payload weiterhin.
  - name: fans_out_telemetry
    payload: '{"event": "checkout"}'
    expected_child_agents:
      telemetry-writer:
        expected_triggered: 1
        expected_payload: payload.event == "checkout"

Welche Prüfungen auf die Antwort warten müssen

  • wait_for_response=True: expected_result, expected_tool_calls, expected_tool_call_order, expected_no_tool_calls und verschachtelte expected_child_agents gelten vollständig.
  • wait_for_response=False: Fire-and-Forget. Prüfe den Call und seine Payload mit expected_triggered und expected_payload. Ergebnis-, Tool- und verschachtelte Assertions benötigen wait_for_response=True.

Trigger Payload prüfen

expected_payload verwendet dieselbe Syntax wie expected_result, prüft aber die übergebenen Eingabedaten. Mit payload.<key> lassen sich einzelne Felder prüfen, wenn der aufrufende Agent ein Dictionary oder einen JSON-String übergibt. Über payload_raw lässt sich prüfen, ob eine Texteingabe einen bestimmten Text enthält. context.<key> greift wie bei den anderen Assertions auf den Builder-Kontext zu. So prüft beispielsweise payload.charge_id == context.charge_id, ob die vom Builder erzeugte ID unverändert weitergegeben wurde. Diese Prüfung funktioniert auch bei Fire-and-Forget-Aufrufen, weil die Payload beim Aufruf erfasst wird.

trigger_agent_at wird in Tests als Fire-and-Forget behandelt. Der Test prüft den Namen des Agenten, die Anzahl der geplanten Aufrufe und die erfasste Payload. Er wartet nicht auf den geplanten Ausführungszeitpunkt.

Wie Aufrufe geprüft werden

  • Jeder Aufruf wird einzeln geprüft. Für jeden Aufruf von trigger_agent werden die zugehörigen Tool-Aufrufe und die Aufrufe weiterer Agenten separat ausgewertet.
  • Ein passender Aufruf genügt. Wird derselbe untergeordnete Agent mehrfach aufgerufen, besteht die Assertion, sobald einer der Aufrufe, auf dessen Antwort gewartet wird, die festgelegten Bedingungen erfüllt.
  • Der Builder-Kontext wird geteilt. context.<key> in expected_result oder expected_tool_calls eines untergeordneten Agenten liest dasselbe Builder-Dictionary wie der Testfall auf oberster Ebene. Eine in build() gespeicherte Fixture-ID ist dadurch auf jeder Ebene erreichbar.

Approvals testen (HITL)

approval_decisions liefert approve-, reject- oder timeout-Antworten für passende ausstehende Freigaben. Der Test wird nach der ausgewählten Entscheidung fortgesetzt, sofern die Freigaberegel dies erlaubt.

tests/billing-agent.yaml
tests:
  - name: approves_the_exact_refund
    builder: create_charge_then_refund
    approval_decisions:
      - tool: billing.refund
        params: params.charge_id == context.charge_id
        decision: approve
        reason: Durch diesen Test genehmigt
    expected_result: status == "completed"
  • Passende Freigabeanfrage auswählen. tool enthält die vollständige Tool-Referenz. Das optionale Feld params schränkt ein, für welche Aufrufparameter die Entscheidung gilt. Der Ausdruck kann auf params und den Builder-Kontext context zugreifen sowie true, false und null verwenden. Ohne dieses Feld gilt die Entscheidung unabhängig von den Parametern des Tools.
  • Ergebnis wählen. decision ist approve, reject oder timeout; reason ist optional. Ablehnungen und Timeouts berücksichtigen die Einstellung on_rejection des Approvals.
  • Jeden Eintrag höchstens einmal anwenden. Pro Aufruf wird ein Entscheidungseintrag höchstens einmal verwendet.
  • Vollständige Zuordnung verlangen. Mit strict_approval_decisions: true in defaults oder einem Testfall schlägt der Test fehl, wenn eine Freigabeanfrage keine passende Entscheidung hat oder ein Entscheidungseintrag ungenutzt bleibt. Standardmäßig ist diese Prüfung mit false deaktiviert.
  • Ohne Entscheidung auf Freigabe warten. Ist die strikte Prüfung deaktiviert und keine passende Entscheidung vorhanden, endet der Aufruf mit status == "awaiting_approval".