detached adcf282

feat(post-install): add installation verification gate with warnings

commit
adcf282ed8fc36ed4aaf60e293a5322763e94e07
author
brice · · 2026-02-27 05:38 UTC
parent
cb324d6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +148 -3
M internal/application/commands/post_install.go +1 -0
@@ -9,4 +9,5 @@ type PostInstallCommand struct {
99 PlymouthTheme string // Plymouth theme to install (optional)
1010 InstallDankLinux bool // Whether to write the Dank Linux flag file for first-boot auto-install
1111 TargetDisk string // Target disk for bootloader hook (e.g. /dev/sda)
12+ Encrypted bool // Whether disk encryption is enabled
1213 }
@@ -9,4 +9,5 @@ type PostInstallCommand struct {
9 PlymouthTheme string // Plymouth theme to install (optional)9 PlymouthTheme string // Plymouth theme to install (optional)
10 InstallDankLinux bool // Whether to write the Dank Linux flag file for first-boot auto-install10 InstallDankLinux bool // Whether to write the Dank Linux flag file for first-boot auto-install
11 TargetDisk string // Target disk for bootloader hook (e.g. /dev/sda)11 TargetDisk string // Target disk for bootloader hook (e.g. /dev/sda)
12+ Encrypted bool // Whether disk encryption is enabled
12 }13 }
M internal/application/dto/post_install_result.go +4 -3
@@ -2,7 +2,8 @@ package dto
22
33 // PostInstallResult is the result of post-installation tasks
44 type PostInstallResult struct {
5- Success bool
6- TasksRun []string
7- ErrorDetail string
5+ Success bool
6+ TasksRun []string
7+ ErrorDetail string
8+ VerificationWarnings []string
89 }
@@ -2,7 +2,8 @@ package dto
2 2
3 // PostInstallResult is the result of post-installation tasks3 // PostInstallResult is the result of post-installation tasks
4 type PostInstallResult struct {4 type PostInstallResult struct {
5- Success bool5+ Success bool
6- TasksRun []string6+ TasksRun []string
7- ErrorDetail string7+ ErrorDetail string
8+ VerificationWarnings []string
8 }9 }
M internal/application/handlers/post_install_handler.go +23 -0
@@ -114,6 +114,11 @@ func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstal
114114 }
115115
116116 // Final cleanup and verification
117+ result.VerificationWarnings = h.verifyInstallation(cmd.MountPoint, cmd.Encrypted)
118+ if len(result.VerificationWarnings) > 0 {
119+ h.logger.Warn("Post-install verification warnings", "warnings", result.VerificationWarnings)
120+ }
121+
117122 h.logger.Info("Post-installation tasks completed")
118123 result.Success = true
119124
@@ -285,6 +290,24 @@ func enablePacmanOption(conf, commented, replacement string) string {
285290 return strings.ReplaceAll(conf, commented, replacement)
286291 }
287292
293+func (h *PostInstallHandler) verifyInstallation(mountPoint string, encrypted bool) []string {
294+ warnings := []string{}
295+ checks := []struct{ path, name string }{
296+ {filepath.Join(mountPoint, "etc", "fstab"), "fstab"},
297+ {filepath.Join(mountPoint, "boot", "limine.conf"), "limine.conf"},
298+ {filepath.Join(mountPoint, "boot", "EFI", "BOOT", "BOOTX64.EFI"), "EFI boot file"},
299+ }
300+ if encrypted {
301+ checks = append(checks, struct{ path, name string }{filepath.Join(mountPoint, "etc", "crypttab"), "crypttab"})
302+ }
303+ for _, c := range checks {
304+ if _, err := h.fs.Stat(c.path); err != nil {
305+ warnings = append(warnings, fmt.Sprintf("missing %s: %s", c.name, c.path))
306+ }
307+ }
308+ return warnings
309+}
310+
288311 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {
289312 url := fmt.Sprintf("%s/%s", h.rawURL, path)
290313 h.logger.Info("Downloading template", "url", url)
@@ -114,6 +114,11 @@ func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstal
114 }114 }
115 115
116 // Final cleanup and verification116 // Final cleanup and verification
117+ result.VerificationWarnings = h.verifyInstallation(cmd.MountPoint, cmd.Encrypted)
118+ if len(result.VerificationWarnings) > 0 {
119+ h.logger.Warn("Post-install verification warnings", "warnings", result.VerificationWarnings)
120+ }
121+
117 h.logger.Info("Post-installation tasks completed")122 h.logger.Info("Post-installation tasks completed")
118 result.Success = true123 result.Success = true
119 124
@@ -285,6 +290,24 @@ func enablePacmanOption(conf, commented, replacement string) string {
285 return strings.ReplaceAll(conf, commented, replacement)290 return strings.ReplaceAll(conf, commented, replacement)
286 }291 }
287 292
293+func (h *PostInstallHandler) verifyInstallation(mountPoint string, encrypted bool) []string {
294+ warnings := []string{}
295+ checks := []struct{ path, name string }{
296+ {filepath.Join(mountPoint, "etc", "fstab"), "fstab"},
297+ {filepath.Join(mountPoint, "boot", "limine.conf"), "limine.conf"},
298+ {filepath.Join(mountPoint, "boot", "EFI", "BOOT", "BOOTX64.EFI"), "EFI boot file"},
299+ }
300+ if encrypted {
301+ checks = append(checks, struct{ path, name string }{filepath.Join(mountPoint, "etc", "crypttab"), "crypttab"})
302+ }
303+ for _, c := range checks {
304+ if _, err := h.fs.Stat(c.path); err != nil {
305+ warnings = append(warnings, fmt.Sprintf("missing %s: %s", c.name, c.path))
306+ }
307+ }
308+ return warnings
309+}
310+
288 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {311 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {
289 url := fmt.Sprintf("%s/%s", h.rawURL, path)312 url := fmt.Sprintf("%s/%s", h.rawURL, path)
290 h.logger.Info("Downloading template", "url", url)313 h.logger.Info("Downloading template", "url", url)
M internal/application/handlers/post_install_handler_test.go +119 -0
@@ -2,6 +2,7 @@ package handlers
22
33 import (
44 "context"
5+ "fmt"
56 "net/http"
67 "strings"
78 "testing"
@@ -40,6 +41,7 @@ func TestPostInstallHandler_Handle_NoScripts(t *testing.T) {
4041 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
4142 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
4243 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
44+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
4345
4446 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
4547
@@ -86,6 +88,7 @@ func TestPostInstallHandler_Handle_WithScripts(t *testing.T) {
8688 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
8789 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
8890 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
91+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
8992
9093 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
9194
@@ -135,6 +138,7 @@ func TestPostInstallHandler_Handle_WithPlymouth(t *testing.T) {
135138 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
136139 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
137140 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
141+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
138142
139143 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
140144
@@ -185,6 +189,7 @@ func TestPostInstallHandler_Handle_WithDankLinux(t *testing.T) {
185189 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
186190 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
187191 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
192+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
188193
189194 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
190195
@@ -235,6 +240,7 @@ func TestPostInstallHandler_Handle_TunesPacman(t *testing.T) {
235240 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
236241 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
237242 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
243+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
238244
239245 // ReadFile for pacman.conf returns commented options; other reads return generic content
240246 mockFS.EXPECT().ReadFile(gomock.Eq("/mnt/etc/pacman.conf")).Return([]byte(pacmanContent), nil).AnyTimes()
@@ -305,6 +311,7 @@ func TestPostInstallHandler_Handle_InstallsLimineHook(t *testing.T) {
305311 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte(hookTemplate)), nil).AnyTimes()
306312 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
307313 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
314+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
308315
309316 hookWritten := false
310317 mockFS.EXPECT().WriteFile(gomock.Eq(hookFile), gomock.Any(), gomock.Any()).DoAndReturn(
@@ -389,6 +396,7 @@ func TestPostInstallHandler_Handle_EnablesSnapperSync(t *testing.T) {
389396 }).AnyTimes()
390397 // Catch-all for any other ChrootSystemctl calls
391398 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
399+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
392400
393401 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
394402
@@ -439,6 +447,7 @@ func TestPostInstallHandler_Handle_Everything(t *testing.T) {
439447 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
440448 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
441449 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
450+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
442451
443452 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
444453
@@ -477,3 +486,113 @@ func TestPostInstallHandler_Handle_Everything(t *testing.T) {
477486 t.Error("expected plymouth-theme-bgrt task")
478487 }
479488 }
489+
490+func TestPostInstallHandler_Handle_RunsVerification(t *testing.T) {
491+ ctrl := gomock.NewController(t)
492+ defer ctrl.Finish()
493+
494+ mockFS := mocks.NewMockFileSystem(ctrl)
495+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
496+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
497+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
498+ mockLogger := mocks.NewMockLogger(ctrl)
499+
500+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
501+ mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
502+ mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
503+ mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
504+ mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
505+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
506+ mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
507+ mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
508+ mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
509+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
510+
511+ // Stat returns nil (files exist) for the checked paths
512+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
513+
514+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
515+
516+ cmd := commands.PostInstallCommand{
517+ MountPoint: "/mnt",
518+ Username: "testuser",
519+ RunPostBootScripts: false,
520+ PlymouthTheme: "",
521+ Encrypted: false,
522+ }
523+
524+ result, err := handler.Handle(context.Background(), cmd)
525+
526+ if err != nil {
527+ t.Fatalf("expected no error, got %v", err)
528+ }
529+
530+ if !result.Success {
531+ t.Error("expected success")
532+ }
533+
534+ if len(result.VerificationWarnings) != 0 {
535+ t.Errorf("expected no verification warnings, got: %v", result.VerificationWarnings)
536+ }
537+}
538+
539+func TestPostInstallHandler_Handle_VerificationWarnsOnMissingFiles(t *testing.T) {
540+ ctrl := gomock.NewController(t)
541+ defer ctrl.Finish()
542+
543+ mockFS := mocks.NewMockFileSystem(ctrl)
544+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
545+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
546+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
547+ mockLogger := mocks.NewMockLogger(ctrl)
548+
549+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
550+ mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
551+ mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
552+ mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
553+ mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
554+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
555+ mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
556+ mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
557+ mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
558+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
559+
560+ // limine.conf is missing; all other Stat calls succeed
561+ mockFS.EXPECT().Stat(gomock.Eq("/mnt/boot/limine.conf")).Return(nil, fmt.Errorf("not found"))
562+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
563+
564+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
565+
566+ cmd := commands.PostInstallCommand{
567+ MountPoint: "/mnt",
568+ Username: "testuser",
569+ RunPostBootScripts: false,
570+ PlymouthTheme: "",
571+ Encrypted: false,
572+ }
573+
574+ result, err := handler.Handle(context.Background(), cmd)
575+
576+ if err != nil {
577+ t.Fatalf("expected no error, got %v", err)
578+ }
579+
580+ if !result.Success {
581+ t.Error("expected success even with verification warnings")
582+ }
583+
584+ if len(result.VerificationWarnings) == 0 {
585+ t.Error("expected verification warnings for missing limine.conf")
586+ }
587+
588+ found := false
589+ for _, w := range result.VerificationWarnings {
590+ if strings.Contains(w, "limine.conf") {
591+ found = true
592+ break
593+ }
594+ }
595+ if !found {
596+ t.Errorf("expected warning about limine.conf, got: %v", result.VerificationWarnings)
597+ }
598+}
@@ -2,6 +2,7 @@ package handlers
2 2
3 import (3 import (
4 "context"4 "context"
5+ "fmt"
5 "net/http"6 "net/http"
6 "strings"7 "strings"
7 "testing"8 "testing"
@@ -40,6 +41,7 @@ func TestPostInstallHandler_Handle_NoScripts(t *testing.T) {
40 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()41 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
41 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()42 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
42 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()43 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
44+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
43 45
44 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")46 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
45 47
@@ -86,6 +88,7 @@ func TestPostInstallHandler_Handle_WithScripts(t *testing.T) {
86 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()88 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
87 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()89 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
88 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()90 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
91+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
89 92
90 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")93 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
91 94
@@ -135,6 +138,7 @@ func TestPostInstallHandler_Handle_WithPlymouth(t *testing.T) {
135 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()138 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
136 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()139 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
137 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()140 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
141+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
138 142
139 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")143 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
140 144
@@ -185,6 +189,7 @@ func TestPostInstallHandler_Handle_WithDankLinux(t *testing.T) {
185 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()189 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
186 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()190 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
187 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()191 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
192+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
188 193
189 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")194 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
190 195
@@ -235,6 +240,7 @@ func TestPostInstallHandler_Handle_TunesPacman(t *testing.T) {
235 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()240 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
236 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()241 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
237 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()242 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
243+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
238 244
239 // ReadFile for pacman.conf returns commented options; other reads return generic content245 // ReadFile for pacman.conf returns commented options; other reads return generic content
240 mockFS.EXPECT().ReadFile(gomock.Eq("/mnt/etc/pacman.conf")).Return([]byte(pacmanContent), nil).AnyTimes()246 mockFS.EXPECT().ReadFile(gomock.Eq("/mnt/etc/pacman.conf")).Return([]byte(pacmanContent), nil).AnyTimes()
@@ -305,6 +311,7 @@ func TestPostInstallHandler_Handle_InstallsLimineHook(t *testing.T) {
305 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte(hookTemplate)), nil).AnyTimes()311 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte(hookTemplate)), nil).AnyTimes()
306 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()312 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
307 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()313 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
314+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
308 315
309 hookWritten := false316 hookWritten := false
310 mockFS.EXPECT().WriteFile(gomock.Eq(hookFile), gomock.Any(), gomock.Any()).DoAndReturn(317 mockFS.EXPECT().WriteFile(gomock.Eq(hookFile), gomock.Any(), gomock.Any()).DoAndReturn(
@@ -389,6 +396,7 @@ func TestPostInstallHandler_Handle_EnablesSnapperSync(t *testing.T) {
389 }).AnyTimes()396 }).AnyTimes()
390 // Catch-all for any other ChrootSystemctl calls397 // Catch-all for any other ChrootSystemctl calls
391 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()398 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
399+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
392 400
393 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")401 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
394 402
@@ -439,6 +447,7 @@ func TestPostInstallHandler_Handle_Everything(t *testing.T) {
439 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()447 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
440 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()448 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
441 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()449 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
450+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
442 451
443 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")452 handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
444 453
@@ -477,3 +486,113 @@ func TestPostInstallHandler_Handle_Everything(t *testing.T) {
477 t.Error("expected plymouth-theme-bgrt task")486 t.Error("expected plymouth-theme-bgrt task")
478 }487 }
479 }488 }
489+
490+func TestPostInstallHandler_Handle_RunsVerification(t *testing.T) {
491+ ctrl := gomock.NewController(t)
492+ defer ctrl.Finish()
493+
494+ mockFS := mocks.NewMockFileSystem(ctrl)
495+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
496+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
497+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
498+ mockLogger := mocks.NewMockLogger(ctrl)
499+
500+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
501+ mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
502+ mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
503+ mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
504+ mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
505+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
506+ mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
507+ mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
508+ mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
509+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
510+
511+ // Stat returns nil (files exist) for the checked paths
512+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
513+
514+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
515+
516+ cmd := commands.PostInstallCommand{
517+ MountPoint: "/mnt",
518+ Username: "testuser",
519+ RunPostBootScripts: false,
520+ PlymouthTheme: "",
521+ Encrypted: false,
522+ }
523+
524+ result, err := handler.Handle(context.Background(), cmd)
525+
526+ if err != nil {
527+ t.Fatalf("expected no error, got %v", err)
528+ }
529+
530+ if !result.Success {
531+ t.Error("expected success")
532+ }
533+
534+ if len(result.VerificationWarnings) != 0 {
535+ t.Errorf("expected no verification warnings, got: %v", result.VerificationWarnings)
536+ }
537+}
538+
539+func TestPostInstallHandler_Handle_VerificationWarnsOnMissingFiles(t *testing.T) {
540+ ctrl := gomock.NewController(t)
541+ defer ctrl.Finish()
542+
543+ mockFS := mocks.NewMockFileSystem(ctrl)
544+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
545+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
546+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
547+ mockLogger := mocks.NewMockLogger(ctrl)
548+
549+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
550+ mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
551+ mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
552+ mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
553+ mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
554+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
555+ mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
556+ mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
557+ mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
558+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
559+
560+ // limine.conf is missing; all other Stat calls succeed
561+ mockFS.EXPECT().Stat(gomock.Eq("/mnt/boot/limine.conf")).Return(nil, fmt.Errorf("not found"))
562+ mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
563+
564+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
565+
566+ cmd := commands.PostInstallCommand{
567+ MountPoint: "/mnt",
568+ Username: "testuser",
569+ RunPostBootScripts: false,
570+ PlymouthTheme: "",
571+ Encrypted: false,
572+ }
573+
574+ result, err := handler.Handle(context.Background(), cmd)
575+
576+ if err != nil {
577+ t.Fatalf("expected no error, got %v", err)
578+ }
579+
580+ if !result.Success {
581+ t.Error("expected success even with verification warnings")
582+ }
583+
584+ if len(result.VerificationWarnings) == 0 {
585+ t.Error("expected verification warnings for missing limine.conf")
586+ }
587+
588+ found := false
589+ for _, w := range result.VerificationWarnings {
590+ if strings.Contains(w, "limine.conf") {
591+ found = true
592+ break
593+ }
594+ }
595+ if !found {
596+ t.Errorf("expected warning about limine.conf, got: %v", result.VerificationWarnings)
597+ }
598+}
M internal/interfaces/tui/handlers/install_handler.go +1 -0
@@ -217,6 +217,7 @@ func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd
217217 RunPostBootScripts: true,
218218 InstallDankLinux: formData.InstallDankLinux,
219219 TargetDisk: formData.TargetDisk,
220+ Encrypted: parseEncryptionType(formData.EncryptionType) != disk.EncryptionTypeNone,
220221 }
221222 if _, err := svc.RunPostInstall(ctx, postCmd); err != nil {
222223 logger.Error("Post-installation failed", "error", err)
@@ -217,6 +217,7 @@ func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd
217 RunPostBootScripts: true,217 RunPostBootScripts: true,
218 InstallDankLinux: formData.InstallDankLinux,218 InstallDankLinux: formData.InstallDankLinux,
219 TargetDisk: formData.TargetDisk,219 TargetDisk: formData.TargetDisk,
220+ Encrypted: parseEncryptionType(formData.EncryptionType) != disk.EncryptionTypeNone,
220 }221 }
221 if _, err := svc.RunPostInstall(ctx, postCmd); err != nil {222 if _, err := svc.RunPostInstall(ctx, postCmd); err != nil {
222 logger.Error("Post-installation failed", "error", err)223 logger.Error("Post-installation failed", "error", err)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close