245 lines
7.3 KiB
Bash
Executable file
245 lines
7.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
|
|
SKILL_DIR=$(cd "$SCRIPT_DIR/.." && pwd -P)
|
|
HELPER="$SKILL_DIR/scripts/tobserver-agent"
|
|
|
|
# shellcheck disable=SC1090,SC1091 # The absolute helper path is resolved at runtime.
|
|
source "$HELPER"
|
|
|
|
passes=0
|
|
failures=0
|
|
|
|
pass() {
|
|
printf 'ok - %s\n' "$1"
|
|
passes=$((passes + 1))
|
|
}
|
|
|
|
fail() {
|
|
printf 'not ok - %s\n' "$1" >&2
|
|
failures=$((failures + 1))
|
|
}
|
|
|
|
expect_equal() {
|
|
local name=$1
|
|
local expected=$2
|
|
local actual=$3
|
|
|
|
if [[ "$actual" == "$expected" ]]; then
|
|
pass "$name"
|
|
else
|
|
printf 'expected: %q\nactual: %q\n' "$expected" "$actual" >&2
|
|
fail "$name"
|
|
fi
|
|
}
|
|
|
|
expect_success() {
|
|
local name=$1
|
|
shift
|
|
if ("$@" >/dev/null 2>&1); then
|
|
pass "$name"
|
|
else
|
|
fail "$name"
|
|
fi
|
|
}
|
|
|
|
expect_failure() {
|
|
local name=$1
|
|
shift
|
|
if ("$@" >/dev/null 2>&1); then
|
|
fail "$name"
|
|
else
|
|
pass "$name"
|
|
fi
|
|
}
|
|
|
|
expect_contains() {
|
|
local name=$1
|
|
local haystack=$2
|
|
local needle=$3
|
|
|
|
if [[ "$haystack" == *"$needle"* ]]; then
|
|
pass "$name"
|
|
else
|
|
printf 'missing: %q\n' "$needle" >&2
|
|
fail "$name"
|
|
fi
|
|
}
|
|
|
|
strict_eval_config() {
|
|
local config=$1
|
|
local apply_expression
|
|
|
|
apply_expression=$(strict_nixos_apply_expression)
|
|
nix eval --raw --expr \
|
|
"let strict = ($apply_expression); config = $config; in strict config"
|
|
}
|
|
|
|
toplevel_eval_config() {
|
|
local config=$1
|
|
|
|
nix eval --raw --expr \
|
|
"let config = $config; in config.system.build.toplevel.drvPath"
|
|
}
|
|
|
|
valid_pr_json() {
|
|
cat <<'JSON'
|
|
{
|
|
"number": 17,
|
|
"state": "open",
|
|
"merged": false,
|
|
"user": {"login": "oibot"},
|
|
"title": "Enable garbage collection",
|
|
"body": "Original pull-request description.",
|
|
"base": {"ref": "main"},
|
|
"head": {
|
|
"repo": {"full_name": "oibot/Tobserver"},
|
|
"ref": "refs/pull/17/head",
|
|
"label": "oibot/tobserver-agent-enable-gc-20260724-120000-abcdef",
|
|
"sha": "0123456789abcdef"
|
|
},
|
|
"flow": 1,
|
|
"html_url": "https://codeberg.org/oibot/Tobserver/pulls/17"
|
|
}
|
|
JSON
|
|
}
|
|
|
|
expect_equal \
|
|
"branch derives helper AGit topic" \
|
|
"tobserver-agent-enable-gc-20260724-120000-abcdef" \
|
|
"$(topic_for_branch 'agent/enable-gc-20260724-120000-abcdef')"
|
|
|
|
expect_equal \
|
|
"AGit topic derives local branch" \
|
|
"agent/enable-gc-20260724-120000-abcdef" \
|
|
"$(branch_for_topic 'tobserver-agent-enable-gc-20260724-120000-abcdef')"
|
|
|
|
expect_failure "main is not a helper branch" topic_for_branch "main"
|
|
expect_failure "nested branch suffix is rejected" topic_for_branch "agent/nested/change"
|
|
expect_failure "foreign AGit topic is rejected" branch_for_topic "manual-change"
|
|
|
|
encoded=$(encode_push_option_string $'line one\nGrüße')
|
|
decoded=$(printf '%s' "${encoded#\{base64\}}" | base64 --decode)
|
|
expect_equal "push-option base64 round trip" $'line one\nGrüße' "$decoded"
|
|
|
|
expect_success \
|
|
"canonical open AGit PR is revisable" \
|
|
assert_revisable_pull_request "$(valid_pr_json)" 17 \
|
|
"tobserver-agent-enable-gc-20260724-120000-abcdef"
|
|
|
|
wrong_user=$(valid_pr_json | jq '.user.login = "oibot-agent"')
|
|
expect_failure \
|
|
"bot-authored PR is rejected after cutover" \
|
|
assert_revisable_pull_request "$wrong_user" 17
|
|
|
|
non_agit=$(valid_pr_json | jq '.flow = 0')
|
|
expect_failure \
|
|
"non-AGit PR is rejected" \
|
|
assert_revisable_pull_request "$non_agit" 17
|
|
|
|
wrong_head=$(valid_pr_json | jq '.head.ref = "agent/unsafe"')
|
|
expect_failure \
|
|
"unexpected PR head ref is rejected" \
|
|
assert_revisable_pull_request "$wrong_head" 17
|
|
|
|
lazy_unit_config='{ assertions = []; systemd.units.bad.text = builtins.throw "malformed unit text"; system.build.toplevel.drvPath = "/nix/store/lazy-toplevel"; }'
|
|
false_assertion_config='{ assertions = [{ assertion = false; message = "deliberate failed assertion"; }]; systemd.units = {}; system.build.toplevel.drvPath = "/nix/store/assertion-toplevel"; }'
|
|
valid_strict_config='{ assertions = [{ assertion = true; message = "passes"; }]; systemd.units.example.text = "[Unit]"; system.build.toplevel.drvPath = "/nix/store/strict-toplevel"; }'
|
|
lazy_passing_message_config='{ assertions = [{ assertion = true; message = builtins.throw "unused passing message"; }]; systemd.units.example.text = "[Unit]"; system.build.toplevel.drvPath = "/nix/store/lazy-message-toplevel"; }'
|
|
|
|
expect_success \
|
|
"top-level drvPath fixture remains lazy" \
|
|
toplevel_eval_config "$lazy_unit_config"
|
|
expect_failure \
|
|
"strict evaluation catches malformed systemd unit text" \
|
|
strict_eval_config "$lazy_unit_config"
|
|
expect_failure \
|
|
"strict evaluation rejects a false NixOS assertion" \
|
|
strict_eval_config "$false_assertion_config"
|
|
expect_success \
|
|
"strict evaluation accepts rendered units and passing assertions" \
|
|
strict_eval_config "$valid_strict_config"
|
|
expect_success \
|
|
"strict evaluation does not force messages for passing assertions" \
|
|
strict_eval_config "$lazy_passing_message_config"
|
|
|
|
fetch_json_url() {
|
|
case "$1" in
|
|
*'/issues/17/comments?limit=50&page=1')
|
|
printf '%s\n' '[{"user":{"login":"reviewer"},"created_at":"2026-07-24T12:00:00Z","html_url":"https://codeberg.org/oibot/Tobserver/pulls/17#issuecomment-1","body":"General discussion feedback."}]'
|
|
;;
|
|
*'/pulls/17/reviews?limit=50&page=1')
|
|
printf '%s\n' '[{"id":73,"state":"REQUEST_CHANGES","user":{"login":"reviewer"},"submitted_at":"2026-07-24T12:01:00Z","commit_id":"0123456789abcdef","body":"Review summary."}]'
|
|
;;
|
|
*'/pulls/17/reviews/73/comments?limit=50&page=1')
|
|
printf '%s\n' '[{"path":"modules/example.nix","user":{"login":"reviewer"},"html_url":"https://codeberg.org/oibot/Tobserver/pulls/17#issuecomment-2","diff_hunk":"@@ -1 +1 @@","body":"Inline correction."}]'
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
review_bundle_json=$(pull_request_review_bundle_json 17 "$(valid_pr_json)")
|
|
review_bundle=$(render_pull_request_review_bundle <<<"$review_bundle_json")
|
|
expect_contains \
|
|
"review bundle includes pull-request description" \
|
|
"$review_bundle" \
|
|
"Original pull-request description."
|
|
expect_contains \
|
|
"review bundle includes general discussion" \
|
|
"$review_bundle" \
|
|
"General discussion feedback."
|
|
expect_contains \
|
|
"review bundle includes review state and body" \
|
|
"$review_bundle" \
|
|
"Review 73 — REQUEST_CHANGES"
|
|
expect_contains \
|
|
"review bundle includes review summary" \
|
|
"$review_bundle" \
|
|
"Review summary."
|
|
expect_contains \
|
|
"review bundle includes inline path and comment" \
|
|
"$review_bundle" \
|
|
'modules/example.nix'
|
|
expect_contains \
|
|
"review bundle includes inline correction" \
|
|
"$review_bundle" \
|
|
"Inline correction."
|
|
|
|
expect_equal \
|
|
"canonical PR URL yields number" \
|
|
"17" \
|
|
"$(pr_number_from_url 'https://codeberg.org/oibot/Tobserver/pulls/17')"
|
|
expect_failure \
|
|
"foreign PR URL is rejected" \
|
|
pr_number_from_url "https://example.invalid/oibot/Tobserver/pulls/17"
|
|
|
|
if rg -n 'Authorization: token|KEYCHAIN_|read_codeberg_token|codeberg-tobserver-agent' "$HELPER" >/dev/null; then
|
|
fail "helper contains no bot or write-token path"
|
|
else
|
|
pass "helper contains no bot or write-token path"
|
|
fi
|
|
|
|
if bash -n "$HELPER"; then
|
|
pass "helper parses as Bash"
|
|
else
|
|
fail "helper parses as Bash"
|
|
fi
|
|
|
|
if "$HELPER" --help | grep -q 'Codeberg API token'; then
|
|
pass "help documents tokenless boundary"
|
|
else
|
|
fail "help documents tokenless boundary"
|
|
fi
|
|
|
|
if "$HELPER" --help | grep -q 'inspect PR_NUMBER'; then
|
|
pass "help documents pull-request inspection"
|
|
else
|
|
fail "help documents pull-request inspection"
|
|
fi
|
|
|
|
printf '\n%d passed; %d failed\n' "$passes" "$failures"
|
|
((failures == 0))
|