-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_scriptya.sh
More file actions
2556 lines (2345 loc) · 161 KB
/
Copy pathtest_scriptya.sh
File metadata and controls
2556 lines (2345 loc) · 161 KB
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
set -uo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT="$ROOT_DIR/script/scriptya.sh"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/scriptya-tests.XXXXXX")"
LIB="$TMP_ROOT/scriptya-lib.sh"
PASS=0
FAIL=0
cleanup() {
rm -rf -- "$TMP_ROOT"
}
trap cleanup EXIT
# Load the program's functions without invoking main(). This used to be
# "sed '$d' "$SCRIPT" > "$LIB"" (strip the last line) from before
# scriptya.sh had a BASH_SOURCE guard around its final "main "$@"" call:
# it worked, but only because bash executes already-parsed top-level
# commands before choking on the truncated "if" block left behind,
# printing a spurious "syntax error: unexpected end of file" on every
# single test. The guard now makes plain sourcing safe, so LIB is just
# a copy of the real script.
cp -- "$SCRIPT" "$LIB"
ok() {
PASS=$((PASS + 1))
printf 'PASS %s\n' "$1"
}
fail() {
FAIL=$((FAIL + 1))
printf 'FAIL %s\n' "$1" >&2
}
assert_eq() {
local name="$1" expected="$2" actual="$3"
if [[ "$actual" == "$expected" ]]; then ok "$name"; else fail "$name (expected: <$expected>, got: <$actual>)"; fi
}
assert_contains() {
local name="$1" needle="$2" haystack="$3"
if [[ "$haystack" == *"$needle"* ]]; then ok "$name"; else fail "$name (missing: <$needle>)"; fi
}
assert_file_exists() {
local name="$1" path="$2"
if [[ -f "$path" ]]; then ok "$name"; else fail "$name (missing: <$path>)"; fi
}
assert_not_empty() {
local name="$1" value="$2"
if [[ -n "$value" ]]; then ok "$name"; else fail "$name (empty)"; fi
}
assert_not_contains() {
local name="$1" needle="$2" haystack="$3"
if [[ "$haystack" != *"$needle"* ]]; then ok "$name"; else fail "$name (unexpected: <$needle>)"; fi
}
run_cli() {
local home="$1"; shift
HOME="$home" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" "$@"
}
source_lib() {
HOME="$TMP_ROOT/home-lib" bash -c 'source "$1"' _ "$LIB"
}
printf '== Scriptya test suite ==\n'
printf 'Script: %s\n\n' "$SCRIPT"
LUA_TEST_REAL=false
if command -v lua >/dev/null 2>&1; then
LUA_TEST_REAL=true
else
LUA_TEST_BIN="$TMP_ROOT/lua-bin"
mkdir -p "$LUA_TEST_BIN"
cat > "$LUA_TEST_BIN/lua" <<'SHIM'
#!/bin/sh
script="$1"
shift || true
case "$script" in
*run.lua) printf 'LUA_EXEC|%s\n' "${1:-}"; exit 0;;
*fail.lua) printf 'LUA_FAIL_PAYLOAD\n'; exit 7;;
*shebang.lua) printf 'LUA_SHEBANG_OK\n'; exit 0;;
*) exit 0;;
esac
SHIM
chmod +x "$LUA_TEST_BIN/lua"
export PATH="$LUA_TEST_BIN:$PATH"
fi
FISH_TEST_REAL=false
if command -v fish >/dev/null 2>&1; then
FISH_TEST_REAL=true
else
FISH_TEST_BIN="$TMP_ROOT/fish-bin"
mkdir -p "$FISH_TEST_BIN"
cat > "$FISH_TEST_BIN/fish" <<'SHIM'
#!/bin/sh
script="$1"
shift || true
case "$script" in
*run.fish) printf 'FISH_EXEC|%s\n' "${1:-}"; exit 0;;
*fail.fish) printf 'FISH_FAIL_PAYLOAD\n'; exit 7;;
*shebang.fish) printf 'FISH_SHEBANG_OK\n'; exit 0;;
*) exit 0;;
esac
SHIM
chmod +x "$FISH_TEST_BIN/fish"
export PATH="$FISH_TEST_BIN:$PATH"
fi
# 1. Basic file / shell integrity.
if bash -n "$SCRIPT"; then ok 'Bash syntax'; else fail 'Bash syntax'; fi
source "$LIB"
# 1b. The repository launcher must remain directly executable.
if [[ -x "$SCRIPT" ]]; then ok 'Repository launcher is executable'; else fail 'Repository launcher is not executable'; fi
# 2. Locale precedence and fallback. This is tested inside a fresh shell so
# locale variables cannot leak between cases.
for spec in \
'LC_ALL=es_ES.UTF-8 LC_MESSAGES=C LANG=en_US.UTF-8|es' \
'LC_ALL=C LC_MESSAGES=es_ES.UTF-8 LANG=es_ES.UTF-8|en' \
'LC_ALL= LC_MESSAGES=es_ES.UTF-8 LANG=C|es' \
'LC_ALL= LC_MESSAGES=C LANG=|en' \
'LC_ALL=C LC_MESSAGES=C LANG=|en'; do
envspec="${spec%%|*}"; expected="${spec##*|}"
actual=$(env $envspec bash -c 'source "$1"; detect_system_language' _ "$LIB" 2>/dev/null)
assert_eq "Locale detection: $envspec" "$expected" "$actual"
done
# 3. Translation corpus, including dynamic messages.
declare -a CORPUS_EN=(
"Instala 'libglib2.0-bin' (comando 'gio') para evitar el aviso de|Install 'libglib2.0-bin' (command 'gio') to avoid the permission warning from"
"No se pudo marcar 'foo.sh' como 'de confianza'.|Could not mark 'foo.sh' as trusted."
"Terminó con código 7.|Ended with code 7."
"Mostrando las últimas 50 ejecuciones de 73 (más reciente primero).|Showing the latest 50 of 73 executions (most recent first)."
"La carpeta '/tmp/x' no existe. ¿Crearla?|The folder '/tmp/x' does not exist. Create it?"
"'Foo' instalado como aplicación independiente|'Foo' installed as a standalone application"
"'Foo' instalado con icono propio|'Foo' installed with its own icon"
"Icono de 'Foo' actualizado.|Icon for 'Foo' updated."
"página|web page"
"¿Dónde quieres el acceso?|Where do you want the shortcut?"
"Datos pedidos por teclado antes de ejecutar (ASK): ninguno.|Input requested before running (ASK): none."
"/tmp/bin no está en tu PATH.|/tmp/bin is not in your PATH."
)
for pair in "${CORPUS_EN[@]}"; do
src=${pair%%|*}; expected=${pair#*|}
actual=$(bash -c 'source "$1"; LANGUAGE=en; tr "$2"' _ "$LIB" "$src")
assert_eq "English: $src" "$expected" "$actual"
done
for pair in "${CORPUS_EN[@]}"; do
src=${pair%%|*}
actual=$(bash -c 'source "$1"; LANGUAGE=es; tr "$2"' _ "$LIB" "$src")
assert_eq "Spanish source preserved: $src" "$src" "$actual"
done
# 4. English UI from a neutral locale.
HOME_EN="$TMP_ROOT/home-en"; mkdir -p "$HOME_EN/Scripts"
out=$(run_cli "$HOME_EN" <<< '0' 2>&1)
assert_contains 'English main menu title' 'Install Scripts' "$out"
assert_contains 'English language action' 'L) Spanish' "$out"
assert_contains 'English exit' '0) Exit' "$out"
assert_not_contains 'English menu has no Spanish Install label' 'Instalar Scripts' "$out"
assert_not_contains 'English menu has no Spanish language label' 'L) Español' "$out"
# 5. Manual toggle persists, and toggling twice restores English.
HOME_TOGGLE="$TMP_ROOT/home-toggle"; mkdir -p "$HOME_TOGGLE/Scripts"
out=$(run_cli "$HOME_TOGGLE" l 2>&1)
assert_contains 'Toggle to Spanish reports success' 'Idioma cambiado a Español.' "$out"
assert_eq 'Toggle config stores Spanish' 'es' "$(sed -n 's/^LANGUAGE=//p' "$HOME_TOGGLE/.config/scriptya/config.conf")"
out=$(run_cli "$HOME_TOGGLE" --help 2>&1)
assert_contains 'Persisted Spanish help' 'Idioma:' "$out"
assert_contains 'Spanish help documents toggle' 'scriptya.sh l' "$out"
out=$(HOME="$HOME_TOGGLE" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" l 2>&1)
assert_contains 'Toggle back to English reports success' 'Language changed to English.' "$out"
assert_eq 'Toggle config stores English' 'en' "$(sed -n 's/^LANGUAGE=//p' "$HOME_TOGGLE/.config/scriptya/config.conf")"
# 6. The L) menu entry performs the same switch without changing the menu flow.
# Position 8, not 7: an empty Scripts/ shows the 6 fixed actions (install,
# uninstall, change icon, insert metadata, find scripts, Nemo integration
# -- always listed now, see B8) plus history (7) and toggle language (8).
HOME_MENU_TOGGLE="$TMP_ROOT/home-menu-toggle"; mkdir -p "$HOME_MENU_TOGGLE/Scripts"
out=$(run_cli "$HOME_MENU_TOGGLE" <<< $'8\n\n0\n' 2>&1)
assert_contains 'Menu language switch reports Spanish' 'Idioma cambiado a Español.' "$out"
assert_contains 'Menu redraws in Spanish' 'Instalar Scripts' "$out"
assert_contains 'Menu switch exposes English as the return language' 'L) Inglés' "$out"
# 7. Legacy config without LANGUAGE keeps locale detection behavior.
HOME_LEGACY="$TMP_ROOT/home-legacy"; mkdir -p "$HOME_LEGACY/Scripts" "$HOME_LEGACY/.config/scriptya"
printf 'SCRIPTS_DIR=%q\nTERMINAL=x-terminal-emulator\nSY_ICON=utilities-terminal\n' "$HOME_LEGACY/Scripts" > "$HOME_LEGACY/.config/scriptya/config.conf"
out=$(run_cli "$HOME_LEGACY" <<< '0' 2>&1)
assert_contains 'Legacy config defaults to English under C locale' 'Install Scripts' "$out"
assert_not_contains 'Legacy config does not force Spanish' 'Instalar Scripts' "$out"
# 8. Real CLI locale detection must affect the interface before any config exists.
HOME_LOCALE="$TMP_ROOT/home-locale"; mkdir -p "$HOME_LOCALE/Scripts"
out=$(HOME="$HOME_LOCALE" TERM=xterm NO_COLOR=1 LC_ALL= LC_MESSAGES=es_ES.UTF-8 LANG=es_ES.UTF-8 bash "$SCRIPT" --help 2>&1)
assert_contains 'Real CLI detects Spanish locale' 'Uso:' "$out"
out=$(HOME="$HOME_LOCALE" TERM=xterm NO_COLOR=1 LC_ALL=C LC_MESSAGES=C LANG=C bash "$SCRIPT" --help 2>&1)
assert_contains 'Real CLI falls back to English locale' 'Usage:' "$out"
# 9. Confirmation input accepts either language's affirmative in both UI languages.
for lang in en es; do
for answer in y s; do
if printf '%s\n' "$answer" | LANGUAGE_TEST="$lang" HOME="$TMP_ROOT" bash -c 'source "$1"; LANGUAGE="$LANGUAGE_TEST"; confirm_yn "¿Continuar?"' _ "$LIB" >/dev/null 2>&1; then
ok "confirm_yn accepts '$answer' in $lang"
else
fail "confirm_yn accepts '$answer' in $lang"
fi
done
done
# 10. Config serialization keeps legacy fields and adds LANGUAGE safely.
HOME_CFG="$TMP_ROOT/home-config"; mkdir -p "$HOME_CFG/.config/scriptya"
if HOME="$HOME_CFG" bash -c 'source "$1"; SCRIPTS_DIR="/tmp/a path/\$x"; TERMINAL="x-term -e"; SY_ICON="theme icon"; LANGUAGE=en; save_config' _ "$LIB"; then ok 'save_config runs'; else fail 'save_config runs'; fi
cfg=$(cat "$HOME_CFG/.config/scriptya/config.conf")
assert_contains 'Config keeps SCRIPTS_DIR' 'SCRIPTS_DIR=/tmp/a\ path/\$x' "$cfg"
assert_contains 'Config keeps TERMINAL' 'TERMINAL=x-term\ -e' "$cfg"
assert_contains 'Config keeps SY_ICON' 'SY_ICON=theme\ icon' "$cfg"
assert_contains 'Config adds LANGUAGE' 'LANGUAGE=en' "$cfg"
# 11. Nemo action generator emits the localized action labels from the current language.
for lang in en es; do
NHOME="$TMP_ROOT/home-nemo-$lang"; mkdir -p "$NHOME/.local/share/nemo/actions"
action="$NHOME/.local/share/nemo/actions/test.nemo_action"
if HOME="$NHOME" TEST_LANGUAGE="$lang" bash -c 'source "$1"; LANGUAGE="$TEST_LANGUAGE"; NEMO_ACTIONS_DIR="$2"; SCRIPT_PATH="/tmp/scriptya.sh"; write_nemo_action "$3" "$(tr "Lanzar con Scriptya")" "$(tr "Ejecuta este script con Scriptya")" "--run-script" "sh;" "utilities-terminal"' _ "$LIB" "$NHOME/.local/share/nemo/actions" "$action"; then
action_text=$(cat "$action")
if [[ "$lang" == en ]]; then
assert_contains 'Nemo English action name' 'Name=Run with Scriptya' "$action_text"
assert_contains 'Nemo English action comment' 'Comment=Run this script with Scriptya' "$action_text"
else
assert_contains 'Nemo Spanish action name' 'Name=Lanzar con Scriptya' "$action_text"
assert_contains 'Nemo Spanish action comment' 'Comment=Ejecuta este script con Scriptya' "$action_text"
fi
else
fail "Nemo action generation ($lang)"
fi
done
# Nemo action contract: the action must pass the selected path through the documented %F token.
NEMO_ACTION_TEST="$TMP_ROOT/home-nemo-contract"; mkdir -p "$NEMO_ACTION_TEST/.local/share/nemo/actions"
NEMO_ACTION_FILE="$NEMO_ACTION_TEST/.local/share/nemo/actions/run.nemo_action"
HOME="$NEMO_ACTION_TEST" bash -c 'source "$1"; NEMO_ACTIONS_DIR="$2"; write_nemo_action "$3" "Run" "Run" "--run-script" "sh;" "utilities-terminal"' _ "$LIB" "$NEMO_ACTION_TEST/.local/share/nemo/actions" "$NEMO_ACTION_FILE"
nemo_action_text=$(cat "$NEMO_ACTION_FILE")
assert_contains 'Nemo action uses the path selection token' '--run-script %F' "$nemo_action_text"
assert_contains 'Nemo action quotes selected paths' 'Quote=double' "$nemo_action_text"
NEMO_PATH="$NEMO_ACTION_TEST/dir with spaces/demo.py"; mkdir -p "$(dirname "$NEMO_PATH")"; : > "$NEMO_PATH"
nemo_dispatch=$(HOME="$NEMO_ACTION_TEST" bash -c 'source "$1"; open_terminal_running(){ printf "%s|%s|%s\n" "$1" "$2" "$3"; }; main --nemo-run "$2"' _ "$LIB" "$NEMO_PATH" 2>&1)
assert_contains 'Nemo dispatch preserves paths with spaces' "$NEMO_PATH" "$nemo_dispatch"
# 11b. Toggling the UI language refreshes already-written Nemo actions
# (regression: write_nemo_actions used to run once at enable-time only).
NEMO_LANG_HOME="$TMP_ROOT/home-nemo-lang-sync"; mkdir -p "$NEMO_LANG_HOME"
nemo_lang_to_en=$(HOME="$NEMO_LANG_HOME" bash -c '
source "$1"; SCRIPT_PATH="/tmp/scriptya.sh"; LANGUAGE=es
write_nemo_actions >/dev/null
toggle_language 0
cat "$NEMO_ACTIONS_DIR/scriptya-run.nemo_action"
' _ "$LIB")
assert_contains 'Language toggle refreshes Nemo action name (es->en)' 'Name=Run with Scriptya' "$nemo_lang_to_en"
assert_contains 'Language toggle refreshes Nemo action comment (es->en)' 'Comment=Run this script with Scriptya' "$nemo_lang_to_en"
NEMO_LANG_HOME2="$TMP_ROOT/home-nemo-lang-sync-back"; mkdir -p "$NEMO_LANG_HOME2"
nemo_lang_to_es=$(HOME="$NEMO_LANG_HOME2" bash -c '
source "$1"; SCRIPT_PATH="/tmp/scriptya.sh"; LANGUAGE=en
write_nemo_actions >/dev/null
toggle_language 0
cat "$NEMO_ACTIONS_DIR/scriptya-install.nemo_action"
' _ "$LIB")
assert_contains 'Language toggle refreshes Nemo action name (en->es)' 'Name=Instalar con Scriptya' "$nemo_lang_to_es"
NEMO_LANG_HOME3="$TMP_ROOT/home-nemo-lang-notoggle"; mkdir -p "$NEMO_LANG_HOME3"
nemo_lang_untouched=$(HOME="$NEMO_LANG_HOME3" bash -c '
source "$1"; SCRIPT_PATH="/tmp/scriptya.sh"; LANGUAGE=es
toggle_language 0 >/dev/null
[[ -f "$NEMO_ACTIONS_DIR/scriptya-run.nemo_action" ]] && echo EXISTS || echo MISSING
' _ "$LIB")
assert_eq 'Language toggle does not enable Nemo integration by itself' 'MISSING' "$nemo_lang_untouched"
# 12. Installation and uninstall smoke test in an isolated HOME.
HOME_INSTALL="$TMP_ROOT/home-install"; mkdir -p "$HOME_INSTALL/Scripts"
mkdir -p "$HOME_INSTALL/.local/bin"
if printf '1\n\n2\n2\n2\n2\n' | HOME="$HOME_INSTALL" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" --install >/dev/null 2>&1; then
ok 'Installation wizard completes'
else
fail 'Installation wizard completes'
fi
assert_contains 'Installed launcher exists' 'scriptya.sh' "$(find "$HOME_INSTALL/.local/share/scriptya" -maxdepth 1 -type f -name 'scriptya.sh' -printf '%f' 2>/dev/null)"
assert_contains 'Installed config stores language' 'LANGUAGE=en' "$(cat "$HOME_INSTALL/.config/scriptya/config.conf" 2>/dev/null)"
out=$(HOME="$HOME_INSTALL" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$HOME_INSTALL/.local/share/scriptya/scriptya.sh" --help 2>&1)
assert_contains 'Installed launcher starts in English' 'Usage:' "$out"
if printf 'y\nn\n' | HOME="$HOME_INSTALL" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" --uninstall >/dev/null 2>&1; then
ok 'Uninstall completes'
else
fail 'Uninstall completes'
fi
assert_not_contains 'Uninstall removes launcher copy' 'scriptya.sh' "$(find "$HOME_INSTALL/.local/share/scriptya" -maxdepth 1 -type f -name 'scriptya.sh' -printf '%f' 2>/dev/null)"
# 12b. Built-in onboarding example scripts (create_example_scripts) run
# as real, standalone processes and must not depend on scriptya's own
# functions (regression: they used to call "$(tr "...")" expecting the
# i18n helper, but a standalone process never sees it, so it silently
# ran the unrelated coreutils 'tr' instead and printed blank/garbled
# output with a stray error on stderr).
#
# Both language branches are generated with LANGUAGE explicitly set
# (regression: the previous version of this test only sourced the lib
# without setting LANGUAGE, so it silently depended on the locale of
# whichever machine ran the suite instead of on the code path itself).
EXAMPLES_DIR_ES="$TMP_ROOT/generated-examples-es"
HOME="$TMP_ROOT/home-examples-es" bash -c 'source "$1"; LANGUAGE=es; create_example_scripts "$2"' _ "$LIB" "$EXAMPLES_DIR_ES"
EXAMPLES_DIR_EN="$TMP_ROOT/generated-examples-en"
HOME="$TMP_ROOT/home-examples-en" bash -c 'source "$1"; LANGUAGE=en; create_example_scripts "$2"' _ "$LIB" "$EXAMPLES_DIR_EN"
for pair in "ES:$EXAMPLES_DIR_ES" "EN:$EXAMPLES_DIR_EN"; do
lbl="${pair%%:*}"; d="${pair#*:}"
assert_file_exists "Onboarding example ($lbl): system update script exists" "$d/actualizar_sistema.sh"
assert_file_exists "Onboarding example ($lbl): backup script exists" "$d/Backup/copia_documentos.sh"
assert_not_contains "Onboarding examples ($lbl) do not call the i18n tr() helper" '$(tr "' \
"$(cat "$d/actualizar_sistema.sh" "$d/Backup/copia_documentos.sh")"
done
APT_MOCK_BIN="$TMP_ROOT/apt-mock"; mkdir -p "$APT_MOCK_BIN"
printf '#!/bin/sh\nexit 0\n' > "$APT_MOCK_BIN/apt"; chmod +x "$APT_MOCK_BIN/apt"
update_out_es=$(PATH="$APT_MOCK_BIN:$PATH" bash "$EXAMPLES_DIR_ES/actualizar_sistema.sh" 2>&1)
assert_contains 'Onboarding update script (es) prints its real messages' 'Actualizando lista de paquetes...' "$update_out_es"
assert_contains 'Onboarding update script (es) prints completion message' 'Sistema actualizado.' "$update_out_es"
assert_not_contains 'Onboarding update script (es) has no tr: missing operand error' 'missing operand' "$update_out_es"
update_out_en=$(PATH="$APT_MOCK_BIN:$PATH" bash "$EXAMPLES_DIR_EN/actualizar_sistema.sh" 2>&1)
assert_contains 'Onboarding update script (en) prints its real messages' 'Updating package list...' "$update_out_en"
assert_contains 'Onboarding update script (en) prints completion message' 'System updated.' "$update_out_en"
# Deterministic xdg-user-dir coverage: build a minimal, controlled PATH
# per scenario instead of trusting whatever happens to be installed on
# the machine running the suite.
BASE_BIN="$TMP_ROOT/bin-coreutils-only"; mkdir -p "$BASE_BIN"
for tool in date mkdir cp bash; do
tool_path="$(command -v "$tool" 2>/dev/null)" && ln -sf "$tool_path" "$BASE_BIN/$tool"
done
# No source folder at all, and no xdg-user-dir on PATH either -> must
# fail cleanly with the real message (es and en), not with a bash
# "command not found" from "set -e" aborting early.
BACKUP_HOME_FAIL_ES="$TMP_ROOT/home-backup-fail-es"; mkdir -p "$BACKUP_HOME_FAIL_ES"
backup_fail_es=$(HOME="$BACKUP_HOME_FAIL_ES" PATH="$BASE_BIN" bash "$EXAMPLES_DIR_ES/Backup/copia_documentos.sh" 2>&1); backup_fail_es_rc=$?
assert_eq 'Backup (es) fails cleanly when there is no source folder' '1' "$backup_fail_es_rc"
assert_contains 'Backup (es) prints the real missing-folder message' 'No existe la carpeta de origen' "$backup_fail_es"
assert_not_contains 'Backup (es) has no tr: missing operand error' 'missing operand' "$backup_fail_es"
BACKUP_HOME_FAIL_EN="$TMP_ROOT/home-backup-fail-en"; mkdir -p "$BACKUP_HOME_FAIL_EN"
backup_fail_en=$(HOME="$BACKUP_HOME_FAIL_EN" PATH="$BASE_BIN" bash "$EXAMPLES_DIR_EN/Backup/copia_documentos.sh" 2>&1); backup_fail_en_rc=$?
assert_eq 'Backup (en) fails cleanly when there is no source folder' '1' "$backup_fail_en_rc"
assert_contains 'Backup (en) prints the real missing-folder message' 'Source folder not found' "$backup_fail_en"
# Case 1/3: xdg-user-dir is not installed at all. Without the "|| true"
# fix on "ORIGEN=$(xdg-user-dir ...)" this used to hit exit 127 under
# "set -euo pipefail" and abort before the Documents/Documentos
# fallback ever ran.
BACKUP_XDG_ABSENT="$TMP_ROOT/home-backup-xdg-absent"; mkdir -p "$BACKUP_XDG_ABSENT/Documentos"
: > "$BACKUP_XDG_ABSENT/Documentos/nota.txt"
: > "$BACKUP_XDG_ABSENT/fuera-de-documentos.txt"
backup_absent_out=$(HOME="$BACKUP_XDG_ABSENT" PATH="$BASE_BIN" bash "$EXAMPLES_DIR_EN/Backup/copia_documentos.sh" 2>&1); backup_absent_rc=$?
assert_eq 'Backup: xdg-user-dir missing does not abort the script' '0' "$backup_absent_rc"
assert_contains 'Backup: xdg-user-dir missing falls back and succeeds' 'Backup completed at:' "$backup_absent_out"
assert_file_exists 'Backup: xdg-user-dir missing copies the Documentos file' "$BACKUP_XDG_ABSENT/Backups/$(date +%Y-%m-%d)/nota.txt"
assert_not_contains 'Backup: xdg-user-dir missing does not copy the rest of HOME' 'fuera-de-documentos.txt' \
"$(find "$BACKUP_XDG_ABSENT/Backups" -type f 2>/dev/null)"
# Case 2/3: xdg-user-dir "succeeds" but answers $HOME itself (the real
# bug: a fresh user with no ~/.config/user-dirs.dirs). Must be treated
# the same as no answer and fall back, never backing up all of HOME.
XDG_BIN_RETURNS_HOME="$TMP_ROOT/bin-xdg-returns-home"; mkdir -p "$XDG_BIN_RETURNS_HOME"
printf '#!/bin/sh\necho "$HOME"\n' > "$XDG_BIN_RETURNS_HOME/xdg-user-dir"; chmod +x "$XDG_BIN_RETURNS_HOME/xdg-user-dir"
BACKUP_XDG_HOME="$TMP_ROOT/home-backup-xdg-returns-home"; mkdir -p "$BACKUP_XDG_HOME/Documentos"
: > "$BACKUP_XDG_HOME/Documentos/nota.txt"
: > "$BACKUP_XDG_HOME/fuera-de-documentos.txt"
backup_home_out=$(HOME="$BACKUP_XDG_HOME" PATH="$XDG_BIN_RETURNS_HOME:$BASE_BIN" bash "$EXAMPLES_DIR_EN/Backup/copia_documentos.sh" 2>&1); backup_home_rc=$?
assert_eq 'Backup: xdg-user-dir returning $HOME still succeeds via fallback' '0' "$backup_home_rc"
assert_file_exists 'Backup: xdg-user-dir returning $HOME copies the Documentos file' "$BACKUP_XDG_HOME/Backups/$(date +%Y-%m-%d)/nota.txt"
assert_not_contains 'Backup: xdg-user-dir returning $HOME never backs up all of HOME' 'fuera-de-documentos.txt' \
"$(find "$BACKUP_XDG_HOME/Backups" -type f 2>/dev/null)"
# Case 3/3: xdg-user-dir answers a real, different folder -> that
# answer must be trusted and used directly, not just the hardcoded
# Documents/Documentos fallback names.
XDG_BIN_RETURNS_REAL="$TMP_ROOT/bin-xdg-returns-real"; mkdir -p "$XDG_BIN_RETURNS_REAL"
printf '#!/bin/sh\necho "$HOME/MisArchivos"\n' > "$XDG_BIN_RETURNS_REAL/xdg-user-dir"; chmod +x "$XDG_BIN_RETURNS_REAL/xdg-user-dir"
BACKUP_XDG_REAL="$TMP_ROOT/home-backup-xdg-returns-real"; mkdir -p "$BACKUP_XDG_REAL/MisArchivos"
: > "$BACKUP_XDG_REAL/MisArchivos/archivo.txt"
backup_real_out=$(HOME="$BACKUP_XDG_REAL" PATH="$XDG_BIN_RETURNS_REAL:$BASE_BIN" bash "$EXAMPLES_DIR_EN/Backup/copia_documentos.sh" 2>&1); backup_real_rc=$?
assert_eq 'Backup: xdg-user-dir returning a real path succeeds' '0' "$backup_real_rc"
assert_file_exists 'Backup: xdg-user-dir returning a real path copies its file' "$BACKUP_XDG_REAL/Backups/$(date +%Y-%m-%d)/archivo.txt"
# End-to-end: the actual installer wizard (--install), not just the
# function in isolation, with LANGUAGE=en and accepting the onboarding
# examples prompt. This is the exact path the reported symptom went
# through: examples appearing in Spanish while the UI was in English.
E2E_HOME="$TMP_ROOT/home-examples-e2e"; mkdir -p "$E2E_HOME/Scripts" "$E2E_HOME/.local/bin"
if printf '1\ny\n2\n2\nn\n' | HOME="$E2E_HOME" LANGUAGE=en TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" --install >/dev/null 2>&1; then
ok 'End-to-end --install (LANGUAGE=en) accepting example scripts completes'
else
fail 'End-to-end --install (LANGUAGE=en) accepting example scripts completes'
fi
assert_contains 'End-to-end install: example script is created in English' 'Updates system packages' \
"$(cat "$E2E_HOME/Scripts/actualizar_sistema.sh" 2>/dev/null)"
assert_not_contains 'End-to-end install: example script is not left in Spanish' 'Actualiza los paquetes del sistema' \
"$(cat "$E2E_HOME/Scripts/actualizar_sistema.sh" 2>/dev/null)"
# 13. Documentation links and structure.
assert_contains 'English README links to Spanish README' 'README_ES.md' "$(cat "$ROOT_DIR/README.md")"
assert_contains 'Spanish README links to English README' 'README.md' "$(cat "$ROOT_DIR/README_ES.md")"
# 14. Terminal layout smoke check: render-less width check of the main menu.
width_check() {
local name="$1" out="$2" width=88 max=0 line clean len
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed -E $'s/\\x1B\\[[0-9;?]*[ -\\/]*[@-~]//g')
len=${#clean}
(( len > max )) && max=$len
if (( len > width )); then
fail "$name line width ($len > $width): $clean"
return 0
fi
done <<< "$out"
ok "$name line width (max $max/$width)"
}
width_check 'English menu aesthetics' "$(run_cli "$HOME_EN" <<< '0' 2>&1)"
HOME_ES="$TMP_ROOT/home-es"; mkdir -p "$HOME_ES/Scripts" "$HOME_ES/.config/scriptya"
printf 'LANGUAGE=es\n' > "$HOME_ES/.config/scriptya/config.conf"
width_check 'Spanish menu aesthetics' "$(run_cli "$HOME_ES" <<< '0' 2>&1)"
# 15. Regression corpus for previously leaked English/Spanish UI strings.
CORPUS_EN_EXTRA=(
"Pulsa una tecla para continuar...|Press any key to continue..."
"Pulsa una tecla para cerrar...|Press any key to close..."
"Elige una aplicación instalada (Esc para cancelar)|Choose an installed application (Esc to cancel)"
"¿A cuál le cambias el icono?: |Which one do you want to change the icon for: "
" 1) Escritorio| 1) Desktop"
)
for pair in "${CORPUS_EN_EXTRA[@]}"; do
src=${pair%%|*}; expected=${pair#*|}
actual=$(bash -c 'source "$1"; LANGUAGE=en; tr "$2"' _ "$LIB" "$src")
assert_eq "Extra English: $src" "$expected" "$actual"
done
# 16. Locale variants and explicit preference precedence.
for locale in es_ES.UTF-8 es-ES.UTF-8 ES_es.UTF-8 es@traditional; do
actual=$(bash -c 'source "$1"; LC_ALL=C; LC_MESSAGES=; LANG=C; locale_value="$2"; locale_value="${locale_value,,}"; [[ "$locale_value" == es || "$locale_value" == es_* || "$locale_value" == es.* || "$locale_value" == es@* || "$locale_value" == es-* ]] && printf es || printf en' _ "$LIB" "$locale")
assert_eq "Spanish locale variant: $locale" 'es' "$actual"
done
actual=$(bash -c 'source "$1"; LC_ALL=C; LC_MESSAGES=es_ES.UTF-8; LANG=es_ES.UTF-8; detect_system_language' _ "$LIB")
assert_eq 'LC_ALL overrides Spanish LC_MESSAGES' 'en' "$actual"
# 17. Language toggle preserves unrelated configuration fields exactly.
HOME_PRESERVE="$TMP_ROOT/home-preserve"; mkdir -p "$HOME_PRESERVE/.config/scriptya"
printf 'SCRIPTS_DIR=%q\nTERMINAL=%q\nSY_ICON=%q\n' \
'/tmp/space path/$x' 'x-terminal-emulator -e' 'theme icon' > "$HOME_PRESERVE/.config/scriptya/config.conf"
old_cfg=$(cat "$HOME_PRESERVE/.config/scriptya/config.conf")
if HOME="$HOME_PRESERVE" bash -c 'source "$1"; CONFIG_DIR="$HOME/.config/scriptya"; CONFIG_FILE="$CONFIG_DIR/config.conf"; LANGUAGE=en; set_language es' _ "$LIB"; then ok 'Language toggle preserves config file'; else fail 'Language toggle preserves config file'; fi
assert_contains 'Preserved SCRIPTS_DIR after toggle' 'SCRIPTS_DIR=/tmp/space\ path/\$x' "$(cat "$HOME_PRESERVE/.config/scriptya/config.conf")"
assert_contains 'Preserved TERMINAL after toggle' 'TERMINAL=x-terminal-emulator\ -e' "$(cat "$HOME_PRESERVE/.config/scriptya/config.conf")"
assert_contains 'Preserved SY_ICON after toggle' 'SY_ICON=theme\ icon' "$(cat "$HOME_PRESERVE/.config/scriptya/config.conf")"
assert_contains 'Toggle adds LANGUAGE without rewriting other fields' 'LANGUAGE=es' "$(cat "$HOME_PRESERVE/.config/scriptya/config.conf")"
# 18. Core script execution keeps its exit status and output semantics across languages.
RUN_SCRIPT="$TMP_ROOT/behavior.sh"
cat > "$RUN_SCRIPT" <<'EOF'
#!/bin/bash
printf 'BEHAVIOR_OK\n'
exit 7
EOF
chmod +x "$RUN_SCRIPT"
for lang in en es; do
home="$TMP_ROOT/home-run-$lang"; mkdir -p "$home/Scripts"
cfg="$home/.config/scriptya"; mkdir -p "$cfg"
printf 'LANGUAGE=%s\nSCRIPTS_DIR=%q\n' "$lang" "$home/Scripts" > "$cfg/config.conf"
out=$(printf "x" | HOME="$home" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" --run-script "$RUN_SCRIPT" 2>&1) || status=$?
status=${status:-0}
assert_eq "Run-script status is unchanged in $lang" '7' "$status"
assert_contains "Run-script payload is unchanged in $lang" 'BEHAVIOR_OK' "$out"
unset status
done
# 20. No known Spanish UI labels may leak from direct prompts in English mode.
english_ui=$(HOME="$HOME_EN" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" --help 2>&1)
assert_not_contains 'English help has no Spanish usage header' 'Uso:' "$english_ui"
assert_not_contains 'English help has no Spanish toggle wording' 'Intercambia Español / Inglés' "$english_ui"
assert_not_contains 'Script has no unwrapped Spanish option prompt' 'read -r -p "Opción [$def]' "$(grep -n 'read -r -p "Opción \[\$def\]' "$SCRIPT" || true)"
# 20b. English Nemo uninstall summary must use the translated label too.
NEMO_LANG_HOME="$TMP_ROOT/home-nemo-lang"
mkdir -p "$NEMO_LANG_HOME/.local/share/scriptya/registry"
NEMO_TEST_SCRIPT="$NEMO_LANG_HOME/test.pl"
printf '#!/usr/bin/env perl\n' > "$NEMO_TEST_SCRIPT"
NEMO_ID=$(HOME="$NEMO_LANG_HOME" bash -c 'source "$1"; registry_id_for "$2"' _ "$LIB" "$NEMO_TEST_SCRIPT")
HOME="$NEMO_LANG_HOME" bash -c 'source "$1"; registry_write "$2" "Demo" "$3" "utilities-terminal" no no perl' _ "$LIB" "$NEMO_ID" "$NEMO_TEST_SCRIPT"
nemo_lang_out=$(HOME="$NEMO_LANG_HOME" LANGUAGE=en bash -c 'source "$1"; LANGUAGE=en; pause(){ :; }; confirm_yn(){ return 1; }; nemo_uninstall_script "$2"' _ "$LIB" "$NEMO_TEST_SCRIPT" 2>&1)
assert_contains 'English Nemo uninstall summary is translated' 'The following will be uninstalled: Demo' "$nemo_lang_out"
assert_not_contains 'English Nemo uninstall summary has no Spanish label' 'Se desinstalará: Demo' "$nemo_lang_out"
# 20c. English Nemo uninstall must also translate the "not installed" warning.
NEMO_NOTINST_HOME="$TMP_ROOT/home-nemo-uninstall-notinstalled"; mkdir -p "$NEMO_NOTINST_HOME"
NEMO_NOTINST_SCRIPT="$NEMO_NOTINST_HOME/notinstalled.sh"
printf '#!/bin/bash\necho hi\n' > "$NEMO_NOTINST_SCRIPT"
nemo_notinst_out=$(HOME="$NEMO_NOTINST_HOME" LANGUAGE=en bash -c 'source "$1"; pause(){ :; }; nemo_uninstall_script "$2"' _ "$LIB" "$NEMO_NOTINST_SCRIPT" 2>&1)
assert_contains 'English Nemo uninstall warns when not installed' 'This script is not installed as a standalone application.' "$nemo_notinst_out"
assert_not_contains 'English Nemo "not installed" warning has no Spanish text' 'no está instalado' "$nemo_notinst_out"
# 20d. English Nemo uninstall, once confirmed, really removes the app and says so in English.
NEMO_OK_HOME="$TMP_ROOT/home-nemo-uninstall-ok"; mkdir -p "$NEMO_OK_HOME/.local/share/scriptya/registry"
NEMO_OK_SCRIPT="$NEMO_OK_HOME/installed.pl"
printf '#!/usr/bin/env perl\n' > "$NEMO_OK_SCRIPT"
NEMO_OK_ID=$(HOME="$NEMO_OK_HOME" bash -c 'source "$1"; registry_id_for "$2"' _ "$LIB" "$NEMO_OK_SCRIPT")
HOME="$NEMO_OK_HOME" bash -c 'source "$1"; registry_write "$2" "Demo" "$3" "utilities-terminal" no no perl' _ "$LIB" "$NEMO_OK_ID" "$NEMO_OK_SCRIPT"
nemo_ok_out=$(HOME="$NEMO_OK_HOME" LANGUAGE=en bash -c 'source "$1"; pause(){ :; }; confirm_yn(){ return 0; }; nemo_uninstall_script "$2"' _ "$LIB" "$NEMO_OK_SCRIPT" 2>&1)
assert_contains 'English Nemo uninstall confirms success' 'Uninstalled.' "$nemo_ok_out"
[[ ! -f "$NEMO_OK_HOME/.local/share/scriptya/registry/$NEMO_OK_ID.meta" ]] && ok 'English Nemo uninstall removes the registry entry' || fail 'English Nemo uninstall removes the registry entry'
# 20e. English Nemo uninstall translates the failure message when removal cannot finish.
NEMO_FAIL_HOME="$TMP_ROOT/home-nemo-uninstall-fail"; mkdir -p "$NEMO_FAIL_HOME/.local/share/scriptya/registry"
NEMO_FAIL_SCRIPT="$NEMO_FAIL_HOME/installed.pl"
printf '#!/usr/bin/env perl\n' > "$NEMO_FAIL_SCRIPT"
NEMO_FAIL_ID=$(HOME="$NEMO_FAIL_HOME" bash -c 'source "$1"; registry_id_for "$2"' _ "$LIB" "$NEMO_FAIL_SCRIPT")
HOME="$NEMO_FAIL_HOME" bash -c 'source "$1"; registry_write "$2" "Demo" "$3" "utilities-terminal" no no perl' _ "$LIB" "$NEMO_FAIL_ID" "$NEMO_FAIL_SCRIPT"
nemo_fail_out=$(HOME="$NEMO_FAIL_HOME" LANGUAGE=en bash -c 'source "$1"; pause(){ :; }; confirm_yn(){ return 0; }; remove_script_icon(){ return 1; }; nemo_uninstall_script "$2"' _ "$LIB" "$NEMO_FAIL_SCRIPT" 2>&1)
assert_contains 'English Nemo uninstall failure is translated' 'Could not fully uninstall' "$nemo_fail_out"
assert_not_contains 'English Nemo uninstall failure has no Spanish text' 'No se pudo desinstalar' "$nemo_fail_out"
# 20f. English Nemo "Change icon" on an already-installed app delegates to it (no reinstall).
NEMO_CHI_HOME="$TMP_ROOT/home-nemo-change-icon"; mkdir -p "$NEMO_CHI_HOME/.local/share/scriptya/registry"
NEMO_CHI_SCRIPT="$NEMO_CHI_HOME/installed.pl"
printf '#!/usr/bin/env perl\n' > "$NEMO_CHI_SCRIPT"
NEMO_CHI_ID=$(HOME="$NEMO_CHI_HOME" bash -c 'source "$1"; registry_id_for "$2"' _ "$LIB" "$NEMO_CHI_SCRIPT")
HOME="$NEMO_CHI_HOME" bash -c 'source "$1"; registry_write "$2" "Demo" "$3" "utilities-terminal" no no perl' _ "$LIB" "$NEMO_CHI_ID" "$NEMO_CHI_SCRIPT"
nemo_chi_out=$(HOME="$NEMO_CHI_HOME" LANGUAGE=en bash -c 'source "$1"; change_icon_for_script(){ printf "CHANGE_ICON_DISPATCH:%s\n" "$1"; }; nemo_change_icon_script "$2"' _ "$LIB" "$NEMO_CHI_SCRIPT" 2>&1)
assert_eq 'English Nemo change icon dispatches to the installed app' "CHANGE_ICON_DISPATCH:$NEMO_CHI_ID" "$nemo_chi_out"
# 20g. English Nemo "Change icon" on an uninstalled script offers to install it first.
NEMO_CHI2_HOME="$TMP_ROOT/home-nemo-change-icon-notinstalled"; mkdir -p "$NEMO_CHI2_HOME"
NEMO_CHI2_SCRIPT="$NEMO_CHI2_HOME/notinstalled.sh"
printf '#!/bin/bash\necho hi\n' > "$NEMO_CHI2_SCRIPT"
nemo_chi2_decline=$(HOME="$NEMO_CHI2_HOME" LANGUAGE=en bash -c 'source "$1"; pause(){ :; }; confirm_yn(){ return 1; }; install_icon_for_script(){ printf "SCRIPT_DISPATCH:%s\n" "$1"; }; nemo_change_icon_script "$2"' _ "$LIB" "$NEMO_CHI2_SCRIPT" 2>&1)
assert_contains 'English Nemo change icon warns when not installed' 'This script is not installed as a standalone application.' "$nemo_chi2_decline"
assert_contains 'English Nemo change icon explains why it must be installed' 'It must be installed this way to give it its own icon.' "$nemo_chi2_decline"
assert_not_contains 'English Nemo change icon does not install without confirmation' 'SCRIPT_DISPATCH' "$nemo_chi2_decline"
nemo_chi2_confirm=$(HOME="$NEMO_CHI2_HOME" LANGUAGE=en bash -c 'source "$1"; pause(){ :; }; confirm_yn(){ return 0; }; install_icon_for_script(){ printf "SCRIPT_DISPATCH:%s\n" "$1"; }; nemo_change_icon_script "$2"' _ "$LIB" "$NEMO_CHI2_SCRIPT" 2>&1)
assert_contains 'English Nemo change icon installs the script when confirmed' "SCRIPT_DISPATCH:$NEMO_CHI2_SCRIPT" "$nemo_chi2_confirm"
# 20h. Same flow for an uninstalled HTML page: the label and the dispatch differ from a script.
NEMO_CHI3_HOME="$TMP_ROOT/home-nemo-change-icon-html"; mkdir -p "$NEMO_CHI3_HOME"
NEMO_CHI3_HTML="$NEMO_CHI3_HOME/page.html"
printf '<html></html>\n' > "$NEMO_CHI3_HTML"
nemo_chi3_out=$(HOME="$NEMO_CHI3_HOME" LANGUAGE=en bash -c 'source "$1"; pause(){ :; }; confirm_yn(){ return 0; }; install_icon_for_html(){ printf "HTML_DISPATCH:%s\n" "$1"; }; nemo_change_icon_script "$2"' _ "$LIB" "$NEMO_CHI3_HTML" 2>&1)
assert_contains 'English Nemo change icon labels an uninstalled HTML file as a web page' 'This web page is not installed as a standalone application.' "$nemo_chi3_out"
assert_contains 'English Nemo change icon installs the HTML page when confirmed' "HTML_DISPATCH:$NEMO_CHI3_HTML" "$nemo_chi3_out"
# 21. Real English error paths must not leak Spanish UI text.
out=$(HOME="$HOME_EN" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" --bogus 2>&1)
assert_contains 'Unknown option is translated' 'Unknown option: --bogus' "$out"
assert_not_contains 'Unknown option has no Spanish label' 'Opción desconocida' "$out"
for arg in --run-script --install-script --uninstall-script --change-icon-script --nemo-run; do
out=$(HOME="$HOME_EN" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" "$arg" 2>&1)
assert_contains "Missing path usage is translated ($arg)" 'Usage:' "$out"
assert_contains "Missing path placeholder is English ($arg)" '<script-path>' "$out"
assert_not_contains "Missing path usage has no Spanish placeholder ($arg)" '<ruta-al-script>' "$out"
done
HOME_INVALID="$TMP_ROOT/home-invalid-dir"; mkdir -p "$HOME_INVALID/.config/scriptya"; touch "$HOME_INVALID/Scripts-file"
printf 'SCRIPTS_DIR=%q\n' "$HOME_INVALID/Scripts-file" > "$HOME_INVALID/.config/scriptya/config.conf"
out=$(HOME="$HOME_INVALID" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" <<< $'n\n' 2>&1)
assert_contains 'Invalid scripts directory error is translated' 'Could not create' "$out"
assert_not_contains 'Invalid scripts directory error has no Spanish text' 'No se pudo crear' "$out"
# 22. Test the actual locale detector, including variants, not a duplicate implementation.
for locale in es_ES.UTF-8 es-ES.UTF-8 ES_es.UTF-8 es@traditional es; do
actual=$(env LC_ALL= LC_MESSAGES= LANG="$locale" bash -c 'source "$1"; detect_system_language' _ "$LIB" 2>/dev/null)
assert_eq "Real detector Spanish variant: $locale" 'es' "$actual"
done
for locale in C C.UTF-8 en_US.UTF-8 fr_FR.UTF-8 de_DE.UTF-8; do
actual=$(env LC_ALL= LC_MESSAGES= LANG="$locale" bash -c 'source "$1"; detect_system_language' _ "$LIB" 2>/dev/null)
assert_eq "Real detector non-Spanish fallback: $locale" 'en' "$actual"
done
actual=$(env LC_ALL= LC_MESSAGES=en_US.UTF-8 LANG=es_ES.UTF-8 bash -c 'source "$1"; detect_system_language' _ "$LIB" 2>/dev/null)
assert_eq 'Real detector honors LC_MESSAGES over LANG' 'en' "$actual"
actual=$(env LC_ALL=en_US.UTF-8 LC_MESSAGES=es_ES.UTF-8 LANG=es_ES.UTF-8 bash -c 'source "$1"; detect_system_language' _ "$LIB" 2>/dev/null)
assert_eq 'Real detector honors LC_ALL over lower locale categories' 'en' "$actual"
# 23. Uppercase one-letter language switch works and preserves the two-state toggle.
HOME_UPPER="$TMP_ROOT/home-upper"; mkdir -p "$HOME_UPPER/Scripts"
out=$(HOME="$HOME_UPPER" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" L 2>&1)
assert_contains 'Uppercase L switches to Spanish' 'Idioma cambiado a Español.' "$out"
assert_eq 'Uppercase L stores Spanish' 'es' "$(sed -n 's/^LANGUAGE=//p' "$HOME_UPPER/.config/scriptya/config.conf")"
out=$(HOME="$HOME_UPPER" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" L 2>&1)
assert_contains 'Uppercase L switches back to English' 'Language changed to English.' "$out"
assert_eq 'Second uppercase L stores English' 'en' "$(sed -n 's/^LANGUAGE=//p' "$HOME_UPPER/.config/scriptya/config.conf")"
# 24. Stored language preference overrides automatic locale detection in both directions.
HOME_PREF_EN="$TMP_ROOT/home-pref-en"; mkdir -p "$HOME_PREF_EN/Scripts" "$HOME_PREF_EN/.config/scriptya"
printf 'LANGUAGE=en\n' > "$HOME_PREF_EN/.config/scriptya/config.conf"
out=$(HOME="$HOME_PREF_EN" TERM=xterm NO_COLOR=1 LC_ALL= LC_MESSAGES=es_ES.UTF-8 LANG=es_ES.UTF-8 bash "$SCRIPT" --help 2>&1)
assert_contains 'Stored English preference overrides Spanish system locale' 'Usage:' "$out"
assert_not_contains 'Stored English preference has no Spanish help header' 'Uso:' "$out"
HOME_PREF_ES="$TMP_ROOT/home-pref-es"; mkdir -p "$HOME_PREF_ES/Scripts" "$HOME_PREF_ES/.config/scriptya"
printf 'LANGUAGE=es\n' > "$HOME_PREF_ES/.config/scriptya/config.conf"
out=$(HOME="$HOME_PREF_ES" TERM=xterm NO_COLOR=1 LC_ALL=C LC_MESSAGES=C LANG=C bash "$SCRIPT" --help 2>&1)
assert_contains 'Stored Spanish preference overrides non-Spanish system locale' 'Uso:' "$out"
assert_not_contains 'Stored Spanish preference has no English help header' 'Usage:' "$out"
HOME_AUTO_ES="$TMP_ROOT/home-auto-es"; mkdir -p "$HOME_AUTO_ES/Scripts"
out=$(HOME="$HOME_AUTO_ES" TERM=xterm NO_COLOR=1 LC_ALL= LC_MESSAGES=es_ES.UTF-8 LANG=es_ES.UTF-8 bash "$SCRIPT" --help 2>&1)
assert_contains 'No-language config auto-detects Spanish on first start' 'Uso:' "$out"
assert_not_contains 'No-language config does not default to English on Spanish locale' 'Usage:' "$out"
# 25. HTML files are first-class entries in the managed scripts tree.
HTML_ROOT="$TMP_ROOT/html-tree"
mkdir -p "$HTML_ROOT/sub" "$HTML_ROOT/.hidden"
touch "$HTML_ROOT/app.sh" "$HTML_ROOT/page.html" "$HTML_ROOT/page.htm" "$HTML_ROOT/.hidden/secret.html"
listed=$(bash -c 'source "$1"; list_scripts "$2"' _ "$LIB" "$HTML_ROOT")
assert_contains 'Managed list includes .sh files' 'app.sh' "$listed"
assert_contains 'Managed list includes .html files' 'page.html' "$listed"
assert_contains 'Managed list includes .htm files' 'page.htm' "$listed"
assert_not_contains 'Managed list excludes hidden HTML files' 'secret.html' "$listed"
# 26. HTML metadata is read from the HTML-comment form, including after DOCTYPE.
HTML_META="$HTML_ROOT/metadata.html"
cat > "$HTML_META" <<'HTML_META_EOF'
<!DOCTYPE html>
<!-- MENU: My Dashboard -->
<!-- DESCRIPTION: Useful local dashboard -->
<!-- CONFIRM: true -->
<!-- TERMINAL: true -->
<!-- SUDO: true -->
<!-- ORDER: 012 -->
<!-- ICON: assets/icon.png -->
<!-- ASK: ignored-for-html -->
<html><body><h1>Hello</h1></body></html>
HTML_META_EOF
meta=$(bash -c 'source "$1"; read_metadata "$2"; printf "%s|%s|%s|%s|%s|%s|%s|%s\n" "$META_MENU" "$META_DESCRIPTION" "$META_CONFIRM" "$META_TERMINAL" "$META_SUDO" "$META_ORDER" "$META_ICON" "${META_ASK[0]:-}"' _ "$LIB" "$HTML_META")
assert_eq 'HTML metadata parsing' 'My Dashboard|Useful local dashboard|true|true|true|12|assets/icon.png|ignored-for-html' "$meta"
# 27. HTML metadata detection recognizes only the file header, not body comments.
HTML_BODY_COMMENT="$HTML_ROOT/body-comment.html"
cat > "$HTML_BODY_COMMENT" <<'HTML_BODY_EOF'
<!DOCTYPE html>
<html><body>
<!-- MENU: Should Not Be Read -->
<h1>Real page</h1>
</body></html>
HTML_BODY_EOF
meta=$(bash -c 'source "$1"; read_metadata "$2"; printf "%s|%s\n" "$META_MENU" "$META_DESCRIPTION"' _ "$LIB" "$HTML_BODY_COMMENT")
assert_eq 'Body HTML comments are not treated as metadata' 'body-comment|' "$meta"
if bash -c 'source "$1"; script_has_metadata "$2"' _ "$LIB" "$HTML_BODY_COMMENT"; then
fail 'Body HTML comments do not count as metadata'
else
ok 'Body HTML comments do not count as metadata'
fi
# 28. Inserting HTML metadata preserves the DOCTYPE and document content.
HTML_EDIT="$HTML_ROOT/edit.html"
cat > "$HTML_EDIT" <<'HTML_EDIT_EOF'
<!DOCTYPE html>
<!-- Author note: keep this -->
<!-- MENU: Old title -->
<!-- DESCRIPTION: Old description -->
<html>
<head><title>Original title</title></head>
<body><p data-test="123">Original content</p></body>
</html>
HTML_EDIT_EOF
html_body_before=$(sed -n '/^<html>/,$p' "$HTML_EDIT")
HOME="$TMP_ROOT/home-html-edit" bash -c 'source "$1"; NEWMETA_MENU="New title"; NEWMETA_DESCRIPTION="New description"; NEWMETA_CONFIRM=true; NEWMETA_TERMINAL=false; NEWMETA_SUDO=false; NEWMETA_ORDER=7; NEWMETA_ICON="icon.png"; NEWMETA_ASK=("unused"); apply_metadata "$2"' _ "$LIB" "$HTML_EDIT"
html_after=$(cat "$HTML_EDIT")
assert_contains 'Inserted HTML MENU metadata' '<!-- MENU: New title -->' "$html_after"
assert_contains 'Inserted HTML DESCRIPTION metadata' '<!-- DESCRIPTION: New description -->' "$html_after"
assert_contains 'Inserted HTML CONFIRM metadata' '<!-- CONFIRM: true -->' "$html_after"
assert_contains 'Inserted HTML ORDER metadata' '<!-- ORDER: 7 -->' "$html_after"
assert_contains 'Inserted HTML ICON metadata' '<!-- ICON: icon.png -->' "$html_after"
assert_contains 'HTML DOCTYPE is preserved' '<!DOCTYPE html>' "$html_after"
assert_contains 'Non-metadata HTML header comment is preserved' '<!-- Author note: keep this -->' "$html_after"
html_body_after=$(sed -n '/^<html>/,$p' "$HTML_EDIT")
assert_eq 'HTML document body is unchanged by metadata insertion' "$html_body_before" "$html_body_after"
# 29. Removing HTML metadata preserves the rest of the document.
HOME="$TMP_ROOT/home-html-edit" bash -c 'source "$1"; NEWMETA_MENU=""; NEWMETA_DESCRIPTION=""; NEWMETA_CONFIRM=false; NEWMETA_TERMINAL=false; NEWMETA_SUDO=false; NEWMETA_ORDER=500; NEWMETA_ICON=""; NEWMETA_ASK=(); apply_metadata "$2"' _ "$LIB" "$HTML_EDIT"
html_after_remove=$(cat "$HTML_EDIT")
assert_not_contains 'HTML MENU metadata can be removed' '<!-- MENU:' "$html_after_remove"
assert_not_contains 'HTML DESCRIPTION metadata can be removed' '<!-- DESCRIPTION:' "$html_after_remove"
assert_contains 'HTML author comment survives metadata removal' '<!-- Author note: keep this -->' "$html_after_remove"
assert_eq 'HTML body survives metadata removal' "$html_body_before" "$(sed -n '/^<html>/,$p' "$HTML_EDIT")"
# 30. HTML ORDER participates in the same sorted menu as shell scripts.
cat > "$HTML_ROOT/late.html" <<'HTML_LATE_EOF'
<!DOCTYPE html>
<!-- ORDER: 30 -->
<html><body>late</body></html>
HTML_LATE_EOF
cat > "$HTML_ROOT/early.html" <<'HTML_EARLY_EOF'
<!DOCTYPE html>
<!-- ORDER: 3 -->
<html><body>early</body></html>
HTML_EARLY_EOF
sorted=$(bash -c 'source "$1"; sorted_scripts "$2" | xargs -n1 basename' _ "$LIB" "$HTML_ROOT")
first=$(printf '%s\n' "$sorted" | head -n1)
assert_eq 'HTML ORDER places lower numbers first' 'early.html' "$first"
# 31. --run-script opens HTML with xdg-open and does not execute it as Bash/sudo.
MOCK_BIN="$TMP_ROOT/mock-bin"; mkdir -p "$MOCK_BIN"
cat > "$MOCK_BIN/xdg-open" <<'MOCK_XDG_EOF'
#!/bin/bash
printf '%s\n' "$*" >> "$XDG_OPEN_LOG"
exit 0
MOCK_XDG_EOF
cat > "$MOCK_BIN/sudo" <<'MOCK_SUDO_EOF'
#!/bin/bash
printf 'SUDO_USED\n' >> "$XDG_OPEN_LOG"
exit 99
MOCK_SUDO_EOF
chmod +x "$MOCK_BIN/xdg-open" "$MOCK_BIN/sudo"
HTML_RUN="$HTML_ROOT/run.html"
cat > "$HTML_RUN" <<'HTML_RUN_EOF'
<!DOCTYPE html>
<!-- MENU: Run HTML -->
<!-- DESCRIPTION: Opens the local page -->
<!-- SUDO: true -->
<!-- TERMINAL: true -->
<html><body><h1>Run</h1></body></html>
HTML_RUN_EOF
: > "$TMP_ROOT/xdg-open.log"
for lang in en es; do
home="$TMP_ROOT/home-html-run-$lang"; mkdir -p "$home/.config/scriptya"
printf 'LANGUAGE=%s\n' "$lang" > "$home/.config/scriptya/config.conf"
out=$(XDG_OPEN_LOG="$TMP_ROOT/xdg-open.log" PATH="$MOCK_BIN:$PATH" HOME="$home" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash "$SCRIPT" --run-script "$HTML_RUN" <<< 'x' 2>&1)
status=$?
assert_eq "HTML run exits successfully in $lang" '0' "$status"
assert_contains "HTML run displays the metadata title in $lang" 'Run HTML' "$out"
done
assert_eq 'HTML launch was recorded once per language' '2' "$(grep -cF "$HTML_RUN" "$TMP_ROOT/xdg-open.log" || true)"
assert_not_contains 'HTML launch never invokes sudo' 'SUDO_USED' "$(cat "$TMP_ROOT/xdg-open.log")"
assert_eq 'HTML launch target is identical in both languages' "$HTML_RUN" "$(head -n1 "$TMP_ROOT/xdg-open.log")"
assert_eq 'HTML launch target remains identical on second language run' "$HTML_RUN" "$(tail -n1 "$TMP_ROOT/xdg-open.log")"
# 32. HTML CONFIRM metadata is honored.
HTML_CONFIRM="$HTML_ROOT/confirm.html"
cat > "$HTML_CONFIRM" <<'HTML_CONFIRM_EOF'
<!DOCTYPE html>
<!-- MENU: Confirm me -->
<!-- CONFIRM: true -->
<html><body>confirm</body></html>
HTML_CONFIRM_EOF
: > "$TMP_ROOT/xdg-confirm.log"
home="$TMP_ROOT/home-html-confirm"; mkdir -p "$home/.config/scriptya"; printf 'LANGUAGE=en\n' > "$home/.config/scriptya/config.conf"
PATH="$MOCK_BIN:$PATH" HOME="$home" XDG_OPEN_LOG="$TMP_ROOT/xdg-confirm.log" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C \
bash "$SCRIPT" --run-script "$HTML_CONFIRM" <<< $'n\nx' > "$TMP_ROOT/html-confirm-no.out" 2>&1 || true
assert_not_contains 'HTML confirmation cancels without launching' "$HTML_CONFIRM" "$(cat "$TMP_ROOT/xdg-confirm.log")"
PATH="$MOCK_BIN:$PATH" HOME="$home" XDG_OPEN_LOG="$TMP_ROOT/xdg-confirm.log" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C \
bash "$SCRIPT" --run-script "$HTML_CONFIRM" <<< $'y\nx' > "$TMP_ROOT/html-confirm-yes.out" 2>&1
assert_contains 'HTML confirmation allows launch' "$HTML_CONFIRM" "$(cat "$TMP_ROOT/xdg-confirm.log")"
# 33. --icons uses HTML metadata for the standalone application's name and description.
HTML_ICON="$HTML_ROOT/icon-app.html"
cat > "$HTML_ICON" <<'HTML_ICON_EOF'
<!DOCTYPE html>
<!-- MENU: Dashboard App -->
<!-- DESCRIPTION: My dashboard description -->
<html><body>app</body></html>
HTML_ICON_EOF
HOME="$TMP_ROOT/home-html-icons" bash -c '
source "$1"
APPS_DIR="$2/.local/share/applications"
DESKTOP_DIR="$2/Desktop"
REGISTRY_DIR="$2/.local/share/scriptya/registry"
ICONS_DIR="$2/.local/share/scriptya/icons"
mkdir -p "$APPS_DIR" "$DESKTOP_DIR" "$REGISTRY_DIR" "$ICONS_DIR"
choose_and_process_icon() { PICKED_ICON_FINAL="utilities-terminal"; }
finalize_menu_entry() { :; }
finalize_desktop_shortcut() { :; }
pause() { :; }
install_icon_for_html "$3" <<HTML_INPUT_EOF
3
HTML_INPUT_EOF
printf "%s\n" "$(registry_id_for "$3")"
' _ "$LIB" "$TMP_ROOT/home-html-icons" "$HTML_ICON" > "$TMP_ROOT/html-icon-install.out"
id="$(HOME="$TMP_ROOT/home-html-icons" bash -c 'source "$1"; registry_id_for "$2"' _ "$LIB" "$HTML_ICON")"
desktop="$TMP_ROOT/home-html-icons/.local/share/applications/scriptya-app-$id.desktop"
assert_contains 'HTML standalone app uses metadata name' 'Name=Dashboard App' "$(cat "$desktop")"
assert_contains 'HTML standalone app uses metadata description' 'Comment=My dashboard description' "$(cat "$desktop")"
assert_contains 'HTML standalone app uses xdg-open' 'Exec=xdg-open ' "$(cat "$desktop")"
assert_eq 'HTML standalone app registry type is html' 'html' "$(HOME="$TMP_ROOT/home-html-icons" bash -c 'source "$1"; registry_read "$2"; printf "%s\n" "$REG_TYPE"' _ "$LIB" "$id")"
# 34. The Install-Scripts picker dispatches HTML to the HTML installer.
selected_html="$HTML_ROOT/selected.html"
: > "$selected_html"
out=$(bash -c 'source "$1"; selected="$2"; navigate(){ PICKED_SCRIPT="$selected"; return 0; }; install_icon_for_html(){ printf "HTML_DISPATCH:%s\n" "$1"; }; install_icon_for_script(){ printf "SCRIPT_DISPATCH:%s\n" "$1"; }; SCRIPTS_DIR="$(dirname "$selected")"; pick_script_and_install_icon' _ "$LIB" "$selected_html")
assert_contains 'Install picker dispatches HTML to HTML installer' "HTML_DISPATCH:$selected_html" "$out"
# 35. HTML metadata and launch target are language-invariant.
for lang in en es; do
meta_lang=$(LANGUAGE="$lang" HOME="$TMP_ROOT/home-lang-$lang" bash -c 'source "$1"; read_metadata "$2"; printf "%s|%s|%s\n" "$META_MENU" "$META_DESCRIPTION" "$META_CONFIRM"' _ "$LIB" "$HTML_RUN")
assert_eq "HTML metadata is language-invariant ($lang)" 'Run HTML|Opens the local page|false' "$meta_lang"
done
# 36. Existing shell-script metadata behavior remains intact after HTML support.
SHELL_META="$HTML_ROOT/regression.sh"
cat > "$SHELL_META" <<'SHELL_META_EOF'
#!/bin/bash
# MENU: Shell regression
# DESCRIPTION: Shell regression test
# CONFIRM: true
# ORDER: 2
printf 'SHELL_OK\n'
SHELL_META_EOF
meta=$(bash -c 'source "$1"; read_metadata "$2"; printf "%s|%s|%s|%s\n" "$META_MENU" "$META_DESCRIPTION" "$META_CONFIRM" "$META_ORDER"' _ "$LIB" "$SHELL_META")
assert_eq 'Shell metadata still parses correctly' 'Shell regression|Shell regression test|true|2' "$meta"
# 37. HTML files with uppercase extensions are discovered and parsed.
HTML_UPPER="$HTML_ROOT/UPPER.HTML"
cat > "$HTML_UPPER" <<'HTML_UPPER_EOF'
<!DOCTYPE html>
<!-- MENU: Upper HTML -->
<html><body>upper</body></html>
HTML_UPPER_EOF
listed=$(bash -c 'source "$1"; list_scripts "$2"' _ "$LIB" "$HTML_ROOT")
assert_contains 'Uppercase .HTML is listed' 'UPPER.HTML' "$listed"
meta=$(bash -c 'source "$1"; read_metadata "$2"; printf "%s\n" "$META_MENU"' _ "$LIB" "$HTML_UPPER")
assert_eq 'Uppercase .HTML metadata is read' 'Upper HTML' "$meta"
# 38. The interactive metadata wizard can insert metadata into HTML files.
HTML_WIZARD="$HTML_ROOT/wizard.html"
cat > "$HTML_WIZARD" <<'HTML_WIZARD_EOF'
<!DOCTYPE html>
<!-- Keep this comment -->
<html><body><h1>Wizard</h1></body></html>
HTML_WIZARD_EOF
wizard_home="$TMP_ROOT/home-html-wizard"
mkdir -p "$wizard_home/.config/scriptya"
wizard_input=$(printf '%s\n' 'Wizard page' 'Wizard description' '1' '2' '2' '8' 'icon.png' 'n' 'y' 'x')
out=$(printf '%s\n' "$wizard_input" | HOME="$wizard_home" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C \
bash -c 'source "$1"; insert_metadata_wizard "$2"' _ "$LIB" "$HTML_WIZARD" 2>&1)
status=$?
assert_eq 'HTML metadata wizard exits successfully' '0' "$status"
assert_contains 'HTML metadata wizard writes MENU comment' '<!-- MENU: Wizard page -->' "$(cat "$HTML_WIZARD")"
assert_contains 'HTML metadata wizard writes DESCRIPTION comment' '<!-- DESCRIPTION: Wizard description -->' "$(cat "$HTML_WIZARD")"
assert_contains 'HTML metadata wizard writes CONFIRM comment' '<!-- CONFIRM: true -->' "$(cat "$HTML_WIZARD")"
assert_contains 'HTML metadata wizard writes ORDER comment' '<!-- ORDER: 8 -->' "$(cat "$HTML_WIZARD")"
assert_contains 'HTML metadata wizard preserves DOCTYPE' '<!DOCTYPE html>' "$(cat "$HTML_WIZARD")"
assert_contains 'HTML metadata wizard preserves original comment' '<!-- Keep this comment -->' "$(cat "$HTML_WIZARD")"
assert_contains 'HTML metadata wizard preserves body' '<h1>Wizard</h1>' "$(cat "$HTML_WIZARD")"
# 39. HTML confirmation and missing xdg-open messages are localized in English.
home="$TMP_ROOT/home-html-messages"; mkdir -p "$home/.config/scriptya"
printf 'LANGUAGE=en\n' > "$home/.config/scriptya/config.conf"
out=$(HOME="$home" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash -c '
source "$1"
command_exists(){ [[ "$1" != xdg-open ]] && command -v "$1" >/dev/null 2>&1; }
confirm_yn(){ tr "¿Abrir '\''Test page'\''?"; return 1; }
run_script() { :; }
tr "¿Abrir '\''Test page'\''?"
' _ "$LIB" 2>&1)
assert_contains 'English HTML open prompt is translated' "Open 'Test page'?" "$out"
out=$(HOME="$home" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C bash -c 'source "$1"; print_error "No se encontró '\''xdg-open'\'' (paquete xdg-utils); instálalo para que el acceso abra bien el navegador."' _ "$LIB" 2>&1)
assert_contains 'English xdg-open error is translated' "'xdg-open' was not found" "$out"
assert_not_contains 'English xdg-open error does not leak Spanish' 'No se encontró' "$out"
# 40. HTML metadata never changes the document payload when toggling language.
html_hash_en=$(LANGUAGE=en sha256sum "$HTML_RUN" | awk '{print $1}')
html_hash_es=$(LANGUAGE=es sha256sum "$HTML_RUN" | awk '{print $1}')
assert_eq 'HTML file is language-invariant (EN/ES)' "$html_hash_en" "$html_hash_es"
# 41. Public help and README explicitly document HTML as a managed file type.
help_en=$(HOME="$TMP_ROOT/home-help-en" LANGUAGE=en bash -c 'source "$1"; show_help' _ "$LIB")
assert_contains 'English help documents HTML metadata' '.html/.htm' "$help_en"
assert_contains 'English help documents HTML comment metadata' '<!-- MENU: Local dashboard -->' "$help_en"
assert_contains 'English help documents HTML standalone apps' 'HTML pages as applications' "$help_en"
assert_contains 'English README documents HTML management' 'HTML pages are now managed by Scriptya too' "$(cat "$ROOT_DIR/README.md")"
assert_contains 'Spanish README documents HTML management' 'Ahora Scriptya también gestiona páginas HTML' "$(cat "$ROOT_DIR/README_ES.md")"
# 42. The real numbered menu renders an HTML entry with its metadata.
HTML_MENU="$HTML_ROOT/menu.html"
cat > "$HTML_MENU" <<'HTML_MENU_EOF'
<!DOCTYPE html>
<!-- MENU: Local dashboard -->
<!-- DESCRIPTION: Useful offline dashboard -->
<html><body>menu</body></html>
HTML_MENU_EOF
menu_output=$(HOME="$TMP_ROOT/home-html-menu" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C \
bash -c 'source "$1"; SCRIPTS_DIR="$2"; menu_numeric "$SCRIPTS_DIR" run' _ "$LIB" "$HTML_ROOT" <<< '0' 2>&1)
assert_contains 'Numeric menu renders HTML metadata entry' '🌐 Local dashboard — Useful offline dashboard' "$menu_output"
assert_not_contains 'English HTML menu entry has no Spanish action label' 'Instalar Scripts' "$menu_output"
# 42b. The real numbered menu renders a Python entry with its metadata in both languages.
PY_MENU="$TMP_ROOT/python-menu.py"
cat > "$PY_MENU" <<'PY_EOF'
#!/usr/bin/env python3
# MENU: Local Python tool
# DESCRIPTION: Useful Python utility
print("menu")
PY_EOF
PY_MENU_DIR="$TMP_ROOT/home-python-menu-scripts"; mkdir -p "$PY_MENU_DIR"; cp "$PY_MENU" "$PY_MENU_DIR/tool.py"
menu_py_en=$(HOME="$TMP_ROOT/home-python-menu-en" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C \
bash -c 'source "$1"; LANGUAGE=en; SCRIPTS_DIR="$2"; menu_numeric "$2" run' _ "$LIB" "$PY_MENU_DIR" <<< '0' 2>&1)
menu_py_es=$(HOME="$TMP_ROOT/home-python-menu-es" TERM=xterm NO_COLOR=1 LC_ALL=C LANG=C LC_MESSAGES=C \
bash -c 'source "$1"; LANGUAGE=es; SCRIPTS_DIR="$2"; menu_numeric "$2" run' _ "$LIB" "$PY_MENU_DIR" <<< '0' 2>&1)
assert_contains 'Numeric menu renders Python metadata in English' '🐍 Local Python tool — Useful Python utility' "$menu_py_en"
assert_contains 'Numeric menu renders Python metadata in Spanish' '🐍 Local Python tool — Useful Python utility' "$menu_py_es"
assert_not_contains 'English Python menu does not leak Spanish install label' 'Instalar Scripts' "$menu_py_en"
assert_not_contains 'Spanish Python menu does not leak English install label' 'Install Scripts' "$menu_py_es"
# 43. Python files are first-class managed files: discovery, metadata and ordering.
PY_ROOT="$TMP_ROOT/python-managed"; mkdir -p "$PY_ROOT"
cat > "$PY_ROOT/alpha.py" <<'PY_EOF'
#!/usr/bin/env python3
# MENU: Python Alpha
# DESCRIPTION: First Python entry
# ORDER: 20
print("alpha")
PY_EOF
cat > "$PY_ROOT/BETA.PY" <<'PY_EOF'
#!/usr/bin/env python3
# MENU: Python Beta
# DESCRIPTION: Second Python entry
# ORDER: 30
print("beta")
PY_EOF
cat > "$PY_ROOT/ignored.txt" <<'TXT_EOF'
not managed
TXT_EOF
managed=$(list_scripts "$PY_ROOT")
assert_contains 'Managed list includes .py' 'alpha.py' "$managed"
assert_contains 'Managed list is case-insensitive for Python' 'BETA.PY' "$managed"
assert_not_contains 'Managed list excludes unrelated files' 'ignored.txt' "$managed"
sorted=$(sorted_scripts "$PY_ROOT")
assert_eq 'Python ORDER sorts correctly' "$PY_ROOT/alpha.py" "$(printf '%s\n' "$sorted" | sed -n '1p')"
read_metadata "$PY_ROOT/BETA.PY"
assert_eq 'Python metadata MENU' 'Python Beta' "$META_MENU"
assert_eq 'Python metadata DESCRIPTION' 'Second Python entry' "$META_DESCRIPTION"
assert_eq 'Python metadata ORDER' '30' "$META_ORDER"
# 44. Insert Metadata works on Python without damaging the shebang, body, or user comments.
PY_META="$PY_ROOT/metadata.py"
cat > "$PY_META" <<'PY_EOF'
#!/usr/bin/env python3
# Author: Scriptya test
payload = {"ok": True}
print(payload)
PY_EOF
orig_body=$(tail -n 2 "$PY_META")
NEWMETA_MENU='Python Tool'; NEWMETA_DESCRIPTION='A Python utility'; NEWMETA_CONFIRM='true'; NEWMETA_TERMINAL='true'; NEWMETA_SUDO='false'; NEWMETA_ORDER='15'; NEWMETA_ICON='utilities-terminal'; NEWMETA_ASK=('name' 'count')
if apply_metadata "$PY_META"; then ok 'Python metadata insertion runs'; else fail 'Python metadata insertion runs'; fi
head -n 1 "$PY_META" | grep -Fxq '#!/usr/bin/env python3' && ok 'Python shebang is preserved' || fail 'Python shebang is preserved'
assert_contains 'Python inserted MENU' '# MENU: Python Tool' "$(head -n 8 "$PY_META")"
assert_contains 'Python inserted DESCRIPTION' '# DESCRIPTION: A Python utility' "$(head -n 8 "$PY_META")"
assert_contains 'Python inserted CONFIRM' '# CONFIRM: true' "$(head -n 8 "$PY_META")"
assert_contains 'Python inserted ASK 1' '# ASK: name' "$(cat "$PY_META")"
assert_contains 'Python original comment survives' '# Author: Scriptya test' "$(cat "$PY_META")"
assert_eq 'Python body survives metadata insertion' "$orig_body" "$(tail -n 2 "$PY_META")"
# 45. Python execution uses Python 3, respects ASK/CONFIRM/SUDO, and is language-invariant.
PY_RUN="$PY_ROOT/run.py"
cat > "$PY_RUN" <<'PY_EOF'
#!/usr/bin/env python3
# MENU: Python Runner
# DESCRIPTION: Python execution regression
# CONFIRM: false
# TERMINAL: false
# SUDO: true
# ASK: first value
print("PY_PAYLOAD:" + repr(__import__('sys').argv[1:]))
print("PY_CWD:" + __import__('os').getcwd())
PY_EOF
run_py() {
local lang="$1" out
out=$(HOME="$TMP_ROOT/home-py-$lang" LANGUAGE="$lang" bash -c '
source "$1"
LANGUAGE="$2"
LOG_FILE="$3/history.log"
INSTALL_DIR="$3"
pause(){ :; }
clear(){ :; }
print_header(){ :; }
print_info(){ :; }
print_success(){ :; }
print_error(){ printf "ERR:%s\\n" "$1" >&2; }
print_warning(){ :; }
confirm_yn(){ return 1; }
sudo(){ "$@"; }
mkdir -p "$3"