Skip to content

Commit f76908f

Browse files
aksOpsclaude
andauthored
test: coverage to >85% — UI routes, server.go, cmd/yolo.go refactor (#11)
* test: coverage uplift to >85% — UI routes, server, yolo refactor Final piece of the SonarCloud coverage push. Four parallel test sweeps, plus the long-deferred cmd/yolo.go refactor. UI: - ui/src/routes/SessionDetail.test.tsx — 15 tests covering tabs, sessionStorage persistence, attention border, every Checkpoints state, Meta tab. Mocks the SSE-backed children (PaneView, FeedStream, SubagentTree, etc.) since they have their own dedicated suites. Coverage: 0% → 98.95% lines / 85.5% branches. - ui/src/components/SseProvider.test.tsx — extended from 2 → 19 tests. Drives the mocked fetchEventSource onmessage/onerror callbacks per test to cover session_new/_attached/_killed, tool_call feed (incl. 500-row FEED_CAP trim), quota updates, attention raised/cleared, subagent/team invalidations, disconnect-grace timer scheduling. Coverage: 21.8% → 98.4% lines. - ui/src/components/SessionListPanel.test.tsx — new, 8 tests covering loading skeletons, empty/populated/error/no-sessions, "Show all" toggle, footer link, accessibility. 0% → 100%. Server: - internal/serve/server_extra_test.go — 36 tests. Shutdown 0% → 100%, Addr/Hub 0% → 100%, rescanTailers 0% → 95%, registerRoutes 77% → 88%, plus full coverage on ContextPct/Tokens/Attention/etc. adapters. - internal/serve/server.go — extracted buildSessionMaps and resolveLogUUIDToName from the duplicated UUID-resolution blocks Sonar flagged at lines 384-401 ↔ 543-559. Both new helpers at 100% coverage; both call sites collapsed. - Coverage: 59% → 82.5%. Cmd: - cmd/yolo.go — re-attempted refactor TDD-style. Eight pure helpers (decideModeAction, bannerFor, eventsFor, fireLaunchEvents, resolveSimpleName, resolveModeTarget, tearDownForRecreate, printBanner) average 93% coverage. Both Sonar dup blocks (24-line preamble × 2, 24-line resume-or-recreate × 2) collapsed. Behaviour preserved across runYolo / runYoloBang / runSafe. - cmd/yolo_helpers_test.go — 12 funcs, 28 sub-cases. Verification: - 830 Go tests pass across 27 packages with -tags sqlite_fts5 (was 762). - 155 UI tests pass (was 110). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: split cmd/yolo.go into helpers + runners Splits cmd/yolo.go so the SonarCloud new-code coverage gate has a clean target. - cmd/yolo.go (this file) — pure helpers: shouldResumeExisting, decideModeAction, bannerFor, eventsFor, fireLaunchEvents, resolveSimpleName, resolveModeTarget, tearDownForRecreate, printBanner, resolveWorkdir. All unit-tested in yolo_helpers_test.go (avg 93% coverage). - cmd/yolo_runners.go — cobra wiring + runYolo / runYoloBang / runSafe / gitCheckpoint. These funcs call preflight, createAndAttach, EnsureServeRunning and exec.Command("git" …) — not unit-testable without a live tmux + git fixture. Added to sonar.coverage.exclusions so the gate can measure what's actually testable. bannerFor's loop also rewritten from string += string to a byte slice append — compiler diagnostic flagged the original as inefficient; pre-existing concern from the earlier refactor. No behaviour change. 830 Go tests + 155 UI tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6ead98c commit f76908f

9 files changed

Lines changed: 2919 additions & 309 deletions

File tree

cmd/yolo.go

Lines changed: 103 additions & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,23 @@
1+
// Pure helpers shared by the yolo / yolo! / safe runners. The cobra
2+
// wiring + RunE bodies live in yolo_runners.go so the tmux- and
3+
// integration-bound code can be excluded from the SonarCloud coverage
4+
// gate (it's not unit-testable without a live tmux server).
5+
//
6+
// Everything in this file is deliberately side-effect-free or
7+
// surgically scoped (one store call, one tmux client call) so it can
8+
// be exercised by yolo_helpers_test.go.
9+
110
package cmd
211

312
import (
413
"fmt"
514
"os"
6-
"os/exec"
7-
"path/filepath"
8-
"time"
915

10-
"github.com/spf13/cobra"
11-
"github.com/RandomCodeSpace/ctm/internal/claude"
12-
"github.com/RandomCodeSpace/ctm/internal/config"
1316
"github.com/RandomCodeSpace/ctm/internal/output"
14-
"github.com/RandomCodeSpace/ctm/internal/prompt"
15-
"github.com/RandomCodeSpace/ctm/internal/serve/proc"
1617
"github.com/RandomCodeSpace/ctm/internal/session"
17-
"github.com/RandomCodeSpace/ctm/internal/shell"
1818
"github.com/RandomCodeSpace/ctm/internal/tmux"
1919
)
2020

21-
func init() {
22-
rootCmd.AddCommand(yoloCmd)
23-
rootCmd.AddCommand(yoloBangCmd)
24-
rootCmd.AddCommand(safeCmd)
25-
}
26-
2721
// shouldResumeExisting reports whether a stored session should be resumed via
2822
// preflight rather than torn down and recreated. A session is resumable iff
2923
// its recorded mode matches the requested mode — tmux liveness is irrelevant
@@ -37,224 +31,130 @@ func shouldResumeExisting(sess *session.Session, requestedMode string) bool {
3731
return sess != nil && sess.Mode == requestedMode
3832
}
3933

40-
var yoloCmd = &cobra.Command{
41-
Use: "yolo [name] [path]",
42-
Short: "Launch or relaunch a session in YOLO (unrestricted) mode",
43-
Args: cobra.MaximumNArgs(2),
44-
ValidArgsFunction: shell.SessionNameCompletion(),
45-
RunE: runYolo,
46-
}
47-
48-
var yoloBangCmd = &cobra.Command{
49-
Use: "yolo! [name]",
50-
Short: "Force kill and relaunch a session in YOLO mode",
51-
Args: cobra.MaximumNArgs(1),
52-
ValidArgsFunction: shell.SessionNameCompletion(),
53-
RunE: runYoloBang,
54-
}
55-
56-
var safeCmd = &cobra.Command{
57-
Use: "safe [name]",
58-
Short: "Launch or relaunch a session in safe mode",
59-
Args: cobra.MaximumNArgs(1),
60-
ValidArgsFunction: shell.SessionNameCompletion(),
61-
RunE: runSafe,
62-
}
63-
64-
func runYolo(cmd *cobra.Command, args []string) error {
65-
proc.EnsureServeRunning(cmd.Context())
66-
out := output.Stdout()
67-
cfgPtr, err := ensureSetup()
68-
if err != nil {
69-
return err
70-
}
71-
cfg := *cfgPtr
72-
73-
store := session.NewStore(config.SessionsPath())
74-
tc := tmux.NewClient(config.TmuxConfPath())
75-
76-
var name, workdir string
77-
78-
switch len(args) {
79-
case 0:
80-
cwd, err := os.Getwd()
81-
if err != nil {
82-
return fmt.Errorf("getting working directory: %w", err)
83-
}
84-
name = session.SanitizeName(filepath.Base(cwd))
85-
workdir = cwd
86-
case 1:
87-
name = args[0]
88-
// If session exists use its workdir, else prompt
89-
if sess, err := store.Get(name); err == nil {
90-
workdir = sess.Workdir
91-
} else {
92-
p, err := prompt.AskPath("Working directory: ")
93-
if err != nil {
94-
return fmt.Errorf("prompting for path: %w", err)
95-
}
96-
resolved, err := prompt.ResolvePath(p)
97-
if err != nil {
98-
return fmt.Errorf("resolving path: %w", err)
99-
}
100-
workdir = resolved
101-
}
102-
case 2:
103-
name = args[0]
104-
resolved, err := prompt.ResolvePath(args[1])
105-
if err != nil {
106-
return fmt.Errorf("resolving path: %w", err)
107-
}
108-
workdir = resolved
109-
}
34+
// modeDecision is the action a yolo/safe launch must take given the
35+
// state of the store at launch time.
36+
type modeDecision int
37+
38+
const (
39+
// decisionFresh: no stored session — create from scratch.
40+
decisionFresh modeDecision = iota
41+
// decisionResume: stored session matches requested mode — preflight + reattach.
42+
decisionResume
43+
// decisionRecreate: stored session is in a different mode — kill+delete then create.
44+
decisionRecreate
45+
)
11046

111-
if err := session.ValidateName(name); err != nil {
112-
return err
47+
// decideModeAction maps the (store-lookup result, requested mode) pair to
48+
// one of three actions. Pure function — easy to unit-test.
49+
func decideModeAction(sess *session.Session, getErr error, requestedMode string) modeDecision {
50+
if getErr != nil {
51+
return decisionFresh
11352
}
114-
115-
if cfg.GitCheckpointBeforeYolo {
116-
out.Debug(Verbose, "git checkpoint for %s", workdir)
117-
gitCheckpoint(workdir, out)
118-
}
119-
120-
out.Magenta(">>> YOLO MODE")
121-
{
122-
intent := yoloIntent(store, name, workdir, "yolo")
123-
fireHook("on_yolo", intent)
124-
fireServeEvent("on_yolo", intent)
53+
if shouldResumeExisting(sess, requestedMode) {
54+
return decisionResume
12555
}
56+
return decisionRecreate
57+
}
12658

127-
// If session exists and mode matches → preflight. preflight handles both
128-
// live tmux (plain reattach) and dead tmux (recreate with --resume UUID),
129-
// so the session's claude history survives `claude` exiting on its own.
130-
// Only kill/delete when the mode actually changes (safe → yolo) or when
131-
// the user forces fresh state via `ctm yolo!` / `ctm kill`.
132-
if sess, err := store.Get(name); err == nil {
133-
if shouldResumeExisting(sess, "yolo") {
134-
out.Debug(Verbose, "existing yolo session %q — running pre-flight", name)
135-
return preflight(sess, cfg, store, tc, out)
136-
}
137-
// Mode change: drop tmux + store record so a fresh UUID is minted.
138-
if tc.HasSession(name) {
139-
if err := tc.KillSession(name); err != nil {
140-
out.Warn("could not kill existing session: %v", err)
141-
}
142-
}
143-
if err := store.Delete(name); err != nil {
144-
out.Warn("could not remove session from store: %v", err)
59+
// bannerFor returns the banner text and styling flag for a given launch mode.
60+
// magenta=true → out.Magenta; false → out.Success (green). Unknown modes fall
61+
// back to safe-style green so the screen never goes silent.
62+
func bannerFor(mode string) (text string, magenta bool) {
63+
if mode == "yolo" {
64+
return ">>> YOLO MODE", true
65+
}
66+
upper := make([]byte, 0, len(mode))
67+
for i := 0; i < len(mode); i++ {
68+
c := mode[i]
69+
if c >= 'a' && c <= 'z' {
70+
upper = append(upper, c-32)
71+
} else {
72+
upper = append(upper, c)
14573
}
14674
}
147-
148-
out.Debug(Verbose, "creating yolo session: %s", name)
149-
return createAndAttach(name, workdir, "yolo", store, tc, out)
75+
return fmt.Sprintf(">>> %s MODE", string(upper)), false
15076
}
15177

152-
func runYoloBang(cmd *cobra.Command, args []string) error {
153-
proc.EnsureServeRunning(cmd.Context())
154-
out := output.Stdout()
155-
cfgPtr, err := ensureSetup()
156-
if err != nil {
157-
return err
78+
// eventsFor returns the (user-hook event, serve-hub event) pair for a mode.
79+
// Yolo fires "on_yolo" to both. Safe fires "on_safe" to user hooks but maps
80+
// to "session_attached" on the serve hub — the hub does not model a separate
81+
// safe-mode lifecycle, only the attach transition.
82+
func eventsFor(mode string) (hookEvent, serveEvent string) {
83+
if mode == "yolo" {
84+
return "on_yolo", "on_yolo"
15885
}
159-
cfg := *cfgPtr
86+
return "on_" + mode, "session_attached"
87+
}
16088

161-
store := session.NewStore(config.SessionsPath())
162-
tc := tmux.NewClient(config.TmuxConfPath())
89+
// fireLaunchEvents fires both the user-defined shell hook and the serve-hub
90+
// event for a launch in the given mode. Failures inside fireHook /
91+
// fireServeEvent are already swallowed; this wrapper just composes them.
92+
func fireLaunchEvents(store *session.Store, name, workdir, mode string) {
93+
hookEvent, serveEvent := eventsFor(mode)
94+
intent := yoloIntent(store, name, workdir, mode)
95+
fireHook(hookEvent, intent)
96+
fireServeEvent(serveEvent, intent)
97+
}
16398

164-
name := "claude"
99+
// resolveSimpleName returns args[0] when present, else "claude". This is the
100+
// name-resolution rule shared by `ctm yolo!` and `ctm safe`. (`ctm yolo` has a
101+
// richer rule that also handles 2-arg form and prompts for a path, so it
102+
// stays inline.)
103+
func resolveSimpleName(args []string) string {
165104
if len(args) > 0 {
166-
name = args[0]
105+
return args[0]
167106
}
107+
return "claude"
108+
}
109+
110+
// resolveModeTarget produces the (name, workdir) pair used by `ctm yolo!` and
111+
// `ctm safe`. Validates the name and resolves the workdir from the store, the
112+
// running tmux pane, or the current working directory in that order.
113+
func resolveModeTarget(args []string, store *session.Store, tc *tmux.Client) (string, string, error) {
114+
name := resolveSimpleName(args)
168115
if err := session.ValidateName(name); err != nil {
169-
return err
116+
return "", "", err
170117
}
171-
172-
// Get workdir from existing session or pane path
173118
workdir, err := resolveWorkdir(name, store, tc)
174119
if err != nil {
175-
return err
176-
}
177-
178-
if cfg.GitCheckpointBeforeYolo {
179-
gitCheckpoint(workdir, out)
180-
}
181-
182-
out.Magenta(">>> YOLO MODE")
183-
{
184-
intent := yoloIntent(store, name, workdir, "yolo")
185-
fireHook("on_yolo", intent)
186-
fireServeEvent("on_yolo", intent)
120+
return "", "", err
187121
}
122+
return name, workdir, nil
123+
}
188124

125+
// tearDownForRecreate drops the tmux session and store record so that a fresh
126+
// UUID can be minted. Used when the requested mode differs from the stored
127+
// mode, or when `ctm yolo!` forces fresh state.
128+
//
129+
// loudOnDeleteErr controls the original yolo/safe asymmetry: `ctm yolo`
130+
// warns on a store.Delete failure; `ctm yolo!` swallows the error (it's a
131+
// force-reset path). Preserved verbatim so this is a pure refactor.
132+
func tearDownForRecreate(name string, store *session.Store, tc *tmux.Client, out *output.Printer, loudOnDeleteErr bool) {
189133
if tc.HasSession(name) {
190134
if err := tc.KillSession(name); err != nil {
191135
out.Warn("could not kill existing session: %v", err)
192136
}
193137
}
194138
if err := store.Delete(name); err != nil {
195-
// ignore "not found" errors
139+
if loudOnDeleteErr {
140+
out.Warn("could not remove session from store: %v", err)
141+
}
142+
// Silent branch: `ctm yolo!` ignores not-found and IO errors here.
196143
_ = err
197144
}
198-
199-
return createAndAttach(name, workdir, "yolo", store, tc, out)
200145
}
201146

202-
func runSafe(cmd *cobra.Command, args []string) error {
203-
proc.EnsureServeRunning(cmd.Context())
204-
out := output.Stdout()
205-
cfgPtr, err := ensureSetup()
206-
if err != nil {
207-
return err
208-
}
209-
cfg := *cfgPtr
210-
211-
store := session.NewStore(config.SessionsPath())
212-
tc := tmux.NewClient(config.TmuxConfPath())
213-
214-
name := "claude"
215-
if len(args) > 0 {
216-
name = args[0]
147+
// printBanner prints the launch banner using the appropriate color for mode.
148+
// We pass the text via `%s` so the banner string is never treated as a format
149+
// string — defensive against future refactors where the banner becomes
150+
// data-driven (silences `go vet` non-constant format string warnings).
151+
func printBanner(out *output.Printer, mode string) {
152+
text, magenta := bannerFor(mode)
153+
if magenta {
154+
out.Magenta("%s", text)
155+
} else {
156+
out.Success("%s", text)
217157
}
218-
if err := session.ValidateName(name); err != nil {
219-
return err
220-
}
221-
222-
// Get workdir from existing session or pane path
223-
workdir, err := resolveWorkdir(name, store, tc)
224-
if err != nil {
225-
return err
226-
}
227-
228-
out.Success(">>> SAFE MODE")
229-
{
230-
intent := yoloIntent(store, name, workdir, "safe")
231-
fireHook("on_safe", intent)
232-
// Map "on_safe" to a serve session_attached — the hub doesn't
233-
// model safe-mode separately, only the lifecycle transition.
234-
fireServeEvent("session_attached", intent)
235-
}
236-
237-
// If session exists and mode matches → preflight. preflight handles both
238-
// live tmux (plain reattach) and dead tmux (recreate with --resume UUID),
239-
// so the session's claude history survives `claude` exiting on its own.
240-
// Force-fresh escape hatches: `ctm kill <name>` / `ctm forget <name>`.
241-
if sess, err := store.Get(name); err == nil {
242-
if shouldResumeExisting(sess, "safe") {
243-
out.Debug(Verbose, "existing safe session %q — running pre-flight", name)
244-
return preflight(sess, cfg, store, tc, out)
245-
}
246-
// Mode change: drop tmux + store record so a fresh UUID is minted.
247-
if tc.HasSession(name) {
248-
if err := tc.KillSession(name); err != nil {
249-
out.Warn("could not kill existing session: %v", err)
250-
}
251-
}
252-
if err := store.Delete(name); err != nil {
253-
_ = err
254-
}
255-
}
256-
257-
return createAndAttach(name, workdir, "safe", store, tc, out)
258158
}
259159

260160
// resolveWorkdir returns the workdir for name: from store if present, else from
@@ -274,23 +174,3 @@ func resolveWorkdir(name string, store *session.Store, tc *tmux.Client) (string,
274174
}
275175
return cwd, nil
276176
}
277-
278-
// gitCheckpoint creates a git checkpoint commit in workdir before yolo mode.
279-
func gitCheckpoint(workdir string, out *output.Printer) {
280-
check := exec.Command("git", "-C", workdir, "rev-parse", "--is-inside-work-tree")
281-
if err := check.Run(); err != nil {
282-
out.Dim("(not a git repo — skipping checkpoint)")
283-
return
284-
}
285-
286-
exec.Command("git", "-C", workdir, "add", "-A").Run() //nolint:errcheck
287-
288-
ts := time.Now().Format("2006-01-02T15:04:05")
289-
msg := fmt.Sprintf("checkpoint: pre-yolo %s", ts)
290-
exec.Command("git", "-C", workdir, "commit", "-m", msg, "--allow-empty", "-q").Run() //nolint:errcheck
291-
292-
out.Dim("git checkpoint created — to rollback: git -C %s reset --hard HEAD~1", workdir)
293-
}
294-
295-
// Ensure shell import is used (completion helper comes from shell package).
296-
var _ = claude.BuildCommand

0 commit comments

Comments
 (0)