sernixa-cli 0.4.2__tar.gz → 0.4.3__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.
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/PKG-INFO +2 -2
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/pyproject.toml +2 -2
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/__init__.py +1 -1
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/config.py +37 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/main.py +99 -1
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli.egg-info/PKG-INFO +2 -2
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli.egg-info/requires.txt +1 -1
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/tests/test_cli.py +90 -1
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/README.md +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/__main__.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/adapters.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/discovery.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/formatting.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/hooks.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/lifecycle.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/local_agent_targets.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/mcp_server.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli/native_launcher.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli.egg-info/SOURCES.txt +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli.egg-info/dependency_links.txt +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli.egg-info/entry_points.txt +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/sernixa_cli.egg-info/top_level.txt +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/setup.cfg +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/tests/test_discovery.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/tests/test_local_agent_targets.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/tests/test_mcp_server.py +0 -0
- {sernixa_cli-0.4.2 → sernixa_cli-0.4.3}/tests/test_native_launcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sernixa-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Official Sernixa CLI for policy-governed local AI agent actions
|
|
5
5
|
Author: Sernixa Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
Requires-Dist: platformdirs>=4.0.0
|
|
23
23
|
Requires-Dist: prompt-toolkit>=3.0.48
|
|
24
24
|
Requires-Dist: rich>=13.8.0
|
|
25
|
-
Requires-Dist: sernixa>=0.4.
|
|
25
|
+
Requires-Dist: sernixa>=0.4.2
|
|
26
26
|
Requires-Dist: sernixa-agent>=1.6.1
|
|
27
27
|
Requires-Dist: typer>=0.12.0
|
|
28
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sernixa-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.3"
|
|
4
4
|
description = "Official Sernixa CLI for policy-governed local AI agent actions"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
"platformdirs>=4.0.0",
|
|
22
22
|
"prompt-toolkit>=3.0.48",
|
|
23
23
|
"rich>=13.8.0",
|
|
24
|
-
"sernixa>=0.4.
|
|
24
|
+
"sernixa>=0.4.2",
|
|
25
25
|
"sernixa-agent>=1.6.1",
|
|
26
26
|
"typer>=0.12.0",
|
|
27
27
|
]
|
|
@@ -6,6 +6,7 @@ import unicodedata
|
|
|
6
6
|
from dataclasses import asdict, dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Literal
|
|
9
|
+
from uuid import uuid4
|
|
9
10
|
|
|
10
11
|
from platformdirs import user_config_path
|
|
11
12
|
|
|
@@ -51,6 +52,42 @@ def plan_session_path() -> Path:
|
|
|
51
52
|
return user_config_path("sernixa", appauthor=False) / "plan-session.json"
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
def installation_id_path() -> Path:
|
|
56
|
+
override = os.getenv("SERNIXA_INSTALLATION_FILE", "").strip()
|
|
57
|
+
if override:
|
|
58
|
+
return Path(override).expanduser()
|
|
59
|
+
return user_config_path("sernixa", appauthor=False) / "installation.json"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_or_create_installation_id() -> str:
|
|
63
|
+
"""Return a stable local ID that the API immediately hashes with the org ID."""
|
|
64
|
+
path = installation_id_path()
|
|
65
|
+
if path.exists():
|
|
66
|
+
try:
|
|
67
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
68
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
69
|
+
raise ConfigError(f"Installation identity file is unreadable: {path}") from exc
|
|
70
|
+
installation_id = (
|
|
71
|
+
str(payload.get("installation_id") or "") if isinstance(payload, dict) else ""
|
|
72
|
+
)
|
|
73
|
+
if 16 <= len(installation_id) <= 128:
|
|
74
|
+
return installation_id
|
|
75
|
+
|
|
76
|
+
installation_id = uuid4().hex
|
|
77
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
78
|
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
79
|
+
temporary.write_text(
|
|
80
|
+
json.dumps({"installation_id": installation_id}, separators=(",", ":")) + "\n",
|
|
81
|
+
encoding="utf-8",
|
|
82
|
+
)
|
|
83
|
+
if os.name == "posix":
|
|
84
|
+
temporary.chmod(0o600)
|
|
85
|
+
temporary.replace(path)
|
|
86
|
+
if os.name == "posix":
|
|
87
|
+
path.chmod(0o600)
|
|
88
|
+
return installation_id
|
|
89
|
+
|
|
90
|
+
|
|
54
91
|
def save_plan_session(evaluation: dict[str, object]) -> Path:
|
|
55
92
|
token = evaluation.get("plan_token")
|
|
56
93
|
proofs = evaluation.get("step_proofs")
|
|
@@ -34,6 +34,7 @@ from .config import (
|
|
|
34
34
|
ConfigError,
|
|
35
35
|
config_path,
|
|
36
36
|
load_config,
|
|
37
|
+
load_or_create_installation_id,
|
|
37
38
|
normalize_api_key,
|
|
38
39
|
normalize_base_url,
|
|
39
40
|
remove_config,
|
|
@@ -181,6 +182,44 @@ def _print_banner() -> None:
|
|
|
181
182
|
typer.echo("Govern every agent action.")
|
|
182
183
|
|
|
183
184
|
|
|
185
|
+
def _platform_family() -> Literal["macos", "windows", "linux", "other"]:
|
|
186
|
+
value = platform.system().casefold()
|
|
187
|
+
if value == "darwin":
|
|
188
|
+
return "macos"
|
|
189
|
+
if value == "windows":
|
|
190
|
+
return "windows"
|
|
191
|
+
if value == "linux":
|
|
192
|
+
return "linux"
|
|
193
|
+
return "other"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _observed_integrations() -> list[str]:
|
|
197
|
+
integrations: list[str] = []
|
|
198
|
+
for target, status in all_target_statuses().items():
|
|
199
|
+
if not (status.get("wrapper_valid") or status.get("sernixa_installed")):
|
|
200
|
+
continue
|
|
201
|
+
normalized = "claude" if target == "claude-desktop" else target
|
|
202
|
+
if normalized in {"codex", "claude", "cursor", "langchain", "mcp"}:
|
|
203
|
+
integrations.append(normalized)
|
|
204
|
+
return list(dict.fromkeys(integrations))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _record_cli_installation(client: Client, *, integrations: list[str] | None = None) -> None:
|
|
208
|
+
"""Best-effort telemetry must never make sign-in or setup fail."""
|
|
209
|
+
recorder = getattr(client, "record_installation", None)
|
|
210
|
+
if not callable(recorder):
|
|
211
|
+
return
|
|
212
|
+
try:
|
|
213
|
+
recorder(
|
|
214
|
+
installation_id=load_or_create_installation_id(),
|
|
215
|
+
version=__version__,
|
|
216
|
+
platform=_platform_family(),
|
|
217
|
+
integrations=integrations if integrations is not None else _observed_integrations(),
|
|
218
|
+
)
|
|
219
|
+
except (ConfigError, OSError, SernixaError):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
|
|
184
223
|
def _interactive_terminal() -> bool:
|
|
185
224
|
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
186
225
|
|
|
@@ -363,6 +402,51 @@ def _render_console_help() -> None:
|
|
|
363
402
|
typer.echo(" @exit Close the session")
|
|
364
403
|
typer.echo("Slash-prefixed forms remain supported for compatibility.")
|
|
365
404
|
typer.echo("Any normal command also works, for example: validate request.json")
|
|
405
|
+
typer.echo("Run PowerShell and Python snippets in the host terminal, not at sernixa ›.")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _console_input_guidance(raw: str) -> str | None:
|
|
409
|
+
"""Explain common host-language pastes instead of reporting a false CLI error."""
|
|
410
|
+
stripped = raw.strip()
|
|
411
|
+
lowered = stripped.casefold()
|
|
412
|
+
if (
|
|
413
|
+
lowered.startswith(("$env:", "setx ", "set ", "export "))
|
|
414
|
+
or ".trim()" in lowered
|
|
415
|
+
):
|
|
416
|
+
return (
|
|
417
|
+
"That is a host-shell environment command. Type @exit, run it in "
|
|
418
|
+
"PowerShell or your system terminal, then start `sernixa` again."
|
|
419
|
+
)
|
|
420
|
+
python_prefixes = (
|
|
421
|
+
"from ",
|
|
422
|
+
"import ",
|
|
423
|
+
"for ",
|
|
424
|
+
"while ",
|
|
425
|
+
"if ",
|
|
426
|
+
"def ",
|
|
427
|
+
"class ",
|
|
428
|
+
"print(",
|
|
429
|
+
"python ",
|
|
430
|
+
"py ",
|
|
431
|
+
)
|
|
432
|
+
looks_like_python_assignment = (
|
|
433
|
+
"=" in stripped
|
|
434
|
+
and not lowered.startswith(("@", "/", "sernixa "))
|
|
435
|
+
and any(
|
|
436
|
+
token in lowered
|
|
437
|
+
for token in ("client", "decision", "surface", "schema_version")
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
if (
|
|
441
|
+
lowered.startswith(python_prefixes)
|
|
442
|
+
or looks_like_python_assignment
|
|
443
|
+
or stripped.startswith(("{", "}", "[", "]", '"', "'"))
|
|
444
|
+
):
|
|
445
|
+
return (
|
|
446
|
+
"That is Python code, not a Sernixa console command. Type @exit, "
|
|
447
|
+
"start `python` (or run a .py file), then execute the snippet there."
|
|
448
|
+
)
|
|
449
|
+
return None
|
|
366
450
|
|
|
367
451
|
|
|
368
452
|
def _resolve_console_command(raw: str) -> tuple[str, list[str]]:
|
|
@@ -463,7 +547,10 @@ def _run_interactive_console(
|
|
|
463
547
|
elif action == "already_open":
|
|
464
548
|
typer.echo("The Sernixa console is already open.")
|
|
465
549
|
elif action in {"invalid", "unknown"}:
|
|
466
|
-
typer.echo(
|
|
550
|
+
typer.echo(
|
|
551
|
+
_console_input_guidance(raw)
|
|
552
|
+
or "Unknown command. Type @help for available actions."
|
|
553
|
+
)
|
|
467
554
|
elif action == "execute":
|
|
468
555
|
try:
|
|
469
556
|
execute(argv)
|
|
@@ -840,6 +927,7 @@ def auth_login(
|
|
|
840
927
|
expires_at=expires_at,
|
|
841
928
|
)
|
|
842
929
|
)
|
|
930
|
+
_record_cli_installation(client)
|
|
843
931
|
except (ConfigError, SernixaError) as exc:
|
|
844
932
|
_tool_error(str(exc))
|
|
845
933
|
raise typer.Exit(2) from exc
|
|
@@ -1193,6 +1281,7 @@ def setup(
|
|
|
1193
1281
|
"auth": {"configured": False, "valid": False},
|
|
1194
1282
|
"host_tools": _host_tool_statuses(),
|
|
1195
1283
|
}
|
|
1284
|
+
client: Client | None = None
|
|
1196
1285
|
try:
|
|
1197
1286
|
client, _ = _client()
|
|
1198
1287
|
identity = client.whoami()
|
|
@@ -1256,6 +1345,15 @@ def setup(
|
|
|
1256
1345
|
failed = True
|
|
1257
1346
|
payload["launcher"] = {"status": "failed", "error": str(exc)}
|
|
1258
1347
|
payload["healthy"] = not failed and bool(payload["auth"]["valid"])
|
|
1348
|
+
if not dry_run and payload["auth"]["valid"] and client is not None:
|
|
1349
|
+
_record_cli_installation(
|
|
1350
|
+
client,
|
|
1351
|
+
integrations=[
|
|
1352
|
+
"claude" if target == "claude-desktop" else target
|
|
1353
|
+
for target, result in payload["targets"].items()
|
|
1354
|
+
if result.get("status") not in {"manual_required", "failed"}
|
|
1355
|
+
],
|
|
1356
|
+
)
|
|
1259
1357
|
if json_output:
|
|
1260
1358
|
typer.echo(json.dumps(payload, sort_keys=True))
|
|
1261
1359
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sernixa-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Official Sernixa CLI for policy-governed local AI agent actions
|
|
5
5
|
Author: Sernixa Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
Requires-Dist: platformdirs>=4.0.0
|
|
23
23
|
Requires-Dist: prompt-toolkit>=3.0.48
|
|
24
24
|
Requires-Dist: rich>=13.8.0
|
|
25
|
-
Requires-Dist: sernixa>=0.4.
|
|
25
|
+
Requires-Dist: sernixa>=0.4.2
|
|
26
26
|
Requires-Dist: sernixa-agent>=1.6.1
|
|
27
27
|
Requires-Dist: typer>=0.12.0
|
|
28
28
|
|
|
@@ -18,7 +18,9 @@ from sernixa_cli.config import (
|
|
|
18
18
|
CliConfig,
|
|
19
19
|
ConfigError,
|
|
20
20
|
config_path,
|
|
21
|
+
installation_id_path,
|
|
21
22
|
load_config,
|
|
23
|
+
load_or_create_installation_id,
|
|
22
24
|
normalize_api_key,
|
|
23
25
|
save_config,
|
|
24
26
|
)
|
|
@@ -29,6 +31,16 @@ from sernixa_cli.local_agent_targets import hook_snippet
|
|
|
29
31
|
runner = CliRunner()
|
|
30
32
|
|
|
31
33
|
|
|
34
|
+
@pytest.fixture(autouse=True)
|
|
35
|
+
def _isolate_installation_identity(
|
|
36
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
37
|
+
) -> None:
|
|
38
|
+
monkeypatch.setenv(
|
|
39
|
+
"SERNIXA_INSTALLATION_FILE",
|
|
40
|
+
str(tmp_path / "sernixa-installation.json"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
32
44
|
def _configure(monkeypatch, tmp_path: Path) -> Path:
|
|
33
45
|
path = tmp_path / "sernixa-config.json"
|
|
34
46
|
monkeypatch.setenv("SERNIXA_CONFIG_FILE", str(path))
|
|
@@ -51,6 +63,23 @@ def test_config_round_trip_uses_private_permissions(
|
|
|
51
63
|
assert path.stat().st_mode & 0o777 == 0o600
|
|
52
64
|
|
|
53
65
|
|
|
66
|
+
def test_installation_identity_is_stable_and_private(
|
|
67
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
68
|
+
tmp_path: Path,
|
|
69
|
+
) -> None:
|
|
70
|
+
path = tmp_path / "installation.json"
|
|
71
|
+
monkeypatch.setenv("SERNIXA_INSTALLATION_FILE", str(path))
|
|
72
|
+
|
|
73
|
+
first = load_or_create_installation_id()
|
|
74
|
+
second = load_or_create_installation_id()
|
|
75
|
+
|
|
76
|
+
assert first == second
|
|
77
|
+
assert len(first) == 32
|
|
78
|
+
assert installation_id_path() == path
|
|
79
|
+
if os.name == "posix":
|
|
80
|
+
assert path.stat().st_mode & 0o777 == 0o600
|
|
81
|
+
|
|
82
|
+
|
|
54
83
|
def test_auth_login_validates_before_saving_and_config_show_hides_secret(
|
|
55
84
|
monkeypatch,
|
|
56
85
|
tmp_path: Path,
|
|
@@ -65,7 +94,10 @@ def test_auth_login_validates_before_saving_and_config_show_hides_secret(
|
|
|
65
94
|
}
|
|
66
95
|
client.current_plan.return_value = {"plan": {"id": "free", "name": "Free"}}
|
|
67
96
|
|
|
68
|
-
with
|
|
97
|
+
with (
|
|
98
|
+
patch("sernixa_cli.main.Client", return_value=client),
|
|
99
|
+
patch("sernixa_cli.main._observed_integrations", return_value=["codex"]),
|
|
100
|
+
):
|
|
69
101
|
login = runner.invoke(
|
|
70
102
|
app,
|
|
71
103
|
["auth", "login", "--base-url", "https://api.test", "--api-key-stdin"],
|
|
@@ -78,6 +110,11 @@ def test_auth_login_validates_before_saving_and_config_show_hides_secret(
|
|
|
78
110
|
assert json.loads(path.read_text(encoding="utf-8"))["api_key"] == (
|
|
79
111
|
"srnx_live_login_secret"
|
|
80
112
|
)
|
|
113
|
+
client.record_installation.assert_called_once()
|
|
114
|
+
observation = client.record_installation.call_args.kwargs
|
|
115
|
+
assert observation["version"] == __version__
|
|
116
|
+
assert observation["integrations"] == ["codex"]
|
|
117
|
+
assert len(observation["installation_id"]) == 32
|
|
81
118
|
|
|
82
119
|
shown = runner.invoke(app, ["config", "show"])
|
|
83
120
|
assert shown.exit_code == 0
|
|
@@ -168,6 +205,33 @@ def test_auth_login_accepts_secure_stdin_for_powershell_clipboard(
|
|
|
168
205
|
client.whoami.assert_called_once_with()
|
|
169
206
|
|
|
170
207
|
|
|
208
|
+
def test_auth_login_succeeds_when_telemetry_is_unavailable(
|
|
209
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
210
|
+
tmp_path: Path,
|
|
211
|
+
) -> None:
|
|
212
|
+
path = tmp_path / "login-config.json"
|
|
213
|
+
monkeypatch.setenv("SERNIXA_CONFIG_FILE", str(path))
|
|
214
|
+
client = MagicMock()
|
|
215
|
+
client.whoami.return_value = {"organization_id": "org_windows"}
|
|
216
|
+
client.current_plan.return_value = {"plan": {"name": "Free"}}
|
|
217
|
+
client.record_installation.side_effect = SernixaApiError("telemetry unavailable")
|
|
218
|
+
|
|
219
|
+
with patch("sernixa_cli.main.Client", return_value=client):
|
|
220
|
+
login = runner.invoke(
|
|
221
|
+
app,
|
|
222
|
+
["auth", "login", "--api-key-stdin"],
|
|
223
|
+
input="srnx_live_windows_secret\n",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert login.exit_code == 0
|
|
227
|
+
assert "Signed in · org_windows · Free" in login.stdout
|
|
228
|
+
assert "telemetry" not in login.stdout.casefold()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_installation_telemetry_is_compatible_with_an_older_sdk() -> None:
|
|
232
|
+
cli_main._record_cli_installation(SimpleNamespace())
|
|
233
|
+
|
|
234
|
+
|
|
171
235
|
def test_auth_login_stdin_mode_refuses_to_wait_on_an_interactive_terminal(
|
|
172
236
|
monkeypatch,
|
|
173
237
|
tmp_path: Path,
|
|
@@ -659,6 +723,31 @@ def test_interactive_console_runs_multiple_real_commands_and_exits_cleanly(
|
|
|
659
723
|
assert "Session closed." in output
|
|
660
724
|
|
|
661
725
|
|
|
726
|
+
def test_interactive_console_explains_windows_and_python_pastes(
|
|
727
|
+
monkeypatch, tmp_path: Path, capsys
|
|
728
|
+
) -> None:
|
|
729
|
+
commands = iter(
|
|
730
|
+
[
|
|
731
|
+
'$env:SERNIXA_BASE_URL = "https://api.sernixa.com"',
|
|
732
|
+
"from sernixa import Client",
|
|
733
|
+
"client = Client()",
|
|
734
|
+
"@exit",
|
|
735
|
+
]
|
|
736
|
+
)
|
|
737
|
+
monkeypatch.setenv("SERNIXA_HISTORY_FILE", str(tmp_path / "history"))
|
|
738
|
+
|
|
739
|
+
cli_main._run_interactive_console(
|
|
740
|
+
read_command=lambda _: next(commands),
|
|
741
|
+
execute=lambda argv: 0,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
output = capsys.readouterr().out
|
|
745
|
+
assert "run it in PowerShell or your system terminal" in output
|
|
746
|
+
assert "That is Python code" in output
|
|
747
|
+
assert "Unknown command" not in output
|
|
748
|
+
assert "Session closed." in output
|
|
749
|
+
|
|
750
|
+
|
|
662
751
|
def test_console_history_is_private_and_console_commands_are_discoverable(
|
|
663
752
|
monkeypatch, tmp_path: Path
|
|
664
753
|
) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|