hugin/scripts/tool-smoke-test.sh

356 lines
22 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Rauchtest mit den echten Werkzeugen (→ KONZEPT.md 17.2): Server starten,
# Testpakete mit Erkennungswörtern veröffentlichen, per `hugin sync` in ein
# leeres Repo installieren und Claude Code und OpenCode fragen. Dazu die
# Verhaltensfragen 4, 5 und 8 aus Stufe 0 sowie opencode/autoinvoke (→ STUFE0.md).
#
# Dazu: Installiert ein Agent ein Paket auf Bitte richtig, also über .hugin.yaml und
# hugin sync, wie es der MCP-Endpunkt von Hugin vorgibt, statt Dateien selbst zu schreiben?
# Und bricht er ab, statt Dateien selbst zu schreiben, wenn hugin nicht im PATH ist?
#
# Braucht Zugänge zu beiden Werkzeugen und kostet Tokens (rund 30 Modellaufrufe).
# Ergebnis: COMPATIBILITY.md im Wurzelordner.
#
# scripts/tool-smoke-test.sh
# TOOLS=opencode scripts/tool-smoke-test.sh # nur ein Werkzeug
# CLAUDE_MODEL=sonnet OPENCODE_MODEL=anthropic/claude-haiku-5-5 scripts/tool-smoke-test.sh
# OPENCODE=opencode2 OUT=/tmp/c.md scripts/tool-smoke-test.sh
# KEEP=1 scripts/tool-smoke-test.sh # Arbeitsordner und Logs bleiben
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WORK="$(mktemp -d)"
PORT="${PORT:-8766}"
URL="http://127.0.0.1:$PORT"
OUT="${OUT:-$ROOT/COMPATIBILITY.md}"
TOOLS="${TOOLS:-claude opencode}"
OC="${OPENCODE:-opencode}"
CLAUDE_MODEL="${CLAUDE_MODEL:-haiku}"
OPENCODE_MODEL="${OPENCODE_MODEL:-}"
TIMEOUT="${TIMEOUT:-180}" # Sekunden je Aufruf
TRIES="${TRIES:-2}" # Modelle antworten nicht immer gleich
export HUGIN_DATABASE_URL="sqlite:///$WORK/hugin.db"
export HUGIN_ADMIN_TOKEN="tool-smoke-admin"
ADMIN=(-H "Authorization: Bearer $HUGIN_ADMIN_TOKEN")
RESULTS="$WORK/results.tsv"
mkdir -p "$WORK/logs" && : > "$RESULTS"
cleanup() {
[[ -n "${SERVER:-}" ]] && kill "$SERVER" 2>/dev/null || true
if [[ -z "${KEEP:-}" ]]; then rm -rf "$WORK"; else echo "Arbeitsordner: $WORK"; fi
}
trap cleanup EXIT
uses() { [[ " $TOOLS " == *" $1 "* ]]; }
uses claude && ! command -v claude >/dev/null && { echo "claude fehlt (oder TOOLS setzen)"; exit 2; }
uses opencode && ! command -v "$OC" >/dev/null && { echo "$OC fehlt (oder TOOLS setzen)"; exit 2; }
api() { curl -fsS -H "Content-Type: application/json" "$@"; }
key() { python3 -c 'import json,sys; print(json.load(sys.stdin)["key"])'; }
echo "== Server"
(cd "$ROOT/server" && uv run -q hugin-server migrate)
(cd "$ROOT/server" && exec uv run -q hugin-server run --host 127.0.0.1 --port "$PORT") >"$WORK/server.log" 2>&1 &
SERVER=$!
for _ in $(seq 50); do curl -fsS "$URL/readyz" >/dev/null 2>&1 && break; sleep 0.2; done
curl -fsS "$URL/readyz" >/dev/null
api "${ADMIN[@]}" "$URL/api/v1/admin/tenants" -d '{"slug":"plattform","name":"Plattform"}' >/dev/null
api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/projects" -d '{"slug":"kern","name":"Kern"}' >/dev/null
AUTHOR=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/keys" -d '{"kind":"project","name":"autor","project":"kern","roles":["pub_author"]}' | key)
REVIEWER=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/keys" -d '{"kind":"project","name":"pruefer","project":"kern","roles":["pub_reviewer"]}' | key)
READER=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/keys" -d '{"kind":"project","name":"leser","project":"kern"}' | key)
# Lokaler Server hat Risiko „Skripte“: persönlicher Key, zwei Freigaben von Personen
# Plugin hat Risiko „Rechte“: eine der zwei Freigaben vom Tenant-Admin
person() { api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/people" -d "{\"name\":\"$1\",\"mail\":\"$1@smoke.test\",\"project\":\"kern\",\"roles\":$2}" | key; }
PERSON_AUTHOR=$(person autor '["pub_author"]'); PERSON_REVIEWER=$(person pruefer '["pub_reviewer"]')
PERSON_REVIEWER2=$(person pruefer2 '["pub_reviewer"]'); PERSON_ADMIN=$(person admin '["pub_reviewer","tenant_admin"]')
# MCP-Paket zeigt auf diesen Hugin selbst; so lässt sich auch die Verbindung prüfen
PACKAGES="$WORK/packages"
cp -R "$ROOT/scripts/tool-smoke/packages" "$PACKAGES"
mkdir -p "$PACKAGES/pub-smoke-mcp/1.0.0"
cat > "$PACKAGES/pub-smoke-mcp/1.0.0/hugin.yaml" <<YAML
id: pub-smoke-mcp
kind: mcp
version: 1.0.0
owner: kern
scope: public
description: Bindet den Hugin des Rauchtests als MCP-Server an.
server:
name: hugin-smoke
url: $URL/mcp
headers:
Authorization: "Bearer {{env:HUGIN_SMOKE_KEY}}"
YAML
publish() {
local zip="$WORK/package.zip" base="$URL/api/v1/packages/$1/versions/1.0.0"
rm -f "$zip"; (cd "$PACKAGES/$1/1.0.0" && zip -qr "$zip" .)
curl -fsS -H "Authorization: Bearer $AUTHOR" -H "Content-Type: application/zip" --data-binary "@$zip" "$URL/api/v1/drafts" >/dev/null
api -H "Authorization: Bearer $AUTHOR" -X POST "$base/submit" >/dev/null
api -H "Authorization: Bearer $REVIEWER" "$base/approve" -d '{"comment":"Rauchtest"}' >/dev/null
}
# publish_example <paket> <prüfer…>: Beispielpaket aus contract/ mit Annas persönlichem Key
publish_example() {
local zip="$WORK/package.zip" id=$1 base="$URL/api/v1/packages/$1/versions/1.0.0"; shift
rm -f "$zip"; (cd "$ROOT/contract/examples/packages/$id/1.0.0" && zip -qr "$zip" .)
curl -fsS -H "Authorization: Bearer $PERSON_AUTHOR" -H "Content-Type: application/zip" --data-binary "@$zip" "$URL/api/v1/drafts" >/dev/null
api -H "Authorization: Bearer $PERSON_AUTHOR" -X POST "$base/submit" >/dev/null
for r in "$@"; do api -H "Authorization: Bearer $r" "$base/approve" -d '{"comment":"Rauchtest"}' >/dev/null; done
}
echo "== Pakete und Repo"
IDS=(pub-smoke-skill pub-smoke-manual pub-smoke-rules pub-smoke-command pub-smoke-agent pub-smoke-mcp)
for id in "${IDS[@]}"; do publish "$id"; done
# Lokaler MCP-Server (Python, nur Standardbibliothek): hugin sync installiert ihn nach
# HUGIN_MCP_HOME, die Werkzeuge starten ihn über hugin mcp run
publish_example pub-zeit-lokal "$PERSON_REVIEWER" "$PERSON_REVIEWER2"; IDS+=(pub-zeit-lokal)
# OpenCode-Plugin mit eigenem Werkzeug; Claude Code bekommt davon nichts
publish_example pub-specht-plugin "$PERSON_REVIEWER" "$PERSON_ADMIN"; IDS+=(pub-specht-plugin)
export HUGIN_MCP_HOME="$WORK/mcp" ZEIT_ZONE_HINWEIS=smoke
(cd "$ROOT/cli" && go build -o "$WORK/bin/hugin" ./cmd/hugin)
ln -s "$WORK/bin/hugin" "$WORK/hugin"
# PATH ohne hugin (für „Ohne hugin nichts schreiben“): alle Ordner weglassen, in denen eines liegt
NO_HUGIN_PATH=$(IFS=:; for d in $PATH; do [[ -x "$d/hugin" ]] || printf '%s:' "$d"; done); NO_HUGIN_PATH=${NO_HUGIN_PATH%:}
export PATH="$WORK/bin:$PATH" # für Agenten, die hugin sync selbst aufrufen
REPO="$WORK/repo"; mkdir -p "$REPO"; cd "$REPO"; git init -q
printf '# Rauchtest\n' > CLAUDE.md
export HUGIN_KEY="$READER" HUGIN_SMOKE_KEY="$READER"
"$WORK/hugin" init --server "$URL" --project kern >/dev/null
# Werkzeuge fest eintragen: im leeren Repo erkennt `auto` nur Claude (wegen CLAUDE.md)
sed -i.alt -e "s/^packages: \[\]/packages: [$(IFS=,; echo "${IDS[*]}")]/" \
-e "s/^tools: auto/tools: [$(echo $TOOLS | sed 's/ /, /g')]/" .hugin.yaml && rm -f .hugin.yaml.alt
"$WORK/hugin" sync
# Ausführen mit Zeitlimit; Modellaufrufe ohne Lesewerkzeuge, damit die Antwort
# aus dem geladenen Kontext kommt und nicht aus den Dateien im Repo
limit() { perl -e 'alarm shift; exec @ARGV' "$TIMEOUT" "$@" </dev/null; }
# Projekt-MCP-Server aus .mcp.json ohne Rückfrage zulassen; sonst stehen sie auf „Pending approval“
claude_ask() {
limit claude -p "$1" --model "$CLAUDE_MODEL" --settings '{"enableAllProjectMcpServers":true}' \
--disallowedTools "Read,Grep,Glob,Bash,WebFetch,WebSearch" --allowedTools "mcp__hugin-smoke__search_packages,mcp__zeit__zeit"
}
OC_DENY='{"permission":{"read":"deny","grep":"deny","glob":"deny","list":"deny","bash":"deny","webfetch":"deny","websearch":"deny"}}'
opencode_run() { OPENCODE_CONFIG_CONTENT="$OC_DENY" limit "$OC" run --pure ${OPENCODE_MODEL:+-m "$OPENCODE_MODEL"} "$@"; }
# --pure lädt keine Plugins; für das Plugin ohne (dann laden auch eigene globale Plugins)
opencode_plugins() { OPENCODE_CONFIG_CONTENT="$OC_DENY" limit "$OC" run ${OPENCODE_MODEL:+-m "$OPENCODE_MODEL"} "$@"; }
# Kann das Werkzeug überhaupt ein Modell aufrufen (Zugang, Anbieter)? Sonst
# zählen seine Modellprüfungen als „nicht geprüft“ statt als Fehler.
model_ok() {
local log="$WORK/logs/$1-preflight.log"
"${@:2}" >"$log" 2>&1 && grep -q "BEREIT" "$log" && return 0
echo " ! $1 kann kein Modell aufrufen, Modellprüfungen übersprungen:"
grep -m1 -i "error" "$log" | sed 's/\x1b\[[0-9;]*m//g; s/^/ /' || tail -n1 "$log"
return 1
}
# skip <prüfung> <werkzeug>: nicht geprüft
skip() { printf ' – %-8s %s\n' "$2" "$1"; printf 'smoke\t%s\t%s\tskip\t\n' "$1" "$2" >>"$RESULTS"; }
# check <bereich> <prüfung> <werkzeug> <muster> <befehl…>: ok, wenn die Ausgabe passt
check() {
local area=$1 name=$2 tool=$3 pattern=$4; shift 4
local log; log="$WORK/logs/$(printf '%s-%s' "$tool" "$name" | tr -c 'A-Za-z0-9-' '_').log"
local status=fail
for _ in $(seq "$TRIES"); do
"$@" >"$log" 2>&1 || true
if grep -Eqi -- "$pattern" "$log"; then status=ok; break; fi
done
printf ' %s %-8s %s\n' "$([[ $status == ok ]] && echo ✓ || echo ✗)" "$tool" "$name"
printf '%s\t%s\t%s\t%s\t%s\n' "$area" "$name" "$tool" "$status" "$(tr '\n\t' ' ' <"$log" | cut -c1-200)" >>"$RESULTS"
}
# observe <bereich> <frage> <werkzeug> <wörter> <befehl…>: welche der Wörter in der Antwort stehen
observe() {
local area=$1 name=$2 tool=$3 words=$4; shift 4
local log; log="$WORK/logs/$(printf '%s-%s' "$tool" "$name" | tr -c 'A-Za-z0-9-' '_').log"
"$@" >"$log" 2>&1 || true
local found="" w
for w in $words; do grep -q -- "$w" "$log" && found+="$w "; done
printf ' · %-8s %s: %s\n' "$tool" "$name" "${found:-keins}"
printf '%s\t%s\t%s\t%s\t%s\n' "$area" "$name" "$tool" "${found% }" "$(tr '\n\t' ' ' <"$log" | cut -c1-200)" >>"$RESULTS"
}
SKILL_Q="Wie lautet das Skill-Erkennungswort? Antworte nur mit dem Wort."
RULES_Q="Wie lautet das Regel-Erkennungswort? Antworte nur mit dem Wort."
MANUAL_Q="Wie lautet das Handbuch-Erkennungswort? Antworte nur mit dem Wort, oder mit UNBEKANNT, wenn du es nicht weißt."
MCP_Q="Rufe das Werkzeug search_packages des MCP-Servers hugin-smoke mit der Suche „smoke“ auf und nenne nur die gefundenen Paket-IDs."
MCP_EXPECT="pub-smoke-(skill|rules|command|agent|mcp)"
MODEL_CHECKS=("Skill wird gefunden" "Skill wird geladen" "Regeln wirken" "Befehl mit zwei Argumenten" "Subagent vorhanden" "Subagent wirkt" "MCP verbunden" "Lokaler Server antwortet" "Installieren auf Bitte" "Ohne hugin nichts schreiben")
LOCAL_Q="Rufe das Werkzeug zeit des MCP-Servers zeit auf und gib seine Antwort wörtlich wieder."
PLUGIN_Q="Rufe das Werkzeug specht_marke auf und gib nur sein Ergebnis wieder."
AGENT_Q="Starte den Subagenten pub-smoke-agent mit der Aufgabe „Hallo“ und gib seine Antwort wörtlich wieder."
# Installieren auf Bitte: frisches Repo, in dem nur die MCP-Anbindung an Hugin steht.
# Die Bitte sagt nicht, wie; den Weg (.hugin.yaml + hugin sync) muss der Agent aus den
# Anweisungen des MCP-Endpunkts bzw. dem Feld install von get_package kennen.
INSTALL_Q="Ich möchte den Befehl pub-smoke-command aus Hugin dauerhaft in diesem Repo nutzen. Richte das bitte ein."
# install_try <werkzeug> <datei> <befehl…>: ERGEBNIS=INSTALLIERT, wenn hugin sync das Paket geschrieben hat
install_try() {
local tool=$1 file=$2; shift 2
local repo="$WORK/install-$tool"
rm -rf "$repo" && mkdir -p "$repo" && cd "$repo" && git init -q
hugin init --server "$URL" --project kern >/dev/null
sed -i.alt -e "s/^packages: \[\]/packages: [pub-smoke-mcp]/" -e "s/^tools: auto/tools: [$tool]/" .hugin.yaml && rm -f .hugin.yaml.alt
hugin sync >/dev/null
if [[ -n "${WITHOUT_HUGIN:-}" ]]; then PATH="$NO_HUGIN_PATH" "$@" || true; else "$@" || true; fi
echo "--- .hugin.yaml"; cat .hugin.yaml
if grep -q "pub-smoke-command" hugin.lock && [[ -f "$file" ]]; then
echo "ERGEBNIS=INSTALLIERT"
elif [[ -f "$file" ]]; then
echo "ERGEBNIS=SELBST_GESCHRIEBEN (Datei da, aber nicht in hugin.lock)"
else
echo "ERGEBNIS=NICHT_INSTALLIERT"
fi
cd "$REPO"
}
# Ohne hugin im PATH: bestanden, wenn der Agent nichts selbst geschrieben hat
without_hugin() { WITHOUT_HUGIN=1 install_try "$@"; }
claude_install() {
limit "$CLAUDE_BIN" -p "$INSTALL_Q" --model "$CLAUDE_MODEL" --settings '{"enableAllProjectMcpServers":true}' \
--allowedTools "Read,Edit,Write,Bash(hugin *),mcp__hugin-smoke__search_packages,mcp__hugin-smoke__get_package" \
--disallowedTools "WebFetch,WebSearch"
}
OC_INSTALL='{"permission":{"read":"allow","edit":"allow","bash":"allow","webfetch":"deny","websearch":"deny"}}'
OC_BIN=$(command -v "$OC" || true); CLAUDE_BIN=$(command -v claude || true) # fest, weil der PATH wechselt
opencode_install() { OPENCODE_CONFIG_CONTENT="$OC_INSTALL" limit "$OC_BIN" run --pure ${OPENCODE_MODEL:+-m "$OPENCODE_MODEL"} "$INSTALL_Q"; }
if uses claude; then
echo "== Claude Code $(claude --version | cut -d' ' -f1)"
check smoke "MCP eingetragen" claude "hugin-smoke" limit claude mcp get hugin-smoke
# Projektserver stehen bis zur Zustimmung auf „Pending approval“; die Verbindung zeigt „Lokaler Server antwortet“
check smoke "Lokaler Server eingetragen" claude "Args: mcp run pub-zeit-lokal" limit claude mcp get zeit
if model_ok claude claude_ask "Antworte nur mit BEREIT."; then
check smoke "Skill wird gefunden" claude "pub-smoke-skill" \
claude_ask "Welche Skills stehen dir zur Verfügung? Nenne nur die Namen, einen pro Zeile."
check smoke "Skill wird geladen" claude "KRAKE-7381" claude_ask "$SKILL_Q"
check smoke "Regeln wirken" claude "MOEWE-4412" claude_ask "$RULES_Q"
check smoke "Befehl mit zwei Argumenten" claude "ERSTES=rot ZWEITES=fuchs" claude_ask "/pub-smoke-command rot fuchs"
check smoke "Subagent vorhanden" claude "pub-smoke-agent" \
claude_ask "Welche Subagent-Typen kannst du mit dem Agent-Werkzeug starten? Nenne nur die Namen, einen pro Zeile."
check smoke "Subagent wirkt" claude "FALKE-9035" claude_ask "$AGENT_Q"
check smoke "MCP verbunden" claude "$MCP_EXPECT" claude_ask "$MCP_Q"
check smoke "Lokaler Server antwortet" claude "UHR-4711" claude_ask "$LOCAL_Q"
check smoke "Installieren auf Bitte" claude "ERGEBNIS=INSTALLIERT" \
install_try claude .claude/commands/pub-smoke-command.md claude_install
check smoke "Ohne hugin nichts schreiben" claude "ERGEBNIS=(NICHT_INSTALLIERT)" \
without_hugin claude .claude/commands/pub-smoke-command.md claude_install
# Skill nur von Hand (disable-model-invocation): Modell darf ihn nicht selbst laden
observe stage0 manual-auto claude "IGEL-8140" claude_ask "$MANUAL_Q"
observe stage0 manual-slash claude "IGEL-8140" claude_ask "/pub-smoke-manual"
else
for c in "${MODEL_CHECKS[@]}"; do skip "$c" claude; done
fi
fi
if uses opencode; then
echo "== OpenCode $("$OC" --version)"
check smoke "MCP eingetragen" opencode "hugin-smoke" limit "$OC" mcp list --pure
check smoke "Skill wird gefunden" opencode "pub-smoke-skill" limit "$OC" debug skill --pure
check smoke "Subagent vorhanden" opencode "pub-smoke-agent" limit "$OC" agent list --pure
check smoke "MCP verbunden" opencode "hugin-smoke.*connected" limit "$OC" mcp list --pure
check smoke "Lokaler Server verbunden" opencode "zeit.*connected" limit "$OC" mcp list --pure
check smoke "Plugin geladen" opencode "plugins/pub-specht-plugin.ts" limit "$OC" debug config
if model_ok opencode opencode_run "Antworte nur mit BEREIT."; then
check smoke "Skill wird geladen" opencode "KRAKE-7381" opencode_run "$SKILL_Q"
check smoke "Regeln wirken" opencode "MOEWE-4412" opencode_run "$RULES_Q"
check smoke "Befehl mit zwei Argumenten" opencode "ERSTES=rot ZWEITES=fuchs" opencode_run --command pub-smoke-command rot fuchs
check smoke "Subagent wirkt" opencode "FALKE-9035" opencode_run "$AGENT_Q"
check smoke "Lokaler Server antwortet" opencode "UHR-4711" opencode_run "$LOCAL_Q"
check smoke "Plugin wirkt" opencode "SPECHT-5127" opencode_plugins "$PLUGIN_Q"
check smoke "Installieren auf Bitte" opencode "ERGEBNIS=INSTALLIERT" \
install_try opencode .opencode/commands/pub-smoke-command.md opencode_install
check smoke "Ohne hugin nichts schreiben" opencode "ERGEBNIS=(NICHT_INSTALLIERT)" \
without_hugin opencode .opencode/commands/pub-smoke-command.md opencode_install
# Skill nur von Hand (opencode/autoinvoke: "false")
observe stage0 manual-auto opencode "IGEL-8140" opencode_run "$MANUAL_Q"
observe stage0 manual-slash opencode "IGEL-8140" opencode_run --command pub-smoke-manual
OPENCODE_MODEL_OK=1
else
for c in "Skill wird geladen" "Regeln wirken" "Befehl mit zwei Argumenten" "Subagent wirkt" "Lokaler Server antwortet" "Plugin wirkt" "Installieren auf Bitte" "Ohne hugin nichts schreiben"; do skip "$c" opencode; done
fi
fi
# Stufe 0: je ein eigenes Repo ohne Hugin, nur mit den Dateien, um die es geht
WORDS_Q="Nenne alle Erkennungswörter (Form WORT-1234), die in deinen Anweisungen aus CLAUDE.md, AGENTS.md oder eingebundenen Dateien stehen. Nur die Wörter, nichts erfinden."
echo "== Stufe 0"
if uses opencode && [[ -n "${OPENCODE_MODEL_OK:-}" ]]; then
Q4="$WORK/q4"; mkdir -p "$Q4/.hugin/rules"; (cd "$Q4" && git init -q)
printf '# Projekt\n\nErkennungswort: DACHS-2207\n\n@.hugin/rules/q4.md\n' > "$Q4/CLAUDE.md"
printf 'Erkennungswort: OTTER-5590\n' > "$Q4/.hugin/rules/q4.md"
(cd "$Q4" && observe stage0 4 opencode "DACHS-2207 OTTER-5590" opencode_run "$WORDS_Q")
fi
if uses claude; then
Q5="$WORK/q5"; mkdir -p "$Q5"; (cd "$Q5" && git init -q)
printf '# Projekt\n\nErkennungswort: LUCHS-3318\n' > "$Q5/CLAUDE.md"
printf '# Projekt\n\nErkennungswort: ELCH-6624\n' > "$Q5/AGENTS.md"
(cd "$Q5" && observe stage0 5 claude "LUCHS-3318 ELCH-6624" claude_ask "$WORDS_Q")
fi
python3 - "$RESULTS" "$OUT" "$TOOLS" "$(uses claude && claude --version | cut -d' ' -f1)" \
"$(uses opencode && "$OC" --version)" "$CLAUDE_MODEL" "${OPENCODE_MODEL:-Standard}" <<'EOF'
import sys, datetime
results, out, tools, claude_v, oc_v, claude_m, oc_m = sys.argv[1:]
rows = [line.rstrip("\n").split("\t") for line in open(results) if line.strip()]
names = {"claude": f"Claude Code {claude_v}", "opencode": f"OpenCode {oc_v}"}
used = [t for t in ("claude", "opencode") if t in tools.split()]
smoke = {(r[1], r[2]): r[3] for r in rows if r[0] == "smoke"}
order = ["Skill wird gefunden", "Skill wird geladen", "Regeln wirken", "Befehl mit zwei Argumenten",
"Subagent vorhanden", "Subagent wirkt", "MCP eingetragen", "MCP verbunden", "Lokaler Server eingetragen", "Lokaler Server verbunden", "Lokaler Server antwortet", "Plugin geladen", "Plugin wirkt", "Installieren auf Bitte",
"Ohne hugin nichts schreiben"]
checks = [c for c in order if any(k[0] == c for k in smoke)]
package = {"Skill": "pub-smoke-skill", "Regeln": "pub-smoke-rules", "Befehl": "pub-smoke-command",
"Subagent": "pub-smoke-agent", "MCP": "pub-smoke-mcp", "Lokaler": "pub-zeit-lokal", "Plugin": "pub-specht-plugin", "Installieren": "pub-smoke-command", "Ohne": "pub-smoke-command"}
mark = {"ok": "✓", "fail": "✗", "skip": "–"}
lines = [
"# Kompatibilität", "",
f"Erzeugt von `scripts/tool-smoke-test.sh` am {datetime.date.today():%d.%m.%Y} (→ KONZEPT.md 17.2).",
f"Modelle: Claude Code `{claude_m}`, OpenCode `{oc_m}`. Installiert mit `hugin sync`.", "",
"## Rauchtests", "",
"| Paket | Prüfung | " + " | ".join(names[t] for t in used) + " |",
"|---|---|" + "---|" * len(used),
]
for c in checks:
pkg = next(v for k, v in package.items() if c.startswith(k))
lines.append(f"| `{pkg}` | {c} | " + " | ".join(mark.get(smoke.get((c, t), ""), "–") for t in used) + " |")
meaning = {
"4": ("OpenCode, nur `CLAUDE.md` mit `@`-Verweis, keine `AGENTS.md`, keine `instructions`", {
"DACHS-2207 OTTER-5590": "liest `CLAUDE.md` **und** löst `@` auf: Hugin-Regeln kämen doppelt an",
"DACHS-2207": "liest `CLAUDE.md`, löst `@` nicht auf: keine Doppel",
"": "liest `CLAUDE.md` nicht: keine Doppel"}),
"5": ("Claude Code, `CLAUDE.md` und `AGENTS.md`", {
"LUCHS-3318 ELCH-6624": "liest beide",
"LUCHS-3318": "liest nur `CLAUDE.md`",
"": "keins erkannt, Log ansehen"}),
}
stage0 = [r for r in rows if r[0] == "stage0" and not r[1].startswith("manual-")]
manual = {(r[1], r[2]): r[3] for r in rows if r[0] == "stage0" and r[1].startswith("manual-")}
if stage0 or manual:
lines += ["", "## Stufe 0", "", "| Frage | Aufbau | erkannte Wörter | Folgerung |", "|---|---|---|---|"]
for _, q, tool, found, _ in stage0:
setup, text = meaning[q]
lines.append(f"| {q} | {setup} | {found or '–'} | {text.get(found, 'unerwartet, Log ansehen')} |")
field = {"claude": "`disable-model-invocation: true`", "opencode": "`metadata: opencode/autoinvoke: \"false\"`"}
for tool in used:
if ("manual-auto", tool) not in manual:
continue
auto, slash = manual[("manual-auto", tool)], manual[("manual-slash", tool)]
if not slash:
text = "per `/name` nicht aufrufbar, Log ansehen"
elif auto:
text = "wirkt **nicht**: das Modell lädt den Skill selbst"
else:
text = "wirkt: Skill nur per `/name`"
lines.append(f"| autoinvoke | {names[tool]}, Skill mit {field[tool]} | selbst: {auto or '–'}, per `/`: {slash or '–'} | {text} |")
claude_cmd = smoke.get(("Befehl mit zwei Argumenten", "claude"))
if claude_cmd in ("ok", "fail"):
lines.append(f"| 8 | Claude Code {claude_v}, Befehl mit `arguments` und `$name` | – | "
+ ("benannte Argumente wirken" if claude_cmd == "ok" else "benannte Argumente wirken **nicht**") + " |")
lines += ["", "✓ bestanden · ✗ fehlgeschlagen · – nicht geprüft (kein Modellzugang).",
"Modellantworten schwanken; ein einzelnes ✗ mit `KEEP=1` wiederholen und die Logs ansehen.", ""]
open(out, "w").write("\n".join(lines))
print(f"\n{out} geschrieben.")
sys.exit(1 if any(v == "fail" for v in smoke.values()) else 0)
EOF