polish(v0.5.4): invocation-name audit + targeted regression tests

Audit walked every cmd/ and internal/ file that produces user-facing
output. All command-form hints (text the user is meant to type back)
were already routed through invocationName() in v0.5.3 — the audit is
a verification pass, not a code rewrite.

Additions:

- Extract formatResumeRestoreHint and formatDirectModeTmuxHint as
  testable string-only helpers. Production paths are unchanged
  behaviorally; the helpers exist purely so the audit can pin down
  the format strings without simulating tmux or stderr capture.

- Two new tests pinning the invocation name to a non-canonical value
  ("my-bin"). The pre-existing tests already protect these surfaces
  but pin the name to "ctask", so they cannot detect a regression
  that hard-codes "ctask" inside the format string. The new tests
  flush that out for the resume restore hint and the Layer-1
  active-session attach hint.

- Drop the duplicated withInvocationName helper accidentally added in
  the info-session tests; reuse the canonical helper from
  persistent_test.go.

Product-identity references ("ctask persistent mode requires tmux",
the SSH-remote `ssh -t <host> ctask <subcmd>` hint, doctor's "[ctask]"
diagnostic prefix, root-command Use:/Long:) deliberately remain
literal per spec §2.
This commit is contained in:
2026-05-14 19:56:05 -04:00
parent 0c8076aba9
commit 0fb8de697b
4 changed files with 81 additions and 17 deletions
+9 -1
View File
@@ -153,9 +153,17 @@ func directModeTmuxHint(opts WorkspaceEntryOptions) string {
if !shell.HasSession(tmuxPath, sessionName) {
return ""
}
return formatDirectModeTmuxHint(opts.WsMeta.Slug)
}
// formatDirectModeTmuxHint builds the hint string itself, with no tmux
// or filesystem checks. Split out so unit tests can verify that the
// command-form line uses invocationName() without needing a live tmux
// session set up against a real workspace.
func formatDirectModeTmuxHint(slug string) string {
return fmt.Sprintf(
"Tip: a tmux session exists for this workspace.\nTo reattach instead of starting a second direct-mode session, run:\n %s attach %s",
invocationName(), opts.WsMeta.Slug)
invocationName(), slug)
}
func invokePersistentRun(opts WorkspaceEntryOptions, tmuxPath, sessionName string) error {
+4 -13
View File
@@ -12,16 +12,7 @@ import (
"github.com/warrenronsiek/ctask/internal/workspace"
)
// withInvocationNameInfo pins invocationName() to "ctask" for the duration
// of the test so attach-hint substring assertions are stable across hosts.
// Mirrors the helper in persistent_test.go but kept local to avoid
// cross-test coupling.
func withInvocationNameInfo(t *testing.T, name string) {
t.Helper()
prev := invocationNameOverride
invocationNameOverride = name
t.Cleanup(func() { invocationNameOverride = prev })
}
// withInvocationName is provided by persistent_test.go in this package.
// makeInfoSessionWorkspace writes a workspace under root with the given
// slug and returns the workspace directory path. The workspace metadata
@@ -65,7 +56,7 @@ func TestInfoNoSession(t *testing.T) {
}
func TestInfoShowsActivePersistentSessionWithAttachHint(t *testing.T) {
withInvocationNameInfo(t, "ctask")
withInvocationName(t, "ctask")
root := t.TempDir()
wsDir := makeInfoSessionWorkspace(t, root, "active-persist")
@@ -95,7 +86,7 @@ func TestInfoShowsActivePersistentSessionWithAttachHint(t *testing.T) {
}
func TestInfoShowsActiveDirectSessionWithoutAttachHint(t *testing.T) {
withInvocationNameInfo(t, "ctask")
withInvocationName(t, "ctask")
root := t.TempDir()
wsDir := makeInfoSessionWorkspace(t, root, "active-direct")
@@ -124,7 +115,7 @@ func TestInfoShowsActiveDirectSessionWithoutAttachHint(t *testing.T) {
}
func TestInfoShowsRemoteHostnameInOwnerLine(t *testing.T) {
withInvocationNameInfo(t, "ctask")
withInvocationName(t, "ctask")
root := t.TempDir()
wsDir := makeInfoSessionWorkspace(t, root, "remote-active")
+51
View File
@@ -0,0 +1,51 @@
package cmd
import (
"strings"
"testing"
)
// v0.5.4 invocation-name audit: spec §2 codifies that command-form
// hints use invocationName() while product-identity references stay
// literal. The pre-existing tests already cover most paths individually
// (resume restore hint in resume_test.go, persistent bypass hints in
// persistent_test.go). These tests pin down the remaining piece — the
// Layer-1 active-session prompt's attach hint — and re-assert the
// resume restore-hint contract against an explicitly non-canonical
// invocation name so a regression that hard-codes "ctask" anywhere
// upstream of the hint format will fail loudly.
func TestInvocationNameInActiveSessionPrompt(t *testing.T) {
// directModeTmuxHint composes the Layer-1 prompt's attach suggestion
// from formatDirectModeTmuxHint. The format-only helper is the right
// surface to test: it isolates the rendering decision from the tmux
// presence checks (which are environment-dependent) but exercises the
// exact string the user will see.
withInvocationName(t, "my-bin")
got := formatDirectModeTmuxHint("demo-ws")
if !strings.Contains(got, "my-bin attach demo-ws") {
t.Errorf("Layer-1 attach hint should use invocation name; got:\n%s", got)
}
if strings.Contains(got, "ctask attach demo-ws") {
t.Errorf("Layer-1 attach hint must NOT hard-code 'ctask attach' when invocation name differs; got:\n%s", got)
}
}
func TestInvocationNameInRestoreHintNonCanonical(t *testing.T) {
// Complement to TestResumeArchivedShowsRestoreHint in resume_test.go,
// which pins invocationName to "ctask" — that protects against test
// binary noise but cannot detect a regression that hard-codes "ctask"
// in the format string. Pinning a non-default name flushes that out.
withInvocationName(t, "my-bin")
got := formatResumeRestoreHint("my-archived-ws")
if !strings.Contains(got, "my-bin restore my-archived-ws") {
t.Errorf("resume restore hint should use invocation name; got:\n%s", got)
}
if strings.Contains(got, "ctask restore") {
t.Errorf("resume restore hint must NOT hard-code 'ctask restore' when invocation name differs; got:\n%s", got)
}
}
+17 -3
View File
@@ -58,9 +58,7 @@ func doResume(query string, container, useShell, force bool, agentOverride strin
ws := resolveOne(roots, query, true)
if ws.Meta.Status == "archived" {
fmt.Fprintf(os.Stderr,
"[ctask] error: workspace %q is archived\n\nTo restore it:\n %s restore %s\n",
query, invocationName(), query)
fmt.Fprint(os.Stderr, formatResumeRestoreHint(query))
return fmt.Errorf("workspace archived")
}
@@ -88,3 +86,19 @@ func doResume(query string, container, useShell, force bool, agentOverride strin
CommandName: "resume",
})
}
// formatResumeRestoreHint builds the multi-line stderr block printed
// when `ctask resume <query>` resolves to an archived workspace.
// Extracted so the v0.5.4 invocation-name audit can verify the
// command-form line uses invocationName() without depending on the
// surrounding fmt.Fprintf machinery.
//
// The "[ctask]" diagnostic prefix is intentionally a literal product
// reference (spec §2: product-identity references stay literal). The
// `restore <query>` line is the command-form portion and uses
// invocationName().
func formatResumeRestoreHint(query string) string {
return fmt.Sprintf(
"[ctask] error: workspace %q is archived\n\nTo restore it:\n %s restore %s\n",
query, invocationName(), query)
}