diff --git a/docs/CONTRACT.md b/docs/CONTRACT.md index 79fc892..d4b5132 100644 --- a/docs/CONTRACT.md +++ b/docs/CONTRACT.md @@ -118,6 +118,8 @@ Every `--json` mutator (any command that changes iTerm2 state) emits a single JS **Read-only** commands (`status`, `overview`, `get-prompt`, `read`) MAY omit the envelope and return their payload directly — but they MUST still be valid JSON in `--json` mode. +**Key stability across variants (#222).** Every documented key in a `--json` payload MUST be present on every code path, including failure / empty / null branches. Absent values are emitted as `null`; keys are never silently dropped. Consumers iterate keys, so omission is a breaking schema change. + **`overview --json` is the canonical world model (#276).** It is the single read an agent should issue to answer "what exists right now": ```json diff --git a/tests/test_issue_222.py b/tests/test_issue_222.py new file mode 100644 index 0000000..425bf75 --- /dev/null +++ b/tests/test_issue_222.py @@ -0,0 +1,47 @@ +"""Regression: #222 — `focus --json` null path must include `session_name` key. + +CONTRACT §4 Key stability: every documented --json key is present on every +code path; absent values are `null`, never omitted. Consumers iterate keys +and must not hit KeyError on the "no focused window" branch. +""" +import json + +import pytest +from click.testing import CliRunner + +from ita import _orientation +from ita._core import cli + + +FOCUS_KEYS = {'window_id', 'tab_id', 'session_id', 'session_name'} + + +@pytest.mark.regression +def test_issue_222_focus_json_null_path_includes_session_name(monkeypatch): + """Null path (no focused window): all four keys present, all None.""" + monkeypatch.setattr(_orientation, 'run_iterm', lambda _coro: None) + r = CliRunner().invoke(cli, ['focus', '--json']) + assert r.exit_code == 0, r.output + data = json.loads(r.output) + assert set(data.keys()) == FOCUS_KEYS + assert data['session_name'] is None + assert data['window_id'] is None + assert data['tab_id'] is None + assert data['session_id'] is None + + +@pytest.mark.regression +def test_issue_222_focus_json_happy_path_includes_session_name(monkeypatch): + """Happy path (focused session): same key set, populated values.""" + fake = { + 'window_id': 'W-UUID', + 'tab_id': 'T-UUID', + 'session_id': 'S-UUID', + 'session_name': 'main', + } + monkeypatch.setattr(_orientation, 'run_iterm', lambda _coro: fake) + r = CliRunner().invoke(cli, ['focus', '--json']) + assert r.exit_code == 0, r.output + data = json.loads(r.output) + assert set(data.keys()) == FOCUS_KEYS + assert data['session_name'] == 'main' diff --git a/tests/test_orientation.py b/tests/test_orientation.py index e9a9c0e..1ad04db 100644 --- a/tests/test_orientation.py +++ b/tests/test_orientation.py @@ -27,7 +27,7 @@ FOCUS_SCHEMA = { 'type': 'object', - 'required': ['window_id', 'tab_id', 'session_id'], + 'required': ['window_id', 'tab_id', 'session_id', 'session_name'], 'properties': { 'window_id': {}, 'tab_id': {},