Skip to content
Draft
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Waits and assertions fall into five timeout classes:

| Class | Applies to | Default |
| --- | --- | --- |
| `text` | `expect text`, `wait text` | 5000 ms |
| `text` | `expect text`, `wait text`, `wait bell`, `expect bell` | 5000 ms |
| `idle` | `wait idle` | 5000 ms |
| `command` | `wait command`, `expect exit-code` | 30000 ms |
| `exit` | `wait exit` | 30000 ms |
Expand Down Expand Up @@ -179,11 +179,11 @@ way.

| Command | Description |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `state` | cwd, size, cursor, last command + exit code, effective timeouts, text snapshot. |
| `state` | cwd, size, cursor, last command + exit code, bell count, effective timeouts, text snapshot. |
| `text [--full]` | Plain text of the viewport (or scrollback). |
| `screenshot [-o file.svg] [--full]` | Terminal text to stdout, or a crisp full-color SVG image (svg-term-style window) to a file. |
| `cells X Y [W H]` | Per-cell attributes (char, fg, bg, flags). |
| `get command\|output\|exit-code\|cwd\|cursor\|size` | Structured getters. |
| `get command\|output\|exit-code\|cwd\|cursor\|size\|bells` | Structured getters. |

`state` prints `key: value` lines then the screen; `text` and `screenshot`
print the screen bare.
Expand Down Expand Up @@ -216,6 +216,7 @@ print the screen bare.
| `wait command` | Until the current command finishes. |
| `wait exit` | Until the session exits. |
| `wait ready` | Until the shell reports a prompt. |
| `wait bell` | Until the next terminal bell event. |

### Expect (exit 0 = pass, 1 = fail)

Expand All @@ -224,6 +225,7 @@ print the screen bare.
| `expect text "T" [--regex --full --no-strict --not --fg C --bg C --timeout MS]` | Visibility + optional color. |
| `expect exit-code N [--timeout MS]` | Last command's exit code. |
| `expect output "T" [--regex]` | Last command's captured output. |
| `expect bell N [--timeout MS]` | Cumulative bell count reaches at least N. |
| `expect snapshot NAME [-u] [--include-colors]` | Compare against `__snapshots__/NAME.snap`. |

Colors accept ANSI-256 (`9`), hex (`#ff0000`), or rgb (`255,0,0`).
Expand Down
14 changes: 9 additions & 5 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ without parsing text:

| Command | Description |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `state` | cwd, size, cursor, last command + exit code, timeouts, and a text snapshot. |
| `state` | cwd, size, cursor, last command + exit code, bell count, timeouts, and a text snapshot. |
| `text [--full]` | Rendered viewport text, or full scrollback with `--full`. |
| `screenshot [PATH] [-o FILE] [--full]` | Terminal text to stdout, or a full-color SVG image (crisp at any zoom, svg-term-style window) when a path is given. |
| `cells X Y [W H]` | Per-cell attributes (char, fg, bg, flags) for a region. |
| `get command\|output\|exit-code\|cwd\|cursor\|size` | One structured field. |
| `get command\|output\|exit-code\|cwd\|cursor\|size\|bells` | One structured field. |

### Input

Expand Down Expand Up @@ -107,6 +107,7 @@ without parsing text:
| `wait command [--timeout MS]` | Until the current foreground command finishes (needs shell integration). |
| `wait exit [--timeout MS]` | Until the session's program/shell itself exits. |
| `wait ready [--timeout MS]` | Until the shell reports a ready prompt (needs shell integration). `open` waits by default. |
| `wait bell [--timeout MS]` | Until the next terminal bell event. |

### Expect (exit 0 = pass, 1 = fail)

