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,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Test Gemini session resume functionality
|
|
4
|
+
# Runs in Bash 4+ and ZSH
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bash tests/test_gemini.sh
|
|
8
|
+
# zsh tests/test_gemini.sh
|
|
9
|
+
|
|
10
|
+
# Determine which shell we're running in
|
|
11
|
+
if [[ -n "$ZSH_VERSION" ]]; then
|
|
12
|
+
LACY_SHELL_TYPE="zsh"
|
|
13
|
+
elif [[ -n "$BASH_VERSION" ]]; then
|
|
14
|
+
LACY_SHELL_TYPE="bash"
|
|
15
|
+
else
|
|
16
|
+
echo "FAIL: Unsupported shell"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
echo "Testing Gemini Session Resume in: ${LACY_SHELL_TYPE}"
|
|
21
|
+
echo "================================================================"
|
|
22
|
+
|
|
23
|
+
# Find repo root
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
25
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
26
|
+
|
|
27
|
+
# Set up test environment
|
|
28
|
+
TEST_TMPDIR=$(mktemp -d)
|
|
29
|
+
export LACY_SHELL_HOME="$TEST_TMPDIR"
|
|
30
|
+
|
|
31
|
+
# Source core modules
|
|
32
|
+
source "$REPO_DIR/lib/core/constants.sh"
|
|
33
|
+
source "$REPO_DIR/lib/core/mcp.sh"
|
|
34
|
+
source "$REPO_DIR/lib/core/preheat.sh"
|
|
35
|
+
|
|
36
|
+
# Mock dependencies
|
|
37
|
+
lacy_start_spinner() { :; }
|
|
38
|
+
lacy_stop_spinner() { :; }
|
|
39
|
+
lacy_print_color() { :; }
|
|
40
|
+
|
|
41
|
+
# Test counter
|
|
42
|
+
PASS=0
|
|
43
|
+
FAIL=0
|
|
44
|
+
|
|
45
|
+
assert_eq() {
|
|
46
|
+
local test_name="$1"
|
|
47
|
+
local expected="$2"
|
|
48
|
+
local actual="$3"
|
|
49
|
+
|
|
50
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
51
|
+
PASS=$(( PASS + 1 ))
|
|
52
|
+
printf ' \e[32m✓\e[0m %s\n' "$test_name"
|
|
53
|
+
else
|
|
54
|
+
printf ' \e[31m✗\e[0m %s\n' "$test_name"
|
|
55
|
+
echo " Expected: $expected"
|
|
56
|
+
echo " Actual: $actual"
|
|
57
|
+
FAIL=$(( FAIL + 1 ))
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
summary() {
|
|
62
|
+
echo ""
|
|
63
|
+
echo "================================================================"
|
|
64
|
+
printf 'Results: %d passed, %d failed\n' "$PASS" "$FAIL"
|
|
65
|
+
if (( FAIL > 0 )); then
|
|
66
|
+
printf '\e[31mFAILED\e[0m\n'
|
|
67
|
+
exit 1
|
|
68
|
+
else
|
|
69
|
+
printf '\e[32mALL PASSED\e[0m\n'
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# Gemini Session Tests
|
|
76
|
+
# ============================================================================
|
|
77
|
+
|
|
78
|
+
section() {
|
|
79
|
+
echo ""
|
|
80
|
+
echo "--- $1 ---"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
section "Gemini Session State"
|
|
84
|
+
|
|
85
|
+
# Initial state
|
|
86
|
+
assert_eq "LACY_GEMINI_SESSION_ID should be empty" "" "$LACY_GEMINI_SESSION_ID"
|
|
87
|
+
assert_eq "Initial build cmd" "gemini -p" "$(lacy_preheat_gemini_build_cmd)"
|
|
88
|
+
|
|
89
|
+
# Capture session
|
|
90
|
+
MOCK_JSON='{"session_id": "test-uuid-123", "response": "hello"}'
|
|
91
|
+
lacy_preheat_gemini_capture_session "$MOCK_JSON"
|
|
92
|
+
|
|
93
|
+
assert_eq "LACY_GEMINI_SESSION_ID should be captured" "test-uuid-123" "$LACY_GEMINI_SESSION_ID"
|
|
94
|
+
assert_eq "Session ID should be persisted to file" "test-uuid-123" "$(cat "$LACY_GEMINI_SESSION_ID_FILE")"
|
|
95
|
+
assert_eq "Resumed build cmd" "gemini --resume test-uuid-123 -p" "$(lacy_preheat_gemini_build_cmd)"
|
|
96
|
+
|
|
97
|
+
# Extract result
|
|
98
|
+
assert_eq "Extract result from JSON" "hello" "$(lacy_preheat_gemini_extract_result "$MOCK_JSON")"
|
|
99
|
+
|
|
100
|
+
# Restore session (simulating new shell)
|
|
101
|
+
LACY_GEMINI_SESSION_ID=""
|
|
102
|
+
lacy_preheat_gemini_restore_session
|
|
103
|
+
assert_eq "Restore session from file" "test-uuid-123" "$LACY_GEMINI_SESSION_ID"
|
|
104
|
+
|
|
105
|
+
# Reset session
|
|
106
|
+
lacy_preheat_gemini_reset_session
|
|
107
|
+
assert_eq "LACY_GEMINI_SESSION_ID should be cleared" "" "$LACY_GEMINI_SESSION_ID"
|
|
108
|
+
if [[ ! -f "$LACY_GEMINI_SESSION_ID_FILE" ]]; then
|
|
109
|
+
PASS=$(( PASS + 1 ))
|
|
110
|
+
printf ' \e[32m✓\e[0m %s\n' "Session file should be removed"
|
|
111
|
+
else
|
|
112
|
+
printf ' \e[31m✗\e[0m %s\n' "Session file should be removed"
|
|
113
|
+
FAIL=$(( FAIL + 1 ))
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Cleanup via trap (ensures cleanup even on script errors)
|
|
117
|
+
trap 'rm -rf "$TEST_TMPDIR"' EXIT
|
|
118
|
+
|
|
119
|
+
summary
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Test Gemini MCP refactoring (helper and retry logic)
|
|
4
|
+
# Runs in Bash 4+ and ZSH
|
|
5
|
+
|
|
6
|
+
if [[ -n "$ZSH_VERSION" ]]; then
|
|
7
|
+
LACY_SHELL_TYPE="zsh"
|
|
8
|
+
elif [[ -n "$BASH_VERSION" ]]; then
|
|
9
|
+
LACY_SHELL_TYPE="bash"
|
|
10
|
+
else
|
|
11
|
+
echo "FAIL: Unsupported shell"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
echo "Testing Gemini MCP Refactoring in: ${LACY_SHELL_TYPE}"
|
|
16
|
+
echo "================================================================"
|
|
17
|
+
|
|
18
|
+
# Find repo root
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
20
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
21
|
+
|
|
22
|
+
# Set up test environment
|
|
23
|
+
TEST_TMPDIR=$(mktemp -d)
|
|
24
|
+
export LACY_SHELL_HOME="$TEST_TMPDIR"
|
|
25
|
+
|
|
26
|
+
# Source core modules
|
|
27
|
+
source "$REPO_DIR/lib/core/constants.sh"
|
|
28
|
+
source "$REPO_DIR/lib/core/mcp.sh"
|
|
29
|
+
source "$REPO_DIR/lib/core/preheat.sh"
|
|
30
|
+
|
|
31
|
+
# Mock dependencies
|
|
32
|
+
lacy_start_spinner() { :; }
|
|
33
|
+
lacy_stop_spinner() { :; }
|
|
34
|
+
lacy_print_color() { :; }
|
|
35
|
+
|
|
36
|
+
# Mock tool command execution
|
|
37
|
+
MOCK_EXIT_CODE=0
|
|
38
|
+
MOCK_RESPONSE='{"session_id": "new-id", "response": "success"}'
|
|
39
|
+
_lacy_run_tool_cmd() {
|
|
40
|
+
echo "$MOCK_RESPONSE"
|
|
41
|
+
return $MOCK_EXIT_CODE
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Test counter
|
|
45
|
+
PASS=0
|
|
46
|
+
FAIL=0
|
|
47
|
+
|
|
48
|
+
assert_eq() {
|
|
49
|
+
local test_name="$1"
|
|
50
|
+
local expected="$2"
|
|
51
|
+
local actual="$3"
|
|
52
|
+
|
|
53
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
54
|
+
PASS=$(( PASS + 1 ))
|
|
55
|
+
printf ' \e[32m✓\e[0m %s\n' "$test_name"
|
|
56
|
+
else
|
|
57
|
+
printf ' \e[31m✗\e[0m %s\n' "$test_name"
|
|
58
|
+
echo " Expected: $expected"
|
|
59
|
+
echo " Actual: $actual"
|
|
60
|
+
FAIL=$(( FAIL + 1 ))
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
section() {
|
|
65
|
+
echo ""
|
|
66
|
+
echo "--- $1 ---"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ============================================================================
|
|
70
|
+
# Tests
|
|
71
|
+
# ============================================================================
|
|
72
|
+
|
|
73
|
+
section "Helper Function: _lacy_gemini_query_exec"
|
|
74
|
+
|
|
75
|
+
# Initial query (no session)
|
|
76
|
+
LACY_GEMINI_SESSION_ID=""
|
|
77
|
+
out=$(_lacy_gemini_query_exec "hello")
|
|
78
|
+
assert_eq "Initial query should succeed" 0 $?
|
|
79
|
+
assert_eq "Initial query should return mock response" "$MOCK_RESPONSE" "$out"
|
|
80
|
+
|
|
81
|
+
# Query with session
|
|
82
|
+
LACY_GEMINI_SESSION_ID="old-id"
|
|
83
|
+
out=$(_lacy_gemini_query_exec "hello")
|
|
84
|
+
assert_eq "Resumed query should succeed" 0 $?
|
|
85
|
+
assert_eq "Resumed query should return mock response" "$MOCK_RESPONSE" "$out"
|
|
86
|
+
|
|
87
|
+
section "Retry Logic in lacy_shell_query_agent"
|
|
88
|
+
|
|
89
|
+
# Simulate a failure on resume followed by a success on retry
|
|
90
|
+
LACY_GEMINI_SESSION_ID="bad-id"
|
|
91
|
+
LACY_ACTIVE_TOOL="gemini"
|
|
92
|
+
|
|
93
|
+
# Mock behavior: first call fails (if --resume), second call succeeds (if no --resume)
|
|
94
|
+
_lacy_run_tool_cmd() {
|
|
95
|
+
if [[ "$1" == *" --resume "* ]]; then
|
|
96
|
+
return 1
|
|
97
|
+
else
|
|
98
|
+
echo "$MOCK_RESPONSE"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Run query agent
|
|
104
|
+
out=$(lacy_shell_query_agent "retry-test" 2>/dev/null)
|
|
105
|
+
if [[ "$out" == *"success"* ]]; then
|
|
106
|
+
PASS=$(( PASS + 1 ))
|
|
107
|
+
printf ' \e[32m✓\e[0m %s\n' "Retry logic should eventually return success"
|
|
108
|
+
else
|
|
109
|
+
printf ' \e[31m✗\e[0m %s\n' "Retry logic should eventually return success"
|
|
110
|
+
echo " Actual output: $out"
|
|
111
|
+
FAIL=$(( FAIL + 1 ))
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Cleanup
|
|
115
|
+
rm -rf "$TEST_TMPDIR"
|
|
116
|
+
|
|
117
|
+
echo ""
|
|
118
|
+
echo "================================================================"
|
|
119
|
+
printf 'Results: %d passed, %d failed\n' "$PASS" "$FAIL"
|
|
120
|
+
if (( FAIL > 0 )); then
|
|
121
|
+
printf '\e[31mFAILED\e[0m\n'
|
|
122
|
+
exit 1
|
|
123
|
+
else
|
|
124
|
+
printf '\e[32mALL PASSED\e[0m\n'
|
|
125
|
+
exit 0
|
|
126
|
+
fi
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
|
|
3
|
+
# Integration tests for preheat server lifecycle (lash + opencode)
|
|
4
|
+
# Usage: zsh tests/test_preheat_server.zsh
|
|
5
|
+
|
|
6
|
+
setopt NO_MONITOR # suppress job control messages
|
|
7
|
+
|
|
8
|
+
# ============================================================================
|
|
9
|
+
# Test configuration
|
|
10
|
+
# ============================================================================
|
|
11
|
+
|
|
12
|
+
TEST_PORT=14096
|
|
13
|
+
TEST_TMPDIR=$(mktemp -d)
|
|
14
|
+
SCRIPT_DIR="${0:A:h}"
|
|
15
|
+
REPO_ROOT="${SCRIPT_DIR:h}"
|
|
16
|
+
|
|
17
|
+
# Override before sourcing constants.zsh (`:=` pattern respects pre-existing)
|
|
18
|
+
export LACY_SHELL_HOME="$TEST_TMPDIR"
|
|
19
|
+
export LACY_PREHEAT_SERVER_PORT="$TEST_PORT"
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# Source modules (minimal chain — no ZLE/prompt deps)
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
source "$REPO_ROOT/lib/constants.zsh"
|
|
26
|
+
source "$REPO_ROOT/lib/spinner.zsh"
|
|
27
|
+
source "$REPO_ROOT/lib/mcp.zsh"
|
|
28
|
+
source "$REPO_ROOT/lib/preheat.zsh"
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# Assertion helpers
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
34
|
+
_PASS=0 _FAIL=0 _SKIP=0
|
|
35
|
+
|
|
36
|
+
pass() {
|
|
37
|
+
(( _PASS++ ))
|
|
38
|
+
printf ' \e[32m✓\e[0m %s\n' "$1"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fail() {
|
|
42
|
+
(( _FAIL++ ))
|
|
43
|
+
printf ' \e[31m✗\e[0m %s\n' "$1"
|
|
44
|
+
[[ -n "$2" ]] && printf ' %s\n' "$2"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
skip() {
|
|
48
|
+
(( _SKIP++ ))
|
|
49
|
+
printf ' \e[33m⊘\e[0m %s (skipped: %s)\n' "$1" "$2"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
assert_eq() {
|
|
53
|
+
local label="$1" expected="$2" actual="$3"
|
|
54
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
55
|
+
pass "$label"
|
|
56
|
+
else
|
|
57
|
+
fail "$label" "expected='$expected' actual='$actual'"
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
assert_nonblank() {
|
|
62
|
+
local label="$1" value="$2"
|
|
63
|
+
if [[ -n "$value" ]]; then
|
|
64
|
+
pass "$label"
|
|
65
|
+
else
|
|
66
|
+
fail "$label" "expected non-blank value"
|
|
67
|
+
fi
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
assert_empty() {
|
|
71
|
+
local label="$1" value="$2"
|
|
72
|
+
if [[ -z "$value" ]]; then
|
|
73
|
+
pass "$label"
|
|
74
|
+
else
|
|
75
|
+
fail "$label" "expected empty, got='$value'"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
summary() {
|
|
80
|
+
echo ""
|
|
81
|
+
printf '=%.0s' {1..60}; echo ""
|
|
82
|
+
printf 'Results: %d passed, %d failed, %d skipped\n' "$_PASS" "$_FAIL" "$_SKIP"
|
|
83
|
+
if (( _FAIL > 0 )); then
|
|
84
|
+
printf '\e[31mFAILED\e[0m\n'
|
|
85
|
+
return 1
|
|
86
|
+
else
|
|
87
|
+
printf '\e[32mALL PASSED\e[0m\n'
|
|
88
|
+
return 0
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
section() {
|
|
93
|
+
echo ""
|
|
94
|
+
printf '--- %s ---\n' "$1"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# ============================================================================
|
|
98
|
+
# Cleanup (runs on exit, Ctrl-C, assertion failure)
|
|
99
|
+
# ============================================================================
|
|
100
|
+
|
|
101
|
+
cleanup() {
|
|
102
|
+
# Stop server via library function
|
|
103
|
+
lacy_preheat_server_stop 2>/dev/null
|
|
104
|
+
|
|
105
|
+
# Fallback: kill anything on the test port
|
|
106
|
+
local pids
|
|
107
|
+
pids=$(lsof -ti "tcp:$TEST_PORT" 2>/dev/null)
|
|
108
|
+
if [[ -n "$pids" ]]; then
|
|
109
|
+
echo "$pids" | xargs kill 2>/dev/null
|
|
110
|
+
sleep 0.3
|
|
111
|
+
pids=$(lsof -ti "tcp:$TEST_PORT" 2>/dev/null)
|
|
112
|
+
[[ -n "$pids" ]] && echo "$pids" | xargs kill -9 2>/dev/null
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Remove temp directory
|
|
116
|
+
rm -rf "$TEST_TMPDIR"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
trap cleanup EXIT INT TERM
|
|
120
|
+
|
|
121
|
+
# ============================================================================
|
|
122
|
+
# Helpers
|
|
123
|
+
# ============================================================================
|
|
124
|
+
|
|
125
|
+
# Create a mock server script that mimics the lash/opencode REST API.
|
|
126
|
+
# Uses raw sockets for fast startup (< 100ms) since the preheat start
|
|
127
|
+
# function only waits ~3s for health.
|
|
128
|
+
create_mock_server() {
|
|
129
|
+
local tool="$1"
|
|
130
|
+
local mock_bin="$TEST_TMPDIR/bin/$tool"
|
|
131
|
+
mkdir -p "$TEST_TMPDIR/bin"
|
|
132
|
+
|
|
133
|
+
cat > "$mock_bin" << 'MOCK_SERVER'
|
|
134
|
+
#!/usr/bin/env python3
|
|
135
|
+
"""Fast mock server mimicking lash/opencode REST API using raw sockets."""
|
|
136
|
+
import socket, json, sys, uuid, threading
|
|
137
|
+
|
|
138
|
+
if "serve" not in sys.argv:
|
|
139
|
+
print(f"Unknown command: {sys.argv[1:]}", file=sys.stderr)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
port = int(sys.argv[sys.argv.index("--port") + 1]) if "--port" in sys.argv else 4096
|
|
143
|
+
sessions = {}
|
|
144
|
+
|
|
145
|
+
def handle_client(conn):
|
|
146
|
+
try:
|
|
147
|
+
data = conn.recv(65536).decode()
|
|
148
|
+
if not data:
|
|
149
|
+
conn.close()
|
|
150
|
+
return
|
|
151
|
+
lines = data.split("\r\n")
|
|
152
|
+
method, path, _ = lines[0].split(" ", 2)
|
|
153
|
+
|
|
154
|
+
# Extract body (after blank line)
|
|
155
|
+
body = ""
|
|
156
|
+
for i, line in enumerate(lines):
|
|
157
|
+
if line == "":
|
|
158
|
+
body = "\r\n".join(lines[i + 1:])
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
status_code = 404
|
|
162
|
+
resp_body = json.dumps({"error": "not found"})
|
|
163
|
+
|
|
164
|
+
if method == "GET" and path == "/global/health":
|
|
165
|
+
status_code = 200
|
|
166
|
+
resp_body = json.dumps({"status": "ok"})
|
|
167
|
+
|
|
168
|
+
elif method == "POST" and path == "/session":
|
|
169
|
+
sid = str(uuid.uuid4())
|
|
170
|
+
sessions[sid] = []
|
|
171
|
+
status_code = 200
|
|
172
|
+
resp_body = json.dumps({"id": sid})
|
|
173
|
+
|
|
174
|
+
elif method == "POST" and path.startswith("/session/") and path.endswith("/message"):
|
|
175
|
+
sid = path.split("/")[2]
|
|
176
|
+
if sid in sessions:
|
|
177
|
+
d = json.loads(body) if body.strip() else {}
|
|
178
|
+
qt = ""
|
|
179
|
+
for p in d.get("parts", []):
|
|
180
|
+
if p.get("type") == "text":
|
|
181
|
+
qt = p["text"]
|
|
182
|
+
sessions[sid].append(qt)
|
|
183
|
+
status_code = 200
|
|
184
|
+
resp_body = json.dumps([{
|
|
185
|
+
"role": "assistant",
|
|
186
|
+
"parts": [{"type": "text", "text": f"Mock response to: {qt}"}]
|
|
187
|
+
}])
|
|
188
|
+
# else: 404 (default)
|
|
189
|
+
|
|
190
|
+
status_text = "OK" if status_code == 200 else "Not Found"
|
|
191
|
+
resp = (
|
|
192
|
+
f"HTTP/1.1 {status_code} {status_text}\r\n"
|
|
193
|
+
f"Content-Type: application/json\r\n"
|
|
194
|
+
f"Content-Length: {len(resp_body)}\r\n"
|
|
195
|
+
f"Connection: close\r\n"
|
|
196
|
+
f"\r\n"
|
|
197
|
+
f"{resp_body}"
|
|
198
|
+
)
|
|
199
|
+
conn.sendall(resp.encode())
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
finally:
|
|
203
|
+
conn.close()
|
|
204
|
+
|
|
205
|
+
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
206
|
+
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
207
|
+
srv.bind(("127.0.0.1", port))
|
|
208
|
+
srv.listen(5)
|
|
209
|
+
|
|
210
|
+
while True:
|
|
211
|
+
conn, _ = srv.accept()
|
|
212
|
+
threading.Thread(target=handle_client, args=(conn,), daemon=True).start()
|
|
213
|
+
MOCK_SERVER
|
|
214
|
+
|
|
215
|
+
chmod +x "$mock_bin"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Check that a tool (mock) is available on PATH
|
|
219
|
+
ensure_mock_on_path() {
|
|
220
|
+
local tool="$1"
|
|
221
|
+
if [[ ! -x "$TEST_TMPDIR/bin/$tool" ]]; then
|
|
222
|
+
create_mock_server "$tool"
|
|
223
|
+
fi
|
|
224
|
+
export PATH="$TEST_TMPDIR/bin:$PATH"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Wait for the test port to be free (up to 3 seconds)
|
|
228
|
+
wait_for_port_free() {
|
|
229
|
+
local attempts=0
|
|
230
|
+
while (( attempts < 30 )); do
|
|
231
|
+
if ! lsof -ti "tcp:$TEST_PORT" >/dev/null 2>&1; then
|
|
232
|
+
return 0
|
|
233
|
+
fi
|
|
234
|
+
sleep 0.1
|
|
235
|
+
(( attempts++ ))
|
|
236
|
+
done
|
|
237
|
+
# Force kill anything still on the port
|
|
238
|
+
local pids
|
|
239
|
+
pids=$(lsof -ti "tcp:$TEST_PORT" 2>/dev/null)
|
|
240
|
+
[[ -n "$pids" ]] && echo "$pids" | xargs kill -9 2>/dev/null
|
|
241
|
+
sleep 0.3
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Reset preheat state between tool test runs
|
|
245
|
+
reset_preheat_state() {
|
|
246
|
+
lacy_preheat_server_stop 2>/dev/null
|
|
247
|
+
LACY_PREHEAT_SERVER_PID=""
|
|
248
|
+
LACY_PREHEAT_SERVER_PASSWORD=""
|
|
249
|
+
LACY_PREHEAT_SERVER_SESSION_ID=""
|
|
250
|
+
rm -f "$LACY_PREHEAT_SERVER_PID_FILE"
|
|
251
|
+
wait_for_port_free
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# ============================================================================
|
|
255
|
+
# Tests
|
|
256
|
+
# ============================================================================
|
|
257
|
+
|
|
258
|
+
run_tests_for_tool() {
|
|
259
|
+
local tool="$1"
|
|
260
|
+
|
|
261
|
+
section "Tests for: $tool"
|
|
262
|
+
|
|
263
|
+
ensure_mock_on_path "$tool"
|
|
264
|
+
reset_preheat_state
|
|
265
|
+
|
|
266
|
+
# ------------------------------------------------------------------
|
|
267
|
+
# Test 1: Server lifecycle
|
|
268
|
+
# ------------------------------------------------------------------
|
|
269
|
+
lacy_preheat_server_start "$tool"
|
|
270
|
+
local start_rc=$?
|
|
271
|
+
|
|
272
|
+
assert_eq "$tool: server start returns 0" "0" "$start_rc"
|
|
273
|
+
assert_nonblank "$tool: PID is set after start" "$LACY_PREHEAT_SERVER_PID"
|
|
274
|
+
|
|
275
|
+
# PID file written
|
|
276
|
+
if [[ -f "$LACY_PREHEAT_SERVER_PID_FILE" ]]; then
|
|
277
|
+
local file_pid
|
|
278
|
+
file_pid=$(cat "$LACY_PREHEAT_SERVER_PID_FILE")
|
|
279
|
+
assert_eq "$tool: PID file matches in-memory PID" "$LACY_PREHEAT_SERVER_PID" "$file_pid"
|
|
280
|
+
else
|
|
281
|
+
fail "$tool: PID file written" "file not found at $LACY_PREHEAT_SERVER_PID_FILE"
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
# Health check
|
|
285
|
+
lacy_preheat_server_is_healthy
|
|
286
|
+
assert_eq "$tool: server is healthy" "0" "$?"
|
|
287
|
+
|
|
288
|
+
# Stop
|
|
289
|
+
local saved_pid="$LACY_PREHEAT_SERVER_PID"
|
|
290
|
+
lacy_preheat_server_stop
|
|
291
|
+
assert_empty "$tool: PID cleared after stop" "$LACY_PREHEAT_SERVER_PID"
|
|
292
|
+
|
|
293
|
+
# Process is actually gone
|
|
294
|
+
sleep 0.3
|
|
295
|
+
if kill -0 "$saved_pid" 2>/dev/null; then
|
|
296
|
+
fail "$tool: process gone after stop" "PID $saved_pid still alive"
|
|
297
|
+
else
|
|
298
|
+
pass "$tool: process gone after stop"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
# ------------------------------------------------------------------
|
|
302
|
+
# Test 2: Health endpoint validation
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
reset_preheat_state
|
|
305
|
+
lacy_preheat_server_start "$tool"
|
|
306
|
+
|
|
307
|
+
local health_body
|
|
308
|
+
health_body=$(curl -sf --max-time 2 "http://localhost:${TEST_PORT}/global/health" 2>/dev/null)
|
|
309
|
+
local health_rc=$?
|
|
310
|
+
|
|
311
|
+
assert_eq "$tool: health endpoint reachable" "0" "$health_rc"
|
|
312
|
+
|
|
313
|
+
# Verify it's JSON, not HTML (SPA fallback would return <!DOCTYPE or <html)
|
|
314
|
+
if printf '%s' "$health_body" | grep -q '^<'; then
|
|
315
|
+
fail "$tool: health returns JSON (not HTML)" "got HTML: ${health_body:0:80}"
|
|
316
|
+
else
|
|
317
|
+
# Verify it parses as JSON
|
|
318
|
+
if printf '%s' "$health_body" | python3 -c "import json,sys; json.load(sys.stdin)" 2>/dev/null; then
|
|
319
|
+
pass "$tool: health returns JSON (not HTML)"
|
|
320
|
+
else
|
|
321
|
+
fail "$tool: health returns JSON (not HTML)" "not valid JSON: ${health_body:0:80}"
|
|
322
|
+
fi
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
# ------------------------------------------------------------------
|
|
326
|
+
# Test 3: Session creation
|
|
327
|
+
# ------------------------------------------------------------------
|
|
328
|
+
local session_json
|
|
329
|
+
session_json=$(curl -sf --max-time 5 \
|
|
330
|
+
-X POST \
|
|
331
|
+
-H "Content-Type: application/json" \
|
|
332
|
+
-d '{}' \
|
|
333
|
+
"http://localhost:${TEST_PORT}/session" 2>/dev/null)
|
|
334
|
+
|
|
335
|
+
local session_id
|
|
336
|
+
session_id=$(printf '%s' "$session_json" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('id',''))" 2>/dev/null)
|
|
337
|
+
|
|
338
|
+
assert_nonblank "$tool: session creation returns an ID" "$session_id"
|
|
339
|
+
|
|
340
|
+
# ------------------------------------------------------------------
|
|
341
|
+
# Test 4: Job control suppression
|
|
342
|
+
# ------------------------------------------------------------------
|
|
343
|
+
reset_preheat_state
|
|
344
|
+
|
|
345
|
+
local start_output
|
|
346
|
+
start_output=$(lacy_preheat_server_start "$tool" 2>&1)
|
|
347
|
+
|
|
348
|
+
# Check that output does NOT contain job control noise like "[1] 12345"
|
|
349
|
+
if printf '%s' "$start_output" | grep -qE '^\[[0-9]+\] [0-9]+'; then
|
|
350
|
+
fail "$tool: no job control output from start" "got: $start_output"
|
|
351
|
+
else
|
|
352
|
+
pass "$tool: no job control output from start"
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
# ------------------------------------------------------------------
|
|
356
|
+
# Test 5: Stale session reset
|
|
357
|
+
# ------------------------------------------------------------------
|
|
358
|
+
# Set a fake session ID and try to query — should fail and clear it.
|
|
359
|
+
# Redirect to file instead of $() to preserve global state changes.
|
|
360
|
+
LACY_PREHEAT_SERVER_SESSION_ID="fake-session-id-that-does-not-exist"
|
|
361
|
+
|
|
362
|
+
local _stale_out="$TEST_TMPDIR/_stale_out"
|
|
363
|
+
lacy_preheat_server_query "hello" > "$_stale_out" 2>/dev/null
|
|
364
|
+
local stale_rc=$?
|
|
365
|
+
|
|
366
|
+
# The query should fail (404 from mock for unknown session)
|
|
367
|
+
assert_eq "$tool: stale session query fails" "1" "$stale_rc"
|
|
368
|
+
assert_empty "$tool: stale session ID cleared" "$LACY_PREHEAT_SERVER_SESSION_ID"
|
|
369
|
+
|
|
370
|
+
# ------------------------------------------------------------------
|
|
371
|
+
# Test 6: Message sending (uses mock server, no API key needed)
|
|
372
|
+
# ------------------------------------------------------------------
|
|
373
|
+
LACY_PREHEAT_SERVER_SESSION_ID=""
|
|
374
|
+
|
|
375
|
+
local _query_out="$TEST_TMPDIR/_query_out"
|
|
376
|
+
lacy_preheat_server_query "say hello" > "$_query_out" 2>/dev/null
|
|
377
|
+
local query_rc=$?
|
|
378
|
+
local query_result
|
|
379
|
+
query_result=$(cat "$_query_out")
|
|
380
|
+
|
|
381
|
+
assert_eq "$tool: mock query returns 0" "0" "$query_rc"
|
|
382
|
+
assert_nonblank "$tool: mock query returns text" "$query_result"
|
|
383
|
+
|
|
384
|
+
# ------------------------------------------------------------------
|
|
385
|
+
# Test 7: Session reuse
|
|
386
|
+
# ------------------------------------------------------------------
|
|
387
|
+
local first_session="$LACY_PREHEAT_SERVER_SESSION_ID"
|
|
388
|
+
assert_nonblank "$tool: session ID set after first query" "$first_session"
|
|
389
|
+
|
|
390
|
+
# Second query should reuse the same session
|
|
391
|
+
local _query_out2="$TEST_TMPDIR/_query_out2"
|
|
392
|
+
lacy_preheat_server_query "say goodbye" > "$_query_out2" 2>/dev/null
|
|
393
|
+
local query_rc2=$?
|
|
394
|
+
|
|
395
|
+
assert_eq "$tool: second query returns 0" "0" "$query_rc2"
|
|
396
|
+
assert_eq "$tool: session ID reused" "$first_session" "$LACY_PREHEAT_SERVER_SESSION_ID"
|
|
397
|
+
|
|
398
|
+
# ------------------------------------------------------------------
|
|
399
|
+
# Test 8: Full mcp.zsh integration
|
|
400
|
+
# ------------------------------------------------------------------
|
|
401
|
+
LACY_ACTIVE_TOOL="$tool"
|
|
402
|
+
LACY_PREHEAT_SERVER_SESSION_ID=""
|
|
403
|
+
|
|
404
|
+
# Stub spinner to avoid terminal noise
|
|
405
|
+
lacy_start_spinner() { : }
|
|
406
|
+
lacy_stop_spinner() { : }
|
|
407
|
+
|
|
408
|
+
local _mcp_out="$TEST_TMPDIR/_mcp_out"
|
|
409
|
+
lacy_shell_query_agent "what is 2+2" > "$_mcp_out" 2>/dev/null
|
|
410
|
+
local mcp_rc=$?
|
|
411
|
+
local mcp_result
|
|
412
|
+
mcp_result=$(cat "$_mcp_out")
|
|
413
|
+
|
|
414
|
+
assert_eq "$tool: mcp integration returns 0" "0" "$mcp_rc"
|
|
415
|
+
assert_nonblank "$tool: mcp integration returns text" "$mcp_result"
|
|
416
|
+
assert_nonblank "$tool: server PID set during integration" "$LACY_PREHEAT_SERVER_PID"
|
|
417
|
+
|
|
418
|
+
# Clean up for next tool
|
|
419
|
+
reset_preheat_state
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# ============================================================================
|
|
423
|
+
# Main
|
|
424
|
+
# ============================================================================
|
|
425
|
+
|
|
426
|
+
echo "Preheat Server Integration Tests"
|
|
427
|
+
echo "Port: $TEST_PORT | Temp: $TEST_TMPDIR"
|
|
428
|
+
|
|
429
|
+
# Check prerequisites
|
|
430
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
431
|
+
echo "ERROR: python3 required for mock server"
|
|
432
|
+
exit 1
|
|
433
|
+
fi
|
|
434
|
+
|
|
435
|
+
if ! command -v curl >/dev/null 2>&1; then
|
|
436
|
+
echo "ERROR: curl required for HTTP tests"
|
|
437
|
+
exit 1
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# Run tests for each tool
|
|
441
|
+
run_tests_for_tool "lash"
|
|
442
|
+
run_tests_for_tool "opencode"
|
|
443
|
+
|
|
444
|
+
# Print summary and exit with appropriate code
|
|
445
|
+
summary
|
|
446
|
+
exit $?
|