scriptcast 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,606 @@
1
+ import dataclasses
2
+ import json
3
+ from collections import deque
4
+
5
+ import pytest
6
+
7
+ from scriptcast.config import ScriptcastConfig
8
+ from scriptcast.directives import (
9
+ CommentDirective,
10
+ Directive,
11
+ ExpectDirective,
12
+ FilterDirective,
13
+ HelpersDirective,
14
+ MockDirective,
15
+ RecordDirective,
16
+ ScEvent,
17
+ SetDirective,
18
+ SleepDirective,
19
+ )
20
+
21
+
22
+ def test_sc_event_fields():
23
+ e = ScEvent(ts=1.0, type="cmd", text="echo hi")
24
+ assert e.ts == 1.0
25
+ assert e.type == "cmd"
26
+ assert e.text == "echo hi"
27
+
28
+
29
+ def test_sc_event_is_frozen():
30
+ e = ScEvent(ts=1.0, type="cmd", text="echo hi")
31
+ with pytest.raises(dataclasses.FrozenInstanceError):
32
+ e.ts = 2.0 # type: ignore[misc]
33
+
34
+
35
+ def test_directive_pre_passthrough():
36
+ d = Directive()
37
+ lines = ["echo hello\n", "exit 0\n"]
38
+ assert d.pre(lines) == lines
39
+
40
+
41
+ def test_directive_post_passthrough():
42
+ d = Directive()
43
+ events = [ScEvent(1.0, "cmd", "echo hi"), ScEvent(1.1, "out", "hi")]
44
+ assert d.post(events) == events
45
+
46
+
47
+ def test_directive_gen_noop():
48
+ d = Directive()
49
+ q = deque()
50
+ cfg = ScriptcastConfig()
51
+ cursor, cast_lines = d.gen((1.0, "dir", "hi"), q, cfg, 0.0)
52
+ assert cursor == 0.0
53
+ assert cast_lines == []
54
+
55
+
56
+ def test_mock_directive_pre_passthrough():
57
+ d = MockDirective()
58
+ lines = ["echo hello\n", "exit 0\n"]
59
+ assert d.pre(lines) == lines
60
+
61
+
62
+ def test_mock_directive_pre_rewrites():
63
+ d = MockDirective()
64
+ lines = [
65
+ ": SC mock deploy <<'EOF'\n",
66
+ "Deploying...\n",
67
+ "OK\n",
68
+ "EOF\n",
69
+ "echo after\n",
70
+ ]
71
+ result = d.pre(lines)
72
+ joined = "".join(result)
73
+ assert "(: SC mark mock; set +x; echo + deploy; cat) <<'EOF'\n" in joined
74
+ assert "Deploying...\n" in joined
75
+ assert "OK\n" in joined
76
+ assert "echo after\n" in joined
77
+ assert ": SC mock" not in joined
78
+
79
+
80
+ def test_mock_directive_pre_custom_prefix():
81
+ d = MockDirective(dp="MY")
82
+ lines = [": MY mock cmd <<'DONE'\n", "output\n", "DONE\n"]
83
+ result = d.pre(lines)
84
+ assert "(: MY mark mock; set +x; echo + cmd; cat) <<'DONE'\n" in "".join(result)
85
+
86
+
87
+ def test_mock_directive_pre_unquoted_heredoc():
88
+ d = MockDirective()
89
+ lines = [": SC mock deploy <<EOF\n", "${GREEN}OK${RESET}\n", "EOF\n"]
90
+ result = d.pre(lines)
91
+ joined = "".join(result)
92
+ assert "(: SC mark mock; set +x; echo + deploy; cat) <<EOF\n" in joined
93
+
94
+
95
+ def test_mock_directive_post_passthrough():
96
+ d = MockDirective()
97
+ events = [ScEvent(1.0, "cmd", "echo hi")]
98
+ assert d.post(events) == events
99
+
100
+
101
+ def test_mock_directive_post_drops_mark_mock_and_set_x():
102
+ d = MockDirective()
103
+ events = [
104
+ ScEvent(1.0, "dir", "mark mock"),
105
+ ScEvent(1.001, "cmd", "set +x"),
106
+ ScEvent(1.002, "cmd", "deploy arg1"),
107
+ ]
108
+ result = d.post(events)
109
+ assert result == [ScEvent(1.002, "cmd", "deploy arg1")]
110
+
111
+
112
+ def test_mock_directive_post_drops_mark_mock_without_set_x():
113
+ d = MockDirective()
114
+ events = [
115
+ ScEvent(1.0, "dir", "mark mock"),
116
+ ScEvent(1.001, "cmd", "deploy arg1"),
117
+ ]
118
+ result = d.post(events)
119
+ assert result == [ScEvent(1.001, "cmd", "deploy arg1")]
120
+
121
+
122
+ def test_expect_directive_pre_passthrough():
123
+ d = ExpectDirective()
124
+ lines = ["echo hello\n", "exit 0\n"]
125
+ assert d.pre(lines) == lines
126
+
127
+
128
+ def test_expect_directive_pre_rewrites():
129
+ d = ExpectDirective()
130
+ lines = [
131
+ ": SC expect mysql <<'EOF'\n",
132
+ 'expect "Password:"\n',
133
+ 'send "secret\\r"\n',
134
+ "EOF\n",
135
+ "echo after\n",
136
+ ]
137
+ result = d.pre(lines)
138
+ joined = "".join(result)
139
+ assert ": SC mark expect mysql" in joined
140
+ assert "expect <<'EOF'" in joined
141
+ assert "spawn mysql" in joined
142
+ assert 'send_user ": SC mark input secret\\n"' in joined
143
+ assert 'send "secret\\r"' in joined
144
+ assert "echo after\n" in joined
145
+ assert ": SC expect" not in joined
146
+
147
+
148
+ def test_expect_directive_pre_multiple_sends():
149
+ d = ExpectDirective()
150
+ lines = [
151
+ ": SC expect cmd <<'EOF'\n",
152
+ 'expect "p1"\n',
153
+ 'send "a\\r"\n',
154
+ 'expect "p2"\n',
155
+ 'send "b\\r"\n',
156
+ "EOF\n",
157
+ ]
158
+ result = d.pre(lines)
159
+ joined = "".join(result)
160
+ assert joined.count('send_user ": SC mark input') == 2
161
+ assert 'send_user ": SC mark input a\\n"' in joined
162
+ assert 'send_user ": SC mark input b\\n"' in joined
163
+
164
+
165
+ def test_expect_directive_pre_no_send():
166
+ d = ExpectDirective()
167
+ lines = [": SC expect app <<'EOF'\n", 'expect "done"\n', "EOF\n"]
168
+ result = d.pre(lines)
169
+ joined = "".join(result)
170
+ assert "spawn app" in joined
171
+ assert "mark input" not in joined
172
+
173
+
174
+ def test_expect_directive_post_passthrough():
175
+ d = ExpectDirective()
176
+ events = [ScEvent(1.0, "cmd", "echo hi"), ScEvent(1.1, "out", "hi")]
177
+ assert d.post(events) == events
178
+
179
+
180
+ def test_expect_directive_post_emits_cmd_from_mark_expect():
181
+ d = ExpectDirective()
182
+ events = [
183
+ ScEvent(1.0, "dir", "mark expect mysql"),
184
+ ScEvent(1.001, "cmd", "expect"),
185
+ ScEvent(1.002, "out", "spawn mysql"),
186
+ ScEvent(1.003, "out", "Password:"),
187
+ ScEvent(1.004, "cmd", "echo hi"),
188
+ ]
189
+ result = d.post(events)
190
+ assert result[0] == ScEvent(1.0, "cmd", "mysql")
191
+ types = [e.type for e in result]
192
+ assert "out" in types
193
+ # Terminator stays in output
194
+ assert ScEvent(1.004, "cmd", "echo hi") in result
195
+
196
+
197
+ def test_expect_directive_post_skips_spawn_line():
198
+ d = ExpectDirective()
199
+ events = [
200
+ ScEvent(1.0, "dir", "mark expect myapp"),
201
+ ScEvent(1.001, "cmd", "expect"),
202
+ ScEvent(1.002, "out", "spawn myapp"),
203
+ ScEvent(1.003, "cmd", "echo done"),
204
+ ]
205
+ result = d.post(events)
206
+ assert not any("spawn" in e.text for e in result)
207
+
208
+
209
+ def test_expect_directive_post_emits_expect_input_for_mark_input():
210
+ d = ExpectDirective()
211
+ events = [
212
+ ScEvent(1.0, "dir", "mark expect app"),
213
+ ScEvent(1.001, "cmd", "expect"),
214
+ ScEvent(1.002, "out", "spawn app"),
215
+ ScEvent(1.003, "out", "Password: : SC mark input secret"),
216
+ ScEvent(1.004, "out", "Welcome"),
217
+ ScEvent(1.005, "cmd", "echo done"),
218
+ ]
219
+ result = d.post(events)
220
+ dir_events = [e for e in result if e.type == "dir"]
221
+ assert any(e.text.startswith("expect-input") for e in dir_events)
222
+ out_events = [e for e in result if e.type == "out"]
223
+ assert any("Password:" in e.text for e in out_events)
224
+ assert any("Welcome" in e.text for e in out_events)
225
+
226
+
227
+ def test_expect_directive_post_strips_pty_echo():
228
+ d = ExpectDirective()
229
+ events = [
230
+ ScEvent(1.0, "dir", "mark expect app"),
231
+ ScEvent(1.001, "cmd", "expect"),
232
+ ScEvent(1.002, "out", "spawn app"),
233
+ ScEvent(1.003, "out", "prompt> : SC mark input show;"),
234
+ ScEvent(1.004, "out", "show;"), # pty echo — must be stripped
235
+ ScEvent(1.005, "out", "(0 rows)"),
236
+ ScEvent(1.006, "cmd", "echo done"),
237
+ ]
238
+ result = d.post(events)
239
+ out_texts = [e.text for e in result if e.type == "out"]
240
+ assert "show;" not in out_texts
241
+ assert "(0 rows)" in out_texts
242
+
243
+
244
+ def test_expect_directive_post_preserves_cr_lf_from_pty_echo():
245
+ # When the PTY echo includes a trailing \r\n (Enter echo), it must be kept
246
+ # as an out event so the cast shows a newline after the typed input.
247
+ # Bug scenario: "Email: dev@example.comPassword:" (no newline between them).
248
+ d = ExpectDirective()
249
+ events = [
250
+ ScEvent(1.0, "dir", "mark expect app"),
251
+ ScEvent(1.001, "cmd", "expect"),
252
+ ScEvent(1.002, "out", "spawn app"),
253
+ ScEvent(1.003, "out", "Email: : SC mark input dev@example.com"),
254
+ ScEvent(1.004, "out", "dev@example.com\r\n"), # PTY echo with Enter
255
+ ScEvent(1.005, "out", "Password: "),
256
+ ScEvent(1.006, "cmd", "echo done"),
257
+ ]
258
+ result = d.post(events)
259
+ out_texts = [e.text for e in result if e.type == "out"]
260
+ # Characters stripped; \r\n retained so the cast shows a newline after typing
261
+ assert "dev@example.com\r\n" not in out_texts # full echo gone
262
+ assert "\r\n" in out_texts # but the Enter echo preserved
263
+ assert "Password: " in out_texts
264
+ # \r\n must appear before "Password: " in the output
265
+ rn_idx = next(i for i, t in enumerate(out_texts) if t == "\r\n")
266
+ pw_idx = next(i for i, t in enumerate(out_texts) if t == "Password: ")
267
+ assert rn_idx < pw_idx
268
+
269
+
270
+ def test_expect_directive_post_handles_raw_expect_call():
271
+ """Bare 'cmd: expect' event (no SC expect directive) is handled correctly."""
272
+ d = ExpectDirective()
273
+ events = [
274
+ ScEvent(1.0, "cmd", "expect"),
275
+ ScEvent(1.001, "out", "spawn ./fake-db"),
276
+ ScEvent(1.002, "out", "Password:"),
277
+ ScEvent(1.003, "cmd", "echo after"),
278
+ ]
279
+ result = d.post(events)
280
+ assert result[0].type == "cmd"
281
+ assert result[0].text == "./fake-db"
282
+ assert any(e.type == "out" and "Password:" in e.text for e in result)
283
+ assert ScEvent(1.003, "cmd", "echo after") in result
284
+
285
+
286
+ def test_expect_directive_post_terminates_on_dir_event():
287
+ d = ExpectDirective()
288
+ events = [
289
+ ScEvent(1.0, "dir", "mark expect app"),
290
+ ScEvent(1.001, "cmd", "expect"),
291
+ ScEvent(1.002, "out", "spawn app"),
292
+ ScEvent(1.003, "out", "some output"),
293
+ ScEvent(1.004, "dir", "filter sed 's/x/y/g'"),
294
+ ]
295
+ result = d.post(events)
296
+ assert ScEvent(1.004, "dir", "filter sed 's/x/y/g'") in result
297
+ assert any(e.type == "out" and "some output" in e.text for e in result)
298
+
299
+
300
+ def test_filter_directive_post_passthrough_when_no_filter_set():
301
+ d = FilterDirective()
302
+ events = [ScEvent(1.0, "cmd", "echo hi"), ScEvent(1.1, "out", "hi")]
303
+ assert d.post(events) == events
304
+
305
+
306
+ def test_filter_directive_post_registers_filter():
307
+ d = FilterDirective()
308
+ events = [ScEvent(1.0, "dir", "filter sed 's/foo/bar/g'")]
309
+ result = d.post(events)
310
+ assert result == []
311
+ assert d.apply("foo baz") == "bar baz"
312
+
313
+
314
+ def test_filter_directive_post_registers_filter_add():
315
+ d = FilterDirective()
316
+ d.post([ScEvent(1.0, "dir", "filter sed 's/foo/bar/g'")])
317
+ d.post([ScEvent(1.1, "dir", "filter-add sed 's/baz/qux/g'")])
318
+ assert d.apply("foo baz") == "bar qux"
319
+
320
+
321
+ def test_filter_directive_post_replaces_filter():
322
+ d = FilterDirective()
323
+ d.post([ScEvent(1.0, "dir", "filter sed 's/a/b/g'")])
324
+ d.post([ScEvent(1.1, "dir", "filter sed 's/x/y/g'")])
325
+ assert d.apply("aaa xxx") == "aaa yyy"
326
+
327
+
328
+ def test_filter_directive_post_transforms_out_events():
329
+ d = FilterDirective()
330
+ events = [
331
+ ScEvent(1.0, "dir", "filter sed 's/foo/bar/g'"),
332
+ ScEvent(1.1, "out", "foo baz"),
333
+ ]
334
+ result = d.post(events)
335
+ assert ScEvent(1.1, "out", "bar baz") in result
336
+ assert not any(e.text == "foo baz" for e in result)
337
+
338
+
339
+ def test_filter_directive_post_transforms_cmd_events():
340
+ d = FilterDirective()
341
+ events = [
342
+ ScEvent(1.0, "dir", "filter sed 's/foo/bar/g'"),
343
+ ScEvent(1.1, "cmd", "foo command"),
344
+ ]
345
+ result = d.post(events)
346
+ assert ScEvent(1.1, "cmd", "bar command") in result
347
+
348
+
349
+ def test_filter_directive_post_does_not_transform_dir_events():
350
+ d = FilterDirective()
351
+ events = [
352
+ ScEvent(1.0, "dir", "filter sed 's/foo/bar/g'"),
353
+ ScEvent(1.1, "dir", "scene intro"),
354
+ ]
355
+ result = d.post(events)
356
+ assert ScEvent(1.1, "dir", "scene intro") in result
357
+
358
+
359
+ def test_filter_directive_apply_identity_when_empty():
360
+ d = FilterDirective()
361
+ assert d.apply("unchanged") == "unchanged"
362
+
363
+
364
+ def test_filter_directive_apply_real_command():
365
+ d = FilterDirective()
366
+ d.post([ScEvent(1.0, "dir", "filter tr 'a-z' 'A-Z'")])
367
+ assert d.apply("hello") == "HELLO"
368
+
369
+
370
+ def test_filter_directive_apply_preserves_cr_in_content():
371
+ # text=True in subprocess would convert \r → \n via universal newlines;
372
+ # binary mode must be used so progress-bar \r overwrite chars survive the filter.
373
+ d = FilterDirective()
374
+ d.post([ScEvent(1.0, "dir", "filter sed 's/x/x/g'")]) # identity filter
375
+ assert d.apply("frame1\rframe2\rframe3\r\n") == "frame1\rframe2\rframe3\r\n"
376
+
377
+
378
+ def test_filter_directive_failed_command_returns_empty():
379
+ d = FilterDirective()
380
+ d.post([ScEvent(1.0, "dir", "filter false")])
381
+ assert d.apply("anything") == ""
382
+
383
+
384
+ def test_filter_directive_command_not_found_returns_empty():
385
+ d = FilterDirective()
386
+ d.post([ScEvent(1.0, "dir", "filter __nonexistent_command_xyz__")])
387
+ assert d.apply("anything") == ""
388
+
389
+
390
+ def test_record_directive_post_passthrough():
391
+ d = RecordDirective()
392
+ events = [ScEvent(1.0, "cmd", "echo hi"), ScEvent(1.1, "out", "hi")]
393
+ assert d.post(events) == events
394
+
395
+
396
+ def test_record_directive_drops_pause_resume_block():
397
+ d = RecordDirective()
398
+ events = [
399
+ ScEvent(1.0, "dir", "record pause"),
400
+ ScEvent(1.1, "cmd", "PS1=>"),
401
+ ScEvent(1.2, "cmd", "echo ignored"),
402
+ ScEvent(1.3, "dir", "record resume"),
403
+ ScEvent(1.4, "cmd", "echo after"),
404
+ ]
405
+ result = d.post(events)
406
+ assert result == [ScEvent(1.4, "cmd", "echo after")]
407
+
408
+
409
+ def test_record_directive_drops_to_end_if_no_resume():
410
+ d = RecordDirective()
411
+ events = [
412
+ ScEvent(1.0, "dir", "record pause"),
413
+ ScEvent(1.1, "cmd", "echo something"),
414
+ ]
415
+ result = d.post(events)
416
+ assert result == []
417
+
418
+
419
+ def test_record_directive_ignores_resume_without_pause():
420
+ d = RecordDirective()
421
+ events = [ScEvent(1.0, "dir", "record resume"), ScEvent(1.1, "cmd", "echo hi")]
422
+ result = d.post(events)
423
+ assert result == [ScEvent(1.0, "dir", "record resume"), ScEvent(1.1, "cmd", "echo hi")]
424
+
425
+
426
+ def test_set_directive_gen_mutates_active_config():
427
+ d = SetDirective()
428
+ active = ScriptcastConfig()
429
+ active.type_speed = 40
430
+ cursor, events = d.gen((1.0, "dir", "set type_speed 10"), deque(), active, 0.0)
431
+ assert active.type_speed == 10
432
+ assert cursor == 0.0
433
+ assert events == []
434
+
435
+
436
+ def test_set_directive_gen_quoted_value():
437
+ # bash traces `"$ "` as `'$ '`; shlex.split must handle shell quoting
438
+ d = SetDirective()
439
+ active = ScriptcastConfig()
440
+ cursor, events = d.gen((1.0, "dir", "set prompt '$ '"), deque(), active, 0.0)
441
+ assert active.prompt == "$ "
442
+ assert cursor == 0.0
443
+ assert events == []
444
+
445
+
446
+ def test_sleep_directive_gen_advances_cursor():
447
+ d = SleepDirective()
448
+ active = ScriptcastConfig()
449
+ cursor, events = d.gen((1.0, "dir", "sleep 500"), deque(), active, 0.0)
450
+ assert abs(cursor - 0.5) < 1e-9
451
+ assert events == []
452
+
453
+
454
+ def test_sleep_directive_gen_zero():
455
+ d = SleepDirective()
456
+ active = ScriptcastConfig()
457
+ cursor, events = d.gen((1.0, "dir", "sleep 0"), deque(), active, 1.5)
458
+ assert cursor == 1.5
459
+
460
+
461
+ def test_comment_directive_post_passthrough():
462
+ d = CommentDirective()
463
+ events = [ScEvent(1.0, "cmd", "echo hi"), ScEvent(1.1, "out", "hi")]
464
+ assert d.post(events) == events
465
+
466
+
467
+ def test_comment_directive_converts_comment_with_text():
468
+ d = CommentDirective()
469
+ events = [ScEvent(1.0, "dir", "'\\\' This is a comment")]
470
+ result = d.post(events)
471
+ assert result == [ScEvent(1.0, "cmd", "# This is a comment")]
472
+
473
+
474
+ def test_comment_directive_converts_empty_comment():
475
+ d = CommentDirective()
476
+ events = [ScEvent(1.0, "dir", "'\\\'")]
477
+ result = d.post(events)
478
+ assert result == [ScEvent(1.0, "cmd", "#")]
479
+
480
+
481
+ def test_comment_directive_does_not_touch_other_dir_events():
482
+ d = CommentDirective()
483
+ events = [ScEvent(1.0, "dir", "scene intro")]
484
+ assert d.post(events) == events
485
+
486
+
487
+ def test_directive_has_priority():
488
+ d = Directive()
489
+ assert isinstance(d.priority, int)
490
+
491
+
492
+ def test_directive_has_handles_none_by_default():
493
+ d = Directive()
494
+ assert d.handles is None
495
+
496
+
497
+ def test_set_directive_handles_set():
498
+ d = SetDirective()
499
+ assert d.handles == "set"
500
+
501
+
502
+ def test_sleep_directive_handles_sleep():
503
+ d = SleepDirective()
504
+ assert d.handles == "sleep"
505
+
506
+
507
+ def test_expect_directive_priority_lower_than_filter():
508
+ assert ExpectDirective().priority < FilterDirective().priority
509
+
510
+
511
+ def test_expect_directive_handles_expect_input():
512
+ d = ExpectDirective()
513
+ assert d.handles == "expect-input"
514
+
515
+
516
+ def test_expect_directive_gen_advances_cursor_by_input_wait():
517
+ from collections import deque
518
+
519
+ from scriptcast.config import ScriptcastConfig
520
+ d = ExpectDirective()
521
+ active = ScriptcastConfig()
522
+ active.input_wait = 300
523
+ active.type_speed = 0
524
+ cursor, lines = d.gen((1.0, "dir", "expect-input secret"), deque(), active, 0.0)
525
+ assert cursor == pytest.approx(0.3)
526
+
527
+
528
+ def test_expect_directive_gen_emits_chars():
529
+ from collections import deque
530
+
531
+ from scriptcast.config import ScriptcastConfig
532
+ d = ExpectDirective()
533
+ active = ScriptcastConfig()
534
+ active.input_wait = 0
535
+ active.type_speed = 0
536
+ _, lines = d.gen((1.0, "dir", "expect-input hi"), deque(), active, 0.0)
537
+ cast_texts = [json.loads(ln)[2] for ln in lines]
538
+ assert "h" in cast_texts
539
+ assert "i" in cast_texts
540
+ assert "\r\n" not in cast_texts
541
+ event_types = {json.loads(ln)[1] for ln in lines}
542
+ assert event_types == {"o"}
543
+
544
+
545
+ def test_expect_directive_gen_empty_input():
546
+ from collections import deque
547
+
548
+ from scriptcast.config import ScriptcastConfig
549
+ d = ExpectDirective()
550
+ active = ScriptcastConfig()
551
+ active.input_wait = 0
552
+ active.type_speed = 0
553
+ cursor, lines = d.gen((1.0, "dir", "expect-input"), deque(), active, 0.0)
554
+ assert lines == []
555
+
556
+
557
+ def test_helpers_directive_priority_is_lowest():
558
+ from scriptcast.directives import build_directives
559
+ all_directives = build_directives()
560
+ helpers = next(d for d in all_directives if isinstance(d, HelpersDirective))
561
+ others = [d for d in all_directives if not isinstance(d, HelpersDirective)]
562
+ assert all(helpers.priority < d.priority for d in others)
563
+
564
+
565
+ def test_helpers_directive_pre_passthrough():
566
+ d = HelpersDirective()
567
+ lines = ["echo hello\n", "exit 0\n"]
568
+ assert d.pre(lines) == lines
569
+
570
+
571
+ def test_helpers_directive_pre_expands_helpers_line():
572
+ d = HelpersDirective()
573
+ lines = [": SC helpers\n", "echo hi\n"]
574
+ result = d.pre(lines)
575
+ joined = "".join(result)
576
+ assert ": SC record pause\n" in joined
577
+ assert "RED=$'\\033[31m'\n" in joined
578
+ assert "YELLOW=$'\\033[33m'\n" in joined
579
+ assert "GREEN=$'\\033[32m'\n" in joined
580
+ assert "CYAN=$'\\033[36m'\n" in joined
581
+ assert "BOLD=$'\\033[1m'\n" in joined
582
+ assert "RESET=$'\\033[0m'\n" in joined
583
+ assert ": SC record resume\n" in joined
584
+ assert "echo hi\n" in joined
585
+ assert ": SC helpers\n" not in joined
586
+
587
+
588
+ def test_helpers_directive_pre_custom_prefix():
589
+ d = HelpersDirective(dp="MY")
590
+ lines = [": MY helpers\n"]
591
+ result = d.pre(lines)
592
+ joined = "".join(result)
593
+ assert ": MY record pause\n" in joined
594
+ assert ": MY record resume\n" in joined
595
+
596
+
597
+ def test_helpers_directive_pre_ignores_partial_match():
598
+ d = HelpersDirective()
599
+ lines = [": SC helpers extra\n"]
600
+ assert d.pre(lines) == lines
601
+
602
+
603
+ def test_helpers_directive_post_passthrough():
604
+ d = HelpersDirective()
605
+ events = [ScEvent(1.0, "cmd", "echo hi")]
606
+ assert d.post(events) == events