H-337: add local Captain acceptance fixture (#63)
* H-337: add local Captain acceptance fixture * fix(H-337): close acceptance fixture review gaps * fix(H-337): reject non-string skill metadata --------- Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/gochat-acceptance-scripts.XXXXXX")
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
|
||||
fail() {
|
||||
echo "acceptance scripts test failed: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
make_source() {
|
||||
mkdir -p "$1"
|
||||
cat > "$1/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: fixture
|
||||
description: Local fixture
|
||||
---
|
||||
Use the local fixture.
|
||||
EOF
|
||||
printf '%s\n' 'Local assistant instructions.' > "$1/SOUL.md"
|
||||
}
|
||||
|
||||
expect_staging_failure() {
|
||||
if GANBING_SOURCE=$1 "$root/scripts/stage_captain_assets.sh" "$tmp/output" > "$tmp/staging.log" 2>&1; then
|
||||
fail "$2 was accepted"
|
||||
fi
|
||||
if grep -qx 'skill_status=active' "$tmp/output/manifest.txt" 2>/dev/null; then
|
||||
fail "$2 generated an active manifest"
|
||||
fi
|
||||
rm -rf "$tmp/output"
|
||||
}
|
||||
|
||||
mkdir "$tmp/bin"
|
||||
cat > "$tmp/bin/docker" <<'EOF'
|
||||
#!/bin/sh
|
||||
case " $* " in
|
||||
*" config --quiet "*) ;;
|
||||
*" ps --status running --services "*)
|
||||
printf '%s\n' captain-fixture gochat
|
||||
;;
|
||||
*" exec -T postgres "*)
|
||||
if [ -n "${MOCK_PROVIDER_CONFIG:-}" ]; then printf '%s\n' 1; else printf '%s\n' 0; fi
|
||||
;;
|
||||
*" exec -T gochat curl "*|*" exec -T gochat sh "*) ;;
|
||||
*) echo "unexpected docker invocation" >&2; exit 2 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod 700 "$tmp/bin/docker"
|
||||
|
||||
make_source "$tmp/clean"
|
||||
clean_before=$(find "$tmp/clean" -type f -exec sha256sum {} +)
|
||||
GANBING_SOURCE=$tmp/clean PATH=$tmp/bin:$PATH "$root/scripts/preflight_acceptance.sh" > "$tmp/preflight-clean.log" 2>&1 || fail "clean preflight failed"
|
||||
[ "$clean_before" = "$(find "$tmp/clean" -type f -exec sha256sum {} +)" ] || fail "staging modified its source"
|
||||
|
||||
provider_config='{"base_url":"http://captain-fixture:8080/v1","api_key":"must-not-leak","extra":true}'
|
||||
if GANBING_SOURCE=$tmp/clean MOCK_PROVIDER_CONFIG=$provider_config PATH=$tmp/bin:$PATH "$root/scripts/preflight_acceptance.sh" > "$tmp/preflight-override.log" 2>&1; then
|
||||
fail "nonempty database provider override was accepted"
|
||||
fi
|
||||
grep -F "$provider_config" "$tmp/preflight-override.log" >/dev/null && fail "provider override leaked to logs"
|
||||
|
||||
make_source "$tmp/malformed"
|
||||
cat > "$tmp/malformed/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: fixture
|
||||
---
|
||||
description: This is body text, not frontmatter.
|
||||
EOF
|
||||
expect_staging_failure "$tmp/malformed" "missing frontmatter description"
|
||||
|
||||
case_number=0
|
||||
for field in name description; do
|
||||
for value in null Null NULL '~' '""' "''" '[]' '{}' true 123 .5 '[fixture]'; do
|
||||
case_number=$((case_number + 1))
|
||||
source="$tmp/frontmatter-$case_number"
|
||||
make_source "$source"
|
||||
sed -i "s/^$field:.*/$field: $value/" "$source/SKILL.md"
|
||||
expect_staging_failure "$source" "$field value $value"
|
||||
done
|
||||
done
|
||||
|
||||
for fixture in json yaml markdown; do
|
||||
source="$tmp/credential-$fixture"
|
||||
make_source "$source"
|
||||
case "$fixture" in
|
||||
json) printf '%s\n' '{"api_key":"fixture-secret"}' > "$source/config.json" ;;
|
||||
yaml) printf '%s\n' 'access_token: fixture-token' > "$source/reference.md" ;;
|
||||
markdown) printf '%s\n' '**password** = `fixture-password`' > "$source/reference.md" ;;
|
||||
esac
|
||||
expect_staging_failure "$source" "$fixture credential assignment"
|
||||
done
|
||||
|
||||
GANBING_SOURCE=$tmp/clean "$root/scripts/stage_captain_assets.sh" "$tmp/output" >/dev/null
|
||||
[ "$clean_before" = "$(find "$tmp/clean" -type f -exec sha256sum {} +)" ] || fail "staging modified its source"
|
||||
grep -qx 'skill_status=active' "$tmp/output/manifest.txt" || fail "valid string frontmatter did not generate an active manifest"
|
||||
find "$tmp/output" -type f -exec stat -c '%a' {} + | grep -vx 600 >/dev/null && fail "staged file permissions are not 0600"
|
||||
|
||||
echo "acceptance scripts tests passed"
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
||||
stage=$(mktemp -d "${TMPDIR:-/tmp}/gochat-captain-stage.XXXXXX")
|
||||
trap 'rm -rf "$stage"' EXIT HUP INT TERM
|
||||
|
||||
"$root/scripts/stage_captain_assets.sh" "$stage"
|
||||
|
||||
compose() {
|
||||
docker compose -f "$root/compose.yaml" -f "$root/compose.acceptance.yaml" "$@"
|
||||
}
|
||||
|
||||
compose config --quiet
|
||||
compose ps --status running --services | grep -qx captain-fixture || {
|
||||
echo "preflight failed: captain-fixture is not running" >&2
|
||||
exit 1
|
||||
}
|
||||
compose ps --status running --services | grep -qx gochat || {
|
||||
echo "preflight failed: gochat is not running" >&2
|
||||
exit 1
|
||||
}
|
||||
compose exec -T gochat curl -fsS --retry 30 --retry-delay 1 --retry-connrefused --max-time 2 http://127.0.0.1:3000/health >/dev/null
|
||||
|
||||
overrides=$(compose exec -T postgres sh -eu -c '
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "
|
||||
SELECT count(*) FROM installation_configs
|
||||
WHERE deleted_at IS NULL AND (
|
||||
(name = '\''COPILOT_PROVIDER_CONFIG'\'' AND COALESCE(value, '\'''\'') <> '\'''\'') OR
|
||||
(name = '\''COPILOT_CHAT_API_KEY'\'' AND value <> '\''acceptance-fixture-not-a-secret'\'') OR
|
||||
(name = '\''COPILOT_EMBEDDING_API_KEY'\'' AND value <> '\'''\'')
|
||||
)"
|
||||
')
|
||||
[ "$overrides" = "0" ] || {
|
||||
echo "preflight failed: database Copilot settings override the local fixture" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
compose exec -T gochat sh -eu -c '
|
||||
test -n "$GOCHAT_COPILOT_PROVIDER_CONFIG"
|
||||
test "$GOCHAT_COPILOT_CHAT_API_KEY" = acceptance-fixture-not-a-secret
|
||||
case "$GOCHAT_COPILOT_PROVIDER_CONFIG" in
|
||||
*http://captain-fixture:8080/v1*) ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
knowledge=$(curl -fsS http://captain-fixture:8080/knowledge)
|
||||
test -n "$knowledge"
|
||||
case "$knowledge" in *KBASE-LOCAL-2026*) ;; *) exit 1 ;; esac
|
||||
chat=$(curl -fsS -H "Content-Type: application/json" -d "{\"model\":\"gpt-5.6-luna\",\"messages\":[{\"role\":\"user\",\"content\":\"preflight\"}]}" http://captain-fixture:8080/v1/chat/completions)
|
||||
case "$chat" in *chat.completion*) ;; *) exit 1 ;; esac
|
||||
embedding=$(curl -fsS -H "Content-Type: application/json" -d "{\"model\":\"fixture-embedding\",\"input\":[\"preflight\"]}" http://captain-fixture:8080/v1/embeddings)
|
||||
case "$embedding" in *\"object\":\"list\"*) ;; *) exit 1 ;; esac
|
||||
'
|
||||
|
||||
echo "acceptance preflight passed: provider=injected fixture=reachable knowledge=nonempty captain_assets=staged"
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
src=${GANBING_SOURCE:-}
|
||||
dst=${1:-}
|
||||
|
||||
fail() {
|
||||
echo "captain asset staging failed: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -n "$src" ] || fail "set GANBING_SOURCE to the read-only source directory"
|
||||
[ -d "$src" ] || fail "source directory does not exist"
|
||||
[ -n "$dst" ] || fail "usage: GANBING_SOURCE=/path $0 DESTINATION"
|
||||
if [ -e "$dst" ]; then
|
||||
[ -d "$dst" ] && [ -z "$(find "$dst" -mindepth 1 -print -quit)" ] || fail "destination must be absent or empty"
|
||||
fi
|
||||
|
||||
src=$(realpath "$src")
|
||||
case "$(realpath -m "$dst")/" in
|
||||
"$src"/*) fail "destination must be outside the source directory" ;;
|
||||
esac
|
||||
|
||||
[ "$(find "$src" -type l | wc -l | tr -d ' ')" -eq 0 ] || fail "symbolic links are not allowed"
|
||||
[ "$(find "$src" ! -type d ! -type f | wc -l | tr -d ' ')" -eq 0 ] || fail "special files are not allowed"
|
||||
[ "$(find "$src" -type f -name SKILL.md | wc -l | tr -d ' ')" -eq 1 ] || fail "exactly one SKILL.md is required"
|
||||
[ "$(find "$src" -type f -name SOUL.md | wc -l | tr -d ' ')" -eq 1 ] || fail "exactly one SOUL.md is required"
|
||||
|
||||
skill=$(find "$src" -type f -name SKILL.md -print -quit)
|
||||
soul=$(find "$src" -type f -name SOUL.md -print -quit)
|
||||
awk '
|
||||
function present(line, first, lower) {
|
||||
sub(/^[^:]*:[[:space:]]*/, "", line)
|
||||
sub(/[[:space:]]+#.*$/, "", line)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
||||
first = substr(line, 1, 1)
|
||||
if (first == "\"" || first == "\047")
|
||||
return length(line) > 2 && substr(line, length(line), 1) == first && substr(line, 2, length(line) - 2) !~ /^[[:space:]]*$/
|
||||
lower = tolower(line)
|
||||
if (line == "" || line ~ /^#/ || lower ~ /^(~|null|true|false|yes|no|on|off|[+-]?\.(inf|nan))$/)
|
||||
return 0
|
||||
if (first == "[" || first == "{" || first == "|" || first == ">" || first == "!" || first == "&" || first == "*")
|
||||
return 0
|
||||
if (lower ~ /^[+-]?0[xob][0-9a-f_]+$/ || lower ~ /^[+-]?([0-9][0-9_]*(\.[0-9_]*)?|\.[0-9_]+)(e[+-]?[0-9_]+)?$/ || lower ~ /^[0-9][0-9_.:tz+-]*$/)
|
||||
return 0
|
||||
return 1
|
||||
}
|
||||
NR == 1 { if ($0 != "---") exit 1; next }
|
||||
$0 == "---" { closed = 1; exit }
|
||||
/^name:[[:space:]]*/ { if (present($0)) name = 1 }
|
||||
/^description:[[:space:]]*/ { if (present($0)) description = 1 }
|
||||
END { if (!closed || !name || !description) exit 1 }
|
||||
' "$skill" || fail "SKILL.md frontmatter must contain name and description"
|
||||
[ "$(wc -c < "$soul" | tr -d ' ')" -le 20000 ] || fail "SOUL.md exceeds the Assistant instructions UI limit"
|
||||
[ "$(find "$src" -type f ! -name '*.md' ! -name '*.json' | wc -l | tr -d ' ')" -eq 0 ] || fail "only Markdown and JSON source files are allowed"
|
||||
|
||||
find "$src" -type f -exec sh -c '
|
||||
for file do
|
||||
case "$(file -b --mime-encoding "$file")" in
|
||||
utf-8|us-ascii) ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
done
|
||||
' sh {} + || fail "all staged files must be UTF-8"
|
||||
|
||||
credential_scan=0
|
||||
credential_pattern="['\"\`*_]?([[:alnum:]_.-]*[_-])?(api[ _-]?key|token|secret|password|authorization)['\"\`*_]*[[:space:]]*[:=][[:space:]]*['\"\`]?[^[:space:]'\"\`\$<{][^[:space:]]*"
|
||||
grep -ERqi "$credential_pattern" "$src" || credential_scan=$?
|
||||
case "$credential_scan" in
|
||||
0) fail "a possible plaintext credential was detected" ;;
|
||||
1) ;;
|
||||
*) fail "credential scan could not verify the source" ;;
|
||||
esac
|
||||
|
||||
mkdir -m 700 -p "$dst/captain-skill/references" "$dst/quarantine"
|
||||
instructions="$dst/captain-skill/instructions.md"
|
||||
awk 'NR == 1 && $0 == "---" { frontmatter=1; next } frontmatter && $0 == "---" { frontmatter=0; body=1; next } body { print }' "$skill" > "$instructions"
|
||||
chmod 600 "$instructions"
|
||||
[ -s "$instructions" ] || fail "SKILL.md body is empty"
|
||||
[ "$(wc -c < "$instructions" | tr -d ' ')" -le 32768 ] || fail "SKILL.md body exceeds the Captain Skill limit"
|
||||
install -m 600 "$soul" "$dst/assistant-instructions.md"
|
||||
|
||||
instruction_bytes=$(wc -c < "$instructions" | tr -d ' ')
|
||||
export dst instruction_bytes
|
||||
find "$src" -type f -name '*.md' ! -name SKILL.md ! -name SOUL.md -exec sh -eu -c '
|
||||
for reference do
|
||||
bytes=$(wc -c < "$reference" | tr -d " ")
|
||||
[ "$bytes" -le 65536 ] || exit 1
|
||||
key=$(printf "%s" "$reference" | sha256sum | cut -c1-12)
|
||||
# Conservative allowance for JSON/tool wrappers and the complete reference-key list.
|
||||
if [ $((instruction_bytes + bytes + 1200)) -le 8000 ]; then
|
||||
install -m 600 "$reference" "$dst/captain-skill/references/reference-$key.md"
|
||||
else
|
||||
install -m 600 "$reference" "$dst/quarantine/reference-$key.md"
|
||||
fi
|
||||
done
|
||||
' sh {} + || fail "a reference exceeds the Captain reference limit"
|
||||
|
||||
safe=$(find "$dst/captain-skill/references" -type f | wc -l | tr -d ' ')
|
||||
quarantined=$(find "$dst/quarantine" -type f | wc -l | tr -d ' ')
|
||||
total=$((safe + quarantined))
|
||||
[ "$total" -le 20 ] || fail "Captain supports at most 20 references"
|
||||
|
||||
cat > "$dst/manifest.txt" <<EOF
|
||||
skill_name=ganbing-local-acceptance
|
||||
skill_description=Isolated local acceptance skill
|
||||
skill_status=active
|
||||
reference_count=$total
|
||||
reference_importable=$safe
|
||||
reference_quarantined=$quarantined
|
||||
source_modified=false
|
||||
EOF
|
||||
chmod 600 "$dst/manifest.txt"
|
||||
|
||||
echo "captain assets staged: core=ready references=$safe quarantined=$quarantined"
|
||||
Reference in New Issue
Block a user