@boyingliu01/xp-gate 0.31.4 → 0.32.1
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 +65 -16
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +65 -16
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +101 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +259 -57
- 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-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/java.ts +22 -3
- package/principles/adapters/typescript.ts +70 -29
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- 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/hooks/pre-commit
CHANGED
|
@@ -710,48 +710,37 @@ if [ "$is_protected" = "true" ]; then
|
|
|
710
710
|
esac
|
|
711
711
|
done
|
|
712
712
|
|
|
713
|
-
# Doc-only changes
|
|
713
|
+
# Doc-only / build-tooling-only changes need no VERSION bump.
|
|
714
|
+
#
|
|
715
|
+
# #624 defect 2: the split used to test ONLY `has_source`, which is true for
|
|
716
|
+
# any .py/.ts/... path INCLUDING `scripts/*`. So a commit that touched
|
|
717
|
+
# nothing but build tooling still fell into the version-check branch and was
|
|
718
|
+
# blocked. `has_real_source` is exactly "production source that needs a bump"
|
|
719
|
+
# — it was computed a few lines up but never consulted here. It must decide.
|
|
714
720
|
if [ "$has_source" = "false" ]; then
|
|
715
721
|
echo "✅ PASSED - Gate 0: Version Consistency Check (doc-only changes)"
|
|
722
|
+
elif [ "$has_real_source" = "false" ]; then
|
|
723
|
+
echo "✅ PASSED - Gate 0: Version Consistency Check (build-tooling-only changes)"
|
|
716
724
|
else
|
|
717
|
-
#
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
Production source files require VERSION/CHANGELOG update.
|
|
732
|
-
|
|
733
|
-
Files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
|
|
734
|
-
"
|
|
735
|
-
exit 1
|
|
736
|
-
fi
|
|
737
|
-
else
|
|
738
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
739
|
-
VERSION CONSISTENCY CHECK (Gate 0)
|
|
740
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
741
|
-
Branch: $CURRENT_BRANCH
|
|
742
|
-
ERROR: [skip-version-check] bypass prefix invalid.
|
|
743
|
-
|
|
744
|
-
Allowed prefixes: chore:, docs:, release:
|
|
745
|
-
Example: [skip-version-check] chore: update dependencies
|
|
746
|
-
"
|
|
747
|
-
exit 1
|
|
748
|
-
fi
|
|
749
|
-
else
|
|
750
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
725
|
+
# Production source on a protected branch without VERSION/CHANGELOG.
|
|
726
|
+
#
|
|
727
|
+
# #624 defect 1: this branch used to offer a `[skip-version-check]` message
|
|
728
|
+
# prefix as a bypass, read from `.git/COMMIT_EDITMSG`. That cannot work:
|
|
729
|
+
# git runs pre-commit BEFORE it builds the message, so the file does not
|
|
730
|
+
# exist yet (`-m`, `-F` and the interactive editor path all measured on
|
|
731
|
+
# git 2.54), and the hook gets no arguments. What the old code read was the
|
|
732
|
+
# PREVIOUS commit's message, so the prefix never matched — the documented
|
|
733
|
+
# recipe silently could not clear the gate, and --no-verify was the only
|
|
734
|
+
# way through. Rather than keep a bypass whose input this hook structurally
|
|
735
|
+
# cannot see, the escape hatch is the one that actually works and is
|
|
736
|
+
# already documented in CHANGELOG: the SKIP_VERSION_CHECK=1 environment
|
|
737
|
+
# variable (handled earlier in this Gate 0 block).
|
|
738
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
751
739
|
VERSION CONSISTENCY CHECK (Gate 0)
|
|
752
740
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
753
741
|
Branch: $CURRENT_BRANCH
|
|
754
|
-
ERROR: Committing to a protected branch without
|
|
742
|
+
ERROR: Committing production source to a protected branch without a
|
|
743
|
+
VERSION/CHANGELOG update.
|
|
755
744
|
|
|
756
745
|
This bypasses the ship workflow and causes version drift.
|
|
757
746
|
Before committing:
|
|
@@ -759,18 +748,19 @@ Before committing:
|
|
|
759
748
|
2. Add entry to CHANGELOG.md
|
|
760
749
|
3. Run: node scripts/sync-version.cjs
|
|
761
750
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
751
|
+
Build-tooling-only changes (adapters/, scripts/, hooks/) need NO version bump
|
|
752
|
+
and pass this gate automatically — no marker required.
|
|
753
|
+
|
|
754
|
+
If you genuinely must land production source without a version bump, the
|
|
755
|
+
supported escape hatch is the env var (a message prefix cannot work here
|
|
756
|
+
because pre-commit runs before git builds the message):
|
|
757
|
+
SKIP_VERSION_CHECK=1 git commit ...
|
|
767
758
|
|
|
768
759
|
Staged files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
|
|
769
760
|
"
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
fi
|
|
761
|
+
GATE_0_STATUS="BLOCK"
|
|
762
|
+
record_gate_audit "gate-0" "version-consistency" "$GATE_0_STATUS" "1" "$GATE_0_START"
|
|
763
|
+
exit 1
|
|
774
764
|
fi
|
|
775
765
|
fi
|
|
776
766
|
fi # closes SKIP_VERSION_CHECK if/else
|
|
@@ -809,7 +799,44 @@ echo ""
|
|
|
809
799
|
MIRROR_PATH_FILTER='^src/npm-package/'
|
|
810
800
|
DASHBOARD_PATH_FILTER='^dashboard/'
|
|
811
801
|
RAW_CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
812
|
-
|
|
802
|
+
|
|
803
|
+
# Dependencies and build output are not project code (#649).
|
|
804
|
+
#
|
|
805
|
+
# `git diff --cached` knows nothing about .gitignore: anything that reaches the
|
|
806
|
+
# index is listed. A project that vendors its dependencies, a CI fixture that
|
|
807
|
+
# commits after `npm install`, an accidental `git add -A`, or a monorepo's
|
|
808
|
+
# nested `packages/*/node_modules` therefore hands every downstream scanner a
|
|
809
|
+
# list containing third-party source, and each scanner judges it as the
|
|
810
|
+
# project's own.
|
|
811
|
+
#
|
|
812
|
+
# This has already cost two outages, one language branch at a time: #595 (Gate 1
|
|
813
|
+
# PowerShell arm blocked every commit once Playwright's node_modules was
|
|
814
|
+
# scanned for Write-Host) and #648 (Gate 1 JS/TS arm blocked on the bare
|
|
815
|
+
# `debugger;` in node_modules/typescript/dist/internal/utils.js). Both were
|
|
816
|
+
# fixed at the single site that happened to fail, so the next branch re-broke
|
|
817
|
+
# the same way. The filter belongs HERE, at the one place the list is built, so
|
|
818
|
+
# a scanner added tomorrow is safe by default instead of by memory.
|
|
819
|
+
#
|
|
820
|
+
# The exclusion itself is the shared build-output list from jscpd-run.sh
|
|
821
|
+
# (jscpd_is_build_output), so the directory names have one definition across the
|
|
822
|
+
# gates. Deliberately NOT the full Gate 2 ignore set: that one also drops
|
|
823
|
+
# *.yaml/*.yml/*.txt/*.log because repetition there is data shape rather than a
|
|
824
|
+
# refactoring target, and using it here would make a commit that only adds
|
|
825
|
+
# infra/deployment.yaml look like "No code files changed" -- skipping the IaC
|
|
826
|
+
# and lint gates that exist to scan exactly that file. The XP_GATE_JSCPD_DEFAULT_IGNORE=0
|
|
827
|
+
# escape hatch still applies.
|
|
828
|
+
#
|
|
829
|
+
# RAW_CHANGED_FILES stays unfiltered on purpose: it is the only truthful answer
|
|
830
|
+
# to "did the user stage anything at all", and a future "you accidentally
|
|
831
|
+
# committed dependencies" warning needs to see what this filter removed.
|
|
832
|
+
CHANGED_FILES=$(printf '%s\n' "$RAW_CHANGED_FILES" \
|
|
833
|
+
| grep -v "$MIRROR_PATH_FILTER" | grep -v "$DASHBOARD_PATH_FILTER" \
|
|
834
|
+
| {
|
|
835
|
+
while IFS= read -r _cf; do
|
|
836
|
+
[ -n "$_cf" ] || continue
|
|
837
|
+
jscpd_is_build_output "$_cf" || printf '%s\n' "$_cf"
|
|
838
|
+
done
|
|
839
|
+
} || true)
|
|
813
840
|
|
|
814
841
|
# REQ-F2: one commit-level docs-only verdict, computed once and reused by the
|
|
815
842
|
# heavy gates (2/5/9/12). Light gates never consult it — a secret in a README
|
|
@@ -844,7 +871,22 @@ any_changed_files_match() {
|
|
|
844
871
|
local pattern
|
|
845
872
|
|
|
846
873
|
for pattern in "$@"; do
|
|
847
|
-
|
|
874
|
+
# Here-string, not `printf | grep -q`. Under the `set -o pipefail` above,
|
|
875
|
+
# `grep -q` exits the moment it matches, the writer takes SIGPIPE, and the
|
|
876
|
+
# pipeline reports 141 -- so the `if` reads "no match" even though grep DID
|
|
877
|
+
# match. Whether that happens depends on whether the list fits in the pipe
|
|
878
|
+
# buffer, which is why it showed up as a size-dependent, flaky SKIP.
|
|
879
|
+
#
|
|
880
|
+
# It was not theoretical: in the Cross-Platform CI first-commit fixture the
|
|
881
|
+
# staged list is long enough to trip it, and Gate 1 announced "No code files
|
|
882
|
+
# changed, skipping lint" for a commit that had staged 19 shell scripts --
|
|
883
|
+
# while Gate 2, reading the same variable, happily scanned them and reported
|
|
884
|
+
# duplicates. The hook log carried the fingerprint the whole time:
|
|
885
|
+
# `line 847: printf: Broken pipe`.
|
|
886
|
+
#
|
|
887
|
+
# A here-string has no writer process to be killed, so the match cannot be
|
|
888
|
+
# lost. Same reason `run_tsx` and the coverage probes avoid `| head`.
|
|
889
|
+
if grep -qE -- "$pattern" <<< "$CHANGED_FILES"; then
|
|
848
890
|
return 0
|
|
849
891
|
fi
|
|
850
892
|
done
|
|
@@ -1506,9 +1548,32 @@ else
|
|
|
1506
1548
|
fi
|
|
1507
1549
|
|
|
1508
1550
|
# Debug statement detection (debugger statement in JS/TS)
|
|
1509
|
-
|
|
1551
|
+
#
|
|
1552
|
+
# Dependencies and build output are filtered out first (#648). The list
|
|
1553
|
+
# comes from `git diff --cached`, so a project (or a CI fixture) that
|
|
1554
|
+
# vendors or commits its dependencies gets the dependency's OWN source
|
|
1555
|
+
# scanned as if it were project code. TypeScript 7.x ships a bare
|
|
1556
|
+
# `debugger;` in dist/internal/utils.js, which blocked commits that changed
|
|
1557
|
+
# nothing of their own:
|
|
1558
|
+
# ❌ BLOCKED - 'debugger' statements detected:
|
|
1559
|
+
# node_modules/typescript/dist/internal/utils.js:18: debugger;
|
|
1560
|
+
# Pinning the dependency's version only postpones that: any package may
|
|
1561
|
+
# carry a debugger statement, and which one lands changes with every
|
|
1562
|
+
# install. The rule the gate actually wants is "no debugger in OUR code",
|
|
1563
|
+
# so the ignore set Gate 2 already uses decides what counts as ours — one
|
|
1564
|
+
# notion of build output across the gates, overridable through the same
|
|
1565
|
+
# XP_GATE_JSCPD_DEFAULT_IGNORE switch.
|
|
1566
|
+
TS_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' | {
|
|
1567
|
+
while IFS= read -r _df; do
|
|
1568
|
+
[ -n "$_df" ] || continue
|
|
1569
|
+
if ! jscpd_is_build_output "$_df"; then
|
|
1570
|
+
printf '%s\n' "$_df"
|
|
1571
|
+
fi
|
|
1572
|
+
done
|
|
1573
|
+
} || true)
|
|
1510
1574
|
if [ -n "$TS_DEBUG_FILES" ]; then
|
|
1511
|
-
|
|
1575
|
+
# NUL-separated so paths with spaces survive; a bare $VAR would word-split.
|
|
1576
|
+
DEBUGGER_HITS=$(printf '%s\n' "$TS_DEBUG_FILES" | tr '\n' '\0' | xargs -0 grep -n '^\s*debugger\s*;\?\s*$' 2>/dev/null || true)
|
|
1512
1577
|
if [ -n "$DEBUGGER_HITS" ]; then
|
|
1513
1578
|
echo ""
|
|
1514
1579
|
echo "❌ BLOCKED - 'debugger' statements detected:"
|
|
@@ -2189,7 +2254,10 @@ else
|
|
|
2189
2254
|
fi
|
|
2190
2255
|
|
|
2191
2256
|
# --- BLOCK path: New .ts/.tsx files without corresponding tests ---
|
|
2192
|
-
|
|
2257
|
+
# Exclusions come from filter_project_paths (#649). The hand-maintained list
|
|
2258
|
+
# this replaced omitted vendor/target/coverage/out, so a staged vendor/x.ts
|
|
2259
|
+
# was reported as new source with no test.
|
|
2260
|
+
NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)
|
|
2193
2261
|
|
|
2194
2262
|
# Strip PROJECT_SUBDIR prefix from new-TS-file paths ONLY when the hook
|
|
2195
2263
|
# actually cd'd into the subdirectory (IN_PROJECT_SUBDIR=1; single-language
|
|
@@ -2274,10 +2342,12 @@ else
|
|
|
2274
2342
|
fi
|
|
2275
2343
|
|
|
2276
2344
|
# --- WARNING path: Modified files (any lang) + New non-TS files ---
|
|
2345
|
+
# filter_project_paths strips dependencies and build output (#649); without
|
|
2346
|
+
# it a staged vendor/ or target/ module is reported as source needing a test.
|
|
2277
2347
|
WARNING_SOURCE_FILES=$(
|
|
2278
2348
|
{
|
|
2279
|
-
git diff --cached --name-only --diff-filter=M | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
|
|
2280
|
-
git diff --cached --name-only --diff-filter=A | grep -E '\.(py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
|
|
2349
|
+
git diff --cached --name-only --diff-filter=M | filter_project_paths | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
|
|
2350
|
+
git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -E '\.(py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
|
|
2281
2351
|
} | sort -u || true
|
|
2282
2352
|
)
|
|
2283
2353
|
|
|
@@ -2400,7 +2470,7 @@ else
|
|
|
2400
2470
|
# Gate 5b: Mock Density ADVISORY Scan (pure bash — no npx tsx overhead)
|
|
2401
2471
|
# 30% = ADVISORY, 50% = suggests @mock-justified. Does NOT block commit.
|
|
2402
2472
|
# ========================================================================
|
|
2403
|
-
CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs|py|go)$' || true)
|
|
2473
|
+
CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | filter_project_paths | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs|py|go)$' || true)
|
|
2404
2474
|
|
|
2405
2475
|
# Nested plugin sub-projects (plugins/<name>/) own their own vitest config and
|
|
2406
2476
|
# are excluded from the root suite (`vitest.config.ts` excludes `plugins/**`,
|
|
@@ -2548,7 +2618,7 @@ else
|
|
|
2548
2618
|
CHANGED_TEST_FILE_COUNT=$(echo "$SELECTED_TEST_FILES" | grep -c '.' || true)
|
|
2549
2619
|
|
|
2550
2620
|
# Also get changed source files (non-test) for related-test lookup
|
|
2551
|
-
CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|js|jsx)$' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__tests__' | grep -v '\.d\.ts$' | grep -v '__snapshots__' || true)
|
|
2621
|
+
CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM | filter_project_paths | grep -E '\.(ts|tsx|js|jsx)$' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__tests__' | grep -v '\.d\.ts$' | grep -v '__snapshots__' || true)
|
|
2552
2622
|
# Nested plugin sub-projects are excluded from the root suite; their
|
|
2553
2623
|
# sources must not drive root-vitest related-test lookup either (it
|
|
2554
2624
|
# would resolve to plugin-local paths vitest cannot collect).
|
|
@@ -2784,9 +2854,14 @@ else
|
|
|
2784
2854
|
# full -> unchanged behavior: whole suite + real threshold.
|
|
2785
2855
|
# skip -> nothing test-relevant changed; no pytest at all.
|
|
2786
2856
|
PY_CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null \
|
|
2857
|
+
| filter_project_paths \
|
|
2787
2858
|
| grep -E '(^|/)test_[^/]*\.py$|_test\.py$' || true)
|
|
2788
2859
|
PY_CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null \
|
|
2860
|
+
| filter_project_paths \
|
|
2789
2861
|
| grep -E '\.py$' | grep -vE '(^|/)test_[^/]*\.py$|_test\.py$' || true)
|
|
2862
|
+
# NOT filtered on purpose: this list answers "did a requirements
|
|
2863
|
+
# file change", which is a property of the whole commit including
|
|
2864
|
+
# paths under a vendor/ tree that filter_project_paths removes.
|
|
2790
2865
|
PY_CHANGED_ALL=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)
|
|
2791
2866
|
# `grep -c '.'` prints its own zero AND exits 1 on an empty list
|
|
2792
2867
|
# (#493), so the count is derived with `|| true` + wc -l instead.
|
|
@@ -2885,9 +2960,33 @@ else
|
|
|
2885
2960
|
echo "ℹ️ Shell coverage not typically measured"
|
|
2886
2961
|
;;
|
|
2887
2962
|
*)
|
|
2963
|
+
# #640: hand the staged set to the adapter. Java's run_coverage uses it
|
|
2964
|
+
# for three-tier smart selection (changed-tests -> related -> full);
|
|
2965
|
+
# without it the adapter deliberately runs the full suite rather than
|
|
2966
|
+
# guessing, so this export failing closed is safe.
|
|
2967
|
+
# Colon-separated: newlines do not survive every adapter's word
|
|
2968
|
+
# splitting, and paths with spaces stay intact inside the value.
|
|
2969
|
+
GATE_CHANGED_FILES=$(printf '%s' "$CHANGED_FILES" | tr '\n' ':')
|
|
2970
|
+
export GATE_CHANGED_FILES
|
|
2971
|
+
# The adapter runs inside $(...), so it cannot set PARTIAL_TEST_RUN
|
|
2972
|
+
# for us. It writes the chosen strategy to this file and we raise the
|
|
2973
|
+
# flag here, in the shell that later reads it. Without this a Java
|
|
2974
|
+
# subset run under 80% coverage would take the hard-BLOCK branch
|
|
2975
|
+
# instead of the WARN branch, exactly the bug #556 fixed for Python.
|
|
2976
|
+
_java_partial_file=$(mktemp 2>/dev/null || echo "")
|
|
2977
|
+
if [ -n "$_java_partial_file" ]; then
|
|
2978
|
+
export XP_GATE_PARTIAL_RUN_FILE="$_java_partial_file"
|
|
2979
|
+
fi
|
|
2888
2980
|
default_cov_output=$(run_coverage 2>&1)
|
|
2889
2981
|
COV_EXIT=$((COV_EXIT || $?))
|
|
2890
2982
|
echo "$default_cov_output" | head -30
|
|
2983
|
+
unset XP_GATE_PARTIAL_RUN_FILE GATE_CHANGED_FILES
|
|
2984
|
+
if [ -n "$_java_partial_file" ]; then
|
|
2985
|
+
if [ -s "$_java_partial_file" ]; then
|
|
2986
|
+
PARTIAL_TEST_RUN=true
|
|
2987
|
+
fi
|
|
2988
|
+
rm -f "$_java_partial_file" 2>/dev/null || true
|
|
2989
|
+
fi
|
|
2891
2990
|
;;
|
|
2892
2991
|
esac
|
|
2893
2992
|
else
|
|
@@ -3316,7 +3415,7 @@ else
|
|
|
3316
3415
|
# New files must have ≥80% coverage individually.
|
|
3317
3416
|
# ============================================================================
|
|
3318
3417
|
if has_project_lang "typescript" && [ -f "coverage/coverage-summary.json" ]; then
|
|
3319
|
-
NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v '\.d\.ts$' || true)
|
|
3418
|
+
NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -E '\.(ts|tsx)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v '\.d\.ts$' || true)
|
|
3320
3419
|
if [ -n "$NEW_SOURCE_FILES" ]; then
|
|
3321
3420
|
echo ""
|
|
3322
3421
|
echo " └─ Per-file coverage check for new files:"
|
|
@@ -3717,8 +3816,11 @@ else
|
|
|
3717
3816
|
# governed by Mirror Parity in CI, which compares content rather than
|
|
3718
3817
|
# re-linting it. (See the same exclusion in adapters/powershell.sh.)
|
|
3719
3818
|
MIRROR_EXCLUDE="$MIRROR_PATH_FILTER"
|
|
3720
|
-
|
|
3721
|
-
|
|
3819
|
+
# filter_project_paths also removes dependencies and build output (#649):
|
|
3820
|
+
# a staged node_modules/ or dist/ file is not budgetable project code, and
|
|
3821
|
+
# entering it in the Boy Scout baseline would budget a path nobody edits.
|
|
3822
|
+
NEW_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
|
|
3823
|
+
MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | filter_project_paths | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
|
|
3722
3824
|
|
|
3723
3825
|
if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
|
|
3724
3826
|
echo " ✅ No new or modified files to check."
|
|
@@ -4129,6 +4231,90 @@ else
|
|
|
4129
4231
|
fi
|
|
4130
4232
|
# ============================================================================
|
|
4131
4233
|
|
|
4234
|
+
# ============================================================================
|
|
4235
|
+
# GATE 13: Compiler Strictness Check
|
|
4236
|
+
# The compiler is the first and cheapest static analyser for a compiled language
|
|
4237
|
+
# and XP-Gate never used it as one: cpp.sh only runs clang-tidy/cppcheck over the
|
|
4238
|
+
# first 20 files, and Gate 9's build-integrity half is TypeScript-and-Maven only.
|
|
4239
|
+
# A measured `g++ -Wall -Wextra` pass reports -Wnon-virtual-dtor, -Wreorder,
|
|
4240
|
+
# -Wparentheses and -Wclass-memaccess on an innocuous-looking file; bare `javac`
|
|
4241
|
+
# reports NOTHING for a raw type where `-Xlint:all` reports [rawtypes].
|
|
4242
|
+
#
|
|
4243
|
+
# Verdicts: 0 PASS, 1 FAIL (new findings -> blocked), 2 BLOCK (ran but output
|
|
4244
|
+
# untrustworthy), 3 WARN (tool absent / cross-compiling / timeout / warn-only).
|
|
4245
|
+
# A missing compiler is a SKIP-class WARN per the project convention, NOT a block.
|
|
4246
|
+
#
|
|
4247
|
+
# Escape hatches: XP_GATE_DISABLE_13=1 (kill switch),
|
|
4248
|
+
# XP_GATE_COMPILER_STRICTNESS=strict|balanced|warn-only,
|
|
4249
|
+
# XP_GATE_COMPILER_ANALYZER=1 (gcc -fanalyzer, C only, opt-in).
|
|
4250
|
+
# Design: docs/plans/2026-10-26-gate-13-compiler-strictness-design.md
|
|
4251
|
+
#
|
|
4252
|
+
# NOTE: this block is a TAIL APPEND after Gate 12 and intentionally touches no
|
|
4253
|
+
# line of the gates above it.
|
|
4254
|
+
# ============================================================================
|
|
4255
|
+
GATE_13_STATUS="SKIP"
|
|
4256
|
+
if [ "${XP_GATE_DISABLE_13:-}" = "1" ]; then
|
|
4257
|
+
echo "⏭️ SKIPPED - Gate 13: Compiler Strictness (XP_GATE_DISABLE_13=1)"
|
|
4258
|
+
GATE_13_STATUS="SKIP"
|
|
4259
|
+
elif [ -f "$GATE_DIR/gate-13-compiler-strictness.sh" ] || [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/githooks/gate-13-compiler-strictness.sh" ]; then
|
|
4260
|
+
echo "⚙️ Gate 13: Compiler Strictness Check..."
|
|
4261
|
+
GATE13_START=$(gate_start_ms)
|
|
4262
|
+
(
|
|
4263
|
+
# Each stage runs inside the subshell so its shell state cannot leak back
|
|
4264
|
+
# into pre-commit. Verdicts are reduced to the WORST code seen:
|
|
4265
|
+
# FAIL/BLOCK (1/2) always win; WARN (3) only beats PASS (0).
|
|
4266
|
+
_g13_script="$GATE_DIR/gate-13-compiler-strictness.sh"
|
|
4267
|
+
[ -f "$_g13_script" ] || _g13_script="$(git rev-parse --show-toplevel)/githooks/gate-13-compiler-strictness.sh"
|
|
4268
|
+
# shellcheck source=/dev/null
|
|
4269
|
+
. "$_g13_script"
|
|
4270
|
+
|
|
4271
|
+
_g13_worst=0
|
|
4272
|
+
_g13_rc=0
|
|
4273
|
+
_g13_note() {
|
|
4274
|
+
case "$1" in
|
|
4275
|
+
1|2) _g13_worst=$1 ;;
|
|
4276
|
+
3) [ "$_g13_worst" -eq 0 ] && _g13_worst=3 ;;
|
|
4277
|
+
esac
|
|
4278
|
+
return 0
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
gate13_run_cpp_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
4282
|
+
gate13_run_java_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
4283
|
+
gate13_run_analyzer; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
4284
|
+
|
|
4285
|
+
exit "$_g13_worst"
|
|
4286
|
+
)
|
|
4287
|
+
GATE_13_RC=$?
|
|
4288
|
+
GATE13_END=$(now_ms)
|
|
4289
|
+
GATE_13_DURATION=$(( (GATE13_END - GATE13_START) / 1000 ))
|
|
4290
|
+
case "$GATE_13_RC" in
|
|
4291
|
+
0) GATE_13_STATUS="PASS" ;;
|
|
4292
|
+
1) GATE_13_STATUS="FAIL" ;;
|
|
4293
|
+
2) GATE_13_STATUS="BLOCK" ;;
|
|
4294
|
+
*) GATE_13_STATUS="WARN" ;;
|
|
4295
|
+
esac
|
|
4296
|
+
echo " Gate 13 ($GATE_13_STATUS) completed in ${GATE_13_DURATION}s"
|
|
4297
|
+
record_gate_audit "gate-13" "compiler-strictness" "$GATE_13_STATUS" "0" "$GATE13_START"
|
|
4298
|
+
# Every other gate blocks by exiting immediately (e.g. Gate 1's
|
|
4299
|
+
# "❌ BLOCKED ... exit 1"); there is no aggregate-then-exit shared with
|
|
4300
|
+
# gates 0-12, so Gate 13 must do the same or its FAIL would only be cosmetic
|
|
4301
|
+
# in the report while the commit sails through.
|
|
4302
|
+
if [ "$GATE_13_STATUS" = "FAIL" ] || [ "$GATE_13_STATUS" = "BLOCK" ]; then
|
|
4303
|
+
echo ""
|
|
4304
|
+
echo "❌ BLOCKED - Gate 13: Compiler Strictness (${GATE_13_STATUS})"
|
|
4305
|
+
echo " Fix the compiler findings above. To record pre-existing warnings:"
|
|
4306
|
+
echo " xp-gate compiler-baseline create"
|
|
4307
|
+
echo " To relax strictness: XP_GATE_COMPILER_STRICTNESS=warn-only"
|
|
4308
|
+
echo " Emergency only: XP_GATE_DISABLE_13=1"
|
|
4309
|
+
exit 1
|
|
4310
|
+
fi
|
|
4311
|
+
else
|
|
4312
|
+
echo "⏭️ SKIPPED - Gate 13: Compiler Strictness (gate-13-compiler-strictness.sh not found)"
|
|
4313
|
+
GATE_13_STATUS="SKIP"
|
|
4314
|
+
fi
|
|
4315
|
+
echo ""
|
|
4316
|
+
# ============================================================================
|
|
4317
|
+
|
|
4132
4318
|
generate_quality_report() {
|
|
4133
4319
|
local HISTORY_FILE=".quality-history.jsonl"
|
|
4134
4320
|
local COMMIT_HASH
|
|
@@ -4425,6 +4611,22 @@ generate_quality_report() {
|
|
|
4425
4611
|
}
|
|
4426
4612
|
ENDJSON
|
|
4427
4613
|
|
|
4614
|
+
# ── Gate 13 injection (tail append; no existing line above is modified) ────
|
|
4615
|
+
# The heredoc is closed deliberately before Gate 13 so this sprint ADDS lines
|
|
4616
|
+
# instead of editing gates 0-12's report block, keeping the two branches'
|
|
4617
|
+
# hunks disjoint. Scores are recomputed over 13 gates afterwards.
|
|
4618
|
+
if [ -n "${GATE_13_STATUS:-}" ] && [ -f "$REPORT_FILE" ]; then
|
|
4619
|
+
_XP_G13="$REPORT_FILE" _XP_G13S="$GATE_13_STATUS" node -e "
|
|
4620
|
+
const fs=require('fs'),p=process.env._XP_G13;
|
|
4621
|
+
try{
|
|
4622
|
+
const j=JSON.parse(fs.readFileSync(p,'utf8'));
|
|
4623
|
+
j.gates=j.gates||{};
|
|
4624
|
+
j.gates.gate13_compiler_strictness={name:'Compiler Strictness',status:process.env._XP_G13S,tool:'gate-13-compiler-strictness.sh'};
|
|
4625
|
+
fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');
|
|
4626
|
+
}catch(e){}
|
|
4627
|
+
" 2>/dev/null || true
|
|
4628
|
+
fi
|
|
4629
|
+
|
|
4428
4630
|
# ── Persistent per-run report (Issue #101) ────────────────────────────────────────
|
|
4429
4631
|
local REPORTS_DIR=".xp-gate/reports/pre-commit"
|
|
4430
4632
|
local REPORT_TS
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-635
|
|
3
|
+
* @intent `xp-gate init` 会向 ~/.dsh/skills 部署技能、npm 包也带 plugins/dsh,
|
|
4
|
+
* 但 PLATFORM_PROFILES 里没有 dsh 条目,getSkillsDirs('dsh') 静默回落到
|
|
5
|
+
* opencode 的路径,导致 DSH 机器的技能从不被解析(#635)
|
|
6
|
+
* @covers AC-635-04, AC-635-05
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
describe('PLATFORM_PROFILES covers dsh (#635)', () => {
|
|
13
|
+
let tmpHome;
|
|
14
|
+
let originalHome;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
originalHome = process.env.HOME;
|
|
18
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dsh-'));
|
|
19
|
+
process.env.HOME = tmpHome;
|
|
20
|
+
vi.resetModules();
|
|
21
|
+
delete require.cache[require.resolve('../detect-deps')];
|
|
22
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.env.HOME = originalHome;
|
|
27
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// AC-635-04: asking for dsh skills must return the dsh directory, not a
|
|
31
|
+
// silent fallback to opencode's.
|
|
32
|
+
it('getSkillsDirs("dsh") resolves ~/.dsh, not the opencode fallback', () => {
|
|
33
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
34
|
+
const dirs = getSkillsDirs('dsh');
|
|
35
|
+
|
|
36
|
+
expect(dirs.length).toBeGreaterThan(0);
|
|
37
|
+
expect(dirs[0]).toBe(path.join(tmpHome, '.dsh', 'skills'));
|
|
38
|
+
// The pre-fix behaviour: falls through to PLATFORM_PROFILES.opencode.
|
|
39
|
+
expect(dirs[0]).not.toContain(path.join('.config', 'opencode'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// AC-635-05: the profile is a real entry, so a caller enumerating platforms
|
|
43
|
+
// sees dsh alongside the three that were already there.
|
|
44
|
+
it('dsh is an enumerable platform profile with skillsDirs and requiredDeps', () => {
|
|
45
|
+
const { PLATFORM_PROFILES, getSkillsDirs } = require('../detect-deps');
|
|
46
|
+
|
|
47
|
+
expect(Object.keys(PLATFORM_PROFILES)).toContain('dsh');
|
|
48
|
+
expect(Object.keys(PLATFORM_PROFILES)).toEqual(
|
|
49
|
+
expect.arrayContaining(['opencode', 'claude-code', 'qoder', 'dsh'])
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const profile = PLATFORM_PROFILES.dsh;
|
|
53
|
+
expect(Array.isArray(profile.skillsDirs)).toBe(true);
|
|
54
|
+
expect(profile.requiredDeps).toBeDefined();
|
|
55
|
+
// Profiles store home-relative segments; getSkillsDirs resolves them.
|
|
56
|
+
expect(profile.skillsDirs[0]).toEqual(['.dsh', 'skills']);
|
|
57
|
+
expect(getSkillsDirs('dsh')[0]).toBe(path.join(tmpHome, '.dsh', 'skills'));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Every profile must be plain data — no getters mixed in with values, which
|
|
61
|
+
// would break Object.keys()/JSON round-trips and made the object's contract
|
|
62
|
+
// depend on which key you touched (Delphi R1 CI-01).
|
|
63
|
+
it('every profile is plain enumerable data, with no accessor properties', () => {
|
|
64
|
+
const { PLATFORM_PROFILES } = require('../detect-deps');
|
|
65
|
+
|
|
66
|
+
for (const name of Object.keys(PLATFORM_PROFILES)) {
|
|
67
|
+
const descriptor = Object.getOwnPropertyDescriptor(PLATFORM_PROFILES, name);
|
|
68
|
+
expect(descriptor.get, `${name} must not be an accessor`).toBeUndefined();
|
|
69
|
+
// A plain value also survives a JSON round-trip unchanged.
|
|
70
|
+
expect(JSON.parse(JSON.stringify(PLATFORM_PROFILES[name]))).toEqual(
|
|
71
|
+
PLATFORM_PROFILES[name]
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Paths must follow process.env.HOME rather than pinning the home directory
|
|
77
|
+
// at require() time — the trap doctor.js documents at its top.
|
|
78
|
+
it('resolved paths follow a HOME change after require', () => {
|
|
79
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
80
|
+
// Loaded above with the first tmpHome; move HOME and resolve again.
|
|
81
|
+
const secondHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dsh2-'));
|
|
82
|
+
try {
|
|
83
|
+
process.env.HOME = secondHome;
|
|
84
|
+
expect(getSkillsDirs('dsh')[0]).toBe(path.join(secondHome, '.dsh', 'skills'));
|
|
85
|
+
// The pre-existing profiles must follow too — they used to be frozen at
|
|
86
|
+
// require() time from a destructured HOME_DIR.
|
|
87
|
+
expect(getSkillsDirs('opencode')[0]).toBe(
|
|
88
|
+
path.join(secondHome, '.config', 'opencode', 'skills')
|
|
89
|
+
);
|
|
90
|
+
} finally {
|
|
91
|
+
fs.rmSync(secondHome, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Anti-vacuity: the three pre-existing profiles must keep their own paths,
|
|
96
|
+
// proving the getter did not disturb them.
|
|
97
|
+
it('leaves the pre-existing profiles unchanged', () => {
|
|
98
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
99
|
+
|
|
100
|
+
expect(getSkillsDirs('opencode')[0]).toBe(
|
|
101
|
+
path.join(tmpHome, '.config', 'opencode', 'skills')
|
|
102
|
+
);
|
|
103
|
+
expect(getSkillsDirs('qoder')[0]).toBe(path.join(tmpHome, '.qoder', 'skills'));
|
|
104
|
+
expect(getSkillsDirs('claude-code')[0]).toBe(path.join(tmpHome, '.claude', 'skills'));
|
|
105
|
+
});
|
|
106
|
+
});
|