Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 26s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m35s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m22s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m12s
Both C#/TS language servers and the csharp-lsp MCP server were dead; all three are fixed and each demonstrated with a real find-all-references call in this repo. Root causes were one shape — a config naming a path this machine does not have, with nothing checking. None returned a wrong answer; each refused to start: - csharp-ls: MSBuildLocator needs a dotnet root owning host/fxr; Homebrew's bin has none, libexec does. - typescript-language-server: the LSP workspace root is the repo root but `typescript` lives in web/node_modules, and the plugin cannot pass a tsserver path (v5 dropped --tsserver-path; lspServers cannot set initializationOptions). - the csharp-lsp MCP server: .mcp.json named a dotnet install that no longer existed, while ~/.codex/config.toml's copy of the same server had been migrated. Both files are gitignored, so nothing could compare them. Corrects defect-shapes-773.md §5.1: the "workflow agents must use csharp-lsp" note names the MCP server's tools, which subagents DO reach — it was dead because the server could not start, not because agents cannot call it. The LSP tool is the one no subagent has been observed to resolve. Six cold review rounds. Five false greens were found in this PR's own verification code, each introduced by the fix for the previous one — extracted as #796. fixes #777 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
132 lines
6.0 KiB
Bash
Executable File
132 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Verify the local code-intelligence toolchain (ersatztv#777).
|
|
#
|
|
# Operator-run, NOT a CI gate: every dependency it checks is a developer-machine
|
|
# install (Homebrew dotnet, a global csharp-ls, web/node_modules), so there is no
|
|
# runner on which a red here would mean anything. It exists because the #777 root
|
|
# causes were both environment divergence — a config that silently pointed at a
|
|
# path this machine does not have — and that class is invisible until something
|
|
# looks. See docs/local-lsp-tooling.md.
|
|
#
|
|
# Exit 0 = every check ran AND passed. Exit 1 = at least one FAIL *or* SKIP —
|
|
# a skipped check is a non-result, not a success.
|
|
|
|
set -uo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
failures=0
|
|
checks=0
|
|
skipped=0
|
|
|
|
pass() { checks=$((checks + 1)); printf ' PASS %s\n' "$1"; }
|
|
fail() { checks=$((checks + 1)); failures=$((failures + 1)); printf ' FAIL %s\n' "$1"; }
|
|
# A skip is NOT a pass. Counting it as one would let the summary read "all checks
|
|
# passed" while the most important check never ran.
|
|
skip() { checks=$((checks + 1)); skipped=$((skipped + 1)); printf ' SKIP %s\n' "$1"; }
|
|
note() { printf ' %s\n' "$1"; }
|
|
|
|
echo "Local LSP toolchain check — $REPO_ROOT"
|
|
echo
|
|
|
|
# ---------------------------------------------------------------------------
|
|
echo "csharp-ls (the LSP tool's C# server)"
|
|
|
|
if ! command -v csharp-ls >/dev/null 2>&1; then
|
|
fail "csharp-ls is not on PATH"
|
|
note "install: dotnet tool install --global csharp-ls"
|
|
else
|
|
pass "csharp-ls on PATH ($(command -v csharp-ls))"
|
|
fi
|
|
|
|
# MSBuildLocator resolves the SDK next to the dotnet host it finds, and needs a
|
|
# sibling host/fxr. Homebrew's bin/dotnet has none — its real root is libexec.
|
|
dotnet_root="${DOTNET_ROOT:-}"
|
|
if [ -z "$dotnet_root" ]; then
|
|
if [ -n "${DOTNET_HOST_PATH:-}" ]; then
|
|
dotnet_root="$(dirname "$DOTNET_HOST_PATH")"
|
|
elif command -v dotnet >/dev/null 2>&1; then
|
|
dotnet_root="$(dirname "$(readlink -f "$(command -v dotnet)" 2>/dev/null || command -v dotnet)")"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$dotnet_root" ]; then
|
|
fail "no dotnet host could be resolved (DOTNET_ROOT, DOTNET_HOST_PATH, PATH all empty)"
|
|
elif compgen -G "$dotnet_root/host/fxr/*/libhostfxr.*" >/dev/null 2>&1; then
|
|
pass "dotnet root has host/fxr ($dotnet_root)"
|
|
else
|
|
fail "dotnet root has NO host/fxr — csharp-ls cannot initialize ($dotnet_root)"
|
|
note "this is the #777 failure: set DOTNET_ROOT to the root that owns host/fxr,"
|
|
note "e.g. /opt/homebrew/opt/dotnet/libexec on a Homebrew install."
|
|
note "Claude Code picks it up from .claude/settings.local.json -> env.DOTNET_ROOT"
|
|
note "(local, not the tracked settings.json — the value is machine-specific)."
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "typescript-language-server (the LSP tool's TS server)"
|
|
|
|
if ! command -v typescript-language-server >/dev/null 2>&1; then
|
|
fail "typescript-language-server is not on PATH"
|
|
note "install: npm install -g typescript-language-server"
|
|
else
|
|
pass "typescript-language-server on PATH"
|
|
fi
|
|
|
|
# v5 has no --tsserver-path and the plugin cannot pass initializationOptions, so
|
|
# the ONLY lever is making `typescript` resolvable from the workspace root, which
|
|
# for Claude Code is the repo root — not web/.
|
|
if [ -e "$REPO_ROOT/node_modules/typescript/lib/tsserver.js" ]; then
|
|
pass "typescript resolvable from the repo root"
|
|
else
|
|
fail "typescript NOT resolvable from the repo root — the TS server will refuse to start"
|
|
note "the package lives in web/node_modules; link it at the root:"
|
|
note " mkdir -p '$REPO_ROOT/node_modules'"
|
|
note " ln -sfn '$REPO_ROOT/web/node_modules/typescript' '$REPO_ROOT/node_modules/typescript'"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "csharp-lsp MCP server (the C# path subagents can actually reach)"
|
|
|
|
mcp_json="$REPO_ROOT/.mcp.json"
|
|
if [ ! -f "$mcp_json" ]; then
|
|
fail ".mcp.json not present (it is gitignored — see docs/local-lsp-tooling.md to recreate it)"
|
|
elif [ "${SKIP_MCP_SMOKE:-0}" = "1" ]; then
|
|
skip "csharp-lsp MCP server (SKIP_MCP_SMOKE=1) — NOT verified"
|
|
elif ! command -v perl >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then
|
|
fail "cannot run the MCP smoke test: perl and python3 are both required"
|
|
else
|
|
# Actually START the server and list its tools. Checking `[ -x command ]` instead
|
|
# would be vacuous: a DIRECTORY satisfies -x (`[ -x /bin ]` is true), so that
|
|
# predicate can report a pass for a server that cannot run at all.
|
|
#
|
|
# `exec @ARGV or die` is load-bearing. Without the `or die`, perl exits 0 when it
|
|
# cannot exec the command, the `if` reads that as success, and this branch prints
|
|
# a PASS having run nothing — the same false green one level up.
|
|
note "starting the MCP server (this takes ~30-60s on a cold build)..."
|
|
if smoke_out="$(perl -e 'alarm shift; exec @ARGV or die "exec failed: $!\n"' 240 \
|
|
python3 "$REPO_ROOT/scripts/mcp_smoke.py" "$mcp_json" csharp-lsp 200 \
|
|
--expect-server csharp-lsp-mcp \
|
|
--expect-tool csharp_set_workspace --expect-tool csharp_references 2>&1)"; then
|
|
pass "csharp-lsp MCP server starts and serves tools — ${smoke_out#OK: }"
|
|
else
|
|
fail "csharp-lsp MCP server did not come up"
|
|
note "${smoke_out:-(no output — the smoke test itself was killed)}"
|
|
note "#777's failure was an entry naming a dotnet install that no longer existed;"
|
|
note "a net8.0-targeted vendored clone on an SDK-10-only host fails the same way."
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
passed=$(( checks - failures - skipped ))
|
|
if [ "$failures" -eq 0 ] && [ "$skipped" -eq 0 ]; then
|
|
echo "All $checks checks passed."
|
|
elif [ "$failures" -eq 0 ]; then
|
|
echo "$passed passed, $skipped SKIPPED of $checks — a skipped check is not a passed one."
|
|
else
|
|
echo "$failures of $checks checks FAILED ($skipped skipped) — see docs/local-lsp-tooling.md."
|
|
fi
|
|
# A skip is a non-result, so it is not success: exit non-zero unless everything ran.
|
|
exit $(( (failures > 0 || skipped > 0) ? 1 : 0 ))
|