launchline 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. launchline-0.2.1/.generated-go-semantic-release-changelog.md +18 -0
  2. {launchline-0.2.0 → launchline-0.2.1}/.github/workflows/release.yml +6 -0
  3. {launchline-0.2.0 → launchline-0.2.1}/CHANGELOG.md +15 -3
  4. {launchline-0.2.0 → launchline-0.2.1}/PKG-INFO +14 -19
  5. {launchline-0.2.0 → launchline-0.2.1}/README.md +13 -18
  6. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/config.py +3 -17
  7. {launchline-0.2.0 → launchline-0.2.1}/tests/test_config.py +31 -76
  8. {launchline-0.2.0 → launchline-0.2.1}/tests/test_cross_platform.py +101 -55
  9. {launchline-0.2.0 → launchline-0.2.1}/tests/test_runner.py +24 -10
  10. {launchline-0.2.0 → launchline-0.2.1}/tests/test_ui.py +227 -110
  11. launchline-0.2.0/.generated-go-semantic-release-changelog.md +0 -25
  12. {launchline-0.2.0 → launchline-0.2.1}/.github/workflows/ci.yml +0 -0
  13. {launchline-0.2.0 → launchline-0.2.1}/.gitignore +0 -0
  14. {launchline-0.2.0 → launchline-0.2.1}/.markdownlint.json +0 -0
  15. {launchline-0.2.0 → launchline-0.2.1}/.python-version +0 -0
  16. {launchline-0.2.0 → launchline-0.2.1}/.semrel/linux_amd64/changelog-generator-default/1.15.0/changelog-generator-default_v1.15.0_linux_amd64 +0 -0
  17. {launchline-0.2.0 → launchline-0.2.1}/.semrel/linux_amd64/commit-analyzer-default/1.15.0/commit-analyzer-cz_v1.15.0_linux_amd64 +0 -0
  18. {launchline-0.2.0 → launchline-0.2.1}/.semrel/linux_amd64/condition-github/1.11.0/condition-github_v1.11.0_linux_amd64 +0 -0
  19. {launchline-0.2.0 → launchline-0.2.1}/.semrel/linux_amd64/files-updater-npm/1.11.0/files-updater-npm_v1.11.0_linux_amd64 +0 -0
  20. {launchline-0.2.0 → launchline-0.2.1}/.semrel/linux_amd64/provider-github/1.19.0/provider-github_v1.19.0_linux_amd64 +0 -0
  21. {launchline-0.2.0 → launchline-0.2.1}/LICENSE +0 -0
  22. {launchline-0.2.0 → launchline-0.2.1}/assets/launchline.ico +0 -0
  23. {launchline-0.2.0 → launchline-0.2.1}/assets/launchline.png +0 -0
  24. {launchline-0.2.0 → launchline-0.2.1}/assets/launchline.svg +0 -0
  25. {launchline-0.2.0 → launchline-0.2.1}/docs/images/demo.gif +0 -0
  26. {launchline-0.2.0 → launchline-0.2.1}/docs/images/hero.png +0 -0
  27. {launchline-0.2.0 → launchline-0.2.1}/docs/images/navigate.png +0 -0
  28. {launchline-0.2.0 → launchline-0.2.1}/docs/images/search.png +0 -0
  29. {launchline-0.2.0 → launchline-0.2.1}/pyproject.toml +0 -0
  30. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/__init__.py +0 -0
  31. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/__main__.py +0 -0
  32. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/assets/launchline.ico +0 -0
  33. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/cli.py +0 -0
  34. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/exceptions.py +0 -0
  35. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/fuzzy.py +0 -0
  36. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/keys.py +0 -0
  37. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/runner.py +0 -0
  38. {launchline-0.2.0 → launchline-0.2.1}/src/launchline/ui.py +0 -0
  39. {launchline-0.2.0 → launchline-0.2.1}/tests/__init__.py +0 -0
  40. {launchline-0.2.0 → launchline-0.2.1}/tests/conftest.py +0 -0
  41. {launchline-0.2.0 → launchline-0.2.1}/tests/test_cli.py +0 -0
  42. {launchline-0.2.0 → launchline-0.2.1}/tools/generate_icon.py +0 -0
  43. {launchline-0.2.0 → launchline-0.2.1}/tools/generate_screenshots.py +0 -0
  44. {launchline-0.2.0 → launchline-0.2.1}/uv.lock +0 -0
