langstage-cli 0.5.4__tar.gz → 0.5.6__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.
- {langstage_cli-0.5.4/langstage_cli.egg-info → langstage_cli-0.5.6}/PKG-INFO +9 -6
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/README.md +6 -3
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli/cli.py +73 -46
- {langstage_cli-0.5.4 → langstage_cli-0.5.6/langstage_cli.egg-info}/PKG-INFO +9 -6
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/SOURCES.txt +3 -1
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/requires.txt +2 -2
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/pyproject.toml +5 -3
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_show_config.py +2 -2
- langstage_cli-0.5.6/tests/test_stream_mode.py +36 -0
- langstage_cli-0.5.6/tests/test_unicode_console.py +31 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/LICENSE +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/deepagent_code/__init__.py +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli/__init__.py +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli/config.py +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/dependency_links.txt +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/entry_points.txt +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/top_level.txt +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/setup.cfg +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_cli.py +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_cli_help.py +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_codeconfig.py +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_config.py +0 -0
- {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_rename_shim.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langstage-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph
|
|
5
5
|
Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,7 @@ Requires-Python: >=3.11
|
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: langgraph>=0.2.0
|
|
22
|
-
Requires-Dist: langgraph-stream-parser<0.
|
|
22
|
+
Requires-Dist: langgraph-stream-parser<0.7,>=0.6.6
|
|
23
23
|
Requires-Dist: click>=8.0.0
|
|
24
24
|
Requires-Dist: python-dotenv
|
|
25
25
|
Provides-Extra: dev
|
|
@@ -30,7 +30,7 @@ Requires-Dist: ruff==0.15.15; extra == "dev"
|
|
|
30
30
|
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
31
31
|
Requires-Dist: deepagents>=0.3; extra == "dev"
|
|
32
32
|
Provides-Extra: agui
|
|
33
|
-
Requires-Dist: langgraph-stream-parser[agui]<0.
|
|
33
|
+
Requires-Dist: langgraph-stream-parser[agui]<0.7,>=0.6.6; extra == "agui"
|
|
34
34
|
Dynamic: license-file
|
|
35
35
|
|
|
36
36
|
# langstage-cli
|
|
@@ -146,7 +146,7 @@ langstage-cli
|
|
|
146
146
|
# Working directory
|
|
147
147
|
export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
|
|
148
148
|
|
|
149
|
-
# Stream mode (updates or
|
|
149
|
+
# Stream mode (updates or messages)
|
|
150
150
|
export LANGSTAGE_STREAM_MODE="updates"
|
|
151
151
|
```
|
|
152
152
|
|
|
@@ -169,7 +169,9 @@ Example `langstage.toml`:
|
|
|
169
169
|
```toml
|
|
170
170
|
[agent]
|
|
171
171
|
spec = "my_agent.py:graph"
|
|
172
|
-
|
|
172
|
+
|
|
173
|
+
[workspace]
|
|
174
|
+
root = "."
|
|
173
175
|
|
|
174
176
|
[ui]
|
|
175
177
|
verbose = true
|
|
@@ -195,7 +197,8 @@ Options:
|
|
|
195
197
|
-f, --file PATH Read message from a file (any extension)
|
|
196
198
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
197
199
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
198
|
-
--stream-mode
|
|
200
|
+
--stream-mode [updates|messages]
|
|
201
|
+
Stream mode (default: updates)
|
|
199
202
|
-v, --verbose Verbose output
|
|
200
203
|
```
|
|
201
204
|
|
|
@@ -111,7 +111,7 @@ langstage-cli
|
|
|
111
111
|
# Working directory
|
|
112
112
|
export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
|
|
113
113
|
|
|
114
|
-
# Stream mode (updates or
|
|
114
|
+
# Stream mode (updates or messages)
|
|
115
115
|
export LANGSTAGE_STREAM_MODE="updates"
|
|
116
116
|
```
|
|
117
117
|
|
|
@@ -134,7 +134,9 @@ Example `langstage.toml`:
|
|
|
134
134
|
```toml
|
|
135
135
|
[agent]
|
|
136
136
|
spec = "my_agent.py:graph"
|
|
137
|
-
|
|
137
|
+
|
|
138
|
+
[workspace]
|
|
139
|
+
root = "."
|
|
138
140
|
|
|
139
141
|
[ui]
|
|
140
142
|
verbose = true
|
|
@@ -160,7 +162,8 @@ Options:
|
|
|
160
162
|
-f, --file PATH Read message from a file (any extension)
|
|
161
163
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
162
164
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
163
|
-
--stream-mode
|
|
165
|
+
--stream-mode [updates|messages]
|
|
166
|
+
Stream mode (default: updates)
|
|
164
167
|
-v, --verbose Verbose output
|
|
165
168
|
```
|
|
166
169
|
|
|
@@ -190,11 +190,15 @@ class Spinner:
|
|
|
190
190
|
frame = SPINNER_FRAMES[self.frame_idx % len(SPINNER_FRAMES)]
|
|
191
191
|
elapsed = time.time() - self.start_time
|
|
192
192
|
elapsed_str = f"{int(elapsed)}s"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
# Never let a stdout hiccup in this daemon thread crash the run.
|
|
194
|
+
try:
|
|
195
|
+
print(
|
|
196
|
+
f"\r{CYAN}{frame}{RESET} {DIM}{self.message}... {elapsed_str}{RESET}",
|
|
197
|
+
end="",
|
|
198
|
+
flush=True,
|
|
199
|
+
)
|
|
200
|
+
except (UnicodeEncodeError, ValueError):
|
|
201
|
+
pass
|
|
198
202
|
self.frame_idx += 1
|
|
199
203
|
time.sleep(0.08)
|
|
200
204
|
|
|
@@ -751,25 +755,26 @@ async def run_single_turn_async(
|
|
|
751
755
|
spinner = Spinner("Thinking")
|
|
752
756
|
spinner.start()
|
|
753
757
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
758
|
+
try:
|
|
759
|
+
async for chunk in astream_graph_updates(
|
|
760
|
+
graph, input_data, config=config, stream_mode=stream_mode
|
|
761
|
+
):
|
|
762
|
+
# Stop spinner on first chunk
|
|
763
|
+
if first_chunk:
|
|
764
|
+
spinner.stop()
|
|
765
|
+
first_chunk = False
|
|
766
|
+
|
|
767
|
+
print_chunk(chunk, verbose=verbose)
|
|
768
|
+
|
|
769
|
+
if chunk.get("status") == "interrupt":
|
|
770
|
+
has_interrupt = True
|
|
771
|
+
# Count pending action requests
|
|
772
|
+
interrupt_data = chunk.get("interrupt", {})
|
|
773
|
+
action_requests = interrupt_data.get("action_requests", [])
|
|
774
|
+
num_pending_actions = len(action_requests) if action_requests else 1
|
|
775
|
+
finally:
|
|
776
|
+
# Always stop the spinner (see sync variant) so a streaming error
|
|
777
|
+
# can't leave a daemon thread holding stdout at interpreter shutdown.
|
|
773
778
|
spinner.stop()
|
|
774
779
|
|
|
775
780
|
if has_interrupt and interactive:
|
|
@@ -800,25 +805,27 @@ def run_single_turn_sync(
|
|
|
800
805
|
spinner = Spinner("Thinking")
|
|
801
806
|
spinner.start()
|
|
802
807
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
808
|
+
try:
|
|
809
|
+
for chunk in stream_graph_updates(
|
|
810
|
+
graph, input_data, config=config, stream_mode=stream_mode
|
|
811
|
+
):
|
|
812
|
+
# Stop spinner on first chunk
|
|
813
|
+
if first_chunk:
|
|
814
|
+
spinner.stop()
|
|
815
|
+
first_chunk = False
|
|
816
|
+
|
|
817
|
+
print_chunk(chunk, verbose=verbose)
|
|
818
|
+
|
|
819
|
+
if chunk.get("status") == "interrupt":
|
|
820
|
+
has_interrupt = True
|
|
821
|
+
# Count pending action requests
|
|
822
|
+
interrupt_data = chunk.get("interrupt", {})
|
|
823
|
+
action_requests = interrupt_data.get("action_requests", [])
|
|
824
|
+
num_pending_actions = len(action_requests) if action_requests else 1
|
|
825
|
+
finally:
|
|
826
|
+
# Always stop the spinner — if the stream raises, an unstopped daemon
|
|
827
|
+
# spinner thread holds stdout at interpreter shutdown, which crashes
|
|
828
|
+
# with "Fatal Python error: _enter_buffered_busy".
|
|
822
829
|
spinner.stop()
|
|
823
830
|
|
|
824
831
|
if has_interrupt and interactive:
|
|
@@ -1335,7 +1342,8 @@ def run_conversation_loop(
|
|
|
1335
1342
|
)
|
|
1336
1343
|
@click.option(
|
|
1337
1344
|
"--stream-mode",
|
|
1338
|
-
|
|
1345
|
+
type=click.Choice(["updates", "messages"]),
|
|
1346
|
+
help="Stream mode: 'updates' (default) or 'messages' (token-level).",
|
|
1339
1347
|
)
|
|
1340
1348
|
@click.option(
|
|
1341
1349
|
"--verbose",
|
|
@@ -1348,7 +1356,7 @@ def run_conversation_loop(
|
|
|
1348
1356
|
"--demo",
|
|
1349
1357
|
is_flag=True,
|
|
1350
1358
|
default=False,
|
|
1351
|
-
help="Run with the built-in keyless demo agent
|
|
1359
|
+
help="Run with the built-in keyless demo agent (no API key needed)",
|
|
1352
1360
|
)
|
|
1353
1361
|
@click.option(
|
|
1354
1362
|
"--show-config",
|
|
@@ -1387,7 +1395,7 @@ def main(
|
|
|
1387
1395
|
\b
|
|
1388
1396
|
- LANGSTAGE_AGENT_SPEC: Agent location (same formats as above).
|
|
1389
1397
|
- LANGSTAGE_WORKSPACE_ROOT: Working directory for the agent
|
|
1390
|
-
- LANGSTAGE_STREAM_MODE: Stream mode for LangGraph (updates or
|
|
1398
|
+
- LANGSTAGE_STREAM_MODE: Stream mode for LangGraph (updates or messages)
|
|
1391
1399
|
|
|
1392
1400
|
Reads ~/.langstage/config.toml (global) and langstage.toml (project,
|
|
1393
1401
|
walks up from cwd). Precedence: CLI args > env vars > project TOML >
|
|
@@ -1403,6 +1411,16 @@ def main(
|
|
|
1403
1411
|
langstage-cli --demo "try it with no API key"
|
|
1404
1412
|
langstage-cli --show-config
|
|
1405
1413
|
"""
|
|
1414
|
+
# Windows consoles default to cp1252, where the spinner (Braille frames) and
|
|
1415
|
+
# status glyphs (✓ ⏺ —) raise UnicodeEncodeError — the documented
|
|
1416
|
+
# `langstage-cli --demo "hello"` one-liner crashed before any output. Force
|
|
1417
|
+
# UTF-8 with errors="replace" so a glyph can never crash the process.
|
|
1418
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
1419
|
+
try:
|
|
1420
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
1421
|
+
except (AttributeError, ValueError): # non-reconfigurable stream
|
|
1422
|
+
pass
|
|
1423
|
+
|
|
1406
1424
|
if demo:
|
|
1407
1425
|
if agent_spec:
|
|
1408
1426
|
print(f"{RED}⏺ Error: --demo and -a/--agent are mutually exclusive{RESET}")
|
|
@@ -1467,6 +1485,15 @@ def main(
|
|
|
1467
1485
|
final_spec = cfg.agent_spec
|
|
1468
1486
|
final_graph_name_default = cfg.graph_name
|
|
1469
1487
|
final_stream_mode = cfg.stream_mode
|
|
1488
|
+
# The CLI's render path only supports 'updates' / 'messages'. Reject anything
|
|
1489
|
+
# else (e.g. LANGSTAGE_STREAM_MODE=values, which bypasses the flag's Choice)
|
|
1490
|
+
# with a clean error instead of a fatal crash deep in the stream worker.
|
|
1491
|
+
if final_stream_mode not in ("updates", "messages"):
|
|
1492
|
+
print(
|
|
1493
|
+
f"{RED}⏺ Error: unsupported stream mode {final_stream_mode!r} "
|
|
1494
|
+
f"(use 'updates' or 'messages'){RESET}"
|
|
1495
|
+
)
|
|
1496
|
+
sys.exit(2)
|
|
1470
1497
|
use_async = cfg.async_mode
|
|
1471
1498
|
verbose = cfg.verbose
|
|
1472
1499
|
# Only change directory when a workspace root was actually configured.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langstage-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph
|
|
5
5
|
Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,7 @@ Requires-Python: >=3.11
|
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: langgraph>=0.2.0
|
|
22
|
-
Requires-Dist: langgraph-stream-parser<0.
|
|
22
|
+
Requires-Dist: langgraph-stream-parser<0.7,>=0.6.6
|
|
23
23
|
Requires-Dist: click>=8.0.0
|
|
24
24
|
Requires-Dist: python-dotenv
|
|
25
25
|
Provides-Extra: dev
|
|
@@ -30,7 +30,7 @@ Requires-Dist: ruff==0.15.15; extra == "dev"
|
|
|
30
30
|
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
31
31
|
Requires-Dist: deepagents>=0.3; extra == "dev"
|
|
32
32
|
Provides-Extra: agui
|
|
33
|
-
Requires-Dist: langgraph-stream-parser[agui]<0.
|
|
33
|
+
Requires-Dist: langgraph-stream-parser[agui]<0.7,>=0.6.6; extra == "agui"
|
|
34
34
|
Dynamic: license-file
|
|
35
35
|
|
|
36
36
|
# langstage-cli
|
|
@@ -146,7 +146,7 @@ langstage-cli
|
|
|
146
146
|
# Working directory
|
|
147
147
|
export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
|
|
148
148
|
|
|
149
|
-
# Stream mode (updates or
|
|
149
|
+
# Stream mode (updates or messages)
|
|
150
150
|
export LANGSTAGE_STREAM_MODE="updates"
|
|
151
151
|
```
|
|
152
152
|
|
|
@@ -169,7 +169,9 @@ Example `langstage.toml`:
|
|
|
169
169
|
```toml
|
|
170
170
|
[agent]
|
|
171
171
|
spec = "my_agent.py:graph"
|
|
172
|
-
|
|
172
|
+
|
|
173
|
+
[workspace]
|
|
174
|
+
root = "."
|
|
173
175
|
|
|
174
176
|
[ui]
|
|
175
177
|
verbose = true
|
|
@@ -195,7 +197,8 @@ Options:
|
|
|
195
197
|
-f, --file PATH Read message from a file (any extension)
|
|
196
198
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
197
199
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
198
|
-
--stream-mode
|
|
200
|
+
--stream-mode [updates|messages]
|
|
201
|
+
Stream mode (default: updates)
|
|
199
202
|
-v, --verbose Verbose output
|
|
200
203
|
```
|
|
201
204
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langstage-cli"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.6"
|
|
8
8
|
description = "The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -25,7 +25,9 @@ classifiers = [
|
|
|
25
25
|
|
|
26
26
|
dependencies = [
|
|
27
27
|
"langgraph>=0.2.0",
|
|
28
|
-
|
|
28
|
+
# >=0.6.6 modernizes off the stale <0.5 pin and carries the dict-form-message
|
|
29
|
+
# fix, so a CompiledGraph returning {"role":...,"content":...} renders (gh #-dogfood).
|
|
30
|
+
"langgraph-stream-parser>=0.6.6,<0.7",
|
|
29
31
|
"click>=8.0.0",
|
|
30
32
|
"python-dotenv",
|
|
31
33
|
]
|
|
@@ -43,7 +45,7 @@ dev = [
|
|
|
43
45
|
# pull deepagents + langchain + langgraph.
|
|
44
46
|
"deepagents>=0.3",
|
|
45
47
|
]
|
|
46
|
-
agui = ["langgraph-stream-parser[agui]>=0.
|
|
48
|
+
agui = ["langgraph-stream-parser[agui]>=0.6.6,<0.7"]
|
|
47
49
|
|
|
48
50
|
[project.scripts]
|
|
49
51
|
langstage-cli = "langstage_cli.cli:main"
|
|
@@ -15,12 +15,12 @@ from langstage_cli.cli import main
|
|
|
15
15
|
def test_show_config_reflects_cli_flags():
|
|
16
16
|
with CliRunner().isolated_filesystem(): # no stray toml
|
|
17
17
|
r = CliRunner().invoke(
|
|
18
|
-
main, ["-a", "fromcli.py:graph", "-v", "--stream-mode", "
|
|
18
|
+
main, ["-a", "fromcli.py:graph", "-v", "--stream-mode", "messages", "--show-config"]
|
|
19
19
|
)
|
|
20
20
|
assert r.exit_code == 0, r.output
|
|
21
21
|
# CLI-set values appear with the [override] source, not [default].
|
|
22
22
|
assert re.search(r"agent_spec\s*=\s*fromcli\.py:graph\s*\[override\]", r.output), r.output
|
|
23
|
-
assert re.search(r"stream_mode\s*=\s*
|
|
23
|
+
assert re.search(r"stream_mode\s*=\s*messages\s*\[override\]", r.output), r.output
|
|
24
24
|
assert re.search(r"verbose\s*=\s*True\s*\[override\]", r.output), r.output
|
|
25
25
|
|
|
26
26
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""--stream-mode validation (gh #-dogfood).
|
|
2
|
+
|
|
3
|
+
`values` was advertised but unsupported by the CLI's render path; passing it
|
|
4
|
+
crashed with a fatal interpreter-shutdown error (a ValueError surfaced while the
|
|
5
|
+
spinner daemon thread held stdout). Now: the flag is a Choice {updates,messages},
|
|
6
|
+
and the resolved value (incl. LANGSTAGE_STREAM_MODE, which bypasses the flag) is
|
|
7
|
+
validated up front with a clean error.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from click.testing import CliRunner
|
|
11
|
+
|
|
12
|
+
from langstage_cli.cli import main
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_stream_mode_values_flag_rejected_cleanly():
|
|
16
|
+
r = CliRunner().invoke(main, ["--demo", "--no-interactive", "--stream-mode", "values", "hi"])
|
|
17
|
+
assert r.exit_code != 0
|
|
18
|
+
assert "values" in r.output
|
|
19
|
+
assert "Fatal" not in r.output # no interpreter-shutdown crash
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_stream_mode_messages_flag_accepted():
|
|
23
|
+
# A valid mode must still be accepted by the Choice (parses, doesn't error on the flag).
|
|
24
|
+
r = CliRunner().invoke(main, ["--stream-mode", "messages", "--show-config"])
|
|
25
|
+
assert r.exit_code == 0, r.output
|
|
26
|
+
assert "messages" in r.output
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_stream_mode_values_via_env_rejected():
|
|
30
|
+
r = CliRunner().invoke(
|
|
31
|
+
main,
|
|
32
|
+
["--demo", "--no-interactive", "hi"],
|
|
33
|
+
env={"LANGSTAGE_STREAM_MODE": "values"},
|
|
34
|
+
)
|
|
35
|
+
assert r.exit_code == 2, r.output
|
|
36
|
+
assert "unsupported stream mode" in r.output.lower()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Regression: the headline `langstage-cli --demo` must not crash on a non-UTF-8
|
|
2
|
+
console (Windows cp1252).
|
|
3
|
+
|
|
4
|
+
The spinner (Braille frames) and status glyphs (✓ ⏺) raised UnicodeEncodeError on
|
|
5
|
+
a default Windows console, so the documented one-liner crashed before output.
|
|
6
|
+
`main()` now reconfigures stdio to UTF-8 (errors="replace"). We force `cp1252`
|
|
7
|
+
via PYTHONIOENCODING so this reproduces on any platform — without the fix this
|
|
8
|
+
test exits non-zero with a UnicodeEncodeError traceback.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_demo_runs_on_cp1252_console():
|
|
17
|
+
env = dict(os.environ)
|
|
18
|
+
env["PYTHONIOENCODING"] = "cp1252"
|
|
19
|
+
env.pop("PYTHONUTF8", None)
|
|
20
|
+
proc = subprocess.run(
|
|
21
|
+
[sys.executable, "-c", "from langstage_cli.cli import main; main()", "--demo", "hello"],
|
|
22
|
+
capture_output=True,
|
|
23
|
+
text=True,
|
|
24
|
+
encoding="utf-8",
|
|
25
|
+
errors="replace",
|
|
26
|
+
env=env,
|
|
27
|
+
)
|
|
28
|
+
assert proc.returncode == 0, f"stdout={proc.stdout!r}\nstderr={proc.stderr!r}"
|
|
29
|
+
assert "UnicodeEncodeError" not in proc.stderr, proc.stderr
|
|
30
|
+
# The demo stub echoes the input back.
|
|
31
|
+
assert "demo agent" in proc.stdout.lower() or "hello" in proc.stdout.lower(), proc.stdout
|
|
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
|