Skip to content

Commit d91f79b

Browse files
committed
Docs(docs[engines]): Teach every engine
why: Engine users need transport-specific examples that expose real output and boundaries without treating mock results as server evidence. what: - Add one executable first-success example per concrete engine - Map every engine to a focused live or offline tutorial - Test tutorial ownership and transport boundaries
1 parent b364b53 commit d91f79b

16 files changed

Lines changed: 280 additions & 273 deletions

docs/experimental/engines.md

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ boundary around execution.
88

99
## Choose an engine
1010

11-
| Engine | Dispatch model | Use it for |
12-
| ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------- |
13-
| {class}`~libtmux.experimental.engines.subprocess.SubprocessEngine` ([reference](engines/subprocess.md)) | One tmux CLI process per request | Straightforward synchronous live-server work |
14-
| {class}`~libtmux.experimental.engines.asyncio.AsyncSubprocessEngine` ([reference](engines/async-subprocess.md)) | One asyncio subprocess per request | Async applications that do not need a persistent connection |
15-
| {class}`~libtmux.experimental.engines.mock.MockEngine` ([reference](engines/mock.md)) | Stateless in-memory simulation | Offline rendering and result-conversion tests |
16-
| {class}`~libtmux.experimental.engines.mock.AsyncMockEngine` ([reference](engines/async-mock.md)) | Async stateless in-memory simulation | Offline tests of async callers |
17-
| {class}`~libtmux.experimental.engines.control_mode.ControlModeEngine` ([reference](engines/control-mode.md)) | Subprocess bootstrap, then persistent `tmux -C` | Pipelined command batches |
18-
| {class}`~libtmux.experimental.engines.async_control_mode.AsyncControlModeEngine` ([reference](engines/async-control-mode.md)) | Async subprocess bootstrap, then supervised `tmux -C` | Async batches and control-mode notifications |
19-
| {class}`~libtmux.experimental.engines.imsg.base.ImsgEngine` ([reference](engines/imsg.md)) | Native socket with required CLI fallbacks | POSIX protocol experiments and narrow parity checks |
11+
| Engine | Dispatch model | Use it for | Worked example |
12+
| ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------ |
13+
| {class}`~libtmux.experimental.engines.subprocess.SubprocessEngine` ([reference](engines/subprocess.md)) | One tmux CLI process per request | Straightforward synchronous live-server work | {doc}`tutorials/live-operation` |
14+
| {class}`~libtmux.experimental.engines.asyncio.AsyncSubprocessEngine` ([reference](engines/async-subprocess.md)) | One asyncio subprocess per request | Async applications that do not need a persistent connection | {doc}`tutorials/async-subprocess` |
15+
| {class}`~libtmux.experimental.engines.mock.MockEngine` ([reference](engines/mock.md)) | Stateless in-memory simulation | Offline rendering and result-conversion tests | {doc}`tutorials/offline-testing` |
16+
| {class}`~libtmux.experimental.engines.mock.AsyncMockEngine` ([reference](engines/async-mock.md)) | Async stateless in-memory simulation | Offline tests of async callers | {doc}`tutorials/offline-testing` |
17+
| {class}`~libtmux.experimental.engines.control_mode.ControlModeEngine` ([reference](engines/control-mode.md)) | Subprocess bootstrap, then persistent `tmux -C` | Pipelined command batches | {doc}`tutorials/control-mode` |
18+
| {class}`~libtmux.experimental.engines.async_control_mode.AsyncControlModeEngine` ([reference](engines/async-control-mode.md)) | Async subprocess bootstrap, then supervised `tmux -C` | Async batches and control-mode notifications | {doc}`tutorials/async-control-plans` |
19+
| {class}`~libtmux.experimental.engines.imsg.base.ImsgEngine` ([reference](engines/imsg.md)) | Native socket with required CLI fallbacks | POSIX protocol experiments and narrow parity checks | {doc}`tutorials/imsg-parity` |
2020

2121
The control-mode engines open a persistent client only when an existing
2222
session has `destroy-unattached` set to `off`. Until then, they use their
@@ -30,13 +30,10 @@ event loop.
3030

3131
```python
3232
>>> from libtmux.experimental.engines import available_engines, create_engine
33-
>>> from libtmux.experimental.ops import HasSession, run
34-
>>> from libtmux.experimental.ops._types import SessionId
3533
>>> available_engines()
3634
('control_mode', 'imsg', 'mock', 'subprocess')
37-
>>> engine = create_engine("mock")
38-
>>> run(HasSession(target=SessionId("$0")), engine).status
39-
'complete'
35+
>>> type(create_engine("mock")).__name__
36+
'MockEngine'
4037
```
4138

4239
## Engine boundary
@@ -56,16 +53,18 @@ raise at the engine boundary.
5653

