subtask-mcp 0.2.6__tar.gz → 0.2.8__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.
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/PKG-INFO +2 -2
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/README.md +1 -1
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/__init__.py +1 -1
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/core.py +8 -2
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/events.py +23 -5
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/fastserver.py +3 -1
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/server.py +3 -1
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask_mcp.egg-info/PKG-INFO +2 -2
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask_mcp.egg-info/SOURCES.txt +1 -0
- subtask_mcp-0.2.8/tests/test_live.py +76 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_protocol.py +16 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_security.py +16 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/LICENSE +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/pyproject.toml +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/setup.cfg +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/admission.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/capacity.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/cli.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/config.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/doctor.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/__init__.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/base.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/claude_api.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/codex.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/demo.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/dsh.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/kimi.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/opencode.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/engines/registry.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/evidence.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/gc.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/__init__.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/claude_plugin.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/clients.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/commands.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/doctor.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/fileops.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/installer.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/ownership.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/install/tomledit.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/jobs.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/ledger.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/narrate.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/policy.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/procid.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/progress.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/redact.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/roi.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/router.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/runners.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/schemacheck.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/skill/SKILL.md +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/telemetry.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/tools.json +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask/worker.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask_mcp.egg-info/dependency_links.txt +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask_mcp.egg-info/entry_points.txt +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask_mcp.egg-info/requires.txt +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/subtask_mcp.egg-info/top_level.txt +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_concurrency.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_engines.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_falsify.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_independent_client.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_install.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_routing_results.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_schema.py +0 -0
- {subtask_mcp-0.2.6 → subtask_mcp-0.2.8}/tests/test_subtask.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subtask-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Background sub-agents for Claude Code, Codex, Kimi and OpenCode, over MCP or CLI, with a spend cap
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/tedin7/subtask
|
|
@@ -70,7 +70,7 @@ The distribution is `subtask-mcp` (the PyPI name `subtask` is an unrelated
|
|
|
70
70
|
project). Full guide, per client, with native commands: [docs/install.md](docs/install.md).
|
|
71
71
|
|
|
72
72
|
```sh
|
|
73
|
-
uv tool install "git+https://github.com/tedin7/subtask@v0.2.
|
|
73
|
+
uv tool install "git+https://github.com/tedin7/subtask@v0.2.8" # or: uv tool install /path/to/checkout
|
|
74
74
|
subtask install --client all # prints the plan for every detected client
|
|
75
75
|
subtask install --client all --yes # applies it: MCP server + skill
|
|
76
76
|
subtask doctor --client all # checks app, config entry and a real handshake
|
|
@@ -39,7 +39,7 @@ The distribution is `subtask-mcp` (the PyPI name `subtask` is an unrelated
|
|
|
39
39
|
project). Full guide, per client, with native commands: [docs/install.md](docs/install.md).
|
|
40
40
|
|
|
41
41
|
```sh
|
|
42
|
-
uv tool install "git+https://github.com/tedin7/subtask@v0.2.
|
|
42
|
+
uv tool install "git+https://github.com/tedin7/subtask@v0.2.8" # or: uv tool install /path/to/checkout
|
|
43
43
|
subtask install --client all # prints the plan for every detected client
|
|
44
44
|
subtask install --client all --yes # applies it: MCP server + skill
|
|
45
45
|
subtask doctor --client all # checks app, config entry and a real handshake
|
|
@@ -306,7 +306,12 @@ def _delta(job_ids: list[str], since: str) -> tuple[str, str]:
|
|
|
306
306
|
|
|
307
307
|
|
|
308
308
|
def wait(
|
|
309
|
-
job_ids: list[str],
|
|
309
|
+
job_ids: list[str],
|
|
310
|
+
timeout: int = 50,
|
|
311
|
+
any_: bool = False,
|
|
312
|
+
results: bool = True,
|
|
313
|
+
since: str | None = None,
|
|
314
|
+
count: bool = True,
|
|
310
315
|
) -> str:
|
|
311
316
|
"""Block until all jobs end (or the first one, with any_), or timeout.
|
|
312
317
|
|
|
@@ -320,7 +325,8 @@ def wait(
|
|
|
320
325
|
unknown = [j for j in job_ids if not known(j)]
|
|
321
326
|
if unknown:
|
|
322
327
|
return "unknown job: " + ", ".join(unknown)
|
|
323
|
-
|
|
328
|
+
if count: # servers poll wait() every second inside one tool call: count that call once
|
|
329
|
+
telemetry.count_call(job_ids, "wait")
|
|
324
330
|
deadline = time.time() + max(0, min(timeout, 600))
|
|
325
331
|
while True:
|
|
326
332
|
ended = [j for j in job_ids if jobs.status(j).get("state") in jobs.FINAL]
|
|
@@ -35,11 +35,24 @@ def emit(
|
|
|
35
35
|
state: str | None = None,
|
|
36
36
|
source: str = "lifecycle",
|
|
37
37
|
attempt: int | None = None,
|
|
38
|
+
) -> dict | None:
|
|
39
|
+
"""Append one event. Never raises: progress must not be able to fail a job."""
|
|
40
|
+
try:
|
|
41
|
+
return _emit(job_id, phase, msg, state, source, attempt)
|
|
42
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
43
|
+
import sys
|
|
44
|
+
|
|
45
|
+
print(f"subtask: progress event not written: {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _emit(
|
|
50
|
+
job_id: str, phase: str, msg: str, state: str | None, source: str, attempt: int | None
|
|
38
51
|
) -> dict | None:
|
|
39
52
|
path = _path(job_id)
|
|
40
53
|
if not path.parent.is_dir():
|
|
41
54
|
return None
|
|
42
|
-
with path.open("
|
|
55
|
+
with path.open("ab+") as fh:
|
|
43
56
|
fcntl.flock(fh, fcntl.LOCK_EX)
|
|
44
57
|
last = _last(fh)
|
|
45
58
|
seq = int(last.get("seq", 0)) + 1
|
|
@@ -57,16 +70,21 @@ def emit(
|
|
|
57
70
|
"source": source,
|
|
58
71
|
}
|
|
59
72
|
fh.seek(0, os.SEEK_END)
|
|
60
|
-
fh.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
73
|
+
fh.write((json.dumps(event, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
61
74
|
fh.flush()
|
|
62
75
|
return event
|
|
63
76
|
|
|
64
77
|
|
|
65
78
|
def _last(fh) -> dict:
|
|
79
|
+
"""The last complete event, reading only the tail (bytes: the cut may split a UTF-8 character)."""
|
|
66
80
|
fh.seek(0, os.SEEK_END)
|
|
67
81
|
size = fh.tell()
|
|
68
82
|
fh.seek(max(0, size - 4096))
|
|
69
|
-
|
|
83
|
+
data = fh.read()
|
|
84
|
+
text = data.decode("utf-8", errors="replace") if isinstance(data, bytes) else data
|
|
85
|
+
lines = text.splitlines()
|
|
86
|
+
if size > 4096 and lines:
|
|
87
|
+
lines = lines[1:] # the first line is cut by the seek: never trust it
|
|
70
88
|
for line in reversed(lines):
|
|
71
89
|
try:
|
|
72
90
|
return json.loads(line)
|
|
@@ -99,7 +117,7 @@ def read_since(job_id: str, offset: int = 0, limit: int = 500) -> tuple[list[dic
|
|
|
99
117
|
def last(job_id: str) -> dict:
|
|
100
118
|
path = _path(job_id)
|
|
101
119
|
try:
|
|
102
|
-
with path.open("
|
|
120
|
+
with path.open("rb") as fh:
|
|
103
121
|
return _last(fh)
|
|
104
|
-
except FileNotFoundError:
|
|
122
|
+
except (FileNotFoundError, OSError):
|
|
105
123
|
return {}
|
|
@@ -89,8 +89,10 @@ def _wait(
|
|
|
89
89
|
if core.known(job_id):
|
|
90
90
|
call.reporter.add(job_id)
|
|
91
91
|
call.reporter.poll()
|
|
92
|
+
first = True
|
|
92
93
|
while True:
|
|
93
|
-
out = core.wait(ids, 0, any, since=since)
|
|
94
|
+
out = core.wait(ids, 0, any, since=since, count=first)
|
|
95
|
+
first = False
|
|
94
96
|
if not out.startswith("still running") or time.monotonic() >= deadline:
|
|
95
97
|
if call.reporter:
|
|
96
98
|
call.reporter.poll()
|
|
@@ -77,12 +77,14 @@ async def wait(
|
|
|
77
77
|
|
|
78
78
|
reporter = Reporter(send, [i for i in ids if core.known(i)])
|
|
79
79
|
deadline = loop.time() + max(1, min(timeout, 600))
|
|
80
|
+
first = True
|
|
80
81
|
while True:
|
|
81
82
|
if reporter:
|
|
82
83
|
reporter.poll()
|
|
83
84
|
while pending:
|
|
84
85
|
await pending.pop(0) # never after the result: progress stops at completion
|
|
85
|
-
out = core.wait(ids, 0, any, since=since)
|
|
86
|
+
out = core.wait(ids, 0, any, since=since, count=first)
|
|
87
|
+
first = False
|
|
86
88
|
if not out.startswith("still running") or loop.time() >= deadline:
|
|
87
89
|
return out
|
|
88
90
|
await asyncio.sleep(1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subtask-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Background sub-agents for Claude Code, Codex, Kimi and OpenCode, over MCP or CLI, with a spend cap
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/tedin7/subtask
|
|
@@ -70,7 +70,7 @@ The distribution is `subtask-mcp` (the PyPI name `subtask` is an unrelated
|
|
|
70
70
|
project). Full guide, per client, with native commands: [docs/install.md](docs/install.md).
|
|
71
71
|
|
|
72
72
|
```sh
|
|
73
|
-
uv tool install "git+https://github.com/tedin7/subtask@v0.2.
|
|
73
|
+
uv tool install "git+https://github.com/tedin7/subtask@v0.2.8" # or: uv tool install /path/to/checkout
|
|
74
74
|
subtask install --client all # prints the plan for every detected client
|
|
75
75
|
subtask install --client all --yes # applies it: MCP server + skill
|
|
76
76
|
subtask doctor --client all # checks app, config entry and a real handshake
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Live adapter checks: one tiny real job per engine, through subtask itself.
|
|
2
|
+
|
|
3
|
+
Marked `live`: skipped by default (pytest.ini addopts "-m 'not live'"). Run with
|
|
4
|
+
`pytest -m live` when spending is authorized. Each job is capped at 0.20 USD
|
|
5
|
+
(paid engines) and asks for one shell command and a one-word answer, so the
|
|
6
|
+
contract with the real CLI/SDK is exercised: argv accepted, events parsed,
|
|
7
|
+
session id found, result written, cost booked.
|
|
8
|
+
Engines that are not installed or not logged in are skipped, never faked.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
pytestmark = pytest.mark.live
|
|
24
|
+
|
|
25
|
+
TASK = "Run the shell command `echo subtask-live-ok` once, then answer with exactly that output."
|
|
26
|
+
ENGINES = ["haiku-api", "opencode-go", "kimi", "codex"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cli(home: Path, *args: str, timeout: float = 300) -> str:
|
|
30
|
+
env = {**os.environ, "SUBTASK_HOME": str(home), "SUBTASK_MAX_RUNNING": "4"}
|
|
31
|
+
done = subprocess.run([sys.executable, "-m", "subtask.cli", *args], env=env, capture_output=True,
|
|
32
|
+
text=True, timeout=timeout, check=False) # fmt: skip
|
|
33
|
+
return done.stdout + done.stderr
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _available(engine: str) -> str | None:
|
|
37
|
+
from subtask import config
|
|
38
|
+
from subtask.engines import registry
|
|
39
|
+
|
|
40
|
+
adapter = registry.get(engine)
|
|
41
|
+
if adapter is None or not adapter.available():
|
|
42
|
+
return f"{engine} not installed"
|
|
43
|
+
if (
|
|
44
|
+
adapter.capabilities.paid
|
|
45
|
+
and engine.endswith("-api")
|
|
46
|
+
and not config.load_secrets().get("ANTHROPIC_API_KEY")
|
|
47
|
+
):
|
|
48
|
+
return "no ANTHROPIC_API_KEY in the subtask env file"
|
|
49
|
+
cap = adapter.capacity()
|
|
50
|
+
if cap.value is not None and cap.value < 10:
|
|
51
|
+
return f"{engine} has no quota left ({cap.note})"
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("engine", ENGINES)
|
|
56
|
+
def test_engine_runs_a_tiny_real_job(engine, tmp_path):
|
|
57
|
+
reason = _available(engine)
|
|
58
|
+
if reason:
|
|
59
|
+
pytest.skip(reason)
|
|
60
|
+
if shutil.which("git") is None:
|
|
61
|
+
pytest.skip("git missing")
|
|
62
|
+
work = tmp_path / "work"
|
|
63
|
+
work.mkdir()
|
|
64
|
+
out = _cli(tmp_path / "home", "start", TASK, "--engine", engine, "--cwd", str(work), "--max-usd", "0.2",
|
|
65
|
+
"--max-turns", "6", "--effort", "low") # fmt: skip
|
|
66
|
+
assert " started " in out, out
|
|
67
|
+
job = out.split()[0]
|
|
68
|
+
t0 = time.time()
|
|
69
|
+
final = _cli(tmp_path / "home", "wait", job, "--timeout", "280")
|
|
70
|
+
assert " done " in final, final
|
|
71
|
+
env = json.loads(_cli(tmp_path / "home", "result", job, "--evidence"))
|
|
72
|
+
assert "subtask-live-ok" in env["summary"]["text"], env["summary"]
|
|
73
|
+
assert env["commands"]["seen"] >= 1, "no shell command observed: event format changed?"
|
|
74
|
+
if env["cost"]["reserved"] is not None:
|
|
75
|
+
assert 0 <= (env["cost"]["charged_total"] or 0) <= 0.2
|
|
76
|
+
assert time.time() - t0 < 300
|
|
@@ -262,3 +262,19 @@ def test_reporter_follows_the_latest_change_and_admits_missing_detail(home):
|
|
|
262
262
|
assert rep._what(b) == progress.NO_DETAIL # kimi plain output: no structured activity
|
|
263
263
|
core.stop(a)
|
|
264
264
|
core.stop(b)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_a_long_wait_counts_as_one_round_trip(home):
|
|
268
|
+
"""The server polls core.wait every second; that is not a parent round trip."""
|
|
269
|
+
from subtask import telemetry
|
|
270
|
+
|
|
271
|
+
fake_cli(home, "kimi", "sleep 4; echo done")
|
|
272
|
+
c = Client(_env())
|
|
273
|
+
c.send({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
274
|
+
"params": {"name": "start", "arguments": {"task": "x", "engine": "kimi", "cwd": str(home)}}}) # fmt: skip
|
|
275
|
+
job = c.until(1)[0]["result"]["content"][0]["text"].split()[0]
|
|
276
|
+
c.send({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
|
277
|
+
"params": {"name": "wait", "arguments": {"ids": [job], "timeout": 30}}}) # fmt: skip
|
|
278
|
+
c.until(2, timeout=40)
|
|
279
|
+
c.close()
|
|
280
|
+
assert [x["tool"] for x in telemetry.calls(job)].count("wait") == 1
|
|
@@ -180,3 +180,19 @@ def test_isolated_is_refused_when_bwrap_cannot_run(home, monkeypatch):
|
|
|
180
180
|
monkeypatch.setattr(runners, "bwrap_available", lambda: False)
|
|
181
181
|
out = core.start("x", engine="kimi", cwd=str(home), policy="isolated")
|
|
182
182
|
assert "bubblewrap" in out and "started" not in out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_progress_log_survives_multibyte_text_across_the_tail_boundary(home):
|
|
186
|
+
"""Reading the last event seeks back 4096 bytes: that may land inside a UTF-8 character."""
|
|
187
|
+
import json
|
|
188
|
+
|
|
189
|
+
from subtask import events, jobs
|
|
190
|
+
|
|
191
|
+
folder = jobs.job_dir("st-utf8utf800")
|
|
192
|
+
folder.mkdir(parents=True)
|
|
193
|
+
line = json.dumps({"seq": 1, "msg": "…" * 50}, ensure_ascii=False) + "\n" # "…" is 3 bytes
|
|
194
|
+
for pad in range(3): # one of the three alignments puts byte -4096 inside a "…"
|
|
195
|
+
body = ("x" * pad + "\n" + line * 40).encode()
|
|
196
|
+
(folder / "progress.jsonl").write_bytes(body)
|
|
197
|
+
assert events.emit("st-utf8utf800", "tool", "dopo", source="adapter") is not None, pad
|
|
198
|
+
assert events.last("st-utf8utf800")["msg"] == "dopo"
|
|
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
|
|
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
|
|
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
|