Skip to content

Mengapa U9 Play Unik Tersedia Tahu U9 Play Casino Terhadap Stabil Tawaran

Anda boleh menikmati permainan seperti poker, dengan teknologi penyulitan yang memastikan maklumat anda U9 Play Kaedah Keselamatan Klasik U9 Play Casino Baik Dalam Pilihan dilindungi, u9 play casino juga memastikan semua permainan dioptimumkan untuk peranti mudah alih.

Mengasah kemahiran anda di u9 play casino, u9 play casino menawarkan pelbagai pilihan permainan peniaga langsung, u9 play casino memastikan semua U9 Play Pelanggan Keunggulan Mengasah Untuk Mengoptimalkan Pengalaman Mengutamakan Kami Minat permainan mempunyai grafik berkualiti tinggi dan pengalaman yang interaktif, daftar hari ini dan mulakan perjalanan permainan anda di u9 play casino.

U9 Play Cara Slot Keselamatan U9 Play Casino Ada Dalam Berjalan

Dan kami berusaha untuk memastikan semua aspek permainan adalah memuaskan, anda juga boleh berinteraksi dengan peniaga dan pemain lain, U9 Play Casino Kemahiran Mematuhi Unik U9 Play Casino Kami Acara seakan-akan anda berada di kasino sebenar, u9 play casino ingin menjadi rakan kongsi perjudian dalam talian anda yang dipercayai.

Nikmati pengalaman permainan yang tiada tandingan di u9 play casino dan rasai keseronokan tanpa gangguan, dari bonus pendaftaran yang U9 Play Casino Tempat Slot Tahu U9 Play Data Jangan lumayan hingga tawaran cashback.

Cara Mendaftar Di Tanpa Casino Cara U9 Play Boleh Hanya Dapat

Permainan seperti baccarat, maklumat peribadi anda sentiasa selamat, u9 play casino berusaha untuk memberikan pengalaman U9 Play Casino Menyediakan Jackpot Utama U9 Play Casino Responsif Masa Dilindungi permainan yang tanpa gangguan kepada semua pemain, keunggulan permainan slot di u9 play casino, u9 play casino juga menawarkan bonus selamat datang yang menarik untuk membantu pemain baru memulakan dengan baik.

Menjadikannya pilihan yang ideal untuk pemula dan pemain berpengalaman, pengalaman permainan langsung di u9 play casino memberikan keseronokan yang tidak dapat ditandingi, u9 play casino menawarkan platform permainan mudah alih yang membolehkan anda bermain di mana sahaja dan Mengoptimalkan Pengalaman Pembayaran Perjudian U9 Play Casino Strategi Setelah bila-bila masa.

U9 Mengasah Kemahiran Anda Platform Alih Di U9 Play Suasana Pilihan play casino mengutamakan keselamatan dan privasi pemainnya, u9 play casino: permainan terkini dan terpopuler, u9 play casino menawarkan pelbagai jenis permainan yang memenuhi cita rasa setiap pemain.

U9 Play Casino Besar Mengasah U9 Play Tawaran Pemain

U9 play casino: tempat terbaik untuk pemain baru, u9 play casino memastikan semua permainan adil dan selamat, u9 play casino: rakan kongsi perjudian dalam U9 Play Casino Jackpot Malaysia U9 Play Mengutamakan Chat talian anda, anda boleh mendapatkan bantuan pada bila-bila masa yang diperlukan.

Browse files
committed
fix(windows): split server commands with Windows rules; export hashes what it writes
shlex.split treats a backslash as an escape, so on Windows a server command like C:\Python312\python.exe -m server lost every backslash and failed to start. split_command uses the CommandLineToArgvW rules there (the inverse of subprocess.list2cmdline), and join_command builds lines it splits back exactly. toolscore export wrote CRLF on Windows after hashing LF text, so the manifest's sha256 did not match the file. The suite passes natively on Windows (1,441 tests).
1 parent 602b26f commit 8d4dd83

14 files changed