5754
## Tutorials
5855

59-
Start with {doc}`tutorials/live-operation`, then use the task guide that matches
60-
the boundary you need to understand:
56+
Each engine row links to its tested workflow. Start with
57+
{doc}`tutorials/live-operation`, or go directly to the transport you need:
6158

62-
- {doc}`tutorials/results-and-failures`
6359
- {doc}`tutorials/async-control-plans`
6460
- {doc}`tutorials/async-subprocess`
6561
- {doc}`tutorials/control-mode`
6662
- {doc}`tutorials/offline-testing`
6763
- {doc}`tutorials/imsg-parity`
6864

65+
{doc}`tutorials/results-and-failures` is the shared result guide rather than an
66+
engine-owned workflow.
67+
6968
## Shared API
7069

7170
```{eval-rst}

docs/experimental/engines/async-control-mode.md

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,23 @@ the first batch uses
3232
`new-session` bootstrap an empty server without creating a private control
3333
session. A later batch can open the persistent connection.
3434

35+
Within the context, pass typed operations to
36+
{func}`~libtmux.experimental.ops.arun` as you would with any asynchronous
37+
engine.
38+
3539
```python
3640
>>> import asyncio
37-
>>> from libtmux.experimental.engines import AsyncControlModeEngine, CommandRequest
38-
>>> assert session.session_id is not None
39-
>>> assert window.window_id is not None
40-
>>> requests = [
41-
... CommandRequest.from_args(
42-
... "display-message", "-p", "-t", session.session_id, "#{session_id}"
43-
... ),
44-
... CommandRequest.from_args(
45-
... "display-message", "-p", "-t", window.window_id, "#{window_id}"
46-
... ),
47-
... ]
48-
>>> async def query_targets():
41+
>>> from libtmux.experimental.engines import AsyncControlModeEngine
42+
>>> from libtmux.experimental.ops import DisplayMessage, PaneId, arun
43+
>>> assert pane is not None and pane.pane_id is not None
44+
>>> async def read_pane_id():
4945
... async with AsyncControlModeEngine.for_server(server) as engine:
50-
... return await engine.run_batch(requests)
51-
>>> results = asyncio.run(query_targets())
52-
>>> [result.returncode for result in results]
53-
[0, 0]
54-
>>> results[0].stdout == (session.session_id,)
55-
True
56-
>>> results[1].stdout == (window.window_id,)
46+
... result = await arun(
47+
... DisplayMessage(target=PaneId(pane.pane_id), message="#{pane_id}"),
48+
... engine,
49+
... )
50+
... return result.raise_for_status().text
51+
>>> asyncio.run(read_pane_id()) == pane.pane_id
5752
True
5853
```
5954

@@ -96,8 +91,7 @@ a notification emitted before the first iteration will be delivered.
9691
:special-members: __aenter__, __aexit__
9792
```
9893

99-
## Related tutorials
94+
## Related tutorial
10095

101-
See {doc}`../tutorials/control-mode` for the shared batching contract and the
102-
notification boundary. See {doc}`../tutorials/async-control-plans` to compose
103-
forward-referenced operations and fold them into control-mode dispatches.
96+
See {doc}`../tutorials/async-control-plans` to compose forward-referenced
97+
operations and fold them into control-mode dispatches.

docs/experimental/engines/async-mock.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ reconnection, notifications, or real tmux state.
1515

1616
## Construction and cleanup
1717

18-
Pass optional canned `capture_lines`. It owns no task, process, or connection,
19-
so no cleanup is required.
18+
Pass optional canned `capture_lines`. Create operations receive fabricated IDs.
19+
The engine owns no task, process, or connection, so no cleanup is required.
20+
21+
The `%1` below is fabricated. The engine neither inspects `@404` nor creates a
22+
pane.
2023

2124
```python
2225
>>> import asyncio
2326
>>> from libtmux.experimental.engines import AsyncMockEngine
24-
>>> from libtmux.experimental.ops import CapturePane, arun
25-
>>> from libtmux.experimental.ops._types import PaneId
26-
>>> async def capture_offline():
27-
... engine = AsyncMockEngine(capture_lines=("offline",))
28-
... return await arun(CapturePane(target=PaneId("%1")), engine)
29-
>>> asyncio.run(capture_offline()).lines
30-
('offline',)
27+
>>> from libtmux.experimental.ops import SplitWindow, WindowId, arun
28+
>>> operation = SplitWindow(target=WindowId("@404"))
29+
>>> async def create_offline():
30+
... return await arun(operation, AsyncMockEngine())
31+
>>> fabricated = asyncio.run(create_offline()).raise_for_status()
32+
>>> fabricated.new_pane_id, fabricated.argv == operation.render()
33+
('%1', True)
3134
```
3235

