aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_purpose_editor.py
blob: f269b868334f0bba6941cb77740e098256dbf807 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"""Setting a pane's purpose from the keyboard.

A purpose is what tooling finds a pane by (D8), and it could only be declared by
a layout or set over the socket -- so every pane anybody arranged by hand had
none, and a layout dumped from a session someone built by splitting panes was a
shape with no tags in it. The editor is the one the pane title and the tab label
already share, with a third subject.
"""

import os
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from harness import Session, check, report

SH = ["/bin/sh", "-c", "read x"]


def emit_cmd(payload):
    """A shell line, typed at a pane, that prints one OSC 5577.

    `\\e`, not `\\033`: the driver's own unescaper reads `\\0` as NUL, so a
    `\\033` written here reaches the pane as NUL + "33" and printf emits no ESC
    -- a sequence that silently never arrives, and a test that passes anyway."""
    return 'printf "\\e]5577;%s\\x07"\\n' % payload.replace("%", "%%")


def shell():
    """A pane that runs lines we send it, with no line editor to eat an ESC --
    the same fixture the OSC tests use, for the same reason."""
    return ["/bin/sh", "-c", 'stty raw -echo; while IFS= read -r l; do eval "$l"; done']


def test_a_purpose_can_be_typed_where_it_will_live():
    with Session(SH) as s:
        s.key("P")
        snap = s.snapshot()
        check(
            "the editor announces which label it is editing",
            "purpose" in snap.screen(),
            snap.screen(),
        )

        s.send("service:web")
        check(
            "what is typed shows in the title cell",
            "purpose service:web" in s.snapshot().screen(),
            s.snapshot().screen(),
        )

        s.send("\\r")
        check("enter keeps it", s.pane()["purpose"] == "service:web", str(s.pane()))
        check(
            "and it counts as declared, because an operator set it",
            s.pane()["purpose_declared"] is True,
            str(s.pane()),
        )
        check(
            "the editor is gone",
            "purpose service:web" not in s.snapshot().screen(),
            s.snapshot().screen(),
        )


def test_escape_abandons_it():
    with Session(SH) as s:
        s.key("P")
        s.send("half-typed")
        s.send("\\033")
        check(
            "escape leaves the purpose alone", s.pane()["purpose"] == "", str(s.pane())
        )


def test_it_seeds_with_what_is_there():
    """Unlike a tab's name editor, which seeds empty: here the purpose *is* the
    thing being edited, and the usual edit is adding `:2` to the end of one."""
    with Session(SH) as s:
        s.api("set-purpose", id=s.focused()["id"], purpose="agent:main")
        s.key("P")
        check(
            "the editor opens on the purpose it has",
            "purpose agent:main" in s.snapshot().screen(),
            s.snapshot().screen(),
        )
        s.send(":2\\r")
        check(
            "so an edit is an edit",
            s.pane()["purpose"] == "agent:main:2",
            str(s.pane()),
        )


def test_a_typed_purpose_outranks_the_program_in_the_pane():
    """D8's trust model, reached from the keyboard: declared means "from a layout or
    an operator", and a person at the keyboard is the operator."""
    with Session(shell(), cols=60, rows=8) as s:
        s.settle()
        s.key("P")
        s.send("project:real\\r")
        s.raw(emit_cmd("1;purpose;evil:relabelled"))
        s.settle()
        check(
            "a pane cannot relabel a purpose an operator declared",
            s.pane()["purpose"] == "project:real",
            str(s.pane()),
        )


def test_clearing_it_hands_the_label_back_to_the_program():
    """A lock held over an empty string is the one state nobody can get out of:
    the program can no longer label itself and there is nothing there to have
    outranked it. Same shape as clearing a pane's name, which gives the title
    back to whatever is running."""
    with Session(shell(), cols=60, rows=8) as s:
        s.settle()
        s.key("P")
        s.send("project:real\\r")
        check("set and locked", s.pane()["purpose_declared"] is True, str(s.pane()))

        s.key("P")
        for _ in range(len("project:real")):
            s.send("\\x7f")
        s.send("\\r")
        check("cleared", s.pane()["purpose"] == "", str(s.pane()))
        check("and unlocked", s.pane()["purpose_declared"] is False, str(s.pane()))

        s.raw(emit_cmd("1;purpose;mine-now"))
        s.settle()
        check(
            "so the pane can speak for itself again",
            s.pane()["purpose"] == "mine-now",
            str(s.pane()),
        )


def test_the_action_is_bindable_and_in_the_palette():
    with Session(SH) as s:
        s.key("p")
        s.send("purpose")
        snap = s.snapshot()
        check(
            "the palette offers it without it having to be bound",
            "tag it with a purpose" in snap.screen(),
            snap.screen(),
        )
        s.send("\\r")
        check(
            "and choosing it opens the editor",
            "purpose" in s.snapshot().screen(),
            s.snapshot().screen(),
        )


def test_id_zero_is_the_focused_pane_here_too():
    """`close`, `rerun` and `clear-shaders` all read 0 as "the focused one".
    `set-purpose` was the one addressable thing that refused it, and answered
    `refused` -- which reads as "that purpose is not allowed" rather than "say
    which pane"."""
    with Session(SH) as s:
        s.api("split", dir="rows")
        r = s.api("set-purpose", id=0, purpose="service:web")
        check("0 is accepted", r["ok"] is True, str(r))
        focused = [p for p in s.panes() if p["focused"]][0]
        check(
            "and it tagged the focused pane",
            focused["purpose"] == "service:web",
            str(focused),
        )
        others = [p for p in s.panes() if not p["focused"]]
        check("and nothing else", all(p["purpose"] == "" for p in others), str(others))

        t = s.api("set-purpose", target="tab", id=0, purpose="notes")
        check("a tab takes 0 as the one you are looking at", t["ok"] is True, str(t))
        here = [x for x in s.tabs() if x["active"]][0]
        check("and that is the tab it tagged", here["purpose"] == "notes", str(here))


def test_a_dumped_layout_carries_what_was_tagged_by_hand():
    """The reason this exists: a session you built by splitting panes and tagging
    them is a layout worth writing down."""
    with Session(SH) as s:
        s.key("P")
        s.send("agent:main\\r")
        s.key("\\\\")
        s.key("P")
        s.send("shell:scratch\\r")
        kdl = s.api("dump-layout")["kdl"]
        check(
            "both tags are in the file",
            'purpose="agent:main"' in kdl and 'purpose="shell:scratch"' in kdl,
            kdl,
        )


if __name__ == "__main__":
    for name, fn in sorted(list(globals().items())):
        if name.startswith("test_"):
            fn()
    sys.exit(report())