From a33234f79bb8042dd65dc40ee1bd7171148e67df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:01:12 +0000 Subject: [PATCH 1/2] Initial plan From 3a4b695563b10e47c4628e952e39ab1fb69e3691 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:23:43 +0000 Subject: [PATCH 2/2] Remove Start/Wait from CmdRunner interface; add regression test Co-authored-by: meling <810999+meling@users.noreply.github.com> --- cmd.go | 2 -- cmd_test.go | 2 -- iago_test.go | 21 +++++++++++++++++++++ ssh.go | 8 ++++---- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cmd.go b/cmd.go index caa954e..02260d2 100644 --- a/cmd.go +++ b/cmd.go @@ -13,8 +13,6 @@ import ( type CmdRunner interface { Run(cmd string) error RunContext(ctx context.Context, cmd string) error - Start(cmd string) error - Wait() error StdinPipe() (io.WriteCloser, error) StdoutPipe() (io.ReadCloser, error) diff --git a/cmd_test.go b/cmd_test.go index 083b5a0..fa7a2a3 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -20,8 +20,6 @@ type fakeCmdRunner struct { func (r *fakeCmdRunner) Run(string) error { return r.err } func (r *fakeCmdRunner) RunContext(context.Context, string) error { return r.err } -func (r *fakeCmdRunner) Start(string) error { return r.err } -func (r *fakeCmdRunner) Wait() error { return r.err } func (r *fakeCmdRunner) StdinPipe() (io.WriteCloser, error) { return nil, errors.New("not supported") } func (r *fakeCmdRunner) StdoutPipe() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader(r.output)), nil diff --git a/iago_test.go b/iago_test.go index f9b0e95..69a2669 100644 --- a/iago_test.go +++ b/iago_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/relab/iago" "github.com/relab/iago/iagotest" @@ -78,6 +79,26 @@ func TestIago(t *testing.T) { } } +// TestStartHonorsContextTimeout verifies that a task using the "start now, +// wait later" pattern via RunContext respects the group's timeout: a +// long-running remote command must be aborted promptly when the context +// deadline expires, instead of blocking until the remote process exits. +func TestStartHonorsContextTimeout(t *testing.T) { + g := iagotest.CreateSSHGroup(t, 1, false) + g.Timeout = 500 * time.Millisecond + + start := time.Now() + g.ErrorHandler = iago.Ignore + g.Run("Sleep beyond timeout", func(ctx context.Context, host iago.Host) error { + return iago.Shell{Command: "sleep 60"}.Apply(ctx, host) + }) + elapsed := time.Since(start) + + if elapsed >= 60*time.Second { + t.Fatalf("Wait did not honor context timeout: took %s", elapsed) + } +} + func TestIagoDownloadExample(t *testing.T) { dir := t.TempDir() diff --git a/ssh.go b/ssh.go index b0535df..d6e79af 100644 --- a/ssh.go +++ b/ssh.go @@ -639,7 +639,7 @@ func (c sshCmd) Run(cmd string) (err error) { } func (c sshCmd) RunContext(ctx context.Context, cmd string) (err error) { - if err = c.session.Start(cmd); err != nil { + if err = c.start(cmd); err != nil { return err } @@ -657,14 +657,14 @@ func (c sshCmd) RunContext(ctx context.Context, cmd string) (err error) { errChan <- c.session.Close() }() - return c.session.Wait() + return c.wait() } -func (c sshCmd) Start(cmd string) error { +func (c sshCmd) start(cmd string) error { return c.session.Start(cmd) } -func (c sshCmd) Wait() (err error) { +func (c sshCmd) wait() (err error) { defer safeClose(c.session, &err, io.EOF) return c.session.Wait() }