-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_suggest_optimize.bats
More file actions
executable file
·574 lines (502 loc) · 24.8 KB
/
test_suggest_optimize.bats
File metadata and controls
executable file
·574 lines (502 loc) · 24.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
#!/usr/bin/env bats
# Integration tests for scripts/suggest-optimize.sh
#
# Each test creates an isolated git repo + transcript in $BATS_TEST_TMPDIR.
# Commits use future timestamps to guarantee they are "after" the session start
# without needing sleep.
load helpers/setup
setup() {
setup_test_repo
}
# ═══════════════════════════════════════════════════════════════════════════════
# Early exits — hook should produce no output and exit 0
# ═══════════════════════════════════════════════════════════════════════════════
# Setup: Fully configured Python project with a .py commit.
# Hook input has stop_hook_active=true.
# Validates: When Claude Code signals that the stop hook has already fired
# (e.g., Claude already responded to a previous block), the hook
# must exit immediately to avoid an infinite block loop.
# Expected: Exit 0, no JSON output (no block).
@test "exits when stop_hook_active is true" {
add_python_commit
create_pyproject
create_fake_venv "$REPO/.venv"
run run_hook true "VIRTUAL_ENV=$REPO/.venv"
assert_no_block
}
# Setup: Git repo with a .py commit. Hook input has an empty transcript_path.
# Validates: The hook uses the transcript file's birth time to determine when
# the session started. Without a valid path, it cannot compute session
# start and must bail out.
# Expected: Exit 0, no JSON output.
@test "exits when transcript_path is empty" {
add_python_commit
local input_file="$BATS_TEST_TMPDIR/hook_input.json"
jq -nc '{transcript_path: "", stop_hook_active: false}' > "$input_file"
cd "$REPO"
run bash "$SUGGEST_OPTIMIZE" < "$input_file"
assert_no_block
}
# Setup: Git repo with a .py commit. Hook input points to a transcript file
# that does not exist on disk.
# Validates: The hook checks `[ ! -f "$TRANSCRIPT_PATH" ]` before proceeding.
# A stale or incorrect transcript path must not cause errors.
# Expected: Exit 0, no JSON output.
@test "exits when transcript file does not exist" {
add_python_commit
local input_file="$BATS_TEST_TMPDIR/hook_input.json"
jq -nc --arg tp "/nonexistent/path/transcript.jsonl" \
'{transcript_path: $tp, stop_hook_active: false}' > "$input_file"
cd "$REPO"
run bash "$SUGGEST_OPTIMIZE" < "$input_file"
assert_no_block
}
# Setup: Git repo with a commit that only touches a .txt file (no .py/.js/.ts).
# Valid transcript exists.
# Validates: The hook scans `git log` for commits touching *.py, *.js, *.ts,
# *.jsx, *.tsx. When no matching files are found, there is nothing
# to optimize.
# Expected: Exit 0, no JSON output.
@test "exits when no commits have py/js/ts files" {
add_irrelevant_commit
run run_hook false
assert_no_block
}
# Setup: Fully configured Python project with one .py commit.
# Run the hook twice with the exact same commit state.
# Validates: The hook writes a dedup marker (SHA-256 of commit hashes) to
# $TRANSCRIPT_DIR/codeflash-seen. On the second invocation with
# identical commits, it finds the marker and skips to avoid
# suggesting optimization twice for the same changes.
# Expected: First run blocks; second run exits silently (dedup).
@test "exits on second run (dedup via seen marker)" {
add_python_commit
create_pyproject
create_fake_venv "$REPO/.venv"
# First run — should block
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
# Second run with same commits — dedup marker exists
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_no_block
}
# Setup: Fully configured Python project. Make one commit, run hook (blocks).
# Then make a second commit and run hook again.
# Validates: The dedup marker is a hash of all relevant commit SHAs. When a
# new commit is added, the hash changes, so the hook correctly
# recognizes there are new changes to optimize.
# Expected: Both runs produce a block decision.
@test "triggers again after a new commit (dedup hash changes)" {
create_pyproject
create_fake_venv "$REPO/.venv"
add_python_commit "first.py"
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
# New commit changes the dedup hash
add_python_commit "second.py"
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
}
# ═══════════════════════════════════════════════════════════════════════════════
# Python projects
# ═══════════════════════════════════════════════════════════════════════════════
# Setup: pyproject.toml with [tool.codeflash] section. Fake venv at .venv/
# with a mock codeflash binary. VIRTUAL_ENV pointed at the fake venv.
# One .py file committed after session start.
# Validates: The "happy path" — everything is set up, codeflash should just run.
# The hook instructs Claude to execute `codeflash --subagent` as a
# background task.
# Expected: Block with reason containing "codeflash --subagent" and
# "run_in_background".
@test "python: configured + codeflash installed → run codeflash" {
add_python_commit
create_pyproject true
create_fake_venv "$REPO/.venv"
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
assert_reason_contains "codeflash --subagent"
assert_reason_contains "run_in_background"
}
# Setup: pyproject.toml with [tool.codeflash]. Fake venv exists but does NOT
# contain a codeflash binary. VIRTUAL_ENV set.
# Validates: When codeflash is configured but not installed in the venv, the
# hook should prompt the user to install it before optimization can run.
# Expected: Block with reason containing "pip install codeflash".
@test "python: configured + codeflash NOT installed → install prompt" {
add_python_commit
create_pyproject true
create_fake_venv "$REPO/.venv" false
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
assert_reason_contains "pip install codeflash"
}
# Setup: pyproject.toml exists but has NO [tool.codeflash] section. Fake venv
# with codeflash binary installed. VIRTUAL_ENV set.
# Validates: When codeflash is installed but not configured, the hook should
# instruct Claude to discover the project structure (module root,
# tests folder) and write the [tool.codeflash] config section.
# Expected: Block with reason containing "[tool.codeflash]" and "module-root"
# (the config fields to be written).
@test "python: NOT configured + codeflash installed → setup prompt" {
add_python_commit
create_pyproject false
create_fake_venv "$REPO/.venv"
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
assert_reason_contains "[tool.codeflash]"
assert_reason_contains "module-root"
}
# Setup: pyproject.toml without [tool.codeflash]. Fake venv WITHOUT codeflash
# binary. VIRTUAL_ENV set.
# Validates: When both installation and configuration are missing, the hook
# should instruct Claude to both install codeflash and set up the
# config. The install step is embedded within the setup instructions.
# Expected: Block with reason containing both "[tool.codeflash]" (setup) and
# "install codeflash" (installation).
@test "python: NOT configured + NOT installed → setup + install prompt" {
add_python_commit
create_pyproject false
create_fake_venv "$REPO/.venv" false
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
assert_reason_contains "[tool.codeflash]"
assert_reason_contains "install codeflash"
}
# Setup: pyproject.toml with [tool.codeflash]. No .venv or venv directory
# anywhere. VIRTUAL_ENV not set.
# Validates: Without a virtual environment, codeflash cannot run. The hook
# should instruct the user to create a venv, install codeflash in it,
# and restart Claude Code from within the activated environment.
# Expected: Block with reason containing "virtual environment" and
# "python3 -m venv" (the venv creation command).
@test "python: no venv + configured → create venv prompt" {
add_python_commit
create_pyproject true
# No venv created, no VIRTUAL_ENV set
run run_hook false
assert_block
assert_reason_contains "virtual environment"
assert_reason_contains "python3 -m venv"
}
# Setup: pyproject.toml WITHOUT [tool.codeflash]. No venv anywhere.
# VIRTUAL_ENV not set.
# Validates: The worst case — nothing is set up. The hook should instruct the
# user to create a venv, install codeflash, AND set up the config.
# The setup instructions (with [tool.codeflash] template) are included
# alongside the venv creation steps.
# Expected: Block with reason containing "virtual environment",
# "python3 -m venv", and "[tool.codeflash]".
@test "python: no venv + NOT configured → create venv + setup prompt" {
add_python_commit
create_pyproject false
run run_hook false
assert_block
assert_reason_contains "virtual environment"
assert_reason_contains "python3 -m venv"
assert_reason_contains "[tool.codeflash]"
}
# Setup: pyproject.toml with [tool.codeflash]. Fake venv at $REPO/.venv with
# codeflash binary. VIRTUAL_ENV is NOT set (not passed to env).
# Validates: The hook sources find-venv.sh which searches CHECK_DIR/.venv,
# CHECK_DIR/venv, REPO_ROOT/.venv, REPO_ROOT/venv for an activate
# script. It should find .venv, activate it (setting VIRTUAL_ENV),
# and then proceed as if the venv was active from the start.
# Expected: Block with reason containing "codeflash --subagent" (same as the
# happy path — auto-discovery is transparent).
@test "python: auto-discovers .venv when VIRTUAL_ENV not set" {
add_python_commit
create_pyproject true
create_fake_venv "$REPO/.venv" true
# Don't pass VIRTUAL_ENV — script should find .venv itself
run run_hook false
assert_block
assert_reason_contains "codeflash --subagent"
}
# ═══════════════════════════════════════════════════════════════════════════════
# JavaScript / TypeScript projects
# ═══════════════════════════════════════════════════════════════════════════════
# Setup: package.json with "codeflash" config key. Mock npx that returns
# success for `codeflash --version`. PATH includes mock bin.
# One .js file committed after session start. No pyproject.toml.
# Validates: The JS "happy path" — package.json is configured, codeflash npm
# package is available via npx. The hook instructs Claude to run
# `npx codeflash --subagent` in the background.
# Expected: Block with reason containing "npx codeflash --subagent" and
# "run_in_background".
@test "js: configured + codeflash installed → run codeflash" {
add_js_commit
create_package_json true
setup_mock_npx true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "npx codeflash --subagent"
assert_reason_contains "run_in_background"
}
# Setup: package.json with "codeflash" key. Mock npx returns failure for
# `codeflash --version` (package not installed). One .js commit.
# Validates: When codeflash is configured in package.json but the npm package
# is not installed, the hook should prompt to install it as a dev
# dependency before running.
# Expected: Block with reason containing "npm install --save-dev codeflash".
@test "js: configured + NOT installed → install prompt" {
add_js_commit
create_package_json true
setup_mock_npx false
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "npm install --save-dev codeflash"
}
# Setup: package.json exists but has NO "codeflash" key. Mock npx returns
# success (codeflash is installed). One .js commit.
# Validates: When codeflash is installed but not configured, the hook should
# instruct Claude to discover project structure and add the "codeflash"
# config key to package.json with moduleRoot, testsRoot, etc.
# Expected: Block with reason containing "moduleRoot" and "testsRoot"
# (the config fields to be added to package.json).
@test "js: NOT configured + installed → setup prompt" {
add_js_commit
create_package_json false
setup_mock_npx true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "moduleRoot"
assert_reason_contains "testsRoot"
}
# Setup: package.json without "codeflash" key. Mock npx fails (not installed).
# One .js commit.
# Validates: When both installation and configuration are missing for a JS
# project. The setup message should include an install step
# ("npm install --save-dev codeflash") embedded within the broader
# config setup instructions.
# Expected: Block with reason containing both "moduleRoot" (setup) and
# "npm install --save-dev codeflash" (installation).
@test "js: NOT configured + NOT installed → setup + install prompt" {
add_js_commit
create_package_json false
setup_mock_npx false
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "moduleRoot"
assert_reason_contains "npm install --save-dev codeflash"
}
# Setup: Configured package.json + mock npx. Commit touches a .ts file
# instead of .js.
# Validates: TypeScript files (*.ts) are detected by the git log filter and
# route through the JS project path (since package.json is the
# project config). The hook should treat .ts the same as .js.
# Expected: Block with reason containing "npx codeflash --subagent".
@test "js: typescript file triggers JS path" {
add_ts_commit "utils.ts"
create_package_json true
setup_mock_npx true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "npx codeflash --subagent"
}
# Setup: Configured package.json + mock npx. Commit touches a .jsx file.
# Validates: JSX files (*.jsx) are also detected by the git log filter
# (-- '*.jsx') and processed via the JS path. Ensures React
# component files trigger optimization.
# Expected: Block with reason containing "npx codeflash --subagent".
@test "js: jsx file triggers JS path" {
add_js_commit "Component.jsx"
create_package_json true
setup_mock_npx true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "npx codeflash --subagent"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Java projects
# ═══════════════════════════════════════════════════════════════════════════════
# Setup: pom.xml exists (Maven project). Mock codeflash binary on PATH.
# One .java file committed after session start. No codeflash.toml.
# Validates: Fresh Java project detected via pom.xml (not codeflash.toml).
# The CLI auto-configures on first run, so the hook should just
# tell Claude to run codeflash --subagent directly.
# Expected: Block with reason containing "codeflash --subagent" and
# "run_in_background".
@test "java: pom.xml + codeflash installed → run codeflash" {
add_java_commit
create_pom_xml
setup_mock_codeflash true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "codeflash --subagent"
assert_reason_contains "run_in_background"
}
# Setup: pom.xml exists. No codeflash binary on PATH, uv mocked to fail.
# One .java commit.
# Validates: When codeflash is not installed, the hook should prompt
# the user to install it before optimization can run.
# Expected: Block with reason containing "pip install codeflash".
@test "java: pom.xml + NOT installed → install prompt" {
add_java_commit
create_pom_xml
# Use a minimal PATH that excludes codeflash and real uv.
# Mock uv to fail so it doesn't find a global codeflash.
cat > "$MOCK_BIN/uv" << 'MOCK'
#!/bin/bash
exit 1
MOCK
chmod +x "$MOCK_BIN/uv"
run run_hook false "PATH=$MOCK_BIN:/usr/bin:/bin"
assert_block
assert_reason_contains "pip install codeflash"
}
# Setup: codeflash.toml with [tool.codeflash] section (already configured).
# Mock codeflash binary on PATH. One .java commit.
# Validates: Projects that already have codeflash.toml (from a previous
# codeflash run or manual init) are detected and handled correctly.
# Expected: Block with reason containing "codeflash --subagent".
@test "java: codeflash.toml configured + installed → run codeflash" {
add_java_commit
create_codeflash_toml true
setup_mock_codeflash true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "codeflash --subagent"
}
# Setup: build.gradle exists (Gradle project). Mock codeflash on PATH.
# One .java commit.
# Validates: Gradle projects are detected via build.gradle, same as Maven
# projects are detected via pom.xml.
# Expected: Block with reason containing "codeflash --subagent".
@test "java: build.gradle + installed → run codeflash" {
add_java_commit
create_build_gradle
setup_mock_codeflash true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "codeflash --subagent"
}
# Setup: pom.xml exists. Mock codeflash on PATH. Commit only touches .py
# file (no .java files).
# Validates: The HAS_JAVA_CHANGES guard prevents the Java path from firing
# when no .java files were actually committed. Even though
# detect_project() finds pom.xml and sets PROJECT_TYPE=java,
# the guard skips the Java path. The Python fallback path may still
# fire (it checks HAS_PYTHON_CHANGES, not PROJECT_TYPE).
# Expected: Block does NOT contain "Java" — the Java path was correctly skipped.
@test "java: pom.xml but only .py committed → no Java trigger" {
add_python_commit
create_pom_xml
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_not_contains "Java"
}
# Setup: Both pom.xml and pyproject.toml exist. Mock codeflash on PATH.
# Commit touches .java file only.
# Validates: When both Java and Python markers exist, pom.xml is checked
# before pyproject.toml in detect_project(). With only .java
# changes, the Java path fires (not Python).
# Expected: Block with "codeflash --subagent" and NOT "npx" or "venv".
@test "java: pom.xml takes precedence over pyproject.toml" {
add_java_commit
create_pom_xml
create_pyproject true
setup_mock_codeflash true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "codeflash --subagent"
assert_reason_not_contains "npx"
assert_reason_not_contains "virtual environment"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Permissions — auto-allow instructions
# ═══════════════════════════════════════════════════════════════════════════════
# Setup: Fully configured Python project. No .claude/settings.json exists.
# Validates: When codeflash is not yet auto-allowed, the hook appends
# instructions telling Claude to add `Bash(*codeflash*)` to the
# permissions.allow array in .claude/settings.json. This enables
# future runs to execute without user permission prompts.
# Expected: Block reason contains "permissions.allow" and "Bash(*codeflash*)".
@test "includes auto-allow instructions when settings.json missing" {
add_python_commit
create_pyproject true
create_fake_venv "$REPO/.venv"
run run_hook false "VIRTUAL_ENV=$REPO/.venv" "CODEFLASH_API_KEY=cf-test-key"
assert_block
assert_reason_contains "permissions.allow"
assert_reason_contains 'Bash(*codeflash*)'
}
# Setup: Fully configured Python project. .claude/settings.json exists and
# already has "Bash(*codeflash*)" in permissions.allow.
# Validates: When auto-allow is already configured, the hook should NOT include
# the permissions setup instructions. The message should only contain
# the "run codeflash" instruction.
# Expected: Block reason does NOT contain "permissions.allow".
@test "omits auto-allow when already configured" {
add_python_commit
create_pyproject true
create_fake_venv "$REPO/.venv"
create_auto_allow
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
assert_reason_not_contains "permissions.allow"
}
# Setup: Fully configured JS project. No .claude/settings.json exists.
# Validates: Same as the Python auto-allow test, but for JS projects. The
# auto-allow logic is shared (checked at script top before branching
# on project type), but the instructions are appended separately in
# each path. This verifies the JS path also appends them.
# Expected: Block reason contains "permissions.allow".
@test "js: includes auto-allow instructions when settings.json missing" {
add_js_commit
create_package_json true
setup_mock_npx true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "permissions.allow"
}
# Setup: Fully configured JS project. .claude/settings.json has
# "Bash(*codeflash*)" in permissions.allow.
# Validates: JS path correctly omits auto-allow instructions when already set.
# Expected: Block reason does NOT contain "permissions.allow".
@test "js: omits auto-allow when already configured" {
add_js_commit
create_package_json true
setup_mock_npx true
create_auto_allow
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_not_contains "permissions.allow"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Project detection precedence
# ═══════════════════════════════════════════════════════════════════════════════
# Setup: BOTH pyproject.toml (with [tool.codeflash]) and package.json (with
# "codeflash" key) exist in the same directory. Fake venv with
# codeflash installed. One .py commit.
# Validates: The detect_project function checks pyproject.toml before
# package.json at each directory level. When both exist, the Python
# path should be chosen. This ensures Python projects with a
# package.json (e.g., for JS tooling) don't accidentally take the
# JS path.
# Expected: Block with "codeflash --subagent" (bare, Python-style) and
# NOT "npx" (which would indicate the JS path).
@test "pyproject.toml takes precedence over package.json in same directory" {
add_python_commit
create_pyproject true
create_package_json true
create_fake_venv "$REPO/.venv"
run run_hook false "VIRTUAL_ENV=$REPO/.venv"
assert_block
# Python path: uses bare codeflash, not npx
assert_reason_contains "codeflash --subagent"
assert_reason_not_contains "npx"
}
# Setup: Only package.json exists (no pyproject.toml). Configured with
# "codeflash" key. Mock npx available. One .js commit.
# Validates: When pyproject.toml is absent, detect_project correctly falls
# through to package.json and identifies the project as JS/TS.
# Expected: Block with "npx codeflash --subagent" (JS-style invocation).
@test "detects package.json when no pyproject.toml exists" {
add_js_commit
# Only package.json, no pyproject.toml
create_package_json true
setup_mock_npx true
run run_hook false "PATH=$MOCK_BIN:$PATH"
assert_block
assert_reason_contains "npx codeflash --subagent"
}