Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,18 @@ EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1 \
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) -g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) -g -O1 $(SANITIZE)
# The build system is the single source of truth for "is this binary
# instrumented": compiler-specific probes (__SANITIZE_ADDRESS__) miss
# clang's feature-check spelling and every non-ASan sanitizer, so the
# trap-UBSan leg once ran NATIVE timing budgets on an instrumented
# binary. Any non-empty SANITIZE ⇒ sanitized budgets everywhere.
ifneq ($(strip $(SANITIZE)),)
SANITIZED_DEFINE = -DCBM_SANITIZED_BUILD=1
else
SANITIZED_DEFINE =
endif
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE)

# TSan (can't combine with ASan)
TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
Expand Down
21 changes: 19 additions & 2 deletions scripts/run-tests-parallel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,17 @@ stamp_windows_build_dir() {
MINGW* | MSYS*) ;;
*) return 0 ;;
esac
local runner_dir_w me stamp_out reset_out
local runner_dir_w me norm_out stamp_out reset_out
runner_dir_w="$(cygpath -w "$(dirname "$RUNNER")")"
me="$(whoami | tr -d '\r')"
# Normalize FIRST: some runner images stamp EXPLICIT (non-inherited)
# Authenticated-Users ACEs onto the workspace tree, which /inheritance:r
# cannot strip and /grant:r does not touch (it replaces only the granted
# SIDs' own entries). /reset drops every explicit ACE and restores pure
# inheritance, so the protect-and-grant below starts from a known shape
# regardless of image provisioning.
norm_out=$(MSYS2_ARG_CONV_EXCL='*' icacls "$runner_dir_w" /reset /Q 2>&1) ||
echo "WARN: build-dir DACL normalize ($when) failed: $norm_out"
stamp_out=$(MSYS2_ARG_CONV_EXCL='*' icacls "$runner_dir_w" /inheritance:r \
/grant:r "${me}:(OI)(CI)F" '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' \
/Q 2>&1) || echo "WARN: build-dir DACL stamp ($when) failed (user=$me dir=$runner_dir_w): $stamp_out"
Expand All @@ -67,8 +75,9 @@ stamp_windows_build_dir() {
# even see WHETHER it had run.
if MSYS2_ARG_CONV_EXCL='*' icacls "$runner_dir_w" 2>/dev/null |
grep -qE 'Authenticated Users|CREATOR OWNER'; then
echo "WARN: build-dir DACL still grants cross-account mutation after $when stamp:"
echo "FAIL: build-dir DACL still grants cross-account mutation after $when stamp:"
MSYS2_ARG_CONV_EXCL='*' icacls "$runner_dir_w" 2>&1 | head -8
exit 1
else
echo "build-dir DACL stamped clean ($when, $runner_dir_w, user=$me)"
fi
Expand Down Expand Up @@ -215,6 +224,14 @@ run_one() {
fi
secs=$((SECONDS - t0))
summary=$(grep -E '^ [0-9]+ passed' "$LOGDIR/$s.log" | tail -1)
# A suite that exits 0 WITHOUT printing its completion summary ran zero
# tests as far as anyone can prove (mis-parsed argv, drifted registration
# macro, early return) — that is a failure, not a green with pass=0.
if [ "$rc" -eq 0 ] && [ -z "$summary" ]; then
rc=97
echo " FAIL: suite '$s' exited 0 without a completion summary (ran nothing?)" \
>> "$LOGDIR/$s.log"
fi
pass=$(printf '%s' "$summary" | sed -n 's/^ \([0-9]*\) passed.*/\1/p')
failn=$(printf '%s' "$summary" | sed -n 's/.* \([0-9]*\) failed.*/\1/p')
skip=$(printf '%s' "$summary" | sed -n 's/.* \([0-9]*\) skipped.*/\1/p')
Expand Down
30 changes: 24 additions & 6 deletions scripts/security-fuzz-random.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail

# The fuzzer IS python3; without it this gate would run zero mutations while
# still reporting green. Fail loudly instead.
command -v python3 >/dev/null 2>&1 || {
echo "FAIL: python3 missing — the fuzz gate cannot generate payloads" >&2
exit 1
}
# Reproducibility: every payload derives from a logged seed, so any crash is
# re-runnable by construction (CBM_FUZZ_SEED overrides for replay).
FUZZ_SEED="${CBM_FUZZ_SEED:-$$}"
echo "fuzz seed: $FUZZ_SEED (replay: CBM_FUZZ_SEED=$FUZZ_SEED)"

# Fuzz testing: feeds random/mutated inputs to the MCP server and CLI
# to find crashes, hangs, and memory errors. Runs for a limited time.
#
Expand All @@ -25,11 +36,15 @@ END_TIME=$((SECONDS + DURATION))

# Portable timeout
run_with_timeout() {
# PRESERVES the child's exit status: the whole gate exists to observe
# crash codes (139/134/136), and an `|| true` here once made EC
# unconditionally 0 — the crash checks were dead code and a SIGSEGV
# read green. Timeout reports 124, which the checks correctly ignore.
local secs="$1"; shift
if command -v timeout &>/dev/null; then
timeout "$secs" "$@" || true
timeout "$secs" "$@"
else
perl -e "alarm($secs); exec @ARGV" -- "$@" || true
perl -e "alarm($secs); exec @ARGV" -- "$@"
fi
}

Expand All @@ -43,6 +58,7 @@ while [ $SECONDS -lt $END_TIME ]; do
# Generate a random mutated JSON-RPC payload
PAYLOAD=$(python3 -c "
import random, string, json
random.seed($FUZZ_SEED + $ITERATIONS)

# Base valid request
base = {
Expand Down Expand Up @@ -111,8 +127,8 @@ except:
printf '%s\n%s\n' "$INIT" "$PAYLOAD" > "$FUZZ_TMPDIR/input.jsonl"

# Run and check for crashes (not exit code — we expect errors, just not crashes)
run_with_timeout 5 "$BINARY" < "$FUZZ_TMPDIR/input.jsonl" > /dev/null 2>&1
EC=$?
EC=0
run_with_timeout 5 "$BINARY" < "$FUZZ_TMPDIR/input.jsonl" > /dev/null 2>&1 || EC=$?

# 139 = SIGSEGV, 134 = SIGABRT, 136 = SIGFPE — real crashes
if [ $EC -eq 139 ] || [ $EC -eq 134 ] || [ $EC -eq 136 ]; then
Expand All @@ -135,7 +151,8 @@ while [ $SECONDS -lt $CLI_END ]; do
CLI_ITERATIONS=$((CLI_ITERATIONS + 1))

QUERY=$(python3 -c "
import random, string
import random
random.seed($FUZZ_SEED + 100000 + $CLI_ITERATIONS), string
parts = ['MATCH', 'WHERE', 'RETURN', '(', ')', '-[', ']->', '<-[', ']-',
'n', 'r', '.name', '.label', '=', '\"', \"'\", ';', '--', 'DROP',
'DELETE', 'ATTACH', 'DETACH', '*', 'COUNT', 'LIMIT', 'ORDER BY']
Expand All @@ -146,7 +163,8 @@ if random.random() > 0.5:
print(q)
" 2>/dev/null || echo "MATCH (n) RETURN n")

run_with_timeout 3 "$BINARY" cli query_graph "{\"query\":\"$QUERY\"}" > /dev/null 2>&1
EC=0
run_with_timeout 3 "$BINARY" cli query_graph "{\"query\":\"$QUERY\"}" > /dev/null 2>&1 || EC=$?
EC=$?

if [ $EC -eq 139 ] || [ $EC -eq 134 ] || [ $EC -eq 136 ]; then
Expand Down
77 changes: 64 additions & 13 deletions scripts/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ retire_account_daemon() {
fi
}

# Tolerate ERRORS, never CRASHES: the adversarial phases assert "doesn't
# crash", and a bare `|| true` discards exactly the crash exit the claim is
# about. rc >= 128 means killed by signal / fatal status (SIGSEGV=139,
# SIGABRT=134 — ASan aborts land here too).
run_no_crash() {
local label="$1"; shift
local rc=0
"$@" > /dev/null 2>&1 || rc=$?
if [ "$rc" -ge 128 ]; then
echo "FAIL $label: crashed (rc=$rc)"
exit 1
fi
return 0
}

TMPDIR=$(smoke_mktemp_dir)
DRYRUN_HOME=""
# On MSYS2/Windows, convert POSIX path to native Windows path for the binary
Expand Down Expand Up @@ -2596,7 +2611,12 @@ echo "--- Phase 9b: adversarial install/uninstall tests ---"
# completes without crash and prints "Detected agents:" line.
EMPTY_HOME=$(smoke_mktemp_dir)
mkdir -p "$EMPTY_HOME/.local/bin"
INSTALL_OUT=$(HOME="$EMPTY_HOME" LOCALAPPDATA="$EMPTY_HOME/AppData/Local" "$BINARY" install -y 2>&1) || true
INSTALL_RC=0
INSTALL_OUT=$(HOME="$EMPTY_HOME" LOCALAPPDATA="$EMPTY_HOME/AppData/Local" "$BINARY" install -y 2>&1) || INSTALL_RC=$?
if [ "$INSTALL_RC" -ge 128 ]; then
echo "FAIL 9b-1: install crashed (rc=$INSTALL_RC)"
exit 1
fi
if ! echo "$INSTALL_OUT" | grep -qi 'detected agents'; then
echo "FAIL 9b-1: install output missing 'Detected agents' line"
exit 1
Expand All @@ -2609,12 +2629,12 @@ rm -rf "$EMPTY_HOME"
IDEM_HOME=$(smoke_mktemp_dir)
mkdir -p "$IDEM_HOME/.claude" "$IDEM_HOME/.local/bin"
copy_smoke_binary "$IDEM_HOME/.local/bin/codebase-memory-mcp"
HOME="$IDEM_HOME" LOCALAPPDATA="$IDEM_HOME/AppData/Local" "$BINARY" install -y 2>&1 > /dev/null || true
run_no_crash 9b-2 env HOME="$IDEM_HOME" LOCALAPPDATA="$IDEM_HOME/AppData/Local" "$BINARY" install -y
IDEM_INSTALLER="$BINARY"
if [[ "$BINARY" == *.exe ]]; then
IDEM_INSTALLER="$IDEM_HOME/.local/bin/codebase-memory-mcp.exe"
fi
HOME="$IDEM_HOME" LOCALAPPDATA="$IDEM_HOME/AppData/Local" "$IDEM_INSTALLER" install -y 2>&1 > /dev/null || true
run_no_crash 9b-2-second env HOME="$IDEM_HOME" LOCALAPPDATA="$IDEM_HOME/AppData/Local" "$IDEM_INSTALLER" install -y
# Count MCP entries — should be exactly 1
COUNT=$(cat "$IDEM_HOME/.claude.json" 2>/dev/null | python3 -c "
import json, sys
Expand All @@ -2632,7 +2652,12 @@ rm -rf "$IDEM_HOME"
# 9b-3: Uninstall without prior install
CLEAN_HOME=$(smoke_mktemp_dir)
mkdir -p "$CLEAN_HOME/.claude" "$CLEAN_HOME/.local/bin"
UNINSTALL_OUT=$(HOME="$CLEAN_HOME" "$BINARY" uninstall -y -n 2>&1) || true
UNINSTALL_RC=0
UNINSTALL_OUT=$(HOME="$CLEAN_HOME" "$BINARY" uninstall -y -n 2>&1) || UNINSTALL_RC=$?
if [ "$UNINSTALL_RC" -ge 128 ]; then
echo "FAIL 9b-3: uninstall crashed (rc=$UNINSTALL_RC)"
exit 1
fi
echo "OK 9b-3: uninstall without install doesn't crash"
retire_account_daemon "9b-3-cleanup"
rm -rf "$CLEAN_HOME"
Expand All @@ -2642,7 +2667,7 @@ CORRUPT_HOME=$(smoke_mktemp_dir)
mkdir -p "$CORRUPT_HOME/.claude" "$CORRUPT_HOME/.local/bin"
copy_smoke_binary "$CORRUPT_HOME/.local/bin/codebase-memory-mcp"
echo '{invalid json here' > "$CORRUPT_HOME/.claude.json"
HOME="$CORRUPT_HOME" "$BINARY" install -y 2>&1 > /dev/null || true
run_no_crash 9b-4 env HOME="$CORRUPT_HOME" "$BINARY" install -y
# Should either fix it or handle gracefully — not crash
echo "OK 9b-4: install over corrupt JSON doesn't crash"
retire_account_daemon "9b-4-cleanup"
Expand All @@ -2652,13 +2677,13 @@ rm -rf "$CORRUPT_HOME"
DBL_HOME=$(smoke_mktemp_dir)
mkdir -p "$DBL_HOME/.claude" "$DBL_HOME/.local/bin"
copy_smoke_binary "$DBL_HOME/.local/bin/codebase-memory-mcp"
HOME="$DBL_HOME" "$BINARY" install -y 2>&1 > /dev/null || true
run_no_crash 9b-8-install env HOME="$DBL_HOME" "$BINARY" install -y
DBL_UNINSTALLER="$BINARY"
if [[ "$BINARY" == *.exe ]]; then
DBL_UNINSTALLER="$DBL_HOME/.local/bin/codebase-memory-mcp.exe"
fi
HOME="$DBL_HOME" "$DBL_UNINSTALLER" uninstall -y -n 2>&1 > /dev/null || true
HOME="$DBL_HOME" "$BINARY" uninstall -y -n 2>&1 > /dev/null || true
run_no_crash 9b-8-first env HOME="$DBL_HOME" "$DBL_UNINSTALLER" uninstall -y -n
run_no_crash 9b-8-second env HOME="$DBL_HOME" "$BINARY" uninstall -y -n
echo "OK 9b-8: double uninstall doesn't crash"
retire_account_daemon "9b-8-cleanup"
rm -rf "$DBL_HOME"
Expand Down Expand Up @@ -2783,7 +2808,8 @@ echo "=== Phase 11: process kill E2E ==="
# Start MCP server in background
MCP_KILL_INPUT=$(smoke_mktemp_file)
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"kill-test","version":"1.0"}}}' > "$MCP_KILL_INPUT"
"$BINARY" < "$MCP_KILL_INPUT" > /dev/null 2>&1 &
KILL_OUT=$(smoke_mktemp_file)
"$BINARY" < "$MCP_KILL_INPUT" > "$KILL_OUT" 2>&1 &
KILL_PID=$!
sleep 1

Expand All @@ -2798,8 +2824,20 @@ if kill -0 "$KILL_PID" 2>/dev/null; then
fi
echo "OK 11c-d: process killed successfully"
else
echo "OK 11: MCP server already exited (clean shutdown on EOF)"
# The server ended before the probe: distinguish clean shutdown-on-EOF
# from an instant crash — the old branch treated both as OK with the
# output discarded, so a startup segfault read as a clean pass on any
# fast machine. A clean run must have exited 0 AND produced the
# initialize response.
KILL_RC=0
wait "$KILL_PID" 2>/dev/null || KILL_RC=$?
if [ "$KILL_RC" -ge 128 ] || ! grep -q '"jsonrpc"' "$KILL_OUT"; then
echo "FAIL 11: server exited before probe with rc=$KILL_RC and $(wc -c < "$KILL_OUT") bytes of output — crash, not clean EOF shutdown"
exit 1
fi
echo "OK 11: MCP server already exited (clean shutdown on EOF, initialize answered)"
fi
rm -f "$KILL_OUT"

rm -f "$MCP_KILL_INPUT"

Expand Down Expand Up @@ -3228,13 +3266,24 @@ fi
echo ""
echo "=== Phase 15: UI HTTP server ==="

UI_PORT=19876
# A kernel-assigned free port instead of a fixed one: a squatter on a fixed
# port made the whole phase read as SKIP. The tiny TOCTOU window between
# probe and bind is the residual risk, not the common case.
UI_PORT=$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')
UI_INPUT=$(smoke_mktemp_file)
"$BINARY" --port "$UI_PORT" < "$UI_INPUT" > /dev/null 2>&1 &
UI_PID=$!
sleep 1
# Readiness poll instead of a fixed sleep: SKIP is legitimate ONLY when the
# process exited (the documented no-embedded-assets case); a slow start on a
# loaded runner must not masquerade as it.
UI_READY=0
for _ in $(seq 1 100); do
if ! kill -0 "$UI_PID" 2>/dev/null; then break; fi
if curl -sf "http://127.0.0.1:$UI_PORT/" -o /dev/null 2>/dev/null; then UI_READY=1; break; fi
sleep 0.1
done

if kill -0 "$UI_PID" 2>/dev/null; then
if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/dev/null; then
# 15a: GET / returns 200 with HTML content
UI_BODY=$(curl -sf "http://127.0.0.1:$UI_PORT/" 2>/dev/null || echo "")
if echo "$UI_BODY" | grep -qi "<html"; then
Expand All @@ -3258,6 +3307,8 @@ if kill -0 "$UI_PID" 2>/dev/null; then
echo "SKIP 15b: /rpc not reachable"
else
echo "FAIL 15b: /rpc did not return JSON-RPC"
kill "$UI_PID" 2>/dev/null || true
exit 1
fi

kill "$UI_PID" 2>/dev/null || true
Expand Down
25 changes: 25 additions & 0 deletions scripts/soak-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,19 @@ mcp_call() {
MCP_LAST_RESPONSE="$resp"
if json_rpc_response_ok "$id" "$resp"; then
exit_code=0
else
# A stale (previous, late) response desyncs every later
# round-trip: drain queued lines until the matching id shows up
# or the stream runs dry, so one slow reply costs one failed op
# instead of corrupting the whole run's verdicts.
local drain=""
while read -r -t 2 drain <&4 2>/dev/null; do
MCP_LAST_RESPONSE="$drain"
if json_rpc_response_ok "$id" "$drain"; then
exit_code=0
break
fi
done
fi
fi
local t1
Expand Down Expand Up @@ -426,7 +439,9 @@ wait_for_diagnostics_snapshot() {
return 1
}

SNAPSHOT_ATTEMPTS=0
collect_snapshot() {
SNAPSHOT_ATTEMPTS=$((SNAPSHOT_ATTEMPTS + 1))
refresh_diagnostics_paths || return 0
if [ -f "$DIAG_FILE" ]; then
python3 -c "
Expand Down Expand Up @@ -681,6 +696,16 @@ fi

# ── Analysis ─────────────────────────────────────────────────────

# Check 0: the analysis must have DATA. Snapshot collection silently
# tolerates transient misses, so a run where diagnostics never materialized
# used to sail through every check vacuously — the leak/FD/latency verdicts
# below are only meaningful if most sampling attempts actually landed.
SNAPSHOT_ROWS=$(($(wc -l < "$METRICS_CSV") - 1))
if [ "$SNAPSHOT_ATTEMPTS" -gt 0 ] && [ $((SNAPSHOT_ROWS * 10)) -lt $((SNAPSHOT_ATTEMPTS * 6)) ]; then
echo "FAIL: only ${SNAPSHOT_ROWS}/${SNAPSHOT_ATTEMPTS} snapshots collected — analysis would be vacuous" | tee -a "$SUMMARY"
PASS=false
fi

# Check 1: Memory leak detection via RSS trend
# This is the primary leak detector on ALL platforms (including Windows
# where LeakSanitizer is unavailable). Catches both linear leaks (slope)
Expand Down
15 changes: 13 additions & 2 deletions scripts/test-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,16 @@ foreach ($t in $guards) {
} elseif ($code -eq 1 -or $t -eq "tests\windows\test_windows_launcher.py") {
Write-Host "RED ($t) - REGRESSION: a fixed Windows bug is broken again" -ForegroundColor Red
$guardFailures += $t
} else {
Write-Host "PRECONDITION ($t) exit=$code - skipped (see message above)" -ForegroundColor Yellow
} elseif ($code -eq 2) {
# Exit 2 is the guards' DOCUMENTED precondition-skip contract; every
# other unexpected code (a crashed python, an access-violation status,
# a mistyped guard) is a FAILURE - an uncontracted exit once let a
# crashing guard read as an invisible skip under a green banner.
Write-Host "PRECONDITION ($t) exit=2 - skipped (see message above)" -ForegroundColor Yellow
$guardSkips += $t
} else {
Write-Host "FAILED ($t) exit=$code - crashed or exited outside the guard contract 0/1/2" -ForegroundColor Red
$guardFailures += $t
}
}

Expand Down Expand Up @@ -266,6 +273,10 @@ if ($guardSkips.Count -gt 0) {
if ($fixedKeepers.Count -gt 0) {
Write-Host ("Known-red repros that are now GREEN (promote to guards): {0}" -f ($fixedKeepers -join ", ")) -ForegroundColor Green
}
if ($guardSkips.Count -eq $guards.Count -and $guards.Count -gt 0) {
Write-Host "FAIL: every guard skipped - nothing was actually verified" -ForegroundColor Red
exit 1
}
if ($guardFailures.Count -gt 0) {
Write-Host ("REGRESSION: {0} green guard(s) went red: {1}" -f $guardFailures.Count, ($guardFailures -join ", ")) -ForegroundColor Red
Write-Host "A previously-fixed Windows bug is broken again (see the guard's docstring and its referenced issue)." -ForegroundColor Red
Expand Down
Loading
Loading