relay-flow 0.2.11-alpha → 0.3.1-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/cmd/relay-flow/main.go +114 -19
- package/cmd/relay-flow/observability_test.go +118 -6
- package/internal/execution/goworkflows/activities.go +11 -6
- package/internal/execution/goworkflows/engine_test.go +52 -0
- package/internal/execution/goworkflows/fakes_test.go +14 -7
- package/internal/execution/goworkflows/interpreter.go +11 -7
- package/internal/execution/goworkflows/node_runtime_test.go +8 -8
- package/internal/execution/temporal/activities.go +11 -6
- package/internal/execution/temporal/end_cleanup_order_test.go +147 -0
- package/internal/execution/temporal/interpreter.go +11 -7
- package/internal/harness/opencode/opencode.go +7 -5
- package/internal/harness/opencode/opencode_test.go +1 -1
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/harness/opencode/validation_test.go +106 -0
- package/internal/harness/pi/pi.go +18 -8
- package/internal/harness/pi/pi_test.go +1 -1
- package/internal/harness/pi/prompt_test.go +2 -2
- package/internal/runner/clean.go +38 -0
- package/internal/runner/clean_test.go +84 -0
- package/internal/runner/herdr/herdr.go +24 -10
- package/internal/runner/herdr/herdr_test.go +66 -0
- package/internal/runner/orca/orca.go +16 -2
- package/internal/runner/orca/orca_test.go +103 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,13 +24,13 @@ same `relay-flow-plugin` package with host-specific entrypoints.
|
|
|
24
24
|
**OpenCode**
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
|
-
opencode plugin relay-flow-plugin@0.
|
|
27
|
+
opencode plugin relay-flow-plugin@0.3.1-alpha
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
**Pi**
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
pi install npm:relay-flow-plugin@0.
|
|
33
|
+
pi install npm:relay-flow-plugin@0.3.1-alpha
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Pi loads the package's `pi.ts` extension from its manifest. Do not add
|
|
@@ -160,7 +160,7 @@ OpenCode plugin configuration uses both entrypoints. The server entrypoint is li
|
|
|
160
160
|
```json
|
|
161
161
|
{
|
|
162
162
|
"$schema": "https://opencode.ai/config.json",
|
|
163
|
-
"plugin": ["relay-flow-plugin@0.
|
|
163
|
+
"plugin": ["relay-flow-plugin@0.3.1-alpha"]
|
|
164
164
|
}
|
|
165
165
|
```
|
|
166
166
|
|
|
@@ -169,7 +169,7 @@ The native HITL approval entrypoint is listed in `.opencode/tui.json`:
|
|
|
169
169
|
```json
|
|
170
170
|
{
|
|
171
171
|
"$schema": "https://opencode.ai/tui.json",
|
|
172
|
-
"plugin": ["relay-flow-plugin@0.
|
|
172
|
+
"plugin": ["relay-flow-plugin@0.3.1-alpha"]
|
|
173
173
|
}
|
|
174
174
|
```
|
|
175
175
|
|
|
@@ -185,7 +185,7 @@ Pi plugin: install the same published package manually in Pi's global package
|
|
|
185
185
|
settings before starting a Pi harness session:
|
|
186
186
|
|
|
187
187
|
```sh
|
|
188
|
-
pi install npm:relay-flow-plugin@0.
|
|
188
|
+
pi install npm:relay-flow-plugin@0.3.1-alpha
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
Relay-flow does not install or configure the package automatically. Pi resolves
|
|
@@ -629,7 +629,7 @@ EXPECTED RESULT: ...
|
|
|
629
629
|
|
|
630
630
|
The labels above are fixed; configurable templates do not change the parsed report contract. The plugin submits one `report` object containing both lower-camel `summary` and `feedback` objects. Relay-flow validates that complete shape once, renders `summaryReport` through the task system's summary-comment template on the current mailbox, and renders `feedbackReport` through its feedback-comment template on only the selected next mailbox. `None` is the literal marker for an intentionally empty section. When `NEXT STEP` is `end`, every FEEDBACK field must be `None` and no feedback comment is written.
|
|
631
631
|
|
|
632
|
-
The plugin delivers `{runId, node, reportId, report}` as one JSON object via `relay-flow report` stdin with the shared backoff (initial 2s, factor 2, jitter 0.2, max 5m) until acknowledged. It derives `reportId` from the harness session/message identity. Duplicate/stale reports are acked safely with no repeated graph effects. Invalid agent output is nudged;
|
|
632
|
+
The plugin delivers `{runId, node, reportId, report}` as one JSON object via `relay-flow report` stdin with the shared backoff (initial 2s, factor 2, jitter 0.2, max 5m) until acknowledged. It derives `reportId` from the harness session/message identity. Duplicate/stale reports are acked safely with no repeated graph effects. Invalid agent output is nudged; ordinary, missing, or empty HITL output stays silent, while partial report-shaped HITL output is corrected and a valid HITL report opens the native TUI approval dialog. Relay-flow HITL approval does not use OpenCode's Question tool.
|
|
633
633
|
|
|
634
634
|
---
|
|
635
635
|
|
package/cmd/relay-flow/main.go
CHANGED
|
@@ -180,6 +180,8 @@ Run:
|
|
|
180
180
|
relay-flow run restart --ticket <key>
|
|
181
181
|
relay-flow run cancel --ticket <key>
|
|
182
182
|
|
|
183
|
+
Use relay-flow <command> --help for command-specific details.
|
|
184
|
+
|
|
183
185
|
Examples:
|
|
184
186
|
relay-flow workflow list --json
|
|
185
187
|
relay-flow run get --ticket PAY-101
|
|
@@ -235,7 +237,14 @@ func printScopedHelp(args []string, w io.Writer) int {
|
|
|
235
237
|
case "init":
|
|
236
238
|
fmt.Fprintln(w, "Usage: relay-flow init [flags]")
|
|
237
239
|
fmt.Fprintln(w, "\nInitialize relay-flow and its durable state.")
|
|
238
|
-
fmt.Fprintln(w, "Flags:
|
|
240
|
+
fmt.Fprintln(w, "Flags:")
|
|
241
|
+
fmt.Fprintln(w, " --task-plugin <name> Task-system plugin (required with runner and harness plugin flags).")
|
|
242
|
+
fmt.Fprintln(w, " --runner-plugin <name> Runner plugin (required with task and harness plugin flags).")
|
|
243
|
+
fmt.Fprintln(w, " --harness-plugin <name> Harness plugin (required with task and runner plugin flags).")
|
|
244
|
+
fmt.Fprintln(w, " --executor-plugin <name> Durable executor: goworkflows or temporal.")
|
|
245
|
+
fmt.Fprintln(w, " --temporal-address <host:port> Temporal server address; applies only to --executor-plugin temporal (optional, defaults to localhost:7233).")
|
|
246
|
+
fmt.Fprintln(w, " --temporal-namespace <name> Temporal namespace/team name; applies only to --executor-plugin temporal (required).")
|
|
247
|
+
fmt.Fprintln(w, " --force Update plugin selections while preserving existing durable state.")
|
|
239
248
|
fmt.Fprintln(w, "Example: relay-flow init --task-plugin jira --runner-plugin orca --harness-plugin opencode")
|
|
240
249
|
case "serve":
|
|
241
250
|
fmt.Fprintln(w, "Usage: relay-flow serve [--recover] [--debug] [--background]")
|
|
@@ -251,11 +260,15 @@ func printScopedHelp(args []string, w io.Writer) int {
|
|
|
251
260
|
fmt.Fprintln(w, "Example: relay-flow report < report.json")
|
|
252
261
|
case "task":
|
|
253
262
|
fmt.Fprintln(w, "Usage: relay-flow task <command>")
|
|
254
|
-
fmt.Fprintln(w, "\
|
|
263
|
+
fmt.Fprintln(w, "\nRun task-system commands through the configured task plugin.")
|
|
264
|
+
fmt.Fprintln(w, "Commands:")
|
|
265
|
+
fmt.Fprintln(w, " auth Authenticate the configured task-system plugin.")
|
|
266
|
+
fmt.Fprintln(w, "Use relay-flow task <subcommand> --help for command-specific details.")
|
|
255
267
|
fmt.Fprintln(w, "Example: relay-flow task auth")
|
|
256
268
|
case "task auth":
|
|
257
269
|
fmt.Fprintln(w, "Usage: relay-flow task auth [task-plugin options]")
|
|
258
|
-
fmt.Fprintln(w, "\
|
|
270
|
+
fmt.Fprintln(w, "\nAuthenticate the configured task-system plugin.")
|
|
271
|
+
fmt.Fprintln(w, "Arguments: task-plugin options are passed through unchanged; relay-flow adds no flags.")
|
|
259
272
|
fmt.Fprintln(w, "Example: relay-flow task auth")
|
|
260
273
|
case "runtime-register":
|
|
261
274
|
fmt.Fprintln(w, "Usage: relay-flow runtime-register < session.json")
|
|
@@ -267,61 +280,143 @@ func printScopedHelp(args []string, w io.Writer) int {
|
|
|
267
280
|
fmt.Fprintln(w, "Example: relay-flow version")
|
|
268
281
|
case "workflow":
|
|
269
282
|
fmt.Fprintln(w, "Usage: relay-flow workflow <command>")
|
|
270
|
-
fmt.Fprintln(w, "\
|
|
271
|
-
fmt.Fprintln(w, "
|
|
272
|
-
fmt.Fprintln(w, "
|
|
283
|
+
fmt.Fprintln(w, "\nManage validated workflow definitions and inspect their runs.")
|
|
284
|
+
fmt.Fprintln(w, "Commands:")
|
|
285
|
+
fmt.Fprintln(w, " submit Create or replace a workflow definition.")
|
|
286
|
+
fmt.Fprintln(w, " Usage: relay-flow workflow submit --file <path>")
|
|
287
|
+
fmt.Fprintln(w, " Example: relay-flow workflow submit --file workflow.yaml")
|
|
288
|
+
fmt.Fprintln(w, " remove Remove a workflow with no active runs.")
|
|
289
|
+
fmt.Fprintln(w, " Usage: relay-flow workflow remove --name <name>")
|
|
290
|
+
fmt.Fprintln(w, " Example: relay-flow workflow remove --name basicFlow")
|
|
291
|
+
fmt.Fprintln(w, " list List configured workflows and latest execution summaries.")
|
|
292
|
+
fmt.Fprintln(w, " Usage: relay-flow workflow list [--json] [--no-color]")
|
|
293
|
+
fmt.Fprintln(w, " Example: relay-flow workflow list --json")
|
|
294
|
+
fmt.Fprintln(w, " get Show a workflow graph and recent runs.")
|
|
295
|
+
fmt.Fprintln(w, " Usage: relay-flow workflow get --name <name> [--json] [--no-color]")
|
|
296
|
+
fmt.Fprintln(w, " Example: relay-flow workflow get --name basicFlow")
|
|
297
|
+
fmt.Fprintln(w, "Use relay-flow workflow <subcommand> --help for flag details.")
|
|
273
298
|
case "workflow submit":
|
|
274
299
|
fmt.Fprintln(w, "Usage: relay-flow workflow submit --file <path>")
|
|
275
300
|
fmt.Fprintln(w, "\nCreate or replace a validated workflow definition.")
|
|
301
|
+
fmt.Fprintln(w, "Flags:")
|
|
302
|
+
fmt.Fprintln(w, " --file <path> Workflow YAML file to submit (required).")
|
|
276
303
|
fmt.Fprintln(w, "Example: relay-flow workflow submit --file workflow.yaml")
|
|
277
304
|
case "workflow remove":
|
|
278
305
|
fmt.Fprintln(w, "Usage: relay-flow workflow remove --name <name>")
|
|
279
306
|
fmt.Fprintln(w, "\nRemove a workflow with no active runs.")
|
|
307
|
+
fmt.Fprintln(w, "Flags:")
|
|
308
|
+
fmt.Fprintln(w, " --name <name> Workflow name (required).")
|
|
280
309
|
fmt.Fprintln(w, "Example: relay-flow workflow remove --name basicFlow")
|
|
281
310
|
case "workflow list":
|
|
282
311
|
fmt.Fprintln(w, "Usage: relay-flow workflow list [--json] [--no-color]")
|
|
283
312
|
fmt.Fprintln(w, "\nList configured workflows and their latest execution summaries.")
|
|
313
|
+
fmt.Fprintln(w, "Flags:")
|
|
314
|
+
fmt.Fprintln(w, " --json Print stable JSON instead of the human-readable table.")
|
|
315
|
+
fmt.Fprintln(w, " --no-color Disable terminal color decoration.")
|
|
284
316
|
fmt.Fprintln(w, "Example: relay-flow workflow list --json")
|
|
285
317
|
case "workflow get":
|
|
286
318
|
fmt.Fprintln(w, "Usage: relay-flow workflow get --name <name> [--json] [--no-color]")
|
|
287
|
-
fmt.Fprintln(w, "\nShow validated static graph and recent runs.
|
|
319
|
+
fmt.Fprintln(w, "\nShow a workflow's validated static graph and recent runs.")
|
|
320
|
+
fmt.Fprintln(w, "Flags:")
|
|
321
|
+
fmt.Fprintln(w, " --name <name> Workflow name (required).")
|
|
322
|
+
fmt.Fprintln(w, " --json Print stable JSON instead of the human-readable detail view.")
|
|
323
|
+
fmt.Fprintln(w, " --no-color Disable terminal color decoration.")
|
|
288
324
|
fmt.Fprintln(w, "Example: relay-flow workflow get --name basicFlow")
|
|
289
325
|
case "repo":
|
|
290
326
|
fmt.Fprintln(w, "Usage: relay-flow repo <command>")
|
|
291
|
-
fmt.Fprintln(w, "\
|
|
292
|
-
fmt.Fprintln(w, "
|
|
293
|
-
fmt.Fprintln(w, "
|
|
327
|
+
fmt.Fprintln(w, "\nManage registered runner repositories and task-system configuration.")
|
|
328
|
+
fmt.Fprintln(w, "Commands:")
|
|
329
|
+
fmt.Fprintln(w, " register Register a runner repository and its task-system configuration.")
|
|
330
|
+
fmt.Fprintln(w, " Usage: relay-flow repo register [--name <name>] [--path <path>] [--set key=value ...]")
|
|
331
|
+
fmt.Fprintln(w, " Example (Beads): relay-flow repo register --name payments --path /work/payments --set beadsDir=/work/payments/.beads")
|
|
332
|
+
fmt.Fprintln(w, " remove Remove a registered repository.")
|
|
333
|
+
fmt.Fprintln(w, " Usage: relay-flow repo remove --name <name>")
|
|
334
|
+
fmt.Fprintln(w, " Example: relay-flow repo remove --name payments")
|
|
335
|
+
fmt.Fprintln(w, " list List registered repositories.")
|
|
336
|
+
fmt.Fprintln(w, " Usage: relay-flow repo list")
|
|
337
|
+
fmt.Fprintln(w, " Example: relay-flow repo list")
|
|
338
|
+
fmt.Fprintln(w, " get Show one registered repository as JSON.")
|
|
339
|
+
fmt.Fprintln(w, " Usage: relay-flow repo get --name <name>")
|
|
340
|
+
fmt.Fprintln(w, " Example: relay-flow repo get --name payments")
|
|
341
|
+
fmt.Fprintln(w, "Use relay-flow repo <subcommand> --help for flag details.")
|
|
294
342
|
case "repo register":
|
|
295
|
-
fmt.Fprintln(w, "Usage: relay-flow repo register [--name <name> --path <path> --set key=value]")
|
|
343
|
+
fmt.Fprintln(w, "Usage: relay-flow repo register [--name <name>] [--path <path>] [--set key=value ...]")
|
|
296
344
|
fmt.Fprintln(w, "\nRegister a runner repository and task-system configuration.")
|
|
297
|
-
fmt.Fprintln(w, "
|
|
298
|
-
|
|
299
|
-
fmt.
|
|
300
|
-
fmt.
|
|
345
|
+
fmt.Fprintln(w, "Flags:")
|
|
346
|
+
fmt.Fprintln(w, " --name <name> Repository name. Optional for interactive registration; required in flagged/non-interactive mode.")
|
|
347
|
+
fmt.Fprintln(w, " --path <path> Local repository path. Optional for interactive registration; required in flagged/non-interactive mode.")
|
|
348
|
+
fmt.Fprintln(w, " --set key=value Optional, repeatable task-system configuration override. Key and value must be non-empty; duplicate keys are rejected.")
|
|
349
|
+
fmt.Fprintln(w, "\nInteractive mode: omit --name, --path, and --set; a TTY is required.")
|
|
350
|
+
fmt.Fprintln(w, "Flagged mode: --name and --path are required. Repeat --set once per task-system key; the selected plugin validates keys and values, and derived keys cannot be overridden.")
|
|
351
|
+
fmt.Fprintln(w, "Example (Beads): relay-flow repo register --name payments --path /work/payments --set beadsDir=/work/payments/.beads")
|
|
352
|
+
case "repo remove":
|
|
353
|
+
fmt.Fprintln(w, "Usage: relay-flow repo remove --name <name>")
|
|
354
|
+
fmt.Fprintln(w, "\nRemove a registered repository.")
|
|
355
|
+
fmt.Fprintln(w, "Flags:")
|
|
356
|
+
fmt.Fprintln(w, " --name <name> Registered repository name (required).")
|
|
357
|
+
fmt.Fprintln(w, "No other flags are supported.")
|
|
358
|
+
fmt.Fprintln(w, "Example: relay-flow repo remove --name payments")
|
|
301
359
|
case "repo list":
|
|
302
360
|
fmt.Fprintln(w, "Usage: relay-flow repo list")
|
|
303
361
|
fmt.Fprintln(w, "\nList registered repositories.")
|
|
362
|
+
fmt.Fprintln(w, "Flags: none.")
|
|
304
363
|
fmt.Fprintln(w, "Example: relay-flow repo list")
|
|
364
|
+
case "repo get":
|
|
365
|
+
fmt.Fprintln(w, "Usage: relay-flow repo get --name <name>")
|
|
366
|
+
fmt.Fprintln(w, "\nShow one registered repository and its task-system configuration as JSON.")
|
|
367
|
+
fmt.Fprintln(w, "Flags:")
|
|
368
|
+
fmt.Fprintln(w, " --name <name> Registered repository name (required).")
|
|
369
|
+
fmt.Fprintln(w, "No other flags are supported.")
|
|
370
|
+
fmt.Fprintln(w, "Example: relay-flow repo get --name payments")
|
|
305
371
|
case "run":
|
|
306
372
|
fmt.Fprintln(w, "Usage: relay-flow run <command>")
|
|
307
|
-
fmt.Fprintln(w, "\
|
|
308
|
-
fmt.Fprintln(w, "
|
|
309
|
-
fmt.Fprintln(w, "
|
|
373
|
+
fmt.Fprintln(w, "\nInspect and control durable ticket runs.")
|
|
374
|
+
fmt.Fprintln(w, "Commands:")
|
|
375
|
+
fmt.Fprintln(w, " list List runs, optionally filtered by repository, workflow, ticket, or active state.")
|
|
376
|
+
fmt.Fprintln(w, " Usage: relay-flow run list [--repo <name>] [--workflow <name>] [--ticket <key>] [--active] [--json] [--no-color]")
|
|
377
|
+
fmt.Fprintln(w, " Example: relay-flow run list --active --json")
|
|
378
|
+
fmt.Fprintln(w, " get Show one run's execution detail and actual step visits.")
|
|
379
|
+
fmt.Fprintln(w, " Usage: relay-flow run get --ticket <key> [--json] [--no-color]")
|
|
380
|
+
fmt.Fprintln(w, " Example: relay-flow run get --ticket PAY-101")
|
|
381
|
+
fmt.Fprintln(w, " restart Start a fresh attempt for a canceled run.")
|
|
382
|
+
fmt.Fprintln(w, " Usage: relay-flow run restart --ticket <key>")
|
|
383
|
+
fmt.Fprintln(w, " Example: relay-flow run restart --ticket PAY-101")
|
|
384
|
+
fmt.Fprintln(w, " cancel Permanently cancel the current execution attempt.")
|
|
385
|
+
fmt.Fprintln(w, " Usage: relay-flow run cancel --ticket <key> [--reason <text>]")
|
|
386
|
+
fmt.Fprintln(w, " Example: relay-flow run cancel --ticket PAY-101 --reason "+`"operator request"`)
|
|
387
|
+
fmt.Fprintln(w, "Use relay-flow run <subcommand> --help for flag details.")
|
|
310
388
|
case "run list":
|
|
311
389
|
fmt.Fprintln(w, "Usage: relay-flow run list [--repo <name>] [--workflow <name>] [--ticket <key>] [--active] [--json] [--no-color]")
|
|
312
|
-
fmt.Fprintln(w, "\nList durable runs
|
|
390
|
+
fmt.Fprintln(w, "\nList durable runs, with optional filters.")
|
|
391
|
+
fmt.Fprintln(w, "Flags:")
|
|
392
|
+
fmt.Fprintln(w, " --repo <name> Filter by registered repository name.")
|
|
393
|
+
fmt.Fprintln(w, " --workflow <name> Filter by workflow name.")
|
|
394
|
+
fmt.Fprintln(w, " --ticket <key> Filter by task ticket key.")
|
|
395
|
+
fmt.Fprintln(w, " --active Show only active runs.")
|
|
396
|
+
fmt.Fprintln(w, " --json Print stable JSON instead of the human-readable table.")
|
|
397
|
+
fmt.Fprintln(w, " --no-color Disable terminal color decoration.")
|
|
313
398
|
fmt.Fprintln(w, "Example: relay-flow run list --active --json")
|
|
314
399
|
case "run get":
|
|
315
400
|
fmt.Fprintln(w, "Usage: relay-flow run get --ticket <key> [--json] [--no-color]")
|
|
316
401
|
fmt.Fprintln(w, "\nShow an Argo-shaped execution detail view and actual step visits.")
|
|
402
|
+
fmt.Fprintln(w, "Flags:")
|
|
403
|
+
fmt.Fprintln(w, " --ticket <key> Task ticket key (required).")
|
|
404
|
+
fmt.Fprintln(w, " --json Print stable JSON instead of the human-readable detail view.")
|
|
405
|
+
fmt.Fprintln(w, " --no-color Disable terminal color decoration.")
|
|
317
406
|
fmt.Fprintln(w, "Example: relay-flow run get --ticket PAY-101")
|
|
318
407
|
case "run restart":
|
|
319
408
|
fmt.Fprintln(w, "Usage: relay-flow run restart --ticket <key>")
|
|
320
409
|
fmt.Fprintln(w, "\nStart a fresh attempt for a canceled run.")
|
|
410
|
+
fmt.Fprintln(w, "Flags:")
|
|
411
|
+
fmt.Fprintln(w, " --ticket <key> Task ticket key (required).")
|
|
412
|
+
fmt.Fprintln(w, "No other flags are supported.")
|
|
321
413
|
fmt.Fprintln(w, "Example: relay-flow run restart --ticket PAY-101")
|
|
322
414
|
case "run cancel":
|
|
323
415
|
fmt.Fprintln(w, "Usage: relay-flow run cancel --ticket <key> [--reason <text>]")
|
|
324
416
|
fmt.Fprintln(w, "\nPermanently cancel the current execution attempt.")
|
|
417
|
+
fmt.Fprintln(w, "Flags:")
|
|
418
|
+
fmt.Fprintln(w, " --ticket <key> Task ticket key (required).")
|
|
419
|
+
fmt.Fprintln(w, " --reason <text> Optional cancellation reason recorded with the request.")
|
|
325
420
|
fmt.Fprintln(w, "Example: relay-flow run cancel --ticket PAY-101 --reason "+`"operator request"`)
|
|
326
421
|
default:
|
|
327
422
|
fmt.Fprintf(os.Stderr, "unknown help target: %s\n", name)
|
|
@@ -4,6 +4,7 @@ import (
|
|
|
4
4
|
"bytes"
|
|
5
5
|
"io"
|
|
6
6
|
"os"
|
|
7
|
+
"path/filepath"
|
|
7
8
|
"strings"
|
|
8
9
|
"testing"
|
|
9
10
|
"time"
|
|
@@ -43,9 +44,10 @@ func TestRunDetailRendererKeepsPendingAndNestedVisitOrder(t *testing.T) {
|
|
|
43
44
|
func TestScopedHelpCoversAllCommandLevels(t *testing.T) {
|
|
44
45
|
for _, args := range [][]string{
|
|
45
46
|
{"--help"}, {"workflow", "--help"}, {"workflow", "list", "--help"},
|
|
46
|
-
{"repo", "--help"}, {"repo", "register", "--help"}, {"
|
|
47
|
+
{"repo", "--help"}, {"repo", "register", "--help"}, {"repo", "remove", "--help"},
|
|
48
|
+
{"repo", "get", "--help"}, {"repo", "list", "--help"}, {"run", "--help"},
|
|
47
49
|
{"run", "get", "--help"}, {"task", "--help"}, {"task", "auth", "--help"},
|
|
48
|
-
{"serve", "--help"}, {"runtime-register", "--help"}, {"version", "--help"},
|
|
50
|
+
{"init", "--help"}, {"serve", "--help"}, {"runtime-register", "--help"}, {"version", "--help"},
|
|
49
51
|
} {
|
|
50
52
|
var out bytes.Buffer
|
|
51
53
|
if code := printScopedHelp(args, &out); code != exitOK {
|
|
@@ -54,16 +56,126 @@ func TestScopedHelpCoversAllCommandLevels(t *testing.T) {
|
|
|
54
56
|
if out.Len() == 0 {
|
|
55
57
|
t.Fatalf("help %v was empty", args)
|
|
56
58
|
}
|
|
57
|
-
if !strings.Contains(out.String(), "Example
|
|
59
|
+
if !strings.Contains(out.String(), "Example") {
|
|
58
60
|
t.Fatalf("help %v missing example: %q", args, out.String())
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
if
|
|
63
|
-
t.Fatalf("
|
|
63
|
+
root := scopedHelp(t, "--help")
|
|
64
|
+
if !strings.Contains(root, "Use relay-flow <command> --help for command-specific details.") {
|
|
65
|
+
t.Fatalf("root help missing scoped-help guidance:\n%s", root)
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
func TestInitHelpDocumentsTemporalFlags(t *testing.T) {
|
|
70
|
+
text := scopedHelp(t, "init", "--help")
|
|
71
|
+
for _, want := range []string{
|
|
72
|
+
"--executor-plugin <name>",
|
|
73
|
+
"--temporal-address <host:port>",
|
|
74
|
+
"--temporal-namespace <name>",
|
|
75
|
+
"applies only to --executor-plugin temporal",
|
|
76
|
+
} {
|
|
77
|
+
if !strings.Contains(text, want) {
|
|
78
|
+
t.Fatalf("init help missing %q:\n%s", want, text)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func TestRepositoryHelpDocumentsGroupAndEverySubcommand(t *testing.T) {
|
|
84
|
+
group := scopedHelp(t, "repo", "--help")
|
|
85
|
+
for _, want := range []string{
|
|
86
|
+
"Manage registered runner repositories and task-system configuration.",
|
|
87
|
+
"register", "remove", "list", "get",
|
|
88
|
+
"Usage: relay-flow repo register", "Usage: relay-flow repo remove --name <name>",
|
|
89
|
+
"Usage: relay-flow repo list", "Usage: relay-flow repo get --name <name>",
|
|
90
|
+
"Example (Beads):", "beadsDir=/work/payments/.beads",
|
|
91
|
+
"Use relay-flow repo <subcommand> --help for flag details.",
|
|
92
|
+
} {
|
|
93
|
+
if !strings.Contains(group, want) {
|
|
94
|
+
t.Fatalf("repo group help missing %q:\n%s", want, group)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
register := scopedHelp(t, "repo", "register", "--help")
|
|
99
|
+
for _, want := range []string{
|
|
100
|
+
"Register a runner repository and task-system configuration.",
|
|
101
|
+
"--name <name>", "--path <path>", "--set key=value",
|
|
102
|
+
"Optional for interactive registration", "required in flagged/non-interactive mode",
|
|
103
|
+
"Interactive mode: omit --name, --path, and --set", "Repeat --set",
|
|
104
|
+
"duplicate keys are rejected", "derived keys cannot be overridden",
|
|
105
|
+
"Example (Beads):", "beadsDir=/work/payments/.beads",
|
|
106
|
+
} {
|
|
107
|
+
if !strings.Contains(register, want) {
|
|
108
|
+
t.Fatalf("repo register help missing %q:\n%s", want, register)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if strings.Contains(register, "component=backend") {
|
|
112
|
+
t.Fatalf("repo register help advertises a derived Jira component override:\n%s", register)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for _, tc := range []struct {
|
|
116
|
+
name string
|
|
117
|
+
want []string
|
|
118
|
+
}{
|
|
119
|
+
{name: "remove", want: []string{"Remove a registered repository.", "--name <name>", "required", "No other flags are supported.", "Example:"}},
|
|
120
|
+
{name: "list", want: []string{"List registered repositories.", "Flags: none.", "Example:"}},
|
|
121
|
+
{name: "get", want: []string{"Show one registered repository", "--name <name>", "required", "No other flags are supported.", "Example:"}},
|
|
122
|
+
} {
|
|
123
|
+
text := scopedHelp(t, "repo", tc.name, "--help")
|
|
124
|
+
for _, want := range tc.want {
|
|
125
|
+
if !strings.Contains(text, want) {
|
|
126
|
+
t.Fatalf("repo %s help missing %q:\n%s", tc.name, want, text)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func TestWorkflowRunAndTaskGroupsDescribeTheirSubcommands(t *testing.T) {
|
|
133
|
+
for _, tc := range []struct {
|
|
134
|
+
args []string
|
|
135
|
+
want []string
|
|
136
|
+
}{
|
|
137
|
+
{args: []string{"workflow", "--help"}, want: []string{"Manage validated workflow definitions", "workflow submit --file <path>", "workflow remove --name <name>", "workflow list [--json]", "workflow get --name <name>", "<subcommand> --help"}},
|
|
138
|
+
{args: []string{"run", "--help"}, want: []string{"Inspect and control durable ticket runs.", "run list", "run get", "run restart", "run cancel", "--reason <text>", "<subcommand> --help"}},
|
|
139
|
+
{args: []string{"task", "--help"}, want: []string{"Run task-system commands", "auth", "task <subcommand> --help"}},
|
|
140
|
+
{args: []string{"workflow", "submit", "--help"}, want: []string{"--file <path>", "required", "Example:"}},
|
|
141
|
+
{args: []string{"run", "list", "--help"}, want: []string{"--repo <name>", "--workflow <name>", "--ticket <key>", "--active", "--json", "--no-color", "Example:"}},
|
|
142
|
+
{args: []string{"task", "auth", "--help"}, want: []string{"task-plugin options are passed through unchanged", "no flags", "Example:"}},
|
|
143
|
+
} {
|
|
144
|
+
text := scopedHelp(t, tc.args...)
|
|
145
|
+
for _, want := range tc.want {
|
|
146
|
+
if !strings.Contains(text, want) {
|
|
147
|
+
t.Fatalf("help %v missing %q:\n%s", tc.args, want, text)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func TestScopedHelpDoesNotRequireRelayFlowHomeOrServer(t *testing.T) {
|
|
154
|
+
home := filepath.Join(t.TempDir(), "missing-home")
|
|
155
|
+
t.Setenv("RELAY_FLOW_HOME", home)
|
|
156
|
+
code, output := captureStdout(t, func() int {
|
|
157
|
+
return run([]string{"repo", "register", "--help"}, strings.NewReader(""))
|
|
158
|
+
})
|
|
159
|
+
if code != exitOK {
|
|
160
|
+
t.Fatalf("help exit = %d, want %d", code, exitOK)
|
|
161
|
+
}
|
|
162
|
+
if !strings.Contains(output, "Usage: relay-flow repo register") {
|
|
163
|
+
t.Fatalf("help output = %q", output)
|
|
164
|
+
}
|
|
165
|
+
if _, err := os.Stat(home); !os.IsNotExist(err) {
|
|
166
|
+
t.Fatalf("help touched relay-flow home: stat error = %v", err)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func scopedHelp(t *testing.T, args ...string) string {
|
|
171
|
+
t.Helper()
|
|
172
|
+
var out bytes.Buffer
|
|
173
|
+
if code := printScopedHelp(args, &out); code != exitOK {
|
|
174
|
+
t.Fatalf("help %v exit = %d", args, code)
|
|
175
|
+
}
|
|
176
|
+
return out.String()
|
|
177
|
+
}
|
|
178
|
+
|
|
67
179
|
func TestFilteredEmptyRunListShowsEffectiveFilters(t *testing.T) {
|
|
68
180
|
var out bytes.Buffer
|
|
69
181
|
active := true
|
|
@@ -157,7 +157,8 @@ func (a *Activities) LoadNodeRuntime(ctx context.Context, id run.ID, node string
|
|
|
157
157
|
|
|
158
158
|
// EnsureNodeRuntime uses only persisted terminal/session IDs on the normal
|
|
159
159
|
// path. A live terminal is rebound to the new visit; otherwise EnsureTerminal
|
|
160
|
-
// creates a replacement and its direct ID is persisted immediately.
|
|
160
|
+
// creates a replacement and its direct ID is persisted immediately. A stored
|
|
161
|
+
// session receives feedback whether its terminal is reused or replaced.
|
|
161
162
|
func (a *Activities) EnsureNodeRuntime(ctx context.Context, nw run.NodeWork, repoPath string, spec harness.LaunchSpec, rt NodeRuntime) error {
|
|
162
163
|
a.Runs.runtimeMu.Lock()
|
|
163
164
|
defer a.Runs.runtimeMu.Unlock()
|
|
@@ -196,6 +197,10 @@ func (a *Activities) EnsureNodeRuntime(ctx context.Context, nw run.NodeWork, rep
|
|
|
196
197
|
rt.TerminalID = currentRuntime.TerminalID
|
|
197
198
|
rt.SessionID = currentRuntime.SessionID
|
|
198
199
|
spec.ResumeID = rt.SessionID
|
|
200
|
+
promptKind := harness.PromptInitial
|
|
201
|
+
if rt.SessionID != "" {
|
|
202
|
+
promptKind = harness.PromptFeedback
|
|
203
|
+
}
|
|
199
204
|
stored := runner.Terminal{ID: rt.TerminalID, Title: spec.Title}
|
|
200
205
|
terminal, live, err := a.Runner.FindTerminal(ctx, stored)
|
|
201
206
|
if err != nil {
|
|
@@ -210,7 +215,7 @@ func (a *Activities) EnsureNodeRuntime(ctx context.Context, nw run.NodeWork, rep
|
|
|
210
215
|
// Same-visit retry/restart is silent: do not render, build, or send.
|
|
211
216
|
return nil
|
|
212
217
|
}
|
|
213
|
-
prompt, err := a.Harness.RenderPrompt(
|
|
218
|
+
prompt, err := a.Harness.RenderPrompt(promptKind, spec.PromptData, spec.NudgePrompt)
|
|
214
219
|
if err != nil {
|
|
215
220
|
return err
|
|
216
221
|
}
|
|
@@ -224,14 +229,14 @@ func (a *Activities) EnsureNodeRuntime(ctx context.Context, nw run.NodeWork, rep
|
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// node nudge; a new visit includes it.
|
|
232
|
+
// A fresh session receives the initial prompt; a stored session receives
|
|
233
|
+
// feedback even when its terminal must be replaced. Same-visit
|
|
234
|
+
// replacements omit the node nudge; a new visit includes it.
|
|
230
235
|
nudge := ""
|
|
231
236
|
if rt.NodeVisitID == "" || revisit {
|
|
232
237
|
nudge = spec.NudgePrompt
|
|
233
238
|
}
|
|
234
|
-
spec.Prompt, err = a.Harness.RenderPrompt(
|
|
239
|
+
spec.Prompt, err = a.Harness.RenderPrompt(promptKind, spec.PromptData, nudge)
|
|
235
240
|
if err != nil {
|
|
236
241
|
return err
|
|
237
242
|
}
|
|
@@ -369,6 +369,10 @@ func TestEndAppliesConfigAndCompletes(t *testing.T) {
|
|
|
369
369
|
if cleanupIdx < endApplyIdx {
|
|
370
370
|
t.Fatalf("runner cleanup ran before end taskConfig; events=%v", events)
|
|
371
371
|
}
|
|
372
|
+
closeIdx := indexOf(events, "closeTerminal:PAY-101:coding")
|
|
373
|
+
if closeIdx < 0 || cleanupIdx >= closeIdx {
|
|
374
|
+
t.Fatalf("runner cleanup did not precede runtime finalization: cleanup=%d close=%d events=%v", cleanupIdx, closeIdx, events)
|
|
375
|
+
}
|
|
372
376
|
r2, _ := engine.GetRun(context.Background(), rid)
|
|
373
377
|
if r2.State == run.StateCompleted && r2.FinishedAt == nil {
|
|
374
378
|
t.Fatal("completed run has no FinishedAt")
|
|
@@ -382,6 +386,54 @@ func TestEndAppliesConfigAndCompletes(t *testing.T) {
|
|
|
382
386
|
}
|
|
383
387
|
}
|
|
384
388
|
|
|
389
|
+
func TestEndCleanupRetriesDirtyCheckoutBeforeFinalization(t *testing.T) {
|
|
390
|
+
log := newEventLog()
|
|
391
|
+
sys := newFakeTaskSystem(log)
|
|
392
|
+
fr := newFakeRunner(log)
|
|
393
|
+
fr.cleanupDirty = true
|
|
394
|
+
engine := newEngine(t, goworkflows.Dependencies{
|
|
395
|
+
Repos: repoRegistryWith("payments", sys), Runner: fr, Harness: newFakeHarness(log),
|
|
396
|
+
Runtime: &run.RuntimePolicy{KeepTerminalsAlive: true, KeepSessionsAlive: true},
|
|
397
|
+
})
|
|
398
|
+
rid, _ := startRun(engine, linearWorkflow(true))
|
|
399
|
+
waitFor(t, 10*time.Second, func() bool {
|
|
400
|
+
r, _ := engine.GetRun(context.Background(), rid)
|
|
401
|
+
return r.CurrentNode == "coding"
|
|
402
|
+
})
|
|
403
|
+
if _, err := engine.SubmitReport(context.Background(), reportRequest(rid, "coding", successReport("end"))); err != nil {
|
|
404
|
+
t.Fatal(err)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
waitFor(t, 10*time.Second, func() bool {
|
|
408
|
+
return indexOf(log.all(), "cleanupRunDirty:"+string(rid)) >= 0
|
|
409
|
+
})
|
|
410
|
+
r, _ := engine.GetRun(context.Background(), rid)
|
|
411
|
+
if r.State == run.StateCompleted || r.FinishedAt != nil {
|
|
412
|
+
t.Fatalf("run completed while dirty cleanup was retrying: %+v", r)
|
|
413
|
+
}
|
|
414
|
+
if fr.liveTerminals() != 1 {
|
|
415
|
+
t.Fatalf("dirty cleanup closed the terminal before retry: live=%d events=%v", fr.liveTerminals(), log.all())
|
|
416
|
+
}
|
|
417
|
+
if indexOf(log.all(), "closeTerminal:PAY-101:coding") >= 0 {
|
|
418
|
+
t.Fatalf("runtime finalization ran before dirty cleanup retry: events=%v", log.all())
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
waitFor(t, 10*time.Second, func() bool {
|
|
422
|
+
r, _ := engine.GetRun(context.Background(), rid)
|
|
423
|
+
return r.State == run.StateCompleted
|
|
424
|
+
})
|
|
425
|
+
events := log.all()
|
|
426
|
+
dirtyIdx := indexOf(events, "cleanupRunDirty:"+string(rid))
|
|
427
|
+
cleanupIdx := indexOf(events, "cleanupRun:"+string(rid))
|
|
428
|
+
closeIdx := indexOf(events, "closeTerminal:PAY-101:coding")
|
|
429
|
+
if dirtyIdx < 0 || cleanupIdx < 0 || closeIdx < 0 || dirtyIdx >= cleanupIdx || cleanupIdx >= closeIdx {
|
|
430
|
+
t.Fatalf("cleanup retry/finalization order = %v", events)
|
|
431
|
+
}
|
|
432
|
+
if len(fr.cleaned) != 1 {
|
|
433
|
+
t.Fatalf("successful CleanupRun calls = %v, want one after the dirty retry", fr.cleaned)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
385
437
|
func TestEndCleanupDisabledKeepsRetainedRunner(t *testing.T) {
|
|
386
438
|
log := newEventLog()
|
|
387
439
|
fr := newFakeRunner(log)
|
|
@@ -340,13 +340,14 @@ func (s *fakeTaskSystem) parentByKey(key string) (task.Ticket, bool) {
|
|
|
340
340
|
type fakeRunner struct {
|
|
341
341
|
log *eventLog
|
|
342
342
|
|
|
343
|
-
mu
|
|
344
|
-
envs
|
|
345
|
-
terminals
|
|
346
|
-
cleaned
|
|
347
|
-
closedRun
|
|
348
|
-
|
|
349
|
-
|
|
343
|
+
mu sync.Mutex
|
|
344
|
+
envs map[string]runner.Environment
|
|
345
|
+
terminals map[string]*fakeTerminal // envID/title
|
|
346
|
+
cleaned []string
|
|
347
|
+
closedRun []string
|
|
348
|
+
cleanupDirty bool
|
|
349
|
+
createErr error
|
|
350
|
+
nextID int
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
type fakeTerminal struct {
|
|
@@ -460,6 +461,11 @@ func (f *fakeRunner) CloseTerminals(_ context.Context, spec runner.RunSpec) erro
|
|
|
460
461
|
func (f *fakeRunner) CleanupRun(_ context.Context, spec runner.RunSpec) error {
|
|
461
462
|
f.mu.Lock()
|
|
462
463
|
defer f.mu.Unlock()
|
|
464
|
+
if f.cleanupDirty {
|
|
465
|
+
f.cleanupDirty = false
|
|
466
|
+
f.log.add("cleanupRunDirty:" + string(spec.RunID))
|
|
467
|
+
return errDirtyCleanup
|
|
468
|
+
}
|
|
463
469
|
prefix := "env-" + spec.TicketKey + "/"
|
|
464
470
|
for k := range f.terminals {
|
|
465
471
|
if len(k) >= len(prefix) && k[:len(prefix)] == prefix {
|
|
@@ -581,3 +587,4 @@ func (e *conflictError) Error() string { return e.msg }
|
|
|
581
587
|
|
|
582
588
|
var errConflict = &conflictError{msg: "human moved mailbox"}
|
|
583
589
|
var errStartConflict = &conflictError{msg: "human moved ticket status"}
|
|
590
|
+
var errDirtyCleanup = &transientError{msg: "ticket checkout is dirty; commit required before runner cleanup"}
|
|
@@ -484,13 +484,10 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
484
484
|
if wf.CleanupRunnerOnEnd {
|
|
485
485
|
finalPolicy.KeepTerminalsAlive = false
|
|
486
486
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}); err != nil {
|
|
492
|
-
return err
|
|
493
|
-
}
|
|
487
|
+
// Cleanup must run before finalizing node runtimes when enabled. The
|
|
488
|
+
// runner performs its Git cleanliness check before closing terminals, so a
|
|
489
|
+
// dirty checkout leaves the agent terminal available for a commit while
|
|
490
|
+
// retryLoop waits.
|
|
494
491
|
if wf.CleanupRunnerOnEnd {
|
|
495
492
|
if _, err := retryLoop(ctx, start.ID, a, work, "",
|
|
496
493
|
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
@@ -499,6 +496,13 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
499
496
|
return err
|
|
500
497
|
}
|
|
501
498
|
}
|
|
499
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "",
|
|
500
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
501
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries,
|
|
502
|
+
a.FinalizeNodeRuntimes, work, start.RepoPath, finalPolicy)
|
|
503
|
+
}); err != nil {
|
|
504
|
+
return err
|
|
505
|
+
}
|
|
502
506
|
endFinished := goworkflow.Now(ctx).UTC()
|
|
503
507
|
endStep.Status, endStep.FinishedAt = run.StepSucceeded, &endFinished
|
|
504
508
|
if _, err := retryLoop(ctx, start.ID, a, work, "end",
|
|
@@ -189,7 +189,7 @@ func TestNodeRuntimeSessionRegistrationKeepsOldSessionBoundToOldVisit(t *testing
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
func
|
|
192
|
+
func TestEnsureNodeRuntimeUsesDirectIDsAndResumesWithFeedback(t *testing.T) {
|
|
193
193
|
ctx := context.Background()
|
|
194
194
|
fr := &runtimeTestRunner{}
|
|
195
195
|
fh := &runtimeTestHarness{}
|
|
@@ -233,12 +233,12 @@ func TestEnsureNodeRuntimeUsesDirectIDsAndFallsBackFresh(t *testing.T) {
|
|
|
233
233
|
if len(fh.resumeIDs) != 1 || fh.resumeIDs[0] != "dead-session" {
|
|
234
234
|
t.Fatalf("BuildCommand ResumeIDs = %v, want [dead-session]", fh.resumeIDs)
|
|
235
235
|
}
|
|
236
|
-
if !reflect.DeepEqual(fh.rendered, []harness.PromptKind{harness.
|
|
237
|
-
t.Fatalf("dead-terminal rendered prompts = %v, want
|
|
236
|
+
if !reflect.DeepEqual(fh.rendered, []harness.PromptKind{harness.PromptFeedback}) {
|
|
237
|
+
t.Fatalf("dead-terminal rendered prompts = %v, want feedback", fh.rendered)
|
|
238
238
|
}
|
|
239
239
|
for _, prompt := range fh.prompts {
|
|
240
|
-
if prompt != "
|
|
241
|
-
t.Fatalf("same-visit relaunch prompt = %q, want
|
|
240
|
+
if prompt != "feedback" {
|
|
241
|
+
t.Fatalf("same-visit relaunch prompt = %q, want feedback only", prompt)
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
}
|
|
@@ -363,7 +363,7 @@ func TestEnsureNodeRuntimeSendFailureClosesLiveTerminal(t *testing.T) {
|
|
|
363
363
|
if len(fr.sentTexts) != 1 || fr.sentTexts[0] != "feedback\n\nRead the latest review feedback." {
|
|
364
364
|
t.Fatalf("live revisit prompt = %q", fr.sentTexts)
|
|
365
365
|
}
|
|
366
|
-
if len(fh.prompts) != 1 || fh.prompts[0] != "
|
|
366
|
+
if len(fh.prompts) != 1 || fh.prompts[0] != "feedback\n\nRead the latest review feedback." {
|
|
367
367
|
t.Fatalf("revisit replacement prompt = %q", fh.prompts)
|
|
368
368
|
}
|
|
369
369
|
rt, _ := p.getNodeRuntime(ctx, id, "implement")
|
|
@@ -373,8 +373,8 @@ func TestEnsureNodeRuntimeSendFailureClosesLiveTerminal(t *testing.T) {
|
|
|
373
373
|
if len(fh.resumeIDs) != 1 || fh.resumeIDs[0] != "session-old" {
|
|
374
374
|
t.Fatalf("replacement ResumeIDs = %v, want [session-old]", fh.resumeIDs)
|
|
375
375
|
}
|
|
376
|
-
if !reflect.DeepEqual(fh.rendered, []harness.PromptKind{harness.PromptFeedback, harness.
|
|
377
|
-
t.Fatalf("rendered prompts = %v, want feedback then replacement
|
|
376
|
+
if !reflect.DeepEqual(fh.rendered, []harness.PromptKind{harness.PromptFeedback, harness.PromptFeedback}) {
|
|
377
|
+
t.Fatalf("rendered prompts = %v, want feedback then replacement feedback", fh.rendered)
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
380
|
|