@boyingliu01/xp-gate 0.31.3 → 0.32.0
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/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/adapters/java.sh
CHANGED
|
@@ -33,6 +33,15 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
33
33
|
PLUGIN_DIR="$SCRIPT_DIR/plugins/p3c-java"
|
|
34
34
|
WHALECLOUD_PLUGIN_DIR="$SCRIPT_DIR/plugins/whalecloud-java"
|
|
35
35
|
|
|
36
|
+
# #640: smart test selection helpers (strategy truth table + path-convention
|
|
37
|
+
# lookup). Sourced from lib/ so the logic stays unit-testable in isolation;
|
|
38
|
+
# `|| true` keeps a stale install (adapter present, lib missing) from breaking
|
|
39
|
+
# static analysis, which does not depend on it. run_coverage degrades to the
|
|
40
|
+
# full suite when the functions are absent.
|
|
41
|
+
_JAVA_SELECTION_LIB="$SCRIPT_DIR/../lib/java-test-selection.sh"
|
|
42
|
+
# shellcheck source=/dev/null
|
|
43
|
+
[ -f "$_JAVA_SELECTION_LIB" ] && . "$_JAVA_SELECTION_LIB"
|
|
44
|
+
|
|
36
45
|
WC_JAVA_LINT_DEFAULT_TIMEOUT=180
|
|
37
46
|
|
|
38
47
|
# ---------------------------------------------------------------------------
|
|
@@ -528,14 +537,257 @@ _detect_jacoco_configured() {
|
|
|
528
537
|
return 1
|
|
529
538
|
}
|
|
530
539
|
|
|
540
|
+
# ── #640: Java smart test selection ─────────────────────────────────────────
|
|
541
|
+
# The Java arm ran the FULL suite on every commit (measured: `mvn test
|
|
542
|
+
# jacoco:report` = 77.8s for 1079 tests whether one file or a hundred changed).
|
|
543
|
+
# TS (#286) and Python (#399) already had three-tier selection; Java did not,
|
|
544
|
+
# and PARTIAL_TEST_RUN was therefore never set on this path — the downgrade
|
|
545
|
+
# channel in Stage 2 was dead code for Java.
|
|
546
|
+
#
|
|
547
|
+
# This mirrors the Python selector exactly:
|
|
548
|
+
# 1. changed-tests — only test files changed: run exactly those.
|
|
549
|
+
# 2. related — sources changed, no tests: run what the path convention maps.
|
|
550
|
+
# 3. full — >20 changed tests, a build manifest changed, or no related
|
|
551
|
+
# tests found.
|
|
552
|
+
# Tiers 1/2 set PARTIAL_TEST_RUN so Stage 2 WARNS about subset coverage instead
|
|
553
|
+
# of blocking on it (subset coverage is meaningless as a global floor).
|
|
554
|
+
#
|
|
555
|
+
# Escape valve: XP_GATE_JAVA_TEST_SELECTION=off forces the full suite — for
|
|
556
|
+
# anyone who would rather pay the time than risk a missed transitive impact.
|
|
557
|
+
# Selection can only narrow the run, never widen it: every non-selected case
|
|
558
|
+
# falls back to `full`.
|
|
559
|
+
|
|
560
|
+
# Colon-separated list of changed paths, as handed in by pre-commit. Empty when
|
|
561
|
+
# the caller does not provide one (older pre-commit, direct adapter use), in
|
|
562
|
+
# which case we must NOT silently narrow the run — see _java_selection_enabled.
|
|
563
|
+
_JAVA_CHANGED_FILES="${GATE_CHANGED_FILES:-}"
|
|
564
|
+
|
|
565
|
+
# True when selection is usable: the escape valve is off AND the caller told us
|
|
566
|
+
# what changed. Without changed files we cannot select, and guessing "nothing
|
|
567
|
+
# changed" would run zero tests and report PASS — the exact failure mode the
|
|
568
|
+
# fail-closed contract forbids.
|
|
569
|
+
_java_selection_enabled() {
|
|
570
|
+
[ "${XP_GATE_JAVA_TEST_SELECTION:-on}" != "off" ] || return 1
|
|
571
|
+
[ -n "$_JAVA_CHANGED_FILES" ] || return 1
|
|
572
|
+
return 0
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
# Split the changed-file list into (test_count, src_count, deps_changed) and
|
|
576
|
+
# echo the strategy. Pure delegation to the lib so the truth table lives in one
|
|
577
|
+
# testable place.
|
|
578
|
+
_java_select_strategy() {
|
|
579
|
+
local test_count=0 src_count=0 deps_changed=0 path
|
|
580
|
+
local _java_files
|
|
581
|
+
# Newline-delimited, either from a colon list or a pre-split newline list.
|
|
582
|
+
_java_files=$(printf '%s' "$_JAVA_CHANGED_FILES" | tr ':' '\n')
|
|
583
|
+
|
|
584
|
+
while IFS= read -r path; do
|
|
585
|
+
[ -n "$path" ] || continue
|
|
586
|
+
case "$path" in
|
|
587
|
+
*.java) ;; # only Java sources influence the Java counts
|
|
588
|
+
*) continue ;;
|
|
589
|
+
esac
|
|
590
|
+
if java_is_test_file "$path"; then
|
|
591
|
+
test_count=$((test_count + 1))
|
|
592
|
+
else
|
|
593
|
+
src_count=$((src_count + 1))
|
|
594
|
+
fi
|
|
595
|
+
done <<EOF
|
|
596
|
+
$_java_files
|
|
597
|
+
EOF
|
|
598
|
+
|
|
599
|
+
# Build manifests are checked across ALL changed paths (a pom.xml change is
|
|
600
|
+
# not a .java file but invalidates every test).
|
|
601
|
+
# Build manifests are checked across ALL changed paths (a pom.xml change is
|
|
602
|
+
# not a .java file but invalidates every test). Newline-delimited and passed
|
|
603
|
+
# through a here-string rather than unquoted word splitting: a path with a
|
|
604
|
+
# space would otherwise arrive as two fragments, and on Windows a fragment
|
|
605
|
+
# containing a drive colon is indistinguishable from a separator.
|
|
606
|
+
while IFS= read -r path; do
|
|
607
|
+
[ -n "$path" ] || continue
|
|
608
|
+
if java_dependency_changed "$path"; then
|
|
609
|
+
deps_changed=1
|
|
610
|
+
break
|
|
611
|
+
fi
|
|
612
|
+
done <<EOF
|
|
613
|
+
$(printf '%s' "$_JAVA_CHANGED_FILES" | tr ':' '\n')
|
|
614
|
+
EOF
|
|
615
|
+
|
|
616
|
+
select_java_test_strategy "$test_count" "$src_count" "$deps_changed"
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
# Resolve the concrete test files to run for `related`, by mapping every changed
|
|
620
|
+
# non-test Java source through the path convention. Echoes one path per line.
|
|
621
|
+
#
|
|
622
|
+
# Exit code distinguishes the two outcomes that both produce empty stdout:
|
|
623
|
+
# 0 — lookup completed; no related test exists (caller escalates to full)
|
|
624
|
+
# 1 — the lookup itself broke (helper function missing). The caller must ALSO
|
|
625
|
+
# escalate to full, but must not silently treat a broken lookup as
|
|
626
|
+
# "nothing related" — that is the difference between a slow commit and a
|
|
627
|
+
# commit that never ran the tests it should have.
|
|
628
|
+
_java_related_test_files() {
|
|
629
|
+
command -v java_related_tests >/dev/null 2>&1 || return 1
|
|
630
|
+
local path seen="" rc=0
|
|
631
|
+
local _java_files
|
|
632
|
+
_java_files=$(printf '%s' "$_JAVA_CHANGED_FILES" | tr ':' '\n')
|
|
633
|
+
while IFS= read -r path; do
|
|
634
|
+
[ -n "$path" ] || continue
|
|
635
|
+
case "$path" in *.java) ;; *) continue ;; esac
|
|
636
|
+
java_is_test_file "$path" && continue
|
|
637
|
+
local related
|
|
638
|
+
while IFS= read -r related; do
|
|
639
|
+
[ -n "$related" ] || continue
|
|
640
|
+
case "$seen" in
|
|
641
|
+
*"|$related|"*) continue ;;
|
|
642
|
+
esac
|
|
643
|
+
seen="$seen|$related|"
|
|
644
|
+
printf '%s\n' "$related"
|
|
645
|
+
done <<EOF
|
|
646
|
+
$(java_related_tests "$path")
|
|
647
|
+
EOF
|
|
648
|
+
done <<EOF
|
|
649
|
+
$_java_files
|
|
650
|
+
EOF
|
|
651
|
+
return "$rc"
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
# Run the Maven suite. With a non-empty target list, runs only those classes;
|
|
655
|
+
# otherwise runs everything.
|
|
656
|
+
#
|
|
657
|
+
# $1 target list (comma-separated simple class names), may be empty
|
|
658
|
+
#
|
|
659
|
+
# Both fail-if-no-tests switches are required and they are NOT the same flag:
|
|
660
|
+
# -Dsurefire.failIfNoSpecifiedTests=false — a module with NO class matching
|
|
661
|
+
# -Dtest otherwise fails the build. In a multi-module repo the changed
|
|
662
|
+
# class lives in one module and every other module holds zero matches;
|
|
663
|
+
# without this the selective run is WORSE than the full one (measured:
|
|
664
|
+
# BUILD FAILURE on a 2-module project vs. a clean full-suite pass).
|
|
665
|
+
# -DfailIfNoTests=false — the older property some surefire versions read;
|
|
666
|
+
# kept for plugins predating the renamed key.
|
|
667
|
+
_run_maven_tests() {
|
|
668
|
+
local targets="$1"
|
|
669
|
+
if [ -n "$targets" ]; then
|
|
670
|
+
echo " ℹ️ Java selective run — $targets"
|
|
671
|
+
mvn test jacoco:report -q -Dtest="$targets" \
|
|
672
|
+
-Dsurefire.failIfNoSpecifiedTests=false \
|
|
673
|
+
-DfailIfNoTests=false 2>&1 | tail -10
|
|
674
|
+
else
|
|
675
|
+
mvn test jacoco:report -q 2>&1 | tail -10
|
|
676
|
+
fi
|
|
677
|
+
return "${PIPESTATUS[0]}"
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
# Run the Gradle suite, optionally filtered with repeated --tests flags.
|
|
681
|
+
#
|
|
682
|
+
# $1..$N simple class names, may be empty
|
|
683
|
+
_run_gradle_tests() {
|
|
684
|
+
if [ "$#" -gt 0 ] && [ -n "$1" ]; then
|
|
685
|
+
local filters=()
|
|
686
|
+
local name
|
|
687
|
+
for name in "$@"; do
|
|
688
|
+
[ -n "$name" ] || continue
|
|
689
|
+
filters+=("--tests" "$name")
|
|
690
|
+
done
|
|
691
|
+
echo " ℹ️ Java selective run — ${*}"
|
|
692
|
+
gradle test jacocoTestReport --quiet "${filters[@]}" 2>&1 | tail -10
|
|
693
|
+
else
|
|
694
|
+
gradle test jacocoTestReport --quiet 2>&1 | tail -10
|
|
695
|
+
fi
|
|
696
|
+
return "${PIPESTATUS[0]}"
|
|
697
|
+
}
|
|
698
|
+
|
|
531
699
|
run_coverage() {
|
|
532
700
|
local build_system
|
|
533
701
|
build_system=$(_detect_java_build)
|
|
534
702
|
|
|
703
|
+
# Decide the selection once; every branch below honours it the same way.
|
|
704
|
+
local strategy="full" targets="" gradle_filters=()
|
|
705
|
+
if _java_selection_enabled; then
|
|
706
|
+
# lib/java-test-selection.sh is sourced by run_static_analysis's caller
|
|
707
|
+
# chain; guard so a direct invocation still works.
|
|
708
|
+
if command -v select_java_test_strategy >/dev/null 2>&1; then
|
|
709
|
+
strategy=$(_java_select_strategy)
|
|
710
|
+
case "$strategy" in
|
|
711
|
+
changed-tests)
|
|
712
|
+
# Feed ONLY actual test files to the selector. The strategy said
|
|
713
|
+
# "test files changed", but the raw list also carries every staged
|
|
714
|
+
# path; passing a production class here would have surefire report
|
|
715
|
+
# "No tests were executed" — and because of the failIfNoSpecifiedTests
|
|
716
|
+
# flag this run deliberately sets, that would exit 0 and count as a
|
|
717
|
+
# PASS while nothing ran. Filtering by java_is_test_file keeps the
|
|
718
|
+
# selector list and the strategy decision consistent.
|
|
719
|
+
local _ct_files=""
|
|
720
|
+
local _ct_path
|
|
721
|
+
while IFS= read -r _ct_path; do
|
|
722
|
+
[ -n "$_ct_path" ] || continue
|
|
723
|
+
case "$_ct_path" in *.java) ;; *) continue ;; esac
|
|
724
|
+
java_is_test_file "$_ct_path" || continue
|
|
725
|
+
_ct_files="$_ct_files$_ct_path"$'\n'
|
|
726
|
+
done <<EOF
|
|
727
|
+
$(printf '%s' "$_JAVA_CHANGED_FILES" | tr ':' '\n')
|
|
728
|
+
EOF
|
|
729
|
+
# shellcheck disable=SC2046
|
|
730
|
+
targets=$(java_maven_test_selector $(printf '%s' "$_ct_files"))
|
|
731
|
+
# shellcheck disable=SC2046,SC2207
|
|
732
|
+
gradle_filters=($(java_gradle_test_filters $(printf '%s' "$_ct_files")))
|
|
733
|
+
if [ -z "$targets" ] && [ "${#gradle_filters[@]}" -eq 0 ]; then
|
|
734
|
+
# Strategy said test files changed but none survived classification.
|
|
735
|
+
# Never run an empty inline list: widen to full rather than pass on
|
|
736
|
+
# a command that executes nothing.
|
|
737
|
+
strategy="full"
|
|
738
|
+
fi
|
|
739
|
+
;;
|
|
740
|
+
related)
|
|
741
|
+
local related_files
|
|
742
|
+
related_files=$(_java_related_test_files)
|
|
743
|
+
local _rel_rc=$?
|
|
744
|
+
if [ "$_rel_rc" -ne 0 ]; then
|
|
745
|
+
# The lookup itself failed (helper missing). Escalate instead of
|
|
746
|
+
# pretending "nothing related" — an empty answer from a broken
|
|
747
|
+
# lookup and an empty answer from a real miss are different things.
|
|
748
|
+
strategy="full"
|
|
749
|
+
elif [ -n "$related_files" ]; then
|
|
750
|
+
# Word splitting IS the intent on both lines: $related_files is a
|
|
751
|
+
# space-separated path list and both helpers take it as $@.
|
|
752
|
+
# shellcheck disable=SC2046,SC2086
|
|
753
|
+
targets=$(java_maven_test_selector $related_files)
|
|
754
|
+
# shellcheck disable=SC2207,SC2086
|
|
755
|
+
gradle_filters=($(java_gradle_test_filters $related_files))
|
|
756
|
+
fi
|
|
757
|
+
if [ -z "$targets" ] && [ "${#gradle_filters[@]}" -eq 0 ]; then
|
|
758
|
+
# No related tests found -> tier 3, never "run nothing".
|
|
759
|
+
strategy="full"
|
|
760
|
+
fi
|
|
761
|
+
;;
|
|
762
|
+
esac
|
|
763
|
+
fi
|
|
764
|
+
fi
|
|
765
|
+
|
|
766
|
+
case "$strategy" in
|
|
767
|
+
changed-tests|related)
|
|
768
|
+
# Subset run: the global coverage floor cannot be verified from it, so
|
|
769
|
+
# mark the run partial and let Stage 2 WARN (same contract as #399/#556).
|
|
770
|
+
#
|
|
771
|
+
# A plain `PARTIAL_TEST_RUN=true` does NOT work here: pre-commit calls
|
|
772
|
+
# `run_coverage` inside `$(...)`, so this function runs in a subshell and
|
|
773
|
+
# the assignment dies with it. Python's selective path gets away with the
|
|
774
|
+
# bare assignment only because it sets the flag at the call site, in the
|
|
775
|
+
# caller's own shell. Publish the decision through the file the caller
|
|
776
|
+
# named instead; the caller turns it into the flag.
|
|
777
|
+
if [ -n "${XP_GATE_PARTIAL_RUN_FILE:-}" ]; then
|
|
778
|
+
printf '%s\n' "$strategy" > "$XP_GATE_PARTIAL_RUN_FILE" 2>/dev/null || true
|
|
779
|
+
fi
|
|
780
|
+
echo " ℹ️ Java: selective run ($strategy)"
|
|
781
|
+
;;
|
|
782
|
+
*)
|
|
783
|
+
echo " ℹ️ Java: full suite"
|
|
784
|
+
;;
|
|
785
|
+
esac
|
|
786
|
+
|
|
535
787
|
if [ "$build_system" = "maven" ]; then
|
|
536
788
|
if _detect_jacoco_configured "$build_system"; then
|
|
537
|
-
|
|
538
|
-
return
|
|
789
|
+
_run_maven_tests "$targets"
|
|
790
|
+
return $?
|
|
539
791
|
else
|
|
540
792
|
echo "ℹ️ JaCoCo not configured in pom.xml — SKIP coverage check"
|
|
541
793
|
echo " To enable: add jacoco-maven-plugin to your pom.xml"
|
|
@@ -543,8 +795,8 @@ run_coverage() {
|
|
|
543
795
|
fi
|
|
544
796
|
elif [ "$build_system" = "gradle" ]; then
|
|
545
797
|
if _detect_jacoco_configured "$build_system"; then
|
|
546
|
-
|
|
547
|
-
return
|
|
798
|
+
_run_gradle_tests "${gradle_filters[@]}"
|
|
799
|
+
return $?
|
|
548
800
|
else
|
|
549
801
|
echo "ℹ️ JaCoCo not configured in Gradle build — SKIP coverage check"
|
|
550
802
|
echo " To enable: add 'id \"jacoco\"' to your build.gradle plugins block"
|
package/adapters/powershell.sh
CHANGED
|
@@ -59,8 +59,36 @@ _detect_pwsh() {
|
|
|
59
59
|
return 1
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
# List PowerShell SOURCE files under the current directory, without walking the
|
|
63
|
+
# pruned artifact directories.
|
|
64
|
+
#
|
|
65
|
+
# #620: the caller used to pass '*.ps1', so a .psm1 module file was invisible --
|
|
66
|
+
# a repo whose code lives in modules (the normal layout for anything that ships
|
|
67
|
+
# one) got "no PowerShell files found outside the pruned directories" on every
|
|
68
|
+
# commit, and the message blamed node_modules/dist instead of the glob. Our own
|
|
69
|
+
# complexity path (gate-3.sh) already treated '.ps(m)?1$' as PowerShell, so the
|
|
70
|
+
# two gates disagreed about what a PowerShell file is.
|
|
71
|
+
#
|
|
72
|
+
# The extension set is owned here rather than at each call site: one definition,
|
|
73
|
+
# so Gate 1 and Gate 3 cannot drift apart again. Callers that want a narrower
|
|
74
|
+
# population (e.g. only *.Tests.ps1) still pass an explicit pattern.
|
|
75
|
+
_POWERSHELL_SOURCE_PATTERNS=('*.ps1' '*.psm1')
|
|
76
|
+
|
|
62
77
|
_find_powershell_files() {
|
|
63
|
-
local name_pattern="$1"
|
|
78
|
+
local name_pattern="${1:-}"
|
|
79
|
+
|
|
80
|
+
# Default to every PowerShell source file. A bare name_pattern keeps the old
|
|
81
|
+
# single-extension behaviour for callers that pass one deliberately.
|
|
82
|
+
local -a name_test=()
|
|
83
|
+
if [ -z "$name_pattern" ]; then
|
|
84
|
+
local p
|
|
85
|
+
for p in "${_POWERSHELL_SOURCE_PATTERNS[@]}"; do
|
|
86
|
+
if [ "${#name_test[@]}" -gt 0 ]; then name_test+=('-o'); fi
|
|
87
|
+
name_test+=('-name' "$p")
|
|
88
|
+
done
|
|
89
|
+
else
|
|
90
|
+
name_test=('-name' "$name_pattern")
|
|
91
|
+
fi
|
|
64
92
|
|
|
65
93
|
# #573: repo-local extra prune entries. The list below covers the artifact
|
|
66
94
|
# directories xp-gate itself produces, but a consuming repo can have its own
|
|
@@ -149,7 +177,7 @@ _find_powershell_files() {
|
|
|
149
177
|
-path './src/npm-package/plugins' \
|
|
150
178
|
"${prune_args[@]+"${prune_args[@]}"}" \
|
|
151
179
|
\) -prune \) -o \
|
|
152
|
-
\( -type f
|
|
180
|
+
\( -type f \( "${name_test[@]}" \) -print \) 2>/dev/null
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
_powershell_invocation() {
|
|
@@ -290,16 +318,27 @@ run_static_analysis() {
|
|
|
290
318
|
# .git, node_modules, dist, coverage and the rest. The two paths disagreed
|
|
291
319
|
# only because this one never called it. Now both use it, so "what this
|
|
292
320
|
# adapter refuses to analyse" has one definition.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
321
|
+
# #620: no pattern means "every PowerShell source file", so .psm1 modules
|
|
322
|
+
# are analysed too. This used to pass '*.ps1', which made a module-only repo
|
|
323
|
+
# look empty and reported the emptiness as a pruning problem.
|
|
324
|
+
#
|
|
325
|
+
# Read into an ARRAY, one path per line. The previous shape captured the
|
|
326
|
+
# list into a string and let word splitting expand it again, which silently
|
|
327
|
+
# split any path containing a space. Reading line by line keeps the paths
|
|
328
|
+
# intact; `mapfile` would do the same but is not guaranteed by the bash
|
|
329
|
+
# versions this hook supports.
|
|
330
|
+
local -a legacy_files=()
|
|
331
|
+
while IFS= read -r _ps_file; do
|
|
332
|
+
[ -n "$_ps_file" ] && legacy_files+=("$_ps_file")
|
|
333
|
+
done < <(_find_powershell_files)
|
|
334
|
+
if [ "${#legacy_files[@]}" -eq 0 ]; then
|
|
296
335
|
# Nothing to analyse is NOT a pass: say so via the reason protocol the
|
|
297
336
|
# module-missing branch uses, so it stays visible in the gate output.
|
|
298
337
|
echo "XP_G1_REASON: no PowerShell files found outside the pruned directories"
|
|
299
338
|
return 3
|
|
300
339
|
fi
|
|
301
340
|
local legacy_array
|
|
302
|
-
legacy_array=$(_powershell_path_array $legacy_files)
|
|
341
|
+
legacy_array=$(_powershell_path_array "${legacy_files[@]}")
|
|
303
342
|
PSSA_OUT=$("$PWSH" "${POWERSHELL_ARGS[@]}" "
|
|
304
343
|
if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) {
|
|
305
344
|
Write-Output 'PSSA_MODULE_MISSING'
|
package/bin/xp-gate.js
CHANGED
|
@@ -460,7 +460,11 @@ for (let i = 0; i <= 11; i++) {
|
|
|
460
460
|
const gateInfo = gateRunner.GATE_REGISTRY[gateId];
|
|
461
461
|
COMMANDS[`gate-${gateId}`] = {
|
|
462
462
|
description: `Run Gate ${gateId}: ${gateInfo?.name || 'Unknown'}`,
|
|
463
|
-
|
|
463
|
+
// #618: a gate that ran nothing yields GATE_NOT_RUN. process.exit() would
|
|
464
|
+
// coerce that string to 0 (a pass), so map it to 2 -- "did not run" must not
|
|
465
|
+
// exit the same way as "ran clean".
|
|
466
|
+
run: subargs => gateRunner.runGate(gateId, subargs[0])
|
|
467
|
+
.then(code => process.exit(code === gateRunner.GATE_NOT_RUN ? 2 : code)),
|
|
464
468
|
usage: `xp-gate gate-${gateId} [<path>]`
|
|
465
469
|
};
|
|
466
470
|
}
|
|
@@ -471,7 +475,9 @@ for (const gateId of ['python-health', 'test-layers', 'pbt']) {
|
|
|
471
475
|
if (gateInfo) {
|
|
472
476
|
COMMANDS[gateId] = {
|
|
473
477
|
description: gateInfo.description,
|
|
474
|
-
|
|
478
|
+
// #618: same coercion guard as the numbered gates above.
|
|
479
|
+
run: subargs => gateRunner.runGate(gateId, subargs[0])
|
|
480
|
+
.then(code => process.exit(code === gateRunner.GATE_NOT_RUN ? 2 : code)),
|
|
475
481
|
usage: `xp-gate ${gateId} [<path>]`
|
|
476
482
|
};
|
|
477
483
|
}
|
|
@@ -1,12 +1,99 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
+
import fss from 'node:fs';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import os from 'os';
|
|
5
|
-
import { runTscCheck, runPackCheck } from '../gate-10';
|
|
6
|
+
import { runTscCheck, runPackCheck, resolveImportPath } from '../gate-10';
|
|
6
7
|
|
|
7
8
|
// Increase test timeout for subprocess calls (tsc, npm pack)
|
|
8
9
|
vi.setConfig({ testTimeout: 30000 });
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @test REQ-627
|
|
13
|
+
* @intent `resolveImportPath` had ZERO direct tests before this file gained them.
|
|
14
|
+
* It decides whether an import in changed code points at a real file, so
|
|
15
|
+
* a silent regression here makes Gate 10 pass while imports are broken.
|
|
16
|
+
* The `.js -> .ts` fallback is the subtle part: TypeScript's ESM output
|
|
17
|
+
* rewrites `./x.ts` to `./x.js`, so a source tree has `x.js` specifiers
|
|
18
|
+
* that must resolve to `x.ts`. Removing that arm left every existing test
|
|
19
|
+
* green, which is exactly why these cases are needed.
|
|
20
|
+
* @covers AC-627-01, AC-627-02, AC-627-03
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
describe('resolveImportPath (build-integrity)', () => {
|
|
24
|
+
let dir: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gate10-resolve-'));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const from = (): string => path.join(dir, 'importer.ts');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a fixture file. The write is wrapped so a failure is reported with
|
|
38
|
+
* the file name instead of surfacing as a bare fs error somewhere later — a
|
|
39
|
+
* test whose fixture silently failed to materialise asserts nothing useful.
|
|
40
|
+
*/
|
|
41
|
+
function writeSource(name: string, content: string): void {
|
|
42
|
+
try {
|
|
43
|
+
fss.writeFileSync(path.join(dir, name), content);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(`could not create fixture ${name}: ${String(error)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Create a fixture subdirectory, for the index-file fallback case. */
|
|
50
|
+
function makeDir(name: string): void {
|
|
51
|
+
try {
|
|
52
|
+
fss.mkdirSync(path.join(dir, name));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`could not create fixture dir ${name}: ${String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
it('AC-627-01: resolves an explicit .js specifier to the sibling .ts source', async () => {
|
|
59
|
+
writeSource('target.ts', 'export const x = 1;\n');
|
|
60
|
+
// The specifier says .js; only target.ts exists. ESM output depends on this.
|
|
61
|
+
expect(resolveImportPath('./target.js', from())).toBe(path.join(dir, 'target.ts'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('AC-627-01: prefers an existing .js file over the .ts fallback', async () => {
|
|
65
|
+
writeSource('both.js', 'module.exports = {};\n');
|
|
66
|
+
writeSource('both.ts', 'export const x = 1;\n');
|
|
67
|
+
expect(resolveImportPath('./both.js', from())).toBe(path.join(dir, 'both.js'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('AC-627-02: an explicit non-.js extension must match exactly (no fallback)', async () => {
|
|
71
|
+
writeSource('mod.mjs', 'export {};\n');
|
|
72
|
+
// .mjs exists, resolves directly
|
|
73
|
+
expect(resolveImportPath('./mod.mjs', from())).toBe(path.join(dir, 'mod.mjs'));
|
|
74
|
+
// .cjs does NOT exist, and there is no .cjs -> .ts fallback
|
|
75
|
+
expect(resolveImportPath('./missing.cjs', from())).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('AC-627-03: an extensionless import tries known extensions, then index files', async () => {
|
|
79
|
+
writeSource('plain.ts', 'export const y = 2;\n');
|
|
80
|
+
expect(resolveImportPath('./plain', from())).toBe(path.join(dir, 'plain.ts'));
|
|
81
|
+
|
|
82
|
+
makeDir('pkg');
|
|
83
|
+
writeSource('pkg/index.ts', 'export {};\n');
|
|
84
|
+
expect(resolveImportPath('./pkg', from())).toBe(path.join(dir, 'pkg', 'index.ts'));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('AC-627-03: a non-relative specifier is not resolved at all', () => {
|
|
88
|
+
expect(resolveImportPath('lodash', from())).toBeNull();
|
|
89
|
+
expect(resolveImportPath('@scope/pkg', from())).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('AC-627-03: a relative import to nothing returns null', async () => {
|
|
93
|
+
expect(resolveImportPath('./nope', from())).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
10
97
|
/**
|
|
11
98
|
* @test Gate 10 runTscCheck
|
|
12
99
|
* @intent Verify tsc --noEmit check behavior for build integrity gate
|
|
@@ -281,34 +281,69 @@ function tryAccess(filePath: string): boolean {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Extensions tried when an import has none, in precedence order.
|
|
286
|
+
*
|
|
287
|
+
* `.js` first because a TS source that imports `./x` is compiled to `x.js`, so
|
|
288
|
+
* the emitted name is the most likely intent in a mixed tree.
|
|
289
|
+
*/
|
|
290
|
+
const IMPORT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
286
291
|
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
/** Directory-index filenames tried last, when no sibling file matched. */
|
|
293
|
+
const INDEX_FILES = ['index.ts', 'index.js'];
|
|
289
294
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Source extensions an explicit `.js` import may actually refer to.
|
|
297
|
+
*
|
|
298
|
+
* TypeScript's ESM output rewrites `./x.ts` to `./x.js`, so an import that names
|
|
299
|
+
* a `.js` file legitimately resolves to the TypeScript source beside it. Checked
|
|
300
|
+
* in order, and only for `.js` -- an explicit `.ts` or `.mjs` must match exactly.
|
|
301
|
+
*/
|
|
302
|
+
const JS_TO_SOURCE_EXTENSIONS = ['.ts', '.tsx'];
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolve an import that already names an extension.
|
|
306
|
+
*
|
|
307
|
+
* A `.js` specifier may point at TypeScript source (ESM output rewrites `.ts` to
|
|
308
|
+
* `.js`), so it gets one extra look at the sibling source extensions. Every other
|
|
309
|
+
* extension must match the file on disk exactly.
|
|
310
|
+
*/
|
|
311
|
+
function resolveExplicitExtension(resolved: string, ext: string): string | null {
|
|
312
|
+
if (tryAccess(resolved)) return resolved;
|
|
313
|
+
if (ext !== '.js') return null;
|
|
314
|
+
const stem = resolved.slice(0, -'.js'.length);
|
|
315
|
+
for (const sourceExt of JS_TO_SOURCE_EXTENSIONS) {
|
|
316
|
+
if (tryAccess(stem + sourceExt)) return stem + sourceExt;
|
|
298
317
|
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
299
320
|
|
|
300
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Resolve an extensionless import: try the known extensions in order, then the
|
|
323
|
+
* directory-index names.
|
|
324
|
+
*/
|
|
325
|
+
function resolveBareImport(resolved: string): string | null {
|
|
326
|
+
for (const tryExt of IMPORT_EXTENSIONS) {
|
|
301
327
|
if (tryAccess(resolved + tryExt)) return resolved + tryExt;
|
|
302
328
|
}
|
|
303
|
-
|
|
304
|
-
for (const idxName of ['index.ts', 'index.js']) {
|
|
329
|
+
for (const idxName of INDEX_FILES) {
|
|
305
330
|
const candidate = path.join(resolved, idxName);
|
|
306
331
|
if (tryAccess(candidate)) return candidate;
|
|
307
332
|
}
|
|
308
|
-
|
|
309
333
|
return null;
|
|
310
334
|
}
|
|
311
335
|
|
|
336
|
+
export function resolveImportPath(importPath: string, fromFile: string): string | null {
|
|
337
|
+
if (!isRelativeImport(importPath)) return null;
|
|
338
|
+
|
|
339
|
+
const resolved = path.resolve(path.dirname(fromFile), importPath);
|
|
340
|
+
const ext = path.extname(importPath);
|
|
341
|
+
|
|
342
|
+
return ext
|
|
343
|
+
? resolveExplicitExtension(resolved, ext)
|
|
344
|
+
: resolveBareImport(resolved);
|
|
345
|
+
}
|
|
346
|
+
|
|
312
347
|
/**
|
|
313
348
|
* Runs the import check on a list of changed files.
|
|
314
349
|
* Verifies that all relative imports in the changed files resolve to files
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# XP-Gate DSH bundle patch (npm main package).
|
|
2
|
+
#
|
|
3
|
+
# Mounts the xp-gate tool plugin so that
|
|
4
|
+
# dsh plugin add @boyingliu01/xp-gate
|
|
5
|
+
# is sufficient on its own — no separate install of
|
|
6
|
+
# @boyingliu01/dsh-plugin-xp-gate is required.
|
|
7
|
+
#
|
|
8
|
+
# The entry point is the package-internal subpath "./dsh", which resolves via
|
|
9
|
+
# this package.json's `exports` map to plugins/dsh/lib/index.js (ESM). Shipping
|
|
10
|
+
# the code inside the same tarball keeps `dependencies` empty, preserving the
|
|
11
|
+
# zero-install distribution contract.
|
|
12
|
+
|
|
13
|
+
- insert:
|
|
14
|
+
- id: tool-xp-gate
|
|
15
|
+
name: '@boyingliu01/xp-gate/dsh'
|