@@ -0,0 +1,18 @@
1
+ ## 0.2.1 (2026-03-30)
2
+
3
+ #### Bug Fixes
4
+
5
+ * add force-patch input to release workflow (1954a256)
6
+
7
+ #### Reverts
8
+
9
+ * remove array syntax for command config key (79d5f175)
10
+
11
+ #### Documentation
12
+
13
+ * add PyPI badge and update changelog for v0.2.0 (4aa7e495)
14
+
15
+ #### Tests
16
+
17
+ * improve test naming and add diagnostic assertion messages (3169b7cd)
18
+
@@ -2,6 +2,11 @@ name: Release
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
+ inputs:
6
+ force-patch:
7
+ description: "Force a patch version bump (use when only docs/test/chore commits exist)"
8
+ type: boolean
9
+ default: false
5
10
 
6
11
  permissions:
7
12
  contents: write
@@ -20,6 +25,7 @@ jobs:
20
25
  with:
21
26
  github-token: ${{ secrets.GITHUB_TOKEN }}
22
27
  allow-initial-development-versions: true
28
+ force-bump-patch-version: ${{ inputs.force-patch }}
23
29
  dry: true
24
30
 
25
31
  - name: Require new version
@@ -7,11 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Removed
11
+
12
+ - Reverted array syntax for `command` config key; `command` is string-only,
13
+ use `args` for arguments
14
+
15
+ ### Changed
16
+
17
+ - Improved test naming to follow `[unit]_[scenario]_[expectedResult]` pattern
18
+ - Added diagnostic failure messages to all test assertions
19
+ - Release workflow now supports `force-patch` input for non-bumping commits
20
+
21
+ ## [0.2.0] - 2026-03-30
22
+
10
23
  ### Added
11
24
 
12
- - `command` config key now accepts a list of strings (e.g.,
13
- `command = ["ssh", "-t", "my server"]`) for easier argument handling,
14
- especially with values containing spaces
15
25
  - `--config-path` CLI flag to print the active configuration file path
16
26
 
17
27
  ### Fixed
@@ -19,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
29
  - PyPI package page now renders README images correctly (paths are rewritten
20
30
  to absolute URLs during the release build)
21
31
  - Release artifacts no longer accidentally include non-distribution files
32
+ - Release workflow now produces correct version numbers (fixed dirty-tree
33
+ version detection via `SETUPTOOLS_SCM_PRETEND_VERSION`)
22
34
 
23
35
  ## [0.1.0] - 2026-03-30
24
36
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchline
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A lightweight terminal launcher for interactive CLI tools.
5
5
  Project-URL: Homepage, https://github.com/mikejhill/launchline
6
6
  Project-URL: Repository, https://github.com/mikejhill/launchline
@@ -24,12 +24,14 @@ Description-Content-Type: text/markdown
24
24
 
25
25
  [![CI](https://github.com/mikejhill/launchline/actions/workflows/ci.yml/badge.svg)](https://github.com/mikejhill/launchline/actions/workflows/ci.yml)
26
26
  [![Release](https://github.com/mikejhill/launchline/actions/workflows/release.yml/badge.svg)](https://github.com/mikejhill/launchline/actions/workflows/release.yml)
27
+ [![PyPI](https://img.shields.io/pypi/v/launchline)](https://pypi.org/project/launchline/)
28
+ [![Python](https://img.shields.io/pypi/pyversions/launchline)](https://pypi.org/project/launchline/)
27
29
 
28
30
  A lightweight terminal launcher for interactive CLI tools. Define your frequently
29
31
  used commands in a TOML config file, then pick them from a fuzzy-searchable menu
30
32
  instead of remembering flags and paths.
31
33
 
32
- ![LaunchLine demo](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/demo.gif)
34
+ ![LaunchLine demo](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.1/docs/images/demo.gif)
33
35
 
34
36
  ## Why
35
37
 
@@ -50,7 +52,7 @@ instead.
50
52
 
51
53
  | Fuzzy search | Arrow-key navigation |
52
54
  | :-------------------------------: | :-----------------------------------: |
53
- | ![Search](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/search.png) | ![Navigate](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/navigate.png) |
55
+ | ![Search](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.1/docs/images/search.png) | ![Navigate](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.1/docs/images/navigate.png) |
54
56
 
55
57
  ## Installation
56
58
 
@@ -123,24 +125,21 @@ Config file format is [TOML](https://toml.io). The file has an optional
123
125
 
124
126
  Each `[[entries]]` table defines one launchable tool:
125
127
 
126
- | Key | Type | Required | Default | Description |
127
- | ------------------- | ------------------------- | -------- | ------- | -------------------------------------------------------------- |
128
- | `name` | string | yes | — | Display name shown in the menu |
129
- | `command` | string or list of strings | yes | — | Executable to run (with optional inline arguments) |
130
- | `args` | list of strings | no | `[]` | Additional arguments appended after any inline command args |
131
- | `description` | string | no | `""` | Short description shown next to the name |
132
- | `working_directory` | string | no | — | Working directory for the subprocess |
133
- | `env` | table | no | `{}` | Extra environment variables (`KEY = "value"`) |
128
+ | Key | Type | Required | Default | Description |
129
+ | ------------------- | --------------- | -------- | ------- | -------------------------------------------------------------- |
130
+ | `name` | string | yes | — | Display name shown in the menu |
131
+ | `command` | string | yes | — | Executable to run |
132
+ | `args` | list of strings | no | `[]` | Arguments passed to the command |
133
+ | `description` | string | no | `""` | Short description shown next to the name |
134
+ | `working_directory` | string | no | — | Working directory for the subprocess |
135
+ | `env` | table | no | `{}` | Extra environment variables (`KEY = "value"`) |
134
136
 
135
137
  ### Validation Rules
136
138
 
137
139
  - At least one `[[entries]]` table is required.
138
140
  - Every entry must have both `name` and `command`.
139
- - `command` may be a string (`"pwsh"`) or a list (`["ssh", "-t", "host"]`).
140
- When a list, the first element is the executable and the rest are arguments.
141
141
  - `on_exit` must be `"restart"` or `"exit"`.
142
- - `args` must be a list (not a bare string). These are appended after any
143
- arguments provided inline in a `command` list.
142
+ - `args` must be a list (not a bare string).
144
143
  - `env` must be a TOML table (not a string or list).
145
144
  - If `working_directory` does not exist at load time, it is silently reset to
146
145
  `None` (a warning is logged).
@@ -171,10 +170,6 @@ name = "PowerShell"
171
170
  command = "pwsh"
172
171
  args = ["-NoProfile"]
173
172
  env = { TERM = "xterm-256color" }
174
-
175
- [[entries]]
176
- name = "Remote Server"
177
- command = ["ssh", "-t", "my server", "bash"]
178
173
  ```
179
174
 
180
175
  ## Usage
@@ -2,12 +2,14 @@
2
2
 
3
3
  [![CI](https://github.com/mikejhill/launchline/actions/workflows/ci.yml/badge.svg)](https://github.com/mikejhill/launchline/actions/workflows/ci.yml)
4
4
  [![Release](https://github.com/mikejhill/launchline/actions/workflows/release.yml/badge.svg)](https://github.com/mikejhill/launchline/actions/workflows/release.yml)
5
+ [![PyPI](https://img.shields.io/pypi/v/launchline)](https://pypi.org/project/launchline/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/launchline)](https://pypi.org/project/launchline/)
5
7
 
6
8
  A lightweight terminal launcher for interactive CLI tools. Define your frequently
7
9
  used commands in a TOML config file, then pick them from a fuzzy-searchable menu
8
10
  instead of remembering flags and paths.
9
11
 
10
- ![LaunchLine demo](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/demo.gif)
12
+ ![LaunchLine demo](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.1/docs/images/demo.gif)
11
13
 
12
14
  ## Why
13
15
 
@@ -28,7 +30,7 @@ instead.
28
30
 
29
31
  | Fuzzy search | Arrow-key navigation |
30
32
  | :-------------------------------: | :-----------------------------------: |
31
- | ![Search](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/search.png) | ![Navigate](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/navigate.png) |
33
+ | ![Search](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.1/docs/images/search.png) | ![Navigate](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.1/docs/images/navigate.png) |
32
34
 
33
35
  ## Installation
34
36
 
@@ -101,24 +103,21 @@ Config file format is [TOML](https://toml.io). The file has an optional
101
103
 
102
104
  Each `[[entries]]` table defines one launchable tool:
103
105
 
104
- | Key | Type | Required | Default | Description |
105
- | ------------------- | ------------------------- | -------- | ------- | -------------------------------------------------------------- |
106
- | `name` | string | yes | — | Display name shown in the menu |
107
- | `command` | string or list of strings | yes | — | Executable to run (with optional inline arguments) |
108
- | `args` | list of strings | no | `[]` | Additional arguments appended after any inline command args |
109
- | `description` | string | no | `""` | Short description shown next to the name |
110
- | `working_directory` | string | no | — | Working directory for the subprocess |
111
- | `env` | table | no | `{}` | Extra environment variables (`KEY = "value"`) |
106
+ | Key | Type | Required | Default | Description |
107
+ | ------------------- | --------------- | -------- | ------- | -------------------------------------------------------------- |
108
+ | `name` | string | yes | — | Display name shown in the menu |
109
+ | `command` | string | yes | — | Executable to run |
110
+ | `args` | list of strings | no | `[]` | Arguments passed to the command |
111
+ | `description` | string | no | `""` | Short description shown next to the name |
112
+ | `working_directory` | string | no | — | Working directory for the subprocess |
113
+ | `env` | table | no | `{}` | Extra environment variables (`KEY = "value"`) |
112
114
 
113
115
  ### Validation Rules
114
116
 
115
117
  - At least one `[[entries]]` table is required.
116
118
  - Every entry must have both `name` and `command`.
117
- - `command` may be a string (`"pwsh"`) or a list (`["ssh", "-t", "host"]`).
118
- When a list, the first element is the executable and the rest are arguments.
119
119
  - `on_exit` must be `"restart"` or `"exit"`.
120
- - `args` must be a list (not a bare string). These are appended after any
121
- arguments provided inline in a `command` list.
120
+ - `args` must be a list (not a bare string).
122
121
  - `env` must be a TOML table (not a string or list).
123
122
  - If `working_directory` does not exist at load time, it is silently reset to
124
123
  `None` (a warning is logged).
@@ -149,10 +148,6 @@ name = "PowerShell"
149
148
  command = "pwsh"
150
149
  args = ["-NoProfile"]
151
150
  env = { TERM = "xterm-256color" }
152
-
153
- [[entries]]
154
- name = "Remote Server"
155
- command = ["ssh", "-t", "my server", "bash"]
156
151
  ```
157
152
 
158
153
  ## Usage
@@ -188,19 +188,7 @@ description = "PowerShell 7"
188
188
  entries: list[EntryConfig] = []
189
189
  for i, entry_raw in enumerate(raw_entries, 1):
190
190
  name = entry_raw.get("name", "")
191
- raw_command = entry_raw.get("command", "")
192
-
193
- if isinstance(raw_command, list):
194
- if not raw_command:
195
- raise ConfigurationError(
196
- f"Entry {i}: 'command' list must not be empty."
197
- )
198
- command = str(raw_command[0])
199
- command_args = tuple(str(a) for a in raw_command[1:])
200
- else:
201
- command = str(raw_command)
202
- command_args = ()
203
-
191
+ command = entry_raw.get("command", "")
204
192
  if not name or not command:
205
193
  raise ConfigurationError(
206
194
  f"Entry {i}: 'name' and 'command' are required."
@@ -212,8 +200,6 @@ description = "PowerShell 7"
212
200
  f"Entry {i}: 'args' must be a list of strings."
213
201
  )
214
202
 
215
- combined_args = command_args + tuple(str(a) for a in raw_args)
216
-
217
203
  wd_raw = entry_raw.get("working_directory")
218
204
  wd: Path | None = None
219
205
  if wd_raw:
@@ -235,8 +221,8 @@ description = "PowerShell 7"
235
221
  entries.append(
236
222
  EntryConfig(
237
223
  name=str(name),
238
- command=command,
239
- args=combined_args,
224
+ command=str(command),
225
+ args=tuple(str(a) for a in raw_args),
240
226
  description=str(entry_raw.get("description", "")),
241
227
  working_directory=wd,
242
228
  env={str(k): str(v) for k, v in raw_env.items()},
@@ -21,7 +21,7 @@ _README = Path(__file__).resolve().parent.parent / "README.md"
21
21
  class TestLoadConfig:
22
22
  """Tests for ConfigLoader.load."""
23
23
 
24
- def test_loads_valid_config(self, sample_toml: Path) -> None:
24
+ def test_valid_toml_loads_all_entry_fields(self, sample_toml: Path) -> None:
25
25
  config = ConfigLoader.load(sample_toml)
26
26
  assert isinstance(config, LaunchLineConfig), (
27
27
  f"Expected LaunchLineConfig, got {type(config).__name__}"
@@ -30,7 +30,7 @@ class TestLoadConfig:
30
30
  f"Expected 2 entries, got {len(config.entries)}"
31
31
  )
32
32
 
33
- def test_default_settings(self, tmp_path: Path) -> None:
33
+ def test_missing_settings_use_defaults(self, tmp_path: Path) -> None:
34
34
  cfg = tmp_path / "config.toml"
35
35
  cfg.write_text('[[entries]]\nname = "X"\ncommand = "x"\n', encoding="utf-8")
36
36
  config = ConfigLoader.load(cfg)
@@ -44,7 +44,7 @@ class TestLoadConfig:
44
44
  assert config.ghost_text is True, "Default ghost_text should be True"
45
45
  assert config.numeric_trigger is True, "Default numeric_trigger should be True"
46
46
 
47
- def test_custom_settings(self, tmp_path: Path) -> None:
47
+ def test_custom_settings_override_defaults(self, tmp_path: Path) -> None:
48
48
  cfg = tmp_path / "config.toml"
49
49
  cfg.write_text(
50
50
  '[settings]\non_exit = "exit"\ntitle = "My Launcher"\n'
@@ -72,7 +72,9 @@ class TestLoadConfig:
72
72
  encoding="utf-8",
73
73
  )
74
74
  config = ConfigLoader.load(cfg)
75
- assert config.entries[0].args == ("--flag", "val")
75
+ assert config.entries[0].args == ("--flag", "val"), (
76
+ f"Expected args ('--flag', 'val'), got {config.entries[0].args!r}"
77
+ )
76
78
 
77
79
  def test_entry_env_parsed(self, tmp_path: Path) -> None:
78
80
  cfg = tmp_path / "config.toml"
@@ -81,7 +83,9 @@ class TestLoadConfig:
81
83
  encoding="utf-8",
82
84
  )
83
85
  config = ConfigLoader.load(cfg)
84
- assert config.entries[0].env == {"FOO": "bar"}
86
+ assert config.entries[0].env == {"FOO": "bar"}, (
87
+ f"Expected env {{'FOO': 'bar'}}, got {config.entries[0].env!r}"
88
+ )
85
89
 
86
90
  def test_rejects_empty_entries(self, tmp_path: Path) -> None:
87
91
  cfg = tmp_path / "config.toml"
@@ -89,19 +93,19 @@ class TestLoadConfig:
89
93
  with pytest.raises(ConfigurationError, match="No entries configured"):
90
94
  ConfigLoader.load(cfg)
91
95
 
92
- def test_rejects_missing_name(self, tmp_path: Path) -> None:
96
+ def test_missing_name_raises_configuration_error(self, tmp_path: Path) -> None:
93
97
  cfg = tmp_path / "config.toml"
94
98
  cfg.write_text('[[entries]]\ncommand = "x"\n', encoding="utf-8")
95
99
  with pytest.raises(ConfigurationError, match="'name' and 'command'"):
96
100
  ConfigLoader.load(cfg)
97
101
 
98
- def test_rejects_missing_command(self, tmp_path: Path) -> None:
102
+ def test_missing_command_raises_configuration_error(self, tmp_path: Path) -> None:
99
103
  cfg = tmp_path / "config.toml"
100
104
  cfg.write_text('[[entries]]\nname = "X"\n', encoding="utf-8")
101
105
  with pytest.raises(ConfigurationError, match="'name' and 'command'"):
102
106
  ConfigLoader.load(cfg)
103
107
 
104
- def test_rejects_invalid_on_exit(self, tmp_path: Path) -> None:
108
+ def test_invalid_on_exit_raises_configuration_error(self, tmp_path: Path) -> None:
105
109
  cfg = tmp_path / "config.toml"
106
110
  cfg.write_text(
107
111
  '[settings]\non_exit = "crash"\n\n[[entries]]\nname = "X"\ncommand = "x"\n',
@@ -110,7 +114,7 @@ class TestLoadConfig:
110
114
  with pytest.raises(ConfigurationError, match="Invalid on_exit"):
111
115
  ConfigLoader.load(cfg)
112
116
 
113
- def test_rejects_malformed_toml(self, tmp_path: Path) -> None:
117
+ def test_malformed_toml_raises_configuration_error(self, tmp_path: Path) -> None:
114
118
  cfg = tmp_path / "config.toml"
115
119
  cfg.write_text("this is not valid toml [[[", encoding="utf-8")
116
120
  with pytest.raises(ConfigurationError, match="Invalid TOML"):
@@ -143,7 +147,7 @@ class TestLoadConfig:
143
147
  "Non-existent working_directory should resolve to None"
144
148
  )
145
149
 
146
- def test_rejects_invalid_args_type(self, tmp_path: Path) -> None:
150
+ def test_invalid_args_type_raises_configuration_error(self, tmp_path: Path) -> None:
147
151
  cfg = tmp_path / "config.toml"
148
152
  cfg.write_text(
149
153
  '[[entries]]\nname = "X"\ncommand = "x"\nargs = "not-a-list"\n',
@@ -152,64 +156,7 @@ class TestLoadConfig:
152
156
  with pytest.raises(ConfigurationError, match="'args' must be a list"):
153
157
  ConfigLoader.load(cfg)
154
158
 
155
- def test_command_as_array(self, tmp_path: Path) -> None:
156
- """Command as list splits into executable + args."""
157
- cfg = tmp_path / "config.toml"
158
- cfg.write_text(
159
- '[[entries]]\nname = "X"\ncommand = ["ssh", "-t", "my server"]\n',
160
- encoding="utf-8",
161
- )
162
- config = ConfigLoader.load(cfg)
163
- assert config.entries[0].command == "ssh", (
164
- f"Expected command='ssh', got {config.entries[0].command!r}"
165
- )
166
- assert config.entries[0].args == ("-t", "my server"), (
167
- f"Expected args=('-t', 'my server'), got {config.entries[0].args!r}"
168
- )
169
-
170
- def test_command_array_with_extra_args(self, tmp_path: Path) -> None:
171
- """Command array args are prepended before explicit args."""
172
- cfg = tmp_path / "config.toml"
173
- cfg.write_text(
174
- '[[entries]]\nname = "X"\n'
175
- 'command = ["ssh", "-t"]\n'
176
- 'args = ["my server", "bash"]\n',
177
- encoding="utf-8",
178
- )
179
- config = ConfigLoader.load(cfg)
180
- assert config.entries[0].command == "ssh", (
181
- f"Expected command='ssh', got {config.entries[0].command!r}"
182
- )
183
- assert config.entries[0].args == ("-t", "my server", "bash"), (
184
- f"Expected combined args, got {config.entries[0].args!r}"
185
- )
186
-
187
- def test_command_array_single_element(self, tmp_path: Path) -> None:
188
- """Single-element command array is valid."""
189
- cfg = tmp_path / "config.toml"
190
- cfg.write_text(
191
- '[[entries]]\nname = "X"\ncommand = ["pwsh"]\n',
192
- encoding="utf-8",
193
- )
194
- config = ConfigLoader.load(cfg)
195
- assert config.entries[0].command == "pwsh", (
196
- f"Expected command='pwsh', got {config.entries[0].command!r}"
197
- )
198
- assert config.entries[0].args == (), (
199
- f"Expected empty args, got {config.entries[0].args!r}"
200
- )
201
-
202
- def test_command_empty_array_rejected(self, tmp_path: Path) -> None:
203
- """Empty command array is an error."""
204
- cfg = tmp_path / "config.toml"
205
- cfg.write_text(
206
- '[[entries]]\nname = "X"\ncommand = []\n',
207
- encoding="utf-8",
208
- )
209
- with pytest.raises(ConfigurationError, match="must not be empty"):
210
- ConfigLoader.load(cfg)
211
-
212
- def test_rejects_invalid_env_type(self, tmp_path: Path) -> None:
159
+ def test_invalid_env_type_raises_configuration_error(self, tmp_path: Path) -> None:
213
160
  cfg = tmp_path / "config.toml"
214
161
  cfg.write_text(
215
162
  '[[entries]]\nname = "X"\ncommand = "x"\nenv = "not-a-table"\n',
@@ -222,21 +169,23 @@ class TestLoadConfig:
222
169
  class TestResolveConfigPath:
223
170
  """Tests for ConfigLoader.resolve_path."""
224
171
 
225
- def test_cli_path_takes_priority(self, sample_toml: Path) -> None:
172
+ def test_cli_path_takes_priority_over_env_var(self, sample_toml: Path) -> None:
226
173
  result = ConfigLoader.resolve_path(sample_toml)
227
- assert result == sample_toml
174
+ assert result == sample_toml, f"Expected CLI path {sample_toml}, got {result}"
228
175
 
229
176
  def test_cli_path_missing_raises(self, tmp_path: Path) -> None:
230
177
  missing = tmp_path / "nope.toml"
231
178
  with pytest.raises(ConfigurationError, match="not found"):
232
179
  ConfigLoader.resolve_path(missing)
233
180
 
234
- def test_env_var_path(
181
+ def test_env_var_resolves_config_path(
235
182
  self, sample_toml: Path, monkeypatch: pytest.MonkeyPatch
236
183
  ) -> None:
237
184
  monkeypatch.setenv(ConfigLoader.ENV_VAR_NAME, str(sample_toml))
238
185
  result = ConfigLoader.resolve_path(None)
239
- assert result == sample_toml
186
+ assert result == sample_toml, (
187
+ f"Expected env var path {sample_toml}, got {result}"
188
+ )
240
189
 
241
190
  def test_env_var_missing_file_raises(
242
191
  self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@@ -245,7 +194,7 @@ class TestResolveConfigPath:
245
194
  with pytest.raises(ConfigurationError, match=ConfigLoader.ENV_VAR_NAME):
246
195
  ConfigLoader.resolve_path(None)
247
196
 
248
- def test_no_config_creates_default(
197
+ def test_missing_config_creates_default_file(
249
198
  self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
250
199
  ) -> None:
251
200
  monkeypatch.delenv(ConfigLoader.ENV_VAR_NAME, raising=False)
@@ -255,7 +204,9 @@ class TestResolveConfigPath:
255
204
  default_path,
256
205
  )
257
206
  result = ConfigLoader.resolve_path(None)
258
- assert result == default_path
207
+ assert result == default_path, (
208
+ f"Expected default path {default_path}, got {result}"
209
+ )
259
210
  assert default_path.exists(), "Default config file should have been created"
260
211
  assert default_path.read_text(encoding="utf-8") == (
261
212
  ConfigLoader.STARTER_CONFIG
@@ -305,12 +256,16 @@ class TestConfigReferenceSync:
305
256
  "entries"
306
257
  }
307
258
  readme_fields = self._readme_table_keys("`[settings]`")
308
- assert readme_fields == code_fields
259
+ assert readme_fields == code_fields, (
260
+ f"Expected settings fields {code_fields}, got {readme_fields}"
261
+ )
309
262
 
310
263
  def test_entry_fields_match_readme(self) -> None:
311
264
  code_fields = {f.name for f in dataclasses.fields(EntryConfig)}
312
265
  readme_fields = self._readme_table_keys("`[[entries]]`")
313
- assert readme_fields == code_fields
266
+ assert readme_fields == code_fields, (
267
+ f"Expected entry fields {code_fields}, got {readme_fields}"
268
+ )
314
269
 
315
270
  def test_valid_on_exit_values_in_readme(self) -> None:
316
271
  text = _README.read_text(encoding="utf-8")