3336
## Lifecycle and failure boundary
@@ -48,5 +51,5 @@ caller supplies `version`.
4851

4952
## Related tutorial
5053

51-
See {doc}`../tutorials/offline-testing` for parallel sync and async test
52-
patterns.
54+
See {doc}`../tutorials/offline-testing` for synchronous and asynchronous
55+
offline test patterns.

docs/experimental/engines/async-subprocess.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ reaps its child.
2424
```python
2525
>>> import asyncio
2626
>>> from libtmux.experimental.engines import AsyncSubprocessEngine
27-
>>> from libtmux.experimental.ops import DisplayMessage, arun
28-
>>> from libtmux.experimental.ops._types import PaneId
27+
>>> from libtmux.experimental.ops import DisplayMessage, PaneId, arun
2928
>>> assert pane is not None and pane.pane_id is not None
3029
>>> async def read_pane_id():
3130
... engine = AsyncSubprocessEngine.for_server(server)

docs/experimental/engines/control-mode.md

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,19 @@ If no safe session exists, the batch uses
3030
This lets `new-session` bootstrap an empty server without creating a private
3131
control session. A later batch can open the persistent connection.
3232

33+
Within the context, pass typed operations to
34+
{func}`~libtmux.experimental.ops.run` as you would with any synchronous engine.
35+
3336
```python
34-
>>> from libtmux.experimental.engines import CommandRequest, ControlModeEngine
35-
>>> assert session.session_id is not None
36-
>>> assert window.window_id is not None
37-
>>> requests = [
38-
... CommandRequest.from_args(
39-
... "display-message", "-p", "-t", session.session_id, "#{session_id}"
40-
... ),
41-
... CommandRequest.from_args(
42-
... "display-message", "-p", "-t", window.window_id, "#{window_id}"
43-
... ),
44-
... ]
37+
>>> from libtmux.experimental.engines import ControlModeEngine
38+
>>> from libtmux.experimental.ops import DisplayMessage, PaneId, run
39+
>>> assert pane is not None and pane.pane_id is not None
4540
>>> with ControlModeEngine.for_server(server) as engine:
46-
... results = engine.run_batch(requests)
47-
>>> [result.returncode for result in results]
48-
[0, 0]
49-
>>> results[0].stdout == (session.session_id,)
50-
True
51-
>>> results[1].stdout == (window.window_id,)
41+
... result = run(
42+
... DisplayMessage(target=PaneId(pane.pane_id), message="#{pane_id}"),
43+
... engine,
44+
... ).raise_for_status()
45+
>>> result.text == pane.pane_id
5246
True
5347
```
5448

@@ -78,5 +72,5 @@ new policy.
7872

7973
## Related tutorial
8074

81-
See {doc}`../tutorials/control-mode` for sync and async batching and lifecycle
82-
differences.
75+
See {doc}`../tutorials/control-mode` for ordered pipelining over one synchronous
76+
control client.

docs/experimental/engines/imsg.md

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,19 @@ in every
2626

2727
```python
2828
>>> from libtmux.experimental.engines import CommandRequest, ImsgEngine
29-
>>> from libtmux.experimental.engines import SubprocessEngine
29+
>>> assert server.socket_name is not None
3030
>>> assert session.session_id is not None
31-
>>> if server.socket_name is not None:
32-
... prefix = (f"-L{server.socket_name}",)
33-
... else:
34-
... socket_path = server.cmd(
35-
... "display-message", "-p", "#{socket_path}"
36-
... ).stdout[0]
37-
... prefix = (f"-S{socket_path}",)
3831
>>> request = CommandRequest.from_args(
39-
... *prefix,
32+
... f"-L{server.socket_name}",
4033
... "display-message",
4134
... "-p",
4235
... "-t",
4336
... session.session_id,
4437
... "#{session_id}",
4538
... )
46-
>>> native = ImsgEngine().run(request)
47-
>>> classic = SubprocessEngine().run(request)
48-
>>> (
49-
... native.returncode,
50-
... native.stdout == classic.stdout,
51-
... native.stdout[0] == session.session_id,
52-
... )
53-
(0, True, True)
39+
>>> result = ImsgEngine().run(request)
40+
>>> result.returncode, result.stdout == (session.session_id,)
41+
(0, True)
5442
```
5543

5644
## Lifecycle and failure boundary

docs/experimental/engines/mock.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@ permissions, shell execution, transport failures, or tmux output parsing.
1818
Pass optional canned `capture_lines`. An instance keeps only monotonic ID
1919
counters and needs no cleanup.
2020

21+
The result below is canned. The engine does not contact tmux or inspect whether
22+
the target pane exists.
23+
2124
```python
2225
>>> from libtmux.experimental.engines import MockEngine
23-
>>> from libtmux.experimental.ops import CapturePane, SplitWindow, run
24-
>>> from libtmux.experimental.ops._types import PaneId, WindowId
25-
>>> engine = MockEngine(capture_lines=("hello", "world"))
26-
>>> operation = SplitWindow(target=WindowId("@1"))
27-
>>> created = run(operation, engine)
28-
>>> created.new_pane_id, created.argv == operation.render()
29-
('%1', True)
30-
>>> run(CapturePane(target=PaneId("%1")), engine).lines
31-
('hello', 'world')
26+
>>> from libtmux.experimental.ops import CapturePane, PaneId, run
27+
>>> engine = MockEngine(capture_lines=("canned, no tmux",))
28+
>>> canned = run(
29+
... CapturePane(target=PaneId("%404")),
30+
... engine,
31+
... ).raise_for_status()
32+
>>> canned.lines
33+
('canned, no tmux',)
3234
```
3335

3436
## Lifecycle and failure boundary

docs/experimental/engines/subprocess.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ process and has no cleanup method.
2323
```python
2424
>>> from libtmux.experimental.engines import SubprocessEngine
2525
>>> from libtmux.experimental.ops import ListSessions, run
26+
>>> assert session.session_id is not None
2627
>>> engine = SubprocessEngine.for_server(server)
2728
>>> result = run(ListSessions(), engine).raise_for_status()
2829
>>> any(item.session_id == session.session_id for item in result.sessions)