Lines changed: 157 additions & 20 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ Toolscore 1.11 speaks the current MCP revision (2026-07-28) and tests remote ser
169169
- The release workflow moves the `v1` tag to each release, so `uses: yotambraun/toolscore@v1` resolves, and is ready for PyPI Trusted Publishing (one line to delete once the publisher is added on PyPI).
170170

171171
### Fixed
172+
- Windows: a server command such as `C:\Python312\python.exe -m server` is split with Windows rules (the inverse of `subprocess.list2cmdline`), where POSIX splitting dropped every backslash; `toolscore export` writes `\n` line endings so the manifest's sha256 matches the file; the fault-injection examples quote their command for the platform.
172173

173174
-**Claude Agent SDK messages yielded no calls.** The SDK's `ToolUseBlock` and `ToolResultBlock` dataclasses carry no `type` field, so `from_claude_agent_sdk()` and auto-detection returned nothing for the messages `query()` yields. Blocks are recognized by class, tool results are paired to their calls (`is_error` makes a call failed), and the `ResultMessage` text is the final answer.
174175
- A `claimed-success` finding said a failed call "was not retried" when a later call to the same tool failed too; it now says "no later call to it succeeded".

‎examples/fault_injection/scripted_agent.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525
fromtoolscore.mcpimportMCPStdioClient
2626

2727
HERE=Path(__file__).resolve().parent
28-
SERVER=shlex.join([sys.executable, "-m", "toolscore.mcp.sample_server"])
28+
# one command line for the platform: Windows quoting on Windows, POSIX elsewhere
29+
SERVER= (
30+
subprocess.list2cmdline([sys.executable, "-m", "toolscore.mcp.sample_server"])
31+
ifsys.platform=="win32"
32+
elseshlex.join([sys.executable, "-m", "toolscore.mcp.sample_server"])
33+
)
2934

3035

3136
defmain() ->int:

‎examples/fault_injection/scripted_injection.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@
2828
fromtoolscore.mcpimportMCPStdioClient
2929

3030
HERE=Path(__file__).resolve().parent
31-
SERVER=shlex.join([sys.executable, "-m", "toolscore.mcp.sample_server"])
31+
# one command line for the platform: Windows quoting on Windows, POSIX elsewhere
32+
SERVER= (
33+
subprocess.list2cmdline([sys.executable, "-m", "toolscore.mcp.sample_server"])
34+
ifsys.platform=="win32"
35+
elseshlex.join([sys.executable, "-m", "toolscore.mcp.sample_server"])
36+
)
3237

3338

3439
defmain() ->int:

‎tests/unit/test_cli.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Unit tests for CLI commands."""
22

33
importjson
4-
importshlex
54
importsys
65
frompathlibimportPath
76

87
importpytest
98
fromclick.testingimportCliRunner
109

10+
fromtoolscore._cmdlineimportjoin_command
1111
fromtoolscore.cliimportmain
1212

1313
FIXTURE_SERVER=Path(__file__).resolve().parents[1] /"fixtures"/"fake_mcp_server.py"
@@ -16,7 +16,7 @@
1616
def_fake_server_arg(*extra: str) ->str:
1717
"""Return a single shell-quoted command string launching the fake server."""
1818
parts= [sys.executable, str(FIXTURE_SERVER), *extra]
19-
returnshlex.join(parts)
19+
returnjoin_command(parts)
2020

2121

2222
@pytest.fixture

‎tests/unit/test_cmdline.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""split_command: POSIX rules, Windows rules on Windows."""
2+
3+
from __future__ importannotations
4+
5+
importsubprocess
6+
importsys
7+
8+
importpytest
9+
10+
fromtoolscore._cmdlineimport_split_windows, join_command, split_command
11+
12+
CASES= [
13+
["C:\\Python312\\python.exe", "-m", "toolscore.mcp.sample_server"],
14+
["C:\\Program Files\\node\\node.exe", "server.js", "--root", "C:\\data dir\\"],
15+
["python", "server.py", "--instructions", "Always use 'missing_tool' first."],
16+
["tool", 'say "hi"', "a\\\\b", "trailing\\", ""],
17+
]
18+
19+
20+
@pytest.mark.parametrize("argv", CASES)
21+
deftest_windows_split_inverts_list2cmdline(argv: list[str]) ->None:
22+
assert_split_windows(subprocess.list2cmdline(argv)) ==argv
23+
24+
25+
deftest_unquoted_windows_paths_keep_their_backslashes() ->None:
26+
assert_split_windows("C:\\Python312\\python.exe -m pkg") == [
27+
"C:\\Python312\\python.exe",
28+
"-m",
29+
"pkg",
30+
]
31+
32+
33+
@pytest.mark.skipif(sys.platform=="win32", reason="POSIX rules")
34+
deftest_posix_rules_elsewhere() ->None:
35+
assertsplit_command("npx -y '@scope/server' --dir \"a b\"") == [
36+
"npx",
37+
"-y",
38+
"@scope/server",
39+
"--dir",
40+
"a b",
41+
]
42+
43+
44+
@pytest.mark.parametrize("argv", CASES)
45+
deftest_join_and_split_round_trip_on_this_platform(argv: list[str]) ->None:
46+
assertsplit_command(join_command(argv)) ==argv

