@boyingliu01/xp-gate 0.9.1 → 0.9.3
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/cpp.sh +3 -3
- package/adapters/dart.sh +2 -2
- package/adapters/flutter.sh +2 -2
- package/adapters/java.sh +4 -4
- package/adapters/kotlin.sh +4 -4
- package/adapters/objectivec.sh +2 -2
- package/adapters/plugins/p3c-java/scripts/install-maven-p3c.sh +3 -3
- package/adapters/plugins/whalecloud-java/scripts/install-maven-whalecloud.sh +3 -3
- package/adapters/powershell.sh +3 -3
- package/adapters/python.sh +2 -2
- package/adapters/shell.sh +3 -3
- package/adapters/swift.sh +2 -2
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/__tests__/tui-plugin.test.ts +306 -0
- package/plugins/opencode/__tests__/xp-gate-update.test.ts +255 -0
- package/plugins/opencode/index.ts +158 -75
- package/plugins/opencode/package.json +14 -3
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/tsconfig.json +1 -1
- package/plugins/opencode/tui-plugin.ts +188 -0
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
package/adapters/cpp.sh
CHANGED
|
@@ -13,12 +13,12 @@ _detect_cpp_build() {
|
|
|
13
13
|
run_static_analysis() {
|
|
14
14
|
if command -v clang-tidy &>/dev/null; then
|
|
15
15
|
local files
|
|
16
|
-
files=$(find . -name "*.cpp" -o -name "*.cc" -o -name "*.c" -o -name "*.h" 2>/dev/null |
|
|
16
|
+
files=$(find . -name "*.cpp" -o -name "*.cc" -o -name "*.c" -o -name "*.h" 2>/dev/null | sed -n '1,20p; 20q')
|
|
17
17
|
if [ -n "$files" ]; then
|
|
18
|
-
echo "$files" | xargs clang-tidy 2>&1 |
|
|
18
|
+
echo "$files" | xargs clang-tidy 2>&1 | sed -n '1,20p; 20q'
|
|
19
19
|
fi
|
|
20
20
|
elif command -v cppcheck &>/dev/null; then
|
|
21
|
-
cppcheck --enable=all --inline-suppr . 2>&1 |
|
|
21
|
+
cppcheck --enable=all --inline-suppr . 2>&1 | sed -n '1,20p; 20q'
|
|
22
22
|
else
|
|
23
23
|
echo "No C++ analysis tools (clang-tidy/cppcheck)"
|
|
24
24
|
return 1
|
package/adapters/dart.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
run_static_analysis() {
|
|
4
4
|
if command -v dart &>/dev/null; then
|
|
5
|
-
dart analyze 2>&1 |
|
|
5
|
+
dart analyze 2>&1 | sed -n '1,30p; 30q'
|
|
6
6
|
return $?
|
|
7
7
|
else
|
|
8
8
|
echo "Dart SDK not available"
|
|
@@ -12,7 +12,7 @@ run_static_analysis() {
|
|
|
12
12
|
|
|
13
13
|
run_lint() {
|
|
14
14
|
if command -v dart &>/dev/null; then
|
|
15
|
-
dart format --output=none --set-exit-if-changed . 2>&1 |
|
|
15
|
+
dart format --output=none --set-exit-if-changed . 2>&1 | sed -n '1,30p; 30q'
|
|
16
16
|
return $?
|
|
17
17
|
else
|
|
18
18
|
echo "Dart SDK not available"
|
package/adapters/flutter.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
run_static_analysis() {
|
|
4
4
|
if command -v flutter &>/dev/null; then
|
|
5
|
-
flutter analyze 2>&1 |
|
|
5
|
+
flutter analyze 2>&1 | sed -n '1,30p; 30q'
|
|
6
6
|
return $?
|
|
7
7
|
else
|
|
8
8
|
echo "Flutter SDK not available"
|
|
@@ -12,7 +12,7 @@ run_static_analysis() {
|
|
|
12
12
|
|
|
13
13
|
run_lint() {
|
|
14
14
|
if command -v flutter &>/dev/null; then
|
|
15
|
-
flutter format --output=none --set-exit-if-changed . 2>&1 |
|
|
15
|
+
flutter format --output=none --set-exit-if-changed . 2>&1 | sed -n '1,30p; 30q'
|
|
16
16
|
return $?
|
|
17
17
|
else
|
|
18
18
|
echo "Flutter SDK not available"
|
package/adapters/java.sh
CHANGED
|
@@ -27,13 +27,13 @@ run_static_analysis() {
|
|
|
27
27
|
build_system=$(_detect_java_build)
|
|
28
28
|
|
|
29
29
|
if [ "$build_system" = "maven" ]; then
|
|
30
|
-
mvn compile -q 2>&1 | grep -i "error\|fail" |
|
|
30
|
+
mvn compile -q 2>&1 | grep -i "error\|fail" | sed -n '1,5p; 5q'
|
|
31
31
|
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
|
32
32
|
echo "❌ Maven compilation failed"
|
|
33
33
|
return 1
|
|
34
34
|
fi
|
|
35
35
|
elif [ "$build_system" = "gradle" ]; then
|
|
36
|
-
gradle compileJava --quiet 2>&1 | grep -i "error\|fail" |
|
|
36
|
+
gradle compileJava --quiet 2>&1 | grep -i "error\|fail" | sed -n '1,5p; 5q'
|
|
37
37
|
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
|
38
38
|
echo "❌ Gradle compilation failed"
|
|
39
39
|
return 1
|
|
@@ -42,12 +42,12 @@ run_static_analysis() {
|
|
|
42
42
|
|
|
43
43
|
# CheckStyle with Google style (legacy fallback)
|
|
44
44
|
if command -v checkstyle &>/dev/null; then
|
|
45
|
-
checkstyle -c /google_checks.xml . 2>&1 |
|
|
45
|
+
checkstyle -c /google_checks.xml . 2>&1 | sed -n '1,20p; 20q'
|
|
46
46
|
fi
|
|
47
47
|
|
|
48
48
|
# PMD error detection (legacy fallback)
|
|
49
49
|
if command -v pmd &>/dev/null; then
|
|
50
|
-
pmd check -d . -R category/java/errorprone.xml 2>&1 |
|
|
50
|
+
pmd check -d . -R category/java/errorprone.xml 2>&1 | sed -n '1,20p; 20q'
|
|
51
51
|
fi
|
|
52
52
|
|
|
53
53
|
# p3c-pmd check (Alibaba coding guidelines) — primary Java quality gate
|
package/adapters/kotlin.sh
CHANGED
|
@@ -15,13 +15,13 @@ run_static_analysis() {
|
|
|
15
15
|
build_system=$(_detect_kotlin_build)
|
|
16
16
|
|
|
17
17
|
if [ "$build_system" = "maven" ]; then
|
|
18
|
-
mvn compile -q 2>&1 | grep -i "error\|fail" |
|
|
18
|
+
mvn compile -q 2>&1 | grep -i "error\|fail" | sed -n '1,5p; 5q'
|
|
19
19
|
if [ ${PIPESTATUS[0]} -ne 0 ]; then
|
|
20
20
|
echo "❌ Maven compilation failed"
|
|
21
21
|
return 1
|
|
22
22
|
fi
|
|
23
23
|
elif [ "$build_system" = "gradle" ]; then
|
|
24
|
-
gradle compileKotlin --quiet 2>&1 | grep -i "error\|fail" |
|
|
24
|
+
gradle compileKotlin --quiet 2>&1 | grep -i "error\|fail" | sed -n '1,5p; 5q'
|
|
25
25
|
if [ ${PIPESTATUS[0]} -ne 0 ]; then
|
|
26
26
|
echo "❌ Gradle compilation failed"
|
|
27
27
|
return 1
|
|
@@ -29,10 +29,10 @@ run_static_analysis() {
|
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
31
|
if command -v ktlint &>/dev/null; then
|
|
32
|
-
ktlint "**/*.kt" 2>&1 |
|
|
32
|
+
ktlint "**/*.kt" 2>&1 | sed -n '1,20p; 20q'
|
|
33
33
|
return $?
|
|
34
34
|
elif command -v detekt &>/dev/null; then
|
|
35
|
-
detekt --input . 2>&1 |
|
|
35
|
+
detekt --input . 2>&1 | sed -n '1,20p; 20q'
|
|
36
36
|
return $?
|
|
37
37
|
else
|
|
38
38
|
echo "No Kotlin lint tools (ktlint/detekt)"
|
package/adapters/objectivec.sh
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
run_static_analysis() {
|
|
4
4
|
if command -v oclint &>/dev/null; then
|
|
5
|
-
oclint-json-compilation-database 2>&1 |
|
|
5
|
+
oclint-json-compilation-database 2>&1 | sed -n '1,30p; 30q'
|
|
6
6
|
return $?
|
|
7
7
|
elif command -v clang-tidy &>/dev/null; then
|
|
8
|
-
find . -name "*.m" -o -name "*.mm" -o -name "*.h" |
|
|
8
|
+
find . -name "*.m" -o -name "*.mm" -o -name "*.h" | sed -n '1,20p; 20q' | xargs clang-tidy 2>&1 | sed -n '1,30p; 30q'
|
|
9
9
|
return $?
|
|
10
10
|
else
|
|
11
11
|
echo "No Objective-C analysis tools available (oclint/clang-tidy)"
|
|
@@ -46,10 +46,10 @@ if grep -q '<profiles>' pom.xml; then
|
|
|
46
46
|
|
|
47
47
|
# Use sed to insert before </profiles>
|
|
48
48
|
# Find the line number of </profiles>
|
|
49
|
-
PROFILES_END_LINE=$(grep -n '</profiles>' pom.xml |
|
|
49
|
+
PROFILES_END_LINE=$(grep -n '</profiles>' pom.xml | sed -n '1p; 1q' | cut -d: -f1)
|
|
50
50
|
|
|
51
51
|
if [ -n "$PROFILES_END_LINE" ]; then
|
|
52
|
-
|
|
52
|
+
sed -n "1,$((PROFILES_END_LINE - 1))p; $((PROFILES_END_LINE - 1))q" pom.xml > pom.xml.tmp
|
|
53
53
|
{
|
|
54
54
|
echo ""
|
|
55
55
|
echo " <!-- XP-Gate: Alibaba p3c-pmd quality gate profile -->"
|
|
@@ -66,7 +66,7 @@ else
|
|
|
66
66
|
PROJECT_END_LINE=$(grep -n '</project>' pom.xml | tail -1 | cut -d: -f1)
|
|
67
67
|
|
|
68
68
|
if [ -n "$PROJECT_END_LINE" ]; then
|
|
69
|
-
|
|
69
|
+
sed -n "1,$((PROJECT_END_LINE - 1))p; $((PROJECT_END_LINE - 1))q" pom.xml > pom.xml.tmp
|
|
70
70
|
{
|
|
71
71
|
echo ""
|
|
72
72
|
echo " <!-- XP-Gate: Alibaba p3c-pmd quality gate profile -->"
|
|
@@ -154,9 +154,9 @@ else
|
|
|
154
154
|
echo "Step 2: Installing xp-gate-whalecloud-java profile into pom.xml..."
|
|
155
155
|
|
|
156
156
|
if grep -q '<profiles>' pom.xml; then
|
|
157
|
-
PROFILES_END_LINE=$(grep -n '</profiles>' pom.xml |
|
|
157
|
+
PROFILES_END_LINE=$(grep -n '</profiles>' pom.xml | sed -n '1p; 1q' | cut -d: -f1)
|
|
158
158
|
if [ -n "$PROFILES_END_LINE" ]; then
|
|
159
|
-
|
|
159
|
+
sed -n "1,$((PROFILES_END_LINE - 1))p; $((PROFILES_END_LINE - 1))q" pom.xml > pom.xml.tmp
|
|
160
160
|
{
|
|
161
161
|
echo ""
|
|
162
162
|
echo " <!-- XP-Gate: WhaleCloud Java Coding Standards profile -->"
|
|
@@ -169,7 +169,7 @@ else
|
|
|
169
169
|
else
|
|
170
170
|
PROJECT_END_LINE=$(grep -n '</project>' pom.xml | tail -1 | cut -d: -f1)
|
|
171
171
|
if [ -n "$PROJECT_END_LINE" ]; then
|
|
172
|
-
|
|
172
|
+
sed -n "1,$((PROJECT_END_LINE - 1))p; $((PROJECT_END_LINE - 1))q" pom.xml > pom.xml.tmp
|
|
173
173
|
{
|
|
174
174
|
echo ""
|
|
175
175
|
echo " <!-- XP-Gate: WhaleCloud Java Coding Standards profile -->"
|
package/adapters/powershell.sh
CHANGED
|
@@ -72,7 +72,7 @@ run_tests() {
|
|
|
72
72
|
test_paths="test/"
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
-
if [ -n "$test_paths" ] && find "$test_paths" -name "*.Tests.ps1" -type f 2>/dev/null |
|
|
75
|
+
if [ -n "$test_paths" ] && find "$test_paths" -name "*.Tests.ps1" -type f 2>/dev/null | sed -n '1p; 1q' | grep -q "."; then
|
|
76
76
|
echo "Running Pester tests..."
|
|
77
77
|
"$PWSH" -NoProfile -Command "
|
|
78
78
|
\$results = Invoke-Pester -Path '$test_paths' -PassThru
|
|
@@ -84,7 +84,7 @@ run_tests() {
|
|
|
84
84
|
exit 0
|
|
85
85
|
"
|
|
86
86
|
return $?
|
|
87
|
-
elif find . -maxdepth 2 -name "*.Tests.ps1" -type f 2>/dev/null |
|
|
87
|
+
elif find . -maxdepth 2 -name "*.Tests.ps1" -type f 2>/dev/null | sed -n '1p; 1q' | grep -q "."; then
|
|
88
88
|
echo "Running Pester tests in current directory..."
|
|
89
89
|
"$PWSH" -NoProfile -Command "
|
|
90
90
|
\$results = Invoke-Pester -CI -PassThru
|
|
@@ -117,7 +117,7 @@ run_coverage() {
|
|
|
117
117
|
test_paths="test/"
|
|
118
118
|
fi
|
|
119
119
|
|
|
120
|
-
if [ -n "$test_paths" ] || find . -maxdepth 2 -name "*.Tests.ps1" -type f 2>/dev/null |
|
|
120
|
+
if [ -n "$test_paths" ] || find . -maxdepth 2 -name "*.Tests.ps1" -type f 2>/dev/null | sed -n '1p; 1q' | grep -q "."; then
|
|
121
121
|
echo "Running Pester with code coverage..."
|
|
122
122
|
local path_arg="${test_paths:-.}"
|
|
123
123
|
"$PWSH" -NoProfile -Command "
|
package/adapters/python.sh
CHANGED
|
@@ -63,7 +63,7 @@ run_tests() {
|
|
|
63
63
|
fi
|
|
64
64
|
|
|
65
65
|
# Check for errors in summary line (e.g. "3 errors")
|
|
66
|
-
ERROR_COUNT=$(echo "$PYTEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' |
|
|
66
|
+
ERROR_COUNT=$(echo "$PYTEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' | sed -n '1p; 1q')
|
|
67
67
|
if [ -n "$ERROR_COUNT" ] && [ "$ERROR_COUNT" -gt 0 ]; then
|
|
68
68
|
echo "❌ $ERROR_COUNT test collection/execution errors detected"
|
|
69
69
|
return 1
|
|
@@ -94,7 +94,7 @@ run_coverage() {
|
|
|
94
94
|
fi
|
|
95
95
|
|
|
96
96
|
# Check for errors in summary line (e.g. "3 errors")
|
|
97
|
-
ERROR_COUNT=$(echo "$PYTEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' |
|
|
97
|
+
ERROR_COUNT=$(echo "$PYTEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' | sed -n '1p; 1q')
|
|
98
98
|
if [ -n "$ERROR_COUNT" ] && [ "$ERROR_COUNT" -gt 0 ]; then
|
|
99
99
|
echo "❌ $ERROR_COUNT test collection/execution errors detected"
|
|
100
100
|
return 1
|
package/adapters/shell.sh
CHANGED
|
@@ -26,13 +26,13 @@ run_lint() {
|
|
|
26
26
|
|
|
27
27
|
run_tests() {
|
|
28
28
|
# Look for and run shell test files
|
|
29
|
-
if [
|
|
29
|
+
if [ -d "tests" ] && [ -n "$(find tests -name "*.sh" -type f | sed -n '1p; 1q')" ]; then
|
|
30
30
|
echo "Running Shell tests..."
|
|
31
31
|
for test_file in tests/*.sh; do
|
|
32
|
-
if [
|
|
32
|
+
if [ -x "$test_file" ]; then
|
|
33
33
|
"$test_file"
|
|
34
34
|
local exit_code=$?
|
|
35
|
-
if [
|
|
35
|
+
if [ "$exit_code" -ne 0 ]; then
|
|
36
36
|
return $exit_code
|
|
37
37
|
fi
|
|
38
38
|
fi
|
package/adapters/swift.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
run_static_analysis() {
|
|
4
4
|
if command -v swiftlint &>/dev/null; then
|
|
5
|
-
swiftlint lint 2>&1 |
|
|
5
|
+
swiftlint lint 2>&1 | sed -n '1,30p; 30q'
|
|
6
6
|
return $?
|
|
7
7
|
else
|
|
8
8
|
echo "SwiftLint not available"
|
|
@@ -12,7 +12,7 @@ run_static_analysis() {
|
|
|
12
12
|
|
|
13
13
|
run_lint() {
|
|
14
14
|
if command -v swiftlint &>/dev/null; then
|
|
15
|
-
swiftlint lint --strict 2>&1 |
|
|
15
|
+
swiftlint lint --strict 2>&1 | sed -n '1,30p; 30q'
|
|
16
16
|
return $?
|
|
17
17
|
else
|
|
18
18
|
echo "SwiftLint not available"
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-06-18
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** b67fc85
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-06-18
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** b67fc85
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-06-18
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** b67fc85
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-06-18
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** b67fc85
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-06-18
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** b67fc85
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.3.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Slot Plugin tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for pure functions:
|
|
5
|
+
* - readSprintState: file I/O + JSON parse
|
|
6
|
+
* - isStale: staleness detection
|
|
7
|
+
* - statusSymbol: status → icon mapping
|
|
8
|
+
* - renderPhaseLine: single phase → formatted line
|
|
9
|
+
* - renderSprintSidebar: full state → sidebar string
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, before, after } from "node:test"
|
|
13
|
+
import assert from "node:assert/strict"
|
|
14
|
+
import { randomUUID } from "node:crypto"
|
|
15
|
+
import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"
|
|
16
|
+
import { join } from "node:path"
|
|
17
|
+
import { tmpdir } from "node:os"
|
|
18
|
+
|
|
19
|
+
// Import the functions under test
|
|
20
|
+
// Since the file uses type-only imports from @opencode-ai/plugin/tui,
|
|
21
|
+
// test runner provides a separate copy of the pure functions
|
|
22
|
+
import { readSprintState, renderSprintSidebar } from "../tui-plugin.ts"
|
|
23
|
+
|
|
24
|
+
// Duplicate helpers here to test in isolation
|
|
25
|
+
function isStale(state: { started_at?: string; phase_history?: Array<{ started_at?: string; completed_at?: string }> }): boolean {
|
|
26
|
+
if (!state || !state.started_at) return false
|
|
27
|
+
const started = new Date(state.started_at).getTime()
|
|
28
|
+
if (isNaN(started)) return false
|
|
29
|
+
let latest = started
|
|
30
|
+
if (Array.isArray(state.phase_history)) {
|
|
31
|
+
for (const ph of state.phase_history) {
|
|
32
|
+
if (ph.completed_at) {
|
|
33
|
+
const t = new Date(ph.completed_at).getTime()
|
|
34
|
+
if (!isNaN(t) && t > latest) latest = t
|
|
35
|
+
}
|
|
36
|
+
if (ph.started_at) {
|
|
37
|
+
const t = new Date(ph.started_at).getTime()
|
|
38
|
+
if (!isNaN(t) && t > latest) latest = t
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return Date.now() - latest > 3_600_000
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function statusSymbol(status: string | undefined, key: string, currentPhase: string | number | undefined): string {
|
|
46
|
+
if (status === "completed") return "✓"
|
|
47
|
+
if (status === "in_progress") return "→"
|
|
48
|
+
if (String(currentPhase) === key) return "·"
|
|
49
|
+
return "○"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const PHASE_NAMES: Record<string, string> = {
|
|
53
|
+
"-1": "ISOLATE",
|
|
54
|
+
"-0.5": "AUTO-ESTIMATE",
|
|
55
|
+
"0": "THINK",
|
|
56
|
+
"1": "PLAN",
|
|
57
|
+
"2": "BUILD",
|
|
58
|
+
"3": "REVIEW",
|
|
59
|
+
"4": "USER ACCEPT",
|
|
60
|
+
"5": "FEEDBACK",
|
|
61
|
+
"6": "SHIP",
|
|
62
|
+
"7": "LAND",
|
|
63
|
+
"8": "CLEANUP",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderPhaseLine(key: string, history: { status?: string; phase_name?: string } | undefined, currentPhase: string | number | undefined): string {
|
|
67
|
+
const name = history?.phase_name || PHASE_NAMES[key] || key
|
|
68
|
+
const status = history?.status || (String(currentPhase) === key ? "in_progress" : "pending")
|
|
69
|
+
const sym = statusSymbol(status, key, currentPhase)
|
|
70
|
+
return `${sym} ${name.padEnd(14)} ${status === "completed" ? "done" : status === "in_progress" ? "active" : ""}`
|
|
71
|
+
.replace(/\s+$/, "")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Test fixtures ──
|
|
75
|
+
|
|
76
|
+
function makeSprintState(overrides: Record<string, unknown> = {}) {
|
|
77
|
+
const base = {
|
|
78
|
+
id: "sprint-001",
|
|
79
|
+
phase: "2",
|
|
80
|
+
status: "in_progress",
|
|
81
|
+
started_at: new Date(Date.now() - 3_600_000).toISOString(),
|
|
82
|
+
task_description: "Implement user login with OAuth2",
|
|
83
|
+
metrics: { tests_passed: 42, coverage_pct: 87 },
|
|
84
|
+
phase_history: [
|
|
85
|
+
{ phase: "-1", phase_name: "ISOLATE", status: "completed" as const, started_at: new Date(Date.now() - 86_400_000).toISOString(), completed_at: new Date(Date.now() - 86_000_000).toISOString() },
|
|
86
|
+
{ phase: "0", status: "completed" as const, started_at: new Date(Date.now() - 86_000_000).toISOString(), completed_at: new Date(Date.now() - 72_000_000).toISOString() },
|
|
87
|
+
{ phase: "1", status: "completed" as const, started_at: new Date(Date.now() - 72_000_000).toISOString(), completed_at: new Date(Date.now() - 36_000_000).toISOString() },
|
|
88
|
+
{ phase: "2", status: "in_progress" as const, reqs: { "REQ-001": { name: "JWT auth", status: "completed" as const }, "REQ-002": { name: "OAuth2 flow", status: "in_progress" as const } } },
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
return { ...base, ...overrides } as any
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── isStale ──
|
|
95
|
+
|
|
96
|
+
void describe("isStale", () => {
|
|
97
|
+
void it("returns false when state has no started_at", () => {
|
|
98
|
+
assert.equal(isStale({}), false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
void it("returns false when started_at is recent", () => {
|
|
102
|
+
const state = { started_at: new Date().toISOString() }
|
|
103
|
+
assert.equal(isStale(state), false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
void it("returns true when started_at is >1h ago and no phase_history", () => {
|
|
107
|
+
const state = { started_at: new Date(Date.now() - 7_200_000).toISOString() }
|
|
108
|
+
assert.equal(isStale(state), true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
void it("returns false when latest phase_history is recent", () => {
|
|
112
|
+
const state = {
|
|
113
|
+
started_at: new Date(Date.now() - 7_200_000).toISOString(),
|
|
114
|
+
phase_history: [
|
|
115
|
+
{ phase: "0", started_at: new Date(Date.now() - 3_600_000).toISOString(), completed_at: new Date(Date.now() - 60_000).toISOString() },
|
|
116
|
+
],
|
|
117
|
+
}
|
|
118
|
+
assert.equal(isStale(state), false)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
void it("returns true when all activity is >1h ago", () => {
|
|
122
|
+
const state = {
|
|
123
|
+
started_at: new Date(Date.now() - 86_400_000).toISOString(),
|
|
124
|
+
phase_history: [
|
|
125
|
+
{ phase: "-1", started_at: new Date(Date.now() - 86_000_000).toISOString(), completed_at: new Date(Date.now() - 85_000_000).toISOString() },
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
assert.equal(isStale(state), true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
void it("returns false when started_at is invalid date", () => {
|
|
132
|
+
const state = { started_at: "not-a-date" }
|
|
133
|
+
assert.equal(isStale(state), false)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// ── statusSymbol ──
|
|
138
|
+
|
|
139
|
+
void describe("statusSymbol", () => {
|
|
140
|
+
void it("returns ✓ for completed", () => {
|
|
141
|
+
assert.equal(statusSymbol("completed", "0", undefined), "✓")
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
void it("returns → for in_progress", () => {
|
|
145
|
+
assert.equal(statusSymbol("in_progress", "1", undefined), "→")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
void it("returns · when key matches currentPhase", () => {
|
|
149
|
+
assert.equal(statusSymbol(undefined, "2", "2"), "·")
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
void it("returns ○ for pending (no match)", () => {
|
|
153
|
+
assert.equal(statusSymbol("pending", "3", "2"), "○")
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
void it("returns ✓ for completed even when key matches", () => {
|
|
157
|
+
// completed takes priority over currentPhase match
|
|
158
|
+
assert.equal(statusSymbol("completed", "2", "2"), "✓")
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// ── renderPhaseLine ──
|
|
163
|
+
|
|
164
|
+
void describe("renderPhaseLine", () => {
|
|
165
|
+
void it("renders completed phase", () => {
|
|
166
|
+
const line = renderPhaseLine("0", { status: "completed" }, "2")
|
|
167
|
+
assert.ok(line.includes("✓"))
|
|
168
|
+
assert.ok(line.includes("THINK"))
|
|
169
|
+
assert.ok(line.includes("done"))
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
void it("renders in_progress phase", () => {
|
|
173
|
+
const line = renderPhaseLine("2", { status: "in_progress" }, "2")
|
|
174
|
+
assert.ok(line.includes("→"))
|
|
175
|
+
assert.ok(line.includes("BUILD"))
|
|
176
|
+
assert.ok(line.includes("active"))
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
void it("renders pending phase (current phase match)", () => {
|
|
180
|
+
const line = renderPhaseLine("3", undefined, "2")
|
|
181
|
+
assert.ok(line.includes("○"))
|
|
182
|
+
assert.ok(line.includes("REVIEW"))
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
void it("renders current phase with arrow when no history entry (current assigned in_progress)", () => {
|
|
186
|
+
const line = renderPhaseLine("2", undefined, "2")
|
|
187
|
+
assert.ok(line.includes("→"))
|
|
188
|
+
assert.ok(line.includes("BUILD"))
|
|
189
|
+
assert.ok(line.includes("active"))
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
void it("uses custom phase_name from history", () => {
|
|
193
|
+
const line = renderPhaseLine("2", { status: "completed", phase_name: "MY BUILD" }, "2")
|
|
194
|
+
assert.ok(line.includes("MY BUILD"))
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// ── readSprintState ──
|
|
199
|
+
|
|
200
|
+
void describe("readSprintState", () => {
|
|
201
|
+
const tmpDir = join(tmpdir(), "xp-gate-tui-test-" + randomUUID())
|
|
202
|
+
|
|
203
|
+
before(() => {
|
|
204
|
+
mkdirSync(join(tmpDir, ".sprint-state"), { recursive: true })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
after(() => {
|
|
208
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
void it("returns null when no sprint-state.json", () => {
|
|
212
|
+
assert.equal(readSprintState(tmpDir), null)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
void it("returns parsed state when file exists", () => {
|
|
216
|
+
const state = { id: "test", phase: "1", task_description: "Test" }
|
|
217
|
+
writeFileSync(join(tmpDir, ".sprint-state", "sprint-state.json"), JSON.stringify(state))
|
|
218
|
+
const result = readSprintState(tmpDir)
|
|
219
|
+
assert.notEqual(result, null)
|
|
220
|
+
assert.equal(result?.id, "test")
|
|
221
|
+
assert.equal(result?.phase, "1")
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
void it("returns null on malformed JSON", () => {
|
|
225
|
+
writeFileSync(join(tmpDir, ".sprint-state", "sprint-state.json"), "{not-json")
|
|
226
|
+
assert.equal(readSprintState(tmpDir), null)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
void it("returns null when directory does not exist", () => {
|
|
230
|
+
assert.equal(readSprintState("/nonexistent/path"), null)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// ── renderSprintSidebar ──
|
|
235
|
+
|
|
236
|
+
void describe("renderSprintSidebar", () => {
|
|
237
|
+
void it("returns empty string for null state", () => {
|
|
238
|
+
assert.equal(renderSprintSidebar(null as unknown as Parameters<typeof renderSprintSidebar>[0]), "")
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
void it("returns empty string when no task_description", () => {
|
|
242
|
+
assert.equal(renderSprintSidebar({} as Parameters<typeof renderSprintSidebar>[0]), "")
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
void it("includes sprint title on first line", () => {
|
|
246
|
+
const output = renderSprintSidebar(makeSprintState())
|
|
247
|
+
const lines = output.split("\n")
|
|
248
|
+
assert.ok(lines[0].includes("SPRINT:"))
|
|
249
|
+
assert.ok(lines[0].includes("OAuth2"))
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
void it("includes metrics when available", () => {
|
|
253
|
+
const output = renderSprintSidebar(makeSprintState())
|
|
254
|
+
assert.ok(output.includes("tests:42"))
|
|
255
|
+
assert.ok(output.includes("cov:87%"))
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
void it("omits metrics section when none present", () => {
|
|
259
|
+
const output = renderSprintSidebar(makeSprintState({ metrics: {} }))
|
|
260
|
+
assert.ok(!output.includes("tests:"))
|
|
261
|
+
assert.ok(!output.includes("cov:"))
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
void it("shows stale warning when >1h idle", () => {
|
|
265
|
+
const state = makeSprintState({ started_at: new Date(Date.now() - 7_200_000).toISOString(), phase_history: [] })
|
|
266
|
+
const output = renderSprintSidebar(state)
|
|
267
|
+
assert.ok(output.includes("idle"))
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
void it("does not show stale warning when recent activity", () => {
|
|
271
|
+
const state = makeSprintState({
|
|
272
|
+
started_at: new Date(Date.now() - 7_200_000).toISOString(),
|
|
273
|
+
phase_history: [
|
|
274
|
+
{ phase: "0", started_at: new Date().toISOString(), completed_at: new Date().toISOString() },
|
|
275
|
+
],
|
|
276
|
+
})
|
|
277
|
+
const output = renderSprintSidebar(state)
|
|
278
|
+
assert.ok(!output.includes("idle"))
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
void it("shows phases with activity or current", () => {
|
|
282
|
+
const output = renderSprintSidebar(makeSprintState())
|
|
283
|
+
// Should show ISOLATE (completed in history), THINK, PLAN, BUILD (current)
|
|
284
|
+
assert.ok(output.includes("ISOLATE"))
|
|
285
|
+
assert.ok(output.includes("THINK"))
|
|
286
|
+
assert.ok(output.includes("PLAN"))
|
|
287
|
+
assert.ok(output.includes("BUILD"))
|
|
288
|
+
// Should NOT show REVIEW, FEEDBACK, etc. (no history, not current)
|
|
289
|
+
assert.ok(!output.includes("REVIEW"))
|
|
290
|
+
assert.ok(!output.includes("FEEDBACK"))
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
void it("shows REQ-level progress for BUILD phase", () => {
|
|
294
|
+
const output = renderSprintSidebar(makeSprintState())
|
|
295
|
+
assert.ok(output.includes("JWT"))
|
|
296
|
+
assert.ok(output.includes("OAuth2"))
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
void it("renders correct status symbols per phase", () => {
|
|
300
|
+
const output = renderSprintSidebar(makeSprintState())
|
|
301
|
+
// ISOLATE completed → ✓
|
|
302
|
+
const lines = output.split("\n")
|
|
303
|
+
const isolateLine = lines.find((l: string) => l.includes("ISOLATE"))
|
|
304
|
+
assert.ok(isolateLine && isolateLine.startsWith("✓"), `Expected ISOLATE line to start with ✓, got: ${isolateLine}`)
|
|
305
|
+
})
|
|
306
|
+
})
|