From 3d47aade124891b7e053b4a520bfbe5caea6f7fd Mon Sep 17 00:00:00 2001 From: Kei Nakayama Date: Tue, 9 Jun 2026 01:26:17 +0900 Subject: [PATCH 1/3] Prefer YAML structures for JSON expectations Avoid making scenario authors embed JSON strings when typed YAML values already deserialize to the JSON comparison model. This keeps examples readable, reduces escaping, and locks the behavior with a focused parser test. --- README.md | 66 +++++++++++++++++++------ e2e/scenarios/positive/expect-json.yaml | 26 +++++++--- src/config/step.rs | 22 +++++++++ 3 files changed, 91 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 84f1566..a59d50c 100644 --- a/README.md +++ b/README.md @@ -86,18 +86,28 @@ workspace: cwd: . # run dir, relative to the scenario file temp: true # OR run in a fresh temp dir (0700 on Unix) steps: - - spawn: bash # or {command, cwd, env} + - spawn: bash - send: echo ${username} # stdin line; \r (CR) appended; ${var} expanded - send_raw: "y" # raw bytes, no newline - key: enter # named key -> control bytes - wait: 2s # or 500ms - - expect: {contains: hello, timeout: 30s} # wait for substring (timeout optional) - - expect_regex: {pattern: "hello.*world"} # wait for regex (regex::bytes) - - expect_not: {contains: error} # immediate: must NOT be present now - - expect_file_exists: {path: result.txt} - - expect_file_contains: {path: result.txt, contains: success} - - expect_file_not_contains: {path: result.txt, contains: error} - - expect_file_changed: {path: src/auth.ts} # content differs vs. spawn time + - expect: # wait for substring + contains: hello + timeout: 30s # optional + - expect_regex: # wait for regex (regex::bytes) + pattern: 'hello.*world' + - expect_not: # immediate: must NOT be present now + contains: error + - expect_file_exists: + path: result.txt + - expect_file_contains: + path: result.txt + contains: success + - expect_file_not_contains: + path: result.txt + contains: error + - expect_file_changed: # content differs vs. spawn time + path: src/auth.ts - expect_exit: 0 - expect_running: true ``` @@ -211,14 +221,38 @@ session directly.) `expect_json` extracts a JSON value and asserts on a field addressed by a path: ```yaml -- expect_json: {path: result.status, equals: success} -- expect_json: {path: result.message, contains: "expired"} -- expect_json: {path: result.items, exists: true} -- expect_json: {path: result.items.0.name, equals: first} # dotted array index -- expect_json: {path: 'result.items[0].name', equals: first} # bracket array index -- expect_json: {path: 'result["a.b"].value', equals: 7} # key containing a dot -- expect_json: {path: status, equals: passed, source: {file: report.json}} -- expect_json: {path: status, equals: ok, timeout: 5s} # wait for output JSON +- expect_json: + path: result.status + equals: success +- expect_json: + path: result.message + contains: expired +- expect_json: + path: result.items + exists: true +- expect_json: + path: result.items.0.name # dotted array index + equals: first +- expect_json: + path: 'result.items[0].name' # bracket array index + equals: first +- expect_json: + path: 'result["a.b"].value' # key containing a dot + equals: 7 +- expect_json: + path: status + equals: passed + source: + file: report.json +- expect_json: + path: status + equals: ok + timeout: 5s # wait for output JSON +- expect_json: # YAML value, JSON comparison + path: result + equals: + status: success + items: [1, 2] ``` - **Source.** With no `source` (the default `output`, which may also be written diff --git a/e2e/scenarios/positive/expect-json.yaml b/e2e/scenarios/positive/expect-json.yaml index 4b8195d..ec93bcb 100644 --- a/e2e/scenarios/positive/expect-json.yaml +++ b/e2e/scenarios/positive/expect-json.yaml @@ -1,7 +1,8 @@ # Dogfood: expect_json over a real PTY. A shell prints a JSON object after some # log noise; expect_json must extract the tail JSON block and assert on it via -# the dotted path grammar (equals, contains, exists). Proves tail-block -# extraction and path navigation work end to end when pitty runs itself. +# whole-document equality, dotted path navigation, contains, and exists. Proves +# tail-block extraction and path navigation work end to end when pitty runs +# itself. name: expect-json workspace: temp: true @@ -9,19 +10,30 @@ steps: - spawn: bash # Emit a line of noise, then a single-line JSON object. The expect below # ensures the JSON has been written before the assertions read the buffer. - - send: 'echo "LOG: starting"; echo "{\"status\": \"ok\", \"items\": [1,2,3], \"message\": \"token expired\"}"' + - send: >- + echo "LOG: starting"; + printf '%s\n' '{"result":{"status":"ok","items":[1,2,3],"message":"token expired"}}' - expect: contains: status timeout: 10s - expect_json: - path: status + path: result + equals: + status: ok + items: + - 1 + - 2 + - 3 + message: token expired + - expect_json: + path: result.status equals: ok - expect_json: - path: items.0 + path: result.items.0 equals: 1 - expect_json: - path: message + path: result.message contains: expired - expect_json: - path: items + path: result.items exists: true diff --git a/src/config/step.rs b/src/config/step.rs index 6c6c3d8..0f21d0f 100644 --- a/src/config/step.rs +++ b/src/config/step.rs @@ -932,6 +932,28 @@ mod tests { } } + #[test] + fn expect_json_equals_accepts_yaml_structures() { + // Nested YAML maps and lists must deserialize to the equivalent typed + // JSON value for whole-structure equality checks. + let s = step( + "expect_json:\n path: result\n equals:\n status: ok\n items:\n - 1\n - true", + ); + match s { + Step::ExpectJson(spec) => match spec.check { + JsonCheck::Equals(v) => assert_eq!( + v, + serde_json::json!({ + "status": "ok", + "items": [1, true] + }) + ), + other => panic!("expected Equals object, got {other:?}"), + }, + other => panic!("expected ExpectJson, got {other:?}"), + } + } + #[test] fn expect_json_contains_with_file_source_and_timeout() { // contains plus a file source and a timeout must all parse. From 5128a901f3f2fed66c27c5e95cf9b58526a81eb3 Mon Sep 17 00:00:00 2001 From: Kei Nakayama Date: Sat, 13 Jun 2026 07:01:30 +0900 Subject: [PATCH 2/3] docs: record YAML-structure expectations in CHANGELOG and SCHEMA PR #23 documents nested YAML maps/lists as the preferred form for expect_json equals, but left the Unreleased changelog entry and the SCHEMA.md reference untouched. Without them, the scenario-format reference would contradict the README and the release notes would silently omit a documented-behavior change. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 9 +++++++++ SCHEMA.md | 2 ++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d38531..97e0344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ releases; only 1.0.0 carries a release date. ## [Unreleased] +### Changed + +- **YAML structures are the documented form for JSON expectations.** The README + now writes step examples in block YAML and shows `expect_json`'s `equals` + taking a nested YAML map/list, which deserializes to the equivalent typed + JSON value for whole-structure equality. The nested form was already + accepted; it is now documented in the README and `SCHEMA.md` and locked in by + a parser test. + ## [1.2.1] - 2026-06-08 ### Added diff --git a/SCHEMA.md b/SCHEMA.md index dc6c60f..a17de05 100644 --- a/SCHEMA.md +++ b/SCHEMA.md @@ -149,6 +149,8 @@ delay, precede `expect_not` with a `wait`. **exactly one** of: - `equals: ` — typed equality (`200` is a number, `"200"` a string); + `` may be a nested YAML map or list, which deserializes to the + equivalent typed JSON structure and compares the whole value at `path`; - `contains: ` — substring of the value's string form; - `exists: true` — the path resolves to a value. From b0017ad221426309677a00a23b16e8335c1dec68 Mon Sep 17 00:00:00 2001 From: Kei Nakayama Date: Sat, 13 Jun 2026 22:15:14 +0900 Subject: [PATCH 3/3] fix: gate expect-json dogfood on a sentinel the PTY echo cannot satisfy PR #23 rewrote the JSON payload to use single quotes, which made the literal JSON appear verbatim in the PTY echo of the typed command. The expect guard matched the echoed "status" before bash ran, so expect_json raced against a half-emitted buffer and intermittently read [1,2,3] as the tail block (path 'result' does not exist). Gate on JSON_READY_42, produced by $((40+2)) so it appears only in real output, never in echo. Co-Authored-By: Claude --- e2e/scenarios/positive/expect-json.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/e2e/scenarios/positive/expect-json.yaml b/e2e/scenarios/positive/expect-json.yaml index ec93bcb..0a8ff4a 100644 --- a/e2e/scenarios/positive/expect-json.yaml +++ b/e2e/scenarios/positive/expect-json.yaml @@ -8,13 +8,19 @@ workspace: temp: true steps: - spawn: bash - # Emit a line of noise, then a single-line JSON object. The expect below - # ensures the JSON has been written before the assertions read the buffer. + # Emit a line of noise, then a single-line JSON object, then a sentinel built + # by shell arithmetic. The PTY echoes the typed command line verbatim, so the + # literal JSON above also appears in the buffer as echo; gating on a substring + # of that JSON (e.g. "status") would match the echo before bash runs and let + # expect_json read a half-emitted buffer. The sentinel is `$((40+2))` in the + # command but `42` only in real output, so JSON_READY_42 is reached only after + # bash has finished printing the JSON. - send: >- echo "LOG: starting"; - printf '%s\n' '{"result":{"status":"ok","items":[1,2,3],"message":"token expired"}}' + printf '%s\n' '{"result":{"status":"ok","items":[1,2,3],"message":"token expired"}}'; + echo "JSON_READY_$((40+2))" - expect: - contains: status + contains: JSON_READY_42 timeout: 10s - expect_json: path: result