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.
- package/.claude/settings.local.json +26 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- package/.github/SECURITY.md +32 -0
- package/.github/assets/logo-horizontal-dark.png +0 -0
- package/.github/assets/logo-horizontal-dark.svg +17 -0
- package/.github/assets/logo-horizontal.png +0 -0
- package/.github/assets/logo-horizontal.svg +17 -0
- package/.github/assets/logo.png +0 -0
- package/.github/assets/logo.svg +12 -0
- package/.github/assets/social-preview.png +0 -0
- package/.github/assets/social-preview.svg +50 -0
- package/.github/dependabot.yml +21 -0
- package/.github/workflows/ci.yml +80 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/CHANGELOG.md +366 -0
- package/CLAUDE.md +340 -0
- package/CONTRIBUTING.md +141 -0
- package/LICENSE +110 -0
- package/README.md +201 -31
- package/RELEASING.md +148 -0
- package/STYLE.md +202 -0
- package/assets/hero.jpeg +0 -0
- package/assets/mode-indicators.jpeg +0 -0
- package/assets/real-time-indicator.jpeg +0 -0
- package/assets/supported-tools.jpeg +0 -0
- package/bin/lacy +1028 -0
- package/docs/ADDING-BACKENDS.md +124 -0
- package/docs/DEVTO-ARTICLE.md +94 -0
- package/docs/DOCS.md +68 -0
- package/docs/GROWTH-STRATEGY.md +119 -0
- package/docs/HN-RESPONSES.md +122 -0
- package/docs/LAUNCH-COPY-FINAL.md +105 -0
- package/docs/MARKETING.md +411 -0
- package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
- package/docs/UGC_VIDEO_SCRIPT.md +114 -0
- package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
- package/docs/demo-color-transition.gif +0 -0
- package/docs/demo-full.gif +0 -0
- package/docs/demo-indicator.gif +0 -0
- package/docs/launch-thread-may6.sh +158 -0
- package/docs/videos/README.md +189 -0
- package/docs/videos/generate_frames.py +510 -0
- package/docs/videos/generate_frames_v2.py +729 -0
- package/docs/videos/generate_short.py +328 -0
- package/docs/videos/generate_short_v2.py +526 -0
- package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-demo.mp4 +0 -0
- package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-short.mp4 +0 -0
- package/install.sh +1009 -0
- package/lacy.plugin.bash +75 -0
- package/lacy.plugin.fish +43 -0
- package/lacy.plugin.zsh +65 -0
- package/lib/animations.zsh +3 -0
- package/lib/bash/completions.bash +40 -0
- package/lib/bash/execute.bash +233 -0
- package/lib/bash/init.bash +40 -0
- package/lib/bash/keybindings.bash +134 -0
- package/lib/bash/prompt.bash +85 -0
- package/lib/commands/info.sh +25 -0
- package/lib/config.zsh +3 -0
- package/lib/constants.zsh +3 -0
- package/lib/core/animations.sh +271 -0
- package/lib/core/commands.sh +297 -0
- package/lib/core/config.sh +340 -0
- package/lib/core/constants.sh +366 -0
- package/lib/core/context.sh +260 -0
- package/lib/core/detection.sh +417 -0
- package/lib/core/mcp.sh +741 -0
- package/lib/core/modes.sh +123 -0
- package/lib/core/preheat.sh +496 -0
- package/lib/core/spinner.sh +174 -0
- package/lib/core/telemetry.sh +99 -0
- package/lib/detection.zsh +3 -0
- package/lib/execute.zsh +3 -0
- package/lib/fish/config.fish +66 -0
- package/lib/fish/detection.fish +90 -0
- package/lib/fish/execute.fish +105 -0
- package/lib/fish/keybindings.fish +42 -0
- package/lib/fish/prompt.fish +30 -0
- package/lib/keybindings.zsh +3 -0
- package/lib/mcp.zsh +3 -0
- package/lib/modes.zsh +3 -0
- package/lib/preheat.zsh +3 -0
- package/lib/prompt.zsh +3 -0
- package/lib/spinner.zsh +3 -0
- package/lib/zsh/completions.zsh +60 -0
- package/lib/zsh/execute.zsh +294 -0
- package/lib/zsh/init.zsh +26 -0
- package/lib/zsh/keybindings.zsh +551 -0
- package/lib/zsh/prompt.zsh +90 -0
- package/package.json +42 -27
- package/packages/lacy/README.md +61 -0
- package/packages/lacy/commands/info.sh +25 -0
- package/{index.mjs → packages/lacy/index.mjs} +247 -20
- package/packages/lacy/package-lock.json +71 -0
- package/packages/lacy/package.json +42 -0
- package/script/release.ts +487 -0
- package/squirrel.toml +36 -0
- package/tests/test_bash.bash +163 -0
- package/tests/test_core.sh +607 -0
- package/tests/test_gemini.sh +119 -0
- package/tests/test_gemini_mcp.sh +126 -0
- package/tests/test_preheat_server.zsh +446 -0
- 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
|