Skip to content

Commit 63b0a73

Browse files
hironowclaude
andauthored
refact tests (#26)
* fail tests exists * Ignoring the fail --------- Co-authored-by: Claude <[email protected]>
1 parent b860453 commit 63b0a73

23 files changed

+589
-232
lines changed

.claude/settings.local.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(docker logs:*)",
5+
"Bash(docker compose stop:*)",
6+
"Bash(docker compose rm:*)",
7+
"Bash(docker volume:*)",
8+
"Bash(docker compose:*)",
9+
"Bash(uv run pytest:*)"
10+
],
11+
"deny": [],
12+
"ask": []
13+
}
14+
}

.github/workflows/test-emulators.yaml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ concurrency:
1919
jobs:
2020
unit-integration-e2e:
2121
runs-on: ubuntu-latest
22-
timeout-minutes: 15
22+
timeout-minutes: 30
2323

2424
steps:
2525
- uses: actions/checkout@v5
@@ -37,7 +37,14 @@ jobs:
3737
uv sync --all-extras --frozen
3838
3939
- name: Clean up any existing Docker volumes
40-
run: docker compose down -v || true
40+
run: |
41+
# Only clean up if services are already running
42+
if docker compose ps -q 2>/dev/null | grep -q .; then
43+
echo "Cleaning up existing services and volumes..."
44+
docker compose down -v
45+
else
46+
echo "No existing services to clean up."
47+
fi
4148
4249
- name: Pre-build selected images (incl. Postgres 18)
4350
run: bash scripts/prebuild-images.sh a2a-inspector firebase-emulator postgres
@@ -47,12 +54,20 @@ jobs:
4754

4855
- name: Wait for services to be ready
4956
run: bash scripts/wait-for-services.sh --default 90 --a2a 180 --postgres 150
50-
57+
58+
- name: Test Elasticsearch separately
59+
timeout-minutes: 2
60+
run: bash scripts/test-elasticsearch.sh
61+
5162
- name: Run unit/integration tests (non-e2e)
63+
timeout-minutes: 5
5264
run: bash scripts/run-tests-fast.sh
5365

5466
- name: Run e2e tests
55-
run: bash scripts/run-tests-e2e.sh | tee e2e.log
67+
timeout-minutes: 20
68+
run: |
69+
set -o pipefail
70+
bash scripts/run-tests-e2e.sh | tee e2e.log
5671
5772
- name: Summarize skipped e2e tests
5873
if: always()

docker-compose.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ services:
300300
- cli # Only run when explicitly requested
301301
# Elasticsearch
302302
elasticsearch:
303-
image: elasticsearch:8.19.5
303+
image: elasticsearch:9.2.1
304304
container_name: elasticsearch-emulator
305305
ports:
306306
- "${ELASTICSEARCH_PORT:-9200}:9200" # REST API
@@ -309,6 +309,9 @@ services:
309309
- discovery.type=single-node
310310
- xpack.security.enabled=false
311311
- ES_JAVA_OPTS=-Xms512m -Xmx512m
312+
- "bootstrap.memory_lock=false"
313+
- "cluster.routing.allocation.disk.threshold_enabled=false"
314+
- "action.destructive_requires_name=false"
312315
volumes:
313316
- elasticsearch_data:/usr/share/elasticsearch/data
314317
healthcheck:

elasticsearch-cli/main.go

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import (
1616
)
1717

1818
var (
19-
host string
20-
port string
21-
client *http.Client
19+
host string
20+
port string
21+
client *http.Client
22+
verbose bool
2223
)
2324

2425
func init() {
@@ -32,8 +33,11 @@ func init() {
3233
port = "9200"
3334
}
3435

36+
// Enable verbose logging for debugging (especially in CI)
37+
verbose = os.Getenv("ES_CLI_VERBOSE") == "1" || os.Getenv("ES_CLI_VERBOSE") == "true"
38+
3539
client = &http.Client{
36-
Timeout: 30 * time.Second,
40+
Timeout: 60 * time.Second,
3741
}
3842
}
3943

