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
2 changes: 2 additions & 0 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions tests/test_issue_222.py
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 1 addition & 1 deletion tests/test_orientation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': {},
Expand Down
Loading