@boyingliu01/xp-gate 0.35.24 → 0.36.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/adapter-common.sh +55 -2
- package/adapters/csharp.sh +245 -0
- package/adapters/gate-3.sh +1 -1
- package/adapters/gate-4.sh +1 -1
- package/adapters/gate-9.sh +37 -2
- package/adapters/iac.sh +45 -5
- package/bin/xp-gate.js +18 -11
- package/gate-10.sh +18 -3
- package/gate-13-compiler-strictness.sh +181 -0
- package/gate-3.sh +1 -1
- package/gate-4.sh +1 -1
- package/gate-9.sh +37 -2
- package/gates/common.ts +9 -2
- package/hooks/adapter-common.sh +55 -2
- package/hooks/gate-10.sh +18 -3
- package/hooks/gate-13-compiler-strictness.sh +181 -0
- package/hooks/gate-3.sh +1 -1
- package/hooks/gate-4.sh +1 -1
- package/hooks/gate-9.sh +37 -2
- package/hooks/gate-registry.sh +161 -0
- package/hooks/lib/test-failure.sh +29 -4
- package/hooks/pre-commit +122 -3
- package/lib/__tests__/agent-runtime-journal.test.js +42 -18
- package/lib/__tests__/check-single-file.test.js +55 -4
- package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
- package/lib/__tests__/doctor-delphi.test.js +175 -0
- package/lib/__tests__/install-skill-full-dir.test.js +45 -2
- package/lib/__tests__/install-skill.test.js +84 -3
- package/lib/__tests__/mirror-sync.test.js +23 -5
- package/lib/__tests__/uninstall-global-residue.test.js +192 -28
- package/lib/__tests__/update-skill-flags.test.js +78 -0
- package/lib/__tests__/update-skill.test.js +156 -3
- package/lib/detect-deps.js +24 -0
- package/lib/doctor.js +80 -12
- package/lib/install-skill.js +67 -18
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/uninstall.js +96 -2
- package/lib/update-skill.js +143 -2
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/lib/index.d.ts.map +1 -1
- package/plugins/dsh/lib/index.js +7 -4
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/index.test.ts +25 -0
- package/plugins/dsh/src/index.ts +7 -4
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/principles/adapters/csharp.ts +309 -0
- package/principles/analyzer.ts +5 -0
- package/scripts/delphi-external-review.cjs +130 -101
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-specification-alignment/AGENTS.md +4 -4
package/hooks/gate-9.sh
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# ⚠️ DEAD CODE — NOT EXECUTED. Read this before changing anything here.
|
|
4
|
+
# ============================================================================
|
|
5
|
+
# This file is NOT sourced by githooks/pre-commit or githooks/pre-push (both
|
|
6
|
+
# source count = 0; verify with: grep -c "gate-9.sh" githooks/pre-commit).
|
|
7
|
+
# It is shipped by githooks/install.sh but never runs.
|
|
8
|
+
#
|
|
9
|
+
# The gate that actually runs semgrep SAST is githooks/gate-10.sh, and it
|
|
10
|
+
# reports as `gate10_sast`. Gate 9 in the report is `gate9_build_integrity`
|
|
11
|
+
# (Build Integrity), a different gate entirely.
|
|
12
|
+
#
|
|
13
|
+
# This mislabelling is not cosmetic: in #717 a C# SAST fix was first written
|
|
14
|
+
# into THIS file, where it changed nothing, while .cs stayed ungoverned. The
|
|
15
|
+
# C# ruleset loading that works lives in gate-10.sh. Do not relocate it here.
|
|
16
|
+
#
|
|
17
|
+
# Kept rather than deleted because githooks/install.sh:GATE_SCRIPTS ships it;
|
|
18
|
+
# removing it is a separate change. See issue #717 walkthrough MC-03.
|
|
19
|
+
# ============================================================================
|
|
1
20
|
# ============================================================================
|
|
2
21
|
# GATE 9: Semgrep SAST Security Scan
|
|
3
22
|
# Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
|
|
@@ -112,8 +131,22 @@ EOF
|
|
|
112
131
|
# code is demoted to "did the tool itself run". --error stays OFF: it would
|
|
113
132
|
# add nothing the JSON does not already provide, and keeping it off leaves
|
|
114
133
|
# failure exit codes unambiguous for the #475 diagnostics path.
|
|
134
|
+
# #717: p/security-audit carries NO C# rules. Measured: a .cs file with a
|
|
135
|
+
# textbook SQL injection (string-concatenated SqlCommand) and a
|
|
136
|
+
# Process.Start command injection produced 0 findings under
|
|
137
|
+
# p/security-audit and 1 ERROR under p/csharp. Since gate-9.sh:45 lists
|
|
138
|
+
# `cs` in the file filter, .cs was accepted and then scanned by a ruleset
|
|
139
|
+
# that cannot see C# -- the identical "filter looks like coverage" defect
|
|
140
|
+
# this issue removed from Gate 4. p/csharp is loaded IN ADDITION to
|
|
141
|
+
# p/security-audit (not instead of: the security-audit rules still apply
|
|
142
|
+
# to the other languages in a mixed commit).
|
|
143
|
+
SEMGREP_RULES=(--config=p/security-audit)
|
|
144
|
+
if printf "%s
|
|
145
|
+
" "$SEMGREP_FILES" | grep -qE ".cs$"; then
|
|
146
|
+
SEMGREP_RULES+=(--config=p/csharp)
|
|
147
|
+
fi
|
|
115
148
|
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
116
|
-
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan
|
|
149
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan "${SEMGREP_RULES[@]}" --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
117
150
|
SEMGREP_EXIT=$?
|
|
118
151
|
|
|
119
152
|
# One node pass over the JSON: first line = verdict counts, remaining lines =
|
|
@@ -191,7 +224,9 @@ console.log("");});
|
|
|
191
224
|
echo " ❌ BLOCKED — blocking-severity vulnerability found"
|
|
192
225
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
193
226
|
echo "$FINDING_DETAILS"
|
|
194
|
-
|
|
227
|
+
# #717: name BOTH rulesets. A C# finding comes from p/csharp, so
|
|
228
|
+
# pointing the author at p/security-audit alone reproduces zero hits.
|
|
229
|
+
echo " Run 'semgrep scan --config=p/security-audit --config=p/csharp' to review all findings."
|
|
195
230
|
GATE_9_STATUS="FAIL"
|
|
196
231
|
exit 1
|
|
197
232
|
else
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# GATE REGISTRY — the single source of truth for "which gate is which"
|
|
4
|
+
# ============================================================================
|
|
5
|
+
#
|
|
6
|
+
# WHY THIS FILE EXISTS
|
|
7
|
+
#
|
|
8
|
+
# Gate numbers alone are not self-describing, and the numbers are not stable
|
|
9
|
+
# facts about the repository — they are labels. Three sources disagreed about
|
|
10
|
+
# what "Gate 9" is:
|
|
11
|
+
#
|
|
12
|
+
# root AGENTS.md -> Semgrep SAST (WRONG)
|
|
13
|
+
# githooks/AGENTS.md -> Build Integrity (right)
|
|
14
|
+
# pre-commit actual -> Build Integrity (right)
|
|
15
|
+
#
|
|
16
|
+
# In issue #717 a C# SAST fix was written into githooks/gate-9.sh, because the
|
|
17
|
+
# root document said Gate 9 was SAST. SAST is gate 10. gate-9.sh is not sourced
|
|
18
|
+
# by any hook and never executes, so the fix changed nothing while appearing to
|
|
19
|
+
# land. The defect was not "the number was hard to remember" — it was that
|
|
20
|
+
# nothing connected a gate's NAME to the FILE that runs it.
|
|
21
|
+
#
|
|
22
|
+
# This registry makes that connection explicit and testable. The drift test
|
|
23
|
+
# (githooks/__tests__/gate-registry.test.bats) asserts every `impl` below
|
|
24
|
+
# against the code, so a document that disagrees with reality, or a fix that
|
|
25
|
+
# lands in a file nobody sources, fails immediately.
|
|
26
|
+
#
|
|
27
|
+
# COLUMNS
|
|
28
|
+
# name stable semantic id — use this in prose, commits, and tests
|
|
29
|
+
# number display label only. Mirrors the report key (gateN_*) for backward
|
|
30
|
+
# compatibility; do NOT treat it as the identity of the gate.
|
|
31
|
+
# impl where the gate actually runs. One of:
|
|
32
|
+
# <script>.sh a file sourced/executed by pre-commit
|
|
33
|
+
# inline implemented inside pre-commit itself
|
|
34
|
+
# blocks YES the gate can reject a commit
|
|
35
|
+
# NO the gate reports but cannot reject
|
|
36
|
+
#
|
|
37
|
+
# USAGE
|
|
38
|
+
# source githooks/gate-registry.sh
|
|
39
|
+
# gate_name_for_file gate-10.sh # -> sast
|
|
40
|
+
# gate_impl sast # -> gate-10.sh
|
|
41
|
+
# gate_number sast # -> 10
|
|
42
|
+
# gate_can_block sast # -> YES
|
|
43
|
+
# gate_registry_dump # -> one line per gate
|
|
44
|
+
#
|
|
45
|
+
# ⚠️ Adding a gate: add a row here AND update the drift test's expectations.
|
|
46
|
+
# A gate that exists only in pre-commit and not here is a gate nobody can
|
|
47
|
+
# look up by name — which is how #717's wrong-file patch happened.
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
# name|number|impl|blocks
|
|
51
|
+
GATE_REGISTRY_ROWS=(
|
|
52
|
+
"version-consistency|0|inline|YES"
|
|
53
|
+
"code-quality|1|inline|NO"
|
|
54
|
+
"duplicate-code|2|inline|NO"
|
|
55
|
+
"complexity|3|gate-3.sh|YES"
|
|
56
|
+
"principles|4|gate-4.sh|YES"
|
|
57
|
+
"tests-coverage|5|inline|YES"
|
|
58
|
+
"architecture|6|inline|YES"
|
|
59
|
+
"iac-security|7|gate-7.sh|YES"
|
|
60
|
+
"secret-scanning|8|gate-8.sh|YES"
|
|
61
|
+
"build-integrity|9|inline|NO"
|
|
62
|
+
"sast|10|gate-10.sh|YES"
|
|
63
|
+
"sprint-flow|11|sprint-gate.sh|YES"
|
|
64
|
+
"file-hygiene|12|gate-12-file-hygiene.sh|NO"
|
|
65
|
+
"compiler-strictness|13|gate-13-compiler-strictness.sh|YES"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Human-readable title for each name. Kept separate from the row above so the
|
|
69
|
+
# title can change without touching the lookup keys.
|
|
70
|
+
_gate_title() {
|
|
71
|
+
case "$1" in
|
|
72
|
+
version-consistency) echo "Version Consistency" ;;
|
|
73
|
+
code-quality) echo "Code Quality (Static + Lint)" ;;
|
|
74
|
+
duplicate-code) echo "Duplicate Code" ;;
|
|
75
|
+
complexity) echo "Cyclomatic Complexity" ;;
|
|
76
|
+
principles) echo "Clean Code + SOLID" ;;
|
|
77
|
+
tests-coverage) echo "Tests + Coverage" ;;
|
|
78
|
+
architecture) echo "Architecture + Boy Scout Rule" ;;
|
|
79
|
+
iac-security) echo "IaC Security Scanning" ;;
|
|
80
|
+
secret-scanning) echo "Secret Scanning" ;;
|
|
81
|
+
build-integrity) echo "Build Integrity" ;;
|
|
82
|
+
sast) echo "SAST Security Scan" ;;
|
|
83
|
+
sprint-flow) echo "Sprint Flow Enforcement" ;;
|
|
84
|
+
file-hygiene) echo "File Hygiene Check" ;;
|
|
85
|
+
compiler-strictness) echo "Compiler Strictness" ;;
|
|
86
|
+
*) return 1 ;;
|
|
87
|
+
esac
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Lookups. Each returns non-zero when the name is unknown, so a typo fails
|
|
92
|
+
# loudly instead of silently matching nothing.
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
# gate_row <name> -> echoes the raw row, or returns 1.
|
|
96
|
+
gate_row() {
|
|
97
|
+
local want="$1" row
|
|
98
|
+
for row in "${GATE_REGISTRY_ROWS[@]}"; do
|
|
99
|
+
case "$row" in
|
|
100
|
+
"$want"\|*) printf '%s\n' "$row"; return 0 ;;
|
|
101
|
+
esac
|
|
102
|
+
done
|
|
103
|
+
return 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# _gate_field <name> <1-based field index>
|
|
107
|
+
_gate_field() {
|
|
108
|
+
local row
|
|
109
|
+
row=$(gate_row "$1") || return 1
|
|
110
|
+
printf '%s\n' "$row" | cut -d'|' -f"$2"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
gate_number() { _gate_field "$1" 2; }
|
|
114
|
+
gate_impl() { _gate_field "$1" 3; }
|
|
115
|
+
gate_blocks() { _gate_field "$1" 4; }
|
|
116
|
+
gate_can_block() { [ "$(gate_blocks "$1")" = "YES" ] && echo YES || echo NO; }
|
|
117
|
+
gate_title() { _gate_title "$1"; }
|
|
118
|
+
|
|
119
|
+
# gate_name_for_number <N> -> echoes the semantic name.
|
|
120
|
+
gate_name_for_number() {
|
|
121
|
+
local want="$1" row
|
|
122
|
+
for row in "${GATE_REGISTRY_ROWS[@]}"; do
|
|
123
|
+
case "$row" in
|
|
124
|
+
*\|"$want"\|*) printf '%s\n' "${row%%|*}"; return 0 ;;
|
|
125
|
+
esac
|
|
126
|
+
done
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# gate_name_for_file <file> -> semantic name of the gate implemented by <file>.
|
|
131
|
+
# Returns 1 for `inline` gates and unknown files. This is the lookup that would
|
|
132
|
+
# have caught #717: gate-9.sh resolves to build-integrity, not sast.
|
|
133
|
+
gate_name_for_file() {
|
|
134
|
+
local want="$1" row impl name
|
|
135
|
+
for row in "${GATE_REGISTRY_ROWS[@]}"; do
|
|
136
|
+
name="${row%%|*}"
|
|
137
|
+
impl=$(printf '%s\n' "$row" | cut -d'|' -f3)
|
|
138
|
+
[ "$impl" = "$want" ] && { printf '%s\n' "$name"; return 0; }
|
|
139
|
+
done
|
|
140
|
+
return 1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# List every script the registry claims the hook runs.
|
|
144
|
+
gate_all_impls() {
|
|
145
|
+
local row impl
|
|
146
|
+
for row in "${GATE_REGISTRY_ROWS[@]}"; do
|
|
147
|
+
impl=$(printf '%s\n' "$row" | cut -d'|' -f3)
|
|
148
|
+
[ "$impl" != "inline" ] && printf '%s\n' "$impl"
|
|
149
|
+
done
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
gate_registry_dump() {
|
|
153
|
+
local row name num impl blocks
|
|
154
|
+
for row in "${GATE_REGISTRY_ROWS[@]}"; do
|
|
155
|
+
name="${row%%|*}"
|
|
156
|
+
num=$(printf '%s\n' "$row" | cut -d'|' -f2)
|
|
157
|
+
impl=$(printf '%s\n' "$row" | cut -d'|' -f3)
|
|
158
|
+
blocks=$(printf '%s\n' "$row" | cut -d'|' -f4)
|
|
159
|
+
printf '%-3s %-22s %-34s %-6s %s\n' "$num" "$name" "$(_gate_title "$name")" "$blocks" "$impl"
|
|
160
|
+
done
|
|
161
|
+
}
|
|
@@ -31,6 +31,20 @@ has_real_failure_marker() {
|
|
|
31
31
|
is_runner_infrastructure_error() {
|
|
32
32
|
local _out="$1"
|
|
33
33
|
[ -n "$_out" ] || return 1
|
|
34
|
+
# #717: dotnet/VSTest's testhost failing to START is the same class of event
|
|
35
|
+
# -- the runner could not run, so no test was judged. Its signature is a
|
|
36
|
+
# Win32Exception from ProcessManager.OpenProcess (the testhost cannot open a
|
|
37
|
+
# handle to its own parent) followed by "Testhost process exited with error".
|
|
38
|
+
# Without this arm, a .NET repo on a machine with that permission fault would
|
|
39
|
+
# report "Tests FAILED" on every commit for a reason that has nothing to do
|
|
40
|
+
# with its tests.
|
|
41
|
+
#
|
|
42
|
+
# A real dotnet failure ("Failed!", "Failed: 3") never carries these strings,
|
|
43
|
+
# so the has_real_failure_marker guard below still lets genuine failures block.
|
|
44
|
+
if printf '%s' "$_out" | grep -qE 'Win32Exception|Testhost.*(exited with error|已退出)|test host.*(could not|did not) start'; then
|
|
45
|
+
has_real_failure_marker "$_out" && return 1
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
34
48
|
printf '%s' "$_out" | grep -qE 'Unhandled Error' || return 1
|
|
35
49
|
has_real_failure_marker "$_out" && return 1
|
|
36
50
|
return 0
|
|
@@ -116,10 +130,21 @@ handle_test_failure() {
|
|
|
116
130
|
local _ctx="$2"
|
|
117
131
|
if is_runner_infrastructure_error "$_out"; then
|
|
118
132
|
echo ""
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
# The advice has to match the runner that failed (#717). A C# developer
|
|
134
|
+
# handed the vitest EPERM workaround would chase a cache problem in a
|
|
135
|
+
# language they are not using, and the real cause -- the testhost could not
|
|
136
|
+
# open a handle to its own parent -- would never be named.
|
|
137
|
+
if printf '%s' "$_out" | grep -qE 'Win32Exception|Testhost.*(exited with error|已退出)|test host.*(could not|did not) start'; then
|
|
138
|
+
echo "⏭️ SKIPPED - dotnet test host could not start (no test was judged)"
|
|
139
|
+
echo " Detected: VSTest testhost exited before running a test."
|
|
140
|
+
echo " This is an environment fault, NOT a test verdict -- it must not block."
|
|
141
|
+
echo " Check: antivirus / endpoint protection blocking process handles, or run the suite outside this path."
|
|
142
|
+
else
|
|
143
|
+
echo "⏭️ SKIPPED - vitest runner infrastructure error (no test failed)"
|
|
144
|
+
echo " Detected: Unhandled Errors with 0 failed tests."
|
|
145
|
+
echo " Known Windows issue: EPERM on vitest temp/ssr cache (#454)."
|
|
146
|
+
echo " Workaround: npx vitest run --no-file-parallelism <targets>"
|
|
147
|
+
fi
|
|
123
148
|
TESTS_SKIPPED=true
|
|
124
149
|
TESTS_EXIT_CODE=0
|
|
125
150
|
return 0
|
package/hooks/pre-commit
CHANGED
|
@@ -271,6 +271,17 @@ if [ -z "$GATE_DIR" ]; then
|
|
|
271
271
|
GATE_DIR="$ADAPTER_DIR"
|
|
272
272
|
fi
|
|
273
273
|
|
|
274
|
+
# Gate registry (#717 walkthrough): binds each gate's NAME to the file that
|
|
275
|
+
# runs it, so a gate can be referred to by what it does rather than by a
|
|
276
|
+
# positional number. Optional by design — a consumer repo installed from an
|
|
277
|
+
# older package will not have it, and the hooks must keep working there.
|
|
278
|
+
_GATE_REGISTRY="$GATE_DIR/gate-registry.sh"
|
|
279
|
+
[ -f "$_GATE_REGISTRY" ] || _GATE_REGISTRY="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/gate-registry.sh"
|
|
280
|
+
if [ -f "$_GATE_REGISTRY" ]; then
|
|
281
|
+
# shellcheck source=/dev/null
|
|
282
|
+
. "$_GATE_REGISTRY"
|
|
283
|
+
fi
|
|
284
|
+
|
|
274
285
|
# Trap: ensure quality report generated on ANY exit (pass or fail).
|
|
275
286
|
# Using a guard prevents recursion when 'exit' is called from inside the trap.
|
|
276
287
|
# IMPORTANT: Captures exit status FIRST so generate_quality_report cannot override it.
|
|
@@ -1267,6 +1278,14 @@ if [ -z "$PROJECT_LANGS" ] && [ -z "$_CONFIG_LANGS" ]; then
|
|
|
1267
1278
|
if [ "$(_detect_ext_lang "ps1" "psm1")" = "1" ]; then
|
|
1268
1279
|
PROJECT_LANGS="$PROJECT_LANGS powershell"
|
|
1269
1280
|
fi
|
|
1281
|
+
# #717: without this arm a staged .cs file never produced a `csharp` entry in
|
|
1282
|
+
# PROJECT_LANGS, so the per-language loop at the adapter-routing site had no
|
|
1283
|
+
# turn in which to resolve adapters/csharp.sh -- the adapter was unreachable
|
|
1284
|
+
# no matter how correct it was. Detection on extensions (not manifests) is why
|
|
1285
|
+
# one arm is enough: a repo staging .cs always lands here.
|
|
1286
|
+
if [ "$(_detect_ext_lang "cs" "csx")" = "1" ]; then
|
|
1287
|
+
PROJECT_LANGS="$PROJECT_LANGS csharp"
|
|
1288
|
+
fi
|
|
1270
1289
|
# IaC: .tf, .tfvars, Dockerfile, docker-compose*, .yaml/.yml in kube context
|
|
1271
1290
|
if echo "$CHANGED_FILES" | grep -qE '(\.tf$|\.tfvars$|Dockerfile|docker-compose)' 2>/dev/null; then
|
|
1272
1291
|
PROJECT_LANGS="$PROJECT_LANGS iac"
|
|
@@ -2256,7 +2275,18 @@ else
|
|
|
2256
2275
|
if [ "$LINT_EXIT" -ne 0 ]; then
|
|
2257
2276
|
echo "ℹ️ Lint check had issues (may be non-blocking)"
|
|
2258
2277
|
fi
|
|
2259
|
-
|
|
2278
|
+
|
|
2279
|
+
# #717: rc=2 is the adapters' "could not execute" channel (no toolchain,
|
|
2280
|
+
# or a tool that refused to start), distinct from rc=1 "ran and found
|
|
2281
|
+
# problems". Collapsing both into one SKIPPED line -- as this branch did
|
|
2282
|
+
# -- makes a permanently-broken analyzer indistinguishable from a clean
|
|
2283
|
+
# one, which is the silent-failure shape #717 is about. The gate still
|
|
2284
|
+
# does not block (tool-missing must not block), but the reason is now
|
|
2285
|
+
# named in the log instead of implied.
|
|
2286
|
+
if [ "$ANALYSIS_EXIT" -eq 2 ] || [ "$LINT_EXIT" -eq 2 ]; then
|
|
2287
|
+
echo "⏭️ SKIPPED - Code quality (${CURRENT_LANG} toolchain could not execute — NOT a pass)"
|
|
2288
|
+
fi
|
|
2289
|
+
|
|
2260
2290
|
if [ "$ANALYSIS_EXIT" -eq 0 ] && [ "$LINT_EXIT" -eq 0 ]; then
|
|
2261
2291
|
echo "✅ PASSED - Language-specific code quality checks."
|
|
2262
2292
|
else
|
|
@@ -4307,8 +4337,72 @@ JAVAARCHROOTS
|
|
|
4307
4337
|
fi
|
|
4308
4338
|
fi
|
|
4309
4339
|
;;
|
|
4340
|
+
"csharp")
|
|
4341
|
+
# #717: C# had no arm at all, so it fell through to the `*)` default
|
|
4342
|
+
# below and was reported as "not configured for csharp" -- a language
|
|
4343
|
+
# the gate silently could not measure, with no way to configure it
|
|
4344
|
+
# either, because the config file was never looked for.
|
|
4345
|
+
#
|
|
4346
|
+
# Architecture for .NET is normally expressed as *tests* (NetArchTest,
|
|
4347
|
+
# ArchUnitNET, or a plain xUnit project asserting layering). There is no
|
|
4348
|
+
# universal CLI, so this arm keys off a config the project opts into,
|
|
4349
|
+
# exactly like the Go arm's arch-go.yaml.
|
|
4350
|
+
if [ -f "arch-csharp.yaml" ] || [ -f ".xp-gate/arch-csharp.yaml" ]; then
|
|
4351
|
+
_cs_arch_cfg="arch-csharp.yaml"
|
|
4352
|
+
[ -f "$_cs_arch_cfg" ] || _cs_arch_cfg=".xp-gate/arch-csharp.yaml"
|
|
4353
|
+
# Find an architecture test project: a .csproj whose name or contents
|
|
4354
|
+
# mention architecture tests. Guessing one fixed path is the #602
|
|
4355
|
+
# defect, which reported "not configured" for repos that had them.
|
|
4356
|
+
_cs_arch_proj=""
|
|
4357
|
+
while IFS= read -r _p; do
|
|
4358
|
+
[ -n "$_p" ] || continue
|
|
4359
|
+
if grep -qiE "architecture|archunit|netarchtest" "$_p" 2>/dev/null; then
|
|
4360
|
+
_cs_arch_proj="$_p"
|
|
4361
|
+
break
|
|
4362
|
+
fi
|
|
4363
|
+
done <<CSARCHPROJ
|
|
4364
|
+
$(git ls-files '*.csproj' 2>/dev/null)
|
|
4365
|
+
CSARCHPROJ
|
|
4366
|
+
|
|
4367
|
+
if [ -z "$_cs_arch_proj" ]; then
|
|
4368
|
+
echo " ℹ️ No architecture test project found - skipping C# architecture"
|
|
4369
|
+
echo " ⏭️ SKIPPED - C# architecture validation (no arch test project)"
|
|
4370
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
4371
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_not_configured"
|
|
4372
|
+
fi
|
|
4373
|
+
elif ! command -v dotnet >/dev/null 2>&1; then
|
|
4374
|
+
echo " ⏭️ SKIPPED - C# architecture validation (dotnet SDK not installed)"
|
|
4375
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
4376
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="tool_missing"
|
|
4377
|
+
fi
|
|
4378
|
+
else
|
|
4379
|
+
echo " Running C# architecture tests ($_cs_arch_proj)..."
|
|
4380
|
+
# Same reading as the Go/Java arms (#630/#602): the checker's own
|
|
4381
|
+
# status decides the verdict. Piping into `tail` and printing
|
|
4382
|
+
# "completed" unconditionally is the fail-open shape those issues
|
|
4383
|
+
# removed; PIPESTATUS[0] is what keeps this arm from repeating it.
|
|
4384
|
+
dotnet test "$_cs_arch_proj" --nologo 2>&1 | tail -20
|
|
4385
|
+
_cs_arch_rc="${PIPESTATUS[0]}"
|
|
4386
|
+
if [ "$_cs_arch_rc" -ne 0 ]; then
|
|
4387
|
+
echo ""
|
|
4388
|
+
echo "❌ BLOCKED - C# architecture violations ($_cs_arch_proj, exit $_cs_arch_rc)"
|
|
4389
|
+
echo "Fix the layering violation(s) above before committing."
|
|
4390
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
4391
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_tests_failed"
|
|
4392
|
+
fi
|
|
4393
|
+
exit 1
|
|
4394
|
+
fi
|
|
4395
|
+
echo " ✅ C# architecture validation completed."
|
|
4396
|
+
fi
|
|
4397
|
+
else
|
|
4398
|
+
echo " ℹ️ No arch-csharp.yaml found - skipping C# architecture"
|
|
4399
|
+
echo " ⏭️ SKIPPED - C# architecture validation (no config)"
|
|
4400
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
4401
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_not_configured"
|
|
4402
|
+
fi
|
|
4403
|
+
fi
|
|
4404
|
+
;;
|
|
4310
4405
|
*)
|
|
4311
|
-
echo " ℹ️ Architecture validation not configured for $CURRENT_LANG"
|
|
4312
4406
|
echo " ⏭️ SKIPPED - Architecture validation (not configured for $CURRENT_LANG)"
|
|
4313
4407
|
# #537: archlint ships no detector for this language. Say so in a way
|
|
4314
4408
|
# the quality report can read, instead of leaving the dimension silent.
|
|
@@ -4987,6 +5081,19 @@ if [ -n "$MIRROR_STAGED" ] && printf '%s\n' "$MIRROR_STAGED" | grep -qE '^(githo
|
|
|
4987
5081
|
# each other, and the only way past was to edit a generated file by hand.
|
|
4988
5082
|
case "$_mf" in
|
|
4989
5083
|
*/__tests__/*) continue ;;
|
|
5084
|
+
# AGENTS.md is documentation, not a hook artifact: sync-package-content.js
|
|
5085
|
+
# copies the hook suite, and no AGENTS.md exists under hooks/. Demanding a
|
|
5086
|
+
# mirror for it reported a BLOCK on every doc edit (harmless only because
|
|
5087
|
+
# GATE_FAILED was never read -- see below).
|
|
5088
|
+
*/AGENTS.md) continue ;;
|
|
5089
|
+
# `githooks/adapters/` mirrors to `src/npm-package/adapters/`, NOT under
|
|
5090
|
+
# `hooks/` -- that is what scripts/check-hook-mirror.sh declares (line 6)
|
|
5091
|
+
# and what sync-package-content.js writes. The blanket `githooks/*` rule
|
|
5092
|
+
# below sent it to `src/npm-package/hooks/adapters/<f>`, a path that has
|
|
5093
|
+
# never existed, so ANY edit to an adapter was un-committable with
|
|
5094
|
+
# "has no mirror at". It stayed hidden because nobody had edited an
|
|
5095
|
+
# adapter since this gate was narrowed to diff files.
|
|
5096
|
+
githooks/adapters/*) _mm="src/npm-package/adapters/${_mf#githooks/adapters/}" ;;
|
|
4990
5097
|
githooks/*) _mm="src/npm-package/hooks/${_mf#githooks/}" ;;
|
|
4991
5098
|
plugins/*/src/*) _mm=$(printf '%s' "$_mf" | sed 's|^plugins/\([^/]*\)/src/|src/npm-package/plugins/\1/src/|') ;;
|
|
4992
5099
|
*) continue ;;
|
|
@@ -5006,7 +5113,15 @@ if [ -n "$MIRROR_STAGED" ] && printf '%s\n' "$MIRROR_STAGED" | grep -qE '^(githo
|
|
|
5006
5113
|
echo "❌ BLOCKED - Gate 12b: $MIRROR_FAIL mirror parity violation(s)."
|
|
5007
5114
|
echo " The mirror is what gets published; a mirror-only fix is silently"
|
|
5008
5115
|
echo " reverted by the next prepack (v0.27.3 shipped that bug)."
|
|
5009
|
-
GATE_FAILED=1
|
|
5116
|
+
# BLOCKS, and must actually block. This used to set GATE_FAILED=1, a
|
|
5117
|
+
# variable that appears exactly once in this file -- the assignment --
|
|
5118
|
+
# and is read nowhere, so the gate printed BLOCKED on every doc edit and
|
|
5119
|
+
# let the commit through. A gate that reports a verdict nobody consumes is
|
|
5120
|
+
# the same failure as a filter that accepts a file no adapter claims:
|
|
5121
|
+
# it looks like enforcement. exit is the contract every other blocking
|
|
5122
|
+
# gate here uses (see Gate 13: `exit "$_g13_worst"`).
|
|
5123
|
+
GATE_12_STATUS=WARN
|
|
5124
|
+
exit 1
|
|
5010
5125
|
else
|
|
5011
5126
|
echo "✅ PASSED - Gate 12b: mirror parity (touched files)"
|
|
5012
5127
|
fi
|
|
@@ -5061,6 +5176,10 @@ elif [ -f "$GATE_DIR/gate-13-compiler-strictness.sh" ] || [ -f "$(git rev-parse
|
|
|
5061
5176
|
|
|
5062
5177
|
gate13_run_cpp_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
5063
5178
|
gate13_run_java_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
5179
|
+
# #717: C# joins the same reduce-the-worst loop. Without this call the
|
|
5180
|
+
# function would exist but never execute -- the "silently skipped" shape
|
|
5181
|
+
# #717 is about, one level up.
|
|
5182
|
+
gate13_run_csharp_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
5064
5183
|
gate13_run_analyzer; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
5065
5184
|
|
|
5066
5185
|
exit "$_g13_worst"
|
|
@@ -58,27 +58,41 @@ function journalText(events) {
|
|
|
58
58
|
function toolPair(callId, name, args, startT, endT, result = {}, pos = {}) {
|
|
59
59
|
const turn = pos.turn ?? 1;
|
|
60
60
|
const step = pos.step ?? 1;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
const call = {
|
|
62
|
+
type: 'tool/call',
|
|
63
|
+
time: startT,
|
|
64
|
+
data: { turn, step, callId, name, arguments: JSON.stringify(args) },
|
|
65
|
+
};
|
|
66
|
+
// Built separately so the pair stays flat: inlining it put the event five
|
|
67
|
+
// objects deep, which trips clean-code.deep-nesting (max 4) on every run.
|
|
68
|
+
const reply = {
|
|
69
|
+
type: 'tool/result',
|
|
70
|
+
time: endT,
|
|
71
|
+
data: { turn, step, message: toolResultMessage(callId, result) },
|
|
72
|
+
};
|
|
73
|
+
return [call, reply];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The `message` payload of a tool/result event. */
|
|
77
|
+
function toolResultMessage(callId, result) {
|
|
78
|
+
return {
|
|
79
|
+
role: 'tool',
|
|
80
|
+
toolCallId: callId,
|
|
81
|
+
isError: !!result.isError,
|
|
82
|
+
content: [{ type: 'text', text: result.text ?? 'ok' }],
|
|
83
|
+
};
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
describe('parseSessionBuffer', () => {
|
|
81
|
-
|
|
87
|
+
// `zlib.zstdCompressSync` / `zstdDecompressSync` landed in Node 22. CI pins
|
|
88
|
+
// Node 20.x, so without this guard the test throws "zstdCompressSync is not a
|
|
89
|
+
// function" there and turns Gate 5 red on a machine difference rather than a
|
|
90
|
+
// code defect. Locally (Node 24) it passed, which is exactly why the gap
|
|
91
|
+
// survived. Feature-detected rather than version-sniffed, so a backport or a
|
|
92
|
+
// future rename cannot silently disable the coverage.
|
|
93
|
+
const hasZstd = typeof zlib.zstdCompressSync === 'function' && typeof zlib.zstdDecompressSync === 'function';
|
|
94
|
+
|
|
95
|
+
it.skipIf(!hasZstd)('reads a zstd-compressed session journal', () => {
|
|
82
96
|
const events = toolPair('c1', 'read', { file_path: '/a.ts' }, 1000, 1010);
|
|
83
97
|
const buf = zlib.zstdCompressSync(Buffer.from(journalText(events), 'utf8'));
|
|
84
98
|
const parsed = parseSessionBuffer(buf);
|
|
@@ -86,6 +100,16 @@ describe('parseSessionBuffer', () => {
|
|
|
86
100
|
expect(parsed[0].type).toBe('tool/call');
|
|
87
101
|
});
|
|
88
102
|
|
|
103
|
+
it('skips a zstd-magic buffer it cannot decode, rather than throwing', () => {
|
|
104
|
+
// The counterpart to the test above: on a runtime without zstd the magic
|
|
105
|
+
// number still gets recognised, `zstdDecompressSync` is missing, and the
|
|
106
|
+
// catch must degrade to "no events" instead of crashing the whole journal.
|
|
107
|
+
// This runs on every Node version, so the degraded path is never untested.
|
|
108
|
+
const magic = Buffer.from([0x28, 0xb5, 0x2f, 0xfd, 0x00, 0x00, 0x00, 0x00]);
|
|
109
|
+
expect(() => parseSessionBuffer(magic)).not.toThrow();
|
|
110
|
+
expect(parseSessionBuffer(magic)).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
89
113
|
it('reads an uncompressed journal too, so fixtures need no encoder', () => {
|
|
90
114
|
const events = toolPair('c1', 'read', { file_path: '/a.ts' }, 1000, 1010);
|
|
91
115
|
const parsed = parseSessionBuffer(Buffer.from(journalText(events), 'utf8'));
|
|
@@ -43,6 +43,32 @@ function runCheck(relPath) {
|
|
|
43
43
|
return { stdout, status, elapsed: Date.now() - started };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 量一次「同一 CLI 做最少的事」要多久——只解析参数并打印版本,不跑任何 gate。
|
|
48
|
+
* 用作 AC-653-05 的相对基线:Node 启动 + bin 模块加载 + 依赖解析的固有成本,
|
|
49
|
+
* 这部分与机器负载同步伸缩,减掉它才能量出 check 自己的开销。
|
|
50
|
+
*
|
|
51
|
+
* 取三次的最小值:最小值受负载尖峰影响最小,比平均值稳定得多。
|
|
52
|
+
*/
|
|
53
|
+
function measure(args) {
|
|
54
|
+
let best = Infinity;
|
|
55
|
+
for (let i = 0; i < 3; i += 1) {
|
|
56
|
+
const started = Date.now();
|
|
57
|
+
try {
|
|
58
|
+
execFileSync(process.execPath, [CLI, ...args], {
|
|
59
|
+
cwd: REPO,
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
timeout: 120000,
|
|
62
|
+
stdio: 'pipe',
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// --version 不该失败;真失败时这一次不计入,别把错误吞成 0ms。
|
|
66
|
+
}
|
|
67
|
+
best = Math.min(best, Date.now() - started);
|
|
68
|
+
}
|
|
69
|
+
return best;
|
|
70
|
+
}
|
|
71
|
+
|
|
46
72
|
describe('check --gates 单文件自检 (#653)', () => {
|
|
47
73
|
it('AC-653-04: 传文件路径时 gate 真的执行,而不是报 spawn 失败', () => {
|
|
48
74
|
const target = 'src/npm-package/lib/doctor-brief.js';
|
|
@@ -54,10 +80,35 @@ describe('check --gates 单文件自检 (#653)', () => {
|
|
|
54
80
|
expect(stdout).toMatch(/principles/i);
|
|
55
81
|
});
|
|
56
82
|
|
|
57
|
-
it('AC-653-05:
|
|
83
|
+
it('AC-653-05: 单文件自检的增量开销远小于一次完整 pre-commit', () => {
|
|
84
|
+
// #653 的价值主张:提交前自检要快过等一轮完整 pre-commit(几分钟)。
|
|
85
|
+
// "快路径"如果慢就不叫快路径。
|
|
86
|
+
//
|
|
87
|
+
// 但这里不能用绝对墙钟阈值。原实现写 `elapsed < 2000`,实测同一台机器:
|
|
88
|
+
// 裸跑 `node bin/xp-gate.js check ...` 654–822ms(远快于阈值)
|
|
89
|
+
// 在 vitest 里跑同一命令 2191–2352ms(偶尔超阈值)
|
|
90
|
+
// 它测的是「机器此刻有多空」,不是「这个功能够不够快」——并行跑测试时翻红、
|
|
91
|
+
// 空闲时变绿,红绿与功能无关(#722)。一个随负载漂移的断言,既不能证明快,
|
|
92
|
+
// 也不能证明慢。
|
|
93
|
+
//
|
|
94
|
+
// 改成相对判定:先用 `--version` 量出「同一 CLI、同一 Node 启动、同一份模块
|
|
95
|
+
// 加载」的固有成本,再看 check 是它的几倍。两者对机器快慢同步伸缩,比值因此
|
|
96
|
+
// 稳定。实测:
|
|
97
|
+
// 空闲 check 852ms / base 172ms = 4.95x
|
|
98
|
+
// 重负载 check 2563ms / base 445ms = 5.76x (4 个 CPU 密集进程并行)
|
|
99
|
+
// 比值只涨 16%,而绝对时间涨 3 倍——所以比值才是与负载无关的那个量。
|
|
100
|
+
//
|
|
101
|
+
// 门槛取 12x:它意味着 check 花的时间不超过「启动 CLI 本身」的一打倍——
|
|
102
|
+
// 一次完整 pre-commit 是分钟级,12x 启动成本仍是亚秒到一秒级,离「慢」很远;
|
|
103
|
+
// 同时留足余量,不会因为机器一时抖动就翻红。
|
|
104
|
+
const baseline = measure(['--version']);
|
|
58
105
|
const { elapsed } = runCheck('src/npm-package/lib/doctor-brief.js');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
expect(
|
|
106
|
+
const ratio = elapsed / baseline;
|
|
107
|
+
|
|
108
|
+
expect(ratio).toBeLessThan(12);
|
|
109
|
+
// 翻红时把三个数字一起打出来,能立刻分辨是「基线被拖慢」还是「check 真变慢」。
|
|
110
|
+
expect(
|
|
111
|
+
`${elapsed}ms / ${baseline}ms = ${ratio.toFixed(1)}x`,
|
|
112
|
+
).toMatch(/^\d+ms \/ \d+ms = [\d.]+x$/);
|
|
62
113
|
});
|
|
63
114
|
});
|
|
@@ -104,3 +104,46 @@ describe('PLATFORM_PROFILES covers dsh (#635)', () => {
|
|
|
104
104
|
expect(getSkillsDirs('claude-code')[0]).toBe(path.join(tmpHome, '.claude', 'skills'));
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @test REQ-725
|
|
110
|
+
* @intent 多平台共存时 detectPlatform() 按优先级短路返回单个平台,install-skill/
|
|
111
|
+
* update-skill 与 doctor 都只拿它选中的目录,其余平台(含 detectPlatform()
|
|
112
|
+
* 从不返回的 dsh)被永久冻结。detectInstalledPlatforms() 是「检测所有已安装
|
|
113
|
+
* agent」的原始能力:按 PLATFORM_PROFILES 里目录真实存在的平台返回全集。
|
|
114
|
+
* @covers AC-725-01, AC-725-06
|
|
115
|
+
*/
|
|
116
|
+
describe('detectInstalledPlatforms (#725 detect-all)', () => {
|
|
117
|
+
let tmpHome;
|
|
118
|
+
let originalHome;
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
originalHome = process.env.HOME;
|
|
122
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-detectall-'));
|
|
123
|
+
process.env.HOME = tmpHome;
|
|
124
|
+
vi.resetModules();
|
|
125
|
+
delete require.cache[require.resolve('../detect-deps')];
|
|
126
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
process.env.HOME = originalHome;
|
|
131
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns every platform whose directory exists, including dsh', () => {
|
|
135
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder'), { recursive: true });
|
|
136
|
+
fs.mkdirSync(path.join(tmpHome, '.config', 'opencode'), { recursive: true });
|
|
137
|
+
fs.mkdirSync(path.join(tmpHome, '.dsh'), { recursive: true });
|
|
138
|
+
|
|
139
|
+
const { detectInstalledPlatforms } = require('../detect-deps');
|
|
140
|
+
const installed = detectInstalledPlatforms();
|
|
141
|
+
expect(installed).toEqual(expect.arrayContaining(['opencode', 'qoder', 'dsh']));
|
|
142
|
+
expect(installed).not.toContain('claude-code');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns an empty list when no platform directory exists', () => {
|
|
146
|
+
const { detectInstalledPlatforms } = require('../detect-deps');
|
|
147
|
+
expect(detectInstalledPlatforms()).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
});
|