@@ -236,6 +240,14 @@ func executeCommand(command string) {
236240
return
237241
}
238242

243+
// Wait for index shards to be ready after PUT (index creation)
244+
if method == "PUT" && strings.HasPrefix(path, "/") && !strings.Contains(path, "/_") {
245+
indexName := strings.Split(strings.TrimPrefix(path, "/"), "/")[0]
246+
if indexName != "" {
247+
waitForIndexReady(indexName)
248+
}
249+
}
250+
239251
// Pretty print JSON response
240252
var prettyJSON bytes.Buffer
241253
if err := json.Indent(&prettyJSON, []byte(resp), "", " "); err != nil {
@@ -247,15 +259,65 @@ func executeCommand(command string) {
247259
fmt.Printf("\nTime: %v\n", time.Since(start))
248260
}
249261

262+
func waitForIndexReady(indexName string) {
263+
// Wait for index shards to be ready (yellow or green status)
264+
// Use a longer timeout for CI environments and poll for readiness
265+
maxRetries := 60 // 60 seconds total
266+
267+
if verbose {
268+
fmt.Printf("[VERBOSE] Waiting for index '%s' to be ready (max %ds)...\n", indexName, maxRetries)
269+
}
270+
271+
for i := 0; i < maxRetries; i++ {
272+
healthPath := fmt.Sprintf("/_cluster/health/%s", indexName)
273+
start := time.Now()
274+
resp, err := makeRequest("GET", healthPath, nil)
275+
elapsed := time.Since(start)
276+
277+
if err != nil {
278+
if verbose {
279+
fmt.Printf("[VERBOSE] Retry %d/%d: Health check failed after %v: %v\n", i+1, maxRetries, elapsed, err)
280+
}
281+
time.Sleep(1 * time.Second)
282+
continue
283+
}
284+
285+
result := gjson.Parse(resp)
286+
status := result.Get("status").String()
287+
initializingShards := result.Get("initializing_shards").Int()
288+
activeShards := result.Get("active_shards").Int()
289+
290+
if verbose {
291+
fmt.Printf("[VERBOSE] Retry %d/%d: status=%s, initializing_shards=%d, active_shards=%d (took %v)\n",
292+
i+1, maxRetries, status, initializingShards, activeShards, elapsed)
293+
}
294+
295+
if (status == "green" || status == "yellow") && initializingShards == 0 {
296+
if verbose {
297+
fmt.Printf("[VERBOSE] Index '%s' ready after %d attempts (%.2fs total)\n", indexName, i+1, float64(i+1))
298+
}
299+
return // Index is ready
300+
}
301+
302+
time.Sleep(1 * time.Second)
303+
}
304+
305+
// Log warning but don't fail - the index might still become available
306+
fmt.Printf("Warning: Index %s not ready after %d seconds\n", indexName, maxRetries)
307+
}
308+
250309
func makeRequest(method, path string, body []byte) (string, error) {
251310
url := fmt.Sprintf("http://%s:%s%s", host, port, path)
311+
start := time.Now()
252312

253313
var req *http.Request
254314
var err error
255315

256316
if body != nil {
257317
req, err = http.NewRequest(method, url, bytes.NewBuffer(body))
258-
req.Header.Set("Content-Type", "application/json")
318+
if err == nil {
319+
req.Header.Set("Content-Type", "application/json")
320+
}
259321
} else {
260322
req, err = http.NewRequest(method, url, nil)
261323
}
@@ -264,17 +326,41 @@ func makeRequest(method, path string, body []byte) (string, error) {
264326
return "", err
265327
}
266328

329+
if verbose {
330+
bodyPreview := ""
331+
if body != nil && len(body) > 0 {
332+
if len(body) > 100 {
333+
bodyPreview = fmt.Sprintf(" (body: %d bytes)", len(body))
334+
} else {
335+
bodyPreview = fmt.Sprintf(" (body: %s)", string(body))
336+
}
337+
}
338+
fmt.Printf("[VERBOSE] Request: %s %s%s\n", method, path, bodyPreview)
339+
}
340+
267341
resp, err := client.Do(req)
342+
elapsed := time.Since(start)
343+
268344
if err != nil {
345+
if verbose {
346+
fmt.Printf("[VERBOSE] Request failed after %v: %v\n", elapsed, err)
347+
}
269348
return "", err
270349
}
271350
defer resp.Body.Close()
272351

273352
respBody, err := io.ReadAll(resp.Body)
274353
if err != nil {
354+
if verbose {
355+
fmt.Printf("[VERBOSE] Failed to read response body after %v: %v\n", elapsed, err)
356+
}
275357
return "", err
276358
}
277359

360+
if verbose {
361+
fmt.Printf("[VERBOSE] Response: HTTP %d (took %v, %d bytes)\n", resp.StatusCode, elapsed, len(respBody))
362+
}
363+
278364
if resp.StatusCode >= 400 {
279365
return string(respBody), fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
280366
}

justfile

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,22 @@ up nobuild='no':
4141
fi
4242

4343
# Wait for services
44-
wait default='60' a2a='180':
45-
@bash scripts/wait-for-services.sh --default {{default}} --a2a {{a2a}}
44+
wait default='30' a2a='60' postgres='60':
45+
@bash scripts/wait-for-services.sh --default {{default}} --a2a {{a2a}} --postgres {{postgres}}
46+
47+
# Clean up volumes (use with caution - deletes all data)
48+
clean-volumes:
49+
@echo '⚠️ Cleaning up Docker volumes...'
50+
docker compose down -v || true
51+
@echo '✅ Volumes cleaned.'
4652

4753
# One-shot: prebuild -> up -> wait
4854
start:
55+
@echo '🧹 Cleaning up old volumes...'
56+
@docker compose down -v || true
4957
@bash scripts/prebuild-images.sh a2a-inspector firebase-emulator postgres
5058
@bash scripts/start-services.sh
51-
@bash scripts/wait-for-services.sh --default 60 --a2a 180
59+
@bash scripts/wait-for-services.sh --default 30 --a2a 60 --postgres 60
5260

5361
# Stop emulators (with Firebase export)
5462
stop:
@@ -90,7 +98,7 @@ lint path='tests/' opts='--fix':
9098
@echo '🔍 Linting code with ruff...'
9199
uv run ruff check '{{path}}' '{{opts}}'
92100
@echo 'Semgrep linting...'
93-
uv run semgrep --config .semgrep/
101+
uv run semgrep --config .semgrep/ --error
94102
@echo '✅ Linting finished.'
95103

96104

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ dev = [
2222
"docker>=7.1.0",
2323
"pytest>=8.4.1",
2424
"pytest-asyncio>=0.25.0",
25+
"pytest-timeout>=2.3.1",
2526
"ruff>=0.12.4",
2627
]
2728

2829
[tool.pytest.ini_options]
2930
markers = [
3031
"e2e: end-to-end tests that require Docker and running emulators",
3132
]
33+
# Default timeout for all tests (can be overridden per test)
34+
timeout = 180
35+
# Timeout method: 'thread' is more compatible with async tests
36+
timeout_method = "thread"

scripts/run-tests-e2e.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ if ! command -v uv >/dev/null 2>&1; then
99
fi
1010

1111
echo "Running E2E tests"
12+
# E2E tests run sequentially due to Docker client state management
13+
# Parallel execution (-n auto) causes Docker API 404 errors
1214
uv run pytest tests/e2e -v -m e2e -ra
1315
echo "Done."
1416

scripts/test-elasticsearch.sh

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Test Elasticsearch functionality
5+
# Usage: bash scripts/test-elasticsearch.sh
6+
7+
echo "Testing Elasticsearch..."
8+
9+
# 1. Check cluster health
10+
echo "1. Checking cluster health..."
11+
HEALTH_RESPONSE=$(curl -s http://localhost:9200/_cluster/health)
12+
echo "Response: $HEALTH_RESPONSE"
13+
14+
STATUS=$(echo "$HEALTH_RESPONSE" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
15+
echo "Cluster status: $STATUS"
16+
17+
if [[ "$STATUS" != "green" && "$STATUS" != "yellow" ]]; then
18+
echo "ERROR: Cluster status is not green or yellow" >&2
19+
exit 1
20+
fi
21+
22+
# 2. Check if shards are initialized
23+
INITIALIZING_SHARDS=$(echo "$HEALTH_RESPONSE" | grep -o '"initializing_shards":[0-9]*' | cut -d':' -f2)
24+
echo "Initializing shards: $INITIALIZING_SHARDS"
25+
26+
if [[ "$INITIALIZING_SHARDS" != "0" ]]; then
27+
echo "WARNING: Shards are still initializing" >&2
28+
fi
29+
30+
# 3. Create a test index
31+
TEST_INDEX="es_health_check_$(date +%s)"
32+
echo ""
33+
echo "2. Creating test index: $TEST_INDEX"
34+
CREATE_RESPONSE=$(curl -s -X PUT "http://localhost:9200/$TEST_INDEX" \
35+
-H 'Content-Type: application/json' \
36+
-d '{
37+
"settings": {
38+
"number_of_shards": 1,
39+
"number_of_replicas": 0
40+
}
41+
}')
42+
echo "Response: $CREATE_RESPONSE"
43+
44+
# 4. Wait for index to be ready
45+
echo ""
46+
echo "3. Waiting for index to be ready..."
47+
MAX_WAIT=30
48+
for i in $(seq 1 $MAX_WAIT); do
49+
INDEX_HEALTH=$(curl -s "http://localhost:9200/_cluster/health/$TEST_INDEX?wait_for_status=yellow&timeout=1s")
50+
INDEX_STATUS=$(echo "$INDEX_HEALTH" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
51+
52+
if [[ "$INDEX_STATUS" == "green" || "$INDEX_STATUS" == "yellow" ]]; then
53+
echo "Index is ready (status: $INDEX_STATUS)"
54+
break
55+
fi
56+
57+
if [[ $i -eq $MAX_WAIT ]]; then
58+
echo "ERROR: Index not ready after ${MAX_WAIT}s" >&2
59+
exit 1
60+
fi
61+
62+
sleep 1
63+
done
64+
65+
# 5. Insert a test document
66+
echo ""
67+
echo "4. Inserting test document..."
68+
INSERT_RESPONSE=$(curl -s -X POST "http://localhost:9200/$TEST_INDEX/_doc" \
69+
-H 'Content-Type: application/json' \
70+
-d '{"test": "document", "timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}')
71+
echo "Response: $INSERT_RESPONSE"
72+
73+
# 6. Search for the document
74+
echo ""
75+
echo "5. Searching for test document..."
76+
sleep 1 # Wait for indexing
77+
SEARCH_RESPONSE=$(curl -s -X GET "http://localhost:9200/$TEST_INDEX/_search" \
78+
-H 'Content-Type: application/json' \
79+
-d '{
80+
"query": {
81+
"match_all": {}
82+
}
83+
}')
84+
echo "Response: $SEARCH_RESPONSE"
85+
86+
HITS=$(echo "$SEARCH_RESPONSE" | grep -o '"total":{"value":[0-9]*' | grep -o '[0-9]*$')
87+
echo "Total hits: $HITS"
88+
89+
if [[ "$HITS" != "1" ]]; then
90+
echo "ERROR: Expected 1 hit, got $HITS" >&2
91+
exit 1
92+
fi
93+
94+
# 7. Delete the test index
95+
echo ""
96+
echo "6. Cleaning up test index..."
97+
DELETE_RESPONSE=$(curl -s -X DELETE "http://localhost:9200/$TEST_INDEX")
98+
echo "Response: $DELETE_RESPONSE"
99+
100+
echo ""
101+
echo "✓ All Elasticsearch tests passed!"

0 commit comments

Comments
 (0)