Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,20 +202,27 @@ prints a session's effective timeouts.

| Command | Description |
| ------------------------------------------------------------ | ------------------------------------------- |
| `open [--shell S] [--backend B] [--cols N --rows N] [--cwd D] [--env K=V] [--config F] [--profile P] [--timeout-<class> MS]` | Spawn a shell session. |
| `run [--backend B] [--config F] [--profile P] <program> [args...]` | Spawn a session running a program directly. |
| `open [--shell S] [--backend B] [--cols N --rows N] [--cwd D] [--env K=V] [--config F] [--profile P] [--timeout-<class> MS] [--restart]` | Spawn or reuse a shell session. |
| `run [--backend B] [--config F] [--profile P] [--restart] <program> [args...]` | Spawn or reuse a session running a program. |
| `sessions` | List active sessions. |
| `close [--all]` | Close the current session (or all). |
| `daemon start` / `daemon status` / `daemon stop --session N \| --all` | Start, inspect, or stop a session's daemon. |

Each session has its own daemon, so `daemon stop` needs `--session <name>` or
`--all`. `close` stops it too.

When a client finds a daemon from another `tui-test` version, it shuts that
daemon down and starts the current version before sending the command. The
restart is serialized per session so concurrent clients cannot race.

`open` waits for a prompt before returning, `run` does not. Override with
`--wait-ready` / `--no-wait-ready`. An explicit `--wait-ready` fails (exit 1) if
no prompt appears; `open`'s implicit wait reports `ready` in its payload either
way.

Calling `open` or `run` for a session that already has a live child reuses that
child. Pass `--restart` (or its `--force` alias) to replace it explicitly.

### Terminal backends