Expand All @@ -115,6 +116,7 @@ without parsing text:
| `expect text "T" [--regex --full --no-strict --not --fg C --bg C --timeout MS]` | Visibility plus optional color. `--no-strict` relaxes a strict single-match. |
| `expect exit-code N [--timeout MS]` | The last command's exit code. Waits for the command to finish first. |
| `expect output "T" [--regex]` | The last command's captured output. |
| `expect bell N [--timeout MS]` | The cumulative bell count reaches at least N. |
| `expect snapshot NAME [-u] [--include-colors]` | Compare the screen against `__snapshots__/NAME.snap`; `-u` writes/updates it. |

Colors accept ansi-256 (`9`), hex (`#ff0000`), or rgb (`255,0,0`).
Expand Down Expand Up @@ -284,10 +286,12 @@ await su.close();
Methods mirror the cli commands: `open` / `run`, `submit` / `type` / `write`,
`press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` /
`kill`, `state`, `text`, `cells`, the dedicated `get_command` / `get_output` /
`get_exit_code` / `get_cwd` / `get_cursor` / `get_size` methods,
`get_exit_code` / `get_cwd` / `get_cursor` / `get_size` / `get_bell_count`
methods,
`screenshot`, `wait_text` / `wait_idle` / `wait_command` / `wait_exit` /
`wait_ready`, `expect_text` / `expect_exit_code` / `expect_output` /
`expect_snapshot`, and `close`. Python module-level helpers are `sessions`,
`wait_ready` / `wait_bell`, `expect_text` / `expect_exit_code` /
`expect_output` / `expect_bell_count` / `expect_snapshot`, and `close`.
Python module-level helpers are `sessions`,
`close_all`, and `get_recording`; JavaScript exports `sessions`, `closeAll`,
and `getRecording`. The JavaScript client otherwise uses the same names in
camelCase (`waitCommand`, `expectText`, `getExitCode`, etc.).
Expand Down
2 changes: 1 addition & 1 deletion bindings/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ All derive from `ShellUseError` and carry `kind` and `exitCode`. `waitX` and `ex

## API

`new ShellUse(session?, { timeouts?, artifacts? })` mirrors the cli: `open` / `run`, `type` / `write`, `submit`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `getCommand` / `getOutput` / `getExitCode` / `getCwd` / `getCursor` / `getSize`, `screenshot`, `waitText` / `waitIdle` / `waitCommand` / `waitExit` / `waitReady`, `expectText` / `expectExitCode` / `expectOutput` / `expectSnapshot`, `close`, and `closeQuiet`.
`new ShellUse(session?, { timeouts?, artifacts? })` mirrors the cli: `open` / `run`, `type` / `write`, `submit`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `getCommand` / `getOutput` / `getExitCode` / `getCwd` / `getCursor` / `getSize` / `getBellCount`, `screenshot`, `waitText` / `waitIdle` / `waitCommand` / `waitExit` / `waitReady` / `waitBell`, `expectText` / `expectExitCode` / `expectOutput` / `expectBellCount` / `expectSnapshot`, `close`, and `closeQuiet`.

Module-level helpers: `sessions()`, `closeAll()`, `getRecording()`, `uniqueSession()`.

Expand Down
4 changes: 4 additions & 0 deletions bindings/js/native/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export declare class NativeSession {
getCwd(): Promise<string | null>
getCursor(): Promise<Cursor>
getSize(): Promise<Size>
getBellCount(): Promise<number>
write(data: string): Promise<void>
type(text: string): Promise<void>
submit(data?: string | undefined | null): Promise<void>
Expand All @@ -33,9 +34,11 @@ export declare class NativeSession {
waitCommand(timeoutMs?: number | undefined | null): Promise<void>
waitExit(timeoutMs?: number | undefined | null): Promise<void>
waitReady(timeoutMs?: number | undefined | null): Promise<void>
waitBell(timeoutMs?: number | undefined | null): Promise<void>
expectText(text: string, options?: ExpectTextOptions | undefined | null): Promise<void>
expectExitCode(code: number, timeoutMs?: number | undefined | null): Promise<void>
expectOutput(text: string, regex?: boolean | undefined | null): Promise<void>
expectBellCount(count: number, timeoutMs?: number | undefined | null): Promise<void>
snapshot(name: string, options?: SnapshotOptions | undefined | null): Promise<SnapshotResult>
screenshot(options?: ScreenshotOptions | undefined | null): Promise<string>
panicProbe(): Promise<void>
Expand Down Expand Up @@ -189,6 +192,7 @@ export interface State {
last_exit: number | null
exited: number | null
ready: boolean
bell_count: number
timeouts: EffectiveTimeouts
text: string
}
Expand Down
41 changes: 41 additions & 0 deletions bindings/js/native/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ pub struct State {
pub last_exit: Option<i32>,
pub exited: Option<i32>,
pub ready: bool,
#[napi(js_name = "bell_count")]
pub bell_count: f64,
pub timeouts: EffectiveTimeouts,
pub text: String,
}
Expand All @@ -181,6 +183,7 @@ impl From<CoreState> for State {
last_exit: value.last_exit,
exited: value.exited,
ready: value.ready,
bell_count: value.bell_count as f64,
timeouts: value.timeouts.into(),
text: value.text,
}
Expand Down Expand Up @@ -713,6 +716,20 @@ impl NativeSession {
.await
}

#[napi]
pub async fn get_bell_count(&self) -> Result<f64> {
execute(
self.handle.clone(),
"getBellCount",
Operation::GetBellCount,
|result| match result {
OperationResult::BellCount(value) => Ok(value as f64),
_ => Err(unexpected("getBellCount")),
},
)
.await
}

#[napi]
pub async fn write(&self, data: String) -> Result<()> {
self.unit("write", Operation::Write { data }).await
Expand Down Expand Up @@ -929,6 +946,14 @@ impl NativeSession {
.await
}

#[napi]
pub async fn wait_bell(&self, timeout_ms: Option<f64>) -> Result<()> {
self.timeout_unit("waitBell", timeout_ms, |timeout_ms| Operation::WaitBell {
timeout_ms,
})
.await
}

#[napi]
pub async fn expect_text(
&self,
Expand Down Expand Up @@ -992,6 +1017,22 @@ impl NativeSession {
.await
}

#[napi]
pub async fn expect_bell_count(&self, count: f64, timeout_ms: Option<f64>) -> Result<()> {
let handle = self.handle.clone();
blocking("expectBellCount", move || {
let operation = Operation::ExpectBellCount {
count: integer(count, "count", u64::MAX)?,
timeout_ms: timeout(timeout_ms, "timeoutMs")?,
};
match handle.execute(operation)? {
OperationResult::Unit => Ok(()),
_ => Err(unexpected("expectBellCount")),
}
})
.await
}

#[napi]
pub async fn snapshot(
&self,
Expand Down
16 changes: 16 additions & 0 deletions bindings/js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,10 @@ export class ShellUse {
return this.#runtime.getSize();
}

async getBellCount(): Promise<number> {
return this.#runtime.getBellCount();
}

async screenshot(path: string | null = null, opts: { full?: boolean } = {}): Promise<string> {
return this.#runtime.screenshot({
full: opts.full ?? false,
Expand Down Expand Up @@ -355,6 +359,12 @@ export class ShellUse {
);
}

async waitBell(opts: { timeout?: number } = {}): Promise<void> {
await this.#guard("waitBell", () =>
this.#runtime.waitBell(this.#timeout("text", opts.timeout)),
);
}

async expectText(text: string, opts: ExpectTextOptions = {}): Promise<void> {
await this.#guard("expectText", () =>
this.#runtime.expectText(text, {
Expand All @@ -381,6 +391,12 @@ export class ShellUse {
);
}

async expectBellCount(count: number, opts: { timeout?: number } = {}): Promise<void> {
await this.#guard("expectBellCount", () =>
this.#runtime.expectBellCount(count, this.#timeout("text", opts.timeout)),
);
}

async expectSnapshot(
name: string,
opts: { update?: boolean; includeColors?: boolean } = {},
Expand Down
12 changes: 12 additions & 0 deletions bindings/js/src/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ export class NativeRuntime {
return this.#call((session) => session.getSize());
}

getBellCount(): Promise<number> {
return this.#call((session) => session.getBellCount());
}

write(data: string): Promise<void> {
return this.#call((session) => session.write(data));
}
Expand Down Expand Up @@ -273,6 +277,10 @@ export class NativeRuntime {
return this.#call((session) => session.waitReady(timeoutMs));
}

waitBell(timeoutMs?: number): Promise<void> {
return this.#call((session) => session.waitBell(timeoutMs));
}

expectText(text: string, options?: ExpectTextOptions): Promise<void> {
return this.#call((session) => session.expectText(text, options));
}
Expand All @@ -285,6 +293,10 @@ export class NativeRuntime {
return this.#call((session) => session.expectOutput(text, regex));
}

expectBellCount(count: number, timeoutMs?: number): Promise<void> {
return this.#call((session) => session.expectBellCount(count, timeoutMs));
}

async snapshot(name: string, options?: SnapshotOptions): Promise<string> {
return this.#call((session) => session.snapshot(name, options));
}
Expand Down
30 changes: 30 additions & 0 deletions bindings/js/test/integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ const evalArgs =
typeof globalThis.Deno === "undefined"
? ["-e", "console.log('ready'); setInterval(() => {}, 1000)"]
: ["eval", "console.log('ready'); setInterval(() => {}, 1000)"];
const twoBellsCommand =
process.platform === "win32"
? "[Console]::Out.Write([char]7); [Console]::Out.Write([char]7)"
: "printf '\\a\\a'";
const delayedBellCommand =
process.platform === "win32"
? "Start-Sleep -Seconds 1; [Console]::Out.Write([char]7)"
: "sleep 1; printf '\\a'";

test("echo roundtrip drives a real session", async () => {
await withTerminal({ shell }, async (su) => {
Expand Down Expand Up @@ -56,6 +64,28 @@ test("echo roundtrip drives a real session", async () => {
});
});

test("bell state, waits, and expectations stay consistent", async () => {
const su = ShellUse.ephemeral("bell-events");

try {
await su.open({ shell });
await su.submit(twoBellsCommand);
await su.expectBellCount(2, { timeout: 5000 });
await su.waitCommand();

assert.equal((await su.state()).bell_count, 2);
assert.equal(await su.getBellCount(), 2);
assert.equal(await su.getBellCount(), 2);

await su.submit(delayedBellCommand);
await su.waitBell({ timeout: 5000 });
await su.expectBellCount(3);
assert.equal(await su.getBellCount(), 3);
} finally {
await su.closeQuiet();
}
});

test(
"assertion errors include the current terminal",
async () => {
Expand Down
2 changes: 1 addition & 1 deletion bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ All derive from `ShellUseError`. `wait_*` and `expect_*` raise `ExpectationError

## API

`ShellUse(session="default", *, timeouts=None, artifacts=None)` mirrors the cli: `open` / `run`, `type` / `write`, `submit`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `get_command` / `get_output` / `get_exit_code` / `get_cwd` / `get_cursor` / `get_size`, `screenshot`, `wait_text` / `wait_idle` / `wait_command` / `wait_exit` / `wait_ready`, `expect_text` / `expect_exit_code` / `expect_output` / `expect_snapshot`, `close`, and `close_quiet`.
`ShellUse(session="default", *, timeouts=None, artifacts=None)` mirrors the cli: `open` / `run`, `type` / `write`, `submit`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `get_command` / `get_output` / `get_exit_code` / `get_cwd` / `get_cursor` / `get_size` / `get_bell_count`, `screenshot`, `wait_text` / `wait_idle` / `wait_command` / `wait_exit` / `wait_ready` / `wait_bell`, `expect_text` / `expect_exit_code` / `expect_output` / `expect_bell_count` / `expect_snapshot`, `close`, and `close_quiet`.

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

Expand Down
Loading
Loading