‎tests/unit/test_config_lint.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ def test_discovers_instruction_files_and_host_configs(tmp_path: Path) -> None:
4545
path.parent.mkdir(parents=True, exist_ok=True)
4646
path.write_text("{}"ifname.endswith(".json") else"# x\n", encoding="utf-8")
4747
instructions, configs=discover_files([tmp_path])
48-
rel=sorted(str(p.relative_to(tmp_path)) forpininstructions)
48+
rel=sorted(p.relative_to(tmp_path).as_posix() forpininstructions)
4949
assertrel==sorted(files[:9])
50-
assertsorted(str(p.relative_to(tmp_path)) forpinconfigs) ==sorted(files[9:13])
50+
assertsorted(p.relative_to(tmp_path).as_posix() forpinconfigs) ==sorted(files[9:13])
5151
# an explicitly named Markdown file is always read
5252
assertdiscover_files([tmp_path/"README.md"])[0] == [tmp_path/"README.md"]
5353

‎tests/unit/test_mcp_proxy.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
importio
66
importjson
7-
importshlex
87
importsys
98
fromtypingimportTYPE_CHECKING
109

1110
importpytest
1211

12+
fromtoolscore._cmdlineimportjoin_command
1313
fromtoolscore.coreimportload_trace
1414
fromtoolscore.mcpimportMCPRecorder, MCPStdioClient, MCPTimeoutError
1515
fromtoolscore.mcp.faultsimport (
@@ -36,7 +36,7 @@ def _proxy(output: Path, spec: Path) -> list[str]:
3636
"from toolscore.cli import main; main()",
3737
"mcp",
3838
"proxy",
39-
shlex.join(SAMPLE),
39+
join_command(SAMPLE),
4040
"--inject",
4141
str(spec),
4242
"-o",
@@ -247,7 +247,7 @@ def test_cli_proxy_without_inject_only_records(tmp_path: Path) -> None:
247247
"from toolscore.cli import main; main()",
248248
"mcp",
249249
"proxy",
250-
shlex.join(SAMPLE),
250+
join_command(SAMPLE),
251251
"-o",
252252
str(out),
253253
]

‎tests/unit/test_mcp_record.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@
99
importio
1010
importjson
1111
importos
12-
importshlex
1312
importsys
1413
importthreading
1514
frompathlibimportPath
1615

1716
fromtoolscoreimportevaluate
17+
fromtoolscore._cmdlineimportjoin_command
1818
fromtoolscore.coreimportload_trace
1919
fromtoolscore.mcpimportMCPRecorder, MCPStdioClient
2020

2121
FIXTURE_SERVER=Path(__file__).resolve().parents[1] /"fixtures"/"fake_mcp_server.py"
2222

2323