Choose an emulator per session with `--backend alacritty|ghostty`. Alacritty
Expand Down
10 changes: 8 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Three commands let an agent look up the rest of the surface instead of guessing:
(`assertion` / `usage` / `no_session` / `internal`).
- **Verbose.** `--verbose` / `-v` starts the daemon with a full PTY traffic log
(see [Debugging](#debugging)). Only takes effect when the daemon starts.
- **Daemon upgrades.** A client automatically replaces a daemon from another
`tui-test` version. Per-session locking prevents concurrent clients from
racing the restart.
- **Defaults.** New sessions are `80x30`. Timeouts come in five classes: `text`
and `idle` default to 5s; `command`, `exit`, and `ready` to 30s. Set a session
default with `open --timeout-<class> <ms>`, or override one call with
Expand All @@ -60,14 +63,17 @@ without parsing text:

| Command | Description |
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `open [--shell S] [--backend B] [--cols N] [--rows N] [--cwd D] [--env K=V]... [--config F] [--profile P]` | Spawn a shell session (auto-starts the daemon). `--env` is repeatable. |
| `run [--backend B] [--cols N] [--rows N] [--cwd D] [--env K=V]... [--config F] [--profile P] <program> [args...]` | Spawn a session running a program directly (no shell). |
| `open [--shell S] [--backend B] [--cols N] [--rows N] [--cwd D] [--env K=V]... [--config F] [--profile P] [--restart]` | Spawn or reuse a shell session. `--env` is repeatable. |
| `run [--backend B] [--cols N] [--rows N] [--cwd D] [--env K=V]... [--config F] [--profile P] [--restart] <program> [args...]` | Spawn or reuse a session running a program directly. |
| `sessions` | List active sessions. |
| `close [--all]` | Close the current session (or every session with `--all`). |
| `daemon start` | Start this session's daemon. Most commands start one on demand. |
| `daemon status` | Inspect a session's daemon (pid, log path). Exit 3 if none is running. |
| `daemon stop --session N \| --all` | Stop one session's daemon, or every daemon. Needs a target. |

`open` and `run` reuse an existing live child for the selected session. Pass
`--restart` (or `--force`) to replace it.

### Inspection

| Command | Description |
Expand Down
7 changes: 4 additions & 3 deletions bindings/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ All derive from `TuiTestError` and carry `kind` and `exitCode`. `waitX` and `exp
Module-level helpers: `sessions()`, `closeAll()`, `getRecording()`, `uniqueSession()`.

`open` and `run` accept
`{ backend, cols, rows, cwd, env, waitReady, retries, profile, timeouts }`.
The constructor also accepts `backend` and `profile` as defaults for later
opens and runs. Backend values are `"alacritty"` (default) and `"ghostty"`:
`{ backend, cols, rows, cwd, env, waitReady, restart, retries, profile, timeouts }`.
They reuse a live named session unless `restart: true` is passed. The
constructor also accepts `backend` and `profile` as defaults for later opens
and runs. Backend values are `"alacritty"` (default) and `"ghostty"`:

```js
const terminal = new TuiTest("work", { backend: "ghostty" });
Expand Down
2 changes: 2 additions & 0 deletions bindings/js/native/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export interface OpenOptions {
cwd?: string
env?: Array<[string, string]>
waitReady?: boolean
restart?: boolean
profileScrollback?: number
profileColors?: Array<[string, string]>
timeouts?: Timeouts
Expand Down Expand Up @@ -179,6 +180,7 @@ export interface RunOptions {
cwd?: string
env?: Array<[string, string]>
waitReady?: boolean
restart?: boolean
profileScrollback?: number
profileColors?: Array<[string, string]>
timeouts?: Timeouts
Expand Down
4 changes: 4 additions & 0 deletions bindings/js/native/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub struct OpenOptions {
pub cwd: Option<String>,
pub env: Option<Vec<(String, String)>>,
pub wait_ready: Option<bool>,
pub restart: Option<bool>,
pub profile_scrollback: Option<f64>,
pub profile_colors: Option<Vec<(String, String)>>,
pub timeouts: Option<Timeouts>,
Expand All @@ -117,6 +118,7 @@ pub struct RunOptions {
pub cwd: Option<String>,
pub env: Option<Vec<(String, String)>>,
pub wait_ready: Option<bool>,
pub restart: Option<bool>,
pub profile_scrollback: Option<f64>,
pub profile_colors: Option<Vec<(String, String)>>,
pub timeouts: Option<Timeouts>,
Expand Down Expand Up @@ -564,6 +566,7 @@ fn open_options(value: Option<OpenOptions>) -> std::result::Result<CoreOpenOptio
cwd: value.cwd,
env: value.env.unwrap_or_default(),
wait_ready: value.wait_ready,
restart: value.restart.unwrap_or(false),
timeouts: core_timeouts(value.timeouts)?,
})
}
Expand Down Expand Up @@ -592,6 +595,7 @@ fn run_options(value: RunOptions) -> std::result::Result<CoreRunOptions, TuiTest
cwd: value.cwd,
env: value.env.unwrap_or_default(),
wait_ready: value.wait_ready,
restart: value.restart.unwrap_or(false),
timeouts: core_timeouts(value.timeouts)?,
})
}
Expand Down
2 changes: 2 additions & 0 deletions bindings/js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export class TuiTest {
cwd: opts.cwd,
env: envPairs(opts.env),
waitReady: opts.waitReady,
restart: opts.restart,
profileScrollback: profile?.scrollback,
profileColors: profile?.colors,
timeouts: timeoutsPayload(opts.timeouts),
Expand All @@ -262,6 +263,7 @@ export class TuiTest {
cwd: opts.cwd,
env: envPairs(opts.env),
waitReady: opts.waitReady,
restart: opts.restart,
profileScrollback: profile?.scrollback,
profileColors: profile?.colors,
timeouts: timeoutsPayload(opts.timeouts),
Expand Down
1 change: 1 addition & 0 deletions bindings/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export interface SpawnOptions {
cwd?: string;
env?: Record<string, string | number | boolean> | [string, string][];
waitReady?: boolean;
restart?: boolean;
retries?: number;
profile?: Profile;
timeouts?: Timeouts;
Expand Down
1 change: 1 addition & 0 deletions bindings/js/test/options.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ test("constructor and per-run profile objects recolor the terminal", async () =>
await su.expectText("constructor-profile", { fg: "#010203" });

await su.run(process.execPath, argsFor("call-profile"), {
restart: true,
profile: { colors: { red: "#040506" } },
});
await su.waitText("call-profile", { timeout: 5000 });
Expand Down
9 changes: 5 additions & 4 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ All derive from `TuiTestError`. `wait_*` and `expect_*` raise `ExpectationError`

Module-level helpers: `sessions()`, `close_all()`, `get_recording()`, `unique_session()`.

`open()` and `run()` accept `backend=`, `wait_ready=`, `retries=`, `profile=`,
and `timeouts=`. The constructor also accepts `backend=` and `profile=` as
defaults for later opens and runs. Backend values are `"alacritty"` (default)
and `"ghostty"`:
`open()` and `run()` accept `backend=`, `wait_ready=`, `restart=`, `retries=`,
`profile=`, and `timeouts=`. They reuse a live named session unless
`restart=True` is passed. The constructor also accepts `backend=` and
`profile=` as defaults for later opens and runs. Backend values are
`"alacritty"` (default) and `"ghostty"`:

```python
terminal = TuiTest(backend="ghostty")
Expand Down
6 changes: 6 additions & 0 deletions bindings/python/native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ impl NativeSession {
cwd,
env,
wait_ready,
restart,
profile_scrollback,
profile_colors,
text_timeout,
Expand All @@ -78,6 +79,7 @@ impl NativeSession {
cwd: Option<String>,
env: Vec<(String, String)>,
wait_ready: Option<bool>,
restart: bool,
profile_scrollback: Option<Bound<'py, PyAny>>,
profile_colors: Vec<(String, String)>,
text_timeout: Option<Bound<'py, PyAny>>,
Expand Down Expand Up @@ -109,6 +111,7 @@ impl NativeSession {
cwd,
env,
wait_ready,
restart,
timeouts: Timeouts {
text: optional_u64(text_timeout.as_ref(), "text_timeout")?,
idle: optional_u64(idle_timeout.as_ref(), "idle_timeout")?,
Expand All @@ -132,6 +135,7 @@ impl NativeSession {
cwd,
env,
wait_ready,
restart,
profile_scrollback,
profile_colors,
text_timeout,
Expand All @@ -152,6 +156,7 @@ impl NativeSession {
cwd: Option<String>,
env: Vec<(String, String)>,
wait_ready: Option<bool>,
restart: bool,
profile_scrollback: Option<Bound<'py, PyAny>>,
profile_colors: Vec<(String, String)>,
text_timeout: Option<Bound<'py, PyAny>>,
Expand Down Expand Up @@ -184,6 +189,7 @@ impl NativeSession {
cwd,
env,
wait_ready,
restart,
timeouts: Timeouts {
text: optional_u64(text_timeout.as_ref(), "text_timeout")?,
idle: optional_u64(idle_timeout.as_ref(), "idle_timeout")?,
Expand Down
4 changes: 2 additions & 2 deletions bindings/python/src/tui_test/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class NativeSession:
@property
def name(self) -> builtins.str: ...
def __new__(cls, name: str) -> NativeSession: ...
def open(self, shell: typing.Optional[str], backend: typing.Optional[str], cols: int, rows: int, cwd: typing.Optional[str], env: typing.List[typing.Tuple[str, str]], wait_ready: typing.Optional[bool], profile_scrollback: typing.Optional[int], profile_colors: typing.List[typing.Tuple[str, str]], text_timeout: typing.Optional[int], idle_timeout: typing.Optional[int], command_timeout: typing.Optional[int], exit_timeout: typing.Optional[int], ready_timeout: typing.Optional[int]) -> typing.Awaitable[typing.Dict[str, typing.Any]]: ...
def run(self, program: str, args: typing.List[str], backend: typing.Optional[str], cols: int, rows: int, cwd: typing.Optional[str], env: typing.List[typing.Tuple[str, str]], wait_ready: typing.Optional[bool], profile_scrollback: typing.Optional[int], profile_colors: typing.List[typing.Tuple[str, str]], text_timeout: typing.Optional[int], idle_timeout: typing.Optional[int], command_timeout: typing.Optional[int], exit_timeout: typing.Optional[int], ready_timeout: typing.Optional[int]) -> typing.Awaitable[typing.Dict[str, typing.Any]]: ...
def open(self, shell: typing.Optional[str], backend: typing.Optional[str], cols: int, rows: int, cwd: typing.Optional[str], env: typing.List[typing.Tuple[str, str]], wait_ready: typing.Optional[bool], restart: bool, profile_scrollback: typing.Optional[int], profile_colors: typing.List[typing.Tuple[str, str]], text_timeout: typing.Optional[int], idle_timeout: typing.Optional[int], command_timeout: typing.Optional[int], exit_timeout: typing.Optional[int], ready_timeout: typing.Optional[int]) -> typing.Awaitable[typing.Dict[str, typing.Any]]: ...
def run(self, program: str, args: typing.List[str], backend: typing.Optional[str], cols: int, rows: int, cwd: typing.Optional[str], env: typing.List[typing.Tuple[str, str]], wait_ready: typing.Optional[bool], restart: bool, profile_scrollback: typing.Optional[int], profile_colors: typing.List[typing.Tuple[str, str]], text_timeout: typing.Optional[int], idle_timeout: typing.Optional[int], command_timeout: typing.Optional[int], exit_timeout: typing.Optional[int], ready_timeout: typing.Optional[int]) -> typing.Awaitable[typing.Dict[str, typing.Any]]: ...
def close(self) -> typing.Awaitable[None]: ...
def state(self) -> typing.Awaitable[typing.Dict[str, typing.Any]]: ...
def text(self, full: bool) -> typing.Awaitable[str]: ...
Expand Down
4 changes: 4 additions & 0 deletions bindings/python/src/tui_test/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ async def open(
cwd: Optional[str] = None,
env: EnvLike = None,
wait_ready: Optional[bool] = None,
restart: bool = False,
profile: Optional[Profile] = None,
timeouts: Optional[Timeouts] = None,
retries: int = 0,
Expand All @@ -254,6 +255,7 @@ async def open(
cwd,
env_values,
wait_ready,
restart,
*profile_values,
*timeout_values,
),
Expand All @@ -270,6 +272,7 @@ async def run(
cwd: Optional[str] = None,
env: EnvLike = None,
wait_ready: Optional[bool] = None,
restart: bool = False,
profile: Optional[Profile] = None,
timeouts: Optional[Timeouts] = None,
retries: int = 0,
Expand All @@ -291,6 +294,7 @@ async def run(
cwd,
env_values,
wait_ready,
restart,
*profile_values,
*timeout_values,
),
Expand Down
2 changes: 2 additions & 0 deletions bindings/python/stub-gen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ mod stubs {
cwd: typing.Optional[str],
env: typing.List[typing.Tuple[str, str]],
wait_ready: typing.Optional[bool],
restart: bool,
profile_scrollback: typing.Optional[int],
profile_colors: typing.List[typing.Tuple[str, str]],
text_timeout: typing.Optional[int],
Expand All @@ -119,6 +120,7 @@ mod stubs {
cwd: typing.Optional[str],
env: typing.List[typing.Tuple[str, str]],
wait_ready: typing.Optional[bool],
restart: bool,
profile_scrollback: typing.Optional[int],
profile_colors: typing.List[typing.Tuple[str, str]],
text_timeout: typing.Optional[int],
Expand Down
13 changes: 8 additions & 5 deletions bindings/python/tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def test_open_uses_typed_arguments(self):
cols=120,
rows=40,
env={"K": "V"},
restart=True,
profile=Profile(
scrollback=321,
colors=Colors(red="#010203"),
Expand All @@ -133,9 +134,10 @@ def test_open_uses_typed_arguments(self):
self.assertEqual(
args[:7], (None, None, 120, 40, None, [("K", "V")], None)
)
self.assertEqual(args[7], 321)
self.assertEqual(args[8], [("red", "#010203")])
self.assertEqual(args[9:], (100, None, None, None, 200))
self.assertTrue(args[7])
self.assertEqual(args[8], 321)
self.assertEqual(args[9], [("red", "#010203")])
self.assertEqual(args[10:], (100, None, None, None, 200))

def test_run_uses_program_and_argv(self):
terminal = _CapturingClient("s")
Expand All @@ -151,8 +153,9 @@ def test_constructor_profile_is_forwarded_to_run(self):
)
run(terminal.run("vim"))
args = terminal.fake.calls[0][1]
self.assertIsNone(args[8])
self.assertEqual(args[9], [("background", "#112233")])
self.assertFalse(args[8])
self.assertIsNone(args[9])
self.assertEqual(args[10], [("background", "#112233")])

def test_constructor_and_call_backends_are_forwarded(self):
terminal = _CapturingClient("s", backend="ghostty")
Expand Down
31 changes: 31 additions & 0 deletions crates/tui-test-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ pub enum Command {
/// Return as soon as the shell is spawned, without waiting for a prompt.
#[arg(long, conflicts_with = "wait_ready")]
no_wait_ready: bool,
/// Replace a live session instead of reusing it.
#[arg(long, visible_alias = "force")]
restart: bool,
#[command(flatten)]
profile: ProfileArgs,
#[command(flatten)]
Expand Down Expand Up @@ -208,6 +211,9 @@ pub enum Command {
/// Return as soon as the program is spawned (the default).
#[arg(long, conflicts_with = "wait_ready")]
no_wait_ready: bool,
/// Replace a live session instead of reusing it.
#[arg(long, visible_alias = "force")]
restart: bool,
#[command(flatten)]
profile: ProfileArgs,
#[command(flatten)]
Expand Down Expand Up @@ -477,6 +483,31 @@ mod tests {
));
}

#[test]
fn open_and_run_accept_restart_and_force() {
for args in [
vec!["tui-test", "open", "--restart"],
vec!["tui-test", "open", "--force"],
] {
let cli = Cli::try_parse_from(args).expect("parse open restart");
assert!(matches!(
cli.command,
Some(Command::Open { restart: true, .. })
));
}

for args in [
vec!["tui-test", "run", "--restart", "vim"],
vec!["tui-test", "run", "--force", "vim"],
] {
let cli = Cli::try_parse_from(args).expect("parse run restart");
assert!(matches!(
cli.command,
Some(Command::Run { restart: true, .. })
));
}
}

#[test]
fn open_shell_values_map_to_library_shells() {
let cases = [
Expand Down
4 changes: 4 additions & 0 deletions crates/tui-test-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ pub fn pid_file(session: &str) -> PathBuf {
home_dir().join(format!("{session}.pid"))
}

pub fn daemon_lock_file(session: &str) -> PathBuf {
home_dir().join(format!("{session}.pid.lock"))
}

pub fn log_file(session: &str) -> PathBuf {
home_dir().join(format!("{session}.log"))
}
Expand Down
Loading
Loading