88 "encoding/json"
99 "errors"
1010 "fmt"
11+ "io"
1112 "net/http"
12- "net/http/httptest"
1313 "os"
1414 "path/filepath"
1515 "strings"
@@ -21,6 +21,7 @@ import (
2121 "github.com/Gradient-Linux/concave/internal/gpu"
2222 "github.com/Gradient-Linux/concave/internal/suite"
2323 "github.com/Gradient-Linux/concave/internal/ui"
24+ "github.com/Gradient-Linux/concave/internal/workspace"
2425 "github.com/spf13/cobra"
2526)
2627
@@ -31,6 +32,12 @@ type mockExitError struct {
3132func (m mockExitError ) Error () string { return fmt .Sprintf ("exit %d" , m .code ) }
3233func (m mockExitError ) ExitCode () int { return m .code }
3334
35+ type roundTripFunc func (* http.Request ) (* http.Response , error )
36+
37+ func (fn roundTripFunc ) RoundTrip (req * http.Request ) (* http.Response , error ) {
38+ return fn (req )
39+ }
40+
3441func restoreCommandDeps (t * testing.T ) {
3542 t .Helper ()
3643
@@ -40,6 +47,7 @@ func restoreCommandDeps(t *testing.T) {
4047 oldEnsureWorkspaceLayout := ensureWorkspaceLayout
4148 oldWorkspaceExists := workspaceExists
4249 oldWorkspaceRoot := workspaceRoot
50+ oldWorkspaceUserRoot := workspaceUserRoot
4351 oldWorkspaceStatus := workspaceStatus
4452 oldWorkspaceBackup := workspaceBackup
4553 oldWorkspaceClean := workspaceClean
@@ -85,15 +93,19 @@ func restoreCommandDeps(t *testing.T) {
8593 oldGPUVerifyPassthrough := gpuVerifyPassthrough
8694 oldGPUSecureBootEnabled := gpuSecureBootEnabled
8795 oldSystemDockerRunning := systemDockerRunning
96+ oldSystemCommandAvailable := systemCommandAvailable
97+ oldSystemDockerCompose := systemDockerCompose
8898 oldSystemUserInDockerGroup := systemUserInDockerGroup
8999 oldSystemInternetReachable := systemInternetReachable
100+ oldSystemRunPrivileged := systemRunPrivileged
90101 oldSystemCheckConflicts := systemCheckConflicts
91102 oldSystemRegisterPorts := systemRegisterPorts
92103 oldSystemDeregisterPorts := systemDeregisterPorts
93104 oldSystemOpenURL := systemOpenURL
94105 oldSystemSignalHandler := systemSignalHandler
95106 oldSystemLock := systemLock
96107 oldUIConfirm := uiConfirm
108+ oldCurrentUsername := currentUsername
97109 oldRunDockerOutput := runDockerOutput
98110 oldRunDockerInteractive := runDockerInteractive
99111 oldLabSuite := labSuite
@@ -108,6 +120,7 @@ func restoreCommandDeps(t *testing.T) {
108120 ensureWorkspaceLayout = oldEnsureWorkspaceLayout
109121 workspaceExists = oldWorkspaceExists
110122 workspaceRoot = oldWorkspaceRoot
123+ workspaceUserRoot = oldWorkspaceUserRoot
111124 workspaceStatus = oldWorkspaceStatus
112125 workspaceBackup = oldWorkspaceBackup
113126 workspaceClean = oldWorkspaceClean
@@ -153,15 +166,19 @@ func restoreCommandDeps(t *testing.T) {
153166 gpuVerifyPassthrough = oldGPUVerifyPassthrough
154167 gpuSecureBootEnabled = oldGPUSecureBootEnabled
155168 systemDockerRunning = oldSystemDockerRunning
169+ systemCommandAvailable = oldSystemCommandAvailable
170+ systemDockerCompose = oldSystemDockerCompose
156171 systemUserInDockerGroup = oldSystemUserInDockerGroup
157172 systemInternetReachable = oldSystemInternetReachable
173+ systemRunPrivileged = oldSystemRunPrivileged
158174 systemCheckConflicts = oldSystemCheckConflicts
159175 systemRegisterPorts = oldSystemRegisterPorts
160176 systemDeregisterPorts = oldSystemDeregisterPorts
161177 systemOpenURL = oldSystemOpenURL
162178 systemSignalHandler = oldSystemSignalHandler
163179 systemLock = oldSystemLock
164180 uiConfirm = oldUIConfirm
181+ currentUsername = oldCurrentUsername
165182 runDockerOutput = oldRunDockerOutput
166183 runDockerInteractive = oldRunDockerInteractive
167184 labSuite = oldLabSuite
@@ -250,18 +267,88 @@ func TestResolveExitCodeFindsWrappedExitErrors(t *testing.T) {
250267func TestInstallInvalidSuite (t * testing.T ) {
251268 restoreCommandDeps (t )
252269
253- gpuDetectState = func () (gpu.GPUState , error ) { return gpu .GPUStateNone , nil }
254- installSuite = func (ctx context.Context , name string , opts suite.InstallOptions ) error {
255- _ , err := suite .Get (name )
256- return err
257- }
258-
259270 err := runInstall (installCmd , []string {"invalid" })
260271 if err == nil || err .Error () != "unknown suite: invalid. Valid suites: boosting, neural, flow, forge" {
261272 t .Fatalf ("runInstall() error = %v" , err )
262273 }
263274}
264275
276+ func TestEnsureDockerRuntimeInstallsAndStartsDocker (t * testing.T ) {
277+ restoreCommandDeps (t )
278+
279+ dockerInstalled := false
280+ dockerRunning := false
281+
282+ systemCommandAvailable = func (name string ) bool {
283+ return name == "docker" && dockerInstalled
284+ }
285+ systemDockerCompose = func () (bool , error ) {
286+ if ! dockerInstalled {
287+ return false , nil
288+ }
289+ return true , nil
290+ }
291+ systemDockerRunning = func () (bool , error ) {
292+ if ! dockerInstalled {
293+ return false , errors .New ("docker info: exec: \" docker\" : executable file not found in $PATH" )
294+ }
295+ return dockerRunning , nil
296+ }
297+ systemUserInDockerGroup = func () (bool , error ) { return true , nil }
298+ uiConfirm = func (question string ) bool { return true }
299+
300+ var privileged [][]string
301+ systemRunPrivileged = func (ctx context.Context , description string , name string , args ... string ) error {
302+ privileged = append (privileged , append ([]string {name }, args ... ))
303+ if len (args ) >= 4 && name == "env" && args [1 ] == "apt-get" && args [2 ] == "install" {
304+ dockerInstalled = true
305+ }
306+ if name == "systemctl" {
307+ dockerRunning = true
308+ }
309+ return nil
310+ }
311+
312+ if err := ensureDockerRuntime (context .Background (), "install neural" ); err != nil {
313+ t .Fatalf ("ensureDockerRuntime() error = %v" , err )
314+ }
315+
316+ if ! dockerInstalled || ! dockerRunning {
317+ t .Fatalf ("expected docker to be installed and running, got installed=%v running=%v" , dockerInstalled , dockerRunning )
318+ }
319+ if len (privileged ) < 2 {
320+ t .Fatalf ("expected privileged install/start calls, got %#v" , privileged )
321+ }
322+ }
323+
324+ func TestEnsureDockerRuntimeRequestsReloginAfterDockerGroupAdd (t * testing.T ) {
325+ restoreCommandDeps (t )
326+
327+ systemCommandAvailable = func (name string ) bool { return name == "docker" }
328+ systemDockerCompose = func () (bool , error ) { return true , nil }
329+ systemDockerRunning = func () (bool , error ) { return true , nil }
330+ systemUserInDockerGroup = func () (bool , error ) { return false , nil }
331+ uiConfirm = func (question string ) bool { return true }
332+ currentUsername = func () string { return "mark" }
333+
334+ var got []string
335+ systemRunPrivileged = func (ctx context.Context , description string , name string , args ... string ) error {
336+ got = append ([]string {name }, args ... )
337+ return nil
338+ }
339+
340+ err := ensureDockerRuntime (context .Background (), "install neural" )
341+ if err == nil {
342+ t .Fatal ("expected relogin error" )
343+ }
344+ if ! strings .Contains (err .Error (), "log out and back in" ) {
345+ t .Fatalf ("expected relogin guidance, got %v" , err )
346+ }
347+ if strings .Join (got , " " ) != "usermod -aG docker mark" {
348+ t .Fatalf ("unexpected privileged call %#v" , got )
349+ }
350+ }
351+
265352func TestStartWithNoInstalledSuites (t * testing.T ) {
266353 restoreCommandDeps (t )
267354 buf := captureOutput (t )
@@ -529,6 +616,10 @@ func TestDoctorAndWorkspaceCommandsStillWork(t *testing.T) {
529616
530617 home := t .TempDir ()
531618 t .Setenv ("HOME" , home )
619+ workspaceUserRoot = func () string { return filepath .Join (home , "gradient" ) }
620+ ensureWorkspaceLayout = func () error {
621+ return os .MkdirAll (filepath .Join (home , "gradient" ), 0o755 )
622+ }
532623 if err := workspaceInitCmd .RunE (workspaceInitCmd , nil ); err != nil {
533624 t .Fatalf ("workspaceInitCmd.RunE() error = %v" , err )
534625 }
@@ -540,6 +631,87 @@ func TestDoctorAndWorkspaceCommandsStillWork(t *testing.T) {
540631 }
541632}
542633
634+ func TestWorkspaceCommandsPreferUserWorkspaceRoot (t * testing.T ) {
635+ restoreCommandDeps (t )
636+
637+ home := t .TempDir ()
638+ expected := filepath .Join (home , "gradient" )
639+ t .Setenv ("HOME" , home )
640+ t .Setenv ("GRADIENT_WORKSPACE_ROOT" , "/var/lib/gradient" )
641+
642+ workspaceUserRoot = func () string { return expected }
643+ ensureWorkspaceLayout = func () error {
644+ if got := os .Getenv ("GRADIENT_WORKSPACE_ROOT" ); got != expected {
645+ t .Fatalf ("GRADIENT_WORKSPACE_ROOT = %q, want %q" , got , expected )
646+ }
647+ return nil
648+ }
649+ workspaceStatus = func () ([]workspace.Usage , error ) {
650+ if got := os .Getenv ("GRADIENT_WORKSPACE_ROOT" ); got != expected {
651+ t .Fatalf ("GRADIENT_WORKSPACE_ROOT = %q, want %q" , got , expected )
652+ }
653+ return []workspace.Usage {{Name : "data" , Bytes : 1 }}, nil
654+ }
655+ workspaceBackup = func () (string , error ) {
656+ if got := os .Getenv ("GRADIENT_WORKSPACE_ROOT" ); got != expected {
657+ t .Fatalf ("GRADIENT_WORKSPACE_ROOT = %q, want %q" , got , expected )
658+ }
659+ return filepath .Join (expected , "backups" , "archive.tar.gz" ), nil
660+ }
661+ workspaceClean = func () error {
662+ if got := os .Getenv ("GRADIENT_WORKSPACE_ROOT" ); got != expected {
663+ t .Fatalf ("GRADIENT_WORKSPACE_ROOT = %q, want %q" , got , expected )
664+ }
665+ return nil
666+ }
667+ workspaceRoot = func () string {
668+ return os .Getenv ("GRADIENT_WORKSPACE_ROOT" )
669+ }
670+
671+ if err := workspaceInitCmd .RunE (workspaceInitCmd , nil ); err != nil {
672+ t .Fatalf ("workspaceInitCmd.RunE() error = %v" , err )
673+ }
674+ if err := workspaceStatusCmd .RunE (workspaceStatusCmd , nil ); err != nil {
675+ t .Fatalf ("workspaceStatusCmd.RunE() error = %v" , err )
676+ }
677+ if err := workspaceBackupCmd .RunE (workspaceBackupCmd , nil ); err != nil {
678+ t .Fatalf ("workspaceBackupCmd.RunE() error = %v" , err )
679+ }
680+ workspacePruneOutputs = true
681+ defer func () { workspacePruneOutputs = false }()
682+ if err := workspacePruneCmd .RunE (workspacePruneCmd , nil ); err != nil {
683+ t .Fatalf ("workspacePruneCmd.RunE() error = %v" , err )
684+ }
685+
686+ if got := os .Getenv ("GRADIENT_WORKSPACE_ROOT" ); got != "/var/lib/gradient" {
687+ t .Fatalf ("GRADIENT_WORKSPACE_ROOT after command = %q, want %q" , got , "/var/lib/gradient" )
688+ }
689+ }
690+
691+ func TestConfigureDefaultWorkspaceRoot (t * testing.T ) {
692+ restoreCommandDeps (t )
693+
694+ workspaceUserRoot = func () string { return "/tmp/user-gradient" }
695+
696+ _ = os .Unsetenv ("GRADIENT_WORKSPACE_ROOT" )
697+ configureDefaultWorkspaceRoot (statusCmd )
698+ if got := os .Getenv ("GRADIENT_WORKSPACE_ROOT" ); got != "/tmp/user-gradient" {
699+ t .Fatalf ("GRADIENT_WORKSPACE_ROOT = %q, want %q" , got , "/tmp/user-gradient" )
700+ }
701+
702+ _ = os .Unsetenv ("GRADIENT_WORKSPACE_ROOT" )
703+ configureDefaultWorkspaceRoot (serveCmd )
704+ if got , ok := os .LookupEnv ("GRADIENT_WORKSPACE_ROOT" ); ok {
705+ t .Fatalf ("GRADIENT_WORKSPACE_ROOT unexpectedly set for serve: %q" , got )
706+ }
707+
708+ _ = os .Setenv ("GRADIENT_WORKSPACE_ROOT" , "/explicit/root" )
709+ configureDefaultWorkspaceRoot (statusCmd )
710+ if got := os .Getenv ("GRADIENT_WORKSPACE_ROOT" ); got != "/explicit/root" {
711+ t .Fatalf ("explicit GRADIENT_WORKSPACE_ROOT overwritten: %q" , got )
712+ }
713+ }
714+
543715func TestUpdateRollbackChangelogAndHelpers (t * testing.T ) {
544716 restoreCommandDeps (t )
545717 buf := captureOutput (t )
@@ -617,6 +789,7 @@ func TestRemoveStopRestartAndShellCommands(t *testing.T) {
617789 }
618790 return nil
619791 }
792+ runDockerOutput = func (ctx context.Context , args ... string ) ([]byte , error ) { return nil , nil }
620793 dockerComposeDown = func (ctx context.Context , path string ) error { return nil }
621794 dockerComposeUp = func (ctx context.Context , path string , detach bool ) error { return nil }
622795
@@ -738,7 +911,13 @@ func TestDriverWizardSetupAndSelfUpdate(t *testing.T) {
738911 }
739912
740913 ensureWorkspaceLayout = func () error { return nil }
741- workspaceRoot = func () string { return t .TempDir () }
914+ home := t .TempDir ()
915+ t .Setenv ("HOME" , home )
916+ workspaceRoot = func () string { return home }
917+ systemCommandAvailable = func (name string ) bool { return name == "docker" }
918+ systemDockerCompose = func () (bool , error ) { return true , nil }
919+ systemDockerRunning = func () (bool , error ) { return true , nil }
920+ systemUserInDockerGroup = func () (bool , error ) { return true , nil }
742921 gpuDetectState = func () (gpu.GPUState , error ) { return gpu .GPUStateNone , nil }
743922 installed := []string {}
744923 isInstalled = func (name string ) (bool , error ) {
@@ -771,25 +950,41 @@ func TestDriverWizardSetupAndSelfUpdate(t *testing.T) {
771950
772951 binary := []byte ("concave-binary" )
773952 sum := sha256 .Sum256 (binary )
774- var server * httptest.Server
775- server = httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
776- switch r .URL .Path {
777- case "/manifest" :
778- _ = json .NewEncoder (w ).Encode (updateManifest {
779- Version : "v0.1.0" ,
780- URL : server .URL + "/concave" ,
781- SHA256 : hex .EncodeToString (sum [:]),
782- })
783- case "/concave" :
784- _ , _ = w .Write (binary )
785- default :
786- http .NotFound (w , r )
787- }
788- }))
789- defer server .Close ()
790-
791- selfUpdateClient = server .Client ()
792- selfUpdateManifestURL = server .URL + "/manifest"
953+ const manifestURL = "https://updates.example.test/manifest"
954+ const binaryURL = "https://updates.example.test/concave"
955+ selfUpdateClient = & http.Client {
956+ Transport : roundTripFunc (func (req * http.Request ) (* http.Response , error ) {
957+ switch req .URL .String () {
958+ case manifestURL :
959+ payload , err := json .Marshal (updateManifest {
960+ Version : "v0.1.0" ,
961+ URL : binaryURL ,
962+ SHA256 : hex .EncodeToString (sum [:]),
963+ })
964+ if err != nil {
965+ return nil , err
966+ }
967+ return & http.Response {
968+ StatusCode : http .StatusOK ,
969+ Body : io .NopCloser (strings .NewReader (string (payload ))),
970+ Header : make (http.Header ),
971+ }, nil
972+ case binaryURL :
973+ return & http.Response {
974+ StatusCode : http .StatusOK ,
975+ Body : io .NopCloser (strings .NewReader (string (binary ))),
976+ Header : make (http.Header ),
977+ }, nil
978+ default :
979+ return & http.Response {
980+ StatusCode : http .StatusNotFound ,
981+ Body : io .NopCloser (strings .NewReader ("not found" )),
982+ Header : make (http.Header ),
983+ }, nil
984+ }
985+ }),
986+ }
987+ selfUpdateManifestURL = manifestURL
793988 selfUpdateTargetPath = filepath .Join (t .TempDir (), "concave" )
794989 if err := selfUpdateCmd .RunE (selfUpdateCmd , nil ); err != nil {
795990 t .Fatalf ("selfUpdateCmd.RunE() error = %v" , err )
0 commit comments