Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ import (
"runtime"

"github.com/CoreyRDean/intent/internal/config"
"github.com/CoreyRDean/intent/internal/daemon"
intentruntime "github.com/CoreyRDean/intent/internal/runtime"
"github.com/CoreyRDean/intent/internal/state"
"github.com/CoreyRDean/intent/internal/version"
)

type daemonStatusCaller interface {
Call(req daemon.Request) (*daemon.Response, error)
}

var newDaemonStatusClient = func(socket string) daemonStatusCaller {
return daemon.NewClient(socket)
}

var daemonServiceInstalled = daemon.IsInstalled

func cmdDoctor(_ context.Context, _ []string) int {
ok := true
check := func(name, status string, good bool) {
Expand All @@ -36,18 +47,23 @@ func cmdDoctor(_ context.Context, _ []string) int {
check("cache directory", dirs.Cache, true)
}

cfg, _ := config.Load(dirs.ConfigPath())
if cfg != nil {
check("config", fmt.Sprintf("backend=%s model=%s", cfg.Backend, cfg.Model), true)
}
if err == nil {
cfg, _ := config.Load(dirs.ConfigPath())
if cfg != nil {
check("config", fmt.Sprintf("backend=%s model=%s", cfg.Backend, cfg.Model), true)
}

rt := intentruntime.New(dirs.Cache)
check("llamafile runtime",
fmt.Sprintf("expected at %s", rt.LlamafilePath()),
rt.HaveLlamafile())
rt := intentruntime.New(dirs.Cache)
check("llamafile runtime",
fmt.Sprintf("expected at %s", rt.LlamafilePath()),
rt.HaveLlamafile())

modelFile, modelStatus := resolveModelCheck(cfg)
check("model", fmt.Sprintf("%s — %s", modelStatus, rt.ModelPath(modelFile)), rt.HaveModel(modelFile))
modelFile, modelStatus := resolveModelCheck(cfg)
check("model", fmt.Sprintf("%s — %s", modelStatus, rt.ModelPath(modelFile)), rt.HaveModel(modelFile))

daemonStatus, daemonOK := doctorDaemonStatus(dirs)
check("daemon", daemonStatus, daemonOK)
}

// Sandbox tooling.
switch runtime.GOOS {
Expand Down Expand Up @@ -91,3 +107,26 @@ func okStr(err error) string {
}
return "missing"
}

func doctorDaemonStatus(dirs state.Dirs) (string, bool) {
installed := daemonServiceInstalled(daemonLabel)
resp, err := newDaemonStatusClient(dirs.SocketPath()).Call(daemon.Request{Op: daemon.OpStatus})
if err != nil {
if installed {
return "installed but not responding", false
}
return "not running (optional)", true
}
if !resp.OK {
return "unhealthy: " + resp.Error, false
}

serviceState := "no"
if installed {
serviceState = "yes"
}
if endpoint, _ := resp.Data["llamafile_endpoint"].(string); endpoint != "" {
return fmt.Sprintf("running (service installed: %s, endpoint: %s)", serviceState, endpoint), true
}
return fmt.Sprintf("running (service installed: %s)", serviceState), true
}
103 changes: 103 additions & 0 deletions internal/cli/doctor_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package cli

import (
"context"
"errors"
"strings"
"testing"

"github.com/CoreyRDean/intent/internal/config"
"github.com/CoreyRDean/intent/internal/daemon"
intentruntime "github.com/CoreyRDean/intent/internal/runtime"
"github.com/CoreyRDean/intent/internal/state"
)

func TestResolveModelCheck(t *testing.T) {
Expand Down Expand Up @@ -52,3 +57,101 @@ func TestResolveModelCheck(t *testing.T) {
})
}
}

type stubDaemonStatusClient struct {
resp *daemon.Response
err error
}

func (s stubDaemonStatusClient) Call(_ daemon.Request) (*daemon.Response, error) {
return s.resp, s.err
}

func TestDoctorDaemonStatus(t *testing.T) {
origNewClient := newDaemonStatusClient
origInstalled := daemonServiceInstalled
t.Cleanup(func() {
newDaemonStatusClient = origNewClient
daemonServiceInstalled = origInstalled
})

dirs := state.Dirs{State: t.TempDir()}

tests := []struct {
name string
installed bool
client stubDaemonStatusClient
want string
wantOK bool
}{
{
name: "missing optional daemon is informational",
installed: false,
client: stubDaemonStatusClient{err: errors.New("dial unix: no such file or directory")},
want: "not running (optional)",
wantOK: true,
},
{
name: "installed daemon that does not respond is unhealthy",
installed: true,
client: stubDaemonStatusClient{err: errors.New("connection refused")},
want: "installed but not responding",
wantOK: false,
},
{
name: "running daemon reports endpoint",
installed: false,
client: stubDaemonStatusClient{resp: &daemon.Response{
OK: true,
Data: map[string]any{
"llamafile_endpoint": "http://127.0.0.1:18080",
},
}},
want: "running (service installed: no, endpoint: http://127.0.0.1:18080)",
wantOK: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
newDaemonStatusClient = func(string) daemonStatusCaller { return tt.client }
daemonServiceInstalled = func(string) bool { return tt.installed }

got, gotOK := doctorDaemonStatus(dirs)
if got != tt.want {
t.Fatalf("status = %q, want %q", got, tt.want)
}
if gotOK != tt.wantOK {
t.Fatalf("ok = %v, want %v", gotOK, tt.wantOK)
}
})
}
}

func TestDoctorPrintsDaemonStatus(t *testing.T) {
origNewClient := newDaemonStatusClient
origInstalled := daemonServiceInstalled
t.Cleanup(func() {
newDaemonStatusClient = origNewClient
daemonServiceInstalled = origInstalled
})

t.Setenv("HOME", t.TempDir())
t.Setenv("INTENT_STATE_DIR", t.TempDir())
t.Setenv("INTENT_CACHE_DIR", t.TempDir())

newDaemonStatusClient = func(string) daemonStatusCaller {
return stubDaemonStatusClient{err: errors.New("dial unix: no such file or directory")}
}
daemonServiceInstalled = func(string) bool { return false }

out := captureStdout(func() {
_ = cmdDoctor(context.Background(), nil)
})
if !strings.Contains(out, "daemon") {
t.Fatalf("doctor output missing daemon line: %q", out)
}
if !strings.Contains(out, "not running (optional)") {
t.Fatalf("doctor output missing optional daemon status: %q", out)
}
}
Loading