docs/experimental/tutorials/async-control-plans.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ back. The pane does not exist when Python records the option operations, so
2020
{class}`~libtmux.experimental.ops._types.SlotRef` that stands in for its future
2121
ID.
2222

23+
The same deterministic story gives an agent tool three explicit phases:
24+
25+
- Observe: preview the unresolved plan and explain its two dispatch steps
26+
without contacting tmux.
27+
- Act: execute the marked fold through
28+
{class}`~libtmux.experimental.engines.async_control_mode.AsyncControlModeEngine`.
29+
- Verify: inspect four typed statuses, the captured `worker:ready` value, and
30+
the live pane lookup.
31+
2332
The two tabs show the same work at the Python and tmux boundaries. `@WINDOW`
2433
stands for the live window ID. `%PANE` stands for the pane ID captured by
2534
`split-window`.
@@ -207,6 +216,6 @@ before it can render the next target.
207216
- Scope the engine with `async with` so its reader, supervisor, and control
208217
client close before the event loop exits.
209218

210-
See {doc}`control-mode` for attachment, notification, and reconnection
211-
lifecycle details. See {doc}`results-and-failures` for typed command failures
212-
and skipped work.
219+
See {doc}`../engines/async-control-mode` for attachment, notification, and
220+
reconnection lifecycle details. See {doc}`results-and-failures` for typed
221+
command failures and skipped work.

docs/experimental/tutorials/async-subprocess.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
{class}`~libtmux.experimental.engines.asyncio.AsyncSubprocessEngine` uses
44
{func}`asyncio.create_subprocess_exec` for command process I/O.
55
{func}`~libtmux.experimental.ops.arun` preserves the same operation and typed
6-
result contract as synchronous execution.
6+
result contract as synchronous execution. The workflow below returns two typed,
7+
independent reads from one live server.
78

89
## Dispatch independent reads concurrently
910

@@ -18,8 +19,7 @@ would deliberately await them in order.
1819
```python
1920
>>> import asyncio
2021
>>> from libtmux.experimental.engines import AsyncSubprocessEngine
21-
>>> from libtmux.experimental.ops import DisplayMessage, arun
22-
>>> from libtmux.experimental.ops._types import PaneId, SessionId
22+
>>> from libtmux.experimental.ops import DisplayMessage, PaneId, SessionId, arun
2323
>>> assert session.session_id is not None
2424
>>> assert pane is not None and pane.pane_id is not None
2525
>>> engine = AsyncSubprocessEngine.for_server(server)
@@ -45,11 +45,16 @@ would deliberately await them in order.
4545
... ),
4646
... )
4747
... return (
48-
... session_result.raise_for_status().text,
49-
... pane_result.raise_for_status().text,
48+
... session_result.raise_for_status(),
49+
... pane_result.raise_for_status(),
5050
... )
51-
>>> asyncio.run(read_ids()) == (session.session_id, pane.pane_id)
52-
True
51+
>>> session_result, pane_result = asyncio.run(read_ids())
52+
>>> [type(result).__name__ for result in (session_result, pane_result)]
53+
['DisplayMessageResult', 'DisplayMessageResult']
54+
>>> session_result.status, pane_result.status
55+
('complete', 'complete')
56+
>>> session_result.text == session.session_id, pane_result.text == pane.pane_id
57+
(True, True)
5358
```
5459

5560
## Cancellation boundary

0 commit comments

Comments
 (0)