Skip to content

Commit e1cf043

Browse files
jamierpondclaude
andauthored
Better error messages (#104)
* ok better errors * Address review feedback: extract truncateStr helper, add FindBareRefs tests, fix example line number - DRY up duplicated truncation logic into a UTF-8-safe truncateStr helper - Add table-driven tests for FindBareRefs covering bare refs, wrapped refs, edge cases - Fix incorrect line number in bare-variable-warning example comment (18 → 25) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix lint: remove unused maxLen parameter from truncateStr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 32fcf5f commit e1cf043

11 files changed

Lines changed: 362 additions & 230 deletions

File tree

PLAN.md

Lines changed: 49 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,262 +1,81 @@
1-
# Plan: `wait_for` Feature - DX Design
1+
# Plan: Better Error Messages & Debuggability
22

3-
## Overview
4-
5-
A new `wait_for` block that repeatedly polls an endpoint until a condition is satisfied. Designed for async server operations that require time to complete (job processing, webhooks, eventual consistency, etc.).
6-
7-
## DX Design
8-
9-
### Basic Syntax
10-
11-
```yaml
12-
yapi: v1
13-
url: ${url}/jobs/${job_id}
14-
method: GET
15-
16-
wait_for:
17-
until:
18-
- .status == "completed"
19-
period: 2s
20-
timeout: 60s
21-
```
22-
23-
### Fixed Period (Simple)
24-
25-
```yaml
26-
wait_for:
27-
until:
28-
- .status == "completed"
29-
period: 2s # Fixed time between attempts
30-
timeout: 60s # Total time limit
31-
```
32-
33-
### Exponential Backoff
34-
35-
```yaml
36-
wait_for:
37-
until:
38-
- .status == "completed"
39-
backoff:
40-
seed: 1s # Initial wait time
41-
multiplier: 2 # Each attempt waits multiplier * previous
42-
timeout: 60s # Total time limit
43-
```
44-
45-
Backoff example with `seed: 1s, multiplier: 2`:
46-
- Attempt 1 → wait 1s
47-
- Attempt 2 → wait 2s
48-
- Attempt 3 → wait 4s
49-
- Attempt 4 → wait 8s
50-
- ...continues until timeout
51-
52-
### Behavior
53-
54-
1. Execute the request
55-
2. If `until` conditions pass → success, stop polling
56-
3. If `until` conditions fail OR request errors (5xx, network) → wait (period or backoff), retry
57-
4. If `timeout` exceeded → fail with timeout error
58-
59-
**Error handling**: Intermediate failures (5xx, network errors, 4xx) are treated as "not ready yet" and polling continues. Only timeout causes failure.
60-
61-
**Timing**: Either `period` OR `backoff` must be specified (mutually exclusive).
3+
## Context
4+
Implementing WISHLIST.md items #1, #2, and #4 (removing #3 since `yapi send` already exists).
625

636
---
647

65-
## Use Cases
66-
67-
### 1. Single Request - Job Completion
68-
69-
```yaml
70-
yapi: v1
71-
url: ${url}/jobs/${job_id}
72-
method: GET
8+
## 1. WISHLIST.md cleanup
9+
Remove item #3 (yapi send) since it's already shipped.
7310

74-
wait_for:
75-
until:
76-
- .status == "completed" or .status == "failed"
77-
period: 2s
78-
timeout: 120s
79-
80-
expect:
81-
status: 200
82-
assert:
83-
- .status == "completed" # Final assertion after wait_for succeeds
84-
```
85-
86-
### 2. Chain Step - Async Workflow
87-
88-
```yaml
89-
yapi: v1
90-
chain:
91-
- name: create_job
92-
url: ${url}/jobs
93-
method: POST
94-
body:
95-
type: "data_export"
96-
expect:
97-
status: 202
98-
assert:
99-
- .job_id != null
100-
101-
- name: wait_for_job
102-
url: ${url}/jobs/${create_job.job_id}
103-
method: GET
104-
wait_for:
105-
until:
106-
- .status == "completed"
107-
backoff:
108-
seed: 1s
109-
multiplier: 2
110-
timeout: 300s
111-
expect:
112-
status: 200
113-
assert:
114-
- .download_url != null
115-
116-
- name: download_result
117-
url: ${wait_for_job.download_url}
118-
method: GET
119-
output_file: ./export.csv
120-
```
121-
122-
### 3. Webhook/Callback Waiting
11+
---
12312

124-
```yaml
125-
yapi: v1
126-
chain:
127-
- name: trigger_webhook
128-
url: ${url}/webhooks/trigger
129-
method: POST
13+
## 2. Warn on bare `$word.word` variable syntax (WISHLIST #1)
13014

131-
- name: check_received
132-
url: ${url}/webhooks/received
133-
method: GET
134-
wait_for:
135-
until:
136-
- . | length > 0
137-
- .[0].payload.event == "user.created"
138-
period: 1s
139-
timeout: 30s
140-
```
15+
The problem: `$step.field` (no braces) silently passes as a literal string instead of being substituted. Only `${step.field}` works.
14116

142-
### 4. Database Eventual Consistency
17+
**Changes:**
14318

144-
```yaml
145-
yapi: v1
146-
chain:
147-
- name: create_user
148-
url: ${url}/users
149-
method: POST
150-
body:
151-
email: "test@example.com"
152-
expect:
153-
status: 201
19+
- **`cli/internal/vars/vars.go`**: Add a `BareChainRef` regex that matches `$word.word` patterns that are NOT inside `${...}`.
20+
- **`cli/internal/vars/vars.go`**: Add `FindBareRefs(s string) []string` that returns the bare refs found.
21+
- **`cli/internal/validation/analyzer.go`**: In `analyzeParsed()`, call a new `warnBareChainRefs(text)` validation function that scans the raw YAML text for bare `$word.word` patterns and emits `SeverityWarning` diagnostics with line numbers and an actionable message like:
22+
`"possible bare variable reference '$step.field' -- did you mean '${step.field}'? Only the ${...} form is substituted."`
15423

155-
- name: verify_searchable
156-
url: ${url}/users/search?email=test@example.com
157-
method: GET
158-
wait_for:
159-
until:
160-
- . | length == 1
161-
period: 500ms
162-
timeout: 10s
163-
```
24+
This catches the problem at config analysis time (before execution), so users see the warning immediately -- even in `yapi validate`.
16425

16526
---
16627

167-
## Interaction with Existing Features
168-
169-
### With `expect`
28+
## 3. Show resolved request details in verbose chain execution (WISHLIST #2)
17029

171-
`wait_for` runs first. Once `until` conditions pass, `expect` runs on the final response:
30+
The problem: When a chain step fails, you can't see what values were actually sent because variable substitution is invisible.
17231

173-
```yaml
174-
wait_for:
175-
until:
176-
- .status != "pending" # Wait until not pending
177-
period: 1s
178-
timeout: 30s
32+
**Changes:**
17933

180-
expect:
181-
status: 200
182-
assert:
183-
- .status == "completed" # Then verify it's completed (not failed)
184-
```
34+
- **`cli/internal/runner/runner.go`**: Add `Verbose bool` field to `runner.Options`.
35+
- **`cli/internal/runner/runner.go`**: In `RunChain()`, after `interpolateConfig()` succeeds and before executing, if `opts.Verbose` is true, print the resolved config to stderr:
36+
- Resolved URL (with method)
37+
- Resolved headers
38+
- Resolved body (JSON-serialized if map, or raw if string)
39+
- Uses `fmt.Fprintf(os.Stderr, ...)` with `[VERBOSE]` prefix, consistent with the existing Logger pattern.
40+
- **`cli/cmd/yapi/run.go`**: Set `opts.Verbose = ctx.verbose` when building runner.Options in `executeRunE()`.
18541

186-
### With `timeout`
187-
188-
The existing `timeout` field is per-request. `wait_for.timeout` is total polling time:
189-
190-
```yaml
191-
timeout: 5s # Each poll attempt times out after 5s
42+
---
19243

193-
wait_for:
194-
until:
195-
- .ready == true
196-
period: 2s
197-
timeout: 60s # Total polling time limit
198-
```
44+
## 4. Print step responses in verbose chain mode (WISHLIST #4)
19945

200-
### With `delay`
46+
The problem: In chain execution, you only see the final failing step's output, not intermediate step responses.
20147

202-
`delay` happens before `wait_for` starts:
48+
**Changes:**
20349

204-
```yaml
205-
delay: 5s # Wait 5s before starting to poll
50+
- **`cli/internal/runner/runner.go`**: In `RunChain()`, after each step executes, if `opts.Verbose` is true, print the step's response details to stderr:
51+
- Status code
52+
- Response body (truncated at 1000 chars for readability)
53+
- Duration
20654

207-
wait_for:
208-
until:
209-
- .status == "done"
210-
period: 2s
211-
timeout: 30s
212-
```
55+
This replaces the need for a per-step `debug: true` field -- verbose mode shows everything, which is simpler and avoids new config surface area.
21356

21457
---
21558

216-
## Output During Polling
59+
## 5. Example files: `examples/debugging/`
21760

218-
When running with verbose/default output:
61+
Create example `.yapi.yml` files that demonstrate the improved debugging experience:
21962

220-
```
221-
[POLL] Attempt 1 - conditions not met, retrying in 2s...
222-
[POLL] Attempt 2 - conditions not met, retrying in 2s...
223-
[POLL] Attempt 3 - request failed (503), retrying in 2s...
224-
[POLL] Attempt 4 - conditions met!
225-
```
63+
- **`bare-variable-warning.yapi.yml`**: A chain that uses `$step.field` (bare) to trigger the new warning.
64+
- **`chain-verbose-demo.yapi.yml`**: A multi-step chain against jsonplaceholder with variables that shows how `--verbose` reveals resolved values.
65+
- **`assertion-failure-demo.yapi.yml`**: A request with an `expect:` block that will fail, showing the detailed assertion error output.
66+
- **`missing-key-demo.yapi.yml`**: A chain that references a nonexistent JSON key, showing the precise error path.
22667

22768
---
22869

229-
## Config Schema
230-
231-
```go
232-
type Backoff struct {
233-
Seed string `yaml:"seed"` // Initial wait, e.g., "1s"
234-
Multiplier float64 `yaml:"multiplier"` // e.g., 2
235-
}
236-
237-
type WaitFor struct {
238-
Until []string `yaml:"until"` // Required: JQ assertions
239-
Period string `yaml:"period,omitempty"` // Fixed interval, e.g., "2s"
240-
Backoff *Backoff `yaml:"backoff,omitempty"` // Exponential backoff
241-
Timeout string `yaml:"timeout"` // Required: total time limit
242-
}
243-
```
244-
245-
Added to `ConfigV1`:
246-
```go
247-
type ConfigV1 struct {
248-
// ... existing fields ...
249-
WaitFor *WaitFor `yaml:"wait_for,omitempty"`
250-
}
251-
```
252-
253-
---
70+
## Files changed
25471

255-
## Validation Rules
72+
| File | Change |
73+
|------|--------|
74+
| `WISHLIST.md` | Remove item #3 |
75+
| `cli/internal/vars/vars.go` | Add `BareChainRef` regex + `FindBareRefs()` function |
76+
| `cli/internal/validation/analyzer.go` | Add `warnBareChainRefs()`, call from `analyzeParsed()` |
77+
| `cli/internal/runner/runner.go` | Add `Verbose` to `Options`, add verbose logging in `RunChain()` |
78+
| `cli/cmd/yapi/run.go` | Thread `verbose` into `runner.Options` |
79+
| `examples/debugging/*.yapi.yml` | 4 new example files |
25680

257-
1. `until` is required and must have at least one assertion
258-
2. `timeout` is required and must be valid Go duration
259-
3. Exactly one of `period` OR `backoff` must be specified (mutually exclusive)
260-
4. If `period`: must be valid Go duration
261-
5. If `backoff`: `seed` must be valid Go duration, `multiplier` must be > 1
262-
6. All `until` expressions must be valid JQ
81+
**No new dependencies. No config schema changes. No breaking changes.**

WISHLIST.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# YAPI Wishlist
2+
3+
Issues encountered while writing MIDI clip note tests against the Ableton Live Remote Script.
4+
5+
## 1. Chain variable syntax not documented clearly
6+
7+
YAPI uses `${step.result.field}` (curly braces required), but the old docs and some examples show `$step.result.field` (bare dollar sign). The bare form is silently passed as a literal string rather than substituted, which makes debugging painful — the Remote Script receives the string `"$create_track.result.index"` instead of `3`.
8+
9+
**Wish:** Either support both forms, or emit a clear warning when a string matching `$word.word` is found without braces.
10+
11+
## 2. No way to inspect resolved variable values
12+
13+
When a chain step fails, the output shows the raw response but not the resolved parameter values that were sent. If variable substitution silently fails (e.g., wrong syntax), you can't tell from the output.
14+
15+
**Wish:** Show the resolved request body in verbose/debug mode so you can verify what was actually sent over the wire.
16+
17+
## 3. No way to print step results mid-chain for debugging
18+
19+
When a chain step fails, you get the response for the failing step but not intermediate steps (unless you scroll through the full output). Being able to mark a step as `debug: true` to print its full response would help.
20+
21+
**Wish:** `debug: true` on chain steps to always print the full response body, or a `--verbose` flag that prints all step responses.

cli/cmd/yapi/run.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ func (app *rootCommand) executeRunE(ctx runContext) error {
206206
NoColor: app.noColor,
207207
BinaryOutput: app.binaryOutput,
208208
Insecure: app.insecure,
209+
Verbose: ctx.verbose,
209210
ConfigFilePath: ctx.path,
210211
StrictEnv: ctx.strictEnv,
211212
}

0 commit comments

Comments
 (0)