Fix: Close verification gate review findings
This commit is contained in:
+183
-23
@@ -89,6 +89,8 @@ ${BOLD}Commands:${RESET}
|
|||||||
pattern to run a subset. --safe selects hermetic contracts only.
|
pattern to run a subset. --safe selects hermetic contracts only.
|
||||||
Plain terminal runs prompt before non-hermetic work. Automation
|
Plain terminal runs prompt before non-hermetic work. Automation
|
||||||
must grant each required capability with a repeatable --allow.
|
must grant each required capability with a repeatable --allow.
|
||||||
|
Each non-hermetic contract announces its exact capabilities
|
||||||
|
before it starts.
|
||||||
Failures print captured stdout/stderr. Successful stdout stays
|
Failures print captured stdout/stderr. Successful stdout stays
|
||||||
quiet; successful stderr is a warning. The default outer timeout
|
quiet; successful stderr is a warning. The default outer timeout
|
||||||
is 180 seconds. Set PANAMA_TEST_TIMEOUT_SECONDS to a positive
|
is 180 seconds. Set PANAMA_TEST_TIMEOUT_SECONDS to a positive
|
||||||
@@ -447,12 +449,20 @@ PROMPT
|
|||||||
CONTRACT_MANIFEST="tests/contracts.manifest"
|
CONTRACT_MANIFEST="tests/contracts.manifest"
|
||||||
CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network privileged)
|
CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network privileged)
|
||||||
|
|
||||||
|
contract_paths() {
|
||||||
|
local candidate
|
||||||
|
while IFS= read -r candidate; do
|
||||||
|
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue
|
||||||
|
printf 'tests/%s\n' "${candidate#"$PANAMA_DIR/tests/"}"
|
||||||
|
done < <(find "$PANAMA_DIR/tests" -type f \
|
||||||
|
-not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||||||
|
}
|
||||||
|
|
||||||
contract_manifest_entries() {
|
contract_manifest_entries() {
|
||||||
local line capabilities path
|
local line capabilities path
|
||||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
line="${line%%#*}"
|
[[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue
|
||||||
read -r capabilities path _ <<<"$line"
|
IFS=$' \t' read -r capabilities path <<<"$line"
|
||||||
[[ -n "${capabilities:-}" && -n "${path:-}" ]] || continue
|
|
||||||
printf '%s\t%s\n' "$path" "$capabilities"
|
printf '%s\t%s\n' "$path" "$capabilities"
|
||||||
done < "$PANAMA_DIR/$CONTRACT_MANIFEST"
|
done < "$PANAMA_DIR/$CONTRACT_MANIFEST"
|
||||||
}
|
}
|
||||||
@@ -464,6 +474,98 @@ require_contract_manifest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_contract_manifest() {
|
||||||
|
require_contract_manifest || return 1
|
||||||
|
|
||||||
|
local manifest="$PANAMA_DIR/$CONTRACT_MANIFEST"
|
||||||
|
local line capabilities path extra previous_comment="" previous_was_comment=0
|
||||||
|
local previous_path="" capability discovered
|
||||||
|
local -a capability_list=() findings=()
|
||||||
|
local -A expected_contracts=() manifest_paths=()
|
||||||
|
|
||||||
|
while IFS= read -r discovered; do
|
||||||
|
expected_contracts["$discovered"]=1
|
||||||
|
done < <(contract_paths)
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*# ]]; then
|
||||||
|
previous_comment="${line#*#}"
|
||||||
|
previous_comment="${previous_comment#"${previous_comment%%[![:space:]]*}"}"
|
||||||
|
previous_comment="${previous_comment%"${previous_comment##*[![:space:]]}"}"
|
||||||
|
previous_was_comment=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*$ ]]; then
|
||||||
|
previous_comment=""
|
||||||
|
previous_was_comment=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=$' \t' read -r capabilities path extra <<<"$line"
|
||||||
|
if [[ -z "${capabilities:-}" || -z "${path:-}" || -n "${extra:-}" ]]; then
|
||||||
|
findings+=("manifest line is not exactly two fields: $line")
|
||||||
|
previous_comment=""
|
||||||
|
previous_was_comment=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$previous_path" && "$path" < "$previous_path" ]]; then
|
||||||
|
findings+=('paths are not lexicographically sorted')
|
||||||
|
fi
|
||||||
|
previous_path="$path"
|
||||||
|
|
||||||
|
if [[ -n "${manifest_paths[$path]:-}" ]]; then
|
||||||
|
findings+=("duplicate path $path")
|
||||||
|
fi
|
||||||
|
manifest_paths["$path"]=1
|
||||||
|
|
||||||
|
local -A line_capabilities=()
|
||||||
|
if [[ "$capabilities" == ,* || "$capabilities" == *, || "$capabilities" == *,,* ]]; then
|
||||||
|
findings+=("empty capability on $path")
|
||||||
|
fi
|
||||||
|
IFS=',' read -r -a capability_list <<<"$capabilities"
|
||||||
|
for capability in "${capability_list[@]}"; do
|
||||||
|
[[ -n "$capability" ]] || continue
|
||||||
|
if [[ -n "${line_capabilities[$capability]:-}" ]]; then
|
||||||
|
findings+=("duplicate capability $capability on $path")
|
||||||
|
fi
|
||||||
|
line_capabilities["$capability"]=1
|
||||||
|
is_contract_capability "$capability" \
|
||||||
|
|| findings+=("unknown capability $capability on $path")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "${line_capabilities[hermetic]:-}" && ${#line_capabilities[@]} -ne 1 ]]; then
|
||||||
|
findings+=("hermetic must appear alone on $path")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$capabilities" != hermetic ]]; then
|
||||||
|
if (( previous_was_comment != 1 )); then
|
||||||
|
findings+=("$path is non-hermetic but lacks a directly preceding comment")
|
||||||
|
elif [[ -z "$previous_comment" ]]; then
|
||||||
|
findings+=("$path is non-hermetic but lacks a non-empty directly preceding comment")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
previous_comment=""
|
||||||
|
previous_was_comment=0
|
||||||
|
done < "$manifest"
|
||||||
|
|
||||||
|
for discovered in "${!expected_contracts[@]}"; do
|
||||||
|
[[ -n "${manifest_paths[$discovered]:-}" ]] \
|
||||||
|
|| findings+=("missing contract $discovered")
|
||||||
|
done
|
||||||
|
for path in "${!manifest_paths[@]}"; do
|
||||||
|
[[ -n "${expected_contracts[$path]:-}" ]] \
|
||||||
|
|| findings+=("stale manifest path $path")
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
err "Contract manifest validation failed with ${#findings[@]} finding(s):"
|
||||||
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
test_usage() {
|
test_usage() {
|
||||||
err "Usage: ${BOLD}$PROGRAM test [--safe] [--allow <capability>] [pattern]${RESET}"
|
err "Usage: ${BOLD}$PROGRAM test [--safe] [--allow <capability>] [pattern]${RESET}"
|
||||||
return 2
|
return 2
|
||||||
@@ -492,10 +594,59 @@ is_contract_capability() {
|
|||||||
# --safe runs only contracts the manifest classifies as hermetic and reports
|
# --safe runs only contracts the manifest classifies as hermetic and reports
|
||||||
# each external capability it skipped. A plain terminal run asks before any
|
# each external capability it skipped. A plain terminal run asks before any
|
||||||
# selected non-hermetic work. Automation must grant every required capability
|
# selected non-hermetic work. Automation must grant every required capability
|
||||||
# with repeatable --allow flags. Each contract gets an outer timeout, 180
|
# with repeatable --allow flags. Non-hermetic contracts announce their exact
|
||||||
|
# capability list before execution. Each contract gets an outer timeout, 180
|
||||||
# seconds by default. PANAMA_TEST_TIMEOUT_SECONDS accepts a positive integer
|
# seconds by default. PANAMA_TEST_TIMEOUT_SECONDS accepts a positive integer
|
||||||
# override. Failures include captured stdout and stderr. Successful stdout stays
|
# override. Failures include captured stdout and stderr. Successful stdout
|
||||||
# quiet, while successful stderr is surfaced as a warning.
|
# stays quiet, while successful stderr is surfaced as a warning.
|
||||||
|
PANAMA_ACTIVE_CONTRACT_PID=""
|
||||||
|
PANAMA_CONTRACT_CAPTURE_DIR=""
|
||||||
|
|
||||||
|
cleanup_contract_capture() {
|
||||||
|
if [[ -n "$PANAMA_CONTRACT_CAPTURE_DIR" && -d "$PANAMA_CONTRACT_CAPTURE_DIR" ]]; then
|
||||||
|
rm -rf -- "$PANAMA_CONTRACT_CAPTURE_DIR" || true
|
||||||
|
fi
|
||||||
|
PANAMA_CONTRACT_CAPTURE_DIR=""
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate_active_contract() {
|
||||||
|
local pid="$PANAMA_ACTIVE_CONTRACT_PID"
|
||||||
|
PANAMA_ACTIVE_CONTRACT_PID=""
|
||||||
|
[[ "$pid" =~ ^[1-9][0-9]*$ && "$pid" != "$$" ]] || return 0
|
||||||
|
|
||||||
|
# GNU timeout owns a process group whose ID is its PID. Signal that complete
|
||||||
|
# group so a contract cannot leave descendants behind, with a direct-PID
|
||||||
|
# fallback for implementations that do not create the group.
|
||||||
|
kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_contract_signal() {
|
||||||
|
local signal_status="$1"
|
||||||
|
trap - INT TERM
|
||||||
|
terminate_active_contract
|
||||||
|
cleanup_contract_capture
|
||||||
|
trap - EXIT
|
||||||
|
exit "$signal_status"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_contract_capture() {
|
||||||
|
local capture_dir=""
|
||||||
|
if ! capture_dir="$(mktemp -d)"; then
|
||||||
|
err 'Could not create contract capture directory.'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$capture_dir" || ! -d "$capture_dir" ]]; then
|
||||||
|
err 'Could not create contract capture directory.'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PANAMA_CONTRACT_CAPTURE_DIR="$capture_dir"
|
||||||
|
trap cleanup_contract_capture EXIT
|
||||||
|
trap 'handle_contract_signal 130' INT
|
||||||
|
trap 'handle_contract_signal 143' TERM
|
||||||
|
}
|
||||||
|
|
||||||
cmd_test() {
|
cmd_test() {
|
||||||
local timeout_seconds="${PANAMA_TEST_TIMEOUT_SECONDS:-180}"
|
local timeout_seconds="${PANAMA_TEST_TIMEOUT_SECONDS:-180}"
|
||||||
[[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || {
|
[[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
@@ -503,11 +654,11 @@ cmd_test() {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
require_contract_manifest || return 1
|
validate_contract_manifest || return 1
|
||||||
cmd_test_impl "$timeout_seconds" "$@"
|
cmd_test_impl "$timeout_seconds" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_test_impl() (
|
cmd_test_impl() {
|
||||||
local timeout_seconds="$1"
|
local timeout_seconds="$1"
|
||||||
shift
|
shift
|
||||||
local pattern="" safe=0 arg capability capabilities rel path
|
local pattern="" safe=0 arg capability capabilities rel path
|
||||||
@@ -605,12 +756,10 @@ cmd_test_impl() (
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local capture_dir stdout_file stderr_file name run_status index=0
|
local capture_dir stdout_file stderr_file name run_status index=0 final_status=0
|
||||||
local -a failed=() runner=()
|
local -a failed=() runner=()
|
||||||
capture_dir="$(mktemp -d)"
|
prepare_contract_capture || return 1
|
||||||
trap 'rm -rf -- "$capture_dir"' EXIT
|
capture_dir="$PANAMA_CONTRACT_CAPTURE_DIR"
|
||||||
trap 'rm -rf -- "$capture_dir"; exit 130' INT
|
|
||||||
trap 'rm -rf -- "$capture_dir"; exit 143' TERM
|
|
||||||
|
|
||||||
info "Running ${#suite[@]} contract(s)"
|
info "Running ${#suite[@]} contract(s)"
|
||||||
for index in "${!suite[@]}"; do
|
for index in "${!suite[@]}"; do
|
||||||
@@ -624,9 +773,16 @@ cmd_test_impl() (
|
|||||||
else
|
else
|
||||||
runner=("$path")
|
runner=("$path")
|
||||||
fi
|
fi
|
||||||
|
capabilities="${manifest_capabilities[$rel]}"
|
||||||
|
if [[ "$capabilities" != hermetic ]]; then
|
||||||
|
info "Running $name [$capabilities]"
|
||||||
|
fi
|
||||||
run_status=0
|
run_status=0
|
||||||
timeout --signal=TERM --kill-after=5 "$timeout_seconds" \
|
timeout --signal=TERM --kill-after=5 "$timeout_seconds" \
|
||||||
"${runner[@]}" >"$stdout_file" 2>"$stderr_file" || run_status=$?
|
"${runner[@]}" >"$stdout_file" 2>"$stderr_file" &
|
||||||
|
PANAMA_ACTIVE_CONTRACT_PID=$!
|
||||||
|
wait "$PANAMA_ACTIVE_CONTRACT_PID" || run_status=$?
|
||||||
|
PANAMA_ACTIVE_CONTRACT_PID=""
|
||||||
if (( run_status == 0 )); then
|
if (( run_status == 0 )); then
|
||||||
ok "$name"
|
ok "$name"
|
||||||
if [[ -s "$stderr_file" ]]; then
|
if [[ -s "$stderr_file" ]]; then
|
||||||
@@ -655,12 +811,16 @@ cmd_test_impl() (
|
|||||||
header "Result"
|
header "Result"
|
||||||
if (( ${#failed[@]} == 0 )); then
|
if (( ${#failed[@]} == 0 )); then
|
||||||
ok "${#suite[@]} contract(s) passed"
|
ok "${#suite[@]} contract(s) passed"
|
||||||
return 0
|
else
|
||||||
fi
|
|
||||||
err "${#failed[@]} of ${#suite[@]} failed:"
|
err "${#failed[@]} of ${#suite[@]} failed:"
|
||||||
printf ' %s\n' "${failed[@]}" >&2
|
printf ' %s\n' "${failed[@]}" >&2
|
||||||
return 1
|
final_status=1
|
||||||
)
|
fi
|
||||||
|
|
||||||
|
cleanup_contract_capture
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
return "$final_status"
|
||||||
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Command: contracts
|
# Command: contracts
|
||||||
@@ -721,7 +881,7 @@ cmd_contracts() {
|
|||||||
suffix="${suffix#*/}"
|
suffix="${suffix#*/}"
|
||||||
done
|
done
|
||||||
|
|
||||||
require_contract_manifest || return 1
|
validate_contract_manifest || return 1
|
||||||
local -A manifest_capabilities=()
|
local -A manifest_capabilities=()
|
||||||
local capabilities
|
local capabilities
|
||||||
while IFS=$'\t' read -r rel capabilities; do
|
while IFS=$'\t' read -r rel capabilities; do
|
||||||
@@ -732,11 +892,11 @@ cmd_contracts() {
|
|||||||
# runner would actually execute.
|
# runner would actually execute.
|
||||||
local -a hits=()
|
local -a hits=()
|
||||||
local candidate rel
|
local candidate rel
|
||||||
while IFS= read -r candidate; do
|
while IFS= read -r rel; do
|
||||||
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue
|
candidate="$PANAMA_DIR/$rel"
|
||||||
grep -qF "${patterns[@]}" "$candidate" 2>/dev/null || continue
|
grep -qF "${patterns[@]}" "$candidate" 2>/dev/null || continue
|
||||||
hits+=("tests/${candidate#"$PANAMA_DIR"/tests/}")
|
hits+=("$rel")
|
||||||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
done < <(contract_paths)
|
||||||
|
|
||||||
if (( ${#hits[@]} == 0 )); then
|
if (( ${#hits[@]} == 0 )); then
|
||||||
printf 'No contract mentions %s — coverage may be indirect (a harness or a generated artifact); nothing verified.\n' "$path" >&2
|
printf 'No contract mentions %s — coverage may be indirect (a harness or a generated artifact); nothing verified.\n' "$path" >&2
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ contract_manifest_entries() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not make a missing manifest mean “everything is safe.” `cmd_test` and `cmd_contracts` must fail clearly if the file cannot be read. The manifest contract owns deeper format validation; the CLI owns the runtime read failure.
|
Do not make a missing or malformed manifest mean “everything is safe.” Before either `cmd_test` or `cmd_contracts` consumes manifest entries, the public CLI independently validates the complete actual manifest: exactly two fields, known/non-empty/non-duplicate capabilities, exclusive `hermetic`, unique sorted paths, the executable/`*_test.py` fixture-excluded discovery set, and a non-empty directly preceding comment for every non-hermetic entry. Missing discovered contracts and stale manifest paths are fatal before selection or execution. Keep `tests/setup/contract-manifest-contract` as an independent validator rather than sourcing runtime code; a later contract cannot protect earlier execution.
|
||||||
|
|
||||||
### Step 3: Implement argument and capability policy
|
### Step 3: Implement argument and capability policy
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ local timeout_seconds="${PANAMA_TEST_TIMEOUT_SECONDS:-180}"
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the implementation body in a subshell, create one capture directory with `mktemp -d`, and trap its removal on `EXIT`, `INT`, and `TERM` inside that subshell. This keeps cleanup reliable without leaking or overwriting traps in the parent CLI process. For each contract, run either `python3 path` or the executable through:
|
The public CLI process must own contract supervision. Check that `mktemp -d` succeeds before constructing any capture path, then install top-level `EXIT`, `INT`, and `TERM` cleanup around the checked directory. Launch the active `timeout` asynchronously and retain its PID/process-group ownership. On INT or TERM sent to the exact CLI PID, signal the active timeout/process group, wait for it, remove capture storage, and exit 130 or 143. On normal completion, remove capture storage and clear the temporary traps without changing the aggregate test status. For each contract, run either `python3 path` or the executable through:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
timeout --signal=TERM --kill-after=5 "$timeout_seconds" \
|
timeout --signal=TERM --kill-after=5 "$timeout_seconds" \
|
||||||
@@ -389,8 +389,10 @@ The leaked fixture must include:
|
|||||||
|
|
||||||
- a literal YAML credential `POSTGRES_PASSWORD: fixture-should-be-rejected`;
|
- a literal YAML credential `POSTGRES_PASSWORD: fixture-should-be-rejected`;
|
||||||
- a literal env credential `API_TOKEN=fixture-should-be-rejected`.
|
- a literal env credential `API_TOKEN=fixture-should-be-rejected`.
|
||||||
|
- a plain-text PEM private-key header with an exact `path:line: private key` finding;
|
||||||
|
- plain-text synthetic `sk-ant-`, minimum-supported-length `ghp_`, and `xoxb-` signatures, each with an exact `path:line: provider token` finding.
|
||||||
|
|
||||||
Use intentionally invalid fixture strings, not realistic provider token formats.
|
Add clean plain-text near-misses for each signature. Keep semantic fixture credentials intentionally invalid and signature fixtures clearly synthetic while still matching the supported signature shapes.
|
||||||
|
|
||||||
### Step 2: Write the scanner with only the Python standard library
|
### Step 2: Write the scanner with only the Python standard library
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ note() { findings+=("$1"); }
|
|||||||
scanner="$repo_dir/tests/server/scan-tracked-secrets.py"
|
scanner="$repo_dir/tests/server/scan-tracked-secrets.py"
|
||||||
fixtures_dir="$repo_dir/tests/server/fixtures/secrets"
|
fixtures_dir="$repo_dir/tests/server/fixtures/secrets"
|
||||||
|
|
||||||
if ! python3 "$scanner" "$fixtures_dir/clean" compose.yml .env.example README.md; then
|
if ! python3 "$scanner" "$fixtures_dir/clean" \
|
||||||
|
compose.yml .env.example README.md signature-near-misses.txt; then
|
||||||
note 'the clean secret-scanning fixture was rejected'
|
note 'the clean secret-scanning fixture was rejected'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -53,6 +54,10 @@ expect_leak .env.example '.env.example:1: API_TOKEN'
|
|||||||
expect_leak plain-list.yml 'plain-list.yml:4: API_TOKEN'
|
expect_leak plain-list.yml 'plain-list.yml:4: API_TOKEN'
|
||||||
expect_leak quoted-mapping.yml 'quoted-mapping.yml:4: API_TOKEN'
|
expect_leak quoted-mapping.yml 'quoted-mapping.yml:4: API_TOKEN'
|
||||||
expect_leak quoted-list.yml 'quoted-list.yml:4: API_TOKEN'
|
expect_leak quoted-list.yml 'quoted-list.yml:4: API_TOKEN'
|
||||||
|
expect_leak pem-private-key.txt 'pem-private-key.txt:1: private key'
|
||||||
|
expect_leak anthropic-token.txt 'anthropic-token.txt:1: provider token'
|
||||||
|
expect_leak github-token.txt 'github-token.txt:1: provider token'
|
||||||
|
expect_leak slack-token.txt 'slack-token.txt:1: provider token'
|
||||||
|
|
||||||
mapfile -t tracked_server_files < <(git -C "$repo_dir" ls-files 'server/**' 'server/*')
|
mapfile -t tracked_server_files < <(git -C "$repo_dir" ls-files 'server/**' 'server/*')
|
||||||
if ! output="$(python3 "$scanner" "$repo_dir" "${tracked_server_files[@]}" 2>&1)"; then
|
if ! output="$(python3 "$scanner" "$repo_dir" "${tracked_server_files[@]}" 2>&1)"; then
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN SYNTHETIC PUBLIC KEY-----
|
||||||
|
sk-ant-
|
||||||
|
ghp_0123456789ABCDEFGHI
|
||||||
|
xoxb-
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
sk-ant-SYNTHETIC_FIXTURE_TOKEN
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ghp_0123456789ABCDEFGHIJ
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-----BEGIN SYNTHETIC PRIVATE KEY-----
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
xoxb-SYNTHETIC_FIXTURE_TOKEN
|
||||||
@@ -9,9 +9,23 @@ repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|||||||
fixture="$(mktemp -d)"
|
fixture="$(mktemp -d)"
|
||||||
output=""
|
output=""
|
||||||
status=0
|
status=0
|
||||||
|
background_cli_pid=""
|
||||||
|
background_contract_pgid=""
|
||||||
|
|
||||||
cleanup() { rm -rf -- "$fixture"; }
|
cleanup() {
|
||||||
trap cleanup EXIT INT TERM
|
trap - EXIT INT TERM
|
||||||
|
if [[ "$background_cli_pid" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
kill -TERM "$background_cli_pid" 2>/dev/null || true
|
||||||
|
wait "$background_cli_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ "$background_contract_pgid" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
kill -KILL -- "-$background_contract_pgid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -rf -- "$fixture"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'cleanup; exit 130' INT
|
||||||
|
trap 'cleanup; exit 143' TERM
|
||||||
|
|
||||||
fail() { printf 'test runner: %s\n' "$*" >&2; exit 1; }
|
fail() { printf 'test runner: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
@@ -25,6 +39,12 @@ assert_not_contains() {
|
|||||||
[[ "$haystack" != *"$needle"* ]] || fail "expected output not to contain: $needle\n$haystack"
|
[[ "$haystack" != *"$needle"* ]] || fail "expected output not to contain: $needle\n$haystack"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_before() {
|
||||||
|
local first="$1" second="$2" haystack="$3"
|
||||||
|
[[ "$haystack" == *"$first"*"$second"* ]] \
|
||||||
|
|| fail "expected '$first' before '$second':\n$haystack"
|
||||||
|
}
|
||||||
|
|
||||||
assert_execution() {
|
assert_execution() {
|
||||||
local expected="$1" actual
|
local expected="$1" actual
|
||||||
actual="$(sort "$fixture/executions" 2>/dev/null || true)"
|
actual="$(sort "$fixture/executions" 2>/dev/null || true)"
|
||||||
@@ -33,6 +53,33 @@ assert_execution() {
|
|||||||
|
|
||||||
reset_executions() { : > "$fixture/executions"; }
|
reset_executions() { : > "$fixture/executions"; }
|
||||||
|
|
||||||
|
wait_for_file() {
|
||||||
|
local path="$1" attempt
|
||||||
|
for (( attempt = 0; attempt < 100; attempt++ )); do
|
||||||
|
[[ -s "$path" ]] && return 0
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_process_exit() {
|
||||||
|
local pid="$1" attempt
|
||||||
|
for (( attempt = 0; attempt < 100; attempt++ )); do
|
||||||
|
kill -0 "$pid" 2>/dev/null || return 0
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_path_removal() {
|
||||||
|
local path="$1" attempt
|
||||||
|
for (( attempt = 0; attempt < 100; attempt++ )); do
|
||||||
|
[[ ! -e "$path" ]] && return 0
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
run_panama() {
|
run_panama() {
|
||||||
output="$(cd "$fixture" && TMPDIR="$fixture" PANAMA_TEST_FIXTURE="$fixture" "$fixture/bin/panama" "$@" </dev/null 2>&1)"
|
output="$(cd "$fixture" && TMPDIR="$fixture" PANAMA_TEST_FIXTURE="$fixture" "$fixture/bin/panama" "$@" </dev/null 2>&1)"
|
||||||
status=$?
|
status=$?
|
||||||
@@ -45,18 +92,39 @@ run_panama_with_timeout() {
|
|||||||
|
|
||||||
run_panama_tty_default_no() {
|
run_panama_tty_default_no() {
|
||||||
local command tty_stdout="$fixture/tty.stdout"
|
local command tty_stdout="$fixture/tty.stdout"
|
||||||
|
local pty_state="$fixture/pty-state"
|
||||||
|
mkdir -p "$pty_state"/{config,state,cache,data,runtime}
|
||||||
|
chmod 700 "$pty_state/runtime"
|
||||||
printf -v command 'cd %q && TMPDIR=%q PANAMA_TEST_FIXTURE=%q %q test composite > %q' \
|
printf -v command 'cd %q && TMPDIR=%q PANAMA_TEST_FIXTURE=%q %q test composite > %q' \
|
||||||
"$fixture" "$fixture" "$fixture" "$fixture/bin/panama" "$tty_stdout"
|
"$fixture" "$fixture" "$fixture" "$fixture/bin/panama" "$tty_stdout"
|
||||||
output="$(python3 - "$command" <<'PY'
|
output="$(
|
||||||
|
HOME="$fixture/pty-home" \
|
||||||
|
BASH_ENV="$fixture/pty-bash-env" \
|
||||||
|
PANAMA_PTY_STARTUP_SENTINEL="$fixture/pty-startup-sourced" \
|
||||||
|
python3 - "$command" "$fixture/pty-home" \
|
||||||
|
"$pty_state/config" "$pty_state/state" "$pty_state/cache" \
|
||||||
|
"$pty_state/data" "$pty_state/runtime" "$fixture" <<'PY'
|
||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
import pty
|
import pty
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
command = sys.argv[1]
|
command = sys.argv[1]
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment.pop('BASH_ENV', None)
|
||||||
|
environment.pop('ENV', None)
|
||||||
|
environment.update({
|
||||||
|
'HOME': sys.argv[2],
|
||||||
|
'XDG_CONFIG_HOME': sys.argv[3],
|
||||||
|
'XDG_STATE_HOME': sys.argv[4],
|
||||||
|
'XDG_CACHE_HOME': sys.argv[5],
|
||||||
|
'XDG_DATA_HOME': sys.argv[6],
|
||||||
|
'XDG_RUNTIME_DIR': sys.argv[7],
|
||||||
|
'TMPDIR': sys.argv[8],
|
||||||
|
})
|
||||||
pid, terminal = pty.fork()
|
pid, terminal = pty.fork()
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
os.execv('/bin/bash', ['bash', '-lc', command])
|
os.execve('/bin/bash', ['bash', '--noprofile', '--norc', '-c', command], environment)
|
||||||
|
|
||||||
chunks = []
|
chunks = []
|
||||||
replied = False
|
replied = False
|
||||||
@@ -88,6 +156,48 @@ assert_occurrences() {
|
|||||||
[[ "$actual" == "$expected" ]] || fail "expected $expected occurrence(s) of '$needle', got $actual\n$haystack"
|
[[ "$actual" == "$expected" ]] || fail "expected $expected occurrence(s) of '$needle', got $actual\n$haystack"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replace_manifest_line() {
|
||||||
|
local original="$1" replacement="$2" line
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
if [[ "$line" == "$original" ]]; then
|
||||||
|
[[ "$replacement" == __REMOVE__ ]] || printf '%s\n' "$replacement"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$line"
|
||||||
|
fi
|
||||||
|
done <<<"$valid_manifest"
|
||||||
|
}
|
||||||
|
|
||||||
|
swap_manifest_contract_paths() {
|
||||||
|
local line
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
case "$line" in
|
||||||
|
'live-compositor,live-desktop tests/composite-contract')
|
||||||
|
printf '%s\n' 'live-compositor,live-desktop tests/desktop-contract'
|
||||||
|
;;
|
||||||
|
'live-desktop tests/desktop-contract')
|
||||||
|
printf '%s\n' 'live-desktop tests/composite-contract'
|
||||||
|
;;
|
||||||
|
*) printf '%s\n' "$line" ;;
|
||||||
|
esac
|
||||||
|
done <<<"$valid_manifest"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_manifest_rejection() {
|
||||||
|
local label="$1" expected="$2" contents="$3"
|
||||||
|
|
||||||
|
printf '%s\n' "$contents" >"$fixture/tests/contracts.manifest"
|
||||||
|
reset_executions
|
||||||
|
run_panama test pass
|
||||||
|
[[ $status -ne 0 ]] || fail "$label manifest unexpectedly allowed test execution"
|
||||||
|
assert_contains "$expected" "$output"
|
||||||
|
assert_execution ''
|
||||||
|
|
||||||
|
run_panama contracts config/subject
|
||||||
|
[[ $status -ne 0 ]] || fail "$label manifest unexpectedly allowed contracts lookup"
|
||||||
|
assert_contains "$expected" "$output"
|
||||||
|
assert_execution ''
|
||||||
|
}
|
||||||
|
|
||||||
mkdir -p "$fixture/bin" "$fixture/tests" "$fixture/config"
|
mkdir -p "$fixture/bin" "$fixture/tests" "$fixture/config"
|
||||||
cp "$repo_dir/bin/panama" "$fixture/bin/panama"
|
cp "$repo_dir/bin/panama" "$fixture/bin/panama"
|
||||||
chmod +x "$fixture/bin/panama"
|
chmod +x "$fixture/bin/panama"
|
||||||
@@ -111,6 +221,8 @@ privileged tests/privileged-contract
|
|||||||
hermetic tests/stderr-contract
|
hermetic tests/stderr-contract
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
valid_manifest="$(<"$fixture/tests/contracts.manifest")"
|
||||||
|
|
||||||
cat > "$fixture/tests/pass-contract" <<'EOF'
|
cat > "$fixture/tests/pass-contract" <<'EOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
printf 'pass\n' >> "$PANAMA_TEST_FIXTURE/executions"
|
printf 'pass\n' >> "$PANAMA_TEST_FIXTURE/executions"
|
||||||
@@ -135,7 +247,13 @@ EOF
|
|||||||
cat > "$fixture/tests/hang-contract" <<'EOF'
|
cat > "$fixture/tests/hang-contract" <<'EOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
printf 'hang\n' >> "$PANAMA_TEST_FIXTURE/executions"
|
printf 'hang\n' >> "$PANAMA_TEST_FIXTURE/executions"
|
||||||
trap 'printf terminated >"$PANAMA_TEST_FIXTURE/terminated"; exit 124' TERM
|
printf '%s\n' "$BASHPID" > "$PANAMA_TEST_FIXTURE/hang.pid"
|
||||||
|
finish() {
|
||||||
|
printf 'terminated\n' >"$PANAMA_TEST_FIXTURE/terminated"
|
||||||
|
exit "$1"
|
||||||
|
}
|
||||||
|
trap 'finish 130' INT
|
||||||
|
trap 'finish 143' TERM
|
||||||
while :; do sleep 1; done
|
while :; do sleep 1; done
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -147,6 +265,7 @@ EOF
|
|||||||
cat > "$fixture/tests/desktop-contract" <<'EOF'
|
cat > "$fixture/tests/desktop-contract" <<'EOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
printf 'desktop\n' >> "$PANAMA_TEST_FIXTURE/executions"
|
printf 'desktop\n' >> "$PANAMA_TEST_FIXTURE/executions"
|
||||||
|
printf 'desktop fixture complete\n' >&2
|
||||||
# config/subject
|
# config/subject
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -172,6 +291,15 @@ chmod +x "$fixture/tests"/{composite,desktop,fail,hang,host,network,pass,privile
|
|||||||
# A PTY-backed default-no confirmation remains visible when stdout is redirected
|
# A PTY-backed default-no confirmation remains visible when stdout is redirected
|
||||||
# but stdin and stderr are terminals. The fixture proves that one prompt gates
|
# but stdin and stderr are terminals. The fixture proves that one prompt gates
|
||||||
# the selected composite capability set without running its contract.
|
# the selected composite capability set without running its contract.
|
||||||
|
mkdir -p "$fixture/pty-home"
|
||||||
|
for profile in .bash_profile .bashrc .profile; do
|
||||||
|
cat >"$fixture/pty-home/$profile" <<'EOF'
|
||||||
|
printf 'profile\n' >>"${PANAMA_PTY_STARTUP_SENTINEL:?}"
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
cat >"$fixture/pty-bash-env" <<'EOF'
|
||||||
|
printf 'BASH_ENV\n' >>"${PANAMA_PTY_STARTUP_SENTINEL:?}"
|
||||||
|
EOF
|
||||||
run_panama_tty_default_no
|
run_panama_tty_default_no
|
||||||
[[ $status -ne 0 ]] || fail 'TTY default-no prompt unexpectedly ran the fixture'
|
[[ $status -ne 0 ]] || fail 'TTY default-no prompt unexpectedly ran the fixture'
|
||||||
assert_execution ''
|
assert_execution ''
|
||||||
@@ -179,6 +307,8 @@ assert_contains 'Run 1 contract(s) requiring: live-compositor live-desktop?' "$o
|
|||||||
assert_occurrences 'Run 1 contract(s) requiring:' "$output" 1
|
assert_occurrences 'Run 1 contract(s) requiring:' "$output" 1
|
||||||
assert_contains 'No contracts were run.' "$(<"$fixture/tty.stdout")"
|
assert_contains 'No contracts were run.' "$(<"$fixture/tty.stdout")"
|
||||||
assert_not_contains 'Run 1 contract(s) requiring:' "$(<"$fixture/tty.stdout")"
|
assert_not_contains 'Run 1 contract(s) requiring:' "$(<"$fixture/tty.stdout")"
|
||||||
|
[[ ! -e "$fixture/pty-startup-sourced" ]] \
|
||||||
|
|| fail 'PTY fixture sourced a shell profile or BASH_ENV'
|
||||||
|
|
||||||
# --safe must select hermetic entries from the manifest, not merely omit a
|
# --safe must select hermetic entries from the manifest, not merely omit a
|
||||||
# legacy desktop list. The failing and timed-out fixtures make the command
|
# legacy desktop list. The failing and timed-out fixtures make the command
|
||||||
@@ -204,6 +334,8 @@ assert_contains 'pass --allow live-desktop' "$output"
|
|||||||
run_panama test --allow live-desktop desktop
|
run_panama test --allow live-desktop desktop
|
||||||
[[ $status -eq 0 ]] || fail "explicit desktop grant failed: $output"
|
[[ $status -eq 0 ]] || fail "explicit desktop grant failed: $output"
|
||||||
assert_execution 'desktop'
|
assert_execution 'desktop'
|
||||||
|
assert_contains 'Running desktop-contract [live-desktop]' "$output"
|
||||||
|
assert_before 'Running desktop-contract [live-desktop]' 'desktop fixture complete' "$output"
|
||||||
|
|
||||||
reset_executions
|
reset_executions
|
||||||
run_panama test --allow live-compositor --allow live-desktop composite
|
run_panama test --allow live-compositor --allow live-desktop composite
|
||||||
@@ -229,6 +361,45 @@ assert_execution 'hang'
|
|||||||
[[ -f "$fixture/terminated" ]] || fail 'timed-out contract was not terminated with TERM'
|
[[ -f "$fixture/terminated" ]] || fail 'timed-out contract was not terminated with TERM'
|
||||||
assert_contains 'timed out' "$output"
|
assert_contains 'timed out' "$output"
|
||||||
|
|
||||||
|
# INT/TERM ownership belongs to the exact public CLI PID, not a runner
|
||||||
|
# subshell. The CLI must wait for the timeout process group and remove its
|
||||||
|
# capture directory before returning the signal-derived status.
|
||||||
|
reset_executions
|
||||||
|
rm -f -- "$fixture/hang.pid" "$fixture/terminated"
|
||||||
|
(
|
||||||
|
cd "$fixture" || exit 1
|
||||||
|
exec env TMPDIR="$fixture" PANAMA_TEST_FIXTURE="$fixture" \
|
||||||
|
"$fixture/bin/panama" test hang
|
||||||
|
) >"$fixture/exact-term.out" 2>&1 &
|
||||||
|
background_cli_pid=$!
|
||||||
|
wait_for_file "$fixture/hang.pid" \
|
||||||
|
|| fail 'exact-PID TERM fixture never started the hang contract'
|
||||||
|
hang_pid="$(<"$fixture/hang.pid")"
|
||||||
|
background_contract_pgid="$(ps -o pgid= -p "$hang_pid" | tr -d '[:space:]')"
|
||||||
|
[[ "$background_contract_pgid" =~ ^[1-9][0-9]*$ ]] \
|
||||||
|
|| fail "could not resolve hang process group for PID $hang_pid"
|
||||||
|
mapfile -t active_capture_dirs < <(
|
||||||
|
find "$fixture" -mindepth 1 -maxdepth 1 -type d -name 'tmp.*' -print
|
||||||
|
)
|
||||||
|
(( ${#active_capture_dirs[@]} == 1 )) \
|
||||||
|
|| fail "expected one active capture directory, got ${#active_capture_dirs[@]}"
|
||||||
|
active_capture_dir="${active_capture_dirs[0]}"
|
||||||
|
|
||||||
|
kill -TERM "$background_cli_pid" \
|
||||||
|
|| fail 'could not send TERM to the exact public CLI PID'
|
||||||
|
term_status=0
|
||||||
|
wait "$background_cli_pid" || term_status=$?
|
||||||
|
background_cli_pid=""
|
||||||
|
[[ "$term_status" -eq 143 ]] \
|
||||||
|
|| fail "exact-PID TERM returned $term_status instead of 143: $(<"$fixture/exact-term.out")"
|
||||||
|
wait_for_process_exit "$hang_pid" \
|
||||||
|
|| fail "hang contract PID $hang_pid survived exact-PID TERM"
|
||||||
|
background_contract_pgid=""
|
||||||
|
wait_for_path_removal "$active_capture_dir" \
|
||||||
|
|| fail "capture directory survived exact-PID TERM: $active_capture_dir"
|
||||||
|
[[ -f "$fixture/terminated" ]] \
|
||||||
|
|| fail 'exact-PID TERM did not reach the hang contract cleanup trap'
|
||||||
|
|
||||||
reset_executions
|
reset_executions
|
||||||
run_panama test fail
|
run_panama test fail
|
||||||
[[ $status -ne 0 ]] || fail 'failed contract unexpectedly passed'
|
[[ $status -ne 0 ]] || fail 'failed contract unexpectedly passed'
|
||||||
@@ -244,6 +415,19 @@ reset_executions
|
|||||||
run_panama test pass
|
run_panama test pass
|
||||||
[[ $status -eq 0 ]] || fail "pass contract failed: $output"
|
[[ $status -eq 0 ]] || fail "pass contract failed: $output"
|
||||||
assert_not_contains 'pass stdout' "$output"
|
assert_not_contains 'pass stdout' "$output"
|
||||||
|
assert_not_contains 'pass-contract [hermetic]' "$output"
|
||||||
|
|
||||||
|
reset_executions
|
||||||
|
printf 'not a directory\n' >"$fixture/invalid-tmpdir"
|
||||||
|
output="$(
|
||||||
|
cd "$fixture" && \
|
||||||
|
TMPDIR="$fixture/invalid-tmpdir" PANAMA_TEST_FIXTURE="$fixture" \
|
||||||
|
"$fixture/bin/panama" test pass </dev/null 2>&1
|
||||||
|
)"
|
||||||
|
status=$?
|
||||||
|
[[ $status -ne 0 ]] || fail 'invalid TMPDIR unexpectedly allowed contract execution'
|
||||||
|
assert_contains 'Could not create contract capture directory.' "$output"
|
||||||
|
assert_execution ''
|
||||||
|
|
||||||
reset_executions
|
reset_executions
|
||||||
run_panama test --safe desktop
|
run_panama test --safe desktop
|
||||||
@@ -258,10 +442,52 @@ assert_contains 'tests/desktop-contract [live-desktop]' "$output"
|
|||||||
assert_contains 'tests/composite-contract [live-compositor,live-desktop]' "$output"
|
assert_contains 'tests/composite-contract [live-compositor,live-desktop]' "$output"
|
||||||
assert_contains 'tests/pass-contract [hermetic]' "$output"
|
assert_contains 'tests/pass-contract [hermetic]' "$output"
|
||||||
|
|
||||||
|
# Both public manifest consumers fail closed on the complete format and
|
||||||
|
# discovery set. Validation happens before selection, lookup, or contract
|
||||||
|
# execution, so even a malformed entry unrelated to the requested pattern is
|
||||||
|
# fatal and leaves the execution log empty.
|
||||||
|
expect_manifest_rejection unknown-capability \
|
||||||
|
'unknown capability hermetik on tests/pass-contract' \
|
||||||
|
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetik tests/pass-contract')"
|
||||||
|
expect_manifest_rejection mixed-hermetic \
|
||||||
|
'hermetic must appear alone on tests/pass-contract' \
|
||||||
|
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic,network tests/pass-contract')"
|
||||||
|
expect_manifest_rejection duplicate-path \
|
||||||
|
'duplicate path tests/pass-contract' \
|
||||||
|
"$(replace_manifest_line 'hermetic tests/pass-contract' $'hermetic tests/pass-contract\nhermetic tests/pass-contract')"
|
||||||
|
expect_manifest_rejection stale-path \
|
||||||
|
'stale manifest path tests/stale-contract' \
|
||||||
|
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic tests/stale-contract')"
|
||||||
|
expect_manifest_rejection missing-contract \
|
||||||
|
'missing contract tests/pass-contract' \
|
||||||
|
"$(replace_manifest_line 'hermetic tests/pass-contract' __REMOVE__)"
|
||||||
|
expect_manifest_rejection extra-field \
|
||||||
|
'manifest line is not exactly two fields' \
|
||||||
|
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic tests/pass-contract unexpected')"
|
||||||
|
expect_manifest_rejection empty-capability \
|
||||||
|
'empty capability on tests/pass-contract' \
|
||||||
|
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic, tests/pass-contract')"
|
||||||
|
expect_manifest_rejection duplicate-capability \
|
||||||
|
'duplicate capability hermetic on tests/pass-contract' \
|
||||||
|
"$(replace_manifest_line 'hermetic tests/pass-contract' 'hermetic,hermetic tests/pass-contract')"
|
||||||
|
expect_manifest_rejection unsorted-paths \
|
||||||
|
'paths are not lexicographically sorted' \
|
||||||
|
"$(swap_manifest_contract_paths)"
|
||||||
|
expect_manifest_rejection uncommented-non-hermetic \
|
||||||
|
'tests/desktop-contract is non-hermetic but lacks a directly preceding comment' \
|
||||||
|
"$(replace_manifest_line '# Maps the live desktop.' __REMOVE__)"
|
||||||
|
expect_manifest_rejection blank-comment \
|
||||||
|
'tests/desktop-contract is non-hermetic but lacks a non-empty directly preceding comment' \
|
||||||
|
"$(replace_manifest_line '# Maps the live desktop.' '#')"
|
||||||
|
|
||||||
|
printf '%s\n' "$valid_manifest" >"$fixture/tests/contracts.manifest"
|
||||||
|
|
||||||
mv "$fixture/tests/contracts.manifest" "$fixture/tests/contracts.manifest.missing"
|
mv "$fixture/tests/contracts.manifest" "$fixture/tests/contracts.manifest.missing"
|
||||||
|
reset_executions
|
||||||
run_panama test pass
|
run_panama test pass
|
||||||
[[ $status -ne 0 ]] || fail 'missing manifest unexpectedly allowed test execution'
|
[[ $status -ne 0 ]] || fail 'missing manifest unexpectedly allowed test execution'
|
||||||
assert_contains 'contracts.manifest' "$output"
|
assert_contains 'contracts.manifest' "$output"
|
||||||
|
assert_execution ''
|
||||||
mv "$fixture/tests/contracts.manifest.missing" "$fixture/tests/contracts.manifest"
|
mv "$fixture/tests/contracts.manifest.missing" "$fixture/tests/contracts.manifest"
|
||||||
|
|
||||||
capture_dirs="$(find "$fixture" -mindepth 1 -maxdepth 1 -type d -name 'tmp.*' -print)"
|
capture_dirs="$(find "$fixture" -mindepth 1 -maxdepth 1 -type d -name 'tmp.*' -print)"
|
||||||
|
|||||||
@@ -234,16 +234,116 @@ grep -qx 'install-packages' <<<"$ran_full" \
|
|||||||
# The fixture also covers a clean fast-forward, installer status propagation,
|
# The fixture also covers a clean fast-forward, installer status propagation,
|
||||||
# and the boundary between update and sync before forcing the conflict below.
|
# and the boundary between update and sync before forcing the conflict below.
|
||||||
|
|
||||||
|
# Make every ambient configuration source hostile before constructing the Git
|
||||||
|
# fixtures. A hermetic fixture overrides these values with its own empty state;
|
||||||
|
# consuming any of them either leaves a sentinel or prevents a commit.
|
||||||
|
hostile="$tmp/hostile-environment"
|
||||||
|
mkdir -p "$hostile/home" "$hostile/xdg-config" "$hostile/xdg-state" \
|
||||||
|
"$hostile/xdg-cache" "$hostile/xdg-data" "$hostile/hooks" \
|
||||||
|
"$hostile/template/hooks"
|
||||||
|
for profile in .bash_profile .bashrc .profile; do
|
||||||
|
cat >"$hostile/home/$profile" <<'EOF'
|
||||||
|
printf 'profile\n' >>"${PANAMA_HOSTILE_PROFILE_SENTINEL:?}"
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
cat >"$hostile/bash-env" <<'EOF'
|
||||||
|
printf 'BASH_ENV\n' >>"${PANAMA_HOSTILE_BASH_ENV_SENTINEL:?}"
|
||||||
|
EOF
|
||||||
|
cat >"$hostile/hooks/pre-commit" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'global hook\n' >>"${PANAMA_HOSTILE_GIT_SENTINEL:?}"
|
||||||
|
exit 97
|
||||||
|
EOF
|
||||||
|
cat >"$hostile/template/hooks/pre-commit" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# PANAMA_HOSTILE_TEMPLATE_HOOK
|
||||||
|
printf 'template hook\n' >>"${PANAMA_HOSTILE_TEMPLATE_SENTINEL:?}"
|
||||||
|
exit 98
|
||||||
|
EOF
|
||||||
|
chmod +x "$hostile/hooks/pre-commit" "$hostile/template/hooks/pre-commit"
|
||||||
|
cat >"$hostile/global.gitconfig" <<EOF
|
||||||
|
[core]
|
||||||
|
hooksPath = $hostile/hooks
|
||||||
|
[commit]
|
||||||
|
gpgSign = true
|
||||||
|
[init]
|
||||||
|
templateDir = $hostile/template
|
||||||
|
EOF
|
||||||
|
cp "$hostile/global.gitconfig" "$hostile/system.gitconfig"
|
||||||
|
|
||||||
|
export HOME="$hostile/home"
|
||||||
|
export XDG_CONFIG_HOME="$hostile/xdg-config"
|
||||||
|
export XDG_STATE_HOME="$hostile/xdg-state"
|
||||||
|
export XDG_CACHE_HOME="$hostile/xdg-cache"
|
||||||
|
export XDG_DATA_HOME="$hostile/xdg-data"
|
||||||
|
export BASH_ENV="$hostile/bash-env"
|
||||||
|
export PANAMA_HOSTILE_PROFILE_SENTINEL="$hostile/profile-sourced"
|
||||||
|
export PANAMA_HOSTILE_BASH_ENV_SENTINEL="$hostile/bash-env-sourced"
|
||||||
|
export PANAMA_HOSTILE_GIT_SENTINEL="$hostile/global-config-sourced"
|
||||||
|
export PANAMA_HOSTILE_TEMPLATE_SENTINEL="$hostile/template-hook-sourced"
|
||||||
|
export GIT_CONFIG_NOSYSTEM=0
|
||||||
|
export GIT_CONFIG_SYSTEM="$hostile/system.gitconfig"
|
||||||
|
export GIT_CONFIG_GLOBAL="$hostile/global.gitconfig"
|
||||||
|
export GIT_CONFIG_COUNT=1
|
||||||
|
export GIT_CONFIG_KEY_0=core.hooksPath
|
||||||
|
export GIT_CONFIG_VALUE_0="$hostile/hooks"
|
||||||
|
export GIT_TEMPLATE_DIR="$hostile/template"
|
||||||
|
|
||||||
|
prepare_cli_fixture_environment() {
|
||||||
|
local root="$1"
|
||||||
|
mkdir -p "$root/home" "$root/xdg-config" "$root/xdg-state" \
|
||||||
|
"$root/xdg-cache" "$root/xdg-data" "$root/xdg-runtime" \
|
||||||
|
"$root/empty-templates" "$root/empty-hooks"
|
||||||
|
chmod 700 "$root/xdg-runtime"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_cli_fixture_environment() {
|
||||||
|
local root="$1"
|
||||||
|
shift
|
||||||
|
env -u BASH_ENV -u ENV -u GIT_CONFIG_PARAMETERS \
|
||||||
|
-u GIT_CONFIG_KEY_0 -u GIT_CONFIG_VALUE_0 \
|
||||||
|
HOME="$root/home" \
|
||||||
|
XDG_CONFIG_HOME="$root/xdg-config" \
|
||||||
|
XDG_STATE_HOME="$root/xdg-state" \
|
||||||
|
XDG_CACHE_HOME="$root/xdg-cache" \
|
||||||
|
XDG_DATA_HOME="$root/xdg-data" \
|
||||||
|
XDG_RUNTIME_DIR="$root/xdg-runtime" \
|
||||||
|
GIT_CONFIG_NOSYSTEM=1 \
|
||||||
|
GIT_CONFIG_SYSTEM=/dev/null \
|
||||||
|
GIT_CONFIG_GLOBAL=/dev/null \
|
||||||
|
GIT_CONFIG_COUNT=0 \
|
||||||
|
GIT_TEMPLATE_DIR="$root/empty-templates" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture_git() {
|
||||||
|
local root="$1"
|
||||||
|
shift
|
||||||
|
run_cli_fixture_environment "$root" \
|
||||||
|
git -c commit.gpgSign=false -c tag.gpgSign=false \
|
||||||
|
-c core.hooksPath="$root/empty-hooks" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_fixture_repo() {
|
||||||
|
local root="$1" repository="$2"
|
||||||
|
fixture_git "$root" -C "$repository" config user.email contract@panama || return 1
|
||||||
|
fixture_git "$root" -C "$repository" config user.name contract || return 1
|
||||||
|
fixture_git "$root" -C "$repository" config commit.gpgSign false || return 1
|
||||||
|
fixture_git "$root" -C "$repository" config tag.gpgSign false || return 1
|
||||||
|
fixture_git "$root" -C "$repository" config core.hooksPath "$root/empty-hooks" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
# Each fixture has the same three repositories as a real update: a bare remote,
|
# Each fixture has the same three repositories as a real update: a bare remote,
|
||||||
# a clone that publishes upstream changes, and the machine clone being updated.
|
# a clone that publishes upstream changes, and the machine clone being updated.
|
||||||
build_cli_fixture() (
|
build_cli_fixture() (
|
||||||
local root="$1"
|
local root="$1"
|
||||||
rm -rf "$root" || return 1
|
rm -rf "$root" || return 1
|
||||||
mkdir -p "$root" || return 1
|
mkdir -p "$root" || return 1
|
||||||
git init -q --bare "$root/origin.git" || return 1
|
prepare_cli_fixture_environment "$root" || return 1
|
||||||
git clone -q "$root/origin.git" "$root/upstream" 2>/dev/null || return 1
|
fixture_git "$root" init -q --bare "$root/origin.git" || return 1
|
||||||
git -C "$root/upstream" config user.email contract@panama || return 1
|
fixture_git "$root" -C "$root/origin.git" config core.hooksPath "$root/empty-hooks" || return 1
|
||||||
git -C "$root/upstream" config user.name contract || return 1
|
fixture_git "$root" clone -q "$root/origin.git" "$root/upstream" 2>/dev/null || return 1
|
||||||
|
configure_fixture_repo "$root" "$root/upstream" || return 1
|
||||||
|
|
||||||
mkdir -p "$root/upstream/bin" || return 1
|
mkdir -p "$root/upstream/bin" || return 1
|
||||||
cp "$panama" "$root/upstream/bin/panama" || return 1
|
cp "$panama" "$root/upstream/bin/panama" || return 1
|
||||||
@@ -254,21 +354,20 @@ exit "${PANAMA_UPDATE_INSTALL_RC:-0}"
|
|||||||
EOF
|
EOF
|
||||||
chmod +x "$root/upstream/bin/panama" "$root/upstream/install" || return 1
|
chmod +x "$root/upstream/bin/panama" "$root/upstream/install" || return 1
|
||||||
printf 'one\n' >"$root/upstream/f" || return 1
|
printf 'one\n' >"$root/upstream/f" || return 1
|
||||||
git -C "$root/upstream" add -A || return 1
|
fixture_git "$root" -C "$root/upstream" add -A || return 1
|
||||||
git -C "$root/upstream" commit -qm initial || return 1
|
fixture_git "$root" -C "$root/upstream" commit -qm initial || return 1
|
||||||
git -C "$root/upstream" push -qu origin HEAD || return 1
|
fixture_git "$root" -C "$root/upstream" push -qu origin HEAD || return 1
|
||||||
|
|
||||||
git clone -q "$root/origin.git" "$root/machine" || return 1
|
fixture_git "$root" clone -q "$root/origin.git" "$root/machine" || return 1
|
||||||
git -C "$root/machine" config user.email contract@panama || return 1
|
configure_fixture_repo "$root" "$root/machine" || return 1
|
||||||
git -C "$root/machine" config user.name contract || return 1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
advance_upstream() (
|
advance_upstream() (
|
||||||
local root="$1" file="$2" contents="$3"
|
local root="$1" file="$2" contents="$3"
|
||||||
printf '%s\n' "$contents" >"$root/upstream/$file" || return 1
|
printf '%s\n' "$contents" >"$root/upstream/$file" || return 1
|
||||||
git -C "$root/upstream" add "$file" || return 1
|
fixture_git "$root" -C "$root/upstream" add "$file" || return 1
|
||||||
git -C "$root/upstream" commit -qm "update $file" || return 1
|
fixture_git "$root" -C "$root/upstream" commit -qm "update $file" || return 1
|
||||||
git -C "$root/upstream" push -q || return 1
|
fixture_git "$root" -C "$root/upstream" push -q || return 1
|
||||||
)
|
)
|
||||||
|
|
||||||
# This write fails before the later Git commands. The helper must return that
|
# This write fails before the later Git commands. The helper must return that
|
||||||
@@ -285,25 +384,27 @@ fi
|
|||||||
|
|
||||||
clean="$tmp/clean-update"
|
clean="$tmp/clean-update"
|
||||||
if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
|
if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
|
||||||
machine_before="$(git -C "$clean/machine" rev-parse HEAD)"
|
machine_before="$(fixture_git "$clean" -C "$clean/machine" rev-parse HEAD)"
|
||||||
upstream_after="$(git -C "$clean/upstream" rev-parse HEAD)"
|
upstream_after="$(fixture_git "$clean" -C "$clean/upstream" rev-parse HEAD)"
|
||||||
[[ "$machine_before" != "$upstream_after" ]] \
|
[[ "$machine_before" != "$upstream_after" ]] \
|
||||||
|| note 'the clean update fixture started current, so it cannot prove a fast-forward'
|
|| note 'the clean update fixture started current, so it cannot prove a fast-forward'
|
||||||
|
|
||||||
: >"$clean/install.log"
|
: >"$clean/install.log"
|
||||||
update_status=0
|
update_status=0
|
||||||
|
run_cli_fixture_environment "$clean" \
|
||||||
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
|
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
|
||||||
"$clean/machine/bin/panama" update >"$clean/update.out" 2>&1 \
|
"$clean/machine/bin/panama" update >"$clean/update.out" 2>&1 \
|
||||||
|| update_status=$?
|
|| update_status=$?
|
||||||
[[ "$update_status" -eq 0 ]] \
|
[[ "$update_status" -eq 0 ]] \
|
||||||
|| note "panama update failed on a clean clone with status $update_status"
|
|| note "panama update failed on a clean clone with status $update_status"
|
||||||
[[ "$(git -C "$clean/machine" rev-parse HEAD)" == "$upstream_after" ]] \
|
[[ "$(fixture_git "$clean" -C "$clean/machine" rev-parse HEAD)" == "$upstream_after" ]] \
|
||||||
|| note 'panama update did not fast-forward the clean machine clone'
|
|| note 'panama update did not fast-forward the clean machine clone'
|
||||||
grep -qx -- '--upgrade' "$clean/install.log" \
|
grep -qx -- '--upgrade' "$clean/install.log" \
|
||||||
|| note 'panama update did not invoke the installer with --upgrade'
|
|| note 'panama update did not invoke the installer with --upgrade'
|
||||||
|
|
||||||
: >"$clean/install.log"
|
: >"$clean/install.log"
|
||||||
update_status=0
|
update_status=0
|
||||||
|
run_cli_fixture_environment "$clean" \
|
||||||
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" PANAMA_UPDATE_INSTALL_RC=23 \
|
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" PANAMA_UPDATE_INSTALL_RC=23 \
|
||||||
"$clean/machine/bin/panama" update >"$clean/failing-update.out" 2>&1 \
|
"$clean/machine/bin/panama" update >"$clean/failing-update.out" 2>&1 \
|
||||||
|| update_status=$?
|
|| update_status=$?
|
||||||
@@ -316,7 +417,8 @@ if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
|
|||||||
printf 'local sync\n' >"$clean/machine/synced"
|
printf 'local sync\n' >"$clean/machine/synced"
|
||||||
sync_status=0
|
sync_status=0
|
||||||
printf 'y\ncontract sync\n' \
|
printf 'y\ncontract sync\n' \
|
||||||
| PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
|
| run_cli_fixture_environment "$clean" \
|
||||||
|
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
|
||||||
"$clean/machine/bin/panama" sync >"$clean/sync.out" 2>&1 \
|
"$clean/machine/bin/panama" sync >"$clean/sync.out" 2>&1 \
|
||||||
|| sync_status=$?
|
|| sync_status=$?
|
||||||
[[ "$sync_status" -eq 0 ]] \
|
[[ "$sync_status" -eq 0 ]] \
|
||||||
@@ -335,6 +437,7 @@ if build_cli_fixture "$conflict"; then
|
|||||||
if advance_upstream "$conflict" f upstream; then
|
if advance_upstream "$conflict" f upstream; then
|
||||||
: >"$conflict/install.log"
|
: >"$conflict/install.log"
|
||||||
conflict_status=0
|
conflict_status=0
|
||||||
|
run_cli_fixture_environment "$conflict" \
|
||||||
PANAMA_UPDATE_FIXTURE_LOG="$conflict/install.log" \
|
PANAMA_UPDATE_FIXTURE_LOG="$conflict/install.log" \
|
||||||
"$conflict/machine/bin/panama" update >"$conflict/update.out" 2>&1 \
|
"$conflict/machine/bin/panama" update >"$conflict/update.out" 2>&1 \
|
||||||
|| conflict_status=$?
|
|| conflict_status=$?
|
||||||
@@ -342,12 +445,14 @@ if build_cli_fixture "$conflict"; then
|
|||||||
|| note "panama update failed while recovering a stash conflict with status $conflict_status"
|
|| note "panama update failed while recovering a stash conflict with status $conflict_status"
|
||||||
[[ "$(<"$conflict/machine/f")" == upstream ]] \
|
[[ "$(<"$conflict/machine/f")" == upstream ]] \
|
||||||
|| note 'panama update did not reset the conflicted file to the upstream version'
|
|| note 'panama update did not reset the conflicted file to the upstream version'
|
||||||
if git -C "$conflict/machine" grep -qE '^(<<<<<<<|=======|>>>>>>>)' -- .; then
|
if fixture_git "$conflict" -C "$conflict/machine" \
|
||||||
|
grep -qE '^(<<<<<<<|=======|>>>>>>>)' -- .; then
|
||||||
note 'panama update left conflict markers in the machine checkout'
|
note 'panama update left conflict markers in the machine checkout'
|
||||||
fi
|
fi
|
||||||
[[ -n "$(git -C "$conflict/machine" stash list)" ]] \
|
[[ -n "$(fixture_git "$conflict" -C "$conflict/machine" stash list)" ]] \
|
||||||
|| note 'panama update dropped the stash after its conflicted pop'
|
|| note 'panama update dropped the stash after its conflicted pop'
|
||||||
recovered="$(git -C "$conflict/machine" show 'stash@{0}:f' 2>/dev/null)"
|
recovered="$(fixture_git "$conflict" -C "$conflict/machine" \
|
||||||
|
show 'stash@{0}:f' 2>/dev/null)"
|
||||||
[[ "$recovered" == local ]] \
|
[[ "$recovered" == local ]] \
|
||||||
|| note 'the stash left by panama update does not contain the local version'
|
|| note 'the stash left by panama update does not contain the local version'
|
||||||
else
|
else
|
||||||
@@ -357,6 +462,18 @@ else
|
|||||||
note 'the conflict update fixture could not be built'
|
note 'the conflict update fixture could not be built'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
for sentinel in profile-sourced bash-env-sourced global-config-sourced template-hook-sourced; do
|
||||||
|
[[ ! -e "$hostile/$sentinel" ]] \
|
||||||
|
|| note "the update Git fixture consumed hostile state: $sentinel"
|
||||||
|
done
|
||||||
|
template_copy="$(
|
||||||
|
find "$tmp" -path "$hostile" -prune -o \
|
||||||
|
-type f -path '*/hooks/pre-commit' \
|
||||||
|
-exec grep -lF 'PANAMA_HOSTILE_TEMPLATE_HOOK' {} + 2>/dev/null
|
||||||
|
)"
|
||||||
|
[[ -z "$template_copy" ]] \
|
||||||
|
|| note "the update Git fixture copied a hostile template hook: $template_copy"
|
||||||
|
|
||||||
if (( ${#findings[@]} > 0 )); then
|
if (( ${#findings[@]} > 0 )); then
|
||||||
printf 'update command contract: %d finding(s)\n' "${#findings[@]}" >&2
|
printf 'update command contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||||
printf ' - %s\n' "${findings[@]}" >&2
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
|||||||
Reference in New Issue
Block a user