fix(snapshot): address coderabbit findings
- commit
- 049957ef17e32211be90634fa7c4f25538e4f2aa
- author
- brice · · 2026-07-06 08:32 UTC
- parent
- caf28f0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +187 -100
M internal/adapters/config/config.go +8 -5
| @@ -71,7 +71,9 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||
| 71 | 71 | } | |
| 72 | 72 | cfg.Codes[codeKey] = value | |
| 73 | 73 | case key == "snapshot.restore_processes": | |
| 74 | - | cfg.Snapshot.RestoreProcesses = parseProcessList(value, lineNo, &warnings) | |
| 74 | + | var processWarnings []domain.Warning | |
| 75 | + | cfg.Snapshot.RestoreProcesses, processWarnings = parseProcessList(value, lineNo) | |
| 76 | + | warnings = append(warnings, processWarnings...) | |
| 75 | 77 | cfg.Snapshot.RestoreProcessesSet = true | |
| 76 | 78 | default: | |
| 77 | 79 | if seenBindingKeys[key] { | |
| @@ -170,12 +172,13 @@ func updateBindingEntry(entries []domain.ConfigEntry, key, value string) { | |||
| 170 | 172 | } | |
| 171 | 173 | } | |
| 172 | 174 | ||
| 173 | - | func parseProcessList(value string, lineNo int, warnings *[]domain.Warning) []string { | |
| 175 | + | func parseProcessList(value string, lineNo int) ([]string, []domain.Warning) { | |
| 174 | 176 | if strings.TrimSpace(value) == "" { | |
| 175 | - | return nil | |
| 177 | + | return nil, nil | |
| 176 | 178 | } | |
| 177 | 179 | parts := strings.Split(value, ",") | |
| 178 | 180 | out := make([]string, 0, len(parts)) | |
| 181 | + | warnings := make([]domain.Warning, 0) | |
| 179 | 182 | seen := make(map[string]struct{}, len(parts)) | |
| 180 | 183 | for _, part := range parts { | |
| 181 | 184 | item := strings.TrimSpace(part) | |
| @@ -183,7 +186,7 @@ func parseProcessList(value string, lineNo int, warnings *[]domain.Warning) []st | |||
| 183 | 186 | continue | |
| 184 | 187 | } | |
| 185 | 188 | if !processNamePattern.MatchString(item) { | |
| 186 | - | *warnings = append(*warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid snapshot restore process %q", item)}) | |
| 189 | + | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid snapshot restore process %q", item)}) | |
| 187 | 190 | continue | |
| 188 | 191 | } | |
| 189 | 192 | if _, ok := seen[item]; ok { | |
| @@ -192,7 +195,7 @@ func parseProcessList(value string, lineNo int, warnings *[]domain.Warning) []st | |||
| 192 | 195 | seen[item] = struct{}{} | |
| 193 | 196 | out = append(out, item) | |
| 194 | 197 | } | |
| 195 | - | return out | |
| 198 | + | return out, warnings | |
| 196 | 199 | } | |
| 197 | 200 | ||
| 198 | 201 | func parseTheme(value string) (domain.ThemeMode, bool) { | |
| @@ -71,7 +71,9 @@ func Parse(r io.Reader) (domain.Config, []domain.Warning, error) { | |||||
| 71 | } | 71 | } | ||
| 72 | cfg.Codes[codeKey] = value | 72 | cfg.Codes[codeKey] = value | ||
| 73 | case key == "snapshot.restore_processes": | 73 | case key == "snapshot.restore_processes": | ||
| 74 | - | cfg.Snapshot.RestoreProcesses = parseProcessList(value, lineNo, &warnings) | 74 | + | var processWarnings []domain.Warning |
| 75 | + | cfg.Snapshot.RestoreProcesses, processWarnings = parseProcessList(value, lineNo) | |||
| 76 | + | warnings = append(warnings, processWarnings...) | |||
| 75 | cfg.Snapshot.RestoreProcessesSet = true | 77 | cfg.Snapshot.RestoreProcessesSet = true | ||
| 76 | default: | 78 | default: | ||
| 77 | if seenBindingKeys[key] { | 79 | if seenBindingKeys[key] { | ||
| @@ -170,12 +172,13 @@ func updateBindingEntry(entries []domain.ConfigEntry, key, value string) { | |||||
| 170 | } | 172 | } | ||
| 171 | } | 173 | } | ||
| 172 | 174 | ||||
| 173 | - | func parseProcessList(value string, lineNo int, warnings *[]domain.Warning) []string { | 175 | + | func parseProcessList(value string, lineNo int) ([]string, []domain.Warning) { |
| 174 | if strings.TrimSpace(value) == "" { | 176 | if strings.TrimSpace(value) == "" { | ||
| 175 | - | return nil | 177 | + | return nil, nil |
| 176 | } | 178 | } | ||
| 177 | parts := strings.Split(value, ",") | 179 | parts := strings.Split(value, ",") | ||
| 178 | out := make([]string, 0, len(parts)) | 180 | out := make([]string, 0, len(parts)) | ||
| 181 | + | warnings := make([]domain.Warning, 0) | |||
| 179 | seen := make(map[string]struct{}, len(parts)) | 182 | seen := make(map[string]struct{}, len(parts)) | ||
| 180 | for _, part := range parts { | 183 | for _, part := range parts { | ||
| 181 | item := strings.TrimSpace(part) | 184 | item := strings.TrimSpace(part) | ||
| @@ -183,7 +186,7 @@ func parseProcessList(value string, lineNo int, warnings *[]domain.Warning) []st | |||||
| 183 | continue | 186 | continue | ||
| 184 | } | 187 | } | ||
| 185 | if !processNamePattern.MatchString(item) { | 188 | if !processNamePattern.MatchString(item) { | ||
| 186 | - | *warnings = append(*warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid snapshot restore process %q", item)}) | 189 | + | warnings = append(warnings, domain.Warning{Line: lineNo, Msg: fmt.Sprintf("invalid snapshot restore process %q", item)}) |
| 187 | continue | 190 | continue | ||
| 188 | } | 191 | } | ||
| 189 | if _, ok := seen[item]; ok { | 192 | if _, ok := seen[item]; ok { | ||
| @@ -192,7 +195,7 @@ func parseProcessList(value string, lineNo int, warnings *[]domain.Warning) []st | |||||
| 192 | seen[item] = struct{}{} | 195 | seen[item] = struct{}{} | ||
| 193 | out = append(out, item) | 196 | out = append(out, item) | ||
| 194 | } | 197 | } | ||
| 195 | - | return out | 198 | + | return out, warnings |
| 196 | } | 199 | } | ||
| 197 | 200 | ||||
| 198 | func parseTheme(value string) (domain.ThemeMode, bool) { | 201 | func parseTheme(value string) (domain.ThemeMode, bool) { | ||
M internal/adapters/config/config_test.go +44 -35
| @@ -91,6 +91,49 @@ func TestParse(t *testing.T) { | |||
| 91 | 91 | {Line: 3, Msg: "missing key"}, | |
| 92 | 92 | }, | |
| 93 | 93 | }, | |
| 94 | + | { | |
| 95 | + | name: "snapshot restore processes", | |
| 96 | + | input: "snapshot.restore_processes = claude, codex, pi, opencode, btop\n", | |
| 97 | + | want: domain.Config{ | |
| 98 | + | Theme: domain.ThemeAuto, | |
| 99 | + | BindingEntries: []domain.ConfigEntry{}, | |
| 100 | + | Codes: map[string]string{}, | |
| 101 | + | Snapshot: domain.SnapshotConfig{ | |
| 102 | + | RestoreProcesses: []string{"claude", "codex", "pi", "opencode", "btop"}, | |
| 103 | + | RestoreProcessesSet: true, | |
| 104 | + | }, | |
| 105 | + | }, | |
| 106 | + | }, | |
| 107 | + | { | |
| 108 | + | name: "snapshot restore processes empty disables", | |
| 109 | + | input: "snapshot.restore_processes =\n", | |
| 110 | + | want: domain.Config{ | |
| 111 | + | Theme: domain.ThemeAuto, | |
| 112 | + | BindingEntries: []domain.ConfigEntry{}, | |
| 113 | + | Codes: map[string]string{}, | |
| 114 | + | Snapshot: domain.SnapshotConfig{ | |
| 115 | + | RestoreProcessesSet: true, | |
| 116 | + | }, | |
| 117 | + | }, | |
| 118 | + | }, | |
| 119 | + | { | |
| 120 | + | name: "snapshot restore processes warns on malformed entries", | |
| 121 | + | input: "snapshot.restore_processes = claude, custom-tool, /bin/sh, :all:, bad name, claude, , zsh\n", | |
| 122 | + | want: domain.Config{ | |
| 123 | + | Theme: domain.ThemeAuto, | |
| 124 | + | BindingEntries: []domain.ConfigEntry{}, | |
| 125 | + | Codes: map[string]string{}, | |
| 126 | + | Snapshot: domain.SnapshotConfig{ | |
| 127 | + | RestoreProcesses: []string{"claude", "custom-tool", "zsh"}, | |
| 128 | + | RestoreProcessesSet: true, | |
| 129 | + | }, | |
| 130 | + | }, | |
| 131 | + | wantWarnings: []domain.Warning{ | |
| 132 | + | {Line: 1, Msg: "invalid snapshot restore process \"/bin/sh\""}, | |
| 133 | + | {Line: 1, Msg: "invalid snapshot restore process \":all:\""}, | |
| 134 | + | {Line: 1, Msg: "invalid snapshot restore process \"bad name\""}, | |
| 135 | + | }, | |
| 136 | + | }, | |
| 94 | 137 | } | |
| 95 | 138 | ||
| 96 | 139 | for _, tt := range tests { | |
| @@ -98,7 +141,7 @@ func TestParse(t *testing.T) { | |||
| 98 | 141 | t.Parallel() | |
| 99 | 142 | ||
| 100 | 143 | if tt.want.Snapshot.RestoreProcesses == nil && !tt.want.Snapshot.RestoreProcessesSet { | |
| 101 | - | tt.want.Snapshot.RestoreProcesses = append([]string(nil), domain.DefaultSnapshotRestoreProcesses...) | |
| 144 | + | tt.want.Snapshot.RestoreProcesses = append([]string(nil), domain.DefaultSnapshotRestoreProcesses()...) | |
| 102 | 145 | } | |
| 103 | 146 | got, warnings, err := Parse(strings.NewReader(tt.input)) | |
| 104 | 147 | if err != nil { | |
| @@ -114,40 +157,6 @@ func TestParse(t *testing.T) { | |||
| 114 | 157 | } | |
| 115 | 158 | } | |
| 116 | 159 | ||
| 117 | - | func TestParseSnapshotRestoreProcesses(t *testing.T) { | |
| 118 | - | t.Parallel() | |
| 119 | - | ||
| 120 | - | cfg, warnings, err := Parse(strings.NewReader("snapshot.restore_processes = claude, codex, pi, opencode, btop\n")) | |
| 121 | - | require.NoError(t, err) | |
| 122 | - | require.Empty(t, warnings) | |
| 123 | - | require.Equal(t, []string{"claude", "codex", "pi", "opencode", "btop"}, cfg.Snapshot.RestoreProcesses) | |
| 124 | - | require.True(t, cfg.Snapshot.RestoreProcessesSet) | |
| 125 | - | } | |
| 126 | - | ||
| 127 | - | func TestParseSnapshotRestoreProcessesEmptyDisables(t *testing.T) { | |
| 128 | - | t.Parallel() | |
| 129 | - | ||
| 130 | - | cfg, warnings, err := Parse(strings.NewReader("snapshot.restore_processes =\n")) | |
| 131 | - | require.NoError(t, err) | |
| 132 | - | require.Empty(t, warnings) | |
| 133 | - | require.Empty(t, cfg.Snapshot.RestoreProcesses) | |
| 134 | - | require.True(t, cfg.Snapshot.RestoreProcessesSet) | |
| 135 | - | } | |
| 136 | - | ||
| 137 | - | func TestParseSnapshotRestoreProcessesWarnsOnMalformedEntries(t *testing.T) { | |
| 138 | - | t.Parallel() | |
| 139 | - | ||
| 140 | - | cfg, warnings, err := Parse(strings.NewReader("snapshot.restore_processes = claude, custom-tool, /bin/sh, :all:, bad name, claude, , zsh\n")) | |
| 141 | - | require.NoError(t, err) | |
| 142 | - | require.Equal(t, []domain.Warning{ | |
| 143 | - | {Line: 1, Msg: "invalid snapshot restore process \"/bin/sh\""}, | |
| 144 | - | {Line: 1, Msg: "invalid snapshot restore process \":all:\""}, | |
| 145 | - | {Line: 1, Msg: "invalid snapshot restore process \"bad name\""}, | |
| 146 | - | }, warnings) | |
| 147 | - | require.Equal(t, []string{"claude", "custom-tool", "zsh"}, cfg.Snapshot.RestoreProcesses) | |
| 148 | - | require.True(t, cfg.Snapshot.RestoreProcessesSet) | |
| 149 | - | } | |
| 150 | - | ||
| 151 | 160 | func TestDefaultsCopiesSnapshotRestoreProcesses(t *testing.T) { | |
| 152 | 161 | t.Parallel() | |
| 153 | 162 | ||
| @@ -91,6 +91,49 @@ func TestParse(t *testing.T) { | |||||
| 91 | {Line: 3, Msg: "missing key"}, | 91 | {Line: 3, Msg: "missing key"}, | ||
| 92 | }, | 92 | }, | ||
| 93 | }, | 93 | }, | ||
| 94 | + | { | |||
| 95 | + | name: "snapshot restore processes", | |||
| 96 | + | input: "snapshot.restore_processes = claude, codex, pi, opencode, btop\n", | |||
| 97 | + | want: domain.Config{ | |||
| 98 | + | Theme: domain.ThemeAuto, | |||
| 99 | + | BindingEntries: []domain.ConfigEntry{}, | |||
| 100 | + | Codes: map[string]string{}, | |||
| 101 | + | Snapshot: domain.SnapshotConfig{ | |||
| 102 | + | RestoreProcesses: []string{"claude", "codex", "pi", "opencode", "btop"}, | |||
| 103 | + | RestoreProcessesSet: true, | |||
| 104 | + | }, | |||
| 105 | + | }, | |||
| 106 | + | }, | |||
| 107 | + | { | |||
| 108 | + | name: "snapshot restore processes empty disables", | |||
| 109 | + | input: "snapshot.restore_processes =\n", | |||
| 110 | + | want: domain.Config{ | |||
| 111 | + | Theme: domain.ThemeAuto, | |||
| 112 | + | BindingEntries: []domain.ConfigEntry{}, | |||
| 113 | + | Codes: map[string]string{}, | |||
| 114 | + | Snapshot: domain.SnapshotConfig{ | |||
| 115 | + | RestoreProcessesSet: true, | |||
| 116 | + | }, | |||
| 117 | + | }, | |||
| 118 | + | }, | |||
| 119 | + | { | |||
| 120 | + | name: "snapshot restore processes warns on malformed entries", | |||
| 121 | + | input: "snapshot.restore_processes = claude, custom-tool, /bin/sh, :all:, bad name, claude, , zsh\n", | |||
| 122 | + | want: domain.Config{ | |||
| 123 | + | Theme: domain.ThemeAuto, | |||
| 124 | + | BindingEntries: []domain.ConfigEntry{}, | |||
| 125 | + | Codes: map[string]string{}, | |||
| 126 | + | Snapshot: domain.SnapshotConfig{ | |||
| 127 | + | RestoreProcesses: []string{"claude", "custom-tool", "zsh"}, | |||
| 128 | + | RestoreProcessesSet: true, | |||
| 129 | + | }, | |||
| 130 | + | }, | |||
| 131 | + | wantWarnings: []domain.Warning{ | |||
| 132 | + | {Line: 1, Msg: "invalid snapshot restore process \"/bin/sh\""}, | |||
| 133 | + | {Line: 1, Msg: "invalid snapshot restore process \":all:\""}, | |||
| 134 | + | {Line: 1, Msg: "invalid snapshot restore process \"bad name\""}, | |||
| 135 | + | }, | |||
| 136 | + | }, | |||
| 94 | } | 137 | } | ||
| 95 | 138 | ||||
| 96 | for _, tt := range tests { | 139 | for _, tt := range tests { | ||
| @@ -98,7 +141,7 @@ func TestParse(t *testing.T) { | |||||
| 98 | t.Parallel() | 141 | t.Parallel() | ||
| 99 | 142 | ||||
| 100 | if tt.want.Snapshot.RestoreProcesses == nil && !tt.want.Snapshot.RestoreProcessesSet { | 143 | if tt.want.Snapshot.RestoreProcesses == nil && !tt.want.Snapshot.RestoreProcessesSet { | ||
| 101 | - | tt.want.Snapshot.RestoreProcesses = append([]string(nil), domain.DefaultSnapshotRestoreProcesses...) | 144 | + | tt.want.Snapshot.RestoreProcesses = append([]string(nil), domain.DefaultSnapshotRestoreProcesses()...) |
| 102 | } | 145 | } | ||
| 103 | got, warnings, err := Parse(strings.NewReader(tt.input)) | 146 | got, warnings, err := Parse(strings.NewReader(tt.input)) | ||
| 104 | if err != nil { | 147 | if err != nil { | ||
| @@ -114,40 +157,6 @@ func TestParse(t *testing.T) { | |||||
| 114 | } | 157 | } | ||
| 115 | } | 158 | } | ||
| 116 | 159 | ||||
| 117 | - | func TestParseSnapshotRestoreProcesses(t *testing.T) { | |||
| 118 | - | t.Parallel() | |||
| 119 | - | ||||
| 120 | - | cfg, warnings, err := Parse(strings.NewReader("snapshot.restore_processes = claude, codex, pi, opencode, btop\n")) | |||
| 121 | - | require.NoError(t, err) | |||
| 122 | - | require.Empty(t, warnings) | |||
| 123 | - | require.Equal(t, []string{"claude", "codex", "pi", "opencode", "btop"}, cfg.Snapshot.RestoreProcesses) | |||
| 124 | - | require.True(t, cfg.Snapshot.RestoreProcessesSet) | |||
| 125 | - | } | |||
| 126 | - | ||||
| 127 | - | func TestParseSnapshotRestoreProcessesEmptyDisables(t *testing.T) { | |||
| 128 | - | t.Parallel() | |||
| 129 | - | ||||
| 130 | - | cfg, warnings, err := Parse(strings.NewReader("snapshot.restore_processes =\n")) | |||
| 131 | - | require.NoError(t, err) | |||
| 132 | - | require.Empty(t, warnings) | |||
| 133 | - | require.Empty(t, cfg.Snapshot.RestoreProcesses) | |||
| 134 | - | require.True(t, cfg.Snapshot.RestoreProcessesSet) | |||
| 135 | - | } | |||
| 136 | - | ||||
| 137 | - | func TestParseSnapshotRestoreProcessesWarnsOnMalformedEntries(t *testing.T) { | |||
| 138 | - | t.Parallel() | |||
| 139 | - | ||||
| 140 | - | cfg, warnings, err := Parse(strings.NewReader("snapshot.restore_processes = claude, custom-tool, /bin/sh, :all:, bad name, claude, , zsh\n")) | |||
| 141 | - | require.NoError(t, err) | |||
| 142 | - | require.Equal(t, []domain.Warning{ | |||
| 143 | - | {Line: 1, Msg: "invalid snapshot restore process \"/bin/sh\""}, | |||
| 144 | - | {Line: 1, Msg: "invalid snapshot restore process \":all:\""}, | |||
| 145 | - | {Line: 1, Msg: "invalid snapshot restore process \"bad name\""}, | |||
| 146 | - | }, warnings) | |||
| 147 | - | require.Equal(t, []string{"claude", "custom-tool", "zsh"}, cfg.Snapshot.RestoreProcesses) | |||
| 148 | - | require.True(t, cfg.Snapshot.RestoreProcessesSet) | |||
| 149 | - | } | |||
| 150 | - | ||||
| 151 | func TestDefaultsCopiesSnapshotRestoreProcesses(t *testing.T) { | 160 | func TestDefaultsCopiesSnapshotRestoreProcesses(t *testing.T) { | ||
| 152 | t.Parallel() | 161 | t.Parallel() | ||
| 153 | 162 | ||||
M internal/domain/config.go +8 -3
| @@ -24,12 +24,17 @@ type SnapshotConfig struct { | |||
| 24 | 24 | RestoreProcessesSet bool | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | - | // DefaultSnapshotRestoreProcesses is the default allowlist for process restore. | |
| 28 | - | var DefaultSnapshotRestoreProcesses = []string{ | |
| 27 | + | // defaultSnapshotRestoreProcesses is the default allowlist for process restore. | |
| 28 | + | var defaultSnapshotRestoreProcesses = []string{ | |
| 29 | 29 | "vi", "vim", "nvim", "emacs", "man", "less", "more", "tail", "top", "htop", "btop", | |
| 30 | 30 | "claude", "codex", "pi", "opencode", | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | + | // DefaultSnapshotRestoreProcesses returns a copy of the default process restore allowlist. | |
| 34 | + | func DefaultSnapshotRestoreProcesses() []string { | |
| 35 | + | return append([]string(nil), defaultSnapshotRestoreProcesses...) | |
| 36 | + | } | |
| 37 | + | ||
| 33 | 38 | // Config is the user-editable vev configuration after parsing. Unknown binding | |
| 34 | 39 | // keys are preserved here (in BindingEntries, in file order) so the usecase | |
| 35 | 40 | // layer can decide which actions it understands. | |
| @@ -54,7 +59,7 @@ func Defaults() Config { | |||
| 54 | 59 | BindingEntries: []ConfigEntry{}, | |
| 55 | 60 | Codes: map[string]string{}, | |
| 56 | 61 | Snapshot: SnapshotConfig{ | |
| 57 | - | RestoreProcesses: append([]string(nil), DefaultSnapshotRestoreProcesses...), | |
| 62 | + | RestoreProcesses: DefaultSnapshotRestoreProcesses(), | |
| 58 | 63 | }, | |
| 59 | 64 | } | |
| 60 | 65 | } | |
| @@ -24,12 +24,17 @@ type SnapshotConfig struct { | |||||
| 24 | RestoreProcessesSet bool | 24 | RestoreProcessesSet bool | ||
| 25 | } | 25 | } | ||
| 26 | 26 | ||||
| 27 | - | // DefaultSnapshotRestoreProcesses is the default allowlist for process restore. | 27 | + | // defaultSnapshotRestoreProcesses is the default allowlist for process restore. |
| 28 | - | var DefaultSnapshotRestoreProcesses = []string{ | 28 | + | var defaultSnapshotRestoreProcesses = []string{ |
| 29 | "vi", "vim", "nvim", "emacs", "man", "less", "more", "tail", "top", "htop", "btop", | 29 | "vi", "vim", "nvim", "emacs", "man", "less", "more", "tail", "top", "htop", "btop", | ||
| 30 | "claude", "codex", "pi", "opencode", | 30 | "claude", "codex", "pi", "opencode", | ||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| 33 | + | // DefaultSnapshotRestoreProcesses returns a copy of the default process restore allowlist. | |||
| 34 | + | func DefaultSnapshotRestoreProcesses() []string { | |||
| 35 | + | return append([]string(nil), defaultSnapshotRestoreProcesses...) | |||
| 36 | + | } | |||
| 37 | + | ||||
| 33 | // Config is the user-editable vev configuration after parsing. Unknown binding | 38 | // Config is the user-editable vev configuration after parsing. Unknown binding | ||
| 34 | // keys are preserved here (in BindingEntries, in file order) so the usecase | 39 | // keys are preserved here (in BindingEntries, in file order) so the usecase | ||
| 35 | // layer can decide which actions it understands. | 40 | // layer can decide which actions it understands. | ||
| @@ -54,7 +59,7 @@ func Defaults() Config { | |||||
| 54 | BindingEntries: []ConfigEntry{}, | 59 | BindingEntries: []ConfigEntry{}, | ||
| 55 | Codes: map[string]string{}, | 60 | Codes: map[string]string{}, | ||
| 56 | Snapshot: SnapshotConfig{ | 61 | Snapshot: SnapshotConfig{ | ||
| 57 | - | RestoreProcesses: append([]string(nil), DefaultSnapshotRestoreProcesses...), | 62 | + | RestoreProcesses: DefaultSnapshotRestoreProcesses(), |
| 58 | }, | 63 | }, | ||
| 59 | } | 64 | } | ||
| 60 | } | 65 | } | ||
M internal/platform/process_linux.go +3 -8
| @@ -35,14 +35,9 @@ func processArgv(path string, pid int) ([]string, error) { | |||
| 35 | 35 | return nil, fmt.Errorf("process argv: empty argv for pid %d", pid) | |
| 36 | 36 | } | |
| 37 | 37 | parts := bytes.Split(data, []byte{0}) | |
| 38 | - | argv := make([]string, 0, len(parts)) | |
| 39 | - | for _, part := range parts { | |
| 40 | - | if len(part) > 0 { | |
| 41 | - | argv = append(argv, string(part)) | |
| 42 | - | } | |
| 43 | - | } | |
| 44 | - | if len(argv) == 0 { | |
| 45 | - | return nil, fmt.Errorf("process argv: empty argv for pid %d", pid) | |
| 38 | + | argv := make([]string, len(parts)) | |
| 39 | + | for i, part := range parts { | |
| 40 | + | argv[i] = string(part) | |
| 46 | 41 | } | |
| 47 | 42 | return argv, nil | |
| 48 | 43 | } | |
| @@ -35,14 +35,9 @@ func processArgv(path string, pid int) ([]string, error) { | |||||
| 35 | return nil, fmt.Errorf("process argv: empty argv for pid %d", pid) | 35 | return nil, fmt.Errorf("process argv: empty argv for pid %d", pid) | ||
| 36 | } | 36 | } | ||
| 37 | parts := bytes.Split(data, []byte{0}) | 37 | parts := bytes.Split(data, []byte{0}) | ||
| 38 | - | argv := make([]string, 0, len(parts)) | 38 | + | argv := make([]string, len(parts)) |
| 39 | - | for _, part := range parts { | 39 | + | for i, part := range parts { |
| 40 | - | if len(part) > 0 { | 40 | + | argv[i] = string(part) |
| 41 | - | argv = append(argv, string(part)) | |||
| 42 | - | } | |||
| 43 | - | } | |||
| 44 | - | if len(argv) == 0 { | |||
| 45 | - | return nil, fmt.Errorf("process argv: empty argv for pid %d", pid) | |||
| 46 | } | 41 | } | ||
| 47 | return argv, nil | 42 | return argv, nil | ||
| 48 | } | 43 | } | ||
M internal/platform/process_linux_test.go +62 -21
| @@ -27,19 +27,40 @@ func TestProcessArgvRejectsEmptyCmdline(t *testing.T) { | |||
| 27 | 27 | } | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | - | func TestProcessGroupArgvSelection(t *testing.T) { | |
| 31 | - | recs := []processRecord{{pid: 10, pgrp: 7}, {pid: 12, pgrp: 7}, {pid: 20, pgrp: 9}} | |
| 32 | - | pid, ok := selectProcessGroupPID(recs, 7, 10) | |
| 33 | - | if !ok || pid != 12 { | |
| 34 | - | t.Fatalf("selectProcessGroupPID = %d, %v; want 12, true", pid, ok) | |
| 30 | + | func TestSelectProcessGroupPID(t *testing.T) { | |
| 31 | + | tests := []struct { | |
| 32 | + | name string | |
| 33 | + | recs []processRecord | |
| 34 | + | pgid int | |
| 35 | + | shell int | |
| 36 | + | wantPID int | |
| 37 | + | wantOK bool | |
| 38 | + | }{ | |
| 39 | + | { | |
| 40 | + | name: "selects other member", | |
| 41 | + | recs: []processRecord{{pid: 10, pgrp: 7}, {pid: 12, pgrp: 7}, {pid: 20, pgrp: 9}}, | |
| 42 | + | pgid: 7, | |
| 43 | + | shell: 10, | |
| 44 | + | wantPID: 12, | |
| 45 | + | wantOK: true, | |
| 46 | + | }, | |
| 47 | + | { | |
| 48 | + | name: "bare shell has no candidate", | |
| 49 | + | recs: []processRecord{{pid: 10, pgrp: 7}, {pid: 20, pgrp: 9}}, | |
| 50 | + | pgid: 7, | |
| 51 | + | shell: 10, | |
| 52 | + | wantPID: 0, | |
| 53 | + | wantOK: false, | |
| 54 | + | }, | |
| 35 | 55 | } | |
| 36 | - | } | |
| 37 | 56 | ||
| 38 | - | func TestProcessGroupArgvBareShellReturnsNoPID(t *testing.T) { | |
| 39 | - | recs := []processRecord{{pid: 10, pgrp: 7}, {pid: 20, pgrp: 9}} | |
| 40 | - | pid, ok := selectProcessGroupPID(recs, 7, 10) | |
| 41 | - | if ok || pid != 0 { | |
| 42 | - | t.Fatalf("selectProcessGroupPID bare shell = %d, %v; want 0, false", pid, ok) | |
| 57 | + | for _, tt := range tests { | |
| 58 | + | t.Run(tt.name, func(t *testing.T) { | |
| 59 | + | pid, ok := selectProcessGroupPID(tt.recs, tt.pgid, tt.shell) | |
| 60 | + | if ok != tt.wantOK || pid != tt.wantPID { | |
| 61 | + | t.Fatalf("selectProcessGroupPID = %d, %v; want %d, %v", pid, ok, tt.wantPID, tt.wantOK) | |
| 62 | + | } | |
| 63 | + | }) | |
| 43 | 64 | } | |
| 44 | 65 | } | |
| 45 | 66 | ||
| @@ -51,16 +72,36 @@ func TestParseStatPgrp(t *testing.T) { | |||
| 51 | 72 | } | |
| 52 | 73 | ||
| 53 | 74 | func TestProcessArgvFileParsing(t *testing.T) { | |
| 54 | - | path := filepath.Join(t.TempDir(), "cmdline") | |
| 55 | - | if err := os.WriteFile(path, []byte("cmd\x00two words\x00"), 0o600); err != nil { | |
| 56 | - | t.Fatal(err) | |
| 75 | + | tests := []struct { | |
| 76 | + | name string | |
| 77 | + | data []byte | |
| 78 | + | want []string | |
| 79 | + | }{ | |
| 80 | + | { | |
| 81 | + | name: "splits nul-delimited argv", | |
| 82 | + | data: []byte("cmd\x00two words\x00"), | |
| 83 | + | want: []string{"cmd", "two words"}, | |
| 84 | + | }, | |
| 85 | + | { | |
| 86 | + | name: "preserves empty argument", | |
| 87 | + | data: []byte("cmd\x00\x00arg\x00"), | |
| 88 | + | want: []string{"cmd", "", "arg"}, | |
| 89 | + | }, | |
| 57 | 90 | } | |
| 58 | - | got, err := processArgv(path, 123) | |
| 59 | - | if err != nil { | |
| 60 | - | t.Fatal(err) | |
| 61 | - | } | |
| 62 | - | want := []string{"cmd", "two words"} | |
| 63 | - | if !reflect.DeepEqual(got, want) { | |
| 64 | - | t.Fatalf("processArgv = %#v; want %#v", got, want) | |
| 91 | + | ||
| 92 | + | for _, tt := range tests { | |
| 93 | + | t.Run(tt.name, func(t *testing.T) { | |
| 94 | + | path := filepath.Join(t.TempDir(), "cmdline") | |
| 95 | + | if err := os.WriteFile(path, tt.data, 0o600); err != nil { | |
| 96 | + | t.Fatal(err) | |
| 97 | + | } | |
| 98 | + | got, err := processArgv(path, 123) | |
| 99 | + | if err != nil { | |
| 100 | + | t.Fatal(err) | |
| 101 | + | } | |
| 102 | + | if !reflect.DeepEqual(got, tt.want) { | |
| 103 | + | t.Fatalf("processArgv = %#v; want %#v", got, tt.want) | |
| 104 | + | } | |
| 105 | + | }) | |
| 65 | 106 | } | |
| 66 | 107 | } | |
| @@ -27,19 +27,40 @@ func TestProcessArgvRejectsEmptyCmdline(t *testing.T) { | |||||
| 27 | } | 27 | } | ||
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | - | func TestProcessGroupArgvSelection(t *testing.T) { | 30 | + | func TestSelectProcessGroupPID(t *testing.T) { |
| 31 | - | recs := []processRecord{{pid: 10, pgrp: 7}, {pid: 12, pgrp: 7}, {pid: 20, pgrp: 9}} | 31 | + | tests := []struct { |
| 32 | - | pid, ok := selectProcessGroupPID(recs, 7, 10) | 32 | + | name string |
| 33 | - | if !ok || pid != 12 { | 33 | + | recs []processRecord |
| 34 | - | t.Fatalf("selectProcessGroupPID = %d, %v; want 12, true", pid, ok) | 34 | + | pgid int |
| 35 | + | shell int | |||
| 36 | + | wantPID int | |||
| 37 | + | wantOK bool | |||
| 38 | + | }{ | |||
| 39 | + | { | |||
| 40 | + | name: "selects other member", | |||
| 41 | + | recs: []processRecord{{pid: 10, pgrp: 7}, {pid: 12, pgrp: 7}, {pid: 20, pgrp: 9}}, | |||
| 42 | + | pgid: 7, | |||
| 43 | + | shell: 10, | |||
| 44 | + | wantPID: 12, | |||
| 45 | + | wantOK: true, | |||
| 46 | + | }, | |||
| 47 | + | { | |||
| 48 | + | name: "bare shell has no candidate", | |||
| 49 | + | recs: []processRecord{{pid: 10, pgrp: 7}, {pid: 20, pgrp: 9}}, | |||
| 50 | + | pgid: 7, | |||
| 51 | + | shell: 10, | |||
| 52 | + | wantPID: 0, | |||
| 53 | + | wantOK: false, | |||
| 54 | + | }, | |||
| 35 | } | 55 | } | ||
| 36 | - | } | |||
| 37 | 56 | ||||
| 38 | - | func TestProcessGroupArgvBareShellReturnsNoPID(t *testing.T) { | 57 | + | for _, tt := range tests { |
| 39 | - | recs := []processRecord{{pid: 10, pgrp: 7}, {pid: 20, pgrp: 9}} | 58 | + | t.Run(tt.name, func(t *testing.T) { |
| 40 | - | pid, ok := selectProcessGroupPID(recs, 7, 10) | 59 | + | pid, ok := selectProcessGroupPID(tt.recs, tt.pgid, tt.shell) |
| 41 | - | if ok || pid != 0 { | 60 | + | if ok != tt.wantOK || pid != tt.wantPID { |
| 42 | - | t.Fatalf("selectProcessGroupPID bare shell = %d, %v; want 0, false", pid, ok) | 61 | + | t.Fatalf("selectProcessGroupPID = %d, %v; want %d, %v", pid, ok, tt.wantPID, tt.wantOK) |
| 62 | + | } | |||
| 63 | + | }) | |||
| 43 | } | 64 | } | ||
| 44 | } | 65 | } | ||
| 45 | 66 | ||||
| @@ -51,16 +72,36 @@ func TestParseStatPgrp(t *testing.T) { | |||||
| 51 | } | 72 | } | ||
| 52 | 73 | ||||
| 53 | func TestProcessArgvFileParsing(t *testing.T) { | 74 | func TestProcessArgvFileParsing(t *testing.T) { | ||
| 54 | - | path := filepath.Join(t.TempDir(), "cmdline") | 75 | + | tests := []struct { |
| 55 | - | if err := os.WriteFile(path, []byte("cmd\x00two words\x00"), 0o600); err != nil { | 76 | + | name string |
| 56 | - | t.Fatal(err) | 77 | + | data []byte |
| 78 | + | want []string | |||
| 79 | + | }{ | |||
| 80 | + | { | |||
| 81 | + | name: "splits nul-delimited argv", | |||
| 82 | + | data: []byte("cmd\x00two words\x00"), | |||
| 83 | + | want: []string{"cmd", "two words"}, | |||
| 84 | + | }, | |||
| 85 | + | { | |||
| 86 | + | name: "preserves empty argument", | |||
| 87 | + | data: []byte("cmd\x00\x00arg\x00"), | |||
| 88 | + | want: []string{"cmd", "", "arg"}, | |||
| 89 | + | }, | |||
| 57 | } | 90 | } | ||
| 58 | - | got, err := processArgv(path, 123) | 91 | + | |
| 59 | - | if err != nil { | 92 | + | for _, tt := range tests { |
| 60 | - | t.Fatal(err) | 93 | + | t.Run(tt.name, func(t *testing.T) { |
| 61 | - | } | 94 | + | path := filepath.Join(t.TempDir(), "cmdline") |
| 62 | - | want := []string{"cmd", "two words"} | 95 | + | if err := os.WriteFile(path, tt.data, 0o600); err != nil { |
| 63 | - | if !reflect.DeepEqual(got, want) { | 96 | + | t.Fatal(err) |
| 64 | - | t.Fatalf("processArgv = %#v; want %#v", got, want) | 97 | + | } |
| 98 | + | got, err := processArgv(path, 123) | |||
| 99 | + | if err != nil { | |||
| 100 | + | t.Fatal(err) | |||
| 101 | + | } | |||
| 102 | + | if !reflect.DeepEqual(got, tt.want) { | |||
| 103 | + | t.Fatalf("processArgv = %#v; want %#v", got, tt.want) | |||
| 104 | + | } | |||
| 105 | + | }) | |||
| 65 | } | 106 | } | ||
| 66 | } | 107 | } | ||
M internal/ports/wire.go +6 -5
| @@ -40,11 +40,12 @@ const ( | |||
| 40 | 40 | ||
| 41 | 41 | // ErrorMsg codes. | |
| 42 | 42 | const ( | |
| 43 | - | ErrVersionMismatch uint16 = 1 | |
| 44 | - | ErrNoSuchSession uint16 = 2 | |
| 45 | - | ErrNameTaken uint16 = 3 | |
| 46 | - | ErrServerShutdown uint16 = 4 | |
| 47 | - | ErrInternal uint16 = 255 | |
| 43 | + | ErrVersionMismatch uint16 = 1 | |
| 44 | + | ErrNoSuchSession uint16 = 2 | |
| 45 | + | ErrNameTaken uint16 = 3 | |
| 46 | + | ErrServerShutdown uint16 = 4 | |
| 47 | + | ErrInvalidSessionName uint16 = 5 | |
| 48 | + | ErrInternal uint16 = 255 | |
| 48 | 49 | ) | |
| 49 | 50 | ||
| 50 | 51 | // Detached reasons. | |
| @@ -40,11 +40,12 @@ const ( | |||||
| 40 | 40 | ||||
| 41 | // ErrorMsg codes. | 41 | // ErrorMsg codes. | ||
| 42 | const ( | 42 | const ( | ||
| 43 | - | ErrVersionMismatch uint16 = 1 | 43 | + | ErrVersionMismatch uint16 = 1 |
| 44 | - | ErrNoSuchSession uint16 = 2 | 44 | + | ErrNoSuchSession uint16 = 2 |
| 45 | - | ErrNameTaken uint16 = 3 | 45 | + | ErrNameTaken uint16 = 3 |
| 46 | - | ErrServerShutdown uint16 = 4 | 46 | + | ErrServerShutdown uint16 = 4 |
| 47 | - | ErrInternal uint16 = 255 | 47 | + | ErrInvalidSessionName uint16 = 5 |
| 48 | + | ErrInternal uint16 = 255 | |||
| 48 | ) | 49 | ) | ||
| 49 | 50 | ||||
| 50 | // Detached reasons. | 51 | // Detached reasons. | ||
M internal/ports/wire_test.go +8 -6
| @@ -542,13 +542,15 @@ func TestMsgTypeConstantsDistinct(t *testing.T) { | |||
| 542 | 542 | ||
| 543 | 543 | t.Run("error codes", func(t *testing.T) { | |
| 544 | 544 | vals := map[uint16]string{ | |
| 545 | - | ErrVersionMismatch: "ErrVersionMismatch", | |
| 546 | - | ErrNoSuchSession: "ErrNoSuchSession", | |
| 547 | - | ErrNameTaken: "ErrNameTaken", | |
| 548 | - | ErrInternal: "ErrInternal", | |
| 545 | + | ErrVersionMismatch: "ErrVersionMismatch", | |
| 546 | + | ErrNoSuchSession: "ErrNoSuchSession", | |
| 547 | + | ErrNameTaken: "ErrNameTaken", | |
| 548 | + | ErrServerShutdown: "ErrServerShutdown", | |
| 549 | + | ErrInvalidSessionName: "ErrInvalidSessionName", | |
| 550 | + | ErrInternal: "ErrInternal", | |
| 549 | 551 | } | |
| 550 | - | if len(vals) != 4 { | |
| 551 | - | t.Fatalf("expected 4 distinct error codes, got %d", len(vals)) | |
| 552 | + | if len(vals) != 6 { | |
| 553 | + | t.Fatalf("expected 6 distinct error codes, got %d", len(vals)) | |
| 552 | 554 | } | |
| 553 | 555 | }) | |
| 554 | 556 | ||
| @@ -542,13 +542,15 @@ func TestMsgTypeConstantsDistinct(t *testing.T) { | |||||
| 542 | 542 | ||||
| 543 | t.Run("error codes", func(t *testing.T) { | 543 | t.Run("error codes", func(t *testing.T) { | ||
| 544 | vals := map[uint16]string{ | 544 | vals := map[uint16]string{ | ||
| 545 | - | ErrVersionMismatch: "ErrVersionMismatch", | 545 | + | ErrVersionMismatch: "ErrVersionMismatch", |
| 546 | - | ErrNoSuchSession: "ErrNoSuchSession", | 546 | + | ErrNoSuchSession: "ErrNoSuchSession", |
| 547 | - | ErrNameTaken: "ErrNameTaken", | 547 | + | ErrNameTaken: "ErrNameTaken", |
| 548 | - | ErrInternal: "ErrInternal", | 548 | + | ErrServerShutdown: "ErrServerShutdown", |
| 549 | + | ErrInvalidSessionName: "ErrInvalidSessionName", | |||
| 550 | + | ErrInternal: "ErrInternal", | |||
| 549 | } | 551 | } | ||
| 550 | - | if len(vals) != 4 { | 552 | + | if len(vals) != 6 { |
| 551 | - | t.Fatalf("expected 4 distinct error codes, got %d", len(vals)) | 553 | + | t.Fatalf("expected 6 distinct error codes, got %d", len(vals)) |
| 552 | } | 554 | } | ||
| 553 | }) | 555 | }) | ||
| 554 | 556 | ||||
M internal/usecase/daemon/config.go +2 -2
| @@ -130,7 +130,7 @@ func restoreProcessAllowlistFromConfig(cfg domain.SnapshotConfig) map[string]str | |||
| 130 | 130 | if len(cfg.RestoreProcesses) > 0 { | |
| 131 | 131 | return buildRestoreProcessAllowlist(cfg.RestoreProcesses) | |
| 132 | 132 | } | |
| 133 | - | return buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses) | |
| 133 | + | return buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses()) | |
| 134 | 134 | } | |
| 135 | 135 | ||
| 136 | 136 | func buildRestoreProcessAllowlist(values []string) map[string]struct{} { | |
| @@ -148,7 +148,7 @@ func buildRestoreProcessAllowlist(values []string) map[string]struct{} { | |||
| 148 | 148 | func (d *Daemon) restoreProcessAllowlistSnapshot() map[string]struct{} { | |
| 149 | 149 | allow := d.restoreProcessAllowlist.Load() | |
| 150 | 150 | if allow == nil { | |
| 151 | - | return buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses) | |
| 151 | + | return buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses()) | |
| 152 | 152 | } | |
| 153 | 153 | return *allow | |
| 154 | 154 | } | |
| @@ -130,7 +130,7 @@ func restoreProcessAllowlistFromConfig(cfg domain.SnapshotConfig) map[string]str | |||||
| 130 | if len(cfg.RestoreProcesses) > 0 { | 130 | if len(cfg.RestoreProcesses) > 0 { | ||
| 131 | return buildRestoreProcessAllowlist(cfg.RestoreProcesses) | 131 | return buildRestoreProcessAllowlist(cfg.RestoreProcesses) | ||
| 132 | } | 132 | } | ||
| 133 | - | return buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses) | 133 | + | return buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses()) |
| 134 | } | 134 | } | ||
| 135 | 135 | ||||
| 136 | func buildRestoreProcessAllowlist(values []string) map[string]struct{} { | 136 | func buildRestoreProcessAllowlist(values []string) map[string]struct{} { | ||
| @@ -148,7 +148,7 @@ func buildRestoreProcessAllowlist(values []string) map[string]struct{} { | |||||
| 148 | func (d *Daemon) restoreProcessAllowlistSnapshot() map[string]struct{} { | 148 | func (d *Daemon) restoreProcessAllowlistSnapshot() map[string]struct{} { | ||
| 149 | allow := d.restoreProcessAllowlist.Load() | 149 | allow := d.restoreProcessAllowlist.Load() | ||
| 150 | if allow == nil { | 150 | if allow == nil { | ||
| 151 | - | return buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses) | 151 | + | return buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses()) |
| 152 | } | 152 | } | ||
| 153 | return *allow | 153 | return *allow | ||
| 154 | } | 154 | } | ||
M internal/usecase/daemon/config_test.go +43 -12
| @@ -50,19 +50,50 @@ func TestApplyConfigHotReloadSwapsBindingsAndCodes(t *testing.T) { | |||
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | 52 | func TestApplyConfigSnapshotRestoreProcesses(t *testing.T) { | |
| 53 | - | d := newTestDaemon(t, nil, stubClock{}) | |
| 54 | - | ||
| 55 | - | _, ok := d.restoreProcessAllowlistSnapshot()["claude"] | |
| 56 | - | require.True(t, ok) | |
| 57 | - | ||
| 58 | - | d.ApplyConfig(domain.Config{Snapshot: domain.SnapshotConfig{RestoreProcessesSet: true}}) | |
| 59 | - | require.Empty(t, d.restoreProcessAllowlistSnapshot()) | |
| 53 | + | tests := []struct { | |
| 54 | + | name string | |
| 55 | + | cfg domain.Config | |
| 56 | + | check func(t *testing.T, allow map[string]struct{}) | |
| 57 | + | }{ | |
| 58 | + | { | |
| 59 | + | name: "default allowlist", | |
| 60 | + | check: func(t *testing.T, allow map[string]struct{}) { | |
| 61 | + | t.Helper() | |
| 62 | + | require.Contains(t, allow, "claude") | |
| 63 | + | }, | |
| 64 | + | }, | |
| 65 | + | { | |
| 66 | + | name: "explicit empty disables restore", | |
| 67 | + | cfg: domain.Config{Snapshot: domain.SnapshotConfig{RestoreProcessesSet: true}}, | |
| 68 | + | check: func(t *testing.T, allow map[string]struct{}) { | |
| 69 | + | t.Helper() | |
| 70 | + | require.Empty(t, allow) | |
| 71 | + | }, | |
| 72 | + | }, | |
| 73 | + | { | |
| 74 | + | name: "explicit values are trimmed", | |
| 75 | + | cfg: domain.Config{Snapshot: domain.SnapshotConfig{ | |
| 76 | + | RestoreProcessesSet: true, | |
| 77 | + | RestoreProcesses: []string{"less", "", " pi "}, | |
| 78 | + | }}, | |
| 79 | + | check: func(t *testing.T, allow map[string]struct{}) { | |
| 80 | + | t.Helper() | |
| 81 | + | require.Contains(t, allow, "less") | |
| 82 | + | require.Contains(t, allow, "pi") | |
| 83 | + | require.NotContains(t, allow, "") | |
| 84 | + | }, | |
| 85 | + | }, | |
| 86 | + | } | |
| 60 | 87 | ||
| 61 | - | d.ApplyConfig(domain.Config{Snapshot: domain.SnapshotConfig{RestoreProcessesSet: true, RestoreProcesses: []string{"less", "", " pi "}}}) | |
| 62 | - | allow := d.restoreProcessAllowlistSnapshot() | |
| 63 | - | require.Contains(t, allow, "less") | |
| 64 | - | require.Contains(t, allow, "pi") | |
| 65 | - | require.NotContains(t, allow, "") | |
| 88 | + | for _, tt := range tests { | |
| 89 | + | t.Run(tt.name, func(t *testing.T) { | |
| 90 | + | d := newTestDaemon(t, nil, stubClock{}) | |
| 91 | + | if tt.cfg.Snapshot.RestoreProcessesSet || len(tt.cfg.Snapshot.RestoreProcesses) > 0 { | |
| 92 | + | d.ApplyConfig(tt.cfg) | |
| 93 | + | } | |
| 94 | + | tt.check(t, d.restoreProcessAllowlistSnapshot()) | |
| 95 | + | }) | |
| 96 | + | } | |
| 66 | 97 | } | |
| 67 | 98 | ||
| 68 | 99 | func TestPaletteCommandsApplyOverridesAndResolveByCode(t *testing.T) { | |
| @@ -50,19 +50,50 @@ func TestApplyConfigHotReloadSwapsBindingsAndCodes(t *testing.T) { | |||||
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | func TestApplyConfigSnapshotRestoreProcesses(t *testing.T) { | 52 | func TestApplyConfigSnapshotRestoreProcesses(t *testing.T) { | ||
| 53 | - | d := newTestDaemon(t, nil, stubClock{}) | 53 | + | tests := []struct { |
| 54 | - | 54 | + | name string | |
| 55 | - | _, ok := d.restoreProcessAllowlistSnapshot()["claude"] | 55 | + | cfg domain.Config |
| 56 | - | require.True(t, ok) | 56 | + | check func(t *testing.T, allow map[string]struct{}) |
| 57 | - | 57 | + | }{ | |
| 58 | - | d.ApplyConfig(domain.Config{Snapshot: domain.SnapshotConfig{RestoreProcessesSet: true}}) | 58 | + | { |
| 59 | - | require.Empty(t, d.restoreProcessAllowlistSnapshot()) | 59 | + | name: "default allowlist", |
| 60 | + | check: func(t *testing.T, allow map[string]struct{}) { | |||
| 61 | + | t.Helper() | |||
| 62 | + | require.Contains(t, allow, "claude") | |||
| 63 | + | }, | |||
| 64 | + | }, | |||
| 65 | + | { | |||
| 66 | + | name: "explicit empty disables restore", | |||
| 67 | + | cfg: domain.Config{Snapshot: domain.SnapshotConfig{RestoreProcessesSet: true}}, | |||
| 68 | + | check: func(t *testing.T, allow map[string]struct{}) { | |||
| 69 | + | t.Helper() | |||
| 70 | + | require.Empty(t, allow) | |||
| 71 | + | }, | |||
| 72 | + | }, | |||
| 73 | + | { | |||
| 74 | + | name: "explicit values are trimmed", | |||
| 75 | + | cfg: domain.Config{Snapshot: domain.SnapshotConfig{ | |||
| 76 | + | RestoreProcessesSet: true, | |||
| 77 | + | RestoreProcesses: []string{"less", "", " pi "}, | |||
| 78 | + | }}, | |||
| 79 | + | check: func(t *testing.T, allow map[string]struct{}) { | |||
| 80 | + | t.Helper() | |||
| 81 | + | require.Contains(t, allow, "less") | |||
| 82 | + | require.Contains(t, allow, "pi") | |||
| 83 | + | require.NotContains(t, allow, "") | |||
| 84 | + | }, | |||
| 85 | + | }, | |||
| 86 | + | } | |||
| 60 | 87 | ||||
| 61 | - | d.ApplyConfig(domain.Config{Snapshot: domain.SnapshotConfig{RestoreProcessesSet: true, RestoreProcesses: []string{"less", "", " pi "}}}) | 88 | + | for _, tt := range tests { |
| 62 | - | allow := d.restoreProcessAllowlistSnapshot() | 89 | + | t.Run(tt.name, func(t *testing.T) { |
| 63 | - | require.Contains(t, allow, "less") | 90 | + | d := newTestDaemon(t, nil, stubClock{}) |
| 64 | - | require.Contains(t, allow, "pi") | 91 | + | if tt.cfg.Snapshot.RestoreProcessesSet || len(tt.cfg.Snapshot.RestoreProcesses) > 0 { |
| 65 | - | require.NotContains(t, allow, "") | 92 | + | d.ApplyConfig(tt.cfg) |
| 93 | + | } | |||
| 94 | + | tt.check(t, d.restoreProcessAllowlistSnapshot()) | |||
| 95 | + | }) | |||
| 96 | + | } | |||
| 66 | } | 97 | } | ||
| 67 | 98 | ||||
| 68 | func TestPaletteCommandsApplyOverridesAndResolveByCode(t *testing.T) { | 99 | func TestPaletteCommandsApplyOverridesAndResolveByCode(t *testing.T) { | ||
M internal/usecase/daemon/daemon.go +3 -3
| @@ -273,7 +273,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt | |||
| 273 | 273 | d.codeOverrides.Store(&empty) | |
| 274 | 274 | } | |
| 275 | 275 | if d.restoreProcessAllowlist.Load() == nil { | |
| 276 | - | allow := buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses) | |
| 276 | + | allow := buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses()) | |
| 277 | 277 | d.restoreProcessAllowlist.Store(&allow) | |
| 278 | 278 | } | |
| 279 | 279 | if records, err := d.persist.LoadAll(); err != nil { | |
| @@ -632,11 +632,11 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||
| 632 | 632 | case ports.IntentNew: | |
| 633 | 633 | if h.Name == "" { | |
| 634 | 634 | d.mu.Unlock() | |
| 635 | - | return nil, nil, &protoErr{ports.ErrInternal, "empty session name"} | |
| 635 | + | return nil, nil, &protoErr{ports.ErrInvalidSessionName, "empty session name"} | |
| 636 | 636 | } | |
| 637 | 637 | if err := domain.ValidateSessionName(h.Name); err != nil { | |
| 638 | 638 | d.mu.Unlock() | |
| 639 | - | return nil, nil, &protoErr{ports.ErrInternal, err.Error()} | |
| 639 | + | return nil, nil, &protoErr{ports.ErrInvalidSessionName, err.Error()} | |
| 640 | 640 | } | |
| 641 | 641 | if d.nameLiveOrStoppedLocked(h.Name) { | |
| 642 | 642 | d.mu.Unlock() | |
| @@ -273,7 +273,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt | |||||
| 273 | d.codeOverrides.Store(&empty) | 273 | d.codeOverrides.Store(&empty) | ||
| 274 | } | 274 | } | ||
| 275 | if d.restoreProcessAllowlist.Load() == nil { | 275 | if d.restoreProcessAllowlist.Load() == nil { | ||
| 276 | - | allow := buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses) | 276 | + | allow := buildRestoreProcessAllowlist(domain.DefaultSnapshotRestoreProcesses()) |
| 277 | d.restoreProcessAllowlist.Store(&allow) | 277 | d.restoreProcessAllowlist.Store(&allow) | ||
| 278 | } | 278 | } | ||
| 279 | if records, err := d.persist.LoadAll(); err != nil { | 279 | if records, err := d.persist.LoadAll(); err != nil { | ||
| @@ -632,11 +632,11 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||||
| 632 | case ports.IntentNew: | 632 | case ports.IntentNew: | ||
| 633 | if h.Name == "" { | 633 | if h.Name == "" { | ||
| 634 | d.mu.Unlock() | 634 | d.mu.Unlock() | ||
| 635 | - | return nil, nil, &protoErr{ports.ErrInternal, "empty session name"} | 635 | + | return nil, nil, &protoErr{ports.ErrInvalidSessionName, "empty session name"} |
| 636 | } | 636 | } | ||
| 637 | if err := domain.ValidateSessionName(h.Name); err != nil { | 637 | if err := domain.ValidateSessionName(h.Name); err != nil { | ||
| 638 | d.mu.Unlock() | 638 | d.mu.Unlock() | ||
| 639 | - | return nil, nil, &protoErr{ports.ErrInternal, err.Error()} | 639 | + | return nil, nil, &protoErr{ports.ErrInvalidSessionName, err.Error()} |
| 640 | } | 640 | } | ||
| 641 | if d.nameLiveOrStoppedLocked(h.Name) { | 641 | if d.nameLiveOrStoppedLocked(h.Name) { | ||
| 642 | d.mu.Unlock() | 642 | d.mu.Unlock() | ||