Skip to content

arazzo: support compound simple criteria, fix numeric 0/1 literals, and tolerate missing response values - #694

Draft
Shaun1 wants to merge 4 commits into
pb33f:mainfrom
ifc7:arazzo-criteria
Draft

Shaun1 wants to merge 4 commits into
pb33f:mainfrom
ifc7:arazzo-criteria

Conversation

@Shaun1

@Shaun1 Shaun1 commented Nov 5, 2026 •

Copy link
Copy Markdown

Summary

This PR brings simple Criterion Object evaluation closer to the Arazzo 1.0.1 specification. It makes four changes, one per commit:

  1. Compound conditions. &&, || and parentheses now work in simple conditions.
  2. Null operands give null operands defined comparison semantics
  3. Empty responses. When a request or response value is missing (for example, a body on an HTTP 204), the engine now uses null instead of failing the whole step. A reference to an unknown step, workflow, input or component is still an error.
  4. Numeric literals. 0 and 1 are now read as numbers, not booleans.

Motivation

1. Logical operators and grouping

The spec's Operators table lists && (and), || (or) and () (logical grouping) next to the comparison operators. Before this change, evaluateSimpleConditionString split a condition on the first comparison operator it found, so only a single left <op> right comparison worked. A condition like this one, which is common in auth and MFA flows, could not be expressed:

successCriteria:
  - condition: >-
      $statusCode == 204 ||
      ($statusCode == 200 &&
        ($response.body#/challengeName == 'NEW_PASSWORD_REQUIRED' ||
         $response.body#/challengeName == 'SOFTWARE_TOKEN_MFA'))

Simple conditions are now tokenized and parsed with a small recursive-descent parser. || binds more loosely than &&, and both bind more loosely than comparisons. A runtime expression ($...) is read as one operand up to the next whitespace, operator or parenthesis, so a character like > inside a JSON pointer path is not treated as an operator.

2. Null operand handling

Fixes an upstream bug: an explicit JSON null passed ordering comparisons such as > 0. Missing values now follow the same null rules.

Null has no ordering: >, <, >= and <= now return false when either side is
null, == is true only when both sides are null, and != is its inverse. A
null used as a bare operand is falsy, so the rest of the condition still
decides the outcome. A present but non-boolean bare operand is still an
error.

https://spec.openapis.org/arazzo/v1.0.1.html#literals

3. Missing response values on empty responses

Runtime expressions such as $response.body#/id or $response.header.Location can legitimately have no value. A 204 has no body, and an optional header may be missing. Before this change, any such miss aborted the step, even inside an || branch the author meant as a fallback.

Missing request and response values (headers, query and path parameters, JSON-pointer segments, and absent bodies or headers) now resolve to null:

  • In criteria: a missing operand becomes null. If a whole &&/|| branch fails only because a value was missing, that branch counts as false, so the rest of the condition still decides the result.
  • In step and workflow outputs: the output is set to null instead of failing the step.

This is deliberately narrow. Unknown steps, workflows, inputs, outputs, components and source descriptions are authoring mistakes, so they still raise errors. The existing atomicity guarantees are unchanged (TestEngine_StepOutputsAreNotPartiallyPopulatedOnEvaluationFailure, TestEngine_WorkflowOutputsAreNotPartiallyPopulatedOnEvaluationFailure): any non-optional failure still publishes no partial outputs, and workflow outputs are still committed only after every output has been evaluated.

4. 0 and 1 are numbers

The spec's Literals section defines true and false as boolean literals and treats numbers separately. Operands were previously passed to strconv.ParseBool before any numeric parsing. ParseBool accepts "0" and "1", so a condition like:

- condition: $response.body#/pagination/totalCount > 0

compared a number to false and never passed. Numbers are now parsed first, and only true/false (plus True/TRUE and False/FALSE) are read as booleans. As a side effect, t, f, T and F are no longer booleans. The spec doesn't define those literals, so this is a fix rather than a regression.

Out of scope

These spec operators and literals are still not supported in simple conditions and could be follow-ups:

  • ! (logical not)
  • [] (array index) and . (property dereference) as operators
  • null as a literal

Tests

  • TestEvaluateCriterion_SimpleCondition_BooleanOperators: nested &&/||/() conditions, including the 204-or-challenge example above.
  • TestEvaluateCriterion_NumericLiteralZeroIsNotBoolean: totalCount > 0 passes when the count is positive and fails when it is zero.
  • TestEvaluateSimpleOperand_ZeroAndOneAreIntegers: operand parsing for 0 and 1.
  • Each commit passes go vet ./arazzo/ and go test ./arazzo/... on its own; go test ./... passes on the final commit.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (07d595c) to head (46f2680).

Additional details and impacted files
@@            Coverage Diff             @@
##              main      #694    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          313       313            
  Lines        39555     39734   +179     
==========================================
+ Hits         39555     39734   +179     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Shaun1

Shaun1 commented Nov 5, 2026

Copy link
Copy Markdown
Author

Not sure if you are accepting external PRs or what the procedure is, but I'm using your lib in an arazzo tool I'm building and want to see if I can get these fixes for issues I ran into merged upstream. This should bring it closer in line with the full arazzo spec.

I see there is already a PR for handling 1/0 properly so I can rebase and modify if that gets pushed.

Thanks for the work you do on this library, love it.

@Shaun1
Shaun1 marked this pull request as draft November 5, 2026 04:37
Shaun1 and others added 4 commits November 4, 2026 22:03
The Arazzo Criterion Object operators table lists && (and), || (or) and
() (logical grouping) alongside the comparison operators, but simple
conditions were split on the first comparison operator found, so only a
single `left <op> right` comparison could be expressed.

Tokenize simple conditions and evaluate them with a small recursive-descent
parser. || binds looser than &&, which binds looser than comparisons.
Runtime expressions are consumed as one operand up to the next whitespace,
operator or parenthesis, so operator characters inside JSON pointer paths
are not misread. Malformed conditions (unterminated strings, unbalanced
parentheses, misplaced operators) are reported as errors.

https://spec.openapis.org/arazzo/v1.0.1.html#operators

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compareSimpleValues fell back to string comparison for non-numeric
operands and rendered nil as "<nil>". That sorts after digits, so an
explicit JSON null passed `$response.body#/count > 0`.

Null has no ordering: >, <, >= and <= now return false when either side is
null, == is true only when both sides are null, and != is its inverse. A
null used as a bare operand is falsy, so the rest of the condition still
decides the outcome. A present but non-boolean bare operand is still an
error.

https://spec.openapis.org/arazzo/v1.0.1.html#literals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runtime expressions such as $response.body#/id or $response.header.Location
can legitimately have no value, e.g. on an HTTP 204. Previously any such
miss aborted the step, even inside an || branch intended as a fallback.

Missing request/response values (headers, query and path parameters, JSON
pointer segments, absent bodies or headers) now resolve to null and follow
the null comparison rules: ordering comparisons fail, a bare operand is
falsy, and step/workflow outputs are set to null. Unknown steps,
workflows, inputs, outputs, components and source descriptions remain
errors, and output atomicity is preserved.

https://spec.openapis.org/arazzo/v1.0.1.html#runtime-expressions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
strconv.ParseBool accepts "0" and "1", and it ran before numeric parsing,
so conditions like `$response.body#/count > 0` compared a number to false
and never passed. Parse numbers first and only accept true/false
(also True/TRUE, False/FALSE) as boolean literals, per the Arazzo
Literals section.

https://spec.openapis.org/arazzo/v1.0.1.html#literals

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant