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.
- launchline-0.2.1/.generated-go-semantic-release-changelog.md +18 -0
- {launchline-0.2.0 → launchline-0.2.1}/.github/workflows/release.yml +6 -0
- {launchline-0.2.0 → launchline-0.2.1}/CHANGELOG.md +15 -3
- {launchline-0.2.0 → launchline-0.2.1}/PKG-INFO +14 -19
- {launchline-0.2.0 → launchline-0.2.1}/README.md +13 -18
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/config.py +3 -17
- {launchline-0.2.0 → launchline-0.2.1}/tests/test_config.py +31 -76
- {launchline-0.2.0 → launchline-0.2.1}/tests/test_cross_platform.py +101 -55
- {launchline-0.2.0 → launchline-0.2.1}/tests/test_runner.py +24 -10
- {launchline-0.2.0 → launchline-0.2.1}/tests/test_ui.py +227 -110
- launchline-0.2.0/.generated-go-semantic-release-changelog.md +0 -25
- {launchline-0.2.0 → launchline-0.2.1}/.github/workflows/ci.yml +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/.gitignore +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/.markdownlint.json +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/.python-version +0 -0
- {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
- {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
- {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
- {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
- {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
- {launchline-0.2.0 → launchline-0.2.1}/LICENSE +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/assets/launchline.ico +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/assets/launchline.png +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/assets/launchline.svg +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/docs/images/demo.gif +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/docs/images/hero.png +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/docs/images/navigate.png +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/docs/images/search.png +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/pyproject.toml +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/__init__.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/__main__.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/assets/launchline.ico +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/cli.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/exceptions.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/fuzzy.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/keys.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/runner.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/src/launchline/ui.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/tests/__init__.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/tests/conftest.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/tests/test_cli.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/tools/generate_icon.py +0 -0
- {launchline-0.2.0 → launchline-0.2.1}/tools/generate_screenshots.py +0 -0
- {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.
|
|
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
|
[](https://github.com/mikejhill/launchline/actions/workflows/ci.yml)
|
|
26
26
|
[](https://github.com/mikejhill/launchline/actions/workflows/release.yml)
|
|
27
|
+
[](https://pypi.org/project/launchline/)
|
|
28
|
+
[](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
|
-

|
|
33
35
|
|
|
34
36
|
## Why
|
|
35
37
|
|
|
@@ -50,7 +52,7 @@ instead.
|
|
|
50
52
|
|
|
51
53
|
| Fuzzy search | Arrow-key navigation |
|
|
52
54
|
| :-------------------------------: | :-----------------------------------: |
|
|
53
|
-
|  |  |
|
|
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
|
|
127
|
-
| ------------------- |
|
|
128
|
-
| `name` | string
|
|
129
|
-
| `command` | string
|
|
130
|
-
| `args` | list of strings
|
|
131
|
-
| `description` | string
|
|
132
|
-
| `working_directory` | string
|
|
133
|
-
| `env` | table
|
|
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).
|
|
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
|
[](https://github.com/mikejhill/launchline/actions/workflows/ci.yml)
|
|
4
4
|
[](https://github.com/mikejhill/launchline/actions/workflows/release.yml)
|
|
5
|
+
[](https://pypi.org/project/launchline/)
|
|
6
|
+
[](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
|
-

|
|
11
13
|
|
|
12
14
|
## Why
|
|
13
15
|
|
|
@@ -28,7 +30,7 @@ instead.
|
|
|
28
30
|
|
|
29
31
|
| Fuzzy search | Arrow-key navigation |
|
|
30
32
|
| :-------------------------------: | :-----------------------------------: |
|
|
31
|
-
|  |  |
|
|
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
|
|
105
|
-
| ------------------- |
|
|
106
|
-
| `name` | string
|
|
107
|
-
| `command` | string
|
|
108
|
-
| `args` | list of strings
|
|
109
|
-
| `description` | string
|
|
110
|
-
| `working_directory` | string
|
|
111
|
-
| `env` | table
|
|
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).
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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")
|