@boyingliu01/xp-gate 0.19.3 → 0.20.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/gate-4.sh +83 -18
- package/adapters/gate-8.sh +49 -5
- package/adapters/iac.sh +1 -1
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +49 -5
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +49 -5
- package/hooks/lib/jscpd-run.sh +63 -0
- package/hooks/lib/sprint-gate-report.sh +58 -0
- package/hooks/lib/test-failure.sh +152 -0
- package/hooks/lib/typecheck.sh +141 -0
- package/hooks/lib/validate-code-walkthrough.cjs +43 -5
- package/hooks/post-merge +100 -0
- package/hooks/pre-commit +389 -162
- package/hooks/pre-push +75 -19
- package/lib/__tests__/bootstrap.test.js +23 -41
- package/lib/__tests__/check-version.test.js +8 -1
- package/lib/__tests__/detect-deps.test.js +16 -0
- package/lib/__tests__/doctor-drift-round2.test.js +212 -0
- package/lib/__tests__/doctor-executed-surface.test.js +209 -0
- package/lib/__tests__/doctor-hook-drift.test.js +329 -0
- package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
- package/lib/__tests__/doctor-module-drift.test.js +206 -0
- package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
- package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
- package/lib/__tests__/doctor.test.js +155 -0
- package/lib/__tests__/gate-runner.test.js +19 -3
- package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
- package/lib/__tests__/init.test.js +44 -17
- package/lib/__tests__/install-skill-full-dir.test.js +151 -0
- package/lib/__tests__/install-skill.test.js +51 -132
- package/lib/__tests__/language-tools-detect.test.js +158 -0
- package/lib/__tests__/phase-transition.test.js +15 -1
- package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
- package/lib/__tests__/shared-paths.test.js +20 -0
- package/lib/__tests__/shared-utils.test.js +69 -0
- package/lib/__tests__/skill-flags.test.js +48 -0
- package/lib/__tests__/ui-detector.test.ts +6 -2
- package/lib/__tests__/uninstall.test.js +20 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/update-skill.test.js +7 -4
- package/lib/detect-deps.js +3 -20
- package/lib/doctor-tui.js +22 -5
- package/lib/doctor.js +1178 -31
- package/lib/init.js +184 -33
- package/lib/install-cmd.js +7 -4
- package/lib/install-skill.js +38 -95
- package/lib/language-tools.js +83 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-paths.js +5 -2
- package/lib/shared-utils.js +47 -3
- package/lib/uninstall.js +12 -0
- package/lib/update-hooks.js +4 -4
- package/lib/update-skill.js +6 -7
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/gate-m.test.ts +66 -14
- package/mutation/__tests__/mutmut-runner.test.ts +91 -42
- package/mutation/__tests__/test-filter-parity.test.ts +37 -0
- package/mutation/gate-m.ts +21 -8
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +169 -4
- package/plugins/dsh/dsh-overlay.patch.yml +67 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +24 -19
- package/plugins/dsh/setup-dsh.ps1 +160 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/command.test.ts +66 -2
- package/plugins/dsh/src/command.ts +36 -14
- package/plugins/dsh/src/contract.test.ts +31 -1
- package/plugins/dsh/src/delphi-config.test.ts +190 -0
- package/plugins/dsh/src/delphi-config.ts +223 -0
- package/plugins/dsh/src/delphi-run.test.ts +181 -0
- package/plugins/dsh/src/delphi-run.ts +129 -0
- package/plugins/dsh/src/delphi.test.ts +222 -0
- package/plugins/dsh/src/delphi.ts +247 -0
- package/plugins/dsh/src/dialect.test.ts +59 -0
- package/plugins/dsh/src/dialect.ts +40 -0
- package/plugins/dsh/src/index.ts +70 -4
- package/plugins/dsh/src/skills.test.ts +26 -19
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/agents/delphi-architecture.md +1 -1
- package/plugins/qoder/agents/delphi-feasibility.md +1 -1
- package/plugins/qoder/agents/delphi-technical.md +1 -1
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +9 -8
- package/principles/__tests__/adapter-metrics.test.ts +292 -0
- package/principles/__tests__/analyzer.test.ts +29 -6
- package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
- package/principles/__tests__/baseline-prune.test.ts +338 -0
- package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
- package/principles/__tests__/boy-scout-integration.test.ts +97 -6
- package/principles/__tests__/boy-scout.test.ts +253 -7
- package/principles/__tests__/config-enforcement.test.ts +469 -0
- package/principles/__tests__/config-git-timeout.test.ts +66 -0
- package/principles/__tests__/direct-execution.test.ts +187 -0
- package/principles/__tests__/index.test.ts +16 -3
- package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
- package/principles/adapters/typescript.ts +613 -13
- package/principles/analyzer.ts +22 -0
- package/principles/baseline-prune.ts +217 -0
- package/principles/boy-scout.ts +481 -51
- package/principles/config.ts +362 -54
- package/principles/direct-execution.ts +60 -0
- package/principles/index.ts +40 -17
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
- package/principles/rules/clean-code/code-duplication.ts +8 -8
- package/principles/rules/clean-code/deep-nesting.ts +8 -8
- package/principles/rules/clean-code/god-class.ts +8 -8
- package/principles/rules/clean-code/large-file.ts +15 -11
- package/principles/rules/clean-code/long-function.ts +8 -8
- package/principles/rules/clean-code/magic-numbers.ts +10 -7
- package/principles/rules/clean-code/many-exports.ts +7 -7
- package/principles/rules/clean-code/missing-error-handling.ts +5 -5
- package/principles/rules/clean-code/too-many-params.ts +8 -8
- package/principles/rules/clean-code/unused-imports.ts +5 -5
- package/principles/rules/solid/dip.ts +18 -8
- package/principles/rules/solid/isp.ts +9 -8
- package/principles/rules/solid/lsp.ts +6 -6
- package/principles/rules/solid/ocp.ts +6 -5
- package/principles/rules/solid/srp.ts +7 -8
- package/principles/types.ts +15 -0
- package/scripts/__tests__/sync-package-content.test.js +28 -0
- package/scripts/delphi-external-review.cjs +193 -10
- package/scripts/sync-package-content.js +7 -4
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/delphi-review/SKILL.md +98 -14
- package/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/skills/delphi-review/references/requirements.md +6 -6
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/lib/__tests__/download-skill.test.js +0 -281
- package/lib/download-skill.js +0 -96
package/hooks/pre-push
CHANGED
|
@@ -89,13 +89,40 @@ while read local_ref local_sha remote_ref remote_sha; do
|
|
|
89
89
|
echo " ℹ️ Branch deletion detected ($local_ref). Skipping validation."
|
|
90
90
|
continue
|
|
91
91
|
elif [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
92
|
-
# New
|
|
93
|
-
#
|
|
94
|
-
#
|
|
92
|
+
# New ref — a branch or a tag.
|
|
93
|
+
# Base-branch resolution shared by the tag check below (#491) and the
|
|
94
|
+
# cumulative new-branch diff.
|
|
95
95
|
BASE_BRANCH="origin/main"
|
|
96
96
|
if ! git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
|
|
97
97
|
BASE_BRANCH="origin/master"
|
|
98
98
|
fi
|
|
99
|
+
# Tag push (#491): a tag carries no code beyond the commit it points at.
|
|
100
|
+
# If that commit is already published on the base branch, its content
|
|
101
|
+
# passed these gates when the base ref was pushed. Re-validating it here
|
|
102
|
+
# false-blocked every release: GATE MW binds evidence to the local HEAD,
|
|
103
|
+
# and a squash merge rewrote the reviewed commit's hash, so no genuine
|
|
104
|
+
# evidence could ever match. The scope is deliberately the PROTECTED base
|
|
105
|
+
# ref, not "any remote-tracking branch": a commit that only ever lived on
|
|
106
|
+
# a side branch still gets the full gates. A tag on a NOT-yet-published
|
|
107
|
+
# commit falls through to the cumulative validation below — creating a
|
|
108
|
+
# tag must not become a path around review. Threat model: like any
|
|
109
|
+
# client-side hook this bounds honest workflows and accidents, not an
|
|
110
|
+
# adversary with write access to this repository — such an actor could
|
|
111
|
+
# forge refs AND the walkthrough evidence file itself; server-side branch
|
|
112
|
+
# protection remains the real boundary.
|
|
113
|
+
if [[ "$local_ref" == refs/tags/* ]]; then
|
|
114
|
+
_tag_commit=$(git rev-parse --verify "${local_sha}^{commit}" 2>/dev/null || true)
|
|
115
|
+
if [ -z "$_tag_commit" ]; then
|
|
116
|
+
echo " ⚠️ Could not dereference $local_ref to a commit — treating it as un-published."
|
|
117
|
+
elif git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1 \
|
|
118
|
+
&& git merge-base --is-ancestor "$_tag_commit" "$BASE_BRANCH" 2>/dev/null; then
|
|
119
|
+
echo " ℹ️ Tag $local_ref points at a commit already published on $BASE_BRANCH — no new code content."
|
|
120
|
+
continue
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
# New branch (or tag on un-published work) - compare cumulative diff vs base branch
|
|
124
|
+
# Use git merge-base HEAD main/master to find the branch point,
|
|
125
|
+
# then diff the full branch range. Falls back to HEAD-only diff if no base found.
|
|
99
126
|
if git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
|
|
100
127
|
MERGE_BASE=$(git merge-base HEAD "$BASE_BRANCH" 2>/dev/null)
|
|
101
128
|
if [ -n "$MERGE_BASE" ] && [ "$MERGE_BASE" != "$(git rev-parse HEAD)" ]; then
|
|
@@ -123,7 +150,7 @@ fi
|
|
|
123
150
|
|
|
124
151
|
# Determine if pushed files contain ONLY documentation/non-source files
|
|
125
152
|
DOC_ONLY=true
|
|
126
|
-
SOURCE_EXTENSIONS="\.py$|\.js$|\.ts$|\.tsx$|\.java$|\.go$|\.rs$|\.cpp$|\.c$|\.swift$|\.kt$|\.sh$|\.dart$|\.ps1$|\.m$|\.mm$|\.h$|\.hpp$"
|
|
153
|
+
SOURCE_EXTENSIONS="\.py$|\.js$|\.cjs$|\.mjs$|\.ts$|\.tsx$|\.java$|\.go$|\.rs$|\.cpp$|\.c$|\.swift$|\.kt$|\.sh$|\.dart$|\.ps1$|\.m$|\.mm$|\.h$|\.hpp$"
|
|
127
154
|
|
|
128
155
|
for file in $PUSHED_FILES; do
|
|
129
156
|
if echo "$file" | grep -qE "$SOURCE_EXTENSIONS"; then
|
|
@@ -152,6 +179,26 @@ echo "Files changed: $FILES_CHANGED"
|
|
|
152
179
|
echo "Lines: +$LINES_ADDED -$LINES_DELETED"
|
|
153
180
|
echo ""
|
|
154
181
|
|
|
182
|
+
# Run a TypeScript entrypoint under tsx without routing arguments through the
|
|
183
|
+
# npx shim: Windows cmd.exe caps command lines at 8191 chars and long changed-file
|
|
184
|
+
# lists overflow it, turning a healthy run into a false BLOCK. Prefer the
|
|
185
|
+
# repo-local tsx CLI driven by node directly; keep npx as fallback for consumer
|
|
186
|
+
# repos relying on a global install. RUN_TSX_TIMEOUT (e.g. "120s") wraps the
|
|
187
|
+
# invocation in `timeout`.
|
|
188
|
+
if ! declare -F run_tsx >/dev/null 2>&1; then
|
|
189
|
+
run_tsx() {
|
|
190
|
+
local _root="${PROJECT_ROOT:-$(pwd)}" _timeout=()
|
|
191
|
+
if [ -n "${RUN_TSX_TIMEOUT:-}" ]; then
|
|
192
|
+
_timeout=(timeout "$RUN_TSX_TIMEOUT")
|
|
193
|
+
fi
|
|
194
|
+
if [ -f "$_root/node_modules/tsx/dist/cli.mjs" ] && command -v node >/dev/null 2>&1; then
|
|
195
|
+
"${_timeout[@]}" node "$_root/node_modules/tsx/dist/cli.mjs" "$@"
|
|
196
|
+
else
|
|
197
|
+
"${_timeout[@]}" npx tsx "$@"
|
|
198
|
+
fi
|
|
199
|
+
}
|
|
200
|
+
fi
|
|
201
|
+
|
|
155
202
|
# ============================================================================
|
|
156
203
|
# GATE S: SPRINT FLOW ENFORCEMENT
|
|
157
204
|
# Validates sprint state consistency before push
|
|
@@ -161,11 +208,17 @@ GATE_S_STATUS="PASS"
|
|
|
161
208
|
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
|
|
162
209
|
SPRINT_GATE_SCRIPT=""
|
|
163
210
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
211
|
+
# Same chain as pre-commit's Gate 11 (#476). The two tiers here both point at
|
|
212
|
+
# gate-script directories, while update-hooks installs sprint-gate.sh under
|
|
213
|
+
# ~/.config/xp-gate/hooks -- a correctly installed machine was told the file was
|
|
214
|
+
# missing here too. Nothing is sourced for this: pre-push resolves its libraries
|
|
215
|
+
# further down (ADAPTER_COMMON), and this gate must decide before that.
|
|
216
|
+
for _sprint_gate_dir in "$PROJECT_ROOT/githooks" "$(dirname "$0")" "$HOME/.config/xp-gate/hooks"; do
|
|
217
|
+
if [ -n "$_sprint_gate_dir" ] && [ -f "$_sprint_gate_dir/sprint-gate.sh" ]; then
|
|
218
|
+
SPRINT_GATE_SCRIPT="$_sprint_gate_dir/sprint-gate.sh"
|
|
219
|
+
break
|
|
220
|
+
fi
|
|
221
|
+
done
|
|
169
222
|
|
|
170
223
|
if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
171
224
|
if ! bash "$SPRINT_GATE_SCRIPT" --pre-push; then
|
|
@@ -174,7 +227,7 @@ if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
|
174
227
|
exit 1
|
|
175
228
|
fi
|
|
176
229
|
else
|
|
177
|
-
echo "⏭️ SKIPPED - Gate MS: Sprint Flow (sprint-gate.sh not found)"
|
|
230
|
+
echo "⏭️ SKIPPED - Gate MS: Sprint Flow (sprint-gate.sh not found; searched: $PROJECT_ROOT/githooks, $(dirname "$0"), $HOME/.config/xp-gate/hooks)"
|
|
178
231
|
GATE_S_STATUS="SKIP"
|
|
179
232
|
fi
|
|
180
233
|
echo ""
|
|
@@ -210,7 +263,7 @@ if [[ -f "package.json" ]] && [[ -f "tsconfig.json" ]] && [[ -n "$TS_FILES" ]];
|
|
|
210
263
|
done <<< "$TS_FILES"
|
|
211
264
|
|
|
212
265
|
GATE_10_OUTPUT=$(mktemp)
|
|
213
|
-
if
|
|
266
|
+
if RUN_TSX_TIMEOUT=120s run_tsx "$GATE_10_SCRIPT" --changed-files "$GATE_10_FILE_LIST" --project-root "$PROJECT_ROOT" > "$GATE_10_OUTPUT" 2>&1; then
|
|
214
267
|
cat "$GATE_10_OUTPUT"
|
|
215
268
|
GATE_10_STATUS="PASS"
|
|
216
269
|
echo "✅ Gate 10: PASS"
|
|
@@ -274,8 +327,11 @@ else
|
|
|
274
327
|
echo "⚠️ Stryker config not found or @stryker-mutator not installed."
|
|
275
328
|
echo " SKIP — Gate M mutation testing."
|
|
276
329
|
else
|
|
277
|
-
# Filter to source files (exclude tests)
|
|
278
|
-
|
|
330
|
+
# Filter to source files (exclude tests). The tree-segment grep excludes
|
|
331
|
+
# test infrastructure (helpers/fixtures under tests/, test/, __tests__/)
|
|
332
|
+
# that is not named *.test.* -- it must be judged by the suites that run
|
|
333
|
+
# it, not by a mutation score budgeted for production code (#480).
|
|
334
|
+
CHANGED_SOURCE_FILES=$(echo "$TS_FILES" | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '\.d\.ts$' | grep -v -E '(^|/)(tests?|__tests__)/' || true)
|
|
279
335
|
|
|
280
336
|
if [ -z "$CHANGED_SOURCE_FILES" ]; then
|
|
281
337
|
echo "📚 No changed TypeScript source files. SKIP — Gate M."
|
|
@@ -298,7 +354,7 @@ else
|
|
|
298
354
|
else
|
|
299
355
|
# Run mutation gate with timeout matching stryker.conf.json (timeoutMS: 600000 = 10min)
|
|
300
356
|
MUTATION_OUTPUT=$(mktemp)
|
|
301
|
-
|
|
357
|
+
RUN_TSX_TIMEOUT=600s run_tsx $MUTATION_SCRIPT --changed-files "$CHANGED_SOURCE_FILES" > "$MUTATION_OUTPUT" 2>&1
|
|
302
358
|
MUTATION_EXIT=$?
|
|
303
359
|
|
|
304
360
|
case $MUTATION_EXIT in
|
|
@@ -361,7 +417,7 @@ else
|
|
|
361
417
|
if detect_python_mutation_testable 2>/dev/null; then
|
|
362
418
|
if [ -f "src/mutation/gate-m.ts" ]; then
|
|
363
419
|
MUTATION_OUTPUT=$(mktemp)
|
|
364
|
-
|
|
420
|
+
RUN_TSX_TIMEOUT=120s run_tsx src/mutation/gate-m.ts --changed-files "$PYTHON_FILE_LIST" > "$MUTATION_OUTPUT" 2>&1
|
|
365
421
|
MUTATION_EXIT=$?
|
|
366
422
|
|
|
367
423
|
case $MUTATION_EXIT in
|
|
@@ -430,7 +486,7 @@ else
|
|
|
430
486
|
echo "⚠ Gate M script not found. SKIP — Gate M (Go)."
|
|
431
487
|
else
|
|
432
488
|
MUTATION_OUTPUT=$(mktemp)
|
|
433
|
-
|
|
489
|
+
RUN_TSX_TIMEOUT=120s run_tsx "$MUTATION_SCRIPT" --changed-files "$GO_FILE_LIST" --timeout-ms 120000 > "$MUTATION_OUTPUT" 2>&1
|
|
434
490
|
MUTATION_EXIT=$?
|
|
435
491
|
|
|
436
492
|
case $MUTATION_EXIT in
|
|
@@ -500,7 +556,7 @@ else
|
|
|
500
556
|
echo "⚠ Gate M script not found. SKIP — Gate M (Java)."
|
|
501
557
|
else
|
|
502
558
|
MUTATION_OUTPUT=$(mktemp)
|
|
503
|
-
|
|
559
|
+
RUN_TSX_TIMEOUT=600s run_tsx "$MUTATION_SCRIPT" --changed-files "$JAVA_FILE_LIST" --timeout-ms 600000 > "$MUTATION_OUTPUT" 2>&1
|
|
504
560
|
MUTATION_EXIT=$?
|
|
505
561
|
|
|
506
562
|
case $MUTATION_EXIT in
|
|
@@ -570,7 +626,7 @@ else
|
|
|
570
626
|
echo "⚠ Gate M script not found. SKIP — Gate M (Kotlin)."
|
|
571
627
|
else
|
|
572
628
|
MUTATION_OUTPUT=$(mktemp)
|
|
573
|
-
|
|
629
|
+
RUN_TSX_TIMEOUT=600s run_tsx "$MUTATION_SCRIPT" --changed-files "$KT_FILE_LIST" --timeout-ms 600000 > "$MUTATION_OUTPUT" 2>&1
|
|
574
630
|
MUTATION_EXIT=$?
|
|
575
631
|
|
|
576
632
|
case $MUTATION_EXIT in
|
|
@@ -752,7 +808,7 @@ run_gate_m3() {
|
|
|
752
808
|
[[ -n "$f" ]] && args+=("$f")
|
|
753
809
|
done <<< "$test_files"
|
|
754
810
|
|
|
755
|
-
if
|
|
811
|
+
if run_tsx "$GATE_M3_SCRIPT" "${args[@]}"; then
|
|
756
812
|
echo "✅ Gate ML: PASS"
|
|
757
813
|
return 0
|
|
758
814
|
else
|
|
@@ -1,78 +1,60 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
1
4
|
const { bootstrap } = require('../bootstrap.js');
|
|
2
5
|
|
|
6
|
+
const GATE_TOOLS = ['jscpd', 'lizard', 'checkov', 'hadolint', 'gitleaks', 'semgrep', 'npx', 'jq'];
|
|
7
|
+
|
|
3
8
|
describe('bootstrap', () => {
|
|
4
9
|
let consoleLogSpy;
|
|
5
|
-
|
|
10
|
+
let realPath;
|
|
11
|
+
let stubDir;
|
|
12
|
+
|
|
13
|
+
// The module mocks these tests used to declare were never applied: bootstrap.js
|
|
14
|
+
// is CommonJS, so its internal require of detect-deps.js bypasses the mock
|
|
15
|
+
// registry and every case fell through to the real installer (global npm/pip
|
|
16
|
+
// installs, ~60s in CI). Stubbing the tools on PATH exercises the real
|
|
17
|
+
// detection path without touching the environment.
|
|
6
18
|
beforeEach(() => {
|
|
7
19
|
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
20
|
+
realPath = process.env.PATH;
|
|
21
|
+
stubDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-gate-bootstrap-'));
|
|
22
|
+
for (const tool of GATE_TOOLS) {
|
|
23
|
+
const stub = path.join(stubDir, process.platform === 'win32' ? `${tool}.cmd` : tool);
|
|
24
|
+
fs.writeFileSync(stub, process.platform === 'win32' ? '@echo 1.0.0\r\n' : '#!/bin/sh\necho 1.0.0\n');
|
|
25
|
+
if (process.platform !== 'win32') fs.chmodSync(stub, 0o755);
|
|
26
|
+
}
|
|
27
|
+
process.env.PATH = `${stubDir}${path.delimiter}${realPath}`;
|
|
8
28
|
});
|
|
9
29
|
|
|
10
30
|
afterEach(() => {
|
|
31
|
+
process.env.PATH = realPath;
|
|
32
|
+
fs.rmSync(stubDir, { recursive: true, force: true });
|
|
11
33
|
vi.restoreAllMocks();
|
|
12
34
|
});
|
|
13
35
|
|
|
14
36
|
it('returns 0 when --dry-run given with no missing tools', () => {
|
|
15
|
-
vi.mock('../detect-deps.js', () => ({
|
|
16
|
-
GATE_CLI_TOOLS: [],
|
|
17
|
-
checkCliTool: () => ({ available: true }),
|
|
18
|
-
getToolInstallCmd: () => 'echo done',
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
const { bootstrap } = require('../bootstrap.js');
|
|
22
37
|
const code = bootstrap(['--dry-run']);
|
|
23
38
|
expect(code).toBe(0);
|
|
24
|
-
vi.restoreAllMocks();
|
|
25
39
|
});
|
|
26
40
|
|
|
27
41
|
it('returns 0 when all CLI tools are available', () => {
|
|
28
|
-
vi.mock('../detect-deps.js', () => ({
|
|
29
|
-
GATE_CLI_TOOLS: [{ tool: 'testtool', gates: ['Gate 1'], install: { linux: 'echo ok' } }],
|
|
30
|
-
checkCliTool: () => ({ available: true, version: '1.0' }),
|
|
31
|
-
getToolInstallCmd: () => 'echo ok',
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
const { bootstrap } = require('../bootstrap.js');
|
|
35
42
|
const code = bootstrap([]);
|
|
36
43
|
expect(code).toBe(0);
|
|
37
|
-
vi.restoreAllMocks();
|
|
38
44
|
});
|
|
39
45
|
|
|
40
46
|
it('accepts --lang ts parameter', () => {
|
|
41
|
-
vi.mock('../detect-deps.js', () => ({
|
|
42
|
-
GATE_CLI_TOOLS: [],
|
|
43
|
-
checkCliTool: () => ({ available: true }),
|
|
44
|
-
getToolInstallCmd: () => 'echo done',
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
const { bootstrap } = require('../bootstrap.js');
|
|
48
47
|
const code = bootstrap(['--lang', 'ts']);
|
|
49
48
|
expect(code).toBe(0);
|
|
50
|
-
vi.restoreAllMocks();
|
|
51
49
|
});
|
|
52
50
|
|
|
53
51
|
it('accepts --lang ts,py parameter', () => {
|
|
54
|
-
vi.mock('../detect-deps.js', () => ({
|
|
55
|
-
GATE_CLI_TOOLS: [],
|
|
56
|
-
checkCliTool: () => ({ available: true }),
|
|
57
|
-
getToolInstallCmd: () => 'echo done',
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
const { bootstrap } = require('../bootstrap.js');
|
|
61
52
|
const code = bootstrap(['--lang', 'ts,py']);
|
|
62
53
|
expect(code).toBe(0);
|
|
63
|
-
vi.restoreAllMocks();
|
|
64
54
|
});
|
|
65
55
|
|
|
66
56
|
it('recognizes --verbose flag without error', () => {
|
|
67
|
-
vi.mock('../detect-deps.js', () => ({
|
|
68
|
-
GATE_CLI_TOOLS: [],
|
|
69
|
-
checkCliTool: () => ({ available: true }),
|
|
70
|
-
getToolInstallCmd: () => 'echo done',
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
|
-
const { bootstrap } = require('../bootstrap.js');
|
|
74
57
|
const code = bootstrap(['--verbose']);
|
|
75
58
|
expect(code).toBe(0);
|
|
76
|
-
vi.restoreAllMocks();
|
|
77
59
|
});
|
|
78
60
|
});
|
|
@@ -318,7 +318,14 @@ describe('check-version.js — REQ-001-01', () => {
|
|
|
318
318
|
const r = await mod.getRemoteVersion('@nonexistent/pkg-test-only');
|
|
319
319
|
// Without network: null. With network (npm registry proxy): object.
|
|
320
320
|
expect(r === null || (typeof r.latest === 'string')).toBe(true);
|
|
321
|
-
|
|
321
|
+
// This case performs a real registry lookup, so it must be given a budget
|
|
322
|
+
// ABOVE the module's own NETWORK_TIMEOUT_MS (5000). At vitest's 5000ms
|
|
323
|
+
// default the two race, and on a machine where the registry is slow or
|
|
324
|
+
// unreachable the test failed as "Test timed out" -- reporting a stuck test
|
|
325
|
+
// rather than the graceful degradation it exists to verify. Raised here
|
|
326
|
+
// rather than lowering the module timeout, which is deliberate production
|
|
327
|
+
// behaviour.
|
|
328
|
+
}, 20000);
|
|
322
329
|
|
|
323
330
|
// AC-001-02-A: writeCache rejects non-semver versions from registry response
|
|
324
331
|
function evictCache() {
|
|
@@ -285,6 +285,22 @@ describe('detect-deps', () => {
|
|
|
285
285
|
const { detectPlatform } = require('../detect-deps');
|
|
286
286
|
expect(detectPlatform()).toBe('qoder');
|
|
287
287
|
});
|
|
288
|
+
|
|
289
|
+
// ── #424: brand-new Qoder installs have no skills/ subdirectory yet, so a
|
|
290
|
+
// skills-only marker misdetects them as opencode and configureQoderDelphiAgents
|
|
291
|
+
// deploys zero Delphi agents. Any ~/.qoder content must count as qoder. ──
|
|
292
|
+
|
|
293
|
+
it('returns "qoder" when only ~/.qoder exists (brand-new install, no skills dir)', () => {
|
|
294
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder'), { recursive: true });
|
|
295
|
+
const { detectPlatform } = require('../detect-deps');
|
|
296
|
+
expect(detectPlatform()).toBe('qoder');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('returns "qoder" when only ~/.qoder/agents exists (no skills dir)', () => {
|
|
300
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder', 'agents'), { recursive: true });
|
|
301
|
+
const { detectPlatform } = require('../detect-deps');
|
|
302
|
+
expect(detectPlatform()).toBe('qoder');
|
|
303
|
+
});
|
|
288
304
|
});
|
|
289
305
|
|
|
290
306
|
// ── getSkillsDirs tests ──
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-495 Round 2: the drift surface must not lie in either direction
|
|
3
|
+
* @intent 三位专家对 origin/main..HEAD 的走查给出三条同族判定:(1) 字节不同但行集
|
|
4
|
+
* 相同(仅行尾/缩进差异)落入 moduleDriftDetail 兜底分支,报 FAIL 却写着
|
|
5
|
+
* "0 line(s) absent from the repo",方向断言为伪且 --fix 清不掉;(2)
|
|
6
|
+
* diagnoseModuleDrift 认 XP_GATE_REPO_ROOT 而 syncModulesFromRepo 只认
|
|
7
|
+
* process.cwd(),在仓库外带 env 运行 --sync-hooks 时诊断报 FAIL、同步静默
|
|
8
|
+
* 空转(#488/#416 已明令禁止的"被忽略的选项"缺陷类);(3) drift 结果被
|
|
9
|
+
* Promise.race 的全局 10s 超时整份丢弃并返回 issues:0,doctor 随即打印
|
|
10
|
+
* "✓ All checks passed" —— 正是 #495 要消灭的"模块陈旧但 doctor 全绿"。
|
|
11
|
+
* 外加 lib/ 通道:syncGlobalHooksFromRepo 对 githooks/lib/*.sh 无条件覆盖,
|
|
12
|
+
* 而同一次发布里模块同步对同类风险显式拒绝,破坏性写入标准不对称。
|
|
13
|
+
* @covers AC-495-07, AC-495-08, AC-495-09, AC-495-10, AC-495-13, AC-488-05
|
|
14
|
+
*/
|
|
15
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { describe, expect, it } from 'vitest';
|
|
19
|
+
|
|
20
|
+
function fixture(spec) {
|
|
21
|
+
const root = mkdtempSync(join(tmpdir(), 'xp-gate-r2-'));
|
|
22
|
+
const repoHooks = join(root, 'githooks');
|
|
23
|
+
const installedHooks = join(root, 'hooks');
|
|
24
|
+
const installedAdapters = join(root, 'adapters');
|
|
25
|
+
mkdirSync(join(repoHooks, 'adapters'), { recursive: true });
|
|
26
|
+
mkdirSync(join(repoHooks, 'lib'), { recursive: true });
|
|
27
|
+
mkdirSync(installedHooks, { recursive: true });
|
|
28
|
+
mkdirSync(installedHooks + '/lib', { recursive: true });
|
|
29
|
+
mkdirSync(installedAdapters, { recursive: true });
|
|
30
|
+
|
|
31
|
+
const write = (file, text) => {
|
|
32
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
33
|
+
writeFileSync(file, text);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (const [name, sides] of Object.entries(spec)) {
|
|
37
|
+
const rootLevel = name.startsWith('gate-') || name === 'adapter-common.sh' || name.startsWith('lib/');
|
|
38
|
+
if (sides.repo !== null) {
|
|
39
|
+
write(join(repoHooks, rootLevel ? name : join('adapters', name)), sides.repo);
|
|
40
|
+
}
|
|
41
|
+
if (sides.installed !== undefined && sides.installed !== null) {
|
|
42
|
+
write(join(installedAdapters, name), sides.installed);
|
|
43
|
+
}
|
|
44
|
+
// lib/ lives under hooks/, not under the adapters dir, so it needs its own pair.
|
|
45
|
+
if (sides.installedLib !== undefined) {
|
|
46
|
+
write(join(installedHooks, name), sides.installedLib);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
write(join(repoHooks, 'pre-commit'), '#!/bin/bash\necho repo\n');
|
|
51
|
+
write(join(installedHooks, 'pre-commit'), '#!/bin/bash\necho repo\n');
|
|
52
|
+
|
|
53
|
+
// Resolution anchors: the installed dir only becomes ADAPTER_DIR / GATE_DIR if
|
|
54
|
+
// adapter-common.sh (githooks/pre-commit:34) and gate-3.sh (:80) live in it.
|
|
55
|
+
// Without them the fixture describes a tier the hook never resolves to, and the
|
|
56
|
+
// comparison silently targets a directory outside the test (#495 Round 3).
|
|
57
|
+
const ANCHOR = '#!/bin/bash\nanchor\n';
|
|
58
|
+
for (const anchor of ['adapter-common.sh', 'gate-3.sh']) {
|
|
59
|
+
if (spec[anchor]) continue;
|
|
60
|
+
write(join(repoHooks, anchor), ANCHOR);
|
|
61
|
+
write(join(installedAdapters, anchor), ANCHOR);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { root, repoHooks, installedHooks, installedAdapters, write };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Name every tier the hook can resolve into so no fixture can reach this
|
|
69
|
+
* machine's real `~/.config/xp-gate`.
|
|
70
|
+
*/
|
|
71
|
+
function ctxFor(f, extra = {}) {
|
|
72
|
+
return {
|
|
73
|
+
repoRoot: f.root,
|
|
74
|
+
repoHooks: f.repoHooks,
|
|
75
|
+
installedAdapters: f.installedAdapters,
|
|
76
|
+
scriptDir: f.installedHooks,
|
|
77
|
+
...extra,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('doctor drift Round 2 (#495)', () => {
|
|
82
|
+
it('AC-495-07: a line-ending-only difference is not reported as drift, and never claims zero unique lines', () => {
|
|
83
|
+
const { diagnoseModuleDrift } = require('../doctor');
|
|
84
|
+
const repo = '#!/bin/bash\ngate_line_one\ngate_line_two\n';
|
|
85
|
+
const f = fixture({
|
|
86
|
+
'gate-4.sh': { repo, installed: repo.replace(/\n/g, '\r\n') },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = diagnoseModuleDrift(ctxFor(f));
|
|
90
|
+
const check = result.checks.find((c) => c.name === 'Module drift: gate-4.sh');
|
|
91
|
+
expect(check).toBeDefined();
|
|
92
|
+
expect(check.status).not.toBe('FAIL');
|
|
93
|
+
expect(result.issues).toBe(0);
|
|
94
|
+
// The self-contradicting verdict ("newer ... 0 line(s) absent") must be gone.
|
|
95
|
+
expect(check.detail).not.toMatch(/0 line\(s\)/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('AC-495-08: sync resolves the repo root the same way the diagnosis does', () => {
|
|
99
|
+
const { syncModulesFromRepo, diagnoseModuleDrift } = require('../doctor');
|
|
100
|
+
const f = fixture({
|
|
101
|
+
'gate-4.sh': { repo: '#!/bin/bash\nline_a\nline_b\n', installed: '#!/bin/bash\nline_a\n' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Run from OUTSIDE the repository with the repo located by env only.
|
|
105
|
+
const previous = process.env.XP_GATE_REPO_ROOT;
|
|
106
|
+
process.env.XP_GATE_REPO_ROOT = f.root;
|
|
107
|
+
try {
|
|
108
|
+
const diagnosis = diagnoseModuleDrift(ctxFor(f));
|
|
109
|
+
expect(diagnosis.issues).toBe(1);
|
|
110
|
+
|
|
111
|
+
// Only the install target is named: the repo must come from the same env
|
|
112
|
+
// the diagnosis used, or the FAIL this produces can never be cleared.
|
|
113
|
+
const synced = syncModulesFromRepo({ installedAdapters: f.installedAdapters, scriptDir: f.installedHooks });
|
|
114
|
+
expect(synced.synced).toContain('gate-4.sh');
|
|
115
|
+
expect(readFileSync(join(f.installedAdapters, 'gate-4.sh'), 'utf8')).toContain('line_b');
|
|
116
|
+
expect(diagnoseModuleDrift(ctxFor(f)).issues).toBe(0);
|
|
117
|
+
} finally {
|
|
118
|
+
if (previous === undefined) delete process.env.XP_GATE_REPO_ROOT;
|
|
119
|
+
else process.env.XP_GATE_REPO_ROOT = previous;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('AC-495-09: sync with no canonical repo source says so instead of returning an empty success', () => {
|
|
124
|
+
const { syncModulesFromRepo } = require('../doctor');
|
|
125
|
+
const root = mkdtempSync(join(tmpdir(), 'xp-gate-r2-nosource-'));
|
|
126
|
+
const installedAdapters = join(root, 'adapters');
|
|
127
|
+
mkdirSync(installedAdapters, { recursive: true });
|
|
128
|
+
writeFileSync(join(installedAdapters, 'gate-4.sh'), '#!/bin/bash\ninstalled\n');
|
|
129
|
+
|
|
130
|
+
const previous = process.env.XP_GATE_REPO_ROOT;
|
|
131
|
+
process.env.XP_GATE_REPO_ROOT = root; // no githooks/ under here at all
|
|
132
|
+
try {
|
|
133
|
+
const result = syncModulesFromRepo({ repoRoot: root, installedAdapters });
|
|
134
|
+
expect(result.sourceMissing).toBe(true);
|
|
135
|
+
expect(result.errors.join('\n')).toMatch(/no (canonical )?githooks/i);
|
|
136
|
+
} finally {
|
|
137
|
+
if (previous === undefined) delete process.env.XP_GATE_REPO_ROOT;
|
|
138
|
+
else process.env.XP_GATE_REPO_ROOT = previous;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('AC-495-10: a timed-out diagnosis can never print "All checks passed"', () => {
|
|
143
|
+
const source = readFileSync(join(__dirname, '..', 'doctor.js'), 'utf8');
|
|
144
|
+
|
|
145
|
+
// The race outcome must carry timedOut out of the destructuring...
|
|
146
|
+
expect(source).toMatch(/const \{[^}]*timedOut[^}]*\} = await Promise\.race/);
|
|
147
|
+
// ...and branch on it, because a timeout discards every computed FAIL.
|
|
148
|
+
expect(source).toMatch(/if \(timedOut\)[\s\S]{0,200}issues\s*(\+\+|\+=|= Math\.max)/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('AC-495-13: a flat copy inside the project githooks/ is diagnosed when the project tier is what executes', () => {
|
|
152
|
+
const { diagnoseModuleDrift } = require('../doctor');
|
|
153
|
+
// Project mode (no global install): pre-commit:36-37 resolves ADAPTER_DIR to
|
|
154
|
+
// <repo>/githooks, and resolve_adapter_path() then tries $ADAPTER_DIR/<lang>.sh
|
|
155
|
+
// before the nested source. The installer wrote those flat copies once and
|
|
156
|
+
// .gitignore:42 keeps them out of the repo, so a later repo fix that only
|
|
157
|
+
// touches adapters/<lang>.sh leaves the stale flat copy executing -- a
|
|
158
|
+
// #493-class repair inert in the very repository that shipped it.
|
|
159
|
+
const root = mkdtempSync(join(tmpdir(), 'xp-gate-project-tier-'));
|
|
160
|
+
const repoHooks = join(root, 'githooks');
|
|
161
|
+
mkdirSync(join(repoHooks, 'adapters'), { recursive: true });
|
|
162
|
+
writeFileSync(join(repoHooks, 'pre-commit'), '#!/bin/bash\necho repo\n');
|
|
163
|
+
writeFileSync(join(repoHooks, 'adapter-common.sh'), '#!/bin/bash\nshared\n');
|
|
164
|
+
writeFileSync(join(repoHooks, 'gate-3.sh'), '#!/bin/bash\ngate3\n');
|
|
165
|
+
writeFileSync(join(repoHooks, 'adapters', 'iac.sh'), '#!/bin/bash\nfixed_count_guard\n');
|
|
166
|
+
writeFileSync(join(repoHooks, 'iac.sh'), '#!/bin/bash\nstale_echo_zero\n');
|
|
167
|
+
|
|
168
|
+
const result = diagnoseModuleDrift({
|
|
169
|
+
repoRoot: root,
|
|
170
|
+
installedAdapters: join(root, 'nothing-installed-globally'),
|
|
171
|
+
scriptDir: join(root, 'nothing-installed-hooks'),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const projectCheck = result.checks.find((c) => /Module drift \(project\): iac\.sh/.test(c.name));
|
|
175
|
+
expect(projectCheck, 'the executed project-tier flat copy is invisible to doctor').toBeDefined();
|
|
176
|
+
expect(projectCheck.status).toBe('FAIL');
|
|
177
|
+
// The verdict has to say what to do with a gitignored installer residue.
|
|
178
|
+
expect(projectCheck.detail).toMatch(/flat|shadow/i);
|
|
179
|
+
expect(result.issues).toBe(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('AC-488-05: lib/ sync refuses an installed library the repo does not have lines for', () => {
|
|
183
|
+
const { syncGlobalHooksFromRepo } = require('../doctor');
|
|
184
|
+
const f = fixture({
|
|
185
|
+
'lib/test-failure.sh': {
|
|
186
|
+
repo: '#!/bin/bash\nrepo_shared_helper\n',
|
|
187
|
+
installedLib: '#!/bin/bash\ninstalled_machine_hardening\n',
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = syncGlobalHooksFromRepo({
|
|
192
|
+
repoHooks: f.repoHooks,
|
|
193
|
+
installedHooks: f.installedHooks,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.refused.map((r) => r.name)).toContain('lib/test-failure.sh');
|
|
197
|
+
expect(readFileSync(join(f.installedHooks, 'lib', 'test-failure.sh'), 'utf8'))
|
|
198
|
+
.toContain('installed_machine_hardening');
|
|
199
|
+
expect(existsSync(join(f.installedHooks, 'lib', 'test-failure.sh'))).toBe(true);
|
|
200
|
+
|
|
201
|
+
// A one-way-stale library still syncs: the guard is for divergence, not staleness.
|
|
202
|
+
const stale = fixture({
|
|
203
|
+
'lib/test-failure.sh': {
|
|
204
|
+
repo: '#!/bin/bash\nshared_a\nshared_b\n',
|
|
205
|
+
installedLib: '#!/bin/bash\nshared_a\n',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
const second = syncGlobalHooksFromRepo({ repoHooks: stale.repoHooks, installedHooks: stale.installedHooks });
|
|
209
|
+
expect(second.synced).toContain('lib/test-failure.sh');
|
|
210
|
+
expect(readFileSync(join(stale.installedHooks, 'lib', 'test-failure.sh'), 'utf8')).toContain('shared_b');
|
|
211
|
+
});
|
|
212
|
+
});
|