lacy 1.8.11 → 1.8.13

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.
Files changed (109) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.github/FUNDING.yml +3 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  7. package/.github/SECURITY.md +32 -0
  8. package/.github/assets/logo-horizontal-dark.png +0 -0
  9. package/.github/assets/logo-horizontal-dark.svg +17 -0
  10. package/.github/assets/logo-horizontal.png +0 -0
  11. package/.github/assets/logo-horizontal.svg +17 -0
  12. package/.github/assets/logo.png +0 -0
  13. package/.github/assets/logo.svg +12 -0
  14. package/.github/assets/social-preview.png +0 -0
  15. package/.github/assets/social-preview.svg +50 -0
  16. package/.github/dependabot.yml +21 -0
  17. package/.github/workflows/ci.yml +80 -0
  18. package/.github/workflows/dependabot-auto-merge.yml +32 -0
  19. package/CHANGELOG.md +366 -0
  20. package/CLAUDE.md +340 -0
  21. package/CONTRIBUTING.md +141 -0
  22. package/LICENSE +110 -0
  23. package/README.md +201 -31
  24. package/RELEASING.md +148 -0
  25. package/STYLE.md +202 -0
  26. package/assets/hero.jpeg +0 -0
  27. package/assets/mode-indicators.jpeg +0 -0
  28. package/assets/real-time-indicator.jpeg +0 -0
  29. package/assets/supported-tools.jpeg +0 -0
  30. package/bin/lacy +1028 -0
  31. package/docs/ADDING-BACKENDS.md +124 -0
  32. package/docs/DEVTO-ARTICLE.md +94 -0
  33. package/docs/DOCS.md +68 -0
  34. package/docs/GROWTH-STRATEGY.md +119 -0
  35. package/docs/HN-RESPONSES.md +122 -0
  36. package/docs/LAUNCH-COPY-FINAL.md +105 -0
  37. package/docs/MARKETING.md +411 -0
  38. package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
  39. package/docs/UGC_VIDEO_SCRIPT.md +114 -0
  40. package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
  41. package/docs/demo-color-transition.gif +0 -0
  42. package/docs/demo-full.gif +0 -0
  43. package/docs/demo-indicator.gif +0 -0
  44. package/docs/launch-thread-may6.sh +158 -0
  45. package/docs/videos/README.md +189 -0
  46. package/docs/videos/generate_frames.py +510 -0
  47. package/docs/videos/generate_frames_v2.py +729 -0
  48. package/docs/videos/generate_short.py +328 -0
  49. package/docs/videos/generate_short_v2.py +526 -0
  50. package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
  51. package/docs/videos/lacy-shell-demo.mp4 +0 -0
  52. package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
  53. package/docs/videos/lacy-shell-short.mp4 +0 -0
  54. package/install.sh +1009 -0
  55. package/lacy.plugin.bash +75 -0
  56. package/lacy.plugin.fish +43 -0
  57. package/lacy.plugin.zsh +65 -0
  58. package/lib/animations.zsh +3 -0
  59. package/lib/bash/completions.bash +40 -0
  60. package/lib/bash/execute.bash +233 -0
  61. package/lib/bash/init.bash +40 -0
  62. package/lib/bash/keybindings.bash +134 -0
  63. package/lib/bash/prompt.bash +85 -0
  64. package/lib/commands/info.sh +25 -0
  65. package/lib/config.zsh +3 -0
  66. package/lib/constants.zsh +3 -0
  67. package/lib/core/animations.sh +271 -0
  68. package/lib/core/commands.sh +297 -0
  69. package/lib/core/config.sh +340 -0
  70. package/lib/core/constants.sh +366 -0
  71. package/lib/core/context.sh +260 -0
  72. package/lib/core/detection.sh +417 -0
  73. package/lib/core/mcp.sh +741 -0
  74. package/lib/core/modes.sh +123 -0
  75. package/lib/core/preheat.sh +496 -0
  76. package/lib/core/spinner.sh +174 -0
  77. package/lib/core/telemetry.sh +99 -0
  78. package/lib/detection.zsh +3 -0
  79. package/lib/execute.zsh +3 -0
  80. package/lib/fish/config.fish +66 -0
  81. package/lib/fish/detection.fish +90 -0
  82. package/lib/fish/execute.fish +105 -0
  83. package/lib/fish/keybindings.fish +42 -0
  84. package/lib/fish/prompt.fish +30 -0
  85. package/lib/keybindings.zsh +3 -0
  86. package/lib/mcp.zsh +3 -0
  87. package/lib/modes.zsh +3 -0
  88. package/lib/preheat.zsh +3 -0
  89. package/lib/prompt.zsh +3 -0
  90. package/lib/spinner.zsh +3 -0
  91. package/lib/zsh/completions.zsh +60 -0
  92. package/lib/zsh/execute.zsh +294 -0
  93. package/lib/zsh/init.zsh +26 -0
  94. package/lib/zsh/keybindings.zsh +551 -0
  95. package/lib/zsh/prompt.zsh +90 -0
  96. package/package.json +42 -27
  97. package/packages/lacy/README.md +61 -0
  98. package/packages/lacy/commands/info.sh +25 -0
  99. package/{index.mjs → packages/lacy/index.mjs} +247 -20
  100. package/packages/lacy/package-lock.json +71 -0
  101. package/packages/lacy/package.json +42 -0
  102. package/script/release.ts +487 -0
  103. package/squirrel.toml +36 -0
  104. package/tests/test_bash.bash +163 -0
  105. package/tests/test_core.sh +607 -0
  106. package/tests/test_gemini.sh +119 -0
  107. package/tests/test_gemini_mcp.sh +126 -0
  108. package/tests/test_preheat_server.zsh +446 -0
  109. package/uninstall.sh +52 -0
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Test harness for core detection/config/modes
4
+ # Runs in both Bash 4+ and ZSH
5
+ #
6
+ # Usage:
7
+ # bash tests/test_core.sh
8
+ # zsh tests/test_core.sh
9
+
10
+ # Note: no set -e — tests use functions that return nonzero intentionally
11
+
12
+ # Determine which shell we're running in
13
+ if [[ -n "$ZSH_VERSION" ]]; then
14
+ LACY_SHELL_TYPE="zsh"
15
+ _LACY_ARR_OFFSET=1
16
+ elif [[ -n "$BASH_VERSION" ]]; then
17
+ LACY_SHELL_TYPE="bash"
18
+ _LACY_ARR_OFFSET=0
19
+ else
20
+ echo "FAIL: Unsupported shell"
21
+ exit 1
22
+ fi
23
+
24
+ echo "Testing Lacy Shell core in: ${LACY_SHELL_TYPE} (${ZSH_VERSION:-}${BASH_VERSION:-})"
25
+ echo "================================================================"
26
+
27
+ # Find repo root
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
29
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
30
+
31
+ # Source core modules
32
+ source "$REPO_DIR/lib/core/constants.sh"
33
+ source "$REPO_DIR/lib/core/detection.sh"
34
+ source "$REPO_DIR/lib/core/modes.sh"
35
+
36
+ # Test counter
37
+ PASS=0
38
+ FAIL=0
39
+
40
+ assert_eq() {
41
+ local test_name="$1"
42
+ local expected="$2"
43
+ local actual="$3"
44
+
45
+ if [[ "$expected" == "$actual" ]]; then
46
+ PASS=$(( PASS + 1 ))
47
+ else
48
+ echo " FAIL: $test_name"
49
+ echo " Expected: $expected"
50
+ echo " Actual: $actual"
51
+ FAIL=$(( FAIL + 1 ))
52
+ fi
53
+ }
54
+
55
+ assert_true() {
56
+ local test_name="$1"
57
+ shift
58
+ if "$@"; then
59
+ PASS=$(( PASS + 1 ))
60
+ else
61
+ echo " FAIL: $test_name (returned false)"
62
+ FAIL=$(( FAIL + 1 ))
63
+ fi
64
+ }
65
+
66
+ assert_false() {
67
+ local test_name="$1"
68
+ shift
69
+ if "$@"; then
70
+ echo " FAIL: $test_name (returned true)"
71
+ FAIL=$(( FAIL + 1 ))
72
+ else
73
+ PASS=$(( PASS + 1 ))
74
+ fi
75
+ }
76
+
77
+ # ============================================================================
78
+ # Detection Tests
79
+ # ============================================================================
80
+
81
+ echo ""
82
+ echo "--- Detection: classify_input ---"
83
+
84
+ LACY_SHELL_CURRENT_MODE="auto"
85
+
86
+ # Basic commands → shell
87
+ assert_eq "ls -la → shell" "shell" "$(lacy_shell_classify_input 'ls -la')"
88
+ assert_eq "git status → shell" "shell" "$(lacy_shell_classify_input 'git status')"
89
+ assert_eq "cd /home → shell" "shell" "$(lacy_shell_classify_input 'cd /home')"
90
+ assert_eq "npm install → shell" "shell" "$(lacy_shell_classify_input 'npm install')"
91
+ assert_eq "pwd → shell" "shell" "$(lacy_shell_classify_input 'pwd')"
92
+
93
+ # Natural language → agent
94
+ assert_eq "what files → agent" "agent" "$(lacy_shell_classify_input 'what files')"
95
+ assert_eq "fix the bug → agent" "agent" "$(lacy_shell_classify_input 'fix the bug')"
96
+ assert_eq "hello there → agent" "agent" "$(lacy_shell_classify_input 'hello there')"
97
+
98
+ # Agent words — single-word conversational
99
+ assert_eq "perfect → agent" "agent" "$(lacy_shell_classify_input 'perfect')"
100
+ assert_eq "yes → agent" "agent" "$(lacy_shell_classify_input 'yes')"
101
+ assert_eq "sure → agent" "agent" "$(lacy_shell_classify_input 'sure')"
102
+ assert_eq "thanks → agent" "agent" "$(lacy_shell_classify_input 'thanks')"
103
+ assert_eq "ok → agent" "agent" "$(lacy_shell_classify_input 'ok')"
104
+ assert_eq "great → agent" "agent" "$(lacy_shell_classify_input 'great')"
105
+ assert_eq "cool → agent" "agent" "$(lacy_shell_classify_input 'cool')"
106
+ assert_eq "nice → agent" "agent" "$(lacy_shell_classify_input 'nice')"
107
+ assert_eq "awesome → agent" "agent" "$(lacy_shell_classify_input 'awesome')"
108
+ assert_eq "lgtm → agent" "agent" "$(lacy_shell_classify_input 'lgtm')"
109
+ assert_eq "help → shell (real builtin)" "shell" "$(lacy_shell_classify_input 'help')"
110
+ assert_eq "stop → agent" "agent" "$(lacy_shell_classify_input 'stop')"
111
+ assert_eq "why → agent" "agent" "$(lacy_shell_classify_input 'why')"
112
+ assert_eq "how → agent" "agent" "$(lacy_shell_classify_input 'how')"
113
+ assert_eq "no → agent" "agent" "$(lacy_shell_classify_input 'no')"
114
+ assert_eq "nope → agent" "agent" "$(lacy_shell_classify_input 'nope')"
115
+
116
+ # New agent words — affirmations/reactions
117
+ assert_eq "gotcha → agent" "agent" "$(lacy_shell_classify_input 'gotcha')"
118
+ assert_eq "roger → agent" "agent" "$(lacy_shell_classify_input 'roger')"
119
+ assert_eq "understood → agent" "agent" "$(lacy_shell_classify_input 'understood')"
120
+ assert_eq "kudos → agent" "agent" "$(lacy_shell_classify_input 'kudos')"
121
+ assert_eq "noice → agent" "agent" "$(lacy_shell_classify_input 'noice')"
122
+
123
+ # New agent words — conversational/reactions
124
+ assert_eq "sheesh → agent" "agent" "$(lacy_shell_classify_input 'sheesh')"
125
+ assert_eq "oof → agent" "agent" "$(lacy_shell_classify_input 'oof')"
126
+ assert_eq "meh → agent" "agent" "$(lacy_shell_classify_input 'meh')"
127
+ assert_eq "duh → agent" "agent" "$(lacy_shell_classify_input 'duh')"
128
+ assert_eq "bummer → agent" "agent" "$(lacy_shell_classify_input 'bummer')"
129
+
130
+ # New agent words — internet/chat shorthand
131
+ assert_eq "lol → agent" "agent" "$(lacy_shell_classify_input 'lol')"
132
+ assert_eq "omg → agent" "agent" "$(lacy_shell_classify_input 'omg')"
133
+ assert_eq "idk → agent" "agent" "$(lacy_shell_classify_input 'idk')"
134
+ assert_eq "btw → agent" "agent" "$(lacy_shell_classify_input 'btw')"
135
+ assert_eq "tbh → agent" "agent" "$(lacy_shell_classify_input 'tbh')"
136
+ assert_eq "fyi → agent" "agent" "$(lacy_shell_classify_input 'fyi')"
137
+
138
+ # New agent words — programming verbs
139
+ assert_eq "debug → agent" "agent" "$(lacy_shell_classify_input 'debug')"
140
+ assert_eq "deploy → agent" "agent" "$(lacy_shell_classify_input 'deploy')"
141
+ assert_eq "implement → agent" "agent" "$(lacy_shell_classify_input 'implement')"
142
+ assert_eq "diagnose → agent" "agent" "$(lacy_shell_classify_input 'diagnose')"
143
+ assert_eq "troubleshoot → agent" "agent" "$(lacy_shell_classify_input 'troubleshoot')"
144
+ assert_eq "rollback → agent" "agent" "$(lacy_shell_classify_input 'rollback')"
145
+
146
+ # New agent words — action/intent
147
+ assert_eq "suggest → agent" "agent" "$(lacy_shell_classify_input 'suggest')"
148
+ assert_eq "recommend → agent" "agent" "$(lacy_shell_classify_input 'recommend')"
149
+ assert_eq "imagine → agent" "agent" "$(lacy_shell_classify_input 'imagine')"
150
+
151
+ # Agent words — with trailing punctuation
152
+ assert_eq "why? → agent" "agent" "$(lacy_shell_classify_input 'why?')"
153
+ assert_eq "how? → agent" "agent" "$(lacy_shell_classify_input 'how?')"
154
+ assert_eq "no! → agent" "agent" "$(lacy_shell_classify_input 'no!')"
155
+ assert_eq "yes. → agent" "agent" "$(lacy_shell_classify_input 'yes.')"
156
+ assert_eq "sure! → agent" "agent" "$(lacy_shell_classify_input 'sure!')"
157
+ assert_eq "do? → agent" "agent" "$(lacy_shell_classify_input 'do?')"
158
+
159
+ # Agent words — multi-word
160
+ assert_eq "what is this → agent" "agent" "$(lacy_shell_classify_input 'what is this')"
161
+ assert_eq "yes lets go → agent" "agent" "$(lacy_shell_classify_input 'yes lets go')"
162
+ assert_eq "no I dont → agent" "agent" "$(lacy_shell_classify_input 'no I dont want that')"
163
+ assert_eq "perfect lets move on → agent" "agent" "$(lacy_shell_classify_input 'perfect lets move on')"
164
+ assert_eq "thanks for the help → agent" "agent" "$(lacy_shell_classify_input 'thanks for the help')"
165
+
166
+ # Inline env var assignments → shell
167
+ assert_eq "RUST_LOG=debug cargo run → shell" "shell" "$(lacy_shell_classify_input 'RUST_LOG=debug cargo run')"
168
+ assert_eq "FOO=bar node index.js → shell" "shell" "$(lacy_shell_classify_input 'FOO=bar node index.js')"
169
+ assert_eq "FOO=bar BAZ=qux node index.js → shell" "shell" "$(lacy_shell_classify_input 'FOO=bar BAZ=qux node index.js')"
170
+ assert_eq "CC=gcc make -j4 → shell" "shell" "$(lacy_shell_classify_input 'CC=gcc make -j4')"
171
+ assert_eq "FOO=bar (bare assignment, no cmd) → shell" "shell" "$(lacy_shell_classify_input 'FOO=bar')"
172
+ assert_eq "FOO=bar nonexistent thing → agent" "agent" "$(lacy_shell_classify_input 'FOO=bar nonexistent_cmd thing')"
173
+
174
+ # Single word non-command → shell (typo)
175
+ assert_eq "asdfgh → shell" "shell" "$(lacy_shell_classify_input 'asdfgh')"
176
+
177
+ # Emergency bypass
178
+ assert_eq "!rm → shell" "shell" "$(lacy_shell_classify_input '!rm /tmp/test')"
179
+
180
+ # Leading whitespace
181
+ assert_eq " ls -la → shell" "shell" "$(lacy_shell_classify_input ' ls -la')"
182
+ assert_eq " what files → agent" "agent" "$(lacy_shell_classify_input ' what files')"
183
+
184
+ # Empty input in auto mode → neutral
185
+ assert_eq "empty → neutral" "neutral" "$(lacy_shell_classify_input '')"
186
+
187
+ # Shell mode: everything → shell
188
+ LACY_SHELL_CURRENT_MODE="shell"
189
+ assert_eq "shell mode: what → shell" "shell" "$(lacy_shell_classify_input 'what files')"
190
+ assert_eq "shell mode: empty → shell" "shell" "$(lacy_shell_classify_input '')"
191
+
192
+ # Agent mode: everything → agent
193
+ LACY_SHELL_CURRENT_MODE="agent"
194
+ assert_eq "agent mode: ls → agent" "agent" "$(lacy_shell_classify_input 'ls -la')"
195
+ assert_eq "agent mode: empty → agent" "agent" "$(lacy_shell_classify_input '')"
196
+
197
+ LACY_SHELL_CURRENT_MODE="auto"
198
+
199
+ # ============================================================================
200
+ # Reserved Words Tests (Layer 1)
201
+ # ============================================================================
202
+
203
+ echo ""
204
+ echo "--- Detection: reserved words → agent ---"
205
+
206
+ LACY_SHELL_CURRENT_MODE="auto"
207
+
208
+ assert_eq "do question → agent" "agent" "$(lacy_shell_classify_input 'do We already have a way to uninstall?')"
209
+ assert_eq "done with this → agent" "agent" "$(lacy_shell_classify_input 'done with this task')"
210
+ assert_eq "then what → agent" "agent" "$(lacy_shell_classify_input 'then what happens next')"
211
+ assert_eq "else something → agent" "agent" "$(lacy_shell_classify_input 'else something')"
212
+ assert_eq "in the codebase → agent" "agent" "$(lacy_shell_classify_input 'in the codebase')"
213
+ assert_eq "function of module → agent" "agent" "$(lacy_shell_classify_input 'function of this module')"
214
+ assert_eq "select all users → agent" "agent" "$(lacy_shell_classify_input 'select all users')"
215
+
216
+ # ============================================================================
217
+ # NL Markers Tests
218
+ # ============================================================================
219
+
220
+ echo ""
221
+ echo "--- Detection: has_nl_markers ---"
222
+
223
+ assert_true "kill the process on localhost" lacy_shell_has_nl_markers "kill the process on localhost:3000"
224
+ assert_true "make the tests pass" lacy_shell_has_nl_markers "make the tests pass"
225
+ assert_true "go ahead and fix it" lacy_shell_has_nl_markers "go ahead and fix it"
226
+ assert_true "find out how auth works" lacy_shell_has_nl_markers "find out how auth works"
227
+ assert_true "find the file" lacy_shell_has_nl_markers "find the file"
228
+ assert_true "go ahead" lacy_shell_has_nl_markers "go ahead"
229
+ assert_true "kill -9 my baby (my is NL)" lacy_shell_has_nl_markers "kill -9 my baby"
230
+ assert_false "kill -9 (no bare words)" lacy_shell_has_nl_markers "kill -9"
231
+ assert_false "git push origin main (no NL marker)" lacy_shell_has_nl_markers "git push origin main"
232
+ assert_false "echo hello | grep the (has pipe)" lacy_shell_has_nl_markers "echo hello | grep the"
233
+
234
+ # ============================================================================
235
+ # Natural Language Detection Tests (Layer 2)
236
+ # ============================================================================
237
+
238
+ echo ""
239
+ echo "--- Detection: detect_natural_language ---"
240
+
241
+ # Successful commands — no detection
242
+ lacy_shell_detect_natural_language "ls -la" "file1" 0
243
+ assert_eq "exit 0 → no detect" "1" "$?"
244
+
245
+ # Non-NL second word — no detection
246
+ lacy_shell_detect_natural_language "ls foo" "no such file or directory" 1
247
+ assert_eq "non-NL second word → no detect" "1" "$?"
248
+
249
+ # Parse error with NL second word
250
+ lacy_shell_detect_natural_language "do We already have a way to uninstall?" "(eval):1: parse error near do" 1
251
+ assert_eq "parse error + NL word → detect" "0" "$?"
252
+
253
+ # go ahead — unknown command
254
+ lacy_shell_detect_natural_language "go ahead and fix it" "go ahead: unknown command" 2
255
+ assert_eq "go ahead → detect" "0" "$?"
256
+
257
+ # make sure — no rule to make target
258
+ lacy_shell_detect_natural_language "make sure the tests pass" "make: *** No rule to make target 'sure'. Stop." 2
259
+ assert_eq "make sure → detect" "0" "$?"
260
+
261
+ # git me — not a git command
262
+ lacy_shell_detect_natural_language "git me the latest changes" "git: 'me' is not a git command." 1
263
+ assert_eq "git me → detect" "0" "$?"
264
+
265
+ # find out — unknown primary
266
+ lacy_shell_detect_natural_language "find out how the auth works" "find: out: unknown primary or operator" 1
267
+ assert_eq "find out → detect" "0" "$?"
268
+
269
+ # find the file — no such file or directory
270
+ lacy_shell_detect_natural_language "find the file" "find: the: No such file or directory" 1
271
+ assert_eq "find the file → detect" "0" "$?"
272
+
273
+ # go ahead — unknown command (2 words)
274
+ lacy_shell_detect_natural_language "go ahead" "go ahead: unknown command" 2
275
+ assert_eq "go ahead (2 words) → detect" "0" "$?"
276
+
277
+ # Real command error — no detection
278
+ lacy_shell_detect_natural_language "grep -r foo" "grep: warning: recursive search" 1
279
+ assert_eq "real grep error → no detect" "1" "$?"
280
+
281
+ # ============================================================================
282
+ # Mode Tests
283
+ # ============================================================================
284
+
285
+ echo ""
286
+ echo "--- Modes ---"
287
+
288
+ LACY_SHELL_MODE_FILE="/tmp/lacy_test_mode_$$"
289
+ LACY_SHELL_DEFAULT_MODE="auto"
290
+
291
+ lacy_shell_set_mode "shell"
292
+ assert_eq "set shell" "shell" "$LACY_SHELL_CURRENT_MODE"
293
+
294
+ lacy_shell_set_mode "agent"
295
+ assert_eq "set agent" "agent" "$LACY_SHELL_CURRENT_MODE"
296
+
297
+ lacy_shell_set_mode "auto"
298
+ assert_eq "set auto" "auto" "$LACY_SHELL_CURRENT_MODE"
299
+
300
+ # Toggle: auto → shell → agent → auto
301
+ lacy_shell_toggle_mode
302
+ assert_eq "toggle auto→shell" "shell" "$LACY_SHELL_CURRENT_MODE"
303
+ lacy_shell_toggle_mode
304
+ assert_eq "toggle shell→agent" "agent" "$LACY_SHELL_CURRENT_MODE"
305
+ lacy_shell_toggle_mode
306
+ assert_eq "toggle agent→auto" "auto" "$LACY_SHELL_CURRENT_MODE"
307
+
308
+ # Mode description
309
+ assert_eq "desc shell" "Normal shell execution" "$(lacy_mode_description 'shell')"
310
+ assert_eq "desc agent" "AI agent assistance via MCP" "$(lacy_mode_description 'agent')"
311
+
312
+ # Cleanup
313
+ rm -f "$LACY_SHELL_MODE_FILE"
314
+
315
+ # ============================================================================
316
+ # Helpers Tests
317
+ # ============================================================================
318
+
319
+ echo ""
320
+ echo "--- Helpers ---"
321
+
322
+ # _lacy_lowercase
323
+ assert_eq "lowercase HELLO" "hello" "$(_lacy_lowercase 'HELLO')"
324
+ assert_eq "lowercase MiXeD" "mixed" "$(_lacy_lowercase 'MiXeD')"
325
+
326
+ # _lacy_in_list
327
+ assert_true "in_list found" _lacy_in_list "b" "a" "b" "c"
328
+ assert_false "in_list not found" _lacy_in_list "d" "a" "b" "c"
329
+
330
+ # Tool cmd lookup
331
+ source "$REPO_DIR/lib/core/mcp.sh"
332
+ assert_eq "tool cmd lash" "lash run -c" "$(lacy_tool_cmd 'lash')"
333
+ assert_eq "tool cmd claude" "claude -p" "$(lacy_tool_cmd 'claude')"
334
+ assert_eq "tool cmd hermes" "hermes chat -q" "$(lacy_tool_cmd 'hermes')"
335
+ assert_eq "tool cmd unknown" "" "$(lacy_tool_cmd 'unknown')"
336
+
337
+ # Hermes in LACY_TOOL_LIST
338
+ assert_true "hermes in tool list" _lacy_in_list "hermes" "${LACY_TOOL_LIST[@]}"
339
+
340
+ # Hermes resume command
341
+ source "$REPO_DIR/lib/core/preheat.sh"
342
+ assert_eq "hermes resume cmd" "hermes --continue" "$(lacy_resume_cmd 'hermes')"
343
+
344
+ # ============================================================================
345
+ # Telemetry JSON Escaping Tests
346
+ # ============================================================================
347
+
348
+ echo ""
349
+ echo "--- Telemetry: JSON escape ---"
350
+
351
+ source "$REPO_DIR/lib/core/telemetry.sh" 2>/dev/null || true
352
+
353
+ assert_eq "escape plain" "hello" "$(_lacy_json_escape_str 'hello')"
354
+ assert_eq "escape double quote" 'say \"hi\"' "$(_lacy_json_escape_str 'say "hi"')"
355
+ assert_eq "escape backslash" 'a\\b' "$(_lacy_json_escape_str 'a\b')"
356
+ assert_eq "escape newline" 'line1\nline2' "$(_lacy_json_escape_str $'line1\nline2')"
357
+ assert_eq "escape tab" 'a\tb' "$(_lacy_json_escape_str $'a\tb')"
358
+ assert_eq "escape combo" 'q\"\\n' "$(_lacy_json_escape_str 'q"\n')"
359
+
360
+ # ============================================================================
361
+ # Context Tests
362
+ # ============================================================================
363
+
364
+ echo ""
365
+ echo "--- Context: delta-based query context ---"
366
+
367
+ source "$REPO_DIR/lib/core/context.sh"
368
+
369
+ # Helper: check if string contains substring
370
+ _str_contains() { [[ "$1" == *"$2"* ]]; }
371
+ _str_not_contains() { [[ "$1" != *"$2"* ]]; }
372
+
373
+ # Reset to known state
374
+ _lacy_ctx_reset
375
+
376
+ # First query — should include cwd (differs from empty string)
377
+ _lacy_build_query_context "hello"
378
+ result="$_LACY_CTX_RESULT"
379
+ assert_true "first query includes cwd" _str_contains "$result" "[cwd: "
380
+ assert_true "first query includes query" _str_contains "$result" "hello"
381
+
382
+ # Second query, nothing changed — bare query
383
+ _lacy_build_query_context "hello again"
384
+ result="$_LACY_CTX_RESULT"
385
+ assert_eq "no-change → bare query" "hello again" "$result"
386
+
387
+ # Mark a command and trigger precmd
388
+ _lacy_ctx_mark_command "npm test"
389
+ _lacy_ctx_on_precmd 1
390
+
391
+ # Query should include exit code and recent command
392
+ _lacy_build_query_context "why did that fail"
393
+ result="$_LACY_CTX_RESULT"
394
+ assert_true "exit code included" _str_contains "$result" "[exit: 1]"
395
+ assert_true "recent cmd included" _str_contains "$result" "[recent: npm test]"
396
+ assert_true "query at end" _str_contains "$result" "why did that fail"
397
+
398
+ # After building context, counters reset — next query should be bare
399
+ _lacy_build_query_context "explain more"
400
+ result="$_LACY_CTX_RESULT"
401
+ assert_eq "after reset → bare query" "explain more" "$result"
402
+
403
+ # Multiple commands between queries
404
+ _lacy_ctx_mark_command "ls -la"
405
+ _lacy_ctx_on_precmd 0
406
+ _lacy_ctx_mark_command "cd /tmp"
407
+ _lacy_ctx_on_precmd 0
408
+ _lacy_ctx_mark_command "git status"
409
+ _lacy_ctx_on_precmd 0
410
+
411
+ _lacy_build_query_context "what happened"
412
+ result="$_LACY_CTX_RESULT"
413
+ assert_true "multiple cmds use pipe separator" _str_contains "$result" "ls -la | cd /tmp | git status"
414
+ # Exit code 0 should NOT be included
415
+ assert_true "exit 0 not included" _str_not_contains "$result" "[exit:"
416
+
417
+ # Reset clears state — forces full context on next query
418
+ _lacy_ctx_reset
419
+ _lacy_build_query_context "hello after reset"
420
+ result="$_LACY_CTX_RESULT"
421
+ assert_true "after reset includes cwd" _str_contains "$result" "[cwd: "
422
+
423
+ # Exit code only included when commands ran
424
+ _lacy_ctx_reset
425
+ # Simulate: no commands ran, but _LACY_CTX_LAST_EXIT_CODE might be stale
426
+ _LACY_CTX_LAST_EXIT_CODE=1
427
+ _LACY_CTX_CMDS_SINCE_QUERY=0
428
+ _lacy_build_query_context "test stale exit"
429
+ result="$_LACY_CTX_RESULT"
430
+ # Should NOT include exit code since no commands ran
431
+ assert_true "stale exit code not included" _str_not_contains "$result" "[exit:"
432
+
433
+ # Command buffer cap at max
434
+ _lacy_ctx_reset
435
+ # Burn through the first-query cwd delta
436
+ _lacy_build_query_context "burn"
437
+ for i in $(seq 1 15); do
438
+ _lacy_ctx_mark_command "cmd$i"
439
+ _lacy_ctx_on_precmd 0
440
+ done
441
+ _lacy_build_query_context "check buffer cap"
442
+ result="$_LACY_CTX_RESULT"
443
+ # Should contain cmd6 through cmd15 (last 10), not cmd1-cmd5
444
+ assert_true "old cmds trimmed (cmd5)" _str_not_contains "$result" "cmd5 |"
445
+ assert_true "recent cmds kept" _str_contains "$result" "cmd15"
446
+
447
+ # Detached HEAD — should show short hash, not literal "HEAD"
448
+ _lacy_ctx_reset
449
+ # Burn first-query delta
450
+ _lacy_build_query_context "burn"
451
+ # Simulate detached HEAD by checking the function handles it
452
+ # (Can't easily detach HEAD in test, but verify the branch name is never "HEAD")
453
+ _current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
454
+ if [[ "$_current_branch" != "HEAD" ]]; then
455
+ # Normal branch — git context should contain branch name
456
+ _lacy_ctx_reset
457
+ _lacy_build_query_context "test git"
458
+ result="$_LACY_CTX_RESULT"
459
+ assert_true "git branch in context" _str_contains "$result" "[git: $_current_branch]"
460
+ fi
461
+
462
+ # ============================================================================
463
+ # Terminal Output Context Tests
464
+ # ============================================================================
465
+
466
+ echo ""
467
+ echo "--- Context: terminal output capture ---"
468
+
469
+ # Save original state
470
+ _saved_capture_cmd="$_LACY_CTX_TERMINAL_CAPTURE_CMD"
471
+ _saved_output_enabled="$_LACY_CTX_OUTPUT_ENABLED"
472
+
473
+ # In test environment, no terminal emulator API is available
474
+ assert_eq "no capture cmd in test env" "" "$_saved_capture_cmd"
475
+
476
+ # Without capture cmd, no output block in context
477
+ _LACY_CTX_TERMINAL_CAPTURE_CMD=""
478
+ _LACY_CTX_OUTPUT_ENABLED=true
479
+ _lacy_ctx_reset
480
+ _lacy_ctx_mark_command "npm test"
481
+ _lacy_ctx_on_precmd 1
482
+ _lacy_build_query_context "why fail"
483
+ result="$_LACY_CTX_RESULT"
484
+ assert_true "no output block without capture" _str_not_contains "$result" "[terminal-output]"
485
+
486
+ # Simulate capture by setting the variable to echo
487
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="echo 'Error: test failed'"
488
+ _LACY_CTX_OUTPUT_ENABLED=true
489
+ _lacy_ctx_reset
490
+ # Burn cwd delta
491
+ _lacy_build_query_context "burn"
492
+ _lacy_ctx_mark_command "npm test"
493
+ _lacy_ctx_on_precmd 1
494
+ _lacy_build_query_context "why fail"
495
+ result="$_LACY_CTX_RESULT"
496
+ assert_true "output block present with capture" _str_contains "$result" "[terminal-output]"
497
+ assert_true "output content present" _str_contains "$result" "Error: test failed"
498
+ assert_true "output block closed" _str_contains "$result" "[/terminal-output]"
499
+
500
+ # Disabled via config
501
+ _LACY_CTX_OUTPUT_ENABLED=false
502
+ _lacy_ctx_mark_command "npm test"
503
+ _lacy_ctx_on_precmd 1
504
+ _lacy_build_query_context "why fail disabled"
505
+ result="$_LACY_CTX_RESULT"
506
+ assert_true "no output when disabled" _str_not_contains "$result" "[terminal-output]"
507
+
508
+ # No capture when no commands ran
509
+ _LACY_CTX_OUTPUT_ENABLED=true
510
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="echo 'should not appear'"
511
+ _lacy_ctx_reset
512
+ _lacy_build_query_context "burn"
513
+ _lacy_build_query_context "no commands ran"
514
+ result="$_LACY_CTX_RESULT"
515
+ assert_true "no output when no commands ran" _str_not_contains "$result" "[terminal-output]"
516
+
517
+ # JSON escape helper
518
+ assert_eq "json escape newlines" 'hello\nworld' "$(_lacy_json_escape_str $'hello\nworld')"
519
+ assert_eq "json escape quotes" 'say \"hi\"' "$(_lacy_json_escape_str 'say "hi"')"
520
+ assert_eq "json escape backslash" 'path\\to' "$(_lacy_json_escape_str 'path\to')"
521
+
522
+ # --- Terminal detection priority tests ---
523
+ echo ""
524
+ echo "--- Context: terminal detection ---"
525
+
526
+ # Save env vars we'll be modifying
527
+ _saved_TMUX="${TMUX:-}"
528
+ _saved_STY="${STY:-}"
529
+ _saved_TERM_PROGRAM="${TERM_PROGRAM:-}"
530
+
531
+ # Clean slate for detection tests
532
+ unset TMUX STY TERM_PROGRAM 2>/dev/null
533
+
534
+ # tmux detection: set TMUX, verify capture command
535
+ TMUX="/tmp/tmux-test/default,12345,0"
536
+ _lacy_ctx_detect_terminal
537
+ assert_eq "tmux detected" "tmux capture-pane -p" "$_LACY_CTX_TERMINAL_CAPTURE_CMD"
538
+ unset TMUX
539
+
540
+ # screen detection: set STY, verify helper function
541
+ STY="12345.pts-0.host"
542
+ _lacy_ctx_detect_terminal
543
+ assert_eq "screen detected" "_lacy_ctx_screen_capture" "$_LACY_CTX_TERMINAL_CAPTURE_CMD"
544
+ unset STY
545
+
546
+ # tmux takes priority over screen
547
+ TMUX="/tmp/tmux-test/default,12345,0"
548
+ STY="12345.pts-0.host"
549
+ _lacy_ctx_detect_terminal
550
+ assert_eq "tmux beats screen" "tmux capture-pane -p" "$_LACY_CTX_TERMINAL_CAPTURE_CMD"
551
+ unset TMUX STY
552
+
553
+ # No env vars set -> no capture (in test env without real terminals)
554
+ _lacy_ctx_detect_terminal
555
+ assert_eq "no terminal no capture" "" "$_LACY_CTX_TERMINAL_CAPTURE_CMD"
556
+
557
+ # macOS iTerm2 detection (only runs on Darwin)
558
+ if [[ "$(uname -s 2>/dev/null)" == "Darwin" ]]; then
559
+ TERM_PROGRAM="iTerm.app"
560
+ _lacy_ctx_detect_terminal
561
+ assert_eq "iterm2 detected" "_lacy_ctx_iterm2_capture" "$_LACY_CTX_TERMINAL_CAPTURE_CMD"
562
+ unset TERM_PROGRAM
563
+
564
+ TERM_PROGRAM="Apple_Terminal"
565
+ _lacy_ctx_detect_terminal
566
+ assert_eq "terminal.app detected" "_lacy_ctx_terminal_app_capture" "$_LACY_CTX_TERMINAL_CAPTURE_CMD"
567
+ unset TERM_PROGRAM
568
+ fi
569
+
570
+ # Capture via helper function works (simulate with a test function)
571
+ _lacy_test_capture_func() { echo "captured via function"; }
572
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="_lacy_test_capture_func"
573
+ _LACY_CTX_OUTPUT_ENABLED=true
574
+ _lacy_ctx_reset
575
+ _lacy_build_query_context "burn"
576
+ _lacy_ctx_mark_command "make build"
577
+ _lacy_ctx_on_precmd 1
578
+ _lacy_build_query_context "what happened"
579
+ result="$_LACY_CTX_RESULT"
580
+ assert_true "function-based capture works" _str_contains "$result" "captured via function"
581
+ assert_true "function capture has block" _str_contains "$result" "[terminal-output]"
582
+ unset -f _lacy_test_capture_func
583
+
584
+ # Restore env vars
585
+ TMUX="$_saved_TMUX"; [[ -z "$TMUX" ]] && unset TMUX 2>/dev/null
586
+ STY="$_saved_STY"; [[ -z "$STY" ]] && unset STY 2>/dev/null
587
+ TERM_PROGRAM="$_saved_TERM_PROGRAM"; [[ -z "$TERM_PROGRAM" ]] && unset TERM_PROGRAM 2>/dev/null
588
+
589
+ # Restore original state
590
+ _LACY_CTX_TERMINAL_CAPTURE_CMD="$_saved_capture_cmd"
591
+ _LACY_CTX_OUTPUT_ENABLED="$_saved_output_enabled"
592
+
593
+ # ============================================================================
594
+ # Results
595
+ # ============================================================================
596
+
597
+ echo ""
598
+ echo "================================================================"
599
+ echo "Results: ${PASS} passed, ${FAIL} failed"
600
+
601
+ if [[ $FAIL -gt 0 ]]; then
602
+ echo "FAILED"
603
+ exit 1
604
+ else
605
+ echo "ALL TESTS PASSED"
606
+ exit 0
607
+ fi