diff --git a/cmd/open.go b/cmd/open.go index 1df23b6..3993137 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" "github.com/warrenronsiek/ctask/internal/config" - "github.com/warrenronsiek/ctask/internal/session" "github.com/warrenronsiek/ctask/internal/workspace" ) @@ -20,23 +19,27 @@ var openCmd = &cobra.Command{ } var ( - openAll bool - openForce bool + openAll bool + openForce bool + openDirect bool ) func init() { openCmd.Flags().BoolVarP(&openAll, "all", "a", false, "Include archived workspaces in query resolution") openCmd.Flags().BoolVar(&openForce, "force", false, "Skip active-session and stale-workspace warnings") - // v0.5.2: completion offers active candidates only (see delete for rationale). + openCmd.Flags().BoolVar(&openDirect, "direct", false, "Bypass persistent session mode for this command") openCmd.ValidArgsFunction = completeWorkspaces(completionActive) rootCmd.AddCommand(openCmd) } func runOpen(cmd *cobra.Command, args []string) error { roots := config.SearchRoots() + // PRESERVED v0.5.2 behavior: open's archive resolution is opt-in via --all. + // resolveOne(roots, query, includeArchived) — distinct from resume's + // archived-inclusive-with-restore-hint behavior. ws := resolveOne(roots, args[0], openAll) - // Update updated_at + // updated_at bump (existing v0.4 behavior). now := time.Now().UTC().Truncate(time.Second) ws.Meta.UpdatedAt = now metaPath := filepath.Join(ws.Path, "task.yaml") @@ -44,16 +47,14 @@ func runOpen(cmd *cobra.Command, args []string) error { return fmt.Errorf("updating metadata: %w", err) } - envVars := config.EnvVars(ws.Meta.Slug, ws.Meta.Mode, ws.Root, ws.Path, ws.Meta.Category, workspace.EffectiveType(ws.Meta), ws.Meta.LaunchDir) - - return session.Run(session.LaunchOpts{ - WsDir: ws.Path, - EnvVars: envVars, - Agent: ws.Meta.Agent, - Mode: ws.Meta.Mode, - Slug: ws.Meta.Slug, - Shell: true, // open always launches shell - LaunchDir: ws.Meta.LaunchDir, - Force: openForce, + return runWorkspaceEntry(WorkspaceEntryOptions{ + WsPath: ws.Path, + WsRoot: ws.Root, + WsMeta: ws.Meta, + Agent: ws.Meta.Agent, + Shell: true, // open always launches a shell + Force: openForce, + Direct: openDirect, + CommandName: "open", }) } diff --git a/cmd/open_persistent_test.go b/cmd/open_persistent_test.go new file mode 100644 index 0000000..13796d9 --- /dev/null +++ b/cmd/open_persistent_test.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/warrenronsiek/ctask/internal/workspace" +) + +// Tests in this file mutate runWorkspaceEntry. Do not run with t.Parallel(). + +func TestOpenDirectFlagRegistered(t *testing.T) { + if openCmd.Flags().Lookup("direct") == nil { + t.Error("--direct flag missing from `ctask open`") + } + if openCmd.Flags().Lookup("all") == nil { + t.Error("--all flag missing from `ctask open` (archive resolution must remain opt-in)") + } +} + +// Open must hit the shared entry helper with Shell: true and CommandName +// "open". Open's archive-inclusive lookup is gated by --all, so a bare-name +// resolution against an active workspace must succeed and the captured opts +// must reflect Shell=true. +func TestOpenForwardsToEntryHelperWithShellTrue(t *testing.T) { + root := t.TempDir() + wsDir := filepath.Join(root, "general", "2026-05-08_open-fwd-demo") + if err := os.MkdirAll(wsDir, 0755); err != nil { + t.Fatalf("mkdir: %v", err) + } + now := time.Now().UTC().Truncate(time.Second) + meta := &workspace.TaskMeta{ + ID: "t", Slug: "open-fwd-demo", Title: "open-fwd-demo", + CreatedAt: now, UpdatedAt: now, + Status: "active", Category: "general", Type: "task", + Mode: "local", Agent: "claude", + } + if err := workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta); err != nil { + t.Fatalf("WriteMeta: %v", err) + } + + prevRoot := os.Getenv("CTASK_ROOT") + os.Setenv("CTASK_ROOT", root) + t.Cleanup(func() { + if prevRoot == "" { + os.Unsetenv("CTASK_ROOT") + } else { + os.Setenv("CTASK_ROOT", prevRoot) + } + }) + + var captured WorkspaceEntryOptions + orig := runWorkspaceEntry + runWorkspaceEntry = func(opts WorkspaceEntryOptions) error { + captured = opts + return nil + } + t.Cleanup(func() { runWorkspaceEntry = orig }) + + // Reset openAll / openForce / openDirect to defaults explicitly + // since they are package globals shared across tests. + prevAll, prevForce, prevDirect := openAll, openForce, openDirect + openAll, openForce, openDirect = false, false, false + t.Cleanup(func() { + openAll = prevAll + openForce = prevForce + openDirect = prevDirect + }) + + if err := runOpen(openCmd, []string{"open-fwd-demo"}); err != nil { + t.Fatalf("runOpen: %v", err) + } + if captured.CommandName != "open" { + t.Errorf("CommandName: got %q, want %q", captured.CommandName, "open") + } + if !captured.Shell { + t.Error("open must set Shell=true") + } + if captured.WsMeta == nil || captured.WsMeta.Slug != "open-fwd-demo" { + t.Errorf("WsMeta not propagated: %+v", captured.WsMeta) + } +}