Files
ctask/cmd/entry_test.go
T
typebasedio b75b82e676 feat(v0.6): launch path carries ResolvedAgent (command + args + env)
LaunchOpts.Agent (string) and WorkspaceEntryOptions.Agent (string) are
replaced by *agent.Resolved, carrying Command, Args, and Env. The five
entry commands (new, resume, last, open, attach) each construct an
AgentSpec from the workspace metadata, apply --agent as a one-shot
agent.command override (Open Q 1 — keeps muscle memory for users
passing executable paths), call agent.Resolve, and pass the result
through. resolveEntryAgent centralises the resume/last/open/attach path.

shell.ExecAgent and shell.ExecTmuxAgent gain an args parameter; agent.env
is merged into the env map at the session.Run launch switch, AFTER
ctask's exported CTASK_* vars (per spec §5: agent.env wins on collision).
mergeAgentEnv is the centralised merge.

Lease, manifest, write lock, heartbeat, summary, and provisional
cleanup are unchanged. The Agent string fields on Lease, SessionSummary,
and SessionInfo continue to record the launched command for diagnostics.
2026-05-15 11:08:03 -04:00

90 lines
2.7 KiB
Go

package cmd
import (
"path/filepath"
"testing"
"github.com/warrenronsiek/ctask/internal/agent"
"github.com/warrenronsiek/ctask/internal/session"
"github.com/warrenronsiek/ctask/internal/workspace"
)
// dispatchPersistent is a pure decision function — table tests are the
// right shape.
func TestDispatchPersistentOwnerWhenNoTmuxSession(t *testing.T) {
got := dispatchPersistent(false, session.LeaseStateNone)
if got != dispatchOwnerCreate {
t.Errorf("got %v, want %v", got, dispatchOwnerCreate)
}
}
func TestDispatchPersistentPassiveWhenFreshLocal(t *testing.T) {
got := dispatchPersistent(true, session.LeaseStateFreshLocal)
if got != dispatchPassive {
t.Errorf("got %v, want %v", got, dispatchPassive)
}
}
func TestDispatchPersistentAdoptedWhenStaleNoneOrRemote(t *testing.T) {
for _, st := range []session.LeaseState{
session.LeaseStateStale,
session.LeaseStateNone,
session.LeaseStateFreshRemote,
} {
got := dispatchPersistent(true, st)
if got != dispatchAdopted {
t.Errorf("state %v: got %v, want %v", st, got, dispatchAdopted)
}
}
}
// SessionName is computed by callers — sanity check determinism.
func TestEntrySessionNameStable(t *testing.T) {
abs, _ := filepath.Abs("/tmp/x")
a := session.SessionName("projects", "demo", abs)
b := session.SessionName("projects", "demo", abs)
if a != b {
t.Errorf("not stable: %q vs %q", a, b)
}
}
// runWorkspaceEntry must be injectable so per-command tests can capture
// the WorkspaceEntryOptions each command produces. This test installs a
// stub and verifies the wiring works end-to-end.
//
// Tests in this file mutate the package-level runWorkspaceEntry. Do not
// run with t.Parallel().
func TestRunWorkspaceEntryIsInjectable(t *testing.T) {
var captured WorkspaceEntryOptions
orig := runWorkspaceEntry
runWorkspaceEntry = func(opts WorkspaceEntryOptions) error {
captured = opts
return nil
}
t.Cleanup(func() { runWorkspaceEntry = orig })
want := WorkspaceEntryOptions{
WsPath: "/tmp/ws",
WsRoot: "/tmp",
WsMeta: &workspace.TaskMeta{Slug: "demo", Category: "projects", Mode: "local", Agent: workspace.AgentSpec{Type: "claude"}},
ResolvedAgent: &agent.Resolved{Command: "claude"},
Shell: true,
CommandName: "test",
}
if err := runWorkspaceEntry(want); err != nil {
t.Fatalf("runWorkspaceEntry: %v", err)
}
if captured.CommandName != "test" {
t.Errorf("CommandName: got %q", captured.CommandName)
}
if !captured.Shell {
t.Error("Shell should be true")
}
if captured.WsMeta == nil || captured.WsMeta.Slug != "demo" {
t.Errorf("WsMeta not propagated: %+v", captured.WsMeta)
}
if captured.WsPath != "/tmp/ws" || captured.WsRoot != "/tmp" {
t.Errorf("path/root not propagated: path=%q root=%q", captured.WsPath, captured.WsRoot)
}
}