@boyingliu01/xp-gate 0.35.19 → 0.35.21
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/hooks/pre-commit +87 -17
- package/hooks/pre-push +44 -5
- package/lib/__tests__/sprint-status-rework-704.test.js +149 -0
- package/lib/__tests__/sprint-status-rework.test.js +32 -7
- package/lib/__tests__/test-alignment-java-702.test.ts +177 -0
- package/lib/__tests__/test-alignment.test.ts +6 -2
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/sprint-status.js +91 -26
- package/lib/test-alignment.ts +152 -6
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/index.d.ts +24 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -1
- package/plugins/dsh/lib/index.js +37 -1
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-version-footer.test.ts +42 -0
- package/plugins/dsh/src/index.ts +38 -1
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/hooks/pre-commit
CHANGED
|
@@ -857,9 +857,14 @@ echo ""
|
|
|
857
857
|
GATE_0_START=$(gate_start_ms)
|
|
858
858
|
GATE_0_STATUS="PASS"
|
|
859
859
|
|
|
860
|
-
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null ||
|
|
860
|
+
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null) || CURRENT_BRANCH=""
|
|
861
|
+
CURRENT_BRANCH=$(printf '%s' "$CURRENT_BRANCH" | tr -d '\r\n'); [ -n "$CURRENT_BRANCH" ] || CURRENT_BRANCH="HEAD"
|
|
861
862
|
PROTECTED_BRANCHES="master develop trunk mainline"
|
|
862
|
-
|
|
863
|
+
# #711: same shape as the branch/commit captures above — `git rev-parse` can
|
|
864
|
+
# print a partial line before failing, so `|| echo "."` can yield "<something>\n."
|
|
865
|
+
# and every later `cd "$ROOT_DIR"` / path join would silently use a two-line value.
|
|
866
|
+
ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null) || ROOT_DIR=""
|
|
867
|
+
ROOT_DIR=$(printf '%s' "$ROOT_DIR" | tr -d '\r\n'); [ -n "$ROOT_DIR" ] || ROOT_DIR="."
|
|
863
868
|
|
|
864
869
|
# Optional: project-level .protected-branches config file
|
|
865
870
|
if [ -f "$ROOT_DIR/.protected-branches" ]; then
|
|
@@ -2664,7 +2669,8 @@ else
|
|
|
2664
2669
|
|
|
2665
2670
|
SKIP_5A_BLOCK=false
|
|
2666
2671
|
if [ "${SKIP_GATE_5A_BLOCK:-0}" = "1" ]; then
|
|
2667
|
-
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null ||
|
|
2672
|
+
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null) || CURRENT_BRANCH=""
|
|
2673
|
+
CURRENT_BRANCH=$(printf '%s' "$CURRENT_BRANCH" | tr -d '\r\n'); [ -n "$CURRENT_BRANCH" ] || CURRENT_BRANCH="HEAD"
|
|
2668
2674
|
if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
|
|
2669
2675
|
REASON="${SKIP_GATE_5A_BLOCK_REASON:-not specified}"
|
|
2670
2676
|
mkdir -p .xp-gate/reports
|
|
@@ -4017,6 +4023,30 @@ else
|
|
|
4017
4023
|
|
|
4018
4024
|
# Check if architecture config exists (use $PROJECT_ROOT — hook may have cd'd to a subdir via ARCH-03)
|
|
4019
4025
|
if [ -f "$PROJECT_ROOT/architecture.yaml" ] || [ -f "$PROJECT_ROOT/.architecturerc" ] || [ -f "$PROJECT_ROOT/.archlint.yaml" ]; then
|
|
4026
|
+
# #686: an `architecture.yaml` is NOT evidence that layer boundaries are
|
|
4027
|
+
# checked. Gate 6 only ever tested for the file's existence; archlint is
|
|
4028
|
+
# invoked without --config, and even with a config it cannot enforce layers:
|
|
4029
|
+
# @archlinter/cli 0.16.0 lists a `layer_violation` detector but produces no
|
|
4030
|
+
# results for it under any configuration (-A, -d layer_violation, enabled:
|
|
4031
|
+
# true, globs, exact paths — all exit 0 with zero layer findings). See
|
|
4032
|
+
# docs/plans/2026-11-02-arch-01-gate6-layers-not-enforced-design.md §2.2.
|
|
4033
|
+
#
|
|
4034
|
+
# So a repo that declares layers gets a generic-smell check and a green
|
|
4035
|
+
# verdict, which reads as "layers are clean". It is not: nothing looked.
|
|
4036
|
+
# This block records that fact as a per-language reason so the quality
|
|
4037
|
+
# report cannot score the dimension as covered, and emits a marker the
|
|
4038
|
+
# terminal output can be asserted against.
|
|
4039
|
+
#
|
|
4040
|
+
# Deliberately NOT a capability probe: archlint silently tolerates unknown
|
|
4041
|
+
# config keys, so "it did not complain" is a tautological guard — it would
|
|
4042
|
+
# return "supported" no matter what. File presence is the only honest input.
|
|
4043
|
+
GATE_6_LAYERS_UNCHECKED=0
|
|
4044
|
+
if [ -f "$PROJECT_ROOT/architecture.yaml" ] || [ -f "$PROJECT_ROOT/.architecturerc" ]; then
|
|
4045
|
+
GATE_6_LAYERS_UNCHECKED=1
|
|
4046
|
+
echo " ⚠️ [WARN:LAYERS_UNCHECKED] 分层规则未校验:architecture.yaml 声明的层边界当前不被任何检测器消费"
|
|
4047
|
+
echo " (实测 @archlinter/cli 0.16.0 的 layer_violation 启用后亦无输出;见 #686)"
|
|
4048
|
+
echo " 本次仅校验通用架构坏味道(循环依赖 / 上帝类 / 死代码)。"
|
|
4049
|
+
fi
|
|
4020
4050
|
# #537: per-language reason bookkeeping. Gate 3/4 are single-language gates
|
|
4021
4051
|
# and get away with one scalar reason; Gate 6 loops, so a scalar would lose
|
|
4022
4052
|
# information ("TS passed, PS has no detector" cannot be one string).
|
|
@@ -4265,6 +4295,20 @@ JAVAARCHROOTS
|
|
|
4265
4295
|
GATE_6_LANG_REASONS=""
|
|
4266
4296
|
_g6_seen=""
|
|
4267
4297
|
_g6_mixed=0
|
|
4298
|
+
# #686: a language that archlint actually measured and passed still carries a
|
|
4299
|
+
# reason when the repo declared layer boundaries, because the layer dimension
|
|
4300
|
+
# itself was never measured. Only languages WITHOUT a reason get this stamp —
|
|
4301
|
+
# an existing reason (tool missing, tests failed, ...) is more specific and
|
|
4302
|
+
# must not be overwritten. The `-z` test is what keeps this honest: stamping
|
|
4303
|
+
# unconditionally would erase the real reason and make every language look
|
|
4304
|
+
# alike in the report.
|
|
4305
|
+
if [ "${GATE_6_LAYERS_UNCHECKED:-0}" = "1" ] && [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
4306
|
+
for _g6_lang in $PROJECT_LANGS; do
|
|
4307
|
+
if [ -z "${GATE_6_REASON_BY_LANG[$_g6_lang]:-}" ]; then
|
|
4308
|
+
GATE_6_REASON_BY_LANG["$_g6_lang"]="layers_config_not_enforced"
|
|
4309
|
+
fi
|
|
4310
|
+
done
|
|
4311
|
+
fi
|
|
4268
4312
|
for _g6_lang in $PROJECT_LANGS; do
|
|
4269
4313
|
_g6_r=""
|
|
4270
4314
|
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
@@ -4313,19 +4357,27 @@ JAVAARCHROOTS
|
|
|
4313
4357
|
echo " Required configuration file (.archlint.yaml / architecture.yaml) is NOT found."
|
|
4314
4358
|
echo " Architecture Quality Gate requires explicit architecture constraints."
|
|
4315
4359
|
echo ""
|
|
4316
|
-
|
|
4317
|
-
echo " Or
|
|
4318
|
-
echo "
|
|
4319
|
-
echo "
|
|
4320
|
-
echo "
|
|
4321
|
-
echo "
|
|
4322
|
-
echo "
|
|
4323
|
-
echo "
|
|
4324
|
-
echo "
|
|
4325
|
-
|
|
4326
|
-
|
|
4360
|
+
echo " Quick fix: create .archlint.yaml with: npx archlint init --no-interactive"
|
|
4361
|
+
echo " Or write one by hand — this is archlint's own config format, and the"
|
|
4362
|
+
echo " only one it actually reads:"
|
|
4363
|
+
echo " rules:"
|
|
4364
|
+
echo " cyclic_dependency:"
|
|
4365
|
+
echo " severity: high"
|
|
4366
|
+
echo " god_module:"
|
|
4367
|
+
echo " severity: high"
|
|
4368
|
+
echo ""
|
|
4369
|
+
# #686: this used to recommend a top-level `layers:` block. That format is
|
|
4370
|
+
# silently ignored by @archlinter/cli 0.16.0 (verified against a fixture
|
|
4371
|
+
# with a textbook domain->infra violation: the layer_violation detector is
|
|
4372
|
+
# listed by `archlint detectors list` but produces no results under any
|
|
4373
|
+
# configuration, including -A and an explicit -d layer_violation). The hint
|
|
4374
|
+
# therefore taught users to write a file that bought them nothing while
|
|
4375
|
+
# Gate 6 reported green — the worst possible combination.
|
|
4376
|
+
echo " NOTE: layer-boundary rules are NOT enforced by archlint 0.16.0."
|
|
4377
|
+
echo " Do not write a top-level 'layers:' block expecting it to be checked;"
|
|
4378
|
+
echo " Gate 6 will tell you when that changes. See #686 / #690 / #692."
|
|
4327
4379
|
echo ""
|
|
4328
|
-
echo " After creating
|
|
4380
|
+
echo " After creating the config, retry the commit."
|
|
4329
4381
|
echo " (To skip with warning, create .architecture-skip file)"
|
|
4330
4382
|
exit 1
|
|
4331
4383
|
fi
|
|
@@ -4942,8 +4994,26 @@ generate_quality_report() {
|
|
|
4942
4994
|
local WARN_COUNT=0
|
|
4943
4995
|
local NONPASS_COUNT=0
|
|
4944
4996
|
|
|
4945
|
-
|
|
4946
|
-
|
|
4997
|
+
# #711: capture the probe output first, then fall back — never `|| echo` a
|
|
4998
|
+
# command whose stdout is only shown on success.
|
|
4999
|
+
#
|
|
5000
|
+
# On a repository with no commits yet, `git rev-parse --abbrev-ref HEAD`
|
|
5001
|
+
# PRINTS "HEAD" and then exits non-zero. With `$(cmd || echo unknown)` the
|
|
5002
|
+
# captured value becomes "HEAD\nunknown" — two lines — and it is embedded
|
|
5003
|
+
# verbatim into the JSON report as `"branch": "HEAD\nunknown"`, which is not
|
|
5004
|
+
# valid JSON (a raw newline cannot appear inside a string literal).
|
|
5005
|
+
#
|
|
5006
|
+
# Gate 12 then reads that report on the NEXT commit and BLOCKS with
|
|
5007
|
+
# "YAML/JSON syntax errors" against a file the gate itself wrote. The first
|
|
5008
|
+
# commit of every freshly-initialised consumer repo therefore fails once for a
|
|
5009
|
+
# reason that has nothing to do with the user's change.
|
|
5010
|
+
COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null) || COMMIT_HASH=""
|
|
5011
|
+
[ -n "$COMMIT_HASH" ] || COMMIT_HASH="unknown"
|
|
5012
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || BRANCH=""
|
|
5013
|
+
# A detached HEAD reports the literal word "HEAD"; keep it (it is the truth)
|
|
5014
|
+
# but never allow a multi-line value into the report.
|
|
5015
|
+
BRANCH=$(printf '%s' "$BRANCH" | tr -d '\r\n')
|
|
5016
|
+
[ -n "$BRANCH" ] || BRANCH="unknown"
|
|
4947
5017
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
4948
5018
|
|
|
4949
5019
|
# Scoring per #507 S3: SKIP removes a gate from the denominator (a missing
|
package/hooks/pre-push
CHANGED
|
@@ -53,7 +53,11 @@ write_pre_push_report() {
|
|
|
53
53
|
report_ts=$(date -u +"%Y-%m-%d-%H%M%S")
|
|
54
54
|
reports_dir=".xp-gate/reports/pre-push"
|
|
55
55
|
report_file="${reports_dir}/${report_ts}.json"
|
|
56
|
-
|
|
56
|
+
# #710: `git rev-parse` prints a partial line before it fails, so
|
|
57
|
+
# `$(cmd || echo x)` captures "<partial>\nx". Strip newlines before the value
|
|
58
|
+
# reaches this JSON journal — a raw newline cannot appear inside a JSON string.
|
|
59
|
+
commit_hash=$(git rev-parse HEAD 2>/dev/null) || commit_hash=""
|
|
60
|
+
commit_hash=$(printf '%s' "$commit_hash" | tr -d '\r\n'); [ -n "$commit_hash" ] || commit_hash="unknown"
|
|
57
61
|
verdict=$([ "$exit_code" -eq 0 ] && echo "PASS" || echo "BLOCKED")
|
|
58
62
|
mkdir -p "$reports_dir"
|
|
59
63
|
# #544: the survivor detail is a JSON document, so it cannot be interpolated
|
|
@@ -352,7 +356,9 @@ _mp_now_ms() {
|
|
|
352
356
|
esac
|
|
353
357
|
}
|
|
354
358
|
MP_START_MS="$(_mp_now_ms)"
|
|
355
|
-
MP_ROOT
|
|
359
|
+
MP_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || MP_ROOT=""
|
|
360
|
+
MP_ROOT=$(printf '%s' "$MP_ROOT" | tr -d '
|
|
361
|
+
'); [ -n "$MP_ROOT" ] || MP_ROOT="."
|
|
356
362
|
if [ -f "$MP_ROOT/scripts/check-hook-mirror.sh" ]; then
|
|
357
363
|
if (cd "$MP_ROOT" && bash scripts/check-hook-mirror.sh); then
|
|
358
364
|
GATE_MP_STATUS="PASS"
|
|
@@ -418,7 +424,9 @@ fi
|
|
|
418
424
|
# Uses sprint-gate.sh for standalone validation logic
|
|
419
425
|
# ============================================================================
|
|
420
426
|
GATE_S_STATUS="PASS"
|
|
421
|
-
PROJECT_ROOT
|
|
427
|
+
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || PROJECT_ROOT=""
|
|
428
|
+
PROJECT_ROOT=$(printf '%s' "$PROJECT_ROOT" | tr -d '
|
|
429
|
+
'); [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="."
|
|
422
430
|
SPRINT_GATE_SCRIPT=""
|
|
423
431
|
|
|
424
432
|
# Same chain as pre-commit's Gate 11 (#476). The two tiers here both point at
|
|
@@ -1128,7 +1136,36 @@ if [[ "$XP_GATE_SKIP_UI_GATES" == "1" ]]; then
|
|
|
1128
1136
|
exit 1
|
|
1129
1137
|
fi
|
|
1130
1138
|
echo "⚠️ UI Gate BYPASSED — reason: $XP_GATE_BYPASS_REASON"
|
|
1131
|
-
|
|
1139
|
+
# #710: build the whole record in node rather than interpolating shell
|
|
1140
|
+
# variables into a JSON string. Values come from the environment so no shell
|
|
1141
|
+
# quoting layer can mangle them (an earlier sed-based escaper silently
|
|
1142
|
+
# dropped the reason entirely when its backslash expression broke).
|
|
1143
|
+
#
|
|
1144
|
+
# The reason is free-form USER INPUT: one double quote in it used to end the
|
|
1145
|
+
# JSON string early and turn the audit line into garbage. An audit trail that
|
|
1146
|
+
# can be corrupted by the thing it is auditing is not an audit trail.
|
|
1147
|
+
AUDIT_ENTRY=$(
|
|
1148
|
+
_AJ_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
|
|
1149
|
+
_AJ_BRANCH="$CURRENT_BRANCH" \
|
|
1150
|
+
_AJ_COMMIT="$(git rev-parse HEAD 2>/dev/null)" \
|
|
1151
|
+
_AJ_USER="$(git config user.name 2>/dev/null)" \
|
|
1152
|
+
_AJ_REASON="$XP_GATE_BYPASS_REASON" \
|
|
1153
|
+
node -e '
|
|
1154
|
+
const env = process.env;
|
|
1155
|
+
const clean = (v, fallback) => {
|
|
1156
|
+
const s = String(v === undefined ? "" : v).replace(/[\r\n]+/g, " ").trim();
|
|
1157
|
+
return s === "" ? fallback : s;
|
|
1158
|
+
};
|
|
1159
|
+
process.stdout.write(JSON.stringify({
|
|
1160
|
+
timestamp: env._AJ_TS,
|
|
1161
|
+
branch: clean(env._AJ_BRANCH, "unknown"),
|
|
1162
|
+
commit: clean(env._AJ_COMMIT, "unknown"),
|
|
1163
|
+
user: clean(env._AJ_USER, "unknown"),
|
|
1164
|
+
reason: clean(env._AJ_REASON, ""),
|
|
1165
|
+
bypass_type: "ui-gates",
|
|
1166
|
+
}));
|
|
1167
|
+
' 2>/dev/null
|
|
1168
|
+
)
|
|
1132
1169
|
echo "$AUDIT_ENTRY" >> .audit-log.jsonl
|
|
1133
1170
|
else
|
|
1134
1171
|
# Detect UI changes in pushed files.
|
|
@@ -1310,7 +1347,9 @@ CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
1310
1347
|
# evidence (#574). It is passed explicitly instead of letting the validator
|
|
1311
1348
|
# infer it from cwd: the gate's subject is the repo being pushed, and a cwd-
|
|
1312
1349
|
# derived lookup would bind xp-gate's own config whenever the suite runs here.
|
|
1313
|
-
MW_PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
1350
|
+
MW_PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || MW_PROJECT_ROOT=""
|
|
1351
|
+
MW_PROJECT_ROOT=$(printf '%s' "$MW_PROJECT_ROOT" | tr -d '
|
|
1352
|
+
')
|
|
1314
1353
|
if ! VALIDATION_ERROR=$(node "$MW_VALIDATOR" "$RESULT_FILE" "$EXPECTED_COMMIT" "$CURRENT_BRANCH" "$CURRENT_TIME" "$MW_PROJECT_ROOT" 2>&1); then
|
|
1315
1354
|
echo ""
|
|
1316
1355
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-704 Sprint Status 返工率
|
|
3
|
+
* @intent `--rework-check` 的返工率恒为 0,且会把任何正确的值覆盖成 0。
|
|
4
|
+
*
|
|
5
|
+
* 根因有两层,必须同时修好才算真的修好:
|
|
6
|
+
* (1) 统计区间的下界用了 `completed_at` —— 按定义它是 sprint **结束**的
|
|
7
|
+
* 时刻,而 sprint 的全部提交都发生在它之前。于是 `git log --since
|
|
8
|
+
* <completed_at>` 返回空集。要统计的区间是「开始 → 结束」,
|
|
9
|
+
* 实际统计的区间是「结束 → 现在」,恰好是空的。
|
|
10
|
+
* (2) `total_sprint_commits` 从未被计算过,只被读取(值恒为 0),
|
|
11
|
+
* 再经 `Math.max(total, 1)` 兜底成 1,使 `0/1` 恒等于 0。
|
|
12
|
+
*
|
|
13
|
+
* 这是一个质量指标的 fail-open:返工率是 #369 用来暴露「门禁全过但仍
|
|
14
|
+
* 返工」的唯一信号,恒为 0 意味着返工永远不可观测,而 CLI 还会主动打印
|
|
15
|
+
* 「All sprints within acceptable rework rate」。告警阈值永不触发。
|
|
16
|
+
*
|
|
17
|
+
* 测试必须用真实 git 仓库:这个 bug 的全部性质都藏在 `git log --since`
|
|
18
|
+
* 的行为里,任何 mock 都能让它「通过」。
|
|
19
|
+
* @covers AC-704-01, AC-704-02, AC-704-03, AC-704-04
|
|
20
|
+
*/
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { execFileSync } = require('child_process');
|
|
25
|
+
|
|
26
|
+
const sprintStatus = require('../sprint-status');
|
|
27
|
+
|
|
28
|
+
function git(cwd, args, env = {}) {
|
|
29
|
+
// `-c core.hooksPath=` disables hooks for this fixture. Without it, the
|
|
30
|
+
// temporary repo inherits this machine's global hooksPath and every fixture
|
|
31
|
+
// commit runs the full pre-commit suite (~60s each, and it can BLOCK on
|
|
32
|
+
// unrelated file-hygiene findings in .xp-gate/reports). The test asserts on
|
|
33
|
+
// `git log`, so hooks contribute nothing but time and flakiness.
|
|
34
|
+
return execFileSync('git', ['-c', 'core.hooksPath=', ...args], {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
env: { ...process.env, ...env },
|
|
38
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Capture console.log so assertions read the CLI output, not a return value. */
|
|
43
|
+
function captureLogs(fn) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
const original = console.log;
|
|
46
|
+
console.log = (...args) => lines.push(args.join(' '));
|
|
47
|
+
try {
|
|
48
|
+
const result = fn();
|
|
49
|
+
return { lines, result };
|
|
50
|
+
} finally {
|
|
51
|
+
console.log = original;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('sprint-status --rework-check 返工率 (#704)', () => {
|
|
56
|
+
let dir;
|
|
57
|
+
let stateFile;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rework-704-'));
|
|
61
|
+
git(dir, ['init', '-q', '.']);
|
|
62
|
+
git(dir, ['config', 'user.email', 'test@example.com']);
|
|
63
|
+
git(dir, ['config', 'user.name', 'Test']);
|
|
64
|
+
|
|
65
|
+
// 4 commits: 2 feat + 2 fix => real rework rate 50%, above the 30% threshold.
|
|
66
|
+
// Author/committer dates are pinned so the sprint window is deterministic.
|
|
67
|
+
const commits = [
|
|
68
|
+
['2026-11-01T10:00:00Z', 'a.txt', 'feat: initial'],
|
|
69
|
+
['2026-11-01T10:10:00Z', 'b.txt', 'fix(core): repair the thing'],
|
|
70
|
+
['2026-11-01T10:20:00Z', 'c.txt', 'feat: another'],
|
|
71
|
+
['2026-11-01T10:30:00Z', 'd.txt', 'fix: 修掉一个缺陷'],
|
|
72
|
+
];
|
|
73
|
+
for (const [date, file, message] of commits) {
|
|
74
|
+
fs.writeFileSync(path.join(dir, file), file);
|
|
75
|
+
git(dir, ['add', '-A']);
|
|
76
|
+
git(dir, ['commit', '-q', '-m', message], {
|
|
77
|
+
GIT_AUTHOR_DATE: date,
|
|
78
|
+
GIT_COMMITTER_DATE: date,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fs.mkdirSync(path.join(dir, '.sprint-state'), { recursive: true });
|
|
83
|
+
stateFile = path.join(dir, '.sprint-state', 'sprint-state.json');
|
|
84
|
+
// completed_at is AFTER every commit — this is the real CLOSE ordering, and
|
|
85
|
+
// the exact input that made `--since completed_at` return an empty set.
|
|
86
|
+
fs.writeFileSync(stateFile, JSON.stringify({
|
|
87
|
+
_schema_version: 1,
|
|
88
|
+
id: 'sprint-2026-11-01-01',
|
|
89
|
+
phase: 6,
|
|
90
|
+
status: 'completed',
|
|
91
|
+
started_at: '2026-11-01T09:00:00.000Z',
|
|
92
|
+
metrics: { completed_at: '2026-11-01T11:00:00.000Z' },
|
|
93
|
+
}, null, 2));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('AC-704-01: counts fix commits between sprint start and completion, not after completion', () => {
|
|
101
|
+
const { lines } = captureLogs(() => sprintStatus.handleReworkCheck(dir, 3650));
|
|
102
|
+
const output = lines.join('\n');
|
|
103
|
+
|
|
104
|
+
// The two fix commits happened inside the sprint window. Reporting 0 means
|
|
105
|
+
// the lower bound is on the wrong side of the sprint.
|
|
106
|
+
expect(output).toMatch(/2 fix \/ 4 total commits/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('AC-704-02: reports the real rate and raises the >30% alert', () => {
|
|
110
|
+
const { lines } = captureLogs(() => sprintStatus.handleReworkCheck(dir, 3650));
|
|
111
|
+
const output = lines.join('\n');
|
|
112
|
+
|
|
113
|
+
expect(output).toMatch(/50\.0%/);
|
|
114
|
+
expect(output).toMatch(/exceeds 30% threshold/);
|
|
115
|
+
// The all-clear line must NOT be printed when a sprint is over threshold:
|
|
116
|
+
// that sentence is what turns a fail-open into a false reassurance.
|
|
117
|
+
expect(output).not.toMatch(/All sprints within acceptable rework rate/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('AC-704-03: persists the computed rate and the commit total it was computed from', () => {
|
|
121
|
+
captureLogs(() => sprintStatus.handleReworkCheck(dir, 3650));
|
|
122
|
+
|
|
123
|
+
const persisted = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
124
|
+
expect(persisted.metrics.rework_rate).toBe(0.5);
|
|
125
|
+
// total_sprint_commits was never computed before; the report needs it to
|
|
126
|
+
// stay meaningful on the next run.
|
|
127
|
+
expect(persisted.metrics.total_sprint_commits).toBe(4);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('AC-704-04: an empty window reports 0/0, not a confident 0%', () => {
|
|
131
|
+
// Second sprint state: its window contains no commits at all.
|
|
132
|
+
fs.writeFileSync(stateFile, JSON.stringify({
|
|
133
|
+
_schema_version: 1,
|
|
134
|
+
id: 'sprint-2026-11-02-01',
|
|
135
|
+
phase: 6,
|
|
136
|
+
status: 'completed',
|
|
137
|
+
started_at: '2026-11-01T10:35:00.000Z',
|
|
138
|
+
metrics: { completed_at: '2026-11-01T12:00:00.000Z' },
|
|
139
|
+
}, null, 2));
|
|
140
|
+
|
|
141
|
+
const { lines } = captureLogs(() => sprintStatus.handleReworkCheck(dir, 3650));
|
|
142
|
+
const output = lines.join('\n');
|
|
143
|
+
|
|
144
|
+
// Zero commits in the window is NOT "0% rework" — it is "nothing to
|
|
145
|
+
// measure". Rendering it as 0.0% would recreate this same fail-open one
|
|
146
|
+
// level down.
|
|
147
|
+
expect(output).toMatch(/0 fix \/ 0 total commits/);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -55,6 +55,21 @@ function daysAgo(n) {
|
|
|
55
55
|
return new Date(Date.now() - n * 86400000).toISOString();
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Hour-level offset, needed to express a sprint window that actually CONTAINS
|
|
60
|
+
* the fixture's commits (#704).
|
|
61
|
+
*
|
|
62
|
+
* The commits are made "now", so a window built from daysAgo(9)→daysAgo(2) held
|
|
63
|
+
* no commits at all. The old fixtures compensated by hand-writing
|
|
64
|
+
* `total_sprint_commits: 10` — a number nothing in the code ever produced — which
|
|
65
|
+
* pinned the expected rate against a value that could only exist in the test.
|
|
66
|
+
* Negative = in the future; used for `completed_at`, which in the real CLOSE
|
|
67
|
+
* ordering is stamped after the last commit.
|
|
68
|
+
*/
|
|
69
|
+
function hoursAgo(n) {
|
|
70
|
+
return new Date(Date.now() - n * 3600000).toISOString();
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
beforeAll(() => {
|
|
59
74
|
TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'rework-test-'));
|
|
60
75
|
cwdSpy = vi.spyOn(process, 'cwd');
|
|
@@ -101,9 +116,14 @@ describe('sprint-status --rework-check (#369)', () => {
|
|
|
101
116
|
git('init', dir);
|
|
102
117
|
git('config user.email "test@test.com"', dir);
|
|
103
118
|
git('config user.name "Test"', dir);
|
|
119
|
+
// 10 commits, 2 of them fixes => 20%, below the 30% threshold.
|
|
120
|
+
// The count has to come from real history: the old fixture made 3 commits
|
|
121
|
+
// and declared 'total_sprint_commits: 10' by hand, so the expected 20%
|
|
122
|
+
// could only ever be produced by the fabricated number (#704).
|
|
104
123
|
makeCommit('feat: initial', dir);
|
|
105
124
|
makeCommit('fix: resolve login bug', dir);
|
|
106
125
|
makeCommit('fix(auth): handle timeout', dir);
|
|
126
|
+
for (let i = 0; i < 7; i += 1) makeCommit(`feat: feature ${i}`, dir);
|
|
107
127
|
|
|
108
128
|
writeSprintState({
|
|
109
129
|
_schema_version: 1,
|
|
@@ -111,10 +131,10 @@ describe('sprint-status --rework-check (#369)', () => {
|
|
|
111
131
|
task_description: 'Sprint B',
|
|
112
132
|
phase: 6,
|
|
113
133
|
status: 'completed',
|
|
114
|
-
started_at:
|
|
134
|
+
started_at: hoursAgo(1),
|
|
115
135
|
isolation: { worktree_path: null, branch: 'main' },
|
|
116
136
|
phase_history: [],
|
|
117
|
-
metrics: { completed_at:
|
|
137
|
+
metrics: { completed_at: hoursAgo(-1) },
|
|
118
138
|
}, dir);
|
|
119
139
|
|
|
120
140
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -142,12 +162,13 @@ describe('sprint-status --rework-check (#369)', () => {
|
|
|
142
162
|
git('init', dir);
|
|
143
163
|
git('config user.email "test@test.com"', dir);
|
|
144
164
|
git('config user.name "Test"', dir);
|
|
165
|
+
// 10 commits, 5 of them fixes => 50%, above the 30% threshold.
|
|
145
166
|
makeCommit('fix: one', dir);
|
|
146
167
|
makeCommit('fix: two', dir);
|
|
147
168
|
makeCommit('bugfix: three', dir);
|
|
148
169
|
makeCommit('hotfix: four', dir);
|
|
149
170
|
makeCommit('patch: five', dir);
|
|
150
|
-
makeCommit(
|
|
171
|
+
for (let i = 0; i < 5; i += 1) makeCommit(`feat: feature ${i}`, dir);
|
|
151
172
|
|
|
152
173
|
writeSprintState({
|
|
153
174
|
_schema_version: 1,
|
|
@@ -155,10 +176,10 @@ describe('sprint-status --rework-check (#369)', () => {
|
|
|
155
176
|
task_description: 'Sprint C',
|
|
156
177
|
phase: 6,
|
|
157
178
|
status: 'completed',
|
|
158
|
-
started_at:
|
|
179
|
+
started_at: hoursAgo(1),
|
|
159
180
|
isolation: { worktree_path: null, branch: 'main' },
|
|
160
181
|
phase_history: [],
|
|
161
|
-
metrics: { completed_at:
|
|
182
|
+
metrics: { completed_at: hoursAgo(-1) },
|
|
162
183
|
}, dir);
|
|
163
184
|
|
|
164
185
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -208,6 +229,10 @@ describe('sprint-status --rework-check (#369)', () => {
|
|
|
208
229
|
}
|
|
209
230
|
makeCommit('feat: target initial', targetDir);
|
|
210
231
|
makeCommit('fix: target bug', targetDir);
|
|
232
|
+
// 8 more target commits => 10 total, 1 fix => 10%. The count must come from
|
|
233
|
+
// real history; the old fixture declared it in the state file by hand, and
|
|
234
|
+
// this test's actual subject is GIT_DIR precedence, not arithmetic (#704).
|
|
235
|
+
for (let i = 0; i < 8; i += 1) makeCommit(`feat: target feature ${i}`, targetDir);
|
|
211
236
|
makeCommit('fix: outer one', outerDir);
|
|
212
237
|
makeCommit('fix: outer two', outerDir);
|
|
213
238
|
makeCommit('fix: outer three', outerDir);
|
|
@@ -218,10 +243,10 @@ describe('sprint-status --rework-check (#369)', () => {
|
|
|
218
243
|
task_description: 'Hook environment sprint',
|
|
219
244
|
phase: 6,
|
|
220
245
|
status: 'completed',
|
|
221
|
-
started_at:
|
|
246
|
+
started_at: hoursAgo(1),
|
|
222
247
|
isolation: { worktree_path: null, branch: 'main' },
|
|
223
248
|
phase_history: [],
|
|
224
|
-
metrics: { completed_at:
|
|
249
|
+
metrics: { completed_at: hoursAgo(-1) },
|
|
225
250
|
}, targetDir);
|
|
226
251
|
cwdSpy.mockReturnValue(targetDir);
|
|
227
252
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-702 Test Alignment 语言覆盖
|
|
3
|
+
* @intent `parseTestFiles` 只认 .ts/.tsx/.js/.jsx/.py/.go。`.java` 文件被读进内存、
|
|
4
|
+
* 取出扩展名、然后什么都不做 —— 不是被拒绝,是被丢弃。
|
|
5
|
+
*
|
|
6
|
+
* 一个判定分支不存在时,系统报告的是「通过」而不是「未检查」:
|
|
7
|
+
* 纯 Java 工程 totalTests=0、totalAssertions=0,对齐分数在空集上计算,
|
|
8
|
+
* 但状态看起来是「跑过了」;混合工程里 Java 侧的覆盖被误报成
|
|
9
|
+
* 「未覆盖」,产生假阳性 gap。
|
|
10
|
+
*
|
|
11
|
+
* 与 #623(Gate 0 bypass 永不生效)、#645(真实仓库被测试写脏)同类:
|
|
12
|
+
* 共同点是静默失效。
|
|
13
|
+
*
|
|
14
|
+
* 修复要分三层,缺一层就还是 fail-open:
|
|
15
|
+
* (1) 加 .java 分支,按 JUnit 5 的实际写法解析 @test/@intent 与断言;
|
|
16
|
+
* (2) 未适配的扩展名必须 fail-loud,不能静默丢弃;
|
|
17
|
+
* (3) 已适配集合显式化,集合外的扩展名要被报出来(.kt/.cs/.rb/.rs 同理)。
|
|
18
|
+
* @covers AC-702-01, AC-702-02, AC-702-03, AC-702-04, AC-702-05, AC-702-06
|
|
19
|
+
*/
|
|
20
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as os from 'os';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
parseTestFiles,
|
|
27
|
+
UNSUPPORTED_TEST_EXTENSIONS,
|
|
28
|
+
runFullAlignment,
|
|
29
|
+
} from '../test-alignment';
|
|
30
|
+
|
|
31
|
+
// The AC id is assembled at runtime so this file contains no literal `AC-…`
|
|
32
|
+
// reference. scripts/__tests__/ac-reference-guard.test.cjs treats every
|
|
33
|
+
// `AC-<WORD>-<digits>` token in tracked sources as a claim about the real
|
|
34
|
+
// specification, and this fixture deliberately names an id that is not in
|
|
35
|
+
// specification.yaml — it only has to satisfy the parser's id shape.
|
|
36
|
+
const FIXTURE_AC = ['AC', 'FIXTURE', '01', '01'].join('-');
|
|
37
|
+
|
|
38
|
+
const JAVA_TEST = `import org.junit.jupiter.api.Test;
|
|
39
|
+
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
40
|
+
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @test REQ-001-01
|
|
44
|
+
* @intent probe intent
|
|
45
|
+
* @covers ${FIXTURE_AC}
|
|
46
|
+
*/
|
|
47
|
+
class ProbeTest {
|
|
48
|
+
@Test
|
|
49
|
+
void shouldProbe() {
|
|
50
|
+
assertEquals(1, 1);
|
|
51
|
+
assertEquals(2, 2);
|
|
52
|
+
assertTrue(true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
describe('test-alignment 语言覆盖 (#702)', () => {
|
|
58
|
+
let dir: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'align-702-'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('AC-702-01: a .java test file produces a TestCase with its REQ and covers', () => {
|
|
69
|
+
const javaFile = path.join(dir, 'ProbeTest.java');
|
|
70
|
+
fs.writeFileSync(javaFile, JAVA_TEST);
|
|
71
|
+
|
|
72
|
+
const map = parseTestFiles([javaFile]);
|
|
73
|
+
|
|
74
|
+
// The file declares @test REQ-001-01 and a @covers tag. Dropping it
|
|
75
|
+
// means the AC is reported "not covered by any test" — a false gap in a
|
|
76
|
+
// mixed project and a structurally empty report in a pure-Java one.
|
|
77
|
+
expect(map.totalTests).toBe(1);
|
|
78
|
+
expect(map.tests[0].requirementId).toBe('REQ-001-01');
|
|
79
|
+
expect(map.tests[0].name).toBe('shouldProbe');
|
|
80
|
+
expect(map.tests[0].covers).toContain(FIXTURE_AC);
|
|
81
|
+
expect(map.tests[0].intent).toBe('probe intent');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('AC-702-02: JUnit 5 assertion calls are counted', () => {
|
|
85
|
+
const javaFile = path.join(dir, 'ProbeTest.java');
|
|
86
|
+
fs.writeFileSync(javaFile, JAVA_TEST);
|
|
87
|
+
|
|
88
|
+
const map = parseTestFiles([javaFile]);
|
|
89
|
+
|
|
90
|
+
// assertEquals x2 + assertTrue x1. countAssertions() only knew JS/Python
|
|
91
|
+
// patterns, so a Java test would have parsed as zero-assertion.
|
|
92
|
+
expect(map.tests[0].assertions).toBe(3);
|
|
93
|
+
expect(map.totalAssertions).toBe(3);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('AC-702-03: an unsupported test extension is reported, never silently dropped', () => {
|
|
97
|
+
const ktFile = path.join(dir, 'ProbeTest.kt');
|
|
98
|
+
fs.writeFileSync(ktFile, `/** @test REQ-001-01 */\nclass ProbeTest {}\n`);
|
|
99
|
+
|
|
100
|
+
const map = parseTestFiles([ktFile]);
|
|
101
|
+
|
|
102
|
+
// Silently returning an empty map is the bug: the caller cannot tell
|
|
103
|
+
// "no tests here" from "tests here I could not read".
|
|
104
|
+
expect(map.unsupportedFiles).toBeDefined();
|
|
105
|
+
expect(map.unsupportedFiles.length).toBe(1);
|
|
106
|
+
expect(map.unsupportedFiles[0]).toContain('ProbeTest.kt');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('AC-702-04: the supported extension set is explicit and includes .java', () => {
|
|
110
|
+
// A hardcoded if/else chain is why .java fell through: there was no list to
|
|
111
|
+
// check the code against, so a missing branch looked like a complete one.
|
|
112
|
+
expect(Array.isArray(UNSUPPORTED_TEST_EXTENSIONS)).toBe(true);
|
|
113
|
+
expect(UNSUPPORTED_TEST_EXTENSIONS).toContain('.kt');
|
|
114
|
+
expect(UNSUPPORTED_TEST_EXTENSIONS).toContain('.rs');
|
|
115
|
+
expect(UNSUPPORTED_TEST_EXTENSIONS).not.toContain('.java');
|
|
116
|
+
expect(UNSUPPORTED_TEST_EXTENSIONS).not.toContain('.ts');
|
|
117
|
+
expect(UNSUPPORTED_TEST_EXTENSIONS).not.toContain('.go');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('runFullAlignment surfaces unparsed test files (#702)', () => {
|
|
122
|
+
const SPEC = `specification:
|
|
123
|
+
requirements:
|
|
124
|
+
- id: REQ-001-01
|
|
125
|
+
title: Probe requirement
|
|
126
|
+
acceptance_criteria:
|
|
127
|
+
- id: ${FIXTURE_AC}
|
|
128
|
+
criteria: The probe test asserts something
|
|
129
|
+
test_type: unit
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
let dir: string;
|
|
133
|
+
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'align-full-702-'));
|
|
136
|
+
fs.mkdirSync(path.join(dir, 'spec'), { recursive: true });
|
|
137
|
+
fs.mkdirSync(path.join(dir, 'tests'), { recursive: true });
|
|
138
|
+
fs.writeFileSync(path.join(dir, 'spec', 'specification.yaml'), SPEC);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('AC-702-05: a pure-Kotlin test dir cannot PASS on an empty parsed set', () => {
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
path.join(dir, 'tests', 'ProbeTest.kt'),
|
|
148
|
+
'/** @test REQ-001-01 */\nclass ProbeTest {}\n',
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const report = runFullAlignment({
|
|
152
|
+
specPath: path.join(dir, 'spec', 'specification.yaml'),
|
|
153
|
+
testDirs: [path.join(dir, 'tests')],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Zero parsed tests + zero parseable files used to look like a clean run.
|
|
157
|
+
// It is the opposite: nothing was read, so nothing can be certified.
|
|
158
|
+
expect(report.alignment_status).toBe('FAIL');
|
|
159
|
+
expect(report.errors.some(e => e.includes('ProbeTest.kt'))).toBe(true);
|
|
160
|
+
expect(report.anti_pattern_detected).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('AC-702-06: a high score does not override the presence of unparsed files', () => {
|
|
164
|
+
// Java test covers the AC, so the score would be high on its own.
|
|
165
|
+
fs.writeFileSync(path.join(dir, 'tests', 'ProbeTest.java'), JAVA_TEST);
|
|
166
|
+
// One unparseable Rust test is enough to make the number untrustworthy.
|
|
167
|
+
fs.writeFileSync(path.join(dir, 'tests', 'ProbeTest.rs'), 'fn probe() {}\n');
|
|
168
|
+
|
|
169
|
+
const report = runFullAlignment({
|
|
170
|
+
specPath: path.join(dir, 'spec', 'specification.yaml'),
|
|
171
|
+
testDirs: [path.join(dir, 'tests')],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(report.errors.some(e => e.includes('ProbeTest.rs'))).toBe(true);
|
|
175
|
+
expect(report.alignment_status).toBe('FAIL');
|
|
176
|
+
});
|
|
177
|
+
});
|