loki-mode 8.37.0 → 8.38.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.
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env bash
2
+ # Semantic Test-Authenticity Detector - Quality Gate #10
3
+ # Catches the HARDER class of fake tests that the regex detectors (gates 5+6,
4
+ # tests/detect-mock-problems.sh and tests/detect-test-mutations.sh) miss: tests
5
+ # that LOOK like real assertions but verify nothing because the asserted value
6
+ # never flows through the code under test.
7
+ #
8
+ # Usage: ./tests/detect-semantic-test-problems.sh [--strict] [--block-high]
9
+ # --strict: Exit 1 if ANY CRITICAL or HIGH finding exists (for CI gates).
10
+ # Mirrors the --strict block in tests/detect-mock-problems.sh.
11
+ # --block-high: Exit 2 when one or more HIGH-severity findings are present, 0
12
+ # otherwise (clean exit-code contract for a run.sh wrapper that
13
+ # does not want to grep stdout). --strict takes precedence.
14
+ #
15
+ # Output contract: every HIGH-severity finding prints a line beginning with the
16
+ # literal token "[HIGH]" on stdout (ANSI-colored), so a wrapper may grep
17
+ # '\[HIGH\]' as an alternative to --block-high. Same severity tokens as the
18
+ # sibling detectors: [CRITICAL] / [HIGH] / [MEDIUM] / [LOW].
19
+ #
20
+ # ----------------------------------------------------------------------------
21
+ # WHAT THIS CATCHES (the gap left by gates 5 and 6)
22
+ # ----------------------------------------------------------------------------
23
+ # 1. Literal-via-variable echo (HIGH): a variable is bound to a literal, then
24
+ # asserted to equal the SAME literal text, with NO function call between the
25
+ # binding and the assertion. Example (genuinely fake):
26
+ # const x = "hello";
27
+ # expect(x).toBe("hello"); // verifies nothing -- x never changed
28
+ # The regex detectors only catch the inline form expect("hello").toBe("hello")
29
+ # or expect(1).toBe(1); the indirection through a variable defeats them.
30
+ #
31
+ # 2. Mock-return echo (MEDIUM): within a small window, a mock is configured to
32
+ # return a literal and an assertion checks that SAME literal. The test then
33
+ # only proves the mock returns what you told it to, not that real code works.
34
+ # mockFn.mockReturnValue("ok");
35
+ # expect(result).toBe("ok"); // possibly just echoing the mock
36
+ # MEDIUM (not HIGH) because provenance is uncertain: "result" may legitimately
37
+ # be a transform of the mock output. Conservative by design.
38
+ #
39
+ # 3. Deleted/weakened assertions across a commit (MEDIUM): a commit that REMOVES
40
+ # more assertion lines than it adds from a test file (the test still "passes"
41
+ # because the check that could fail is gone). Detected from git history, like
42
+ # Check 5 of detect-test-mutations.sh. Degrades silently with no findings
43
+ # outside a git repo or when git returns nothing.
44
+ #
45
+ # ----------------------------------------------------------------------------
46
+ # WHAT THIS DELIBERATELY DOES NOT CATCH (honesty -- read before trusting)
47
+ # ----------------------------------------------------------------------------
48
+ # - Legitimate computed literal assertions. expect(add(2,2)).toBe(4) is CORRECT
49
+ # testing: 4 is the genuine expected output. Any assertion whose target
50
+ # contains a function call is NEVER flagged -- a call means the value has
51
+ # provenance through code. This is the explicit conservative boundary
52
+ # (prefer false-negatives over false-positives).
53
+ # - Deep dataflow. We cannot prove a literal "is just the input" without real
54
+ # dataflow analysis (impossible in bash). We only flag the textually obvious
55
+ # var = literal; expect(var).toBe(SAME literal) with nothing in between.
56
+ # - Multi-line assertion targets, template literals with interpolation, and
57
+ # computed property chains -- these are skipped rather than guessed at.
58
+ # - The inline tautologies expect(true).toBe(true) / expect(1).toBe(1) -- those
59
+ # are already gate 5's job (detect-mock-problems.sh). No double-reporting.
60
+ # - Reassignment between binding and assertion: if anything rebinds the variable,
61
+ # we do not flag (the value may now carry real behavior).
62
+ # - Python and shell tests for the literal/mock-echo checks (JS/TS only). The
63
+ # deleted-assertion git check covers .test/.spec.{ts,js,tsx,jsx} only.
64
+ #
65
+ # ----------------------------------------------------------------------------
66
+ # HOW TO WIRE INTO run.sh (mirror enforce_mock_integrity; DO NOT wire here --
67
+ # run.sh has another owner this wave). Add alongside the gate-5/6 wrappers:
68
+ #
69
+ # enforce_semantic_integrity() {
70
+ # local loki_dir="${TARGET_DIR:-.}/.loki"
71
+ # local quality_dir="$loki_dir/quality"
72
+ # mkdir -p "$quality_dir"
73
+ # local findings_file="$quality_dir/semantic-findings.txt"
74
+ # local detector="$SCRIPT_DIR/../tests/detect-semantic-test-problems.sh"
75
+ # local gate_timeout="${LOKI_GATE_TIMEOUT:-300}"
76
+ # [ -f "$detector" ] || { log_info "Semantic gate: detector not found, skipping"; rm -f "$findings_file" 2>/dev/null||true; return 0; }
77
+ # local output rc
78
+ # output=$(cd "${TARGET_DIR:-.}" && LOKI_SCAN_DIR="${TARGET_DIR:-.}" \
79
+ # timeout "$gate_timeout" bash "$detector" --strict 2>&1); rc=$?
80
+ # if [ "$rc" -eq 124 ]; then log_warn "Semantic gate: timed out -- inconclusive"; rm -f "$findings_file" 2>/dev/null||true; return 0; fi
81
+ # if [ "$rc" -ne 0 ]; then
82
+ # { echo "# Semantic test-authenticity findings (CRITICAL/HIGH block)"; echo "$output" | grep -E '\[(CRITICAL|HIGH|MEDIUM|LOW)\]' || true; } > "$findings_file"
83
+ # log_warn "Semantic gate: CRITICAL/HIGH problems detected -- BLOCK"; return 1
84
+ # fi
85
+ # local med_low; med_low=$(echo "$output" | grep -E '\[(MEDIUM|LOW)\]' || true)
86
+ # if [ -n "$med_low" ]; then { echo "# Semantic advisory findings (MED/LOW, non-blocking)"; echo "$med_low"; } > "$findings_file"; else rm -f "$findings_file" 2>/dev/null||true; fi
87
+ # log_info "Semantic gate: PASS"; return 0
88
+ # }
89
+ #
90
+ # Then call it from the gate loop next to enforce_mock_integrity /
91
+ # enforce_mutation_integrity (run.sh ~14299). Opt out with LOKI_GATE_SEMANTIC=false
92
+ # gating the call, matching the LOKI_GATE_MOCK / LOKI_GATE_MUTATION pattern.
93
+ # ============================================================================
94
+
95
+ set -uo pipefail
96
+
97
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
98
+ # Directory to scan. Defaults to the repo containing this script (so
99
+ # run-all-tests.sh keeps scanning loki-mode unchanged). A run.sh gate wrapper
100
+ # MUST set LOKI_SCAN_DIR to the target project; cwd is NOT used by find/git here,
101
+ # so `cd TARGET_DIR` alone does not redirect the scan. Git history (Check 3) is
102
+ # also read from this directory.
103
+ PROJECT_DIR="${LOKI_SCAN_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
104
+
105
+ STRICT=""
106
+ BLOCK_HIGH=""
107
+ while [ $# -gt 0 ]; do
108
+ case "$1" in
109
+ --strict) STRICT="--strict"; shift ;;
110
+ --block-high) BLOCK_HIGH="--block-high"; shift ;;
111
+ *) shift ;;
112
+ esac
113
+ done
114
+
115
+ RED='\033[0;31m'
116
+ YELLOW='\033[1;33m'
117
+ GREEN='\033[0;32m'
118
+ CYAN='\033[0;36m'
119
+ NC='\033[0m'
120
+
121
+ CRITICAL=0
122
+ HIGH=0
123
+ MEDIUM=0
124
+ LOW=0
125
+
126
+ echo "=========================================="
127
+ echo "Semantic Test-Authenticity Detector - Quality Gate #10"
128
+ echo "=========================================="
129
+ echo ""
130
+
131
+ report() {
132
+ local severity="$1"
133
+ local file="$2"
134
+ local line="$3"
135
+ local message="$4"
136
+
137
+ case "$severity" in
138
+ CRITICAL) echo -e "${RED}[CRITICAL]${NC} $file:$line - $message"; ((CRITICAL++)) ;;
139
+ HIGH) echo -e "${RED}[HIGH]${NC} $file:$line - $message"; ((HIGH++)) ;;
140
+ MEDIUM) echo -e "${YELLOW}[MEDIUM]${NC} $file:$line - $message"; ((MEDIUM++)) ;;
141
+ LOW) echo -e "${CYAN}[LOW]${NC} $file:$line - $message"; ((LOW++)) ;;
142
+ esac
143
+ }
144
+
145
+ # Shared find filter for JS/TS test files (Python tests use different syntax and
146
+ # are out of scope for the literal/mock-echo checks here).
147
+ find_js_tests() {
148
+ find "$PROJECT_DIR" \( -name "*.test.ts" -o -name "*.test.js" \
149
+ -o -name "*.test.tsx" -o -name "*.test.jsx" \
150
+ -o -name "*.spec.ts" -o -name "*.spec.js" \) 2>/dev/null \
151
+ | grep -v node_modules | grep -v dist
152
+ }
153
+
154
+ # A line whose expect(...) target contains a function call has provenance
155
+ # through code and must NEVER be flagged. Returns 0 (true) if the actual value
156
+ # passed to expect() contains a call like foo(...) -- including method calls.
157
+ # Conservative: when in doubt, treat as "has a call" so we do not flag.
158
+ expect_target_has_call() {
159
+ local line="$1"
160
+ # Extract the actual value passed to expect( ... ) up to .toBe / .toEqual.
161
+ local inner
162
+ inner=$(echo "$line" | sed -E 's/.*expect\((.*)\)\.(toBe|toEqual|toStrictEqual|toMatchObject).*/\1/')
163
+ # If sed did not strip anything (no match), be conservative -> treat as call.
164
+ if [ "$inner" = "$line" ]; then
165
+ return 0
166
+ fi
167
+ # A call looks like identifier( ... ). Method chains (.foo()) also count.
168
+ if echo "$inner" | grep -qE '[A-Za-z_$][A-Za-z0-9_$]*[[:space:]]*\('; then
169
+ return 0
170
+ fi
171
+ return 1
172
+ }
173
+
174
+ # ----------------------------------------------------------------------------
175
+ # Check 1: Literal-via-variable echo (HIGH)
176
+ # const|let|var NAME = <literal>; then expect(NAME).toBe(<same literal>)
177
+ # with NO function call in the assertion target and no reassignment of NAME
178
+ # between the binding and the assertion.
179
+ # ----------------------------------------------------------------------------
180
+ echo -e "${CYAN}Scanning for literal-via-variable echo (fake assertions)...${NC}"
181
+ while IFS= read -r test_file; do
182
+ [ -f "$test_file" ] || continue
183
+ rel_path="${test_file#$PROJECT_DIR/}"
184
+
185
+ # Find variable bindings to a bare string or integer literal.
186
+ # const x = "hello"; let n = 42; var s = 'ok';
187
+ while IFS=: read -r bind_lineno bind_line; do
188
+ [ -n "$bind_lineno" ] || continue
189
+ var_name=$(echo "$bind_line" | sed -E "s/^[[:space:]]*(const|let|var)[[:space:]]+([A-Za-z_\$][A-Za-z0-9_\$]*)[[:space:]]*=.*/\2/")
190
+ lit=$(echo "$bind_line" | sed -E "s/^[[:space:]]*(const|let|var)[[:space:]]+[A-Za-z_\$][A-Za-z0-9_\$]*[[:space:]]*=[[:space:]]*//; s/;[[:space:]]*$//")
191
+ [ -n "$var_name" ] || continue
192
+ [ -n "$lit" ] || continue
193
+
194
+ # Defense-in-depth: the binding grep below already pins the RHS to a pure
195
+ # single string ('..'/"..") or integer literal, so this re-confirms the
196
+ # extracted literal is exactly that shape. We do NOT additionally reject
197
+ # literals that merely contain operator characters (- + * / & | etc.):
198
+ # those are valid string contents (e.g. "loki-mode", a URL, a path), and
199
+ # rejecting them would silently miss real fakes. $lit is only ever used in
200
+ # a string equality test ([ "$expected" = "$lit" ]), never as a regex, so
201
+ # punctuation in the literal is harmless.
202
+ if ! echo "$lit" | grep -qE "^('[^']*'|\"[^\"]*\"|[0-9]+)$"; then
203
+ continue
204
+ fi
205
+
206
+ # Scan a forward window for reassignment (disqualifies) or an assertion
207
+ # echoing the same literal back. Window lines come with absolute line nums.
208
+ window_end=$((bind_lineno + 8))
209
+ flagged_lineno=""
210
+ reassigned=false
211
+ while IFS=: read -r a_lineno a_line; do
212
+ [ -n "$a_lineno" ] || continue
213
+ # Reassignment of the same var (NAME = ... but not const/let/var NAME =).
214
+ if echo "$a_line" | grep -qE "(^|[^A-Za-z0-9_\$.])${var_name}[[:space:]]*=[^=]" \
215
+ && ! echo "$a_line" | grep -qE "(const|let|var)[[:space:]]+${var_name}[[:space:]]*="; then
216
+ reassigned=true
217
+ break
218
+ fi
219
+ # Assertion echoing the same literal back.
220
+ if echo "$a_line" | grep -qE "expect\([[:space:]]*${var_name}[[:space:]]*\)\.(toBe|toEqual|toStrictEqual)\("; then
221
+ if expect_target_has_call "$a_line"; then
222
+ continue
223
+ fi
224
+ expected=$(echo "$a_line" | sed -E "s/.*expect\([[:space:]]*${var_name}[[:space:]]*\)\.(toBe|toEqual|toStrictEqual)\((.*)\).*/\2/")
225
+ expected=$(echo "$expected" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
226
+ if [ "$expected" = "$lit" ]; then
227
+ flagged_lineno="$a_lineno"
228
+ fi
229
+ fi
230
+ done < <(awk -v s="$((bind_lineno + 1))" -v e="$window_end" 'NR>=s && NR<=e {print NR":"$0}' "$test_file" 2>/dev/null)
231
+
232
+ if [ "$reassigned" = false ] && [ -n "$flagged_lineno" ]; then
233
+ report "HIGH" "$rel_path" "$flagged_lineno" "Literal echo: variable '$var_name' bound to literal $lit and asserted == same literal with no transformation (verifies nothing)"
234
+ fi
235
+ done < <(grep -nE "^[[:space:]]*(const|let|var)[[:space:]]+[A-Za-z_\$][A-Za-z0-9_\$]*[[:space:]]*=[[:space:]]*('[^']*'|\"[^\"]*\"|[0-9]+)[[:space:]]*;[[:space:]]*$" "$test_file" 2>/dev/null)
236
+ done < <(find_js_tests)
237
+
238
+ # ----------------------------------------------------------------------------
239
+ # Check 2: Mock-return echo (MEDIUM)
240
+ # Within a small window, a mock is configured to return a literal and an
241
+ # assertion checks that SAME literal. MEDIUM because provenance is uncertain.
242
+ # ----------------------------------------------------------------------------
243
+ echo -e "${CYAN}Scanning for mock-return echo...${NC}"
244
+ while IFS= read -r test_file; do
245
+ [ -f "$test_file" ] || continue
246
+ rel_path="${test_file#$PROJECT_DIR/}"
247
+
248
+ while IFS=: read -r mret_lineno mret_line; do
249
+ [ -n "$mret_lineno" ] || continue
250
+ mlit=$(echo "$mret_line" | sed -E "s/.*(mockReturnValueOnce|mockResolvedValueOnce|mockReturnValue|mockResolvedValue|\.returns)\((.*)/\2/")
251
+ mlit=$(echo "$mlit" | sed -E 's/\).*$//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/;[[:space:]]*$//')
252
+ # Only bare string/number literals.
253
+ echo "$mlit" | grep -qE "^('[^']*'|\"[^\"]*\"|[0-9]+)$" || continue
254
+
255
+ window_end=$((mret_lineno + 12))
256
+ while IFS= read -r a_line; do
257
+ echo "$a_line" | grep -qE "\.(toBe|toEqual|toStrictEqual)\(" || continue
258
+ aexpected=$(echo "$a_line" | sed -E "s/.*\.(toBe|toEqual|toStrictEqual)\((.*)\).*/\2/")
259
+ aexpected=$(echo "$aexpected" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
260
+ if [ "$aexpected" = "$mlit" ]; then
261
+ report "MEDIUM" "$rel_path" "$mret_lineno" "Mock-return echo: mock configured to return $mlit and an assertion checks the same literal nearby (may assert the mock, not real behavior)"
262
+ break
263
+ fi
264
+ done < <(awk -v s="$((mret_lineno + 1))" -v e="$window_end" 'NR>=s && NR<=e {print}' "$test_file" 2>/dev/null)
265
+ done < <(grep -nE "(mockReturnValue|mockResolvedValue|mockReturnValueOnce|mockResolvedValueOnce|\.returns)\(" "$test_file" 2>/dev/null)
266
+ done < <(find_js_tests)
267
+
268
+ # ----------------------------------------------------------------------------
269
+ # Check 3: Deleted/weakened assertions across commits (MEDIUM)
270
+ # A commit that REMOVES more assertion lines than it adds from a test file
271
+ # (the test still passes because the failing check is gone).
272
+ # Degrades silently outside a git repo (mirrors detect-test-mutations.sh:144).
273
+ # ----------------------------------------------------------------------------
274
+ echo -e "${CYAN}Scanning for deleted assertions in recent commits...${NC}"
275
+ COMMITS_TO_CHECK=$(cd "$PROJECT_DIR" && git log --oneline -5 --format='%H' 2>/dev/null || true)
276
+ if [ -n "$COMMITS_TO_CHECK" ]; then
277
+ for commit in $COMMITS_TO_CHECK; do
278
+ changed_test_files=$(cd "$PROJECT_DIR" && git diff-tree --no-commit-id --name-only -r "$commit" 2>/dev/null \
279
+ | grep -E '\.(test|spec)\.(ts|js|tsx|jsx)$' || true)
280
+ [ -z "$changed_test_files" ] && continue
281
+
282
+ # shellcheck disable=SC2086
283
+ diff_text=$(cd "$PROJECT_DIR" && git diff "$commit^" "$commit" -- $changed_test_files 2>/dev/null || true)
284
+ [ -z "$diff_text" ] && continue
285
+
286
+ assert_re='(\.toBe\(|\.toEqual\(|\.toStrictEqual\(|\.toThrow\(|\.toHaveBeenCalled|strictEqual\(|deepEqual\(|assert\.|assertEqual\()'
287
+ removed=$(echo "$diff_text" | grep -E '^-' | grep -vE '^---' | grep -cE "$assert_re" || true)
288
+ added=$(echo "$diff_text" | grep -E '^\+' | grep -vE '^\+\+\+' | grep -cE "$assert_re" || true)
289
+ removed="${removed:-0}"; added="${added:-0}"
290
+ removed=$(echo "$removed" | tr -d '[:space:]'); added=$(echo "$added" | tr -d '[:space:]')
291
+
292
+ net_removed=$((removed - added))
293
+ if [ "$net_removed" -gt 0 ]; then
294
+ short_hash=$(echo "$commit" | cut -c1-8)
295
+ report "MEDIUM" "commit:$short_hash" "1" "Net $net_removed assertion line(s) removed from test files ($removed removed, $added added) -- weakened tests may still pass"
296
+ fi
297
+ done
298
+ fi
299
+
300
+ # Summary
301
+ echo ""
302
+ echo "=========================================="
303
+ TOTAL=$((CRITICAL + HIGH + MEDIUM + LOW))
304
+ echo "Results: $TOTAL finding(s)"
305
+ echo " CRITICAL: $CRITICAL"
306
+ echo " HIGH: $HIGH"
307
+ echo " MEDIUM: $MEDIUM"
308
+ echo " LOW: $LOW"
309
+ echo "=========================================="
310
+
311
+ # --strict takes precedence: block on CRITICAL or HIGH (mirrors gate 5).
312
+ if [ "$STRICT" = "--strict" ]; then
313
+ if [ $CRITICAL -gt 0 ] || [ $HIGH -gt 0 ]; then
314
+ echo ""
315
+ echo -e "${RED}GATE FAILED: $CRITICAL critical + $HIGH high findings${NC}"
316
+ exit 1
317
+ fi
318
+ fi
319
+
320
+ # --block-high: exit 2 only when HIGH/CRITICAL present (clean wrapper contract).
321
+ if [ "$BLOCK_HIGH" = "--block-high" ]; then
322
+ if [ $CRITICAL -gt 0 ] || [ $HIGH -gt 0 ]; then
323
+ echo -e "${RED}GATE FAILED: $((CRITICAL + HIGH)) HIGH-severity finding(s)${NC}"
324
+ exit 2
325
+ fi
326
+ fi
327
+
328
+ if [ $TOTAL -eq 0 ]; then
329
+ echo -e "${GREEN}All tests pass semantic authenticity gate.${NC}"
330
+ fi
331
+
332
+ exit 0