Metadata-Version: 2.4
Name: mainframe-mocker
Version: 0.1.2
Summary: Build mock IBM mainframe terminal applications (TN3270 and TN5250) and test terminal clients against them
Keywords: 3270,tn3270,5250,tn5250,mainframe,mock,terminal,emulator,testing,cics,ibm-i
Author: Ben Goldsmith
Author-email: Ben Goldsmith <ben.goldsmith@clearroute.io>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Software Development :: Testing :: Mocking
Classifier: Topic :: Terminals :: Telnet
Classifier: Typing :: Typed
Requires-Dist: jinja2>=3.1
Requires-Python: >=3.13
Project-URL: Homepage, https://github.com/bengieeee/mainframe-mocker
Project-URL: Documentation, https://github.com/bengieeee/mainframe-mocker#readme
Project-URL: Source, https://github.com/bengieeee/mainframe-mocker
Project-URL: Issues, https://github.com/bengieeee/mainframe-mocker/issues
Description-Content-Type: text/markdown

# mainframe-mocker

[![PyPI](https://img.shields.io/pypi/v/mainframe-mocker)](https://pypi.org/project/mainframe-mocker/)
[![Python](https://img.shields.io/pypi/pyversions/mainframe-mocker)](https://pypi.org/project/mainframe-mocker/)
[![CI](https://github.com/bengieeee/mainframe-mocker/actions/workflows/publish.yml/badge.svg)](https://github.com/bengieeee/mainframe-mocker/actions/workflows/publish.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/LICENSE)

**Mock IBM mainframe green-screen hosts in plain Python.** You write the screens; `mfmock`
speaks the wire protocol. Real terminal emulators (x3270, c3270, s3270, tn5250, Reflection,
ZOC...), RPA bots and screen-scraping libraries connect to your mock as if it were a CICS,
TSO or IBM i host.

Use it to:

- **test terminal emulators and 3270/5250 client libraries** against a host you control, with
  every screen size and colour model, TN3270E on or off, and TLS;
- **run integration and E2E tests** for RPA bots and screen-scraping code in CI, with no
  mainframe, no VPN and no shared test region;
- **rebuild a real host flow from a recorded session** and check the mock against that
  recording;
- **demo and train** on a mainframe flow that anybody can run on a laptop.

Features:

- **TN3270 / TN3270E** (models 2-5, mono and colour, TLS) and **TN5250** (IBM i, 24x80 and
  27x132, mono and colour, TLS). The same app serves both.
- **Screens are Jinja2 pictures:** a template looks like the screen, and Python keeps the
  behaviour.
- **A test kit:**
  - an in-memory virtual terminal that speaks real protocol bytes;
  - a TCP client and an ephemeral-port server;
  - a pytest plugin that runs each test over both protocols;
  - a driver for the real `s3270` emulator;
  - replay of recorded sessions.
- **Small footprint:** pure Python 3.13+, needing only the standard library and Jinja2.

## Contents

- [Install](#install)
- [Try it in 30 seconds](#try-it-in-30-seconds)
- [A mock in five minutes](#a-mock-in-five-minutes)
- [Testing with mfmock](#testing-with-mfmock)
- [End-to-end testing of real clients](#end-to-end-testing-of-real-clients)
- [Concepts](#concepts)
- [Screen templates](#screen-templates)
- [Mocks from recordings](#mocks-from-recordings)
- [Command line and environment](#command-line-and-environment)
- [Protocols](#protocols)
- [Architecture](#architecture)
- [For AI agents and coding tools](#for-ai-agents-and-coding-tools)
- [Roadmap](#roadmap)
- [Development](#development)

More detail is in [`docs/`](https://github.com/bengieeee/mainframe-mocker/tree/HEAD/docs/): [templates](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/templates.md), [testing](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/testing.md),
[recordings](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/recordings.md), [protocols](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/protocols.md) and the [API](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/api.md).

## Install

```sh
pip install mainframe-mocker        # or: uv add mainframe-mocker
```

The distribution is `mainframe-mocker`, the import package is `mfmock`, and the command is
`mfmock`. Installing it also registers a pytest plugin (see [Testing](#testing-with-mfmock)).

## Try it in 30 seconds

`mfmock demo` serves a small built-in host (sign-on, menu, a form, a paged list and a field
showcase):

```sh
mfmock demo                        # TN3270 on port 3270
c3270 localhost:3270               # sign on as DEMO / DEMO

mfmock demo --protocol 5250        # TN5250 on port 5250
tn5250 localhost:5250

mfmock demo --port 2323 --users alice:secret,bob:hunter2 --trace --log-level DEBUG
```

The demo has these screens and transaction codes:

| Screen         | Transaction      | Shows                                                              |
| -------------- | ---------------- | ------------------------------------------------------------------ |
| Sign-on        |                  | an input field, a hidden password field, error messages            |
| Main menu      | `MENU`           | a numbered menu                                                    |
| Greeting       | `HELO`           | a form and a message                                               |
| Guest book     | `LIST`           | a paged list (PF7 and PF8)                                         |
| Field showcase | `FLDS`           | every colour, highlight, numeric field, hidden field and validator |
| Help, sign-off | `PF1`, `SIGNOFF` |                                                                    |

Point an emulator, a bot or an E2E suite at it before you write a mock of your own.

## A mock in five minutes

```python
# hello.py
from mfmock import Application, Exit, MessageCatalog, Screen, Stay, msg, run

app = Application(
    "HELLO",
    start="MAIN",
    catalog=MessageCatalog(prefix="HEL", messages={"GREETED": ("HEL0100I", "HELLO {name}")}),
    pf_legend="ENTER=Greet  PF3=Exit",
)


@app.screen
class Main(Screen):
    name = tran = "MAIN"
    title = "HELLO MAINFRAME"

    template_source = """\
{{ row(5) }}{{ prompt(col=2) }}YOUR NAME . . : {{ input('name', 20, label='NAME', validators=[required]) }}
{{ row(7) }}{{ prompt(col=2) }}GREETING  . . : {{ output('greeting', 50) }}
"""                                       # or template = "main.j2", in a templates/ directory

    def enter(self):                      # plain sync code; values are upper-cased text
        self.values["greeting"] = f"HELLO, {self.v('name')}!"
        return Stay(msg("GREETED", name=self.v("name")))

    def pf(self, n):
        return Exit() if n == 3 else None  # None falls through to the app's PF defaults


if __name__ == "__main__":
    run(app, protocols=("tn3270", "tn5250"))  # --protocol, --port, --trace, --tls-*, env vars
```

```sh
python hello.py --port 3270 &      # then: c3270 localhost:3270
```

The frame adds the header (system ID, title, user, clock), the `TRAN:` command line, the
message line and the key legend. This is [`examples/hello.py`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/examples/hello.py); its tests
are in [`examples/test_hello.py`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/examples/test_hello.py).

## Testing with mfmock

### The pytest plugin

Installing `mainframe-mocker` registers a pytest plugin. Its fixtures run each test once per
protocol:

```python
from hello import app


def test_greets(mf_terminal):            # runs as test_greets[3270] and test_greets[5250]
    t = mf_terminal(app)                 # an in-memory terminal: real protocol bytes, no sockets
    t.enter(your_name="ada")             # fill the field captioned "YOUR NAME", press Enter
    assert t.message == "HEL0100I HELLO ADA"
    assert "HELLO, ADA!" in t.text()
    t.pf(3)
    assert t.closed


def test_over_tcp(mf_server):
    server = mf_server(app)              # listening on 127.0.0.1:<free port> for this test
    t = server.connect()                 # a TCP client with full telnet negotiation
    assert "HELLO MAINFRAME" in t.row(1)
```

| Fixture, option or marker                    | What it does                                                                                                                                        |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mf_terminal(app, terminal_type=None, **kw)` | opens an in-memory `VirtualTerminal`. The default terminal is 24x80 colour (`IBM-3279-2-E` or `IBM-3179-2`). `kw` takes `device_name=` and `peer=`. |
| `mf_server(app, config=None)`                | serves the app on TCP. Returns an `MfServer` with `host`, `port`, `address` (`"host:port"`) and `connect(terminal_type=None)`.                      |
| `mf_protocol`                                | `"tn3270"` or `"tn5250"`, the protocol of this run                                                                                                  |
| `--mf-protocol 3270`                         | chooses the protocols for the run: `3270`, `5250` or `3270,5250` (the default)                                                                      |
| `@pytest.mark.mf_protocols("5250")`          | limits one test to some protocols                                                                                                                   |

Terminals and servers are closed when the test ends.

### The VirtualTerminal API

| Read the screen                                                                                                    | Act                                                                              |
| ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `text()`, `lines()`, `row(n)` (1-based)                                                                            | `fill(target, value)`                                                            |
| `find(text)` gives `(row, col)` or `None`                                                                          | `enter(**fill)`: keyword names become captions (`your_name` matches `YOUR NAME`) |
| `assert_text(*texts)`                                                                                              | `press(Key.PF5, **fill)`, `pf(n)`, `pa(n)`, `clear()`                            |
| `message`: the message line                                                                                        | `type(text)`, `move_cursor(row, col)`                                            |
| `alarm`: whether the last screen sounded the alarm                                                                 | `tran(code)`: type a transaction code and press Enter                            |
| `cursor`, `rows`, `cols`, `closed`                                                                                 | `close()`                                                                        |
| `fields`, `all_fields`, `field(target)` give a `FieldView` (`value`, `hidden`, `numeric`, `protected`, `color`...) |                                                                                  |

A `target` is a caption on the screen (the first input field after it on the same row), a
1-based `(row, col)` inside the field, or the field's index.

### Without pytest

The same terminals come from the protocol packages:

```python
from mfmock import ServerConfig
from mfmock.core.testing import running_server
from mfmock.tn3270 import TN3270Adapter
from mfmock.tn3270.testing import connect, virtual_terminal

from hello import app

with virtual_terminal(app, "IBM-3278-5") as t:         # 27x132; any 3278/3279 model works
    t.enter(your_name="grace")
    assert "HELLO, GRACE!" in t.text()

config = ServerConfig(host="127.0.0.1", port=0)        # port 0: any free port
with running_server(TN3270Adapter(), app, config) as server:
    with connect("127.0.0.1", server.port, "IBM-3279-2-E") as t:
        assert "HELLO MAINFRAME" in t.row(1)
```

`mfmock.tn5250.testing` has the same `virtual_terminal` and `connect`. Its default terminal
is `IBM-3179-2`, and `connect` takes `device_name=`.

## End-to-end testing of real clients

The mock is a real TN3270 or TN5250 server, so any client can be tested against it: an
emulator, an RPA bot, a `py3270` or `tnz` script, or your own client library.

**From pytest:** serve the app with `mf_server` and give the client `server.host` and
`server.port`. The built-in driver for the real `s3270` emulator shows the pattern:

```python
import pytest

from mfmock.demo import create_app
from mfmock.tn3270.s3270 import S3270


@pytest.mark.mf_protocols("3270")
@pytest.mark.skipif(not S3270.available(), reason="s3270 not installed")
def test_real_emulator_signs_on(mf_server):
    server = mf_server(create_app())
    emulator = S3270()                        # or your client under test
    try:
        emulator.connect(server.host, server.port)
        emulator.type("demo")
        emulator.cmd("Tab()")
        emulator.type("demo")
        assert "MAIN MENU" in emulator.enter()
    finally:
        emulator.close()
```

**From any language or CI system:** run the host as a process or a service:

```sh
mfmock demo --host 127.0.0.1 --port 3270 &      # or: python my_mock.py --port 3270 &
MFMOCK_PORT=3270 MFMOCK_PROTOCOL=5250 mfmock demo
```

```yaml
# GitHub Actions: a mock host for the job's E2E tests
- run: pip install mainframe-mocker
- run: mfmock demo --port 3270 &
- run: npm run test:e2e # your client connects to localhost:3270
```

**What to test for:**

- Screen sizes and colour models. Pass a terminal type:
  - 3270: `IBM-3278-2` to `-5` (mono), `IBM-3279-2` to `-5` (colour), `-E` for extended
    attributes, and `IBM-DYNAMIC` (the size comes from the Query Reply);
  - 5250: `IBM-3179-2`, `IBM-3477-FC` (27x132) and others.
- `--no-tn3270e` for clients that should fall back to plain TN3270.
- TLS: `--tls-port 992 --tls-cert cert.pem --tls-key key.pem`.
- Idle timeouts: `--idle-timeout 30`.
- Errors and the alarm: send an invalid entry and an unknown transaction.
- Disconnects: `Exit()` closes the session.
- `--trace` hex-dumps every record, to debug a client byte by byte.
- Each connection gets a device or LU name (`ServerConfig(device_prefix=...)` or
  `device_names=`). Its logs are `key=value` lines tagged `lu=...`.

## Concepts

| Concept                                                                                                                            | In short                                                                                                                                                                                                                 |
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Application(name, start=, catalog=, frame=, theme=, templates=, idle_timeout=, signoff_trans=, default_access=, pf_legend=, ...)` | the host. `@app.screen` registers a screen and its transaction code. Subclass it to override hooks.                                                                                                                      |
| `Screen`                                                                                                                           | one screen. Class attributes: `name`, `tran`, `title`, `access`, `help_text`, `pf_legend`, `default_cursor`, `template` or `template_source`.                                                                            |
| Handlers                                                                                                                           | `setup()`, `refresh()`, `resume()`, `enter()`, `pf(n)`, `scroll(delta)`, `clear()`, `pa(n)`. Each returns a Result, or `None` for the app's default behaviour.                                                           |
| Results                                                                                                                            | `Stay(msg, cursor=)`, `Goto(name, args)`, `Back()`, `Home()`, `Restart()` (sign off) and `Exit()` (disconnect)                                                                                                           |
| `self.values`, `self.v(name)`, `self.state`                                                                                        | the screen's field values (typed text is upper-cased unless `upper=False`), one value (`""` if unset), the per-connection state                                                                                          |
| Errors                                                                                                                             | `raise ScreenError("REQUIRED", cursor="name", field="NAME")` from anywhere. Other exceptions show `...9999E UNEXPECTED ERROR - REFERENCE <ref>` and are logged with the reference.                                       |
| `MessageCatalog(prefix, messages)`                                                                                                 | message IDs `{prefix}nnnn{I,W,E}`, with library defaults (`REQUIRED`, `INVALID_FIELD`, `TRAN_UNKNOWN`, `NOT_AUTHORISED`, `TOP`, `BOTTOM`...) that you can override by ID. Use `msg("ID", **params)`.                     |
| Validators                                                                                                                         | `required`, `digits(n=None)`, `one_of(*options)`, `matches(regex)`, or any `(value, field) -> MessageRef \| None`                                                                                                        |
| Widgets                                                                                                                            | `MenuScreen` (`options`), `ListScreen` (`fetch(offset, limit)`, paging, a selection column), `HelpScreen`, `GoodbyeScreen`                                                                                               |
| Frame and theme                                                                                                                    | `StandardFrame(sysid=...)` draws the chrome, and `PlainFrame` draws none. A `Theme` maps semantic styles (`label`, `input`, `msg_error`...) to a `Style(intensity, color, highlight)`, which degrades on mono terminals. |
| App hooks                                                                                                                          | `create_state(conv)`, `authorize(state, requirement)`, `on_restart(conv)`, `on_end(conv)`, `user_label(conv)`, `clock()`                                                                                                 |
| Below the UI                                                                                                                       | any object with `async handle(session)` can be served. Use `session.send(bytes)`, `await session.receive(timeout)` and `session.terminal`.                                                                               |

[`src/mfmock/demo`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/src/mfmock/demo/__init__.py) is a compact example of most of these.

## Screen templates

A template is a Jinja2 picture of the screen body: each rendered line is a row, and tags mark
where protected text and fields begin.

```jinja
{{ row(3) }}{{ center('*** SIGN ON ***') }}
{{ row(6) }}{{ prompt(col=17) }}USER ID  . . . . :{{ input('userid', 8, col=36, validators=[required]) }}
{{ prompt(col=17) }}PASSWORD . . . . :{{ input('password', 8, col=36, hidden=True) }}
{% for i in range(rows - 20) %}
{{ row(10 + i) }}{{ output('line_' ~ i, cols - 2, col=2) }}
{% endfor %}
```

| Tag                                             | Meaning                                                                                                            |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `row(n)`                                        | at the start of a line: this line is row `n`                                                                       |
| `text(style='label', col=, length=)`            | protected text up to the next tag                                                                                  |
| `prompt(...)`                                   | a `text` that captions the next field, which is also the field's label in messages                                 |
| `input(name, length, col=, ...)`, `output(...)` | a field. Options: `style`, `numeric`, `hidden`, `upper`, `align`, `initial`, `label`, `validators`, `wrap`, `end`. |
| `center(text, style='title')`                   | centred text                                                                                                       |

- Templates see `rows`, `cols`, `screen`, the validators, `ljust` and `rjust`. One template
  serves every screen size, and is compiled once per size.
- Mistakes raise `TemplateLayoutError` naming the template, row and column. Examples are an
  overlap, a field off the screen, or text without a tag.
- Preview a template, with a column ruler and its fields:

```sh
mfmock preview templates/signon.j2 --size 27x132 --set user='"ADA"'
```

The library's own templates are `mfmock/menu.j2`, `list.j2`, `help.j2` and `goodbye.j2`, with
macros in `widgets.j2`. See [docs/templates.md](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/templates.md).

## Mocks from recordings

To mock a real host, record a session with an emulator that saves an HTML session (capture)
report. Then rebuild the host screen by screen:

```sh
mfmock capture flow.html            # every screen, entry and key, numbered
mfmock capture flow.html --code 18  # a starting template for screen #18
```

Check the mock against the recording. `replay` types the recorded data, presses the recorded
keys and compares every screen: text, cursor and the field map.

<!-- readme-test: compile-only (needs a recording) -->

```python
from mfmock.core.capture import load_capture
from mfmock.core.testing import mask_row, replay
from mfmock.tn3270.testing import virtual_terminal


def test_matches_recording():
    with virtual_terminal(app, device_name="TCPD00HK") as t:
        replay(t, load_capture("tests/captures/flow.html"),
               variables={"password": "SECRET"},            # values the recording hides
               masks=[r"\d\d:\d\d:\d\d", mask_row(11, 51)])  # clocks, client IP
```

The first screen that differs raises `ReplayMismatch`, which lists the differences. See
[docs/recordings.md](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/recordings.md).

## Command line and environment

`mfmock.run(app, argv=None, protocols=None, prog=, description=, env_prefix="MFMOCK_", config=ServerConfig(...), add_arguments=)`
gives every mock these flags. `mfmock demo` has them too.

| Flag                                                          | Environment variable                        | Default                                                                 |
| ------------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
| `--protocol {3270,5250}` (when there are several `protocols`) | `<PREFIX>PROTOCOL`                          | the first protocol                                                      |
| `--host`, `--port`                                            | `<PREFIX>HOST`, `<PREFIX>PORT`              | `0.0.0.0`; `3270` (`5250` for TN5250 when it is not the first protocol) |
| `--log-level`                                                 | `<PREFIX>LOG_LEVEL`                         | `INFO`                                                                  |
| `--idle-timeout SECONDS`                                      | `<PREFIX>IDLE_TIMEOUT`                      | the app's                                                               |
| `--trace` (hex-dump records)                                  | `<PREFIX>TRACE`                             | off                                                                     |
| `--tls-port`, `--tls-cert`, `--tls-key`                       | `<PREFIX>TLS_PORT`, `_TLS_CERT`, `_TLS_KEY` | off                                                                     |
| `--no-tn3270e`                                                | `<PREFIX>TN3270E=false`                     | TN3270E offered                                                         |
| `--codepage` (TN5250)                                         | `<PREFIX>CODEPAGE`                          | `cp037`                                                                 |

Other `mfmock` commands:

| Command                                                | What it does                                                        |
| ------------------------------------------------------ | ------------------------------------------------------------------- |
| `mfmock demo [--users U:P,...]`                        | serves the demo host                                                |
| `mfmock preview FILE.j2 [--size RxC] [--set key=json]` | prints a template's picture and fields                              |
| `mfmock capture FILE.html [--code SEQ]`                | summarises a recording, or writes a template for one of its screens |
| `mfmock --version`                                     | prints the version                                                  |

## Protocols

|               | TN3270 / TN3270E                                                         | TN5250                                                                 |
| ------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| Package       | `mfmock.tn3270`                                                          | `mfmock.tn5250`                                                        |
| Negotiation   | TN3270E (device type, LU name, functions) or plain TN3270; Query Reply   | NEW-ENVIRON (optional), TERMINAL-TYPE, BINARY, EOR                     |
| Terminals     | `IBM-3278-2` to `-5`, `IBM-3279-2` to `-5`, `-E` variants, `IBM-DYNAMIC` | `IBM-3179-2`, `IBM-5555-C01`, `IBM-5251-11`, `IBM-3477-FC` (27x132)... |
| Keys          | Enter, PF1-24, PA1-3, Clear                                              | Enter, F1-24, Roll Up/Down, Clear, Help, Print, Attn, SysReq           |
| Not supported | printer sessions, DBCS                                                   | the 5250 query, Write Error Code, auto sign-on, DBCS, printers         |

An app runs unchanged on both protocols. On 5250, an input field cannot start at row 1,
column 1. See [docs/protocols.md](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/protocols.md).

## Architecture

```
your app        Application · @app.screen class X(Screen) · templates · catalog
mfmock.core.ui  Conversation (navigation, keys, TRAN routing, timeouts, errors) · Layout · Frame · Theme
                ─────────────── ScreenRenderer (seam) ───────────────
mfmock.tn3270   Layout → 3270 orders          mfmock.tn5250   Layout → 5250 WTD orders
                ─────────────── ProtocolAdapter (seam) ──────────────
mfmock.tn3270   TN3270(E) negotiation         mfmock.tn5250   NEW-ENVIRON / TTYPE negotiation
mfmock.core     MainframeServer · DeviceSession (TCP, TLS, LU names, logs, trace) · telnet · EBCDIC
```

These rules are enforced by `tests/test_architecture.py`:

- `mfmock.core` never imports a protocol package.
- The library imports only the standard library and `jinja2`.
- There is no global state, so several apps and protocols can run in one process.

## For AI agents and coding tools

- [`AGENTS.md`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/AGENTS.md) is for agents working on this repository: the layout, commands,
  rules and pitfalls. [`.github/copilot-instructions.md`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/.github/copilot-instructions.md)
  points to it.
- [`llms.txt`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/llms.txt) is a short, link-indexed summary for LLMs writing code that uses
  `mfmock`.
- To help agents in your own project, copy the
  ["Using mfmock from another project"](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/AGENTS.md#using-mfmock-from-another-project) snippet
  into its agent instructions.

## Roadmap

These ideas would make `mfmock` more useful for integration and E2E testing. Contributions
are welcome.

1. **`mfmock serve-capture flow.html`:** serve a recording as a scripted mock, with no Python
   (each screen follows the recorded keys).
2. **Declarative mocks:** screens and transitions in YAML or JSON, so teams that don't write
   Python can define hosts.
3. **Fault injection:**
   - latency and keyboard-lock delays;
   - forced disconnects;
   - refused or partial TN3270E negotiation, rejected terminal types;
   - malformed or oversized records.

   These test how a client handles errors.

4. **Session recording:**
   - write every record and screen to JSON Lines;
   - `replay` that format;
   - a pass-through proxy that records a real host.
5. **Assertions that wait:**
   - `wait_for_text(text, timeout)` and `wait_until(predicate)` for hosts that update the
     screen on their own;
   - `snapshot()` with golden files;
   - richer `ReplayMismatch` diffs.
6. **Async API:** an `AsyncVirtualTerminal` and an async `running_server` for asyncio test
   suites.
7. **Reach:**
   - support Python 3.11+;
   - a Docker image running `mfmock demo`;
   - a list of clients tested against `mfmock`.

## Development

```sh
uv sync                            # Python 3.13+, installs pytest and ruff
uv run pytest -q                   # unit tests, examples and the README's samples
uv run pytest -q -m integration    # with a real emulator (needs s3270: brew install x3270)
uv run ruff check .
npx prettier --check .            # Markdown and YAML formatting (--write to fix)
uv build
```

Every push runs the tests, Ruff and Prettier (`npx prettier --check .`). Pushes to `main`
publish a release to PyPI and pushes to `develop` publish a `.devN` pre-release; the version is
computed at build time. See [docs/publishing.md](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/publishing.md).

## License

[MIT](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/LICENSE)
