33set -euo pipefail
44
55: " ${PATH_BASE:= " $( git rev-parse --show-toplevel) " } "
6+ : " ${PATH_BIN:= " ${PATH_BASE} /.bin" } "
67: " ${ARGSH_SOURCE:= " argsh" } " ; export ARGSH_SOURCE
78: " ${MIN_COVERAGE:= " 70" } "
89
@@ -85,15 +86,20 @@ _test() {
8586 BATS_LOAD=" argsh.min.sh"
8687 export BATS_LOAD
8788 }
89+
8890 case " ${target} " in
8991 all)
90- ARGSH_BUILTIN_TEST=1 argsh::main test libraries .docker/test
92+ ARGSH_BUILTIN_TEST=1 \
93+ argsh::main test libraries .docker/test
9194 if command -v cargo & > /dev/null; then
9295 test::minifier
9396 fi
9497 ;;
9598 argsh) argsh::main test libraries .docker/test ;;
96- builtin) ARGSH_BUILTIN_TEST=1 argsh::main test libraries .docker/test ;;
99+ builtin)
100+ ARGSH_BUILTIN_TEST=1 \
101+ argsh::main test libraries .docker/test
102+ ;;
97103 minifier) test::minifier ;;
98104 * )
99105 echo " Unknown target: ${target} . Use: all, argsh, builtin, minifier" >&2
@@ -209,16 +215,24 @@ coverage::builtin() {
209215 rustup component add llvm-tools
210216 }
211217
212- # Build with coverage instrumentation (override release profile settings that break coverage)
213- echo " Building with coverage instrumentation..."
214- RUSTFLAGS=" -C instrument-coverage" \
215- CARGO_PROFILE_RELEASE_STRIP=" none" \
216- CARGO_PROFILE_RELEASE_LTO=" false" \
217- CARGO_PROFILE_RELEASE_PANIC=" unwind" \
218- cargo build --release \
219- --manifest-path " ${PATH_BASE} /builtin/Cargo.toml"
218+ # Build coverage-instrumented .so inside Docker (rust:1-slim-bookworm) to ensure
219+ # glibc compatibility with the kcov/kcov test container (also Debian bookworm).
220+ # Mount sources at the same absolute path so llvm-cov finds them on the host.
221+ # Share cargo registry cache to avoid re-downloading dependencies.
222+ echo " Building with coverage instrumentation (via Docker)..."
223+ mkdir -p " ${HOME} /.cargo/registry"
224+ docker run --rm \
225+ --user " $( id -u) :$( id -g) " \
226+ -v " ${PATH_BASE} /builtin:${PATH_BASE} /builtin" \
227+ -v " ${HOME} /.cargo/registry:/usr/local/cargo/registry" \
228+ -w " ${PATH_BASE} /builtin" \
229+ rust:1-slim-bookworm \
230+ bash -c ' RUSTFLAGS="-C instrument-coverage" \
231+ CARGO_PROFILE_RELEASE_STRIP=none \
232+ CARGO_PROFILE_RELEASE_LTO=false \
233+ CARGO_PROFILE_RELEASE_PANIC=unwind \
234+ cargo build --release'
220235
221- rm -f " ${PATH_BIN} /argsh.so" && cp " ${so_path} " " ${PATH_BIN} /argsh.so"
222236 rm -rf " ${cov_dir} "
223237 mkdir -p " ${cov_dir} "
224238
@@ -239,17 +253,17 @@ coverage::builtin() {
239253 " ${llvm_tools} /llvm-cov" report \
240254 --instr-profile=" ${cov_dir} /coverage.profdata" \
241255 " ${so_path} " \
242- --ignore-filename-regex=' \. cargo/registry' \
243- --ignore-filename-regex=' \. rustup/toolchains' \
256+ --ignore-filename-regex=' cargo/registry' \
257+ --ignore-filename-regex=' rustup/toolchains' \
244258 --ignore-filename-regex=' ^/rustc/'
245259
246260 # Export JSON and generate builtin/coverage.json
247261 local _cov_tmp; _cov_tmp=" $( mktemp) "
248262 " ${llvm_tools} /llvm-cov" export \
249263 --instr-profile=" ${cov_dir} /coverage.profdata" \
250264 " ${so_path} " \
251- --ignore-filename-regex=' \. cargo/registry' \
252- --ignore-filename-regex=' \. rustup/toolchains' \
265+ --ignore-filename-regex=' cargo/registry' \
266+ --ignore-filename-regex=' rustup/toolchains' \
253267 --ignore-filename-regex=' ^/rustc/' \
254268 --summary-only > " ${_cov_tmp} "
255269
@@ -528,15 +542,28 @@ _coverage() {
528542}
529543
530544# ##
531- # ## all (minify + lint + coverage + commit)
545+ # ## ci (minify + lint + coverage + commit)
532546# ##
533- _all () {
547+ _ci () {
534548 local message=" regenerate files"
535549 local -a args=(
536550 ' message|m' " Commit message"
537551 )
538552 :args " Minify, lint, coverage, then commit regenerated files" " ${@ } "
539553
554+ if [[ -n " $( git -C " ${PATH_BASE} " status --porcelain) " ]]; then
555+ echo " Warning: repository has uncommitted changes." >&2
556+ git -C " ${PATH_BASE} " status --short >&2
557+ echo >&2
558+ if [[ -t 0 ]]; then
559+ read -rp " Continue anyway? [y/N] " answer
560+ [[ " ${answer} " =~ ^[Yy] ]] || { echo " Aborted." >&2 ; return 1; }
561+ else
562+ echo " Non-interactive mode — aborting. Commit or stash changes first." >&2
563+ return 1
564+ fi
565+ fi
566+
540567 minify::argsh
541568 _lint
542569 _lint -m
@@ -546,27 +573,105 @@ _all() {
546573 git -C " ${PATH_BASE} " commit -m " ${message} " --no-gpg-sign
547574}
548575
576+ # ##
577+ # ## e2e (manual integration tests using claude CLI)
578+ # ##
579+ _e2e () {
580+ local script model budget
581+ # shellcheck disable=SC2016
582+ local prompt=' You have MCP tools from "test-app". Call the serve tool with verbose set to true. Reply with ONLY the raw tool output.'
583+ local -a args=(
584+ ' script|s' " Script to test (default: built-in fixture)"
585+ ' model|m' " Claude model (default: haiku)"
586+ ' budget|b' " Max budget in USD (default: 0.05)"
587+ ' prompt' " Custom prompt for Claude"
588+ )
589+ :args " E2E MCP test: validates tool discovery + invocation via Claude CLI" " ${@ } "
590+
591+ : " ${model:= haiku} "
592+ : " ${budget:= 0.05} "
593+ : " ${script:= " ${PATH_BASE} /libraries/fixtures/args/mcp_test.sh" } "
594+
595+ binary::exists claude || {
596+ echo " claude CLI required. Install: https://docs.anthropic.com/en/docs/claude-code"
597+ return 1
598+ } >&2
599+ script=" $( cd " $( dirname " ${script} " ) " && pwd) /$( basename " ${script} " ) "
600+ [[ -x " ${script} " ]] || { echo " Script not executable: ${script} " >&2 ; return 1; }
601+
602+ # Ensure builtin .so
603+ [[ -f " ${PATH_BIN} /argsh.so" ]] || {
604+ local release=" ${PATH_BASE} /builtin/target/release/libargsh.so"
605+ [[ -f " ${release} " ]] || { echo " builtin .so not found — run: argsh build builtin" >&2 ; return 1; }
606+ cp " ${release} " " ${PATH_BIN} /argsh.so"
607+ }
608+
609+ echo " Pre-flight: testing ${script} in CLI mode..."
610+ local out
611+ out=$( " ${script} " --help 2>&1 ) || { echo " Script --help failed" >&2 ; return 1; }
612+ echo " OK — help text works"
613+
614+ # Build MCP config (JSON-escape the script path)
615+ local mcp_config escaped_script
616+ escaped_script=" ${script// \\ / \\\\ } "
617+ escaped_script=" ${escaped_script// \" / \\\" } "
618+ mcp_config=$( printf ' {"mcpServers":{"test-app":{"type":"stdio","command":"%s","args":["mcp"]}}}' " ${escaped_script} " )
619+
620+ echo " "
621+ echo " Running: claude --print --model ${model} --max-budget-usd ${budget} "
622+ echo " MCP server: ${script} mcp"
623+ echo " "
624+
625+ local result
626+ result=$( claude \
627+ --print \
628+ --model " ${model} " \
629+ --max-budget-usd " ${budget} " \
630+ --mcp-config " ${mcp_config} " \
631+ --allowedTools " mcp__test-app__*" \
632+ --permission-mode " bypassPermissions" \
633+ --no-session-persistence \
634+ -p " ${prompt} " \
635+ 2> /dev/null) || { echo " claude CLI failed" >&2 ; return 1; }
636+
637+ echo " Claude response:"
638+ echo " ${result} "
639+ echo " ---"
640+
641+ local pass=0 total=2
642+ if echo " ${result} " | grep -qi " serving" ; then echo " PASS: contains 'serving'" ; (( ++ pass)) ; else echo " FAIL: missing 'serving'" ; fi
643+ if echo " ${result} " | grep -qi " verbose" ; then echo " PASS: contains 'verbose'" ; (( ++ pass)) ; else echo " FAIL: missing 'verbose'" ; fi
644+
645+ echo " "
646+ echo " Results: ${pass} /${total} "
647+ [[ " ${pass} " -eq " ${total} " ]] || return 1
648+ }
649+
549650# ##
550651# ## main
551652# ##
552653argsh::main () {
553654 local tty=" "
554- argsh::docker & > /dev/null || {
555- echo " Docker build failed. Run 'argsh docker' to see errors." >&2
556- return 1
557- }
655+ local _image=" ${ARGSH_DOCKER_IMAGE:- ghcr.io/ arg-sh/ argsh: latest} "
656+
657+ if [[ -z " ${ARGSH_DOCKER_IMAGE:- } " ]]; then
658+ argsh::docker & > /dev/null || {
659+ echo " Docker build failed. Run 'argsh docker' to see errors." >&2
660+ return 1
661+ }
662+ fi
558663
559664 [[ ! -t 1 ]] || tty=" -it"
560665 # shellcheck disable=SC2046
561666 docker run --rm ${tty} $( docker::user) -w /workspace \
562667 -e " BATS_LOAD" \
563668 -e " ARGSH_SOURCE" \
564669 -e " ARGSH_BUILTIN_TEST" \
565- -e " ARGSH_BUILTIN_PATH" \
670+ ${ARGSH_BUILTIN_PATH : + -e " ARGSH_BUILTIN_PATH= ${ARGSH_BUILTIN_PATH} " } \
566671 -e " LLVM_PROFILE_FILE" \
567672 -e " GIT_COMMIT_SHA=$( git rev-parse HEAD 2> /dev/null || :) " \
568673 -e " GIT_VERSION=$( git describe --tags --dirty 2> /dev/null || :) " \
569- ghcr.io/arg-sh/argsh:latest " ${@ } "
674+ " ${_image} " " ${@ } "
570675}
571676
572677_main () {
@@ -585,7 +690,7 @@ _main() {
585690 local -a usage
586691 usage=(
587692 - " Commands"
588- ' all:-_all ' " Minify, lint, coverage, commit regenerated files"
693+ ' ci| all:-_ci ' " Minify, lint, coverage, commit regenerated files"
589694 ' build:-_build' " Build release binaries [all|builtin|minifier]"
590695 ' lint:-_lint' " Run linters [all|argsh|builtin|minifier]"
591696 ' test:-_test' " Run tests [all|argsh|builtin|minifier]"
@@ -596,6 +701,8 @@ _main() {
596701 ' docs-lint:-lint::docs' " Run documentation linters"
597702 ' docs-test:-test::docs' " Run documentation tests"
598703 ' vale:-lint::vale' " Run vale"
704+ - " Integration"
705+ ' e2e:-_e2e' " E2E MCP test via Claude CLI [--script ./app.sh]"
599706 - " Additional commands"
600707 ' docker:-argsh::docker' " Build docker image"
601708 )
0 commit comments