From 7697ec05077ecc6f8fd9ae3836db2df9530a257e Mon Sep 17 00:00:00 2001 From: typebasedio Date: Fri, 8 May 2026 13:51:57 -0400 Subject: [PATCH] feat(v0.5.3): AttachExisting passive reattach helper --- internal/session/attach.go | 29 ++++++++++++++++++++++ internal/session/attach_test.go | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 internal/session/attach.go create mode 100644 internal/session/attach_test.go diff --git a/internal/session/attach.go b/internal/session/attach.go new file mode 100644 index 0000000..a312a58 --- /dev/null +++ b/internal/session/attach.go @@ -0,0 +1,29 @@ +package session + +import ( + "github.com/warrenronsiek/ctask/internal/shell" +) + +// attacher is the test seam used by AttachExisting. Production code calls +// shell.AttachSession directly; tests override this variable to capture +// invocations or simulate failures. Do not run tests that override this +// variable in parallel — it is a package global. +var attacher = shell.AttachSession + +// AttachExisting is the passive-reattach path. It is invoked when a tmux +// session for the workspace already exists and the lease is fresh and local +// (the original ctask owner is alive and heartbeating). +// +// AttachExisting performs no Preflight, writes no lease, captures no +// manifest, starts no heartbeat, prints no banner, and runs no finalize. +// It connects the user's terminal to the existing tmux session via +// shell.AttachSession and returns when the user detaches or the session +// ends. shell.AttachSession's contract handles failure-mode classification: +// nil on clean exit, wrapped error on non-zero exit. +// +// If the session disappeared between the dispatcher's HasSession check +// and this call, AttachSession returns an error and the user can retry — +// the next invocation will hit the owner-create path. +func AttachExisting(tmuxPath, name string) error { + return attacher(tmuxPath, name) +} diff --git a/internal/session/attach_test.go b/internal/session/attach_test.go new file mode 100644 index 0000000..ce0a248 --- /dev/null +++ b/internal/session/attach_test.go @@ -0,0 +1,43 @@ +package session + +import ( + "errors" + "testing" +) + +// Tests in this file mutate the package-level `attacher` variable and must +// not be run with t.Parallel(). + +func TestAttachExistingDelegatesToAttacher(t *testing.T) { + called := 0 + orig := attacher + attacher = func(tmuxPath, name string) error { + called++ + if name != "ctask-test-abc" { + t.Errorf("name: got %q", name) + } + if tmuxPath != "/usr/bin/tmux" { + t.Errorf("tmuxPath: got %q", tmuxPath) + } + return nil + } + t.Cleanup(func() { attacher = orig }) + + if err := AttachExisting("/usr/bin/tmux", "ctask-test-abc"); err != nil { + t.Fatalf("AttachExisting: %v", err) + } + if called != 1 { + t.Errorf("expected 1 call, got %d", called) + } +} + +func TestAttachExistingPropagatesAttachError(t *testing.T) { + want := errors.New("attach failed") + orig := attacher + attacher = func(_, _ string) error { return want } + t.Cleanup(func() { attacher = orig }) + + if err := AttachExisting("/usr/bin/tmux", "ctask-x"); !errors.Is(err, want) { + t.Errorf("expected %v, got %v", want, err) + } +}