graphban-cli 0.1.0__tar.gz → 0.1.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.
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/PKG-INFO +44 -13
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/README.md +43 -12
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/pyproject.toml +1 -1
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/src/gban/cli.py +8 -1
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/src/gban/doctor.py +46 -6
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/tests/test_doctor.py +108 -4
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/tests/test_packaging.py +28 -0
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/.gitignore +0 -0
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/LICENSE +0 -0
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/src/gban/__init__.py +0 -0
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/src/gban/client.py +0 -0
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/src/gban/config.py +0 -0
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/tests/test_acts.py +0 -0
- {graphban_cli-0.1.0 → graphban_cli-0.1.1}/tests/test_session.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: graphban-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: gban — a Graphban client for the human at a terminal
|
|
5
5
|
Project-URL: Homepage, https://github.com/asc-me/graphban
|
|
6
6
|
Project-URL: Repository, https://github.com/asc-me/graphban
|
|
@@ -26,32 +26,63 @@ Specified by [PRD-40](https://github.com/asc-me/graphban/blob/main/docs/prd-40-g
|
|
|
26
26
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install graphban-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
or, on macOS:
|
|
31
34
|
|
|
32
35
|
```bash
|
|
33
|
-
|
|
36
|
+
brew install asc-me/tap/gban
|
|
34
37
|
```
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
The formula carries no `resource` stanzas — `graphban-cli` has no runtime dependencies, so
|
|
40
|
+
there is nothing to vendor and nothing to regenerate when a transitive moves. The supervisor
|
|
41
|
+
is deliberately not in the tap; see below.
|
|
42
|
+
|
|
43
|
+
Add the supervisor too if you run waves — a separate package, and `gban fleet` hands off to
|
|
44
|
+
it. At a terminal, `gban fleet` will offer to run this for you when it finds no supervisor;
|
|
45
|
+
it asks first and never installs on a bare return, because a person who typed a read-only
|
|
46
|
+
command did not consent to software being installed:
|
|
37
47
|
|
|
38
48
|
```bash
|
|
39
|
-
uv tool install
|
|
49
|
+
uv tool install graphban-fleet
|
|
40
50
|
```
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
That also gives you `gbagent`, the first-party coding agent: it is an entry point of
|
|
53
|
+
`graphban-fleet`, not a package of its own, because the supervisor resolves it on PATH like
|
|
54
|
+
any other vendor binary.
|
|
55
|
+
|
|
56
|
+
`uv tool update-shell` once, if uv says the bin directory is not on your PATH.
|
|
57
|
+
`uv tool upgrade --all` to move both forward.
|
|
58
|
+
|
|
59
|
+
Two commands rather than one, and an extra (`graphban-cli[fleet]`) is deliberately not
|
|
60
|
+
offered: `uv tool install` exposes only the REQUESTED package's executables, so an extra
|
|
61
|
+
installs `gbfleet` into `gban`'s environment and puts it on no path at all. Measured — with
|
|
62
|
+
the extra, `gban fleet` reported "gbfleet is not installed here" while `gbfleet` sat in the
|
|
63
|
+
very environment it was running from.
|
|
64
|
+
|
|
65
|
+
`gban` pulls **nothing**: `client.py` is `urllib.request` throughout, and the install lands
|
|
66
|
+
exactly one distribution. `gbfleet` brings httpx and its transitives, which is why they are
|
|
67
|
+
separate packages and not one.
|
|
45
68
|
|
|
46
69
|
With pip instead, into an environment you already have:
|
|
47
70
|
|
|
48
71
|
```bash
|
|
49
|
-
pip install
|
|
72
|
+
pip install graphban-cli
|
|
50
73
|
```
|
|
51
74
|
|
|
52
|
-
`gban`
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
`gban` looks for `gbfleet` beside its own interpreter before falling back to PATH, so that
|
|
76
|
+
shape works with neither on PATH.
|
|
77
|
+
|
|
78
|
+
To run an unreleased change, install from the repository instead — the same spec the release
|
|
79
|
+
builds from:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv tool install "git+https://github.com/asc-me/graphban.git#subdirectory=cli"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Releasing is [docs/releasing.md](https://github.com/asc-me/graphban/blob/main/docs/releasing.md).
|
|
55
86
|
|
|
56
87
|
```bash
|
|
57
88
|
gban login --server https://cloud.agentldgr.dev
|
|
@@ -11,32 +11,63 @@ Specified by [PRD-40](https://github.com/asc-me/graphban/blob/main/docs/prd-40-g
|
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
```bash
|
|
15
|
+
uv tool install graphban-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
or, on macOS:
|
|
16
19
|
|
|
17
20
|
```bash
|
|
18
|
-
|
|
21
|
+
brew install asc-me/tap/gban
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
The formula carries no `resource` stanzas — `graphban-cli` has no runtime dependencies, so
|
|
25
|
+
there is nothing to vendor and nothing to regenerate when a transitive moves. The supervisor
|
|
26
|
+
is deliberately not in the tap; see below.
|
|
27
|
+
|
|
28
|
+
Add the supervisor too if you run waves — a separate package, and `gban fleet` hands off to
|
|
29
|
+
it. At a terminal, `gban fleet` will offer to run this for you when it finds no supervisor;
|
|
30
|
+
it asks first and never installs on a bare return, because a person who typed a read-only
|
|
31
|
+
command did not consent to software being installed:
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
|
-
uv tool install
|
|
34
|
+
uv tool install graphban-fleet
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
That also gives you `gbagent`, the first-party coding agent: it is an entry point of
|
|
38
|
+
`graphban-fleet`, not a package of its own, because the supervisor resolves it on PATH like
|
|
39
|
+
any other vendor binary.
|
|
40
|
+
|
|
41
|
+
`uv tool update-shell` once, if uv says the bin directory is not on your PATH.
|
|
42
|
+
`uv tool upgrade --all` to move both forward.
|
|
43
|
+
|
|
44
|
+
Two commands rather than one, and an extra (`graphban-cli[fleet]`) is deliberately not
|
|
45
|
+
offered: `uv tool install` exposes only the REQUESTED package's executables, so an extra
|
|
46
|
+
installs `gbfleet` into `gban`'s environment and puts it on no path at all. Measured — with
|
|
47
|
+
the extra, `gban fleet` reported "gbfleet is not installed here" while `gbfleet` sat in the
|
|
48
|
+
very environment it was running from.
|
|
49
|
+
|
|
50
|
+
`gban` pulls **nothing**: `client.py` is `urllib.request` throughout, and the install lands
|
|
51
|
+
exactly one distribution. `gbfleet` brings httpx and its transitives, which is why they are
|
|
52
|
+
separate packages and not one.
|
|
30
53
|
|
|
31
54
|
With pip instead, into an environment you already have:
|
|
32
55
|
|
|
33
56
|
```bash
|
|
34
|
-
pip install
|
|
57
|
+
pip install graphban-cli
|
|
35
58
|
```
|
|
36
59
|
|
|
37
|
-
`gban`
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
`gban` looks for `gbfleet` beside its own interpreter before falling back to PATH, so that
|
|
61
|
+
shape works with neither on PATH.
|
|
62
|
+
|
|
63
|
+
To run an unreleased change, install from the repository instead — the same spec the release
|
|
64
|
+
builds from:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv tool install "git+https://github.com/asc-me/graphban.git#subdirectory=cli"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Releasing is [docs/releasing.md](https://github.com/asc-me/graphban/blob/main/docs/releasing.md).
|
|
40
71
|
|
|
41
72
|
```bash
|
|
42
73
|
gban login --server https://cloud.agentldgr.dev
|
|
@@ -215,10 +215,17 @@ def cmd_fleet(args) -> int:
|
|
|
215
215
|
and leaves `gbfleet --help` authoritative about its own commands.
|
|
216
216
|
"""
|
|
217
217
|
binary = doctor_mod.find_supervisor()
|
|
218
|
+
if not binary:
|
|
219
|
+
# At a terminal, offer to fix it here rather than making the person read a command,
|
|
220
|
+
# copy it, run it and type this one again. Declined or unavailable, the message is
|
|
221
|
+
# what it always was.
|
|
222
|
+
if doctor_mod.offer_to_install():
|
|
223
|
+
binary = doctor_mod.find_supervisor()
|
|
218
224
|
if not binary:
|
|
219
225
|
print(f"{PROG}: gbfleet is not installed here. Install it with:\n"
|
|
220
226
|
f" {doctor_mod.INSTALL_SUPERVISOR}\n"
|
|
221
|
-
f" …or
|
|
227
|
+
f" …or `brew install asc-me/tap/gban` installs this client only — the "
|
|
228
|
+
f"supervisor is a separate package.", file=sys.stderr)
|
|
222
229
|
return EXIT_NO_SUPERVISOR
|
|
223
230
|
argv = [binary, *[a for a in args.rest if a != "--"]]
|
|
224
231
|
url = config.resolve(args.server, config.URL_ENV, "url")
|
|
@@ -118,12 +118,52 @@ def local(url: str, project: str, api_key: str) -> list[dict]:
|
|
|
118
118
|
f"`gbfleet doctor` {verdict}; its own report follows", report=body)]
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
#: The command that actually installs the supervisor.
|
|
122
|
-
#:
|
|
123
|
-
#:
|
|
124
|
-
#:
|
|
125
|
-
INSTALL_SUPERVISOR =
|
|
126
|
-
|
|
121
|
+
#: The command that actually installs the supervisor. It has been wrong twice: first naming a
|
|
122
|
+
#: PyPI package that did not exist, then a git spec that worked but stopped being the right
|
|
123
|
+
#: advice the moment `graphban-fleet` was published. A remedy a tool prints is a promise, and
|
|
124
|
+
#: `cli/tests/test_packaging.py` now checks this one against the same source the README uses.
|
|
125
|
+
INSTALL_SUPERVISOR = "uv tool install graphban-fleet"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def offer_to_install(stream=None) -> bool:
|
|
129
|
+
"""Ask, then install `graphban-fleet` with uv. Returns whether it is there afterwards.
|
|
130
|
+
|
|
131
|
+
**Asked, never assumed.** Installing software is not a side effect anybody should get
|
|
132
|
+
from `gban fleet ps`: it writes outside this program, it takes a minute, and a person
|
|
133
|
+
who typed a read-only command did not consent to it. So it happens only at an
|
|
134
|
+
interactive prompt, only on an explicit `y`, and only when the wall has actually been
|
|
135
|
+
hit — a missing supervisor, on the command that needs one.
|
|
136
|
+
|
|
137
|
+
Nothing is installed into `gban`'s OWN environment. `uv tool install` gives the
|
|
138
|
+
supervisor its own, which is the only correct answer when `gban` may itself live in a
|
|
139
|
+
Homebrew Cellar that brew will replace: a `pip install` next to it would be silently
|
|
140
|
+
undone by the next upgrade.
|
|
141
|
+
|
|
142
|
+
Declines, non-interactive shells and a machine without uv all return False and leave the
|
|
143
|
+
caller to print the command — a prompt nobody can answer is a hang, and a hang in a
|
|
144
|
+
script is worse than the error it replaced.
|
|
145
|
+
"""
|
|
146
|
+
import sys
|
|
147
|
+
|
|
148
|
+
out = stream or sys.stderr
|
|
149
|
+
if not sys.stdin.isatty():
|
|
150
|
+
return False
|
|
151
|
+
if not shutil.which("uv"):
|
|
152
|
+
return False
|
|
153
|
+
print(f"gban: gbfleet is not installed here.", file=out)
|
|
154
|
+
try:
|
|
155
|
+
answer = input(f" run `{INSTALL_SUPERVISOR}` now? [y/N] ").strip().lower()
|
|
156
|
+
except (EOFError, KeyboardInterrupt):
|
|
157
|
+
print("", file=out)
|
|
158
|
+
return False
|
|
159
|
+
if answer not in ("y", "yes"):
|
|
160
|
+
return False
|
|
161
|
+
done = subprocess.run(INSTALL_SUPERVISOR.split())
|
|
162
|
+
if done.returncode != 0:
|
|
163
|
+
print(f"gban: that install failed ({done.returncode}); nothing changed here.",
|
|
164
|
+
file=out)
|
|
165
|
+
return False
|
|
166
|
+
return bool(find_supervisor())
|
|
127
167
|
|
|
128
168
|
|
|
129
169
|
def find_supervisor() -> str:
|
|
@@ -171,7 +171,7 @@ def test_a_missing_supervisor_says_how_to_install_it(home, monkeypatch, capsys):
|
|
|
171
171
|
a real gbfleet on the machine running it."""
|
|
172
172
|
monkeypatch.setattr("gban.cli.doctor_mod.find_supervisor", lambda: "")
|
|
173
173
|
assert main(["fleet", "ps"]) == EXIT_NO_SUPERVISOR
|
|
174
|
-
assert
|
|
174
|
+
assert doctor.INSTALL_SUPERVISOR in capsys.readouterr().err
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
def test_the_credential_reaches_gbfleet_in_the_environment_never_argv(home, monkeypatch):
|
|
@@ -356,8 +356,8 @@ def test_the_install_it_names_is_one_that_exists(home, monkeypatch, capsys):
|
|
|
356
356
|
monkeypatch.setattr("gban.cli.doctor_mod.find_supervisor", lambda: "")
|
|
357
357
|
assert main(["fleet", "ps"]) == EXIT_NO_SUPERVISOR
|
|
358
358
|
err = capsys.readouterr().err
|
|
359
|
-
assert
|
|
360
|
-
assert "uv pip install graphban-fleet" not in err
|
|
359
|
+
assert doctor.INSTALL_SUPERVISOR in err
|
|
360
|
+
assert "uv pip install graphban-fleet" not in err, "that package name has never existed"
|
|
361
361
|
|
|
362
362
|
|
|
363
363
|
def test_the_doctor_names_the_same_install(home, monkeypatch):
|
|
@@ -365,7 +365,7 @@ def test_the_doctor_names_the_same_install(home, monkeypatch):
|
|
|
365
365
|
monkeypatch.setattr("gban.doctor.authenticated", lambda url: _Server())
|
|
366
366
|
lines, _ = doctor.run("http://gb.invalid", "core", "")
|
|
367
367
|
local = [l for l in lines if l["side"] == "local"][0]
|
|
368
|
-
assert
|
|
368
|
+
assert doctor.INSTALL_SUPERVISOR in local["detail"]
|
|
369
369
|
assert "uv pip install graphban-fleet" not in local["detail"]
|
|
370
370
|
|
|
371
371
|
|
|
@@ -404,3 +404,107 @@ def test_both_commands_find_the_supervisor_the_same_way(home):
|
|
|
404
404
|
for fn in (cli_mod.cmd_fleet, doctor.local):
|
|
405
405
|
assert "find_supervisor" in inspect.getsource(fn), (
|
|
406
406
|
f"{fn.__qualname__} looks for gbfleet some other way")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---- offering to install the supervisor, never doing it uninvited -----------------------------
|
|
410
|
+
|
|
411
|
+
class _Tty:
|
|
412
|
+
"""Stdin that claims to be a terminal, so the guard below is not what is under test."""
|
|
413
|
+
|
|
414
|
+
def isatty(self) -> bool:
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def test_it_never_installs_without_being_asked(home, monkeypatch, capsys):
|
|
419
|
+
"""THE INVARIANT. Installing software is not a side effect anybody should get from
|
|
420
|
+
`gban fleet ps` — it writes outside this program and a person who typed a read-only
|
|
421
|
+
command did not consent to it.
|
|
422
|
+
|
|
423
|
+
Sabotage: install on a bare return and this fails."""
|
|
424
|
+
ran = []
|
|
425
|
+
monkeypatch.setattr("sys.stdin", _Tty())
|
|
426
|
+
monkeypatch.setattr("gban.doctor.shutil.which", lambda name: "/usr/bin/uv")
|
|
427
|
+
monkeypatch.setattr("gban.doctor.subprocess.run", lambda *a, **kw: ran.append(a))
|
|
428
|
+
monkeypatch.setattr("builtins.input", lambda *_: "") # a bare return is not a yes
|
|
429
|
+
|
|
430
|
+
assert doctor.offer_to_install() is False
|
|
431
|
+
assert ran == [], "it installed without an explicit yes"
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_a_yes_runs_the_install_and_reports_what_it_found(home, monkeypatch):
|
|
435
|
+
ran = []
|
|
436
|
+
|
|
437
|
+
class Done:
|
|
438
|
+
returncode = 0
|
|
439
|
+
|
|
440
|
+
monkeypatch.setattr("sys.stdin", _Tty())
|
|
441
|
+
monkeypatch.setattr("gban.doctor.shutil.which",
|
|
442
|
+
lambda name: "/usr/bin/uv" if name == "uv" else None)
|
|
443
|
+
monkeypatch.setattr("gban.doctor.subprocess.run",
|
|
444
|
+
lambda argv, **kw: (ran.append(argv), Done())[1])
|
|
445
|
+
monkeypatch.setattr("builtins.input", lambda *_: "y")
|
|
446
|
+
monkeypatch.setattr("gban.doctor.find_supervisor", lambda: "/usr/bin/gbfleet")
|
|
447
|
+
|
|
448
|
+
assert doctor.offer_to_install() is True
|
|
449
|
+
assert ran == [["uv", "tool", "install", "graphban-fleet"]]
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def test_a_failed_install_is_reported_and_not_papered_over(home, monkeypatch, capsys):
|
|
453
|
+
"""Returning True after a failed install would send the caller to look for a binary that
|
|
454
|
+
is not there, and the second error would describe the wrong thing."""
|
|
455
|
+
class Died:
|
|
456
|
+
returncode = 2
|
|
457
|
+
|
|
458
|
+
monkeypatch.setattr("sys.stdin", _Tty())
|
|
459
|
+
monkeypatch.setattr("gban.doctor.shutil.which", lambda name: "/usr/bin/uv")
|
|
460
|
+
monkeypatch.setattr("gban.doctor.subprocess.run", lambda *a, **kw: Died())
|
|
461
|
+
monkeypatch.setattr("builtins.input", lambda *_: "y")
|
|
462
|
+
|
|
463
|
+
assert doctor.offer_to_install() is False
|
|
464
|
+
assert "install failed" in capsys.readouterr().err
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def test_it_does_not_prompt_where_nobody_can_answer(home, monkeypatch):
|
|
468
|
+
"""A prompt in a script is a hang, and a hang is worse than the error it replaced."""
|
|
469
|
+
import io
|
|
470
|
+
|
|
471
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(""))
|
|
472
|
+
monkeypatch.setattr("gban.doctor.shutil.which", lambda name: "/usr/bin/uv")
|
|
473
|
+
monkeypatch.setattr("builtins.input",
|
|
474
|
+
lambda *_: pytest.fail("it prompted with no terminal"))
|
|
475
|
+
assert doctor.offer_to_install() is False
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def test_no_uv_means_no_offer(home, monkeypatch):
|
|
479
|
+
"""The offer names one installer. Without it there is nothing to offer, and the caller's
|
|
480
|
+
message is the whole answer."""
|
|
481
|
+
monkeypatch.setattr("sys.stdin", _Tty())
|
|
482
|
+
monkeypatch.setattr("gban.doctor.shutil.which", lambda name: None)
|
|
483
|
+
monkeypatch.setattr("builtins.input",
|
|
484
|
+
lambda *_: pytest.fail("it offered an installer it does not have"))
|
|
485
|
+
assert doctor.offer_to_install() is False
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def test_gban_fleet_runs_the_command_after_a_successful_install(home, monkeypatch):
|
|
489
|
+
"""The point of asking at all: the person typed `gban fleet ps`, and after saying yes
|
|
490
|
+
they should get `ps` — not a second error telling them to type it again."""
|
|
491
|
+
seen = {}
|
|
492
|
+
|
|
493
|
+
class Done:
|
|
494
|
+
returncode = 0
|
|
495
|
+
|
|
496
|
+
calls = iter(["", "/usr/bin/gbfleet"]) # missing, then present after the install
|
|
497
|
+
monkeypatch.setattr("gban.cli.doctor_mod.find_supervisor", lambda: next(calls))
|
|
498
|
+
monkeypatch.setattr("gban.cli.doctor_mod.offer_to_install", lambda: True)
|
|
499
|
+
monkeypatch.setattr("gban.cli.subprocess.run",
|
|
500
|
+
lambda argv, **kw: (seen.update(argv=argv), Done())[1])
|
|
501
|
+
|
|
502
|
+
assert main(["fleet", "ps"]) == 0
|
|
503
|
+
assert seen["argv"][0] == "/usr/bin/gbfleet"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def test_a_declined_offer_still_prints_the_command(home, monkeypatch, capsys):
|
|
507
|
+
monkeypatch.setattr("gban.cli.doctor_mod.find_supervisor", lambda: "")
|
|
508
|
+
monkeypatch.setattr("gban.cli.doctor_mod.offer_to_install", lambda: False)
|
|
509
|
+
assert main(["fleet", "ps"]) == EXIT_NO_SUPERVISOR
|
|
510
|
+
assert doctor.INSTALL_SUPERVISOR in capsys.readouterr().err
|
|
@@ -183,3 +183,31 @@ def test_the_readme_ships_and_none_of_its_links_are_repo_relative():
|
|
|
183
183
|
if not t.startswith(("http://", "https://", "mailto:", "#"))]
|
|
184
184
|
assert not relative, (
|
|
185
185
|
f"cli/README.md ships to PyPI, where these resolve to nothing: {relative}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_the_install_the_tool_prints_is_the_one_the_readme_gives():
|
|
189
|
+
"""It has been wrong twice: first naming a PyPI package that did not exist, then a git
|
|
190
|
+
spec that worked and stopped being the right advice the moment `graphban-fleet` was
|
|
191
|
+
published. A remedy a tool prints is a promise, and the README is where the promise is
|
|
192
|
+
maintained — so they are checked against each other rather than both against memory.
|
|
193
|
+
|
|
194
|
+
Sabotage: change either and this fails."""
|
|
195
|
+
from gban.doctor import INSTALL_SUPERVISOR
|
|
196
|
+
|
|
197
|
+
readmes = ((CLI / "README.md").read_text(encoding="utf-8")
|
|
198
|
+
+ (REPO / "fleet" / "README.md").read_text(encoding="utf-8"))
|
|
199
|
+
assert INSTALL_SUPERVISOR in readmes, (
|
|
200
|
+
f"gban prints {INSTALL_SUPERVISOR!r}, which neither README tells anyone to run")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_the_printed_install_names_a_package_that_is_actually_published():
|
|
204
|
+
"""The distribution name in the command must be one this repository builds. Checked
|
|
205
|
+
against the pyprojects rather than against PyPI, because a test that reaches the network
|
|
206
|
+
fails for reasons that have nothing to do with the code."""
|
|
207
|
+
from gban.doctor import INSTALL_SUPERVISOR
|
|
208
|
+
|
|
209
|
+
built = {tomllib.loads((REPO / d / "pyproject.toml").read_text())["project"]["name"]
|
|
210
|
+
for d in ("cli", "fleet")}
|
|
211
|
+
named = [w for w in INSTALL_SUPERVISOR.split() if w.startswith("graphban")]
|
|
212
|
+
assert named, f"{INSTALL_SUPERVISOR!r} names no package at all"
|
|
213
|
+
assert set(named) <= built, f"{named} is not built here; built are {sorted(built)}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|