2424
def_recorder(output: Path, *server_flags: str) ->list[str]:
25-
server=shlex.join([sys.executable, str(FIXTURE_SERVER), *server_flags])
25+
server=join_command([sys.executable, str(FIXTURE_SERVER), *server_flags])
2626
return [
2727
sys.executable,
2828
"-c",

‎tests/unit/test_sessions.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def test_honesty_and_eval_read_session_files_directly() -> None:
250250

251251
def_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) ->tuple[Path, Path]:
252252
monkeypatch.setenv("HOME", str(tmp_path))
253+
monkeypatch.setenv("USERPROFILE", str(tmp_path)) # Path.home() on Windows
253254
monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
254255
monkeypatch.delenv("CODEX_HOME", raising=False)
255256
claude=_write_jsonl(
@@ -287,6 +288,7 @@ def test_find_and_summarize_sessions(tmp_path: Path, monkeypatch: pytest.MonkeyP
287288

288289
deftest_config_dir_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) ->None:
289290
monkeypatch.setenv("HOME", str(tmp_path/"nowhere"))
291+
monkeypatch.setenv("USERPROFILE", str(tmp_path/"nowhere")) # Path.home() on Windows
290292
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(tmp_path/"cc"))
291293
monkeypatch.setenv("CODEX_HOME", str(tmp_path/"cx"))
292294
a=_write_jsonl(tmp_path/"cc"/"projects"/"p"/"a.jsonl", _claude_session())

‎toolscore/_cmdline.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Split a command line the way the user's platform does."""
2+
3+
from __future__ importannotations
4+
5+
importshlex
6+
importsubprocess
7+
importsys
8+
9+
__all__= ["join_command", "split_command"]
10+
11+
12+
defsplit_command(command: str) ->list[str]:
13+
"""Tokens of ``command``: POSIX shell rules, or Windows rules on Windows.
14+
15+
``shlex.split`` treats a backslash as an escape, which turns a Windows path
16+
such as ``C:\\Python\\python.exe`` into ``C:Pythonpython.exe``. On Windows the
17+
line is split as ``CommandLineToArgvW`` does, the inverse of
18+
``subprocess.list2cmdline``.
19+
"""
20+
ifsys.platform!="win32":
21+
returnshlex.split(command)
22+
return_split_windows(command)
23+
24+
25+
defjoin_command(argv: list[str]) ->str:
26+
"""One command line for ``argv`` that :func:`split_command` splits back into ``argv``."""
27+
returnsubprocess.list2cmdline(argv) ifsys.platform=="win32"elseshlex.join(argv)
28+
29+
30+
def_split_windows(command: str) ->list[str]:
31+
tokens: list[str] = []
32+
current: list[str] = []
33+
in_token=in_quotes=False
34+
i, n=0, len(command)
35+
whilei<n:
36+
ch=command[i]
37+
ifch=="\\":
38+
j=i
39+
whilej<nandcommand[j] =="\\":
40+
j+=1
41+
count=j-i
42+
ifj<nandcommand[j] =='"':
43+
# 2k backslashes + quote: k backslashes, the quote toggles; 2k+1: k and a literal quote
44+
current.append("\\"* (count//2))
45+
ifcount%2:
46+
current.append('"')
47+
j+=1
48+
i=j
49+
else:
50+
current.append("\\"*count)
51+
i=j
52+
in_token=True
53+
continue
54+
ifch=='"':
55+
ifin_quotesandi+1<nandcommand[i+1] =='"':
56+
current.append('"') # "" inside quotes is a literal quote
57+
i+=2
58+
continue
59+
in_quotes=notin_quotes
60+
in_token=True
61+
i+=1
62+
continue
63+
ifchin" \t"andnotin_quotes:
64+
ifin_token:
65+
tokens.append("".join(current))
66+
current, in_token= [], False
67+
i+=1
68+
continue
69+
current.append(ch)
70+
in_token=True
71+
i+=1
72+
ifin_token:
73+
tokens.append("".join(current))
74+
returntokens

0 commit comments

Comments
 (0)