From ef2d72180c717a9450c2e350d43882a500641f02 Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 09:47:44 -0400 Subject: [PATCH 01/12] Add fake adapter contract matrix --- README.md | 13 + RESTORE.md | 10 + adapter-tests/v1/contract.jq | 53 +++ adapter-tests/v1/fakes/forge-a.sh | 33 ++ adapter-tests/v1/fakes/forge-b.sh | 34 ++ adapter-tests/v1/fakes/producer-a.sh | 19 + adapter-tests/v1/fakes/producer-b.sh | 19 + adapter-tests/v1/fakes/protocol-fault.sh | 20 + adapter-tests/v1/fixture/source.txt | 2 + adapter-tests/v1/inventory.json | 1 + adapter-tests/v1/runner.sh | 343 ++++++++++++++++++ ci/required-files.txt | 12 + .../test/portable-adapter-contracts.test.sh | 169 +++++++++ .../portable-profile-resolution-fixtures.sh | 35 +- 14 files changed, 759 insertions(+), 4 deletions(-) create mode 100644 adapter-tests/v1/contract.jq create mode 100755 adapter-tests/v1/fakes/forge-a.sh create mode 100755 adapter-tests/v1/fakes/forge-b.sh create mode 100755 adapter-tests/v1/fakes/producer-a.sh create mode 100755 adapter-tests/v1/fakes/producer-b.sh create mode 100755 adapter-tests/v1/fakes/protocol-fault.sh create mode 100644 adapter-tests/v1/fixture/source.txt create mode 100644 adapter-tests/v1/inventory.json create mode 100755 adapter-tests/v1/runner.sh create mode 100755 scripts/test/portable-adapter-contracts.test.sh diff --git a/README.md b/README.md index 8d77199..fe612be 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,19 @@ The stable `scripts/core-contract.sh` wrapper and inactive resolver select this generation together. This is a repo-only compatibility switch. It does not install the resolver, select a live profile, or qualify a real forge. +## Inactive fake adapter contract matrix + +`adapter-tests/v1/` runs a fixed 2×2 producer/forge matrix against one unrelated +local Git fixture. Its accepted inventory and four distinct fake entrypoints are +digest-pinned. The runner revalidates each resolved profile with portable core v2, +then checks package, target, artifact, candidate, and Git identities itself. + +The result is observation only. It grants no authority, qualification, approval, +publish, merge, or branch-write capability. The fakes run with a cleared environment, +fixed limits, and disposable directories, but this test does not provide or claim +mechanical network or host-filesystem isolation. Real-adapter sandbox qualification +and an external-target smoke remain required. + ## The current default team You talk **only** to yshifu, in a Claude Code session. yshifu orchestrates the other roles diff --git a/RESTORE.md b/RESTORE.md index d71b954..bebac56 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -308,6 +308,16 @@ materialization in a caller-disposable repository. The package is not qualified It grants no credential, network, publish, push, merge, or remote branch-write capability. The switch does not install the resolver or select a live profile. +Restore every path in the manifest's inactive fake adapter matrix block, then run: + +```sh +bash scripts/test/portable-adapter-contracts.test.sh +``` + +The proof resolves four fake-only profiles and checks the accepted 2×2 matrix plus +closed negative protocol cases. It uses no real adapter or credential and makes no +network/host isolation, qualification, activation, or external-target-smoke claim. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/adapter-tests/v1/contract.jq b/adapter-tests/v1/contract.jq new file mode 100644 index 0000000..3d4a9e0 --- /dev/null +++ b/adapter-tests/v1/contract.jq @@ -0,0 +1,53 @@ +def exact($keys): type == "object" and (keys | sort) == ($keys | sort); +def sha256: type == "string" and test("\\A[0-9a-f]{64}\\z"); + +def positive_assertions: + ["audit-projection","candidate-git","core-validation","environment-clean", + "evidence-projection","gate-projection","outcome-projection", + "risk-projection","target-git"]; + +def expected_cases($fixture): [ + {case_id:"matrix-aa",phase:"pipeline",producer_package_id:"fake.producer.a",forge_package_id:"fake.forge.a",expected_verdict:"pass",expected_error:"",equivalence_group:"portable-fake-v1",assertions:positive_assertions,fixture_sha256:$fixture}, + {case_id:"matrix-ab",phase:"pipeline",producer_package_id:"fake.producer.a",forge_package_id:"fake.forge.b",expected_verdict:"pass",expected_error:"",equivalence_group:"portable-fake-v1",assertions:positive_assertions,fixture_sha256:$fixture}, + {case_id:"matrix-ba",phase:"pipeline",producer_package_id:"fake.producer.b",forge_package_id:"fake.forge.a",expected_verdict:"pass",expected_error:"",equivalence_group:"portable-fake-v1",assertions:positive_assertions,fixture_sha256:$fixture}, + {case_id:"matrix-bb",phase:"pipeline",producer_package_id:"fake.producer.b",forge_package_id:"fake.forge.b",expected_verdict:"pass",expected_error:"",equivalence_group:"portable-fake-v1",assertions:positive_assertions,fixture_sha256:$fixture}, + {case_id:"reject-degraded",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_DEGRADED",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, + {case_id:"reject-empty",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_EMPTY",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, + {case_id:"reject-malformed",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_MALFORMED",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, + {case_id:"reject-partial",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_PARTIAL",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, + {case_id:"reject-relabelled",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_RELABELLED",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, + {case_id:"reject-timeout",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_TIMEOUT",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, + {case_id:"reject-transport",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_TRANSPORT",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture} +]; + +def inventory_ok($fixture): + exact(["authorization_ref","cases","protocol","schema_version"]) and + .schema_version == 1 and .protocol == "ystack.adapter-contract.inventory.v1" and + .authorization_ref == {comment_id:5476938197,issue_number:153,scope_comment_id:5474023028} and + .cases == expected_cases($fixture); + +def producer_response_ok($package): + exact(["artifact","package_id","phase","protocol","status"]) and + .protocol == "ystack.fake-adapter.v1" and .phase == "producer" and + .status == "ok" and .package_id == $package and + (.artifact | exact(["content","sha256"]) and (.content | type == "string") and (.sha256 | sha256)); + +def forge_response_ok($package): + exact(["commit_id","file_object_id","package_id","phase","protocol","status","tree_id"]) and + .protocol == "ystack.fake-adapter.v1" and .phase == "forge" and + .status == "ok" and .package_id == $package and + (.commit_id | test("\\A[0-9a-f]{40}\\z")) and + (.tree_id | test("\\A[0-9a-f]{40}\\z")) and + (.file_object_id | test("\\A[0-9a-f]{40}\\z")); + +def projection($artifact; $target_tree; $candidate_tree): + {artifact_sha256:$artifact,risk:"core:routine",gate_refs:[], + outcome:"change:changed",evidence:["deterministic:passed"], + audit:{producer_status:"completed",forge_status:"completed", + target_tree_id:$target_tree,candidate_tree_id:$candidate_tree}}; + +if $command == "inventory" then inventory_ok($fixture_sha256) +elif $command == "producer-response" then producer_response_ok($package_id) +elif $command == "forge-response" then forge_response_ok($package_id) +elif $command == "projection" then projection($artifact_sha256;$target_tree_id;$candidate_tree_id) +else error("unknown-command") end diff --git a/adapter-tests/v1/fakes/forge-a.sh b/adapter-tests/v1/fakes/forge-a.sh new file mode 100755 index 0000000..06d9ed1 --- /dev/null +++ b/adapter-tests/v1/fakes/forge-a.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 2 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +request=$1 +jq_bin=$2 +"$jq_bin" -e ' + type == "object" and + (keys | sort) == ["artifact","candidate_root","case_id","phase","protocol"] and + .protocol == "ystack.fake-adapter.v1" and .phase == "forge" and + (.case_id | test("\\Amatrix-(aa|ab|ba|bb)\\z")) and + (.candidate_root | type == "string" and startswith("/")) and + (.artifact | type == "object" and + (keys | sort) == ["content","sha256"] and + (.content | type == "string") and + (.sha256 | test("\\A[0-9a-f]{64}\\z"))) +' "$request" >/dev/null +candidate_root=$("$jq_bin" -r '.candidate_root' "$request") +content=$("$jq_bin" -r '.artifact.content' "$request") +digest=$(/usr/bin/printf '%s' "$content" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') +[ "$digest" = "$("$jq_bin" -r '.artifact.sha256' "$request")" ] +/bin/mkdir -m 700 "$candidate_root" +/usr/bin/git init -q "$candidate_root" +/usr/bin/printf '%s' "$content" > "$candidate_root/result.txt" +/usr/bin/git -C "$candidate_root" add result.txt +/usr/bin/git -C "$candidate_root" commit -q -m candidate +commit_id=$(/usr/bin/git -C "$candidate_root" rev-parse HEAD) +tree_id=$(/usr/bin/git -C "$candidate_root" rev-parse 'HEAD^{tree}') +object_id=$(/usr/bin/git -C "$candidate_root" rev-parse HEAD:result.txt) +"$jq_bin" -S -c -n --arg commit "$commit_id" --arg tree "$tree_id" --arg object "$object_id" ' + {commit_id:$commit,file_object_id:$object,package_id:"fake.forge.a",phase:"forge", + protocol:"ystack.fake-adapter.v1",status:"ok",tree_id:$tree}' diff --git a/adapter-tests/v1/fakes/forge-b.sh b/adapter-tests/v1/fakes/forge-b.sh new file mode 100755 index 0000000..f46ba3b --- /dev/null +++ b/adapter-tests/v1/fakes/forge-b.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 2 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +request=$1 +jq_bin=$2 +"$jq_bin" -e ' + type == "object" and + (keys | sort) == ["artifact","candidate_root","case_id","phase","protocol"] and + .protocol == "ystack.fake-adapter.v1" and .phase == "forge" and + (.case_id | test("\\Amatrix-(aa|ab|ba|bb)\\z")) and + (.candidate_root | type == "string" and startswith("/")) and + (.artifact | type == "object" and + (keys | sort) == ["content","sha256"] and + (.content | type == "string") and + (.sha256 | test("\\A[0-9a-f]{64}\\z"))) +' "$request" >/dev/null +candidate_root=$("$jq_bin" -r '.candidate_root' "$request") +content=$("$jq_bin" -r '.artifact.content' "$request") +digest=$(/usr/bin/printf '%s' "$content" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') +[ "$digest" = "$("$jq_bin" -r '.artifact.sha256' "$request")" ] +/bin/mkdir -m 700 "$candidate_root" +/usr/bin/git init -q "$candidate_root" +/usr/bin/printf '%s' "$content" > "$candidate_root/result.pending" +/bin/mv "$candidate_root/result.pending" "$candidate_root/result.txt" +/usr/bin/git -C "$candidate_root" add result.txt +/usr/bin/git -C "$candidate_root" commit -q -m candidate +commit_id=$(/usr/bin/git -C "$candidate_root" rev-parse HEAD) +tree_id=$(/usr/bin/git -C "$candidate_root" rev-parse 'HEAD^{tree}') +object_id=$(/usr/bin/git -C "$candidate_root" rev-parse HEAD:result.txt) +"$jq_bin" -S -c -n --arg commit "$commit_id" --arg tree "$tree_id" --arg object "$object_id" ' + {commit_id:$commit,file_object_id:$object,package_id:"fake.forge.b",phase:"forge", + protocol:"ystack.fake-adapter.v1",status:"ok",tree_id:$tree}' diff --git a/adapter-tests/v1/fakes/producer-a.sh b/adapter-tests/v1/fakes/producer-a.sh new file mode 100755 index 0000000..b152b61 --- /dev/null +++ b/adapter-tests/v1/fakes/producer-a.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 2 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +request=$1 +jq_bin=$2 +"$jq_bin" -e ' + type == "object" and + (keys | sort) == ["case_id","phase","protocol","source_sha256"] and + .protocol == "ystack.fake-adapter.v1" and .phase == "producer" and + (.case_id | test("\\Amatrix-(aa|ab|ba|bb)\\z")) and + (.source_sha256 | test("\\A[0-9a-f]{64}\\z")) +' "$request" >/dev/null +content='portable candidate' +digest=$(/usr/bin/printf '%s' "$content" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --arg content "$content" --arg digest "$digest" ' + {artifact:{content:$content,sha256:$digest},package_id:"fake.producer.a", + phase:"producer",protocol:"ystack.fake-adapter.v1",status:"ok"}' diff --git a/adapter-tests/v1/fakes/producer-b.sh b/adapter-tests/v1/fakes/producer-b.sh new file mode 100755 index 0000000..c139e29 --- /dev/null +++ b/adapter-tests/v1/fakes/producer-b.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 2 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +request=$1 +jq_bin=$2 +"$jq_bin" -e ' + type == "object" and + (keys | sort) == ["case_id","phase","protocol","source_sha256"] and + .protocol == "ystack.fake-adapter.v1" and .phase == "producer" and + (.case_id | test("\\Amatrix-(aa|ab|ba|bb)\\z")) and + (.source_sha256 | test("\\A[0-9a-f]{64}\\z")) +' "$request" >/dev/null +content=$(/usr/bin/printf '%s%s' portable ' candidate') +digest=$(/usr/bin/printf '%s' "$content" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --arg content "$content" --arg digest "$digest" ' + {artifact:{content:$content,sha256:$digest},package_id:"fake.producer.b", + phase:"producer",protocol:"ystack.fake-adapter.v1",status:"ok"}' diff --git a/adapter-tests/v1/fakes/protocol-fault.sh b/adapter-tests/v1/fakes/protocol-fault.sh new file mode 100755 index 0000000..109f460 --- /dev/null +++ b/adapter-tests/v1/fakes/protocol-fault.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +[ "$#" -eq 3 ] || exit 64 +[ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 +mode=$1 +jq_bin=$3 +case "$mode" in + empty) ;; + malformed) /usr/bin/printf '{\n' ;; + partial) /usr/bin/printf '%s\n' '{"package_id":"fake.protocol-fault","phase":"producer","protocol":"ystack.fake-adapter.v1","status":"ok"}' ;; + degraded) /usr/bin/printf '%s\n' '{"package_id":"fake.protocol-fault","phase":"producer","protocol":"ystack.fake-adapter.v1","status":"degraded"}' ;; + relabelled) + "$jq_bin" -S -c -n '{artifact:{content:"portable candidate",sha256:("0"*64)}, + package_id:"fake.producer.a",phase:"forge",protocol:"ystack.fake-adapter.v1",status:"ok"}' + ;; + timeout) /bin/sleep 5 ;; + transport) exit 9 ;; + *) exit 64 ;; +esac diff --git a/adapter-tests/v1/fixture/source.txt b/adapter-tests/v1/fixture/source.txt new file mode 100644 index 0000000..8ed9265 --- /dev/null +++ b/adapter-tests/v1/fixture/source.txt @@ -0,0 +1,2 @@ +project = moon-garden +feature = portable greeting diff --git a/adapter-tests/v1/inventory.json b/adapter-tests/v1/inventory.json new file mode 100644 index 0000000..cf49e29 --- /dev/null +++ b/adapter-tests/v1/inventory.json @@ -0,0 +1 @@ +{"authorization_ref":{"comment_id":5476938197,"issue_number":153,"scope_comment_id":5474023028},"cases":[{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-aa","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ab","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ba","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-bb","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["expected-error"],"case_id":"reject-degraded","equivalence_group":"protocol-negative","expected_error":"E_DEGRADED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-empty","equivalence_group":"protocol-negative","expected_error":"E_EMPTY","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-malformed","equivalence_group":"protocol-negative","expected_error":"E_MALFORMED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-partial","equivalence_group":"protocol-negative","expected_error":"E_PARTIAL","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-relabelled","equivalence_group":"protocol-negative","expected_error":"E_RELABELLED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-timeout","equivalence_group":"protocol-negative","expected_error":"E_TIMEOUT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-transport","equivalence_group":"protocol-negative","expected_error":"E_TRANSPORT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"}],"protocol":"ystack.adapter-contract.inventory.v1","schema_version":1} diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh new file mode 100755 index 0000000..2220cb6 --- /dev/null +++ b/adapter-tests/v1/runner.sh @@ -0,0 +1,343 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +runner_error() { + printf '%s\n' "${1:-E_RUNTIME}" >&2 + exit 1 +} + +[ "$#" -eq 2 ] || runner_error E_USAGE +inventory=$1 +fixture_root=$2 +runner_source=${BASH_SOURCE[0]} +case "$runner_source:$inventory:$fixture_root" in /*:/*:/*) ;; *) runner_error E_USAGE ;; esac +runner_dir=${runner_source%/*} +repo_root=$(CDPATH='' cd -P -- "$runner_dir/../.." && pwd -P) || runner_error +contract="$runner_dir/contract.jq" +core="$repo_root/scripts/core-contract.sh" +for runner_file in "$runner_source" "$contract" "$inventory" "$core"; do + [ -f "$runner_file" ] && [ ! -L "$runner_file" ] || runner_error E_BINDING +done +jq_bin=$(command -v jq) || runner_error E_DEPENDENCY +[ "$($jq_bin --version 2>/dev/null)" = jq-1.6 ] || runner_error E_DEPENDENCY +fixture_root=$(CDPATH='' cd -P -- "$fixture_root" && pwd -P) || runner_error E_FIXTURE +[ -z "$(find "$fixture_root" -type l -print -quit)" ] || runner_error E_FIXTURE +[ -d "$fixture_root/scratch" ] && [ -d "$fixture_root/target/.git" ] || runner_error E_FIXTURE + +run_tmp=$(/usr/bin/mktemp -d "$fixture_root/scratch/run.XXXXXX") || runner_error +cleanup() { /bin/rm -rf -- "$run_tmp"; } +trap cleanup EXIT HUP INT TERM +/bin/mkdir -m 700 "$run_tmp/home" + +sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } +sha_text() { /usr/bin/printf '%s' "$1" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}'; } +canonical() { + local source=$1 + local output=$2 + "$jq_bin" -S -c . "$source" > "$output" 2>/dev/null && cmp -s "$source" "$output" +} + +[ "$(sha_file "$inventory")" = 1be4e488423d753cf083430aa3560b8dda8d66985b234bce9629b5c0fb1c003a ] || + runner_error E_INVENTORY +inventory_canonical="$run_tmp/inventory.canonical" +canonical "$inventory" "$inventory_canonical" || runner_error E_INVENTORY +fixture_source="$fixture_root/target/source.txt" +[ -f "$fixture_source" ] && [ ! -L "$fixture_source" ] || runner_error E_FIXTURE +fixture_sha=$(sha_file "$fixture_source") +[ "$fixture_sha" = ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6 ] || + runner_error E_FIXTURE +"$jq_bin" -L "$runner_dir" -e --arg command inventory \ + --arg fixture_sha256 "$fixture_sha" --arg package_id '' \ + --arg artifact_sha256 '' --arg target_tree_id '' --arg candidate_tree_id '' \ + -f "$contract" "$inventory" >/dev/null || + runner_error E_INVENTORY + +git_safe() { + /usr/bin/env -i HOME="$run_tmp/home" TMPDIR="$run_tmp/home" PATH=/usr/bin:/bin LC_ALL=C \ + GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_NO_REPLACE_OBJECTS=1 \ + GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + /usr/bin/git --no-replace-objects "$@" +} + +verify_repo() { + local root=$1 + [ -d "$root/.git" ] && [ ! -L "$root/.git" ] && + [ ! -e "$root/.git/objects/info/alternates" ] && + [ "$(git_safe -C "$root" rev-parse --show-toplevel)" = "$root" ] && + [ -z "$(git_safe -C "$root" status --porcelain)" ] && + ! git_safe -C "$root" config --local --get-regexp \ + '(^remote\.|promisor|partialclone|alternates|insteadOf)' >/dev/null 2>&1 +} + +mapping="$fixture_root/repository-map.json" +mapping_canonical="$run_tmp/repository-map.canonical" +if [ ! -f "$mapping" ] || [ -L "$mapping" ] || + ! canonical "$mapping" "$mapping_canonical"; then + runner_error E_MAPPING +fi +"$jq_bin" -e --arg root "$fixture_root" ' + . as $map | + .version == 1 and (.repositories | length) == 13 and + ([.repositories[].repository_id] | length == (unique | length)) and + ([.repositories[].root] | length == (unique | length)) and + ([.repositories[] | select(.repository_id == "fixture.target" and .root == ($root+"/target"))] | length) == 1 and + all(["aa","ab","ba","bb"][]; + . as $cell | all(["assets","manifests","profile"][]; + . as $repo | + ([$map.repositories[] | select(.repository_id == ($cell+"."+$repo) and + .root == ($root+"/cells/"+$cell+"/"+$repo))] | length) == 1)) +' "$mapping" >/dev/null || runner_error E_MAPPING + +verify_repo "$fixture_root/target" || runner_error E_GIT +target_commit=$(git_safe -C "$fixture_root/target" rev-parse HEAD) +target_tree=$(git_safe -C "$fixture_root/target" rev-parse 'HEAD^{tree}') +target_entry=$(git_safe -C "$fixture_root/target" ls-tree HEAD -- source.txt) +target_meta=${target_entry%%$'\t'*} +read -r target_mode target_type target_object <<< "$target_meta" +[ "$target_mode:$target_type:$target_object" = \ + "100644:blob:$(git_safe -C "$fixture_root/target" hash-object source.txt)" ] || + runner_error E_GIT + +package_digest() { + case "$1" in + fake.producer.a) printf '%s\n' b141abf12545f39573ee9cd956ba8a80fa3d37c2266929a765b302ab2b832971 ;; + fake.producer.b) printf '%s\n' 329693fbdea876f595d1deeb4a983a3318ac5938add1206472df18c8d5b57211 ;; + fake.forge.a) printf '%s\n' df6891340de99bd82ceb2ff684e1d3b76f87aabf3d401aa15ee7306d932efd1a ;; + fake.forge.b) printf '%s\n' 1da390b4e723a1fb1abe4418c9ef02c5c7d8e886fe38143a61e31e81e89e83ea ;; + fake.protocol-fault) printf '%s\n' 5a5936a315613960ba5c3760cb851c658c49a0ef030aff744a17da72e81ba199 ;; + *) return 1 ;; + esac +} + +package_path() { + case "$1" in + fake.producer.a) printf '%s\n' packages/producer-a.sh ;; + fake.producer.b) printf '%s\n' packages/producer-b.sh ;; + fake.forge.a) printf '%s\n' packages/forge-a.sh ;; + fake.forge.b) printf '%s\n' packages/forge-b.sh ;; + fake.protocol-fault) printf '%s\n' packages/protocol-fault.sh ;; + *) return 1 ;; + esac +} + +verify_package() { + local assets=$1 + local package=$2 + local expected_ref=${3:-} + local relative digest entry meta mode type object + relative=$(package_path "$package") || return 1 + digest=$(package_digest "$package") || return 1 + [ "$(sha_file "$assets/$relative")" = "$digest" ] || return 1 + entry=$(git_safe -C "$assets" ls-tree HEAD -- "$relative") + meta=${entry%%$'\t'*} + read -r mode type object <<< "$meta" + [ "$mode:$type" = 100755:blob ] && + [ "$object" = "$(git_safe -C "$assets" hash-object "$relative")" ] && + { [ -z "$expected_ref" ] || [ "$object" = "$expected_ref" ]; } +} + +CHILD_STATUS=0 +CHILD_ERROR='' +run_child() { + local executable=$1 + local mode=$2 + local request=$3 + local output=$4 + local diagnostic=$5 + local child_home=$6 + local pid tick=0 + CHILD_STATUS=0 + CHILD_ERROR='' + /bin/mkdir -m 700 "$child_home" + ( + ulimit -t 2 -f 2048 -n 64 + child_args=("$request" "$jq_bin") + [ "$mode" = direct ] || child_args=("$mode" "${child_args[@]}") + exec /usr/bin/env -i HOME="$child_home" TMPDIR="$child_home" PATH=/usr/bin:/bin \ + LC_ALL=C GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \ + GIT_NO_REPLACE_OBJECTS=1 GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + GIT_AUTHOR_NAME=fake GIT_AUTHOR_EMAIL=fake@example.invalid \ + GIT_COMMITTER_NAME=fake GIT_COMMITTER_EMAIL=fake@example.invalid \ + GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /bin/bash "$executable" "${child_args[@]}" + ) > "$output" 2> "$diagnostic" & + pid=$! + while kill -0 "$pid" 2>/dev/null; do + tick=$((tick + 1)) + if [ "$tick" -ge 20 ]; then + kill -TERM "$pid" 2>/dev/null || : + /bin/sleep 0.1 + kill -KILL "$pid" 2>/dev/null || : + wait "$pid" 2>/dev/null || : + CHILD_ERROR=E_TIMEOUT + return 1 + fi + /bin/sleep 0.05 + done + wait "$pid" || CHILD_STATUS=$? + [ "$CHILD_STATUS" -eq 0 ] || { CHILD_ERROR=E_TRANSPORT; return 1; } + [ "$(wc -c < "$output" | tr -d ' ')" -le 1048576 ] && + [ "$(wc -c < "$diagnostic" | tr -d ' ')" -le 4096 ] || { + CHILD_ERROR=E_LIMIT + return 1 + } +} + +cells="$run_tmp/cells.ndjson" +negatives="$run_tmp/negatives.ndjson" +: > "$cells" +: > "$negatives" +for cell in aa ab ba bb; do + cell_id="matrix-$cell" + cell_root="$fixture_root/cells/$cell" + assets="$cell_root/assets" + manifests="$cell_root/manifests" + profiles="$cell_root/profile" + resolved="$cell_root/resolved.json" + for mapped_repo in "$assets" "$manifests" "$profiles"; do + verify_repo "$mapped_repo" || runner_error E_GIT + done + producer_variant=${cell%?} + forge_variant=${cell#?} + producer_package="fake.producer.$producer_variant" + forge_package="fake.forge.$forge_variant" + producer_path=$(package_path "$producer_package") + forge_path=$(package_path "$forge_package") + producer_ref=$("$jq_bin" -r '.body.bindings[] | select(.role=="producer") | .package_ref.object_id' "$cell_root/profile/profiles/default.json") + forge_ref=$("$jq_bin" -r '.body.bindings[] | select(.role=="forge") | .package_ref.object_id' "$cell_root/profile/profiles/default.json") + if ! verify_package "$assets" "$producer_package" "$producer_ref" || + ! verify_package "$assets" "$forge_package" "$forge_ref" || + ! verify_package "$assets" fake.protocol-fault; then + runner_error E_PACKAGE + fi + profile="$profiles/profiles/default.json" + manifest_args=() + for role in forge producer publisher reviewer verifier; do + manifest_file="$manifests/manifests/$role.json" + "$core" validate-document "$manifest_file" >/dev/null 2>&1 || runner_error E_CORE + manifest_args+=("$manifest_file") + done + "$core" validate-document "$profile" >/dev/null 2>&1 || runner_error E_CORE + "$core" validate-profile-set "$profile" "$resolved" "${manifest_args[@]}" \ + >/dev/null 2>&1 || runner_error E_CORE + "$jq_bin" -e --arg profile "profile.$cell" --arg producer "fake-$producer_variant" \ + --arg forge "fake-$forge_variant" ' + .id == $profile and + ([.body.bindings[] | select(.role=="producer") | .manifest_ref.id] == ["manifest.producer"]) + ' "$profile" >/dev/null || runner_error E_PROVENANCE + "$jq_bin" -e --arg producer "fake-$producer_variant" --arg forge "fake-$forge_variant" ' + ([.body.bindings[] | select(.binding.role=="producer") | .adapter_implementation.version] == [$producer]) and + ([.body.bindings[] | select(.binding.role=="forge") | .adapter_implementation.version] == [$forge]) + ' "$resolved" >/dev/null || runner_error E_PROVENANCE + + producer_request="$run_tmp/$cell.producer.request" + "$jq_bin" -S -c -n --arg case_id "$cell_id" --arg source "$fixture_sha" \ + '{case_id:$case_id,phase:"producer",protocol:"ystack.fake-adapter.v1",source_sha256:$source}' > "$producer_request" + producer_output="$run_tmp/$cell.producer.output" + producer_error="$run_tmp/$cell.producer.stderr" + run_child "$assets/$producer_path" direct "$producer_request" "$producer_output" \ + "$producer_error" "$run_tmp/$cell.producer.home" || runner_error "$CHILD_ERROR" + if [ -s "$producer_error" ] || + ! canonical "$producer_output" "$run_tmp/$cell.producer.canonical"; then + runner_error E_PROTOCOL + fi + "$jq_bin" -L "$runner_dir" -e --arg command producer-response \ + --arg package_id "$producer_package" --arg fixture_sha256 '' \ + --arg artifact_sha256 '' --arg target_tree_id '' --arg candidate_tree_id '' \ + -f "$contract" "$producer_output" >/dev/null || runner_error E_PROTOCOL + artifact_content=$("$jq_bin" -r '.artifact.content' "$producer_output") + artifact_sha=$(sha_text "$artifact_content") + [ "$artifact_sha" = "$("$jq_bin" -r '.artifact.sha256' "$producer_output")" ] || runner_error E_OBSERVATION + + candidate_root="$run_tmp/$cell.candidate" + forge_request="$run_tmp/$cell.forge.request" + "$jq_bin" -S -c -n --arg case_id "$cell_id" --arg root "$candidate_root" \ + --slurpfile response "$producer_output" \ + '{artifact:$response[0].artifact,candidate_root:$root,case_id:$case_id, + phase:"forge",protocol:"ystack.fake-adapter.v1"}' > "$forge_request" + forge_output="$run_tmp/$cell.forge.output" + forge_error="$run_tmp/$cell.forge.stderr" + run_child "$assets/$forge_path" direct "$forge_request" "$forge_output" \ + "$forge_error" "$run_tmp/$cell.forge.home" || runner_error "$CHILD_ERROR" + if [ -s "$forge_error" ] || + ! canonical "$forge_output" "$run_tmp/$cell.forge.canonical"; then + runner_error E_PROTOCOL + fi + "$jq_bin" -L "$runner_dir" -e --arg command forge-response \ + --arg package_id "$forge_package" --arg fixture_sha256 '' \ + --arg artifact_sha256 '' --arg target_tree_id '' --arg candidate_tree_id '' \ + -f "$contract" "$forge_output" >/dev/null || runner_error E_PROTOCOL + verify_repo "$candidate_root" || runner_error E_GIT + candidate_commit=$(git_safe -C "$candidate_root" rev-parse HEAD) + candidate_tree=$(git_safe -C "$candidate_root" rev-parse 'HEAD^{tree}') + candidate_object=$(git_safe -C "$candidate_root" rev-parse HEAD:result.txt) + [ "$(/bin/cat "$candidate_root/result.txt")" = "$artifact_content" ] && + [ "$candidate_commit:$candidate_tree:$candidate_object" = \ + "$("$jq_bin" -r '[.commit_id,.tree_id,.file_object_id]|join(":")' "$forge_output")" ] || + runner_error E_OBSERVATION + projection=$("$jq_bin" -L "$runner_dir" -n -c --arg command projection \ + --arg artifact_sha256 "$artifact_sha" --arg target_tree_id "$target_tree" \ + --arg candidate_tree_id "$candidate_tree" --arg package_id '' \ + --arg fixture_sha256 '' -f "$contract") + "$jq_bin" -S -c -n --arg case_id "$cell_id" --argjson projection "$projection" \ + --arg profile_sha256 "$(sha_file "$profile")" \ + --arg producer "$producer_package" --arg producer_digest "$(package_digest "$producer_package")" \ + --arg forge "$forge_package" --arg forge_digest "$(package_digest "$forge_package")" ' + {assertion_ids:["audit-projection","candidate-git","core-validation","environment-clean", + "evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"], + case_id:$case_id,projection:$projection, + provenance:{profile_sha256:$profile_sha256, + producer:{package_id:$producer,sha256:$producer_digest}, + forge:{package_id:$forge,sha256:$forge_digest}}} + ' >> "$cells" +done + +fault_executable="$fixture_root/cells/aa/assets/packages/protocol-fault.sh" +for negative in degraded empty malformed partial relabelled timeout transport; do + negative_id="reject-$negative" + negative_request="$run_tmp/$negative.request" + /usr/bin/printf '%s\n' '{}' > "$negative_request" + negative_output="$run_tmp/$negative.output" + negative_stderr="$run_tmp/$negative.stderr" + observed='' + if ! run_child "$fault_executable" "$negative" "$negative_request" "$negative_output" \ + "$negative_stderr" "$run_tmp/$negative.home"; then + observed=$CHILD_ERROR + elif [ ! -s "$negative_output" ]; then + observed=E_EMPTY + elif ! "$jq_bin" -e . "$negative_output" >/dev/null 2>&1; then + observed=E_MALFORMED + elif [ "$("$jq_bin" -r '.status // ""' "$negative_output")" = degraded ]; then + observed=E_DEGRADED + elif [ "$negative" = relabelled ]; then + observed=E_RELABELLED + else + observed=E_PARTIAL + fi + expected=$("$jq_bin" -r --arg id "$negative_id" '.cases[] | select(.case_id==$id) | .expected_error' "$inventory") + [ "$observed" = "$expected" ] || runner_error E_NEGATIVE + "$jq_bin" -S -c -n --arg case_id "$negative_id" --arg observed_error "$observed" \ + '{assertion_ids:["expected-error"],case_id:$case_id,observed_error:$observed_error}' >> "$negatives" +done + +"$jq_bin" -s -e '([.[].projection] | unique | length) == 1 and + ([.[].provenance.producer.package_id] | unique | length) == 2 and + ([.[].provenance.forge.package_id] | unique | length) == 2 and + ([.[].provenance.profile_sha256] | unique | length) == 4' "$cells" >/dev/null || + runner_error E_EQUIVALENCE +inventory_sha=$(sha_file "$inventory") +"$jq_bin" -S -c -n --slurpfile cells "$cells" --slurpfile negatives "$negatives" \ + --arg inventory_sha "$inventory_sha" --arg target_commit "$target_commit" \ + --arg target_object "$target_object" ' + {schema_version:1,kind:"adapter_contract_observation", + inventory_acceptance_ref:{authorization_comment_id:5476938197,sha256:$inventory_sha}, + cells:$cells,negative_observations:$negatives, + target_facts:{commit_id:$target_commit,source_object_id:$target_object}, + non_claims:["authority","qualification","approval","publish","merge","branch-write", + "real-adapter-safety","network-isolation","host-isolation","external-target-smoke"]} +' +trap - EXIT HUP INT TERM +cleanup diff --git a/ci/required-files.txt b/ci/required-files.txt index 9f8fdd4..8411409 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -154,3 +154,15 @@ core/v2/generations/g-392d20099dfa99872764009b268c8871914b4dbc0da467ec346baa9218 core/v2/generations/g-392d20099dfa99872764009b268c8871914b4dbc0da467ec346baa921818ae3e/modules/schema.jq core/v2/generations/g-392d20099dfa99872764009b268c8871914b4dbc0da467ec346baa921818ae3e/modules/stage_request.jq scripts/test/portable-core-v2-fake-forge.test.sh + +# Inactive fake producer/forge adapter contract matrix +adapter-tests/v1/runner.sh +adapter-tests/v1/contract.jq +adapter-tests/v1/inventory.json +adapter-tests/v1/fakes/producer-a.sh +adapter-tests/v1/fakes/producer-b.sh +adapter-tests/v1/fakes/forge-a.sh +adapter-tests/v1/fakes/forge-b.sh +adapter-tests/v1/fakes/protocol-fault.sh +adapter-tests/v1/fixture/source.txt +scripts/test/portable-adapter-contracts.test.sh diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh new file mode 100755 index 0000000..108c8c2 --- /dev/null +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +test_root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) +runner="$test_root/adapter-tests/v1/runner.sh" +inventory="$test_root/adapter-tests/v1/inventory.json" +fixture_builder="$test_root/scripts/test/portable-profile-resolution-fixtures.sh" +resolver_runtime="$test_root/resolver/v1/profile-resolve-runtime.sh" +tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-adapter-contracts.XXXXXX") +tmp=$(CDPATH='' cd -P -- "$tmp" && pwd -P) +trap '/bin/rm -rf -- "$tmp"' EXIT + +fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } +pass_count=0 +pass() { pass_count=$((pass_count + 1)); /usr/bin/printf 'ok %s - %s\n' "$pass_count" "$1"; } +sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } + +platform=$(/usr/bin/uname -s):$(/usr/bin/uname -m) +case "$platform" in + Darwin:*) jq_asset=jq-osx-amd64; compiler=/usr/bin/clang ;; + Linux:x86_64) jq_asset=jq-linux64; compiler=/usr/bin/cc ;; + *) fail "unsupported host $platform" ;; +esac +jq_cache="${TMPDIR:-/tmp}/ystack-portable-core-jq16/$jq_asset" +[ -f "$jq_cache" ] || fail 'verified jq 1.6 cache missing' +case "$jq_asset" in + jq-osx-amd64) jq_sha=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef ;; + *) jq_sha=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 ;; +esac +[ "$(sha_file "$jq_cache")" = "$jq_sha" ] || fail 'jq digest' +bin="$tmp/bin" +/bin/mkdir -m 700 "$bin" +/bin/cp "$jq_cache" "$bin/jq" +/bin/chmod 0555 "$bin/jq" +jq_bin="$bin/jq" +if [ "$platform" = Linux:x86_64 ]; then + /bin/cp /usr/bin/awk "$bin/awk" +else + /usr/bin/printf '%s\n' '#!/bin/bash' 'exec /usr/bin/awk "$@"' > "$bin/awk" +fi +/bin/chmod 0555 "$bin/awk" +"$compiler" -std=c11 -O2 -Wall -Wextra -Werror -pedantic \ + "$test_root/resolver/v1/nofollow-snapshot.c" -o "$bin/nofollow-snapshot" +"$compiler" -std=c11 -O2 -Wall -Wextra -Werror -pedantic \ + "$test_root/scripts/test/portable-profile-resolution-launcher.c" -o "$bin/launcher" +pass 'fixed resolver launcher and dependencies built' + +fixture="$tmp/garden-fixture" +/bin/mkdir -m 700 "$fixture" "$fixture/cells" "$fixture/scratch" "$fixture/target" +/bin/cp "$test_root/adapter-tests/v1/fixture/source.txt" "$fixture/target/source.txt" +/usr/bin/git init -q "$fixture/target" +/usr/bin/git -C "$fixture/target" config user.name fixture +/usr/bin/git -C "$fixture/target" config user.email fixture@example.invalid +GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /usr/bin/git -C "$fixture/target" add source.txt +GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /usr/bin/git -C "$fixture/target" commit -q -m fixture + +for cell in aa ab ba bb; do + cell_root="$fixture/cells/$cell" + PATH="$bin:/usr/bin:/bin" "$fixture_builder" "$cell_root" "$jq_bin" "$cell" >/dev/null + /bin/mkdir -m 700 "$cell_root/resolver-sandbox" + YSTACK_TEST_SANDBOX="$cell_root/resolver-sandbox" \ + "$bin/launcher" resolve "$resolver_runtime" "$bin/nofollow-snapshot" \ + "$jq_bin" "$cell_root/request.json" "$cell_root/map.json" > "$cell_root/resolved.json" +done +pass 'four honest profile variants resolved through the inactive resolver' + +"$jq_bin" -S -c -n --arg root "$fixture" ' + {version:1,repositories: + ([{repository_id:"fixture.target",root:($root+"/target")}] + + (["aa","ab","ba","bb"] | + map(. as $cell | ["assets","manifests","profile"] | + map(. as $repo | {repository_id:($cell+"."+$repo), + root:($root+"/cells/"+$cell+"/"+$repo)})) | add))} +' > "$fixture/repository-map.json" + +fingerprint_before=$(/usr/bin/git -C "$fixture/target" rev-parse 'HEAD^{tree}') +output="$tmp/observation.json" +error="$tmp/observation.stderr" +GH_TOKEN=must-not-leak AWS_SECRET_ACCESS_KEY=must-not-leak SSH_AUTH_SOCK=/must/not/leak \ + PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$output" 2> "$error" || { + /bin/cat "$error" >&2 + fail 'accepted matrix run' +} +[ ! -s "$error" ] || fail 'success diagnostics' +"$jq_bin" -e ' + .schema_version == 1 and .kind == "adapter_contract_observation" and + (.cells | length) == 4 and (.negative_observations | length) == 7 and + ([.cells[].projection] | unique | length) == 1 and + ([.cells[].provenance.profile_sha256] | unique | length) == 4 and + (.non_claims | index("external-target-smoke") != null) and + (.non_claims | index("network-isolation") != null) and + (.inventory_acceptance_ref.authorization_comment_id == 5476938197) +' "$output" >/dev/null || fail 'observation shape and equivalence' +[ "$fingerprint_before" = "$(/usr/bin/git -C "$fixture/target" rev-parse 'HEAD^{tree}')" ] || + fail 'target changed' +[ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || fail 'scratch cleanup' +/usr/bin/grep -Fq 'moon-garden' "$fixture/target/source.txt" || fail 'unrelated target fixture' +pass '2x2 projection, provenance, environment, Git truth, and cleanup' + +expect_failure() { + local name=$1 + local expected=$2 + local chosen_inventory=${3:-$inventory} + local case_out="$tmp/$name.out" + local case_err="$tmp/$name.err" + if PATH="$bin:/usr/bin:/bin" "$runner" "$chosen_inventory" "$fixture" \ + > "$case_out" 2> "$case_err"; then + fail "$name accepted" + fi + [ ! -s "$case_out" ] && [ "$(/bin/cat "$case_err")" = "$expected" ] || + fail "$name error" + pass "$name" +} + +for mutation in dropped duplicated relabelled mutated; do + changed="$tmp/inventory-$mutation.json" + case "$mutation" in + dropped) filter='del(.cases[0])' ;; + duplicated) filter='.cases += [.cases[0]]' ;; + relabelled) filter='.cases[0].case_id = "matrix-zz"' ;; + mutated) filter='.cases[0].assertions[0] = "different"' ;; + esac + "$jq_bin" -S -c "$filter" "$inventory" > "$changed" + expect_failure "inventory-$mutation" E_INVENTORY "$changed" +done + +map_saved="$tmp/repository-map.saved" +/bin/cp "$fixture/repository-map.json" "$map_saved" +"$jq_bin" -S -c '.repositories[1].root = .repositories[0].root' \ + "$map_saved" > "$fixture/repository-map.json" +expect_failure mapping-mismatch E_MAPPING +/bin/cp "$map_saved" "$fixture/repository-map.json" + +/usr/bin/printf '%s\n' /invalid/alternate > "$fixture/target/.git/objects/info/alternates" +expect_failure git-alternates E_GIT +/bin/rm "$fixture/target/.git/objects/info/alternates" + +resolved_saved="$tmp/resolved.saved" +/bin/cp "$fixture/cells/aa/resolved.json" "$resolved_saved" +"$jq_bin" -S -c '.body.profile_ref.sha256 = ("0"*64)' "$resolved_saved" > \ + "$fixture/cells/aa/resolved.json" +expect_failure core-ref-mismatch E_CORE +/bin/cp "$resolved_saved" "$fixture/cells/aa/resolved.json" + +source_saved="$tmp/source.saved" +/bin/cp "$fixture/target/source.txt" "$source_saved" +/usr/bin/printf '%s\n' changed > "$fixture/target/source.txt" +/usr/bin/git -C "$fixture/target" add source.txt +GIT_AUTHOR_DATE=2000-01-02T00:00:00Z GIT_COMMITTER_DATE=2000-01-02T00:00:00Z \ + /usr/bin/git -C "$fixture/target" commit -q -m changed +expect_failure fixture-digest-mismatch E_FIXTURE +/bin/cp "$source_saved" "$fixture/target/source.txt" +/usr/bin/git -C "$fixture/target" add source.txt +GIT_AUTHOR_DATE=2000-01-03T00:00:00Z GIT_COMMITTER_DATE=2000-01-03T00:00:00Z \ + /usr/bin/git -C "$fixture/target" commit -q -m restored + +producer="$fixture/cells/aa/assets/packages/producer-a.sh" +/usr/bin/printf '%s\n' '# changed' >> "$producer" +/usr/bin/git -C "$fixture/cells/aa/assets" add packages/producer-a.sh +GIT_AUTHOR_DATE=2000-01-02T00:00:00Z GIT_COMMITTER_DATE=2000-01-02T00:00:00Z \ + /usr/bin/git -C "$fixture/cells/aa/assets" commit -q -m changed +expect_failure executable-digest-mismatch E_PACKAGE + +/usr/bin/printf 'portable adapter contracts: %s focused checks passed\n' "$pass_count" diff --git a/scripts/test/portable-profile-resolution-fixtures.sh b/scripts/test/portable-profile-resolution-fixtures.sh index c0fd83b..7cc357b 100755 --- a/scripts/test/portable-profile-resolution-fixtures.sh +++ b/scripts/test/portable-profile-resolution-fixtures.sh @@ -67,12 +67,22 @@ fixture_scope() { scope_sha256:$hash}' } -if [ "$#" -ne 2 ]; then - printf '%s\n' 'usage: portable-profile-resolution-fixtures.sh OUTPUT_ROOT JQ_1_6' >&2 +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + printf '%s\n' 'usage: portable-profile-resolution-fixtures.sh OUTPUT_ROOT JQ_1_6 [aa|ab|ba|bb]' >&2 exit 64 fi fixture_root=$1 fixture_jq=$2 +fixture_variant=${3:-default} +case "$fixture_variant" in + default) fixture_producer_variant=a; fixture_forge_variant=a; fixture_profile_id=profile.example ;; + aa|ab|ba|bb) + fixture_producer_variant=${fixture_variant%?} + fixture_forge_variant=${fixture_variant#?} + fixture_profile_id="profile.$fixture_variant" + ;; + *) exit 64 ;; +esac case "$fixture_jq" in /*) ;; *) exit 1 ;; esac [ -x "$fixture_jq" ] && [ -f "$fixture_jq" ] && [ ! -L "$fixture_jq" ] && [ "$("$fixture_jq" --version)" = jq-1.6 ] || exit 1 @@ -96,10 +106,23 @@ fixture_init_repo "$fixture_assets" sha256 /usr/bin/printf '%s\n' 'Bounded fixture skill.' > "$fixture_assets/skills/producer.md" /usr/bin/printf '%s\n' tool-package > "$fixture_assets/tools/producer.bin" /usr/bin/printf '%s\n' '{"tool":true}' > "$fixture_assets/tools/producer.json" +if [ "$fixture_variant" != default ]; then + for fixture_fake in producer-a producer-b forge-a forge-b protocol-fault; do + /bin/cp "$fixture_repo/adapter-tests/v1/fakes/$fixture_fake.sh" \ + "$fixture_assets/packages/$fixture_fake.sh" + /bin/chmod 0755 "$fixture_assets/packages/$fixture_fake.sh" + done +fi /bin/dd if=/dev/zero of="$fixture_assets/packages/producer-large.bin" bs=1048576 count=17 2>/dev/null fixture_assets_commit=$(fixture_commit "$fixture_assets") fixture_producer_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/producer.bin) fixture_forge_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/forge.bin) +if [ "$fixture_variant" != default ]; then + fixture_producer_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 \ + "$fixture_assets_commit" "packages/producer-$fixture_producer_variant.sh") + fixture_forge_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 \ + "$fixture_assets_commit" "packages/forge-$fixture_forge_variant.sh") +fi fixture_publisher_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/publisher.bin) fixture_reviewer_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/reviewer.bin) fixture_verifier_package=$(fixture_git_ref repo.assets "$fixture_assets" sha256 "$fixture_assets_commit" packages/verifier.bin) @@ -119,6 +142,7 @@ fixture_tool=$("$fixture_jq" -S -c -n --argjson package "$fixture_tool_package" fixture_init_repo "$fixture_manifests" sha1 /bin/mkdir -p "$fixture_manifests/manifests" for fixture_role in forge producer publisher reviewer verifier; do + fixture_adapter_version=v1 case "$fixture_role" in forge) fixture_package=$fixture_forge_package @@ -127,6 +151,7 @@ for fixture_role in forge producer publisher reviewer verifier; do fixture_permissions='["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"]' fixture_tools='[]' fixture_extra='' + [ "$fixture_variant" = default ] || fixture_adapter_version="fake-$fixture_forge_variant" ;; producer) fixture_package=$fixture_producer_package @@ -135,6 +160,7 @@ for fixture_role in forge producer publisher reviewer verifier; do fixture_permissions='["core.perm.evidence.write.v1","core.perm.model.invoke.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"]' fixture_tools="[$fixture_tool]" fixture_extra=",\"config_contract_ref\":$fixture_config_scope" + [ "$fixture_variant" = default ] || fixture_adapter_version="fake-$fixture_producer_variant" ;; publisher) fixture_package=$fixture_publisher_package @@ -162,7 +188,7 @@ for fixture_role in forge producer publisher reviewer verifier; do ;; esac /usr/bin/printf '%s\n' \ - "{\"schema_version\":2,\"kind\":\"adapter_manifest\",\"id\":\"manifest.$fixture_role\",\"body\":{\"adapter_version\":\"v1\",\"package_ref\":$fixture_package,\"offered_roles\":[\"$fixture_role\"],\"offered_execution_kinds\":[\"$fixture_execution\"],\"offered_capabilities\":$fixture_capabilities,\"offered_permissions\":$fixture_permissions,\"offered_tools\":$fixture_tools$fixture_extra}}" | + "{\"schema_version\":2,\"kind\":\"adapter_manifest\",\"id\":\"manifest.$fixture_role\",\"body\":{\"adapter_version\":\"$fixture_adapter_version\",\"package_ref\":$fixture_package,\"offered_roles\":[\"$fixture_role\"],\"offered_execution_kinds\":[\"$fixture_execution\"],\"offered_capabilities\":$fixture_capabilities,\"offered_permissions\":$fixture_permissions,\"offered_tools\":$fixture_tools$fixture_extra}}" | fixture_canonical "$fixture_manifests/manifests/$fixture_role.json" /bin/bash "$fixture_core" validate-document "$fixture_manifests/manifests/$fixture_role.json" done @@ -253,7 +279,8 @@ for fixture_role in forge producer publisher reviewer verifier; do fixture_bindings=$("$fixture_jq" -S -c --argjson binding "$fixture_binding" '. + [$binding] | sort_by(.binding_id)' <<< "$fixture_bindings") done "$fixture_jq" -S -c -n --argjson bindings "$fixture_bindings" \ - '{schema_version:2,kind:"profile",id:"profile.example",body:{profile_version:"v1",bindings:$bindings}}' \ + --arg profile_id "$fixture_profile_id" \ + '{schema_version:2,kind:"profile",id:$profile_id,body:{profile_version:"v1",bindings:$bindings}}' \ > "$fixture_profile/profiles/default.json" /bin/bash "$fixture_core" validate-document "$fixture_profile/profiles/default.json" /bin/cp "$fixture_profile/profiles/default.json" "$fixture_profile/default.json" From d3ea5573bc5c617d1bb124a826d940951e3e3dde Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 10:28:28 -0400 Subject: [PATCH 02/12] Validate adapter stage records end to end --- README.md | 3 +- RESTORE.md | 7 +- adapter-tests/v1/contract.jq | 165 +++++++++++++----- adapter-tests/v1/fakes/forge-a.sh | 72 ++++---- adapter-tests/v1/fakes/forge-b.sh | 72 ++++---- adapter-tests/v1/fakes/producer-a.sh | 47 ++++-- adapter-tests/v1/fakes/producer-b.sh | 41 +++-- adapter-tests/v1/fakes/protocol-fault.sh | 10 +- adapter-tests/v1/runner.sh | 204 ++++++++++++++++++----- 9 files changed, 439 insertions(+), 182 deletions(-) mode change 100755 => 100644 adapter-tests/v1/fakes/forge-a.sh mode change 100755 => 100644 adapter-tests/v1/fakes/forge-b.sh mode change 100755 => 100644 adapter-tests/v1/fakes/producer-a.sh mode change 100755 => 100644 adapter-tests/v1/fakes/producer-b.sh diff --git a/README.md b/README.md index fe612be..a428b39 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ the resolver, select a live profile, or qualify a real forge. `adapter-tests/v1/` runs a fixed 2×2 producer/forge matrix against one unrelated local Git fixture. Its accepted inventory and four distinct fake entrypoints are digest-pinned. The runner revalidates each resolved profile with portable core v2, -then checks package, target, artifact, candidate, and Git identities itself. +validates every producer and forge stage request/result, then checks payload links, +package, target, candidate, receipt, and Git identities itself. The result is observation only. It grants no authority, qualification, approval, publish, merge, or branch-write capability. The fakes run with a cleared environment, diff --git a/RESTORE.md b/RESTORE.md index bebac56..2066160 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -314,9 +314,10 @@ Restore every path in the manifest's inactive fake adapter matrix block, then ru bash scripts/test/portable-adapter-contracts.test.sh ``` -The proof resolves four fake-only profiles and checks the accepted 2×2 matrix plus -closed negative protocol cases. It uses no real adapter or credential and makes no -network/host isolation, qualification, activation, or external-target-smoke claim. +The proof resolves four fake-only profiles, validates both core stage records in +each cell, and checks the accepted 2×2 matrix plus closed negative protocol cases. +It uses no real adapter or credential and makes no network/host isolation, +qualification, activation, or external-target-smoke claim. --- diff --git a/adapter-tests/v1/contract.jq b/adapter-tests/v1/contract.jq index 3d4a9e0..ef54f3c 100644 --- a/adapter-tests/v1/contract.jq +++ b/adapter-tests/v1/contract.jq @@ -1,53 +1,130 @@ +import "portable-core-stage-request-fixtures" as request; +import "portable-core-result-truth-fixtures" as result; + def exact($keys): type == "object" and (keys | sort) == ($keys | sort); def sha256: type == "string" and test("\\A[0-9a-f]{64}\\z"); - +def v2: walk(if type == "object" and has("schema_version") then .schema_version = 2 else . end); +def payload_ok: + exact(["data","media_type","payload_id","sha256"]) and + (.data | type == "string") and (.media_type | type == "string") and + (.payload_id | type == "string") and (.sha256 | sha256); def positive_assertions: ["audit-projection","candidate-git","core-validation","environment-clean", "evidence-projection","gate-projection","outcome-projection", "risk-projection","target-git"]; +def expected_cases($fixture): + ["aa","ab","ba","bb"] | map(. as $cell | { + case_id:("matrix-"+$cell),phase:"pipeline", + producer_package_id:("fake.producer."+($cell[0:1])), + forge_package_id:("fake.forge."+($cell[1:2])),expected_verdict:"pass", + expected_error:"",equivalence_group:"portable-fake-v1", + assertions:positive_assertions,fixture_sha256:$fixture + }) + [ + {case_id:"reject-degraded",expected_error:"E_DEGRADED"}, + {case_id:"reject-empty",expected_error:"E_EMPTY"}, + {case_id:"reject-malformed",expected_error:"E_MALFORMED"}, + {case_id:"reject-partial",expected_error:"E_PARTIAL"}, + {case_id:"reject-relabelled",expected_error:"E_RELABELLED"}, + {case_id:"reject-timeout",expected_error:"E_TIMEOUT"}, + {case_id:"reject-transport",expected_error:"E_TRANSPORT"} + ] | map(if has("phase") then . else . + { + phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none", + expected_verdict:"reject",equivalence_group:"protocol-negative", + assertions:["expected-error"],fixture_sha256:$fixture + } end); +def inventory_ok: + (.inventory | exact(["authorization_ref","cases","protocol","schema_version"])) and + .inventory.schema_version == 1 and + .inventory.protocol == "ystack.adapter-contract.inventory.v1" and + .inventory.authorization_ref == + {comment_id:5476938197,issue_number:153,scope_comment_id:5474023028} and + .inventory.cases == expected_cases(.fixture_sha256); -def expected_cases($fixture): [ - {case_id:"matrix-aa",phase:"pipeline",producer_package_id:"fake.producer.a",forge_package_id:"fake.forge.a",expected_verdict:"pass",expected_error:"",equivalence_group:"portable-fake-v1",assertions:positive_assertions,fixture_sha256:$fixture}, - {case_id:"matrix-ab",phase:"pipeline",producer_package_id:"fake.producer.a",forge_package_id:"fake.forge.b",expected_verdict:"pass",expected_error:"",equivalence_group:"portable-fake-v1",assertions:positive_assertions,fixture_sha256:$fixture}, - {case_id:"matrix-ba",phase:"pipeline",producer_package_id:"fake.producer.b",forge_package_id:"fake.forge.a",expected_verdict:"pass",expected_error:"",equivalence_group:"portable-fake-v1",assertions:positive_assertions,fixture_sha256:$fixture}, - {case_id:"matrix-bb",phase:"pipeline",producer_package_id:"fake.producer.b",forge_package_id:"fake.forge.b",expected_verdict:"pass",expected_error:"",equivalence_group:"portable-fake-v1",assertions:positive_assertions,fixture_sha256:$fixture}, - {case_id:"reject-degraded",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_DEGRADED",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, - {case_id:"reject-empty",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_EMPTY",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, - {case_id:"reject-malformed",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_MALFORMED",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, - {case_id:"reject-partial",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_PARTIAL",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, - {case_id:"reject-relabelled",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_RELABELLED",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, - {case_id:"reject-timeout",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_TIMEOUT",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture}, - {case_id:"reject-transport",phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none",expected_verdict:"reject",expected_error:"E_TRANSPORT",equivalence_group:"protocol-negative",assertions:["expected-error"],fixture_sha256:$fixture} -]; - -def inventory_ok($fixture): - exact(["authorization_ref","cases","protocol","schema_version"]) and - .schema_version == 1 and .protocol == "ystack.adapter-contract.inventory.v1" and - .authorization_ref == {comment_id:5476938197,issue_number:153,scope_comment_id:5474023028} and - .cases == expected_cases($fixture); - -def producer_response_ok($package): - exact(["artifact","package_id","phase","protocol","status"]) and - .protocol == "ystack.fake-adapter.v1" and .phase == "producer" and - .status == "ok" and .package_id == $package and - (.artifact | exact(["content","sha256"]) and (.content | type == "string") and (.sha256 | sha256)); - -def forge_response_ok($package): - exact(["commit_id","file_object_id","package_id","phase","protocol","status","tree_id"]) and - .protocol == "ystack.fake-adapter.v1" and .phase == "forge" and - .status == "ok" and .package_id == $package and - (.commit_id | test("\\A[0-9a-f]{40}\\z")) and - (.tree_id | test("\\A[0-9a-f]{40}\\z")) and - (.file_object_id | test("\\A[0-9a-f]{40}\\z")); - -def projection($artifact; $target_tree; $candidate_tree): - {artifact_sha256:$artifact,risk:"core:routine",gate_refs:[], - outcome:"change:changed",evidence:["deterministic:passed"], - audit:{producer_status:"completed",forge_status:"completed", - target_tree_id:$target_tree,candidate_tree_id:$candidate_tree}}; +def revision($c): + {repository_id:"fixture.target",hash_algorithm:"sha1",commit_id:$c.target_commit}; +def source_ref($c): + {revision:revision($c),location:{kind:"path",value:"source.txt"},object_type:"blob", + object_id:$c.target_object,mode:"100644"}; +def tree_ref($c): + {revision:revision($c),location:{kind:"root"},object_type:"tree", + object_id:$c.target_tree,mode:"040000"}; +def target_request($c): + .body.target_repository_id="fixture.target" | + .body.target_revision={state:"present",value:revision($c)} | + .body.base={state:"present",value:revision($c)} | + .body.source={state:"present",value:{type:"git-object",value:source_ref($c)}}; +def producer_request($c): + request::request_doc("producer";$c.resolved_sha) | v2 | + .id=("request."+$c.case_id+".producer") | + .body.stage_id=("stage."+$c.case_id+".producer") | + .body.resolved_profile_ref.id=$c.resolved_id | + .body.selection_ref=$c.resolved_profile.body.selection_ref | + .body.repository_context_ref=$c.resolved_profile.body.repository_context_ref | + target_request($c) | + (.body.inputs[] | select(.input_id=="input.output") | .input_id)="input.allowed-delta" | + .body.inputs |= sort_by(.input_id) | + .body.operation.arguments={artifact_kind:"git-patch", + allowed_delta:request::delivered("allowed-delta";"allowed-delta";request::sha("3"))} | + .body.operation.arguments.allowed_delta.ref.subject_ref as $allowed | + (.body.inputs[] | select(.input_id=="input.allowed-delta") | .value)=$allowed; +def forge_request($c): + request::request_doc("producer";$c.resolved_sha) | v2 | + .id=("request."+$c.case_id+".forge") | + .body.stage_id=("stage."+$c.case_id+".forge") | + .body.resolved_profile_ref.id=$c.resolved_id | + .body.selection_ref=$c.resolved_profile.body.selection_ref | + .body.repository_context_ref=$c.resolved_profile.body.repository_context_ref | + target_request($c) | + .body.inputs=([ + request::named_content_input("finish";request::sha("1")), + request::named_content_input("materialize";request::sha("3")), + request::named_content_input("producer-patch";$c.patch_sha), + {input_id:"input.source-tree",value:{type:"artifact",value:{type:"git-object",value:tree_ref($c)}}}, + request::named_content_input("verify";request::sha("2")) + ] | sort_by(.input_id)) | + .body.operation={role:"forge",binding_id:"binding.forge", + capability_id:"core.forge.materialize-candidate.v2", + permissions:["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1", + "core.perm.scratch.write.v1","core.perm.target.read.v1"], + arguments:{source_tree_input_id:"input.source-tree",candidate_output_id:"candidate.repository", + materialization_contract:request::delivered("output-contract";"materialize";request::sha("3")), + network_mode:"deny"}} | + .body.required_evidence_kinds=["deterministic"]; +def stage_result($c; $output_id; $media_type; $payload_sha): + result::completed_result_doc($c.stage_request;$c.request_sha; + $c.resolved_profile;$c.resolved_sha) | v2 | + .id=("result."+$c.case_id+"."+$c.phase) | + .body.outcome={family:"change",value:"changed"} | + .body.outputs=[{output_id:$output_id, + ref:{content_id:("output."+$c.case_id+"."+$c.phase),media_type:$media_type,sha256:$payload_sha}}] | + if $c.phase == "producer" then .body.delta_ref=.body.outputs[0].ref else . end; +def request_envelope_ok: + exact(["case_id","payloads","phase","protocol_version","stage_request"]) and + .protocol_version == 1 and (.phase == "producer" or .phase == "forge") and + .stage_request.schema_version == 2 and .stage_request.kind == "stage_request" and + (.payloads | type == "array" and all(.[];payload_ok) and + ([.[].payload_id] | length == (unique | length))); +def response_envelope_ok: + (.response | exact(["case_id","payloads","phase","protocol_version","stage_result"])) and + .response.protocol_version == 1 and .response.case_id == .case_id and + .response.phase == .phase and .response.stage_result.schema_version == 2 and + .response.stage_result.kind == "stage_result" and + (.response.payloads | type == "array" and all(.[];payload_ok)); +def projection: + {artifact_sha256:.artifact_sha256,risk:.producer_request.body.risk, + gate_refs:.producer_request.body.risk.required_gate_refs, + outcome:[.producer_result.body.outcome,.forge_result.body.outcome], + evidence:[.producer_result.body.evidence,.forge_result.body.evidence], + audit:{producer_status:.producer_result.body.status,forge_status:.forge_result.body.status, + producer_attempt:.producer_result.body.attempt_number, + forge_attempt:.forge_result.body.attempt_number, + target_tree_id:.target_tree,candidate_tree_id:.candidate_tree}}; -if $command == "inventory" then inventory_ok($fixture_sha256) -elif $command == "producer-response" then producer_response_ok($package_id) -elif $command == "forge-response" then forge_response_ok($package_id) -elif $command == "projection" then projection($artifact_sha256;$target_tree_id;$candidate_tree_id) +if $command == "inventory" then inventory_ok +elif $command == "producer-request" then producer_request(.) +elif $command == "forge-request" then forge_request(.) +elif $command == "stage-result" then stage_result(.;.output_id;.media_type;.payload_sha) +elif $command == "request-envelope" then request_envelope_ok +elif $command == "response-envelope" then response_envelope_ok +elif $command == "projection" then projection else error("unknown-command") end diff --git a/adapter-tests/v1/fakes/forge-a.sh b/adapter-tests/v1/fakes/forge-a.sh old mode 100755 new mode 100644 index 06d9ed1..83650d6 --- a/adapter-tests/v1/fakes/forge-a.sh +++ b/adapter-tests/v1/fakes/forge-a.sh @@ -1,33 +1,47 @@ #!/usr/bin/env bash # shellcheck disable=SC2016 set -euo pipefail -[ "$#" -eq 2 ] || exit 64 +[ "$#" -eq 4 ] || exit 64 [ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 -request=$1 -jq_bin=$2 -"$jq_bin" -e ' - type == "object" and - (keys | sort) == ["artifact","candidate_root","case_id","phase","protocol"] and - .protocol == "ystack.fake-adapter.v1" and .phase == "forge" and - (.case_id | test("\\Amatrix-(aa|ab|ba|bb)\\z")) and - (.candidate_root | type == "string" and startswith("/")) and - (.artifact | type == "object" and - (keys | sort) == ["content","sha256"] and - (.content | type == "string") and - (.sha256 | test("\\A[0-9a-f]{64}\\z"))) -' "$request" >/dev/null -candidate_root=$("$jq_bin" -r '.candidate_root' "$request") -content=$("$jq_bin" -r '.artifact.content' "$request") -digest=$(/usr/bin/printf '%s' "$content" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') -[ "$digest" = "$("$jq_bin" -r '.artifact.sha256' "$request")" ] -/bin/mkdir -m 700 "$candidate_root" -/usr/bin/git init -q "$candidate_root" -/usr/bin/printf '%s' "$content" > "$candidate_root/result.txt" -/usr/bin/git -C "$candidate_root" add result.txt -/usr/bin/git -C "$candidate_root" commit -q -m candidate -commit_id=$(/usr/bin/git -C "$candidate_root" rev-parse HEAD) -tree_id=$(/usr/bin/git -C "$candidate_root" rev-parse 'HEAD^{tree}') -object_id=$(/usr/bin/git -C "$candidate_root" rev-parse HEAD:result.txt) -"$jq_bin" -S -c -n --arg commit "$commit_id" --arg tree "$tree_id" --arg object "$object_id" ' - {commit_id:$commit,file_object_id:$object,package_id:"fake.forge.a",phase:"forge", - protocol:"ystack.fake-adapter.v1",status:"ok",tree_id:$tree}' +request=$1 jq_bin=$2 contract=$3 modules=$4 +"$jq_bin" -L "${contract%/*}" -L "$modules" -e --arg command request-envelope \ + -f "$contract" "$request" >/dev/null +"$jq_bin" -S -c '.stage_request' "$request" > "$HOME/stage-request.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="resolved-profile") | .data' \ + "$request" > "$HOME/resolved.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="source") | .data' "$request" > "$HOME/source.txt" +"$jq_bin" -j '.payloads[] | select(.payload_id=="producer.patch") | .data' "$request" > "$HOME/producer.patch" +patch_sha=$(/usr/bin/shasum -a 256 "$HOME/producer.patch" | /usr/bin/awk '{print $1}') +candidate="$HOME/candidate" +/bin/mkdir -m 700 "$candidate" +/usr/bin/git init -q "$candidate" +/bin/cp "$HOME/source.txt" "$candidate/source.txt" +/usr/bin/git -C "$candidate" add source.txt +/usr/bin/git -C "$candidate" commit -q -m source +/usr/bin/git -C "$candidate" apply "$HOME/producer.patch" +/usr/bin/git -C "$candidate" add source.txt +/usr/bin/git -C "$candidate" commit -q -m candidate +commit_id=$(/usr/bin/git -C "$candidate" rev-parse HEAD) +tree_id=$(/usr/bin/git -C "$candidate" rev-parse 'HEAD^{tree}') +object_id=$(/usr/bin/git -C "$candidate" rev-parse HEAD:source.txt) +source_tree=$("$jq_bin" -r '.stage_request.body.inputs[] | select(.input_id=="input.source-tree") | .value.value.value.object_id' "$request") +"$jq_bin" -S -c -n --arg source_tree "$source_tree" --arg patch "$patch_sha" \ + --arg commit "$commit_id" --arg tree "$tree_id" --arg object "$object_id" ' + {candidate_commit_id:$commit,candidate_object_id:$object,candidate_tree_id:$tree, + patch_sha256:$patch,source_tree_id:$source_tree}' > "$HOME/receipt.json" +receipt_sha=$(/usr/bin/shasum -a 256 "$HOME/receipt.json" | /usr/bin/awk '{print $1}') +request_sha=$(/usr/bin/shasum -a 256 "$HOME/stage-request.json" | /usr/bin/awk '{print $1}') +resolved_sha=$(/usr/bin/shasum -a 256 "$HOME/resolved.json" | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --slurpfile stage_request "$HOME/stage-request.json" \ + --slurpfile resolved "$HOME/resolved.json" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg payload_sha "$receipt_sha" \ + '{case_id:$case_id,phase:"forge",stage_request:$stage_request[0],request_sha:$request_sha, + resolved_profile:$resolved[0],resolved_sha:$resolved_sha,output_id:"candidate.repository", + media_type:"application/json",payload_sha:$payload_sha}' > "$HOME/result-context.json" +"$jq_bin" -L "${contract%/*}" -L "$modules" -S -c --arg command stage-result \ + -f "$contract" "$HOME/result-context.json" > "$HOME/stage-result.json" +"$jq_bin" -S -c -n --slurpfile result "$HOME/stage-result.json" --rawfile receipt "$HOME/receipt.json" \ + --arg case_id "$("$jq_bin" -r '.case_id' "$request")" --arg receipt_sha "$receipt_sha" ' + {case_id:$case_id,payloads:[{data:$receipt,media_type:"application/json", + payload_id:"candidate.repository",sha256:$receipt_sha}],phase:"forge",protocol_version:1, + stage_result:$result[0]}' diff --git a/adapter-tests/v1/fakes/forge-b.sh b/adapter-tests/v1/fakes/forge-b.sh old mode 100755 new mode 100644 index f46ba3b..2196a94 --- a/adapter-tests/v1/fakes/forge-b.sh +++ b/adapter-tests/v1/fakes/forge-b.sh @@ -1,34 +1,46 @@ #!/usr/bin/env bash # shellcheck disable=SC2016 set -euo pipefail -[ "$#" -eq 2 ] || exit 64 +[ "$#" -eq 4 ] || exit 64 [ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 -request=$1 -jq_bin=$2 -"$jq_bin" -e ' - type == "object" and - (keys | sort) == ["artifact","candidate_root","case_id","phase","protocol"] and - .protocol == "ystack.fake-adapter.v1" and .phase == "forge" and - (.case_id | test("\\Amatrix-(aa|ab|ba|bb)\\z")) and - (.candidate_root | type == "string" and startswith("/")) and - (.artifact | type == "object" and - (keys | sort) == ["content","sha256"] and - (.content | type == "string") and - (.sha256 | test("\\A[0-9a-f]{64}\\z"))) -' "$request" >/dev/null -candidate_root=$("$jq_bin" -r '.candidate_root' "$request") -content=$("$jq_bin" -r '.artifact.content' "$request") -digest=$(/usr/bin/printf '%s' "$content" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') -[ "$digest" = "$("$jq_bin" -r '.artifact.sha256' "$request")" ] -/bin/mkdir -m 700 "$candidate_root" -/usr/bin/git init -q "$candidate_root" -/usr/bin/printf '%s' "$content" > "$candidate_root/result.pending" -/bin/mv "$candidate_root/result.pending" "$candidate_root/result.txt" -/usr/bin/git -C "$candidate_root" add result.txt -/usr/bin/git -C "$candidate_root" commit -q -m candidate -commit_id=$(/usr/bin/git -C "$candidate_root" rev-parse HEAD) -tree_id=$(/usr/bin/git -C "$candidate_root" rev-parse 'HEAD^{tree}') -object_id=$(/usr/bin/git -C "$candidate_root" rev-parse HEAD:result.txt) -"$jq_bin" -S -c -n --arg commit "$commit_id" --arg tree "$tree_id" --arg object "$object_id" ' - {commit_id:$commit,file_object_id:$object,package_id:"fake.forge.b",phase:"forge", - protocol:"ystack.fake-adapter.v1",status:"ok",tree_id:$tree}' +request=$1 jq_bin=$2 contract=$3 modules=$4 +"$jq_bin" -L "${contract%/*}" -L "$modules" -e --arg command request-envelope \ + -f "$contract" "$request" >/dev/null +"$jq_bin" -S -c '.stage_request' "$request" > "$HOME/stage-request.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="resolved-profile") | .data' \ + "$request" > "$HOME/resolved.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="source") | .data' "$request" > "$HOME/source.txt" +"$jq_bin" -j '.payloads[] | select(.payload_id=="producer.patch") | .data' "$request" > "$HOME/producer.patch" +patch_sha=$(/usr/bin/shasum -a 256 "$HOME/producer.patch" | /usr/bin/awk '{print $1}') +candidate="$HOME/candidate" +/bin/mkdir -m 700 "$candidate" +/usr/bin/git init -q "$candidate" +/bin/cp "$HOME/source.txt" "$candidate/source.txt" +/usr/bin/git -C "$candidate" add source.txt +/usr/bin/git -C "$candidate" commit -q -m source +/usr/bin/git -C "$candidate" apply --index "$HOME/producer.patch" +/usr/bin/git -C "$candidate" commit -q -m candidate +commit_id=$(/usr/bin/git -C "$candidate" rev-parse HEAD) +tree_id=$(/usr/bin/git -C "$candidate" rev-parse 'HEAD^{tree}') +object_id=$(/usr/bin/git -C "$candidate" rev-parse HEAD:source.txt) +source_tree=$("$jq_bin" -r '.stage_request.body.inputs[] | select(.input_id=="input.source-tree") | .value.value.value.object_id' "$request") +"$jq_bin" -S -c -n --arg source_tree "$source_tree" --arg patch "$patch_sha" \ + --arg commit "$commit_id" --arg tree "$tree_id" --arg object "$object_id" ' + {candidate_commit_id:$commit,candidate_object_id:$object,candidate_tree_id:$tree, + patch_sha256:$patch,source_tree_id:$source_tree}' > "$HOME/receipt.json" +receipt_sha=$(/usr/bin/shasum -a 256 "$HOME/receipt.json" | /usr/bin/awk '{print $1}') +request_sha=$(/usr/bin/shasum -a 256 "$HOME/stage-request.json" | /usr/bin/awk '{print $1}') +resolved_sha=$(/usr/bin/shasum -a 256 "$HOME/resolved.json" | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --slurpfile stage_request "$HOME/stage-request.json" \ + --slurpfile resolved "$HOME/resolved.json" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg payload_sha "$receipt_sha" \ + '{case_id:$case_id,phase:"forge",stage_request:$stage_request[0],request_sha:$request_sha, + resolved_profile:$resolved[0],resolved_sha:$resolved_sha,output_id:"candidate.repository", + media_type:"application/json",payload_sha:$payload_sha}' > "$HOME/result-context.json" +"$jq_bin" -L "${contract%/*}" -L "$modules" -S -c --arg command stage-result \ + -f "$contract" "$HOME/result-context.json" > "$HOME/stage-result.json" +"$jq_bin" -S -c -n --slurpfile result "$HOME/stage-result.json" --rawfile receipt "$HOME/receipt.json" \ + --arg case_id "$("$jq_bin" -r '.case_id' "$request")" --arg receipt_sha "$receipt_sha" ' + {case_id:$case_id,payloads:[{data:$receipt,media_type:"application/json", + payload_id:"candidate.repository",sha256:$receipt_sha}],phase:"forge",protocol_version:1, + stage_result:$result[0]}' diff --git a/adapter-tests/v1/fakes/producer-a.sh b/adapter-tests/v1/fakes/producer-a.sh old mode 100755 new mode 100644 index b152b61..1c9a55f --- a/adapter-tests/v1/fakes/producer-a.sh +++ b/adapter-tests/v1/fakes/producer-a.sh @@ -1,19 +1,36 @@ #!/usr/bin/env bash # shellcheck disable=SC2016 set -euo pipefail -[ "$#" -eq 2 ] || exit 64 +[ "$#" -eq 4 ] || exit 64 [ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 -request=$1 -jq_bin=$2 -"$jq_bin" -e ' - type == "object" and - (keys | sort) == ["case_id","phase","protocol","source_sha256"] and - .protocol == "ystack.fake-adapter.v1" and .phase == "producer" and - (.case_id | test("\\Amatrix-(aa|ab|ba|bb)\\z")) and - (.source_sha256 | test("\\A[0-9a-f]{64}\\z")) -' "$request" >/dev/null -content='portable candidate' -digest=$(/usr/bin/printf '%s' "$content" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') -"$jq_bin" -S -c -n --arg content "$content" --arg digest "$digest" ' - {artifact:{content:$content,sha256:$digest},package_id:"fake.producer.a", - phase:"producer",protocol:"ystack.fake-adapter.v1",status:"ok"}' +request=$1 jq_bin=$2 contract=$3 modules=$4 +"$jq_bin" -L "${contract%/*}" -L "$modules" -e --arg command request-envelope \ + -f "$contract" "$request" >/dev/null +"$jq_bin" -S -c '.stage_request' "$request" > "$HOME/stage-request.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="resolved-profile") | .data' \ + "$request" > "$HOME/resolved.json" +/usr/bin/printf '%s\n' \ + 'diff --git a/source.txt b/source.txt' \ + '--- a/source.txt' \ + '+++ b/source.txt' \ + '@@ -1,2 +1,3 @@' \ + ' project = moon-garden' \ + ' feature = portable greeting' \ + '+result = portable candidate' > "$HOME/producer.patch" +patch_sha=$(/usr/bin/shasum -a 256 "$HOME/producer.patch" | /usr/bin/awk '{print $1}') +request_sha=$(/usr/bin/shasum -a 256 "$HOME/stage-request.json" | /usr/bin/awk '{print $1}') +resolved_sha=$(/usr/bin/shasum -a 256 "$HOME/resolved.json" | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --slurpfile stage_request "$HOME/stage-request.json" \ + --slurpfile resolved "$HOME/resolved.json" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg payload_sha "$patch_sha" \ + '{case_id:$case_id,phase:"producer",stage_request:$stage_request[0],request_sha:$request_sha, + resolved_profile:$resolved[0],resolved_sha:$resolved_sha,output_id:"producer.patch", + media_type:"text/x-diff",payload_sha:$payload_sha}' > "$HOME/result-context.json" +"$jq_bin" -L "${contract%/*}" -L "$modules" -S -c --arg command stage-result \ + -f "$contract" "$HOME/result-context.json" > "$HOME/stage-result.json" +"$jq_bin" -S -c -n --slurpfile result "$HOME/stage-result.json" \ + --rawfile patch "$HOME/producer.patch" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg patch_sha "$patch_sha" ' + {case_id:$case_id,payloads:[{data:$patch,media_type:"text/x-diff", + payload_id:"producer.patch",sha256:$patch_sha}],phase:"producer",protocol_version:1, + stage_result:$result[0]}' diff --git a/adapter-tests/v1/fakes/producer-b.sh b/adapter-tests/v1/fakes/producer-b.sh old mode 100755 new mode 100644 index c139e29..d44b898 --- a/adapter-tests/v1/fakes/producer-b.sh +++ b/adapter-tests/v1/fakes/producer-b.sh @@ -1,19 +1,30 @@ #!/usr/bin/env bash # shellcheck disable=SC2016 set -euo pipefail -[ "$#" -eq 2 ] || exit 64 +[ "$#" -eq 4 ] || exit 64 [ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 -request=$1 -jq_bin=$2 -"$jq_bin" -e ' - type == "object" and - (keys | sort) == ["case_id","phase","protocol","source_sha256"] and - .protocol == "ystack.fake-adapter.v1" and .phase == "producer" and - (.case_id | test("\\Amatrix-(aa|ab|ba|bb)\\z")) and - (.source_sha256 | test("\\A[0-9a-f]{64}\\z")) -' "$request" >/dev/null -content=$(/usr/bin/printf '%s%s' portable ' candidate') -digest=$(/usr/bin/printf '%s' "$content" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') -"$jq_bin" -S -c -n --arg content "$content" --arg digest "$digest" ' - {artifact:{content:$content,sha256:$digest},package_id:"fake.producer.b", - phase:"producer",protocol:"ystack.fake-adapter.v1",status:"ok"}' +request=$1 jq_bin=$2 contract=$3 modules=$4 +"$jq_bin" -L "${contract%/*}" -L "$modules" -e --arg command request-envelope \ + -f "$contract" "$request" >/dev/null +"$jq_bin" -S -c '.stage_request' "$request" > "$HOME/stage-request.json" +"$jq_bin" -j '.payloads[] | select(.payload_id=="resolved-profile") | .data' \ + "$request" > "$HOME/resolved.json" +patch_text=$'diff --git a/source.txt b/source.txt\n--- a/source.txt\n+++ b/source.txt\n@@ -1,2 +1,3 @@\n project = moon-garden\n feature = portable greeting\n+result = portable candidate' +/usr/bin/printf '%s\n' "$patch_text" > "$HOME/producer.patch" +patch_sha=$(/usr/bin/shasum -a 256 "$HOME/producer.patch" | /usr/bin/awk '{print $1}') +request_sha=$(/usr/bin/shasum -a 256 "$HOME/stage-request.json" | /usr/bin/awk '{print $1}') +resolved_sha=$(/usr/bin/shasum -a 256 "$HOME/resolved.json" | /usr/bin/awk '{print $1}') +"$jq_bin" -S -c -n --slurpfile stage_request "$HOME/stage-request.json" \ + --slurpfile resolved "$HOME/resolved.json" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" --arg payload_sha "$patch_sha" \ + '{case_id:$case_id,phase:"producer",stage_request:$stage_request[0],request_sha:$request_sha, + resolved_profile:$resolved[0],resolved_sha:$resolved_sha,output_id:"producer.patch", + media_type:"text/x-diff",payload_sha:$payload_sha}' > "$HOME/result-context.json" +"$jq_bin" -L "${contract%/*}" -L "$modules" -S -c --arg command stage-result \ + -f "$contract" "$HOME/result-context.json" > "$HOME/stage-result.json" +"$jq_bin" -S -c -n --slurpfile result "$HOME/stage-result.json" \ + --rawfile patch "$HOME/producer.patch" --arg case_id "$("$jq_bin" -r '.case_id' "$request")" \ + --arg patch_sha "$patch_sha" ' + {case_id:$case_id,payloads:[{data:$patch,media_type:"text/x-diff", + payload_id:"producer.patch",sha256:$patch_sha}],phase:"producer",protocol_version:1, + stage_result:$result[0]}' diff --git a/adapter-tests/v1/fakes/protocol-fault.sh b/adapter-tests/v1/fakes/protocol-fault.sh index 109f460..dae417e 100755 --- a/adapter-tests/v1/fakes/protocol-fault.sh +++ b/adapter-tests/v1/fakes/protocol-fault.sh @@ -1,18 +1,18 @@ #!/usr/bin/env bash # shellcheck disable=SC2016 set -euo pipefail -[ "$#" -eq 3 ] || exit 64 +[ "$#" -eq 5 ] || exit 64 [ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 mode=$1 jq_bin=$3 case "$mode" in empty) ;; malformed) /usr/bin/printf '{\n' ;; - partial) /usr/bin/printf '%s\n' '{"package_id":"fake.protocol-fault","phase":"producer","protocol":"ystack.fake-adapter.v1","status":"ok"}' ;; - degraded) /usr/bin/printf '%s\n' '{"package_id":"fake.protocol-fault","phase":"producer","protocol":"ystack.fake-adapter.v1","status":"degraded"}' ;; + partial) /usr/bin/printf '%s\n' '{"case_id":"reject-partial","payloads":[],"phase":"producer","protocol_version":1}' ;; + degraded) /usr/bin/printf '%s\n' '{"status":"degraded"}' ;; relabelled) - "$jq_bin" -S -c -n '{artifact:{content:"portable candidate",sha256:("0"*64)}, - package_id:"fake.producer.a",phase:"forge",protocol:"ystack.fake-adapter.v1",status:"ok"}' + "$jq_bin" -S -c -n '{case_id:"matrix-aa",payloads:[],phase:"forge", + protocol_version:1,stage_result:{}}' ;; timeout) /bin/sleep 5 ;; transport) exit 9 ;; diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index 2220cb6..22d7691 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -18,6 +18,7 @@ runner_dir=${runner_source%/*} repo_root=$(CDPATH='' cd -P -- "$runner_dir/../.." && pwd -P) || runner_error contract="$runner_dir/contract.jq" core="$repo_root/scripts/core-contract.sh" +core_fixture_modules="$repo_root/scripts/test" for runner_file in "$runner_source" "$contract" "$inventory" "$core"; do [ -f "$runner_file" ] && [ ! -L "$runner_file" ] || runner_error E_BINDING done @@ -49,10 +50,10 @@ fixture_source="$fixture_root/target/source.txt" fixture_sha=$(sha_file "$fixture_source") [ "$fixture_sha" = ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6 ] || runner_error E_FIXTURE -"$jq_bin" -L "$runner_dir" -e --arg command inventory \ - --arg fixture_sha256 "$fixture_sha" --arg package_id '' \ - --arg artifact_sha256 '' --arg target_tree_id '' --arg candidate_tree_id '' \ - -f "$contract" "$inventory" >/dev/null || +"$jq_bin" -c --slurpfile inventory "$inventory" --arg fixture_sha256 "$fixture_sha" -n \ + '{inventory:$inventory[0],fixture_sha256:$fixture_sha256}' | + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ + --arg command inventory -f "$contract" >/dev/null || runner_error E_INVENTORY git_safe() { @@ -103,11 +104,11 @@ read -r target_mode target_type target_object <<< "$target_meta" package_digest() { case "$1" in - fake.producer.a) printf '%s\n' b141abf12545f39573ee9cd956ba8a80fa3d37c2266929a765b302ab2b832971 ;; - fake.producer.b) printf '%s\n' 329693fbdea876f595d1deeb4a983a3318ac5938add1206472df18c8d5b57211 ;; - fake.forge.a) printf '%s\n' df6891340de99bd82ceb2ff684e1d3b76f87aabf3d401aa15ee7306d932efd1a ;; - fake.forge.b) printf '%s\n' 1da390b4e723a1fb1abe4418c9ef02c5c7d8e886fe38143a61e31e81e89e83ea ;; - fake.protocol-fault) printf '%s\n' 5a5936a315613960ba5c3760cb851c658c49a0ef030aff744a17da72e81ba199 ;; + fake.producer.a) printf '%s\n' 39ee6f968f4b823530e2b36de31faab71c1dc1d675ab9f72c409fd9bcccf3aa4 ;; + fake.producer.b) printf '%s\n' 602650c6cf4b161f5b92b273b6944a42fb84eb5946918e9ec13fed9746a20049 ;; + fake.forge.a) printf '%s\n' 672636d079df76736b3b9160d7a38f67ff83767d7da455458c418c1cb0b539a6 ;; + fake.forge.b) printf '%s\n' a87a8453b2daf7723a9565bb597531f198c7c891d2b47c7dfef80ffd3dc2096b ;; + fake.protocol-fault) printf '%s\n' d0410d55da98008834c5f77bea606111e674edefc2df33fd7b08e0189930b353 ;; *) return 1 ;; esac } @@ -154,7 +155,7 @@ run_child() { /bin/mkdir -m 700 "$child_home" ( ulimit -t 2 -f 2048 -n 64 - child_args=("$request" "$jq_bin") + child_args=("$request" "$jq_bin" "$contract" "$core_fixture_modules") [ "$mode" = direct ] || child_args=("$mode" "${child_args[@]}") exec /usr/bin/env -i HOME="$child_home" TMPDIR="$child_home" PATH=/usr/bin:/bin \ LC_ALL=C GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \ @@ -186,6 +187,44 @@ run_child() { } } +extract_payload() { + local response=$1 + local payload_id=$2 + local output=$3 + [ "$("$jq_bin" -r --arg id "$payload_id" '[.payloads[] | select(.payload_id==$id)] | length' "$response")" -eq 1 ] || + return 1 + "$jq_bin" -j --arg id "$payload_id" '.payloads[] | select(.payload_id==$id) | .data' \ + "$response" > "$output" && + [ "$(sha_file "$output")" = \ + "$("$jq_bin" -r --arg id "$payload_id" '.payloads[] | select(.payload_id==$id) | .sha256' "$response")" ] +} + +stage_consistent() { + local request=$1 + local resolved=$2 + local result=$3 + "$jq_bin" -e --slurpfile request "$request" --slurpfile resolved "$resolved" ' + .body.reported_by == .body.execution.performer and + .body.execution.actual_binding.binding_id == $request[0].body.operation.binding_id and + .body.execution.actual_binding.role == $request[0].body.operation.role and + (.body.execution.actual_binding as $actual | + [$resolved[0].body.bindings[] | + select(.binding.binding_id==$actual.binding_id and + .binding.package_ref==$actual.package_ref and + .binding.principal_id==$actual.principal_id)] | length == 1) + ' "$result" >/dev/null +} + +git_write() { + /usr/bin/env -i HOME="$run_tmp/home" TMPDIR="$run_tmp/home" PATH=/usr/bin:/bin LC_ALL=C \ + GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_NO_REPLACE_OBJECTS=1 \ + GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + GIT_AUTHOR_NAME=fake GIT_AUTHOR_EMAIL=fake@example.invalid \ + GIT_COMMITTER_NAME=fake GIT_COMMITTER_EMAIL=fake@example.invalid \ + GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /usr/bin/git --no-replace-objects "$@" +} + cells="$run_tmp/cells.ndjson" negatives="$run_tmp/negatives.ndjson" : > "$cells" @@ -233,55 +272,140 @@ for cell in aa ab ba bb; do ([.body.bindings[] | select(.binding.role=="forge") | .adapter_implementation.version] == [$forge]) ' "$resolved" >/dev/null || runner_error E_PROVENANCE + resolved_sha=$(sha_file "$resolved") + resolved_id=$("$jq_bin" -r '.id' "$resolved") + producer_context="$run_tmp/$cell.producer.context" + "$jq_bin" -S -c -n --slurpfile resolved_profile "$resolved" \ + --arg case_id "$cell_id" --arg resolved_sha "$resolved_sha" \ + --arg resolved_id "$resolved_id" \ + --arg target_commit "$target_commit" --arg target_tree "$target_tree" \ + --arg target_object "$target_object" \ + '{case_id:$case_id,resolved_sha:$resolved_sha,resolved_id:$resolved_id, + resolved_profile:$resolved_profile[0],target_commit:$target_commit, + target_tree:$target_tree,target_object:$target_object}' > "$producer_context" + producer_stage_request="$run_tmp/$cell.producer.stage-request" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -S -c \ + --arg command producer-request -f "$contract" "$producer_context" > "$producer_stage_request" + "$core" validate-document "$producer_stage_request" >/dev/null 2>&1 || runner_error E_CORE_PRODUCER_REQUEST producer_request="$run_tmp/$cell.producer.request" - "$jq_bin" -S -c -n --arg case_id "$cell_id" --arg source "$fixture_sha" \ - '{case_id:$case_id,phase:"producer",protocol:"ystack.fake-adapter.v1",source_sha256:$source}' > "$producer_request" + "$jq_bin" -S -c -n --arg case_id "$cell_id" --slurpfile stage "$producer_stage_request" \ + --rawfile source "$fixture_source" --arg source_sha "$fixture_sha" \ + --rawfile resolved "$resolved" --arg resolved_sha "$resolved_sha" ' + {case_id:$case_id,payloads:[ + {data:$resolved,media_type:"application/json",payload_id:"resolved-profile",sha256:$resolved_sha}, + {data:$source,media_type:"text/plain",payload_id:"source",sha256:$source_sha}], + phase:"producer",protocol_version:1,stage_request:$stage[0]}' > "$producer_request" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ + --arg command request-envelope -f "$contract" "$producer_request" >/dev/null || runner_error E_PROTOCOL_PRODUCER_REQUEST producer_output="$run_tmp/$cell.producer.output" producer_error="$run_tmp/$cell.producer.stderr" run_child "$assets/$producer_path" direct "$producer_request" "$producer_output" \ "$producer_error" "$run_tmp/$cell.producer.home" || runner_error "$CHILD_ERROR" if [ -s "$producer_error" ] || ! canonical "$producer_output" "$run_tmp/$cell.producer.canonical"; then - runner_error E_PROTOCOL + runner_error E_PROTOCOL_PRODUCER_OUTPUT fi - "$jq_bin" -L "$runner_dir" -e --arg command producer-response \ - --arg package_id "$producer_package" --arg fixture_sha256 '' \ - --arg artifact_sha256 '' --arg target_tree_id '' --arg candidate_tree_id '' \ - -f "$contract" "$producer_output" >/dev/null || runner_error E_PROTOCOL - artifact_content=$("$jq_bin" -r '.artifact.content' "$producer_output") - artifact_sha=$(sha_text "$artifact_content") - [ "$artifact_sha" = "$("$jq_bin" -r '.artifact.sha256' "$producer_output")" ] || runner_error E_OBSERVATION + producer_response_context="$run_tmp/$cell.producer.response-context" + "$jq_bin" -S -c -n --slurpfile response "$producer_output" --arg case_id "$cell_id" \ + '{case_id:$case_id,phase:"producer",response:$response[0]}' > "$producer_response_context" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ + --arg command response-envelope -f "$contract" "$producer_response_context" >/dev/null || runner_error E_PROTOCOL_PRODUCER_RESPONSE + producer_result="$run_tmp/$cell.producer.stage-result" + "$jq_bin" -S -c '.stage_result' "$producer_output" > "$producer_result" + "$core" validate-stage-run "$producer_stage_request" "$resolved" "$producer_result" \ + >/dev/null 2>&1 || runner_error E_CORE_PRODUCER_RESULT + stage_consistent "$producer_stage_request" "$resolved" "$producer_result" || runner_error E_PROVENANCE + producer_patch="$run_tmp/$cell.producer.patch" + extract_payload "$producer_output" producer.patch "$producer_patch" || runner_error E_OBSERVATION + patch_sha=$(sha_file "$producer_patch") + "$jq_bin" -e --arg sha "$patch_sha" ' + .body.outputs == [{output_id:"producer.patch",ref:.body.delta_ref}] and + .body.delta_ref.media_type == "text/x-diff" and .body.delta_ref.sha256 == $sha + ' "$producer_result" >/dev/null || runner_error E_OBSERVATION - candidate_root="$run_tmp/$cell.candidate" + forge_context="$run_tmp/$cell.forge.context" + "$jq_bin" -S -c -n --slurpfile resolved_profile "$resolved" \ + --arg case_id "$cell_id" --arg resolved_sha "$resolved_sha" \ + --arg resolved_id "$resolved_id" \ + --arg target_commit "$target_commit" --arg target_tree "$target_tree" \ + --arg target_object "$target_object" --arg patch_sha "$patch_sha" \ + '{case_id:$case_id,resolved_sha:$resolved_sha,resolved_id:$resolved_id, + resolved_profile:$resolved_profile[0],target_commit:$target_commit, + target_tree:$target_tree,target_object:$target_object,patch_sha:$patch_sha}' > "$forge_context" + forge_stage_request="$run_tmp/$cell.forge.stage-request" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -S -c \ + --arg command forge-request -f "$contract" "$forge_context" > "$forge_stage_request" + "$core" validate-document "$forge_stage_request" >/dev/null 2>&1 || runner_error E_CORE_FORGE_REQUEST forge_request="$run_tmp/$cell.forge.request" - "$jq_bin" -S -c -n --arg case_id "$cell_id" --arg root "$candidate_root" \ - --slurpfile response "$producer_output" \ - '{artifact:$response[0].artifact,candidate_root:$root,case_id:$case_id, - phase:"forge",protocol:"ystack.fake-adapter.v1"}' > "$forge_request" + "$jq_bin" -S -c -n --arg case_id "$cell_id" --slurpfile stage "$forge_stage_request" \ + --rawfile source "$fixture_source" --arg source_sha "$fixture_sha" \ + --rawfile resolved "$resolved" --arg resolved_sha "$resolved_sha" \ + --rawfile patch "$producer_patch" --arg patch_sha "$patch_sha" ' + {case_id:$case_id,payloads:[ + {data:$resolved,media_type:"application/json",payload_id:"resolved-profile",sha256:$resolved_sha}, + {data:$source,media_type:"text/plain",payload_id:"source",sha256:$source_sha}, + {data:$patch,media_type:"text/x-diff",payload_id:"producer.patch",sha256:$patch_sha}], + phase:"forge",protocol_version:1,stage_request:$stage[0]}' > "$forge_request" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ + --arg command request-envelope -f "$contract" "$forge_request" >/dev/null || runner_error E_PROTOCOL_FORGE_REQUEST forge_output="$run_tmp/$cell.forge.output" forge_error="$run_tmp/$cell.forge.stderr" + forge_home="$run_tmp/$cell.forge.home" run_child "$assets/$forge_path" direct "$forge_request" "$forge_output" \ - "$forge_error" "$run_tmp/$cell.forge.home" || runner_error "$CHILD_ERROR" + "$forge_error" "$forge_home" || runner_error "$CHILD_ERROR" if [ -s "$forge_error" ] || ! canonical "$forge_output" "$run_tmp/$cell.forge.canonical"; then - runner_error E_PROTOCOL + runner_error E_PROTOCOL_FORGE_OUTPUT fi - "$jq_bin" -L "$runner_dir" -e --arg command forge-response \ - --arg package_id "$forge_package" --arg fixture_sha256 '' \ - --arg artifact_sha256 '' --arg target_tree_id '' --arg candidate_tree_id '' \ - -f "$contract" "$forge_output" >/dev/null || runner_error E_PROTOCOL + forge_response_context="$run_tmp/$cell.forge.response-context" + "$jq_bin" -S -c -n --slurpfile response "$forge_output" --arg case_id "$cell_id" \ + '{case_id:$case_id,phase:"forge",response:$response[0]}' > "$forge_response_context" + "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ + --arg command response-envelope -f "$contract" "$forge_response_context" >/dev/null || runner_error E_PROTOCOL_FORGE_RESPONSE + forge_result="$run_tmp/$cell.forge.stage-result" + "$jq_bin" -S -c '.stage_result' "$forge_output" > "$forge_result" + "$core" validate-stage-run "$forge_stage_request" "$resolved" "$forge_result" \ + >/dev/null 2>&1 || runner_error E_CORE_FORGE_RESULT + stage_consistent "$forge_stage_request" "$resolved" "$forge_result" || runner_error E_PROVENANCE + receipt="$run_tmp/$cell.receipt" + extract_payload "$forge_output" candidate.repository "$receipt" || runner_error E_OBSERVATION + receipt_sha=$(sha_file "$receipt") + "$jq_bin" -e --arg sha "$receipt_sha" ' + (.body | has("delta_ref") | not) and .body.outputs == [{ + output_id:"candidate.repository",ref:.body.outputs[0].ref}] and + .body.outputs[0].ref.media_type == "application/json" and + .body.outputs[0].ref.sha256 == $sha + ' "$forge_result" >/dev/null || runner_error E_OBSERVATION + candidate_root="$forge_home/candidate" verify_repo "$candidate_root" || runner_error E_GIT candidate_commit=$(git_safe -C "$candidate_root" rev-parse HEAD) candidate_tree=$(git_safe -C "$candidate_root" rev-parse 'HEAD^{tree}') - candidate_object=$(git_safe -C "$candidate_root" rev-parse HEAD:result.txt) - [ "$(/bin/cat "$candidate_root/result.txt")" = "$artifact_content" ] && - [ "$candidate_commit:$candidate_tree:$candidate_object" = \ - "$("$jq_bin" -r '[.commit_id,.tree_id,.file_object_id]|join(":")' "$forge_output")" ] || - runner_error E_OBSERVATION - projection=$("$jq_bin" -L "$runner_dir" -n -c --arg command projection \ - --arg artifact_sha256 "$artifact_sha" --arg target_tree_id "$target_tree" \ - --arg candidate_tree_id "$candidate_tree" --arg package_id '' \ - --arg fixture_sha256 '' -f "$contract") + candidate_object=$(git_safe -C "$candidate_root" rev-parse HEAD:source.txt) + "$jq_bin" -e --arg source "$target_tree" --arg patch "$patch_sha" \ + --arg commit "$candidate_commit" --arg tree "$candidate_tree" --arg object "$candidate_object" ' + . == {candidate_commit_id:$commit,candidate_object_id:$object,candidate_tree_id:$tree, + patch_sha256:$patch,source_tree_id:$source} + ' "$receipt" >/dev/null || runner_error E_OBSERVATION + oracle="$run_tmp/$cell.oracle" + /bin/mkdir -m 700 "$oracle" + git_write init -q "$oracle" + /bin/cp "$fixture_source" "$oracle/source.txt" + git_write -C "$oracle" add source.txt + git_write -C "$oracle" commit -q -m source + git_write -C "$oracle" apply "$producer_patch" + git_write -C "$oracle" add source.txt + git_write -C "$oracle" commit -q -m candidate + [ "$candidate_tree" = "$(git_safe -C "$oracle" rev-parse 'HEAD^{tree}')" ] || runner_error E_OBSERVATION + projection_context="$run_tmp/$cell.projection-context" + "$jq_bin" -S -c -n --slurpfile producer_request "$producer_stage_request" \ + --slurpfile producer_result "$producer_result" --slurpfile forge_request "$forge_stage_request" \ + --slurpfile forge_result "$forge_result" --arg artifact_sha256 "$patch_sha" \ + --arg target_tree "$target_tree" --arg candidate_tree "$candidate_tree" ' + {artifact_sha256:$artifact_sha256,producer_request:$producer_request[0], + producer_result:$producer_result[0],forge_request:$forge_request[0], + forge_result:$forge_result[0],target_tree:$target_tree,candidate_tree:$candidate_tree}' > "$projection_context" + projection=$("$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -c \ + --arg command projection -f "$contract" "$projection_context") "$jq_bin" -S -c -n --arg case_id "$cell_id" --argjson projection "$projection" \ --arg profile_sha256 "$(sha_file "$profile")" \ --arg producer "$producer_package" --arg producer_digest "$(package_digest "$producer_package")" \ From 00821f4f74a2c37173b808466e891f2157d2d69d Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 10:28:59 -0400 Subject: [PATCH 03/12] Keep fake adapter entrypoints executable --- adapter-tests/v1/fakes/forge-a.sh | 0 adapter-tests/v1/fakes/forge-b.sh | 0 adapter-tests/v1/fakes/producer-a.sh | 0 adapter-tests/v1/fakes/producer-b.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 adapter-tests/v1/fakes/forge-a.sh mode change 100644 => 100755 adapter-tests/v1/fakes/forge-b.sh mode change 100644 => 100755 adapter-tests/v1/fakes/producer-a.sh mode change 100644 => 100755 adapter-tests/v1/fakes/producer-b.sh diff --git a/adapter-tests/v1/fakes/forge-a.sh b/adapter-tests/v1/fakes/forge-a.sh old mode 100644 new mode 100755 diff --git a/adapter-tests/v1/fakes/forge-b.sh b/adapter-tests/v1/fakes/forge-b.sh old mode 100644 new mode 100755 diff --git a/adapter-tests/v1/fakes/producer-a.sh b/adapter-tests/v1/fakes/producer-a.sh old mode 100644 new mode 100755 diff --git a/adapter-tests/v1/fakes/producer-b.sh b/adapter-tests/v1/fakes/producer-b.sh old mode 100644 new mode 100755 From 188587b8bbc5b8fc4137552906b85139a96c2e5f Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 10:41:44 -0400 Subject: [PATCH 04/12] Bootstrap jq for adapter contract tests --- .../test/portable-adapter-contracts.test.sh | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index 108c8c2..c27b502 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -11,7 +11,15 @@ fixture_builder="$test_root/scripts/test/portable-profile-resolution-fixtures.sh resolver_runtime="$test_root/resolver/v1/profile-resolve-runtime.sh" tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-adapter-contracts.XXXXXX") tmp=$(CDPATH='' cd -P -- "$tmp" && pwd -P) -trap '/bin/rm -rf -- "$tmp"' EXIT +download='' + +cleanup() { + if [ -n "$download" ] && [ -f "$download" ]; then + /bin/rm -f -- "$download" + fi + /bin/rm -rf -- "$tmp" +} +trap cleanup EXIT fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } pass_count=0 @@ -24,12 +32,23 @@ case "$platform" in Linux:x86_64) jq_asset=jq-linux64; compiler=/usr/bin/cc ;; *) fail "unsupported host $platform" ;; esac -jq_cache="${TMPDIR:-/tmp}/ystack-portable-core-jq16/$jq_asset" -[ -f "$jq_cache" ] || fail 'verified jq 1.6 cache missing' case "$jq_asset" in jq-osx-amd64) jq_sha=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef ;; *) jq_sha=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 ;; esac +jq_cache_dir="${TMPDIR:-/tmp}/ystack-portable-core-jq16" +/bin/mkdir -p "$jq_cache_dir" +jq_cache="$jq_cache_dir/$jq_asset" +if [ ! -f "$jq_cache" ] || [ "$(sha_file "$jq_cache")" != "$jq_sha" ]; then + download=$(/usr/bin/mktemp "$jq_cache_dir/.jq-1.6.XXXXXX") + /usr/bin/curl --proto '=https' --tlsv1.2 -fsSL \ + "https://github.com/jqlang/jq/releases/download/jq-1.6/$jq_asset" \ + -o "$download" + [ "$(sha_file "$download")" = "$jq_sha" ] || fail 'jq release digest' + /bin/chmod 0555 "$download" + /bin/mv "$download" "$jq_cache" + download='' +fi [ "$(sha_file "$jq_cache")" = "$jq_sha" ] || fail 'jq digest' bin="$tmp/bin" /bin/mkdir -m 700 "$bin" From 3de11d92b87a2e31b14775b6c20e7238f7391461 Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 11:35:14 -0400 Subject: [PATCH 05/12] Close adapter contract review gaps --- adapter-tests/v1/contract.jq | 32 +++- adapter-tests/v1/fakes/protocol-fault.sh | 23 ++- adapter-tests/v1/inventory.json | 2 +- adapter-tests/v1/runner.sh | 177 ++++++++++++------ .../test/portable-adapter-contracts.test.sh | 39 +++- 5 files changed, 205 insertions(+), 68 deletions(-) diff --git a/adapter-tests/v1/contract.jq b/adapter-tests/v1/contract.jq index ef54f3c..ef16f37 100644 --- a/adapter-tests/v1/contract.jq +++ b/adapter-tests/v1/contract.jq @@ -26,7 +26,10 @@ def expected_cases($fixture): {case_id:"reject-partial",expected_error:"E_PARTIAL"}, {case_id:"reject-relabelled",expected_error:"E_RELABELLED"}, {case_id:"reject-timeout",expected_error:"E_TIMEOUT"}, - {case_id:"reject-transport",expected_error:"E_TRANSPORT"} + {case_id:"reject-transport",expected_error:"E_TRANSPORT"}, + {case_id:"reject-multiple",expected_error:"E_MULTIPLE"}, + {case_id:"reject-unlinked",expected_error:"E_UNLINKED_PAYLOAD"}, + {case_id:"reject-duplicate",expected_error:"E_DUPLICATE_PAYLOAD"} ] | map(if has("phase") then . else . + { phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none", expected_verdict:"reject",equivalence_group:"protocol-negative", @@ -109,7 +112,31 @@ def response_envelope_ok: .response.protocol_version == 1 and .response.case_id == .case_id and .response.phase == .phase and .response.stage_result.schema_version == 2 and .response.stage_result.kind == "stage_result" and - (.response.payloads | type == "array" and all(.[];payload_ok)); + (.response.payloads | type == "array" and all(.[];payload_ok)) and + ([.response.payloads[].payload_id] | length == (unique | length)) and + ([.response.payloads[].payload_id] | sort) == + ([.response.stage_result.body.outputs[].output_id] | sort) and + .response as $response | + all($response.payloads[]; + . as $payload | + [$response.stage_result.body.outputs[] | + select(.output_id==$payload.payload_id and .ref.media_type==$payload.media_type and + .ref.sha256==$payload.sha256)] | length == 1); +def response_error: + if (.response | type) != "object" then "E_PARTIAL" + elif .response.status? == "degraded" then "E_DEGRADED" + elif (.response | exact(["case_id","payloads","phase","protocol_version","stage_result"]) | not) + then "E_PARTIAL" + elif .response.case_id != .case_id or .response.phase != .phase then "E_RELABELLED" + elif (.response.payloads | type) != "array" then "E_PARTIAL" + elif ([.response.payloads[].payload_id] as $ids | + ($ids | length) != ($ids | unique | length)) + then "E_DUPLICATE_PAYLOAD" + elif ([.response.payloads[].payload_id] | sort) != + ([.response.stage_result.body.outputs[]?.output_id] | sort) + then "E_UNLINKED_PAYLOAD" + elif (response_envelope_ok | not) then "E_PARTIAL" + else "" end; def projection: {artifact_sha256:.artifact_sha256,risk:.producer_request.body.risk, gate_refs:.producer_request.body.risk.required_gate_refs, @@ -126,5 +153,6 @@ elif $command == "forge-request" then forge_request(.) elif $command == "stage-result" then stage_result(.;.output_id;.media_type;.payload_sha) elif $command == "request-envelope" then request_envelope_ok elif $command == "response-envelope" then response_envelope_ok +elif $command == "response-error" then response_error elif $command == "projection" then projection else error("unknown-command") end diff --git a/adapter-tests/v1/fakes/protocol-fault.sh b/adapter-tests/v1/fakes/protocol-fault.sh index dae417e..f30d67d 100755 --- a/adapter-tests/v1/fakes/protocol-fault.sh +++ b/adapter-tests/v1/fakes/protocol-fault.sh @@ -4,17 +4,30 @@ set -euo pipefail [ "$#" -eq 5 ] || exit 64 [ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 mode=$1 +request=$2 jq_bin=$3 +contract=$4 +modules=$5 case "$mode" in empty) ;; malformed) /usr/bin/printf '{\n' ;; - partial) /usr/bin/printf '%s\n' '{"case_id":"reject-partial","payloads":[],"phase":"producer","protocol_version":1}' ;; + partial) /usr/bin/printf '%s\n' \ + '{"case_id":"matrix-aa","payloads":[],"phase":"producer","protocol_version":1}' ;; degraded) /usr/bin/printf '%s\n' '{"status":"degraded"}' ;; - relabelled) - "$jq_bin" -S -c -n '{case_id:"matrix-aa",payloads:[],phase:"forge", - protocol_version:1,stage_result:{}}' - ;; timeout) /bin/sleep 5 ;; transport) exit 9 ;; + relabelled|multiple|unlinked|duplicate) + /bin/bash "${BASH_SOURCE[0]%/*}/producer-a.sh" "$request" "$jq_bin" \ + "$contract" "$modules" > "$HOME/base-response.json" + case "$mode" in + relabelled) "$jq_bin" -S -c '.phase="forge"' "$HOME/base-response.json" ;; + multiple) /bin/cat "$HOME/base-response.json" "$HOME/base-response.json" ;; + unlinked) + "$jq_bin" -S -c '.payloads += [{data:"extra",media_type:"text/plain", + payload_id:"unlinked",sha256:("0"*64)}]' "$HOME/base-response.json" + ;; + duplicate) "$jq_bin" -S -c '.payloads += [.payloads[0]]' "$HOME/base-response.json" ;; + esac + ;; *) exit 64 ;; esac diff --git a/adapter-tests/v1/inventory.json b/adapter-tests/v1/inventory.json index cf49e29..f8ff3a8 100644 --- a/adapter-tests/v1/inventory.json +++ b/adapter-tests/v1/inventory.json @@ -1 +1 @@ -{"authorization_ref":{"comment_id":5476938197,"issue_number":153,"scope_comment_id":5474023028},"cases":[{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-aa","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ab","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ba","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-bb","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["expected-error"],"case_id":"reject-degraded","equivalence_group":"protocol-negative","expected_error":"E_DEGRADED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-empty","equivalence_group":"protocol-negative","expected_error":"E_EMPTY","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-malformed","equivalence_group":"protocol-negative","expected_error":"E_MALFORMED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-partial","equivalence_group":"protocol-negative","expected_error":"E_PARTIAL","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-relabelled","equivalence_group":"protocol-negative","expected_error":"E_RELABELLED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-timeout","equivalence_group":"protocol-negative","expected_error":"E_TIMEOUT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-transport","equivalence_group":"protocol-negative","expected_error":"E_TRANSPORT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"}],"protocol":"ystack.adapter-contract.inventory.v1","schema_version":1} +{"authorization_ref":{"comment_id":5476938197,"issue_number":153,"scope_comment_id":5474023028},"cases":[{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-aa","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ab","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ba","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-bb","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["expected-error"],"case_id":"reject-degraded","equivalence_group":"protocol-negative","expected_error":"E_DEGRADED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-empty","equivalence_group":"protocol-negative","expected_error":"E_EMPTY","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-malformed","equivalence_group":"protocol-negative","expected_error":"E_MALFORMED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-partial","equivalence_group":"protocol-negative","expected_error":"E_PARTIAL","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-relabelled","equivalence_group":"protocol-negative","expected_error":"E_RELABELLED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-timeout","equivalence_group":"protocol-negative","expected_error":"E_TIMEOUT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-transport","equivalence_group":"protocol-negative","expected_error":"E_TRANSPORT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-multiple","equivalence_group":"protocol-negative","expected_error":"E_MULTIPLE","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-unlinked","equivalence_group":"protocol-negative","expected_error":"E_UNLINKED_PAYLOAD","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-duplicate","equivalence_group":"protocol-negative","expected_error":"E_DUPLICATE_PAYLOAD","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"}],"protocol":"ystack.adapter-contract.inventory.v1","schema_version":1} diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index 22d7691..5ea9b34 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -38,10 +38,12 @@ sha_text() { /usr/bin/printf '%s' "$1" | /usr/bin/shasum -a 256 | /usr/bin/awk ' canonical() { local source=$1 local output=$2 - "$jq_bin" -S -c . "$source" > "$output" 2>/dev/null && cmp -s "$source" "$output" + "$jq_bin" -s -e 'length == 1' "$source" >/dev/null 2>&1 && + "$jq_bin" -S -c . "$source" > "$output" 2>/dev/null && + cmp -s "$source" "$output" } -[ "$(sha_file "$inventory")" = 1be4e488423d753cf083430aa3560b8dda8d66985b234bce9629b5c0fb1c003a ] || +[ "$(sha_file "$inventory")" = 73b90745ba3ae879f8d6d958e3133e33fb6169bc89fb9c26a826be1be9fd9fd6 ] || runner_error E_INVENTORY inventory_canonical="$run_tmp/inventory.canonical" canonical "$inventory" "$inventory_canonical" || runner_error E_INVENTORY @@ -82,13 +84,15 @@ fi "$jq_bin" -e --arg root "$fixture_root" ' . as $map | .version == 1 and (.repositories | length) == 13 and - ([.repositories[].repository_id] | length == (unique | length)) and + ([.repositories[] | [.cell_id,.repository_id]] | length == (unique | length)) and ([.repositories[].root] | length == (unique | length)) and - ([.repositories[] | select(.repository_id == "fixture.target" and .root == ($root+"/target"))] | length) == 1 and + ([.repositories[] | select(.cell_id == "shared" and + .repository_id == "fixture.target" and .root == ($root+"/target"))] | length) == 1 and all(["aa","ab","ba","bb"][]; . as $cell | all(["assets","manifests","profile"][]; . as $repo | - ([$map.repositories[] | select(.repository_id == ($cell+"."+$repo) and + ([$map.repositories[] | select(.cell_id == $cell and + .repository_id == ("repo."+$repo) and .root == ($root+"/cells/"+$cell+"/"+$repo))] | length) == 1)) ' "$mapping" >/dev/null || runner_error E_MAPPING @@ -108,7 +112,7 @@ package_digest() { fake.producer.b) printf '%s\n' 602650c6cf4b161f5b92b273b6944a42fb84eb5946918e9ec13fed9746a20049 ;; fake.forge.a) printf '%s\n' 672636d079df76736b3b9160d7a38f67ff83767d7da455458c418c1cb0b539a6 ;; fake.forge.b) printf '%s\n' a87a8453b2daf7723a9565bb597531f198c7c891d2b47c7dfef80ffd3dc2096b ;; - fake.protocol-fault) printf '%s\n' d0410d55da98008834c5f77bea606111e674edefc2df33fd7b08e0189930b353 ;; + fake.protocol-fault) printf '%s\n' 6daeccbda2b6752415c457967dfad03811837da75d25f38370a400381382de28 ;; *) return 1 ;; esac } @@ -125,19 +129,43 @@ package_path() { } verify_package() { - local assets=$1 + local cell=$1 local package=$2 - local expected_ref=${3:-} - local relative digest entry meta mode type object + local ref=$3 + local relative digest repository_id root algorithm commit entry meta mode type object snapshot relative=$(package_path "$package") || return 1 digest=$(package_digest "$package") || return 1 - [ "$(sha_file "$assets/$relative")" = "$digest" ] || return 1 - entry=$(git_safe -C "$assets" ls-tree HEAD -- "$relative") + "$jq_bin" -e --arg path "$relative" ' + type == "object" and + (keys | sort) == ["location","mode","object_id","object_type","revision"] and + (.revision | type == "object" and + (keys | sort) == ["commit_id","hash_algorithm","repository_id"] and + .repository_id == "repo.assets" and + (.hash_algorithm == "sha1" or .hash_algorithm == "sha256") and + (.commit_id | test("\\A[0-9a-f]{40}([0-9a-f]{24})?\\z"))) and + .location == {kind:"path",value:$path} and .object_type == "blob" and + .mode == "100755" and (.object_id | test("\\A[0-9a-f]{40}([0-9a-f]{24})?\\z")) + ' <<< "$ref" >/dev/null || return 1 + repository_id=$("$jq_bin" -r '.revision.repository_id' <<< "$ref") + root=$("$jq_bin" -r --arg cell "$cell" --arg id "$repository_id" ' + [.repositories[] | select(.cell_id==$cell and .repository_id==$id)] | + if length == 1 then .[0].root else "" end + ' "$mapping") + [ -n "$root" ] && verify_repo "$root" || return 1 + algorithm=$(git_safe -C "$root" rev-parse --show-object-format) + [ "$algorithm" = "$("$jq_bin" -r '.revision.hash_algorithm' <<< "$ref")" ] || return 1 + commit=$("$jq_bin" -r '.revision.commit_id' <<< "$ref") + git_safe -C "$root" cat-file -e "$commit^{commit}" >/dev/null 2>&1 || return 1 + entry=$(git_safe -C "$root" ls-tree "$commit" -- "$relative") meta=${entry%%$'\t'*} read -r mode type object <<< "$meta" - [ "$mode:$type" = 100755:blob ] && - [ "$object" = "$(git_safe -C "$assets" hash-object "$relative")" ] && - { [ -z "$expected_ref" ] || [ "$object" = "$expected_ref" ]; } + [ "$mode" = "$("$jq_bin" -r '.mode' <<< "$ref")" ] && + [ "$type" = "$("$jq_bin" -r '.object_type' <<< "$ref")" ] && + [ "$object" = "$("$jq_bin" -r '.object_id' <<< "$ref")" ] || return 1 + snapshot="$run_tmp/package.$cell.${package##*.}" + git_safe -C "$root" cat-file blob "$object" > "$snapshot" || return 1 + [ -f "$root/$relative" ] && [ ! -L "$root/$relative" ] && + cmp -s "$snapshot" "$root/$relative" && [ "$(sha_file "$snapshot")" = "$digest" ] } CHILD_STATUS=0 @@ -215,6 +243,52 @@ stage_consistent() { ' "$result" >/dev/null } +RESPONSE_ERROR='' +validate_response() { + local response=$1 + local request=$2 + local resolved=$3 + local case_id=$4 + local phase=$5 + local result_output=$6 + local roots response_context observed_error payload_id payload_index=0 + RESPONSE_ERROR='' + [ -s "$response" ] || { RESPONSE_ERROR=E_EMPTY; return 1; } + roots=$("$jq_bin" -s 'length' "$response" 2>/dev/null) || { + RESPONSE_ERROR=E_MALFORMED + return 1 + } + [ "$roots" -eq 1 ] || { RESPONSE_ERROR=E_MULTIPLE; return 1; } + canonical "$response" "$run_tmp/response.canonical" || { + RESPONSE_ERROR=E_MALFORMED + return 1 + } + response_context="$run_tmp/response-context.json" + "$jq_bin" -S -c -n --slurpfile response "$response" --arg case_id "$case_id" \ + --arg phase "$phase" '{case_id:$case_id,phase:$phase,response:$response[0]}' > "$response_context" + observed_error=$("$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -r \ + --arg command response-error -f "$contract" "$response_context") || { + RESPONSE_ERROR=E_PARTIAL + return 1 + } + [ -z "$observed_error" ] || { RESPONSE_ERROR=$observed_error; return 1; } + "$jq_bin" -S -c '.stage_result' "$response" > "$result_output" + "$core" validate-stage-run "$request" "$resolved" "$result_output" \ + >/dev/null 2>&1 || { RESPONSE_ERROR=E_CORE; return 1; } + stage_consistent "$request" "$resolved" "$result_output" || { + RESPONSE_ERROR=E_PROVENANCE + return 1 + } + while IFS= read -r payload_id; do + extract_payload "$response" "$payload_id" \ + "$run_tmp/accepted-payload.$payload_index" || { + RESPONSE_ERROR=E_PAYLOAD_DIGEST + return 1 + } + payload_index=$((payload_index + 1)) + done < <("$jq_bin" -r '.payloads[].payload_id' "$response") +} + git_write() { /usr/bin/env -i HOME="$run_tmp/home" TMPDIR="$run_tmp/home" PATH=/usr/bin:/bin LC_ALL=C \ GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_NO_REPLACE_OBJECTS=1 \ @@ -245,11 +319,23 @@ for cell in aa ab ba bb; do forge_package="fake.forge.$forge_variant" producer_path=$(package_path "$producer_package") forge_path=$(package_path "$forge_package") - producer_ref=$("$jq_bin" -r '.body.bindings[] | select(.role=="producer") | .package_ref.object_id' "$cell_root/profile/profiles/default.json") - forge_ref=$("$jq_bin" -r '.body.bindings[] | select(.role=="forge") | .package_ref.object_id' "$cell_root/profile/profiles/default.json") - if ! verify_package "$assets" "$producer_package" "$producer_ref" || - ! verify_package "$assets" "$forge_package" "$forge_ref" || - ! verify_package "$assets" fake.protocol-fault; then + producer_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="producer") | .package_ref' "$cell_root/profile/profiles/default.json") + forge_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="forge") | .package_ref' "$cell_root/profile/profiles/default.json") + producer_resolved_ref=$("$jq_bin" -c '.body.bindings[] | select(.binding.role=="producer") | .binding.package_ref' "$resolved") + forge_resolved_ref=$("$jq_bin" -c '.body.bindings[] | select(.binding.role=="forge") | .binding.package_ref' "$resolved") + assets_commit=$("$jq_bin" -r '.revision.commit_id' <<< "$forge_ref") + fault_entry=$(git_safe -C "$assets" ls-tree "$assets_commit" -- packages/protocol-fault.sh 2>/dev/null) || + runner_error E_PACKAGE + fault_meta=${fault_entry%%$'\t'*} + read -r fault_mode fault_type fault_object <<< "$fault_meta" + fault_ref=$("$jq_bin" -c --arg path packages/protocol-fault.sh \ + --arg mode "$fault_mode" --arg type "$fault_type" --arg object "$fault_object" \ + '.location={kind:"path",value:$path} | .mode=$mode | .object_type=$type | .object_id=$object' \ + <<< "$forge_ref") + if [ "$producer_ref" != "$producer_resolved_ref" ] || [ "$forge_ref" != "$forge_resolved_ref" ] || + ! verify_package "$cell" "$producer_package" "$producer_ref" || + ! verify_package "$cell" "$forge_package" "$forge_ref" || + ! verify_package "$cell" fake.protocol-fault "$fault_ref"; then runner_error E_PACKAGE fi profile="$profiles/profiles/default.json" @@ -301,20 +387,10 @@ for cell in aa ab ba bb; do producer_error="$run_tmp/$cell.producer.stderr" run_child "$assets/$producer_path" direct "$producer_request" "$producer_output" \ "$producer_error" "$run_tmp/$cell.producer.home" || runner_error "$CHILD_ERROR" - if [ -s "$producer_error" ] || - ! canonical "$producer_output" "$run_tmp/$cell.producer.canonical"; then - runner_error E_PROTOCOL_PRODUCER_OUTPUT - fi - producer_response_context="$run_tmp/$cell.producer.response-context" - "$jq_bin" -S -c -n --slurpfile response "$producer_output" --arg case_id "$cell_id" \ - '{case_id:$case_id,phase:"producer",response:$response[0]}' > "$producer_response_context" - "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ - --arg command response-envelope -f "$contract" "$producer_response_context" >/dev/null || runner_error E_PROTOCOL_PRODUCER_RESPONSE producer_result="$run_tmp/$cell.producer.stage-result" - "$jq_bin" -S -c '.stage_result' "$producer_output" > "$producer_result" - "$core" validate-stage-run "$producer_stage_request" "$resolved" "$producer_result" \ - >/dev/null 2>&1 || runner_error E_CORE_PRODUCER_RESULT - stage_consistent "$producer_stage_request" "$resolved" "$producer_result" || runner_error E_PROVENANCE + [ ! -s "$producer_error" ] || runner_error E_PROTOCOL_PRODUCER_OUTPUT + validate_response "$producer_output" "$producer_stage_request" "$resolved" \ + "$cell_id" producer "$producer_result" || runner_error "$RESPONSE_ERROR" producer_patch="$run_tmp/$cell.producer.patch" extract_payload "$producer_output" producer.patch "$producer_patch" || runner_error E_OBSERVATION patch_sha=$(sha_file "$producer_patch") @@ -353,20 +429,10 @@ for cell in aa ab ba bb; do forge_home="$run_tmp/$cell.forge.home" run_child "$assets/$forge_path" direct "$forge_request" "$forge_output" \ "$forge_error" "$forge_home" || runner_error "$CHILD_ERROR" - if [ -s "$forge_error" ] || - ! canonical "$forge_output" "$run_tmp/$cell.forge.canonical"; then - runner_error E_PROTOCOL_FORGE_OUTPUT - fi - forge_response_context="$run_tmp/$cell.forge.response-context" - "$jq_bin" -S -c -n --slurpfile response "$forge_output" --arg case_id "$cell_id" \ - '{case_id:$case_id,phase:"forge",response:$response[0]}' > "$forge_response_context" - "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ - --arg command response-envelope -f "$contract" "$forge_response_context" >/dev/null || runner_error E_PROTOCOL_FORGE_RESPONSE forge_result="$run_tmp/$cell.forge.stage-result" - "$jq_bin" -S -c '.stage_result' "$forge_output" > "$forge_result" - "$core" validate-stage-run "$forge_stage_request" "$resolved" "$forge_result" \ - >/dev/null 2>&1 || runner_error E_CORE_FORGE_RESULT - stage_consistent "$forge_stage_request" "$resolved" "$forge_result" || runner_error E_PROVENANCE + [ ! -s "$forge_error" ] || runner_error E_PROTOCOL_FORGE_OUTPUT + validate_response "$forge_output" "$forge_stage_request" "$resolved" \ + "$cell_id" forge "$forge_result" || runner_error "$RESPONSE_ERROR" receipt="$run_tmp/$cell.receipt" extract_payload "$forge_output" candidate.repository "$receipt" || runner_error E_OBSERVATION receipt_sha=$(sha_file "$receipt") @@ -420,26 +486,23 @@ for cell in aa ab ba bb; do done fault_executable="$fixture_root/cells/aa/assets/packages/protocol-fault.sh" -for negative in degraded empty malformed partial relabelled timeout transport; do +negative_request="$run_tmp/aa.producer.request" +negative_stage_request="$run_tmp/aa.producer.stage-request" +negative_resolved="$fixture_root/cells/aa/resolved.json" +for negative in degraded empty malformed partial relabelled timeout transport multiple unlinked duplicate; do negative_id="reject-$negative" - negative_request="$run_tmp/$negative.request" - /usr/bin/printf '%s\n' '{}' > "$negative_request" negative_output="$run_tmp/$negative.output" negative_stderr="$run_tmp/$negative.stderr" + negative_result="$run_tmp/$negative.stage-result" observed='' if ! run_child "$fault_executable" "$negative" "$negative_request" "$negative_output" \ "$negative_stderr" "$run_tmp/$negative.home"; then observed=$CHILD_ERROR - elif [ ! -s "$negative_output" ]; then - observed=E_EMPTY - elif ! "$jq_bin" -e . "$negative_output" >/dev/null 2>&1; then - observed=E_MALFORMED - elif [ "$("$jq_bin" -r '.status // ""' "$negative_output")" = degraded ]; then - observed=E_DEGRADED - elif [ "$negative" = relabelled ]; then - observed=E_RELABELLED + elif validate_response "$negative_output" "$negative_stage_request" "$negative_resolved" \ + matrix-aa producer "$negative_result"; then + observed=E_UNEXPECTED_PASS else - observed=E_PARTIAL + observed=$RESPONSE_ERROR fi expected=$("$jq_bin" -r --arg id "$negative_id" '.cases[] | select(.case_id==$id) | .expected_error' "$inventory") [ "$observed" = "$expected" ] || runner_error E_NEGATIVE diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index c27b502..4796a25 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -90,10 +90,10 @@ pass 'four honest profile variants resolved through the inactive resolver' "$jq_bin" -S -c -n --arg root "$fixture" ' {version:1,repositories: - ([{repository_id:"fixture.target",root:($root+"/target")}] + + ([{cell_id:"shared",repository_id:"fixture.target",root:($root+"/target")}] + (["aa","ab","ba","bb"] | map(. as $cell | ["assets","manifests","profile"] | - map(. as $repo | {repository_id:($cell+"."+$repo), + map(. as $repo | {cell_id:$cell,repository_id:("repo."+$repo), root:($root+"/cells/"+$cell+"/"+$repo)})) | add))} ' > "$fixture/repository-map.json" @@ -108,7 +108,7 @@ GH_TOKEN=must-not-leak AWS_SECRET_ACCESS_KEY=must-not-leak SSH_AUTH_SOCK=/must/n [ ! -s "$error" ] || fail 'success diagnostics' "$jq_bin" -e ' .schema_version == 1 and .kind == "adapter_contract_observation" and - (.cells | length) == 4 and (.negative_observations | length) == 7 and + (.cells | length) == 4 and (.negative_observations | length) == 10 and ([.cells[].projection] | unique | length) == 1 and ([.cells[].provenance.profile_sha256] | unique | length) == 4 and (.non_claims | index("external-target-smoke") != null) and @@ -166,6 +166,39 @@ resolved_saved="$tmp/resolved.saved" expect_failure core-ref-mismatch E_CORE /bin/cp "$resolved_saved" "$fixture/cells/aa/resolved.json" +profile_file="$fixture/cells/aa/profile/profiles/default.json" +profile_saved="$tmp/profile.saved" +/bin/cp "$profile_file" "$profile_saved" +resolved_ref_saved="$tmp/resolved-ref.saved" +/bin/cp "$fixture/cells/aa/resolved.json" "$resolved_ref_saved" +original_package_object=$("$jq_bin" -r \ + '.body.bindings[] | select(.role=="producer") | .package_ref.object_id' "$profile_file") +for ref_mutation in repository revision hash path type mode; do + case "$ref_mutation" in + repository) mutate='def mutate: .revision.repository_id="repo.manifests";' ;; + revision) mutate='def mutate: .revision.commit_id=("0"*64);' ;; + hash) mutate='def mutate: .revision.hash_algorithm="sha1";' ;; + path) mutate='def mutate: .location={kind:"path",value:"packages/producer-b.sh"};' ;; + type) mutate='def mutate: .object_type="tree";' ;; + mode) mutate='def mutate: .mode="100644";' ;; + esac + "$jq_bin" -S -c "$mutate (.body.bindings[] | select(.role==\"producer\") | .package_ref) |= mutate" \ + "$profile_saved" > "$profile_file" + "$jq_bin" -S -c "$mutate (.body.bindings[] | select(.binding.role==\"producer\") | .binding.package_ref) |= mutate" \ + "$resolved_ref_saved" > "$fixture/cells/aa/resolved.json" + [ "$("$jq_bin" -r '.body.bindings[] | select(.role=="producer") | .package_ref.object_id' \ + "$profile_file")" = "$original_package_object" ] || fail "ref-$ref_mutation changed object id" + /usr/bin/git -C "$fixture/cells/aa/profile" add profiles/default.json + GIT_AUTHOR_DATE=2000-01-04T00:00:00Z GIT_COMMITTER_DATE=2000-01-04T00:00:00Z \ + /usr/bin/git -C "$fixture/cells/aa/profile" commit -q -m "ref-$ref_mutation" + expect_failure "package-ref-$ref_mutation" E_PACKAGE + /bin/cp "$profile_saved" "$profile_file" + /bin/cp "$resolved_ref_saved" "$fixture/cells/aa/resolved.json" + /usr/bin/git -C "$fixture/cells/aa/profile" add profiles/default.json + GIT_AUTHOR_DATE=2000-01-05T00:00:00Z GIT_COMMITTER_DATE=2000-01-05T00:00:00Z \ + /usr/bin/git -C "$fixture/cells/aa/profile" commit -q -m "restore-$ref_mutation" +done + source_saved="$tmp/source.saved" /bin/cp "$fixture/target/source.txt" "$source_saved" /usr/bin/printf '%s\n' changed > "$fixture/target/source.txt" From d0b370acb3415beba8fb01ceebcd9be6da981e60 Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 12:12:37 -0400 Subject: [PATCH 06/12] Verify resolved sources and target stability --- adapter-tests/v1/runner.sh | 109 +++++++++++++++++- .../test/portable-adapter-contracts.test.sh | 56 +++++++++ 2 files changed, 161 insertions(+), 4 deletions(-) diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index 5ea9b34..e2c87e2 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -105,6 +105,26 @@ read -r target_mode target_type target_object <<< "$target_meta" [ "$target_mode:$target_type:$target_object" = \ "100644:blob:$(git_safe -C "$fixture_root/target" hash-object source.txt)" ] || runner_error E_GIT +target_source_snapshot="$run_tmp/target-source.snapshot" +git_safe -C "$fixture_root/target" cat-file blob "$target_object" > "$target_source_snapshot" || + runner_error E_GIT +cmp -s "$target_source_snapshot" "$fixture_source" || runner_error E_GIT + +verify_target_unchanged() { + local current_entry current_meta current_mode current_type current_object current_snapshot + verify_repo "$fixture_root/target" && + [ "$(git_safe -C "$fixture_root/target" rev-parse HEAD)" = "$target_commit" ] && + [ "$(git_safe -C "$fixture_root/target" rev-parse 'HEAD^{tree}')" = "$target_tree" ] || return 1 + current_entry=$(git_safe -C "$fixture_root/target" ls-tree HEAD -- source.txt) || return 1 + current_meta=${current_entry%%$'\t'*} + read -r current_mode current_type current_object <<< "$current_meta" + [ "$current_mode:$current_type:$current_object" = \ + "$target_mode:$target_type:$target_object" ] || return 1 + [ "$(sha_file "$fixture_source")" = "$fixture_sha" ] || return 1 + current_snapshot="$run_tmp/target-source.late" + git_safe -C "$fixture_root/target" cat-file blob "$current_object" > "$current_snapshot" || return 1 + cmp -s "$current_snapshot" "$target_source_snapshot" && cmp -s "$current_snapshot" "$fixture_source" +} package_digest() { case "$1" in @@ -128,6 +148,15 @@ package_path() { esac } +mapped_root() { + local cell=$1 + local repository_id=$2 + "$jq_bin" -r --arg cell "$cell" --arg id "$repository_id" ' + [.repositories[] | select(.cell_id==$cell and .repository_id==$id)] | + if length == 1 then .[0].root else "" end + ' "$mapping" +} + verify_package() { local cell=$1 local package=$2 @@ -147,10 +176,7 @@ verify_package() { .mode == "100755" and (.object_id | test("\\A[0-9a-f]{40}([0-9a-f]{24})?\\z")) ' <<< "$ref" >/dev/null || return 1 repository_id=$("$jq_bin" -r '.revision.repository_id' <<< "$ref") - root=$("$jq_bin" -r --arg cell "$cell" --arg id "$repository_id" ' - [.repositories[] | select(.cell_id==$cell and .repository_id==$id)] | - if length == 1 then .[0].root else "" end - ' "$mapping") + root=$(mapped_root "$cell" "$repository_id") [ -n "$root" ] && verify_repo "$root" || return 1 algorithm=$(git_safe -C "$root" rev-parse --show-object-format) [ "$algorithm" = "$("$jq_bin" -r '.revision.hash_algorithm' <<< "$ref")" ] || return 1 @@ -168,6 +194,79 @@ verify_package() { cmp -s "$snapshot" "$root/$relative" && [ "$(sha_file "$snapshot")" = "$digest" ] } +verify_source_claim() { + local cell=$1 + local claim=$2 + local index=$3 + local ref repository_id root algorithm commit location path entry meta mode type object actual_path + local snapshot format expected_sha + "$jq_bin" -e ' + type == "object" and (keys | sort) == ["source","value_format","value_sha256"] and + (.value_format == "canonical-json" or .value_format == "raw-bytes") and + (.value_sha256 | test("\\A[0-9a-f]{64}\\z")) and + (.source | type == "object" and + (keys | sort) == ["location","mode","object_id","object_type","revision"]) + ' <<< "$claim" >/dev/null || return 1 + ref=$("$jq_bin" -c '.source' <<< "$claim") + repository_id=$("$jq_bin" -r '.revision.repository_id' <<< "$ref") + root=$(mapped_root "$cell" "$repository_id") + [ -n "$root" ] && verify_repo "$root" || return 1 + algorithm=$(git_safe -C "$root" rev-parse --show-object-format) + [ "$algorithm" = "$("$jq_bin" -r '.revision.hash_algorithm' <<< "$ref")" ] || return 1 + commit=$("$jq_bin" -r '.revision.commit_id' <<< "$ref") + git_safe -C "$root" cat-file -e "$commit^{commit}" >/dev/null 2>&1 || return 1 + location=$("$jq_bin" -r '.location.kind' <<< "$ref") + if [ "$location" = root ]; then + mode=040000 + type=tree + object=$(git_safe -C "$root" rev-parse "$commit^{tree}") || return 1 + elif [ "$location" = path ]; then + path=$("$jq_bin" -r '.location.value' <<< "$ref") + entry=$(git_safe -C "$root" ls-tree "$commit" -- "$path" 2>/dev/null) || return 1 + [ -n "$entry" ] || return 1 + meta=${entry%%$'\t'*} + actual_path=${entry#*$'\t'} + read -r mode type object <<< "$meta" + [ "$actual_path" = "$path" ] || return 1 + else + return 1 + fi + [ "$mode" = "$("$jq_bin" -r '.mode' <<< "$ref")" ] && + [ "$type" = "$("$jq_bin" -r '.object_type' <<< "$ref")" ] && + [ "$object" = "$("$jq_bin" -r '.object_id' <<< "$ref")" ] && + [ "$(git_safe -C "$root" cat-file -t "$object")" = "$type" ] || return 1 + snapshot="$run_tmp/source.$cell.$index" + git_safe -C "$root" cat-file "$type" "$object" > "$snapshot" 2>/dev/null || return 1 + expected_sha=$("$jq_bin" -r '.value_sha256' <<< "$claim") + [ "$(sha_file "$snapshot")" = "$expected_sha" ] || return 1 + format=$("$jq_bin" -r '.value_format' <<< "$claim") + [ "$format" = raw-bytes ] || canonical "$snapshot" "$run_tmp/source-canonical.$cell.$index" +} + +verify_all_sources() { + local cell=$1 + local resolved=$2 + local claims="$run_tmp/source-claims.$cell" + local claim index=0 + "$jq_bin" -c '[ + .body.profile_source, + (.body.bindings[] | + .manifest_source, + .package_source, + (.config_source | select(.state=="present") | .value), + (.prompt_source | select(.state=="present") | .value), + .skill_sources[]?, + (.tool_sources[]? | + .package_source, + (.config_source | select(.state=="present") | .value))) + ] | .[]' "$resolved" > "$claims" || return 1 + while IFS= read -r claim; do + verify_source_claim "$cell" "$claim" "$index" || return 1 + index=$((index + 1)) + done < "$claims" + [ "$index" -gt 0 ] +} + CHILD_STATUS=0 CHILD_ERROR='' run_child() { @@ -338,6 +437,7 @@ for cell in aa ab ba bb; do ! verify_package "$cell" fake.protocol-fault "$fault_ref"; then runner_error E_PACKAGE fi + verify_all_sources "$cell" "$resolved" || runner_error E_SOURCE profile="$profiles/profiles/default.json" manifest_args=() for role in forge producer publisher reviewer verifier; do @@ -515,6 +615,7 @@ done ([.[].provenance.forge.package_id] | unique | length) == 2 and ([.[].provenance.profile_sha256] | unique | length) == 4' "$cells" >/dev/null || runner_error E_EQUIVALENCE +verify_target_unchanged || runner_error E_TARGET_STALE inventory_sha=$(sha_file "$inventory") "$jq_bin" -S -c -n --slurpfile cells "$cells" --slurpfile negatives "$negatives" \ --arg inventory_sha "$inventory_sha" --arg target_commit "$target_commit" \ diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index 4796a25..a2a601c 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -199,6 +199,62 @@ for ref_mutation in repository revision hash path type mode; do /usr/bin/git -C "$fixture/cells/aa/profile" commit -q -m "restore-$ref_mutation" done +source_claim_saved="$tmp/source-claim.saved" +/bin/cp "$fixture/cells/aa/resolved.json" "$source_claim_saved" +for source_mutation in manifest-repository config-commit prompt-path skill-oid tool-mode profile-value; do + case "$source_mutation" in + manifest-repository) + source_filter='(.body.bindings[] | select(.binding.role=="forge") | + .manifest_source.source.revision.repository_id)="repo.profile"' + ;; + config-commit) + source_filter='(.body.bindings[] | select(.binding.role=="producer") | + .config_source.value.source.revision.commit_id)=("0"*64)' + ;; + prompt-path) + source_filter='(.body.bindings[] | select(.binding.role=="producer") | + .prompt_source.value.source.location.value)="prompts/reviewer.md"' + ;; + skill-oid) + source_filter='(.body.bindings[] | select(.binding.role=="producer") | + .skill_sources[0].source.object_id)=("0"*64)' + ;; + tool-mode) + source_filter='(.body.bindings[] | select(.binding.role=="producer") | + .tool_sources[0].package_source.source.mode)="100755"' + ;; + profile-value) source_filter='.body.profile_source.value_sha256=("0"*64)' ;; + esac + "$jq_bin" -S -c "$source_filter" "$source_claim_saved" > "$fixture/cells/aa/resolved.json" + expect_failure "source-claim-$source_mutation" E_SOURCE + /bin/cp "$source_claim_saved" "$fixture/cells/aa/resolved.json" +done + +late_out="$tmp/late-target.out" +late_err="$tmp/late-target.err" +PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$late_out" 2> "$late_err" & +late_pid=$! +late_marker='' +for _ in {1..3000}; do + late_marker=$(/usr/bin/find "$fixture/scratch" -path '*/aa.producer.home' -type d -print -quit) + [ -z "$late_marker" ] || break + /bin/kill -0 "$late_pid" 2>/dev/null || break + /bin/sleep 0.02 +done +if [ -z "$late_marker" ]; then + /bin/kill "$late_pid" 2>/dev/null || : + wait "$late_pid" 2>/dev/null || : + fail 'late target marker' +fi +GIT_AUTHOR_DATE=2000-01-06T00:00:00Z GIT_COMMITTER_DATE=2000-01-06T00:00:00Z \ + /usr/bin/git -C "$fixture/target" commit -q --allow-empty -m late-change +if wait "$late_pid"; then + fail 'late target mutation accepted' +fi +[ ! -s "$late_out" ] && [ "$(/bin/cat "$late_err")" = E_TARGET_STALE ] || + fail 'late target mutation error' +pass 'late target commit recheck' + source_saved="$tmp/source.saved" /bin/cp "$fixture/target/source.txt" "$source_saved" /usr/bin/printf '%s\n' changed > "$fixture/target/source.txt" From 3eae0673a19cf40f05719bffa71aec95241c7823 Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 12:43:51 -0400 Subject: [PATCH 07/12] Contain adapter process groups --- adapter-tests/v1/fakes/protocol-fault.sh | 8 ++- adapter-tests/v1/runner.sh | 43 ++++++++++-- .../test/portable-adapter-contracts.test.sh | 68 ++++++++++++++----- 3 files changed, 95 insertions(+), 24 deletions(-) diff --git a/adapter-tests/v1/fakes/protocol-fault.sh b/adapter-tests/v1/fakes/protocol-fault.sh index f30d67d..900813a 100755 --- a/adapter-tests/v1/fakes/protocol-fault.sh +++ b/adapter-tests/v1/fakes/protocol-fault.sh @@ -14,7 +14,13 @@ case "$mode" in partial) /usr/bin/printf '%s\n' \ '{"case_id":"matrix-aa","payloads":[],"phase":"producer","protocol_version":1}' ;; degraded) /usr/bin/printf '%s\n' '{"status":"degraded"}' ;; - timeout) /bin/sleep 5 ;; + timeout) + marker_parent=${TMPDIR%/*} + marker="${marker_parent%/*}/timeout-survived" + (trap '' TERM; /bin/sleep 2; /usr/bin/printf survived > "$marker") & + trap '' TERM + wait + ;; transport) exit 9 ;; relabelled|multiple|unlinked|duplicate) /bin/bash "${BASH_SOURCE[0]%/*}/producer-a.sh" "$request" "$jq_bin" \ diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index e2c87e2..f84d2ff 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -30,7 +30,33 @@ fixture_root=$(CDPATH='' cd -P -- "$fixture_root" && pwd -P) || runner_error E_F run_tmp=$(/usr/bin/mktemp -d "$fixture_root/scratch/run.XXXXXX") || runner_error cleanup() { /bin/rm -rf -- "$run_tmp"; } -trap cleanup EXIT HUP INT TERM +ACTIVE_CHILD_GROUP='' +ACTIVE_CHILD_PID='' +terminate_active_group() { + local group=$ACTIVE_CHILD_GROUP + local leader=$ACTIVE_CHILD_PID + ACTIVE_CHILD_GROUP='' + ACTIVE_CHILD_PID='' + if [[ "$group" =~ ^[1-9][0-9]*$ ]]; then + kill -TERM -- "-$group" 2>/dev/null || : + /bin/sleep 0.1 + kill -KILL -- "-$group" 2>/dev/null || : + fi + if [[ "$leader" =~ ^[1-9][0-9]*$ ]]; then + wait "$leader" 2>/dev/null || : + fi +} +signal_exit() { + local status=$1 + trap - EXIT HUP INT TERM + terminate_active_group + cleanup + exit "$status" +} +trap cleanup EXIT +trap 'signal_exit 129' HUP +trap 'signal_exit 130' INT +trap 'signal_exit 143' TERM /bin/mkdir -m 700 "$run_tmp/home" sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } @@ -132,7 +158,7 @@ package_digest() { fake.producer.b) printf '%s\n' 602650c6cf4b161f5b92b273b6944a42fb84eb5946918e9ec13fed9746a20049 ;; fake.forge.a) printf '%s\n' 672636d079df76736b3b9160d7a38f67ff83767d7da455458c418c1cb0b539a6 ;; fake.forge.b) printf '%s\n' a87a8453b2daf7723a9565bb597531f198c7c891d2b47c7dfef80ffd3dc2096b ;; - fake.protocol-fault) printf '%s\n' 6daeccbda2b6752415c457967dfad03811837da75d25f38370a400381382de28 ;; + fake.protocol-fault) printf '%s\n' d151dffcc4128d5925da82d6defe67efc7a05332441601fc5a740e7099e7bae3 ;; *) return 1 ;; esac } @@ -280,6 +306,7 @@ run_child() { CHILD_STATUS=0 CHILD_ERROR='' /bin/mkdir -m 700 "$child_home" + set -m ( ulimit -t 2 -f 2048 -n 64 child_args=("$request" "$jq_bin" "$contract" "$core_fixture_modules") @@ -293,19 +320,21 @@ run_child() { /bin/bash "$executable" "${child_args[@]}" ) > "$output" 2> "$diagnostic" & pid=$! + ACTIVE_CHILD_GROUP=$pid + ACTIVE_CHILD_PID=$pid + set +m while kill -0 "$pid" 2>/dev/null; do tick=$((tick + 1)) if [ "$tick" -ge 20 ]; then - kill -TERM "$pid" 2>/dev/null || : - /bin/sleep 0.1 - kill -KILL "$pid" 2>/dev/null || : - wait "$pid" 2>/dev/null || : + terminate_active_group CHILD_ERROR=E_TIMEOUT return 1 fi /bin/sleep 0.05 done wait "$pid" || CHILD_STATUS=$? + ACTIVE_CHILD_GROUP='' + ACTIVE_CHILD_PID='' [ "$CHILD_STATUS" -eq 0 ] || { CHILD_ERROR=E_TRANSPORT; return 1; } [ "$(wc -c < "$output" | tr -d ' ')" -le 1048576 ] && [ "$(wc -c < "$diagnostic" | tr -d ' ')" -le 4096 ] || { @@ -609,6 +638,8 @@ for negative in degraded empty malformed partial relabelled timeout transport mu "$jq_bin" -S -c -n --arg case_id "$negative_id" --arg observed_error "$observed" \ '{assertion_ids:["expected-error"],case_id:$case_id,observed_error:$observed_error}' >> "$negatives" done +/bin/sleep 1.2 +[ ! -e "$fixture_root/scratch/timeout-survived" ] || runner_error E_GROUP_LEAK "$jq_bin" -s -e '([.[].projection] | unique | length) == 1 and ([.[].provenance.producer.package_id] | unique | length) == 2 and diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index a2a601c..38a58bc 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -70,13 +70,25 @@ pass 'fixed resolver launcher and dependencies built' fixture="$tmp/garden-fixture" /bin/mkdir -m 700 "$fixture" "$fixture/cells" "$fixture/scratch" "$fixture/target" /bin/cp "$test_root/adapter-tests/v1/fixture/source.txt" "$fixture/target/source.txt" -/usr/bin/git init -q "$fixture/target" -/usr/bin/git -C "$fixture/target" config user.name fixture -/usr/bin/git -C "$fixture/target" config user.email fixture@example.invalid -GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ - /usr/bin/git -C "$fixture/target" add source.txt -GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ - /usr/bin/git -C "$fixture/target" commit -q -m fixture +git_home="$tmp/git-home" +/bin/mkdir -m 700 "$git_home" +fixture_git() { + /usr/bin/env -i HOME="$git_home" TMPDIR="$tmp" PATH=/usr/bin:/bin LC_ALL=C \ + GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_NO_REPLACE_OBJECTS=1 \ + GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /usr/bin/git --no-replace-objects "$@" +} +/usr/bin/env -i HOME="$git_home" TMPDIR="$tmp" PATH=/usr/bin:/bin LC_ALL=C \ + GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_DEFAULT_HASH=sha256 \ + /usr/bin/git -c init.defaultObjectFormat=sha256 init -q --object-format=sha1 "$fixture/target" +[ "$(fixture_git -C "$fixture/target" rev-parse --show-object-format)" = sha1 ] || + fail 'explicit target SHA-1 format' +fixture_git -C "$fixture/target" config user.name fixture +fixture_git -C "$fixture/target" config user.email fixture@example.invalid +fixture_git -C "$fixture/target" add source.txt +fixture_git -C "$fixture/target" commit -q -m fixture +pass 'hostile defaults cannot override explicit target SHA-1' for cell in aa ab ba bb; do cell_root="$fixture/cells/$cell" @@ -97,7 +109,7 @@ pass 'four honest profile variants resolved through the inactive resolver' root:($root+"/cells/"+$cell+"/"+$repo)})) | add))} ' > "$fixture/repository-map.json" -fingerprint_before=$(/usr/bin/git -C "$fixture/target" rev-parse 'HEAD^{tree}') +fingerprint_before=$(fixture_git -C "$fixture/target" rev-parse 'HEAD^{tree}') output="$tmp/observation.json" error="$tmp/observation.stderr" GH_TOKEN=must-not-leak AWS_SECRET_ACCESS_KEY=must-not-leak SSH_AUTH_SOCK=/must/not/leak \ @@ -115,7 +127,7 @@ GH_TOKEN=must-not-leak AWS_SECRET_ACCESS_KEY=must-not-leak SSH_AUTH_SOCK=/must/n (.non_claims | index("network-isolation") != null) and (.inventory_acceptance_ref.authorization_comment_id == 5476938197) ' "$output" >/dev/null || fail 'observation shape and equivalence' -[ "$fingerprint_before" = "$(/usr/bin/git -C "$fixture/target" rev-parse 'HEAD^{tree}')" ] || +[ "$fingerprint_before" = "$(fixture_git -C "$fixture/target" rev-parse 'HEAD^{tree}')" ] || fail 'target changed' [ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || fail 'scratch cleanup' /usr/bin/grep -Fq 'moon-garden' "$fixture/target/source.txt" || fail 'unrelated target fixture' @@ -246,8 +258,7 @@ if [ -z "$late_marker" ]; then wait "$late_pid" 2>/dev/null || : fail 'late target marker' fi -GIT_AUTHOR_DATE=2000-01-06T00:00:00Z GIT_COMMITTER_DATE=2000-01-06T00:00:00Z \ - /usr/bin/git -C "$fixture/target" commit -q --allow-empty -m late-change +fixture_git -C "$fixture/target" commit -q --allow-empty -m late-change if wait "$late_pid"; then fail 'late target mutation accepted' fi @@ -255,17 +266,40 @@ fi fail 'late target mutation error' pass 'late target commit recheck' +signal_out="$tmp/signal.out" +signal_err="$tmp/signal.err" +PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$signal_out" 2> "$signal_err" & +signal_pid=$! +signal_marker='' +for _ in {1..6000}; do + signal_marker=$(/usr/bin/find "$fixture/scratch" -path '*/timeout.home' -type d -print -quit) + [ -z "$signal_marker" ] || break + /bin/kill -0 "$signal_pid" 2>/dev/null || break + /bin/sleep 0.02 +done +if [ -z "$signal_marker" ]; then + /bin/kill "$signal_pid" 2>/dev/null || : + wait "$signal_pid" 2>/dev/null || : + fail 'signal child marker' +fi +/bin/kill -TERM "$signal_pid" +signal_status=0 +wait "$signal_pid" || signal_status=$? +/bin/sleep 2.2 +[ "$signal_status" -eq 143 ] && [ ! -s "$signal_out" ] && [ ! -s "$signal_err" ] && + [ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || + fail 'signal group cleanup' +pass 'TERM stops adapter group and cleans scratch' + source_saved="$tmp/source.saved" /bin/cp "$fixture/target/source.txt" "$source_saved" /usr/bin/printf '%s\n' changed > "$fixture/target/source.txt" -/usr/bin/git -C "$fixture/target" add source.txt -GIT_AUTHOR_DATE=2000-01-02T00:00:00Z GIT_COMMITTER_DATE=2000-01-02T00:00:00Z \ - /usr/bin/git -C "$fixture/target" commit -q -m changed +fixture_git -C "$fixture/target" add source.txt +fixture_git -C "$fixture/target" commit -q -m changed expect_failure fixture-digest-mismatch E_FIXTURE /bin/cp "$source_saved" "$fixture/target/source.txt" -/usr/bin/git -C "$fixture/target" add source.txt -GIT_AUTHOR_DATE=2000-01-03T00:00:00Z GIT_COMMITTER_DATE=2000-01-03T00:00:00Z \ - /usr/bin/git -C "$fixture/target" commit -q -m restored +fixture_git -C "$fixture/target" add source.txt +fixture_git -C "$fixture/target" commit -q -m restored producer="$fixture/cells/aa/assets/packages/producer-a.sh" /usr/bin/printf '%s\n' '# changed' >> "$producer" From a4b3ac881b00702b35f9928df2ea97db969664b0 Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 13:32:39 -0400 Subject: [PATCH 08/12] Snapshot adapters and reject descendants --- adapter-tests/v1/contract.jq | 3 +- adapter-tests/v1/fakes/protocol-fault.sh | 13 +++- adapter-tests/v1/inventory.json | 2 +- adapter-tests/v1/runner.sh | 71 +++++++++++++------ .../test/portable-adapter-contracts.test.sh | 36 +++++++++- 5 files changed, 97 insertions(+), 28 deletions(-) diff --git a/adapter-tests/v1/contract.jq b/adapter-tests/v1/contract.jq index ef16f37..06d2152 100644 --- a/adapter-tests/v1/contract.jq +++ b/adapter-tests/v1/contract.jq @@ -29,7 +29,8 @@ def expected_cases($fixture): {case_id:"reject-transport",expected_error:"E_TRANSPORT"}, {case_id:"reject-multiple",expected_error:"E_MULTIPLE"}, {case_id:"reject-unlinked",expected_error:"E_UNLINKED_PAYLOAD"}, - {case_id:"reject-duplicate",expected_error:"E_DUPLICATE_PAYLOAD"} + {case_id:"reject-duplicate",expected_error:"E_DUPLICATE_PAYLOAD"}, + {case_id:"reject-descendant",expected_error:"E_DESCENDANT"} ] | map(if has("phase") then . else . + { phase:"producer",producer_package_id:"fake.protocol-fault",forge_package_id:"none", expected_verdict:"reject",equivalence_group:"protocol-negative", diff --git a/adapter-tests/v1/fakes/protocol-fault.sh b/adapter-tests/v1/fakes/protocol-fault.sh index 900813a..f9c468a 100755 --- a/adapter-tests/v1/fakes/protocol-fault.sh +++ b/adapter-tests/v1/fakes/protocol-fault.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash # shellcheck disable=SC2016 set -euo pipefail -[ "$#" -eq 5 ] || exit 64 +[ "$#" -eq 6 ] || exit 64 [ -z "${GH_TOKEN-}${GITHUB_TOKEN-}${AWS_SECRET_ACCESS_KEY-}${SSH_AUTH_SOCK-}" ] || exit 70 mode=$1 request=$2 jq_bin=$3 contract=$4 modules=$5 +producer_snapshot=$6 case "$mode" in empty) ;; malformed) /usr/bin/printf '{\n' ;; @@ -22,8 +23,8 @@ case "$mode" in wait ;; transport) exit 9 ;; - relabelled|multiple|unlinked|duplicate) - /bin/bash "${BASH_SOURCE[0]%/*}/producer-a.sh" "$request" "$jq_bin" \ + relabelled|multiple|unlinked|duplicate|descendant) + /bin/bash "$producer_snapshot" "$request" "$jq_bin" \ "$contract" "$modules" > "$HOME/base-response.json" case "$mode" in relabelled) "$jq_bin" -S -c '.phase="forge"' "$HOME/base-response.json" ;; @@ -33,6 +34,12 @@ case "$mode" in payload_id:"unlinked",sha256:("0"*64)}]' "$HOME/base-response.json" ;; duplicate) "$jq_bin" -S -c '.payloads += [.payloads[0]]' "$HOME/base-response.json" ;; + descendant) + marker_parent=${TMPDIR%/*} + marker="${marker_parent%/*}/descendant-survived" + (trap '' TERM; /bin/sleep 2; /usr/bin/printf survived > "$marker") & + /bin/cat "$HOME/base-response.json" + ;; esac ;; *) exit 64 ;; diff --git a/adapter-tests/v1/inventory.json b/adapter-tests/v1/inventory.json index f8ff3a8..53fff13 100644 --- a/adapter-tests/v1/inventory.json +++ b/adapter-tests/v1/inventory.json @@ -1 +1 @@ -{"authorization_ref":{"comment_id":5476938197,"issue_number":153,"scope_comment_id":5474023028},"cases":[{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-aa","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ab","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ba","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-bb","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["expected-error"],"case_id":"reject-degraded","equivalence_group":"protocol-negative","expected_error":"E_DEGRADED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-empty","equivalence_group":"protocol-negative","expected_error":"E_EMPTY","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-malformed","equivalence_group":"protocol-negative","expected_error":"E_MALFORMED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-partial","equivalence_group":"protocol-negative","expected_error":"E_PARTIAL","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-relabelled","equivalence_group":"protocol-negative","expected_error":"E_RELABELLED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-timeout","equivalence_group":"protocol-negative","expected_error":"E_TIMEOUT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-transport","equivalence_group":"protocol-negative","expected_error":"E_TRANSPORT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-multiple","equivalence_group":"protocol-negative","expected_error":"E_MULTIPLE","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-unlinked","equivalence_group":"protocol-negative","expected_error":"E_UNLINKED_PAYLOAD","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-duplicate","equivalence_group":"protocol-negative","expected_error":"E_DUPLICATE_PAYLOAD","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"}],"protocol":"ystack.adapter-contract.inventory.v1","schema_version":1} +{"authorization_ref":{"comment_id":5476938197,"issue_number":153,"scope_comment_id":5474023028},"cases":[{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-aa","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ab","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.a"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-ba","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.a","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["audit-projection","candidate-git","core-validation","environment-clean","evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"],"case_id":"matrix-bb","equivalence_group":"portable-fake-v1","expected_error":"","expected_verdict":"pass","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"fake.forge.b","phase":"pipeline","producer_package_id":"fake.producer.b"},{"assertions":["expected-error"],"case_id":"reject-degraded","equivalence_group":"protocol-negative","expected_error":"E_DEGRADED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-empty","equivalence_group":"protocol-negative","expected_error":"E_EMPTY","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-malformed","equivalence_group":"protocol-negative","expected_error":"E_MALFORMED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-partial","equivalence_group":"protocol-negative","expected_error":"E_PARTIAL","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-relabelled","equivalence_group":"protocol-negative","expected_error":"E_RELABELLED","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-timeout","equivalence_group":"protocol-negative","expected_error":"E_TIMEOUT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-transport","equivalence_group":"protocol-negative","expected_error":"E_TRANSPORT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-multiple","equivalence_group":"protocol-negative","expected_error":"E_MULTIPLE","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-unlinked","equivalence_group":"protocol-negative","expected_error":"E_UNLINKED_PAYLOAD","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-duplicate","equivalence_group":"protocol-negative","expected_error":"E_DUPLICATE_PAYLOAD","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"},{"assertions":["expected-error"],"case_id":"reject-descendant","equivalence_group":"protocol-negative","expected_error":"E_DESCENDANT","expected_verdict":"reject","fixture_sha256":"ce90e53bb6592130c8d56db2d5cda036c11baba589a5ffb3e2e07e75366c2ef6","forge_package_id":"none","phase":"producer","producer_package_id":"fake.protocol-fault"}],"protocol":"ystack.adapter-contract.inventory.v1","schema_version":1} diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index f84d2ff..f837966 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -32,19 +32,28 @@ run_tmp=$(/usr/bin/mktemp -d "$fixture_root/scratch/run.XXXXXX") || runner_error cleanup() { /bin/rm -rf -- "$run_tmp"; } ACTIVE_CHILD_GROUP='' ACTIVE_CHILD_PID='' +group_alive() { + [[ "${1:-}" =~ ^[1-9][0-9]*$ ]] && kill -0 -- "-$1" 2>/dev/null +} terminate_active_group() { local group=$ACTIVE_CHILD_GROUP local leader=$ACTIVE_CHILD_PID - ACTIVE_CHILD_GROUP='' - ACTIVE_CHILD_PID='' + local tick if [[ "$group" =~ ^[1-9][0-9]*$ ]]; then kill -TERM -- "-$group" 2>/dev/null || : - /bin/sleep 0.1 - kill -KILL -- "-$group" 2>/dev/null || : + for tick in {1..5}; do + group_alive "$group" || break + /bin/sleep 0.02 + done + if group_alive "$group"; then + kill -KILL -- "-$group" 2>/dev/null || : + fi fi if [[ "$leader" =~ ^[1-9][0-9]*$ ]]; then wait "$leader" 2>/dev/null || : fi + ACTIVE_CHILD_GROUP='' + ACTIVE_CHILD_PID='' } signal_exit() { local status=$1 @@ -69,7 +78,7 @@ canonical() { cmp -s "$source" "$output" } -[ "$(sha_file "$inventory")" = 73b90745ba3ae879f8d6d958e3133e33fb6169bc89fb9c26a826be1be9fd9fd6 ] || +[ "$(sha_file "$inventory")" = 100edb973c586192683917bc4896121117ef397161263510e09f03a0b269bd4a ] || runner_error E_INVENTORY inventory_canonical="$run_tmp/inventory.canonical" canonical "$inventory" "$inventory_canonical" || runner_error E_INVENTORY @@ -158,7 +167,7 @@ package_digest() { fake.producer.b) printf '%s\n' 602650c6cf4b161f5b92b273b6944a42fb84eb5946918e9ec13fed9746a20049 ;; fake.forge.a) printf '%s\n' 672636d079df76736b3b9160d7a38f67ff83767d7da455458c418c1cb0b539a6 ;; fake.forge.b) printf '%s\n' a87a8453b2daf7723a9565bb597531f198c7c891d2b47c7dfef80ffd3dc2096b ;; - fake.protocol-fault) printf '%s\n' d151dffcc4128d5925da82d6defe67efc7a05332441601fc5a740e7099e7bae3 ;; + fake.protocol-fault) printf '%s\n' a31c07896edb1ee04d2c87b4512e7199c6b6f514a998d110005ce24d42cdd27c ;; *) return 1 ;; esac } @@ -183,11 +192,13 @@ mapped_root() { ' "$mapping" } +VERIFIED_PACKAGE_SNAPSHOT='' verify_package() { local cell=$1 local package=$2 local ref=$3 - local relative digest repository_id root algorithm commit entry meta mode type object snapshot + local relative digest repository_id root algorithm commit entry meta mode type object snapshot package_key + VERIFIED_PACKAGE_SNAPSHOT='' relative=$(package_path "$package") || return 1 digest=$(package_digest "$package") || return 1 "$jq_bin" -e --arg path "$relative" ' @@ -214,10 +225,15 @@ verify_package() { [ "$mode" = "$("$jq_bin" -r '.mode' <<< "$ref")" ] && [ "$type" = "$("$jq_bin" -r '.object_type' <<< "$ref")" ] && [ "$object" = "$("$jq_bin" -r '.object_id' <<< "$ref")" ] || return 1 - snapshot="$run_tmp/package.$cell.${package##*.}" + package_key=${package//./-} + snapshot="$run_tmp/package.$cell.$package_key" git_safe -C "$root" cat-file blob "$object" > "$snapshot" || return 1 - [ -f "$root/$relative" ] && [ ! -L "$root/$relative" ] && - cmp -s "$snapshot" "$root/$relative" && [ "$(sha_file "$snapshot")" = "$digest" ] + /bin/chmod 0500 "$snapshot" || return 1 + [ -f "$snapshot" ] && [ ! -L "$snapshot" ] && + [ -f "$root/$relative" ] && [ ! -L "$root/$relative" ] && + cmp -s "$snapshot" "$root/$relative" && [ "$(sha_file "$snapshot")" = "$digest" ] || + return 1 + VERIFIED_PACKAGE_SNAPSHOT=$snapshot } verify_source_claim() { @@ -302,6 +318,7 @@ run_child() { local output=$4 local diagnostic=$5 local child_home=$6 + local dependency=${7:-} local pid tick=0 CHILD_STATUS=0 CHILD_ERROR='' @@ -310,6 +327,7 @@ run_child() { ( ulimit -t 2 -f 2048 -n 64 child_args=("$request" "$jq_bin" "$contract" "$core_fixture_modules") + [ -z "$dependency" ] || child_args+=("$dependency") [ "$mode" = direct ] || child_args=("$mode" "${child_args[@]}") exec /usr/bin/env -i HOME="$child_home" TMPDIR="$child_home" PATH=/usr/bin:/bin \ LC_ALL=C GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \ @@ -333,6 +351,11 @@ run_child() { /bin/sleep 0.05 done wait "$pid" || CHILD_STATUS=$? + if group_alive "$ACTIVE_CHILD_GROUP"; then + terminate_active_group + CHILD_ERROR=E_DESCENDANT + return 1 + fi ACTIVE_CHILD_GROUP='' ACTIVE_CHILD_PID='' [ "$CHILD_STATUS" -eq 0 ] || { CHILD_ERROR=E_TRANSPORT; return 1; } @@ -445,8 +468,6 @@ for cell in aa ab ba bb; do forge_variant=${cell#?} producer_package="fake.producer.$producer_variant" forge_package="fake.forge.$forge_variant" - producer_path=$(package_path "$producer_package") - forge_path=$(package_path "$forge_package") producer_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="producer") | .package_ref' "$cell_root/profile/profiles/default.json") forge_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="forge") | .package_ref' "$cell_root/profile/profiles/default.json") producer_resolved_ref=$("$jq_bin" -c '.body.bindings[] | select(.binding.role=="producer") | .binding.package_ref' "$resolved") @@ -460,12 +481,16 @@ for cell in aa ab ba bb; do --arg mode "$fault_mode" --arg type "$fault_type" --arg object "$fault_object" \ '.location={kind:"path",value:$path} | .mode=$mode | .object_type=$type | .object_id=$object' \ <<< "$forge_ref") - if [ "$producer_ref" != "$producer_resolved_ref" ] || [ "$forge_ref" != "$forge_resolved_ref" ] || - ! verify_package "$cell" "$producer_package" "$producer_ref" || - ! verify_package "$cell" "$forge_package" "$forge_ref" || - ! verify_package "$cell" fake.protocol-fault "$fault_ref"; then + if [ "$producer_ref" != "$producer_resolved_ref" ] || [ "$forge_ref" != "$forge_resolved_ref" ]; then runner_error E_PACKAGE fi + verify_package "$cell" "$producer_package" "$producer_ref" || runner_error E_PACKAGE + producer_executable=$VERIFIED_PACKAGE_SNAPSHOT + [ "$cell" != aa ] || fault_producer_executable=$producer_executable + verify_package "$cell" "$forge_package" "$forge_ref" || runner_error E_PACKAGE + forge_executable=$VERIFIED_PACKAGE_SNAPSHOT + verify_package "$cell" fake.protocol-fault "$fault_ref" || runner_error E_PACKAGE + [ "$cell" != aa ] || fault_executable=$VERIFIED_PACKAGE_SNAPSHOT verify_all_sources "$cell" "$resolved" || runner_error E_SOURCE profile="$profiles/profiles/default.json" manifest_args=() @@ -514,7 +539,7 @@ for cell in aa ab ba bb; do --arg command request-envelope -f "$contract" "$producer_request" >/dev/null || runner_error E_PROTOCOL_PRODUCER_REQUEST producer_output="$run_tmp/$cell.producer.output" producer_error="$run_tmp/$cell.producer.stderr" - run_child "$assets/$producer_path" direct "$producer_request" "$producer_output" \ + run_child "$producer_executable" direct "$producer_request" "$producer_output" \ "$producer_error" "$run_tmp/$cell.producer.home" || runner_error "$CHILD_ERROR" producer_result="$run_tmp/$cell.producer.stage-result" [ ! -s "$producer_error" ] || runner_error E_PROTOCOL_PRODUCER_OUTPUT @@ -556,7 +581,7 @@ for cell in aa ab ba bb; do forge_output="$run_tmp/$cell.forge.output" forge_error="$run_tmp/$cell.forge.stderr" forge_home="$run_tmp/$cell.forge.home" - run_child "$assets/$forge_path" direct "$forge_request" "$forge_output" \ + run_child "$forge_executable" direct "$forge_request" "$forge_output" \ "$forge_error" "$forge_home" || runner_error "$CHILD_ERROR" forge_result="$run_tmp/$cell.forge.stage-result" [ ! -s "$forge_error" ] || runner_error E_PROTOCOL_FORGE_OUTPUT @@ -614,18 +639,19 @@ for cell in aa ab ba bb; do ' >> "$cells" done -fault_executable="$fixture_root/cells/aa/assets/packages/protocol-fault.sh" +[ -n "${fault_executable:-}" ] || runner_error E_PACKAGE +[ -n "${fault_producer_executable:-}" ] || runner_error E_PACKAGE negative_request="$run_tmp/aa.producer.request" negative_stage_request="$run_tmp/aa.producer.stage-request" negative_resolved="$fixture_root/cells/aa/resolved.json" -for negative in degraded empty malformed partial relabelled timeout transport multiple unlinked duplicate; do +for negative in degraded empty malformed partial relabelled timeout transport multiple unlinked duplicate descendant; do negative_id="reject-$negative" negative_output="$run_tmp/$negative.output" negative_stderr="$run_tmp/$negative.stderr" negative_result="$run_tmp/$negative.stage-result" observed='' if ! run_child "$fault_executable" "$negative" "$negative_request" "$negative_output" \ - "$negative_stderr" "$run_tmp/$negative.home"; then + "$negative_stderr" "$run_tmp/$negative.home" "$fault_producer_executable"; then observed=$CHILD_ERROR elif validate_response "$negative_output" "$negative_stage_request" "$negative_resolved" \ matrix-aa producer "$negative_result"; then @@ -639,7 +665,8 @@ for negative in degraded empty malformed partial relabelled timeout transport mu '{assertion_ids:["expected-error"],case_id:$case_id,observed_error:$observed_error}' >> "$negatives" done /bin/sleep 1.2 -[ ! -e "$fixture_root/scratch/timeout-survived" ] || runner_error E_GROUP_LEAK +[ ! -e "$fixture_root/scratch/timeout-survived" ] && + [ ! -e "$fixture_root/scratch/descendant-survived" ] || runner_error E_GROUP_LEAK "$jq_bin" -s -e '([.[].projection] | unique | length) == 1 and ([.[].provenance.producer.package_id] | unique | length) == 2 and diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index 38a58bc..7b13ccb 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -120,7 +120,7 @@ GH_TOKEN=must-not-leak AWS_SECRET_ACCESS_KEY=must-not-leak SSH_AUTH_SOCK=/must/n [ ! -s "$error" ] || fail 'success diagnostics' "$jq_bin" -e ' .schema_version == 1 and .kind == "adapter_contract_observation" and - (.cells | length) == 4 and (.negative_observations | length) == 10 and + (.cells | length) == 4 and (.negative_observations | length) == 11 and ([.cells[].projection] | unique | length) == 1 and ([.cells[].provenance.profile_sha256] | unique | length) == 4 and (.non_claims | index("external-target-smoke") != null) and @@ -133,6 +133,40 @@ GH_TOKEN=must-not-leak AWS_SECRET_ACCESS_KEY=must-not-leak SSH_AUTH_SOCK=/must/n /usr/bin/grep -Fq 'moon-garden' "$fixture/target/source.txt" || fail 'unrelated target fixture' pass '2x2 projection, provenance, environment, Git truth, and cleanup' +race_out="$tmp/package-race.out" +race_err="$tmp/package-race.err" +race_forge="$fixture/cells/aa/assets/packages/forge-a.sh" +race_saved="$tmp/forge-a.saved" +/bin/cp "$race_forge" "$race_saved" +PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$race_out" 2> "$race_err" & +race_pid=$! +race_marker='' +for _ in {1..12000}; do + race_marker=$(/usr/bin/find "$fixture/scratch" -name 'aa.producer.stage-result' -type f -print -quit) + [ -z "$race_marker" ] || break + /bin/kill -0 "$race_pid" 2>/dev/null || break + /bin/sleep 0.005 +done +if [ -z "$race_marker" ]; then + /bin/kill "$race_pid" 2>/dev/null || : + wait "$race_pid" 2>/dev/null || : + fail 'immutable package race marker' +fi +/usr/bin/printf '%s\n' '#!/bin/bash' \ + "/usr/bin/printf mutable > '$fixture/scratch/mutable-package-ran'" \ + 'exit 99' > "$race_forge" +/bin/chmod 0755 "$race_forge" +if ! wait "$race_pid"; then + fail 'immutable package race run' +fi +[ ! -s "$race_err" ] && [ ! -e "$fixture/scratch/mutable-package-ran" ] && + [ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || + fail 'mutable package executed' +/bin/cp "$race_saved" "$race_forge" +/usr/bin/git -C "$fixture/cells/aa/assets" diff --quiet -- packages/forge-a.sh || + fail 'package race restore' +pass 'mapped package mutation cannot change snapshot execution' + expect_failure() { local name=$1 local expected=$2 From e9dea666bbb5babdd5c1372806fd9cdf69b65f6a Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 14:18:05 -0400 Subject: [PATCH 09/12] Snapshot and verify adapter request payloads --- adapter-tests/v1/contract.jq | 20 +++++++- adapter-tests/v1/runner.sh | 49 ++++++++++++++++--- .../test/portable-adapter-contracts.test.sh | 37 +++++++++++++- 3 files changed, 97 insertions(+), 9 deletions(-) diff --git a/adapter-tests/v1/contract.jq b/adapter-tests/v1/contract.jq index 06d2152..a5988b7 100644 --- a/adapter-tests/v1/contract.jq +++ b/adapter-tests/v1/contract.jq @@ -103,11 +103,29 @@ def stage_result($c; $output_id; $media_type; $payload_sha): ref:{content_id:("output."+$c.case_id+"."+$c.phase),media_type:$media_type,sha256:$payload_sha}}] | if $c.phase == "producer" then .body.delta_ref=.body.outputs[0].ref else . end; def request_envelope_ok: + . as $request | exact(["case_id","payloads","phase","protocol_version","stage_request"]) and .protocol_version == 1 and (.phase == "producer" or .phase == "forge") and .stage_request.schema_version == 2 and .stage_request.kind == "stage_request" and (.payloads | type == "array" and all(.[];payload_ok) and - ([.[].payload_id] | length == (unique | length))); + ([.[].payload_id] | length == (unique | length))) and + ([.payloads[].payload_id] | sort) == + (if .phase == "producer" then ["resolved-profile","source"] + else ["producer.patch","resolved-profile","source"] end) and + ([.payloads[] | select(.payload_id=="resolved-profile" and + .media_type=="application/json" and + .sha256==$request.stage_request.body.resolved_profile_ref.sha256)] | length) == 1 and + ([.payloads[] | select(.payload_id=="source" and .media_type=="text/plain")] | length) == 1 and + (if .phase == "forge" then + ([.stage_request.body.inputs[] | + select(.input_id=="input.producer-patch") | + .value.value.value.sha256] | length) == 1 and + ([.payloads[] as $payload | + select($payload.payload_id=="producer.patch" and $payload.media_type=="text/x-diff" and + ([$request.stage_request.body.inputs[] | + select(.input_id=="input.producer-patch" and + .value.value.value.sha256==$payload.sha256)] | length == 1))] | length) == 1 + else true end); def response_envelope_ok: (.response | exact(["case_id","payloads","phase","protocol_version","stage_result"])) and .response.protocol_version == 1 and .response.case_id == .case_id and diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index f837966..71bc16c 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -143,7 +143,11 @@ read -r target_mode target_type target_object <<< "$target_meta" target_source_snapshot="$run_tmp/target-source.snapshot" git_safe -C "$fixture_root/target" cat-file blob "$target_object" > "$target_source_snapshot" || runner_error E_GIT -cmp -s "$target_source_snapshot" "$fixture_source" || runner_error E_GIT +/bin/chmod 0400 "$target_source_snapshot" || runner_error E_GIT +if [ "$(sha_file "$target_source_snapshot")" != "$fixture_sha" ] || + ! cmp -s "$target_source_snapshot" "$fixture_source"; then + runner_error E_GIT +fi verify_target_unchanged() { local current_entry current_meta current_mode current_type current_object current_snapshot @@ -378,6 +382,28 @@ extract_payload() { "$("$jq_bin" -r --arg id "$payload_id" '.payloads[] | select(.payload_id==$id) | .sha256' "$response")" ] } +verify_request_payloads() { + local request=$1 + local resolved_snapshot=$2 + local source_snapshot=$3 + local patch_snapshot=${4:-} + local payload_id payload_file payload_index=0 + while IFS= read -r payload_id; do + payload_file="$run_tmp/request-payload.$payload_index" + extract_payload "$request" "$payload_id" "$payload_file" || return 1 + case "$payload_id" in + resolved-profile) cmp -s "$payload_file" "$resolved_snapshot" || return 1 ;; + source) cmp -s "$payload_file" "$source_snapshot" || return 1 ;; + producer.patch) + [ -n "$patch_snapshot" ] && cmp -s "$payload_file" "$patch_snapshot" || return 1 + ;; + *) return 1 ;; + esac + payload_index=$((payload_index + 1)) + done < <("$jq_bin" -r '.payloads[].payload_id' "$request") + [ "$payload_index" -eq "$("$jq_bin" '.payloads | length' "$request")" ] +} + stage_consistent() { local request=$1 local resolved=$2 @@ -460,7 +486,12 @@ for cell in aa ab ba bb; do assets="$cell_root/assets" manifests="$cell_root/manifests" profiles="$cell_root/profile" - resolved="$cell_root/resolved.json" + resolved_input="$cell_root/resolved.json" + resolved="$run_tmp/resolved.$cell.snapshot" + [ -f "$resolved_input" ] && [ ! -L "$resolved_input" ] || runner_error E_INPUT + /bin/cp "$resolved_input" "$resolved" || runner_error E_INPUT + /bin/chmod 0400 "$resolved" || runner_error E_INPUT + canonical "$resolved" "$run_tmp/resolved.$cell.canonical" || runner_error E_INPUT for mapped_repo in "$assets" "$manifests" "$profiles"; do verify_repo "$mapped_repo" || runner_error E_GIT done @@ -529,7 +560,7 @@ for cell in aa ab ba bb; do "$core" validate-document "$producer_stage_request" >/dev/null 2>&1 || runner_error E_CORE_PRODUCER_REQUEST producer_request="$run_tmp/$cell.producer.request" "$jq_bin" -S -c -n --arg case_id "$cell_id" --slurpfile stage "$producer_stage_request" \ - --rawfile source "$fixture_source" --arg source_sha "$fixture_sha" \ + --rawfile source "$target_source_snapshot" --arg source_sha "$fixture_sha" \ --rawfile resolved "$resolved" --arg resolved_sha "$resolved_sha" ' {case_id:$case_id,payloads:[ {data:$resolved,media_type:"application/json",payload_id:"resolved-profile",sha256:$resolved_sha}, @@ -537,6 +568,8 @@ for cell in aa ab ba bb; do phase:"producer",protocol_version:1,stage_request:$stage[0]}' > "$producer_request" "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ --arg command request-envelope -f "$contract" "$producer_request" >/dev/null || runner_error E_PROTOCOL_PRODUCER_REQUEST + verify_request_payloads "$producer_request" "$resolved" "$target_source_snapshot" || + runner_error E_REQUEST_PAYLOAD producer_output="$run_tmp/$cell.producer.output" producer_error="$run_tmp/$cell.producer.stderr" run_child "$producer_executable" direct "$producer_request" "$producer_output" \ @@ -568,7 +601,7 @@ for cell in aa ab ba bb; do "$core" validate-document "$forge_stage_request" >/dev/null 2>&1 || runner_error E_CORE_FORGE_REQUEST forge_request="$run_tmp/$cell.forge.request" "$jq_bin" -S -c -n --arg case_id "$cell_id" --slurpfile stage "$forge_stage_request" \ - --rawfile source "$fixture_source" --arg source_sha "$fixture_sha" \ + --rawfile source "$target_source_snapshot" --arg source_sha "$fixture_sha" \ --rawfile resolved "$resolved" --arg resolved_sha "$resolved_sha" \ --rawfile patch "$producer_patch" --arg patch_sha "$patch_sha" ' {case_id:$case_id,payloads:[ @@ -578,6 +611,8 @@ for cell in aa ab ba bb; do phase:"forge",protocol_version:1,stage_request:$stage[0]}' > "$forge_request" "$jq_bin" -L "$runner_dir" -L "$core_fixture_modules" -e \ --arg command request-envelope -f "$contract" "$forge_request" >/dev/null || runner_error E_PROTOCOL_FORGE_REQUEST + verify_request_payloads "$forge_request" "$resolved" "$target_source_snapshot" "$producer_patch" || + runner_error E_REQUEST_PAYLOAD forge_output="$run_tmp/$cell.forge.output" forge_error="$run_tmp/$cell.forge.stderr" forge_home="$run_tmp/$cell.forge.home" @@ -609,7 +644,7 @@ for cell in aa ab ba bb; do oracle="$run_tmp/$cell.oracle" /bin/mkdir -m 700 "$oracle" git_write init -q "$oracle" - /bin/cp "$fixture_source" "$oracle/source.txt" + /bin/cp "$target_source_snapshot" "$oracle/source.txt" git_write -C "$oracle" add source.txt git_write -C "$oracle" commit -q -m source git_write -C "$oracle" apply "$producer_patch" @@ -643,13 +678,15 @@ done [ -n "${fault_producer_executable:-}" ] || runner_error E_PACKAGE negative_request="$run_tmp/aa.producer.request" negative_stage_request="$run_tmp/aa.producer.stage-request" -negative_resolved="$fixture_root/cells/aa/resolved.json" +negative_resolved="$run_tmp/resolved.aa.snapshot" for negative in degraded empty malformed partial relabelled timeout transport multiple unlinked duplicate descendant; do negative_id="reject-$negative" negative_output="$run_tmp/$negative.output" negative_stderr="$run_tmp/$negative.stderr" negative_result="$run_tmp/$negative.stage-result" observed='' + verify_request_payloads "$negative_request" "$negative_resolved" "$target_source_snapshot" || + runner_error E_REQUEST_PAYLOAD if ! run_child "$fault_executable" "$negative" "$negative_request" "$negative_output" \ "$negative_stderr" "$run_tmp/$negative.home" "$fault_producer_executable"; then observed=$CHILD_ERROR diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index 7b13ccb..4b77a20 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -137,12 +137,14 @@ race_out="$tmp/package-race.out" race_err="$tmp/package-race.err" race_forge="$fixture/cells/aa/assets/packages/forge-a.sh" race_saved="$tmp/forge-a.saved" +race_source_saved="$tmp/source-race.saved" /bin/cp "$race_forge" "$race_saved" +/bin/cp "$fixture/target/source.txt" "$race_source_saved" PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$race_out" 2> "$race_err" & race_pid=$! race_marker='' for _ in {1..12000}; do - race_marker=$(/usr/bin/find "$fixture/scratch" -name 'aa.producer.stage-result' -type f -print -quit) + race_marker=$(/usr/bin/find "$fixture/scratch" -name 'package.aa.fake-producer-a' -type f -print -quit) [ -z "$race_marker" ] || break /bin/kill -0 "$race_pid" 2>/dev/null || break /bin/sleep 0.005 @@ -150,22 +152,53 @@ done if [ -z "$race_marker" ]; then /bin/kill "$race_pid" 2>/dev/null || : wait "$race_pid" 2>/dev/null || : + fail 'verified snapshot race marker' +fi +/usr/bin/printf '%s\n' 'project = mutable-garden' 'feature = wrong bytes' > "$fixture/target/source.txt" +race_marker='' +for _ in {1..12000}; do + race_marker=$(/usr/bin/find "$fixture/scratch" -name 'aa.producer.home' -type d -print -quit) + [ -z "$race_marker" ] || break + /bin/kill -0 "$race_pid" 2>/dev/null || break + /bin/sleep 0.005 +done +if [ -z "$race_marker" ]; then + /bin/kill "$race_pid" 2>/dev/null || : + wait "$race_pid" 2>/dev/null || : + /bin/cp "$race_source_saved" "$fixture/target/source.txt" fail 'immutable package race marker' fi /usr/bin/printf '%s\n' '#!/bin/bash' \ "/usr/bin/printf mutable > '$fixture/scratch/mutable-package-ran'" \ 'exit 99' > "$race_forge" /bin/chmod 0755 "$race_forge" +race_marker='' +for _ in {1..12000}; do + race_marker=$(/usr/bin/find "$fixture/scratch" -name 'aa.forge.stage-result' -type f -print -quit) + [ -z "$race_marker" ] || break + /bin/kill -0 "$race_pid" 2>/dev/null || break + /bin/sleep 0.005 +done +/bin/cp "$race_source_saved" "$fixture/target/source.txt" +if [ -z "$race_marker" ]; then + /bin/kill "$race_pid" 2>/dev/null || : + wait "$race_pid" 2>/dev/null || : + fail 'immutable source execution marker' +fi if ! wait "$race_pid"; then fail 'immutable package race run' fi [ ! -s "$race_err" ] && [ ! -e "$fixture/scratch/mutable-package-ran" ] && [ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || fail 'mutable package executed' +"$jq_bin" -e --slurpfile baseline "$output" \ + '[.cells[].projection] == [$baseline[0].cells[].projection]' "$race_out" >/dev/null || + fail 'source snapshot projection drift' /bin/cp "$race_saved" "$race_forge" /usr/bin/git -C "$fixture/cells/aa/assets" diff --quiet -- packages/forge-a.sh || fail 'package race restore' -pass 'mapped package mutation cannot change snapshot execution' +fixture_git -C "$fixture/target" diff --quiet -- source.txt || fail 'source race restore' +pass 'mapped package and source mutation cannot change snapshot execution' expect_failure() { local name=$1 From 2dd595e4e039a659536e1a3598c8b1588d9b628a Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 14:56:18 -0400 Subject: [PATCH 10/12] Snapshot validated fixture inputs --- adapter-tests/v1/runner.sh | 47 ++++++++++++----- .../test/portable-adapter-contracts.test.sh | 52 +++++++++++++++++-- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index 71bc16c..040c1d8 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -78,10 +78,21 @@ canonical() { cmp -s "$source" "$output" } +snapshot_json() { + local source=$1 + local snapshot=$2 + local check=$3 + [ -f "$source" ] && [ ! -L "$source" ] || return 1 + /bin/cp "$source" "$snapshot" || return 1 + /bin/chmod 0400 "$snapshot" || return 1 + canonical "$snapshot" "$check" +} + [ "$(sha_file "$inventory")" = 100edb973c586192683917bc4896121117ef397161263510e09f03a0b269bd4a ] || runner_error E_INVENTORY inventory_canonical="$run_tmp/inventory.canonical" canonical "$inventory" "$inventory_canonical" || runner_error E_INVENTORY +inventory=$inventory_canonical fixture_source="$fixture_root/target/source.txt" [ -f "$fixture_source" ] && [ ! -L "$fixture_source" ] || runner_error E_FIXTURE fixture_sha=$(sha_file "$fixture_source") @@ -116,6 +127,7 @@ if [ ! -f "$mapping" ] || [ -L "$mapping" ] || ! canonical "$mapping" "$mapping_canonical"; then runner_error E_MAPPING fi +mapping=$mapping_canonical "$jq_bin" -e --arg root "$fixture_root" ' . as $map | .version == 1 and (.repositories | length) == 13 and @@ -486,12 +498,26 @@ for cell in aa ab ba bb; do assets="$cell_root/assets" manifests="$cell_root/manifests" profiles="$cell_root/profile" + profile_input="$profiles/profiles/default.json" + profile="$run_tmp/profile.$cell.snapshot" + snapshot_json "$profile_input" "$profile" "$run_tmp/profile.$cell.canonical" || + runner_error E_INPUT resolved_input="$cell_root/resolved.json" resolved="$run_tmp/resolved.$cell.snapshot" - [ -f "$resolved_input" ] && [ ! -L "$resolved_input" ] || runner_error E_INPUT - /bin/cp "$resolved_input" "$resolved" || runner_error E_INPUT - /bin/chmod 0400 "$resolved" || runner_error E_INPUT - canonical "$resolved" "$run_tmp/resolved.$cell.canonical" || runner_error E_INPUT + snapshot_json "$resolved_input" "$resolved" "$run_tmp/resolved.$cell.canonical" || + runner_error E_INPUT + manifest_args=() + manifest_set="$run_tmp/manifest-set.$cell" + : > "$manifest_set" + for role in forge producer publisher reviewer verifier; do + manifest_input="$manifests/manifests/$role.json" + manifest_file="$run_tmp/manifest.$cell.$role.snapshot" + snapshot_json "$manifest_input" "$manifest_file" "$run_tmp/manifest.$cell.$role.canonical" || + runner_error E_INPUT + manifest_args+=("$manifest_file") + /bin/cat "$manifest_file" >> "$manifest_set" + done + manifest_set_sha=$(sha_file "$manifest_set") for mapped_repo in "$assets" "$manifests" "$profiles"; do verify_repo "$mapped_repo" || runner_error E_GIT done @@ -499,8 +525,8 @@ for cell in aa ab ba bb; do forge_variant=${cell#?} producer_package="fake.producer.$producer_variant" forge_package="fake.forge.$forge_variant" - producer_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="producer") | .package_ref' "$cell_root/profile/profiles/default.json") - forge_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="forge") | .package_ref' "$cell_root/profile/profiles/default.json") + producer_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="producer") | .package_ref' "$profile") + forge_ref=$("$jq_bin" -c '.body.bindings[] | select(.role=="forge") | .package_ref' "$profile") producer_resolved_ref=$("$jq_bin" -c '.body.bindings[] | select(.binding.role=="producer") | .binding.package_ref' "$resolved") forge_resolved_ref=$("$jq_bin" -c '.body.bindings[] | select(.binding.role=="forge") | .binding.package_ref' "$resolved") assets_commit=$("$jq_bin" -r '.revision.commit_id' <<< "$forge_ref") @@ -523,12 +549,8 @@ for cell in aa ab ba bb; do verify_package "$cell" fake.protocol-fault "$fault_ref" || runner_error E_PACKAGE [ "$cell" != aa ] || fault_executable=$VERIFIED_PACKAGE_SNAPSHOT verify_all_sources "$cell" "$resolved" || runner_error E_SOURCE - profile="$profiles/profiles/default.json" - manifest_args=() - for role in forge producer publisher reviewer verifier; do - manifest_file="$manifests/manifests/$role.json" + for manifest_file in "${manifest_args[@]}"; do "$core" validate-document "$manifest_file" >/dev/null 2>&1 || runner_error E_CORE - manifest_args+=("$manifest_file") done "$core" validate-document "$profile" >/dev/null 2>&1 || runner_error E_CORE "$core" validate-profile-set "$profile" "$resolved" "${manifest_args[@]}" \ @@ -663,12 +685,13 @@ for cell in aa ab ba bb; do --arg command projection -f "$contract" "$projection_context") "$jq_bin" -S -c -n --arg case_id "$cell_id" --argjson projection "$projection" \ --arg profile_sha256 "$(sha_file "$profile")" \ + --arg manifest_set_sha256 "$manifest_set_sha" \ --arg producer "$producer_package" --arg producer_digest "$(package_digest "$producer_package")" \ --arg forge "$forge_package" --arg forge_digest "$(package_digest "$forge_package")" ' {assertion_ids:["audit-projection","candidate-git","core-validation","environment-clean", "evidence-projection","gate-projection","outcome-projection","risk-projection","target-git"], case_id:$case_id,projection:$projection, - provenance:{profile_sha256:$profile_sha256, + provenance:{profile_sha256:$profile_sha256,manifest_set_sha256:$manifest_set_sha256, producer:{package_id:$producer,sha256:$producer_digest}, forge:{package_id:$forge,sha256:$forge_digest}}} ' >> "$cells" diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index 4b77a20..1dd1703 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -6,7 +6,7 @@ umask 077 test_root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) runner="$test_root/adapter-tests/v1/runner.sh" -inventory="$test_root/adapter-tests/v1/inventory.json" +inventory_source="$test_root/adapter-tests/v1/inventory.json" fixture_builder="$test_root/scripts/test/portable-profile-resolution-fixtures.sh" resolver_runtime="$test_root/resolver/v1/profile-resolve-runtime.sh" tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-adapter-contracts.XXXXXX") @@ -69,6 +69,8 @@ pass 'fixed resolver launcher and dependencies built' fixture="$tmp/garden-fixture" /bin/mkdir -m 700 "$fixture" "$fixture/cells" "$fixture/scratch" "$fixture/target" +inventory="$fixture/inventory.json" +/bin/cp "$inventory_source" "$inventory" /bin/cp "$test_root/adapter-tests/v1/fixture/source.txt" "$fixture/target/source.txt" git_home="$tmp/git-home" /bin/mkdir -m 700 "$git_home" @@ -100,6 +102,19 @@ for cell in aa ab ba bb; do done pass 'four honest profile variants resolved through the inactive resolver' +bad_request_payload="$tmp/bad-request-payload.json" +"$jq_bin" -S -c -n '{case_id:"matrix-aa",phase:"producer",protocol_version:1, + stage_request:{schema_version:2,kind:"stage_request",body:{resolved_profile_ref:{sha256:("a"*64)}}}, + payloads:[ + {data:"resolved",media_type:"application/json",payload_id:"resolved-profile",sha256:("b"*64)}, + {data:"source",media_type:"text/plain",payload_id:"source",sha256:("c"*64)}]}' > "$bad_request_payload" +if "$jq_bin" -L "$test_root/adapter-tests/v1" -L "$test_root/scripts/test" -e \ + --arg command request-envelope -f "$test_root/adapter-tests/v1/contract.jq" \ + "$bad_request_payload" >/dev/null; then + fail 'declared request payload digest mismatch accepted' +fi +pass 'declared request payload digest mismatch' + "$jq_bin" -S -c -n --arg root "$fixture" ' {version:1,repositories: ([{cell_id:"shared",repository_id:"fixture.target",root:($root+"/target")}] + @@ -138,8 +153,18 @@ race_err="$tmp/package-race.err" race_forge="$fixture/cells/aa/assets/packages/forge-a.sh" race_saved="$tmp/forge-a.saved" race_source_saved="$tmp/source-race.saved" +race_inventory_saved="$tmp/inventory-race.saved" +race_map_saved="$tmp/map-race.saved" +race_profile="$fixture/cells/aa/profile/profiles/default.json" +race_profile_saved="$tmp/profile-race.saved" +race_manifest="$fixture/cells/aa/manifests/manifests/forge.json" +race_manifest_saved="$tmp/manifest-race.saved" /bin/cp "$race_forge" "$race_saved" /bin/cp "$fixture/target/source.txt" "$race_source_saved" +/bin/cp "$inventory" "$race_inventory_saved" +/bin/cp "$fixture/repository-map.json" "$race_map_saved" +/bin/cp "$race_profile" "$race_profile_saved" +/bin/cp "$race_manifest" "$race_manifest_saved" PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$race_out" 2> "$race_err" & race_pid=$! race_marker='' @@ -172,6 +197,10 @@ fi "/usr/bin/printf mutable > '$fixture/scratch/mutable-package-ran'" \ 'exit 99' > "$race_forge" /bin/chmod 0755 "$race_forge" +/usr/bin/printf '%s\n' '{}' > "$inventory" +/usr/bin/printf '%s\n' '{}' > "$fixture/repository-map.json" +/usr/bin/printf '%s\n' '{}' > "$race_profile" +/usr/bin/printf '%s\n' '{}' > "$race_manifest" race_marker='' for _ in {1..12000}; do race_marker=$(/usr/bin/find "$fixture/scratch" -name 'aa.forge.stage-result' -type f -print -quit) @@ -180,6 +209,10 @@ for _ in {1..12000}; do /bin/sleep 0.005 done /bin/cp "$race_source_saved" "$fixture/target/source.txt" +/bin/cp "$race_inventory_saved" "$inventory" +/bin/cp "$race_map_saved" "$fixture/repository-map.json" +/bin/cp "$race_profile_saved" "$race_profile" +/bin/cp "$race_manifest_saved" "$race_manifest" if [ -z "$race_marker" ]; then /bin/kill "$race_pid" 2>/dev/null || : wait "$race_pid" 2>/dev/null || : @@ -191,14 +224,25 @@ fi [ ! -s "$race_err" ] && [ ! -e "$fixture/scratch/mutable-package-ran" ] && [ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || fail 'mutable package executed' -"$jq_bin" -e --slurpfile baseline "$output" \ - '[.cells[].projection] == [$baseline[0].cells[].projection]' "$race_out" >/dev/null || +"$jq_bin" -e --slurpfile baseline "$output" ' + .inventory_acceptance_ref == $baseline[0].inventory_acceptance_ref and + [.cells[] | {projection,provenance}] == + [$baseline[0].cells[] | {projection,provenance}] +' "$race_out" >/dev/null || fail 'source snapshot projection drift' /bin/cp "$race_saved" "$race_forge" /usr/bin/git -C "$fixture/cells/aa/assets" diff --quiet -- packages/forge-a.sh || fail 'package race restore' fixture_git -C "$fixture/target" diff --quiet -- source.txt || fail 'source race restore' -pass 'mapped package and source mutation cannot change snapshot execution' +/usr/bin/git -C "$fixture/cells/aa/profile" diff --quiet -- profiles/default.json || + fail 'profile race restore' +/usr/bin/git -C "$fixture/cells/aa/manifests" diff --quiet -- manifests/forge.json || + fail 'manifest race restore' +if ! cmp -s "$race_inventory_saved" "$inventory" || + ! cmp -s "$race_map_saved" "$fixture/repository-map.json"; then + fail 'metadata race restore' +fi +pass 'validated fixture mutations cannot change snapshot execution' expect_failure() { local name=$1 From 71024b5d60e630b5a54d3aa5598a494d4414ad4b Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 15:30:05 -0400 Subject: [PATCH 11/12] Close adapter launch signal window --- adapter-tests/v1/runner.sh | 30 +++++++++++++++---- .../test/portable-adapter-contracts.test.sh | 15 +++++++--- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index 040c1d8..df7a279 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -32,6 +32,7 @@ run_tmp=$(/usr/bin/mktemp -d "$fixture_root/scratch/run.XXXXXX") || runner_error cleanup() { /bin/rm -rf -- "$run_tmp"; } ACTIVE_CHILD_GROUP='' ACTIVE_CHILD_PID='' +PENDING_SIGNAL_STATUS=0 group_alive() { [[ "${1:-}" =~ ^[1-9][0-9]*$ ]] && kill -0 -- "-$1" 2>/dev/null } @@ -57,15 +58,20 @@ terminate_active_group() { } signal_exit() { local status=$1 - trap - EXIT HUP INT TERM + trap '' HUP INT TERM + trap - EXIT terminate_active_group cleanup exit "$status" } +record_pending_signal() { PENDING_SIGNAL_STATUS=$1; } +install_normal_signal_handlers() { + trap 'signal_exit 129' HUP + trap 'signal_exit 130' INT + trap 'signal_exit 143' TERM +} trap cleanup EXIT -trap 'signal_exit 129' HUP -trap 'signal_exit 130' INT -trap 'signal_exit 143' TERM +install_normal_signal_handlers /bin/mkdir -m 700 "$run_tmp/home" sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } @@ -335,10 +341,18 @@ run_child() { local diagnostic=$5 local child_home=$6 local dependency=${7:-} - local pid tick=0 + local pid tick=0 pending_status CHILD_STATUS=0 CHILD_ERROR='' /bin/mkdir -m 700 "$child_home" + if [ -n "$ACTIVE_CHILD_GROUP" ] || [ -n "$ACTIVE_CHILD_PID" ]; then + CHILD_ERROR=E_RUNTIME + return 1 + fi + PENDING_SIGNAL_STATUS=0 + trap 'record_pending_signal 129' HUP + trap 'record_pending_signal 130' INT + trap 'record_pending_signal 143' TERM set -m ( ulimit -t 2 -f 2048 -n 64 @@ -357,6 +371,12 @@ run_child() { ACTIVE_CHILD_GROUP=$pid ACTIVE_CHILD_PID=$pid set +m + install_normal_signal_handlers + pending_status=$PENDING_SIGNAL_STATUS + PENDING_SIGNAL_STATUS=0 + if [ "$pending_status" -ne 0 ]; then + signal_exit "$pending_status" + fi while kill -0 "$pid" 2>/dev/null; do tick=$((tick + 1)) if [ "$tick" -ge 20 ]; then diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index 1dd1703..0a196dd 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -383,7 +383,7 @@ PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$signal_out" 2> " signal_pid=$! signal_marker='' for _ in {1..6000}; do - signal_marker=$(/usr/bin/find "$fixture/scratch" -path '*/timeout.home' -type d -print -quit) + signal_marker=$(/usr/bin/find "$fixture/scratch" -name 'request-payload.1' -type f -print -quit) [ -z "$signal_marker" ] || break /bin/kill -0 "$signal_pid" 2>/dev/null || break /bin/sleep 0.02 @@ -391,13 +391,20 @@ done if [ -z "$signal_marker" ]; then /bin/kill "$signal_pid" 2>/dev/null || : wait "$signal_pid" 2>/dev/null || : - fail 'signal child marker' + fail 'signal launch marker' fi -/bin/kill -TERM "$signal_pid" +signal_sent=0 +for _ in {1..200}; do + /bin/kill -0 "$signal_pid" 2>/dev/null || break + /bin/kill -TERM "$signal_pid" 2>/dev/null || break + signal_sent=$((signal_sent + 1)) + /bin/sleep 0.005 +done signal_status=0 wait "$signal_pid" || signal_status=$? /bin/sleep 2.2 -[ "$signal_status" -eq 143 ] && [ ! -s "$signal_out" ] && [ ! -s "$signal_err" ] && +[ "$signal_sent" -gt 0 ] && [ "$signal_status" -eq 143 ] && + [ ! -s "$signal_out" ] && [ ! -s "$signal_err" ] && [ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || fail 'signal group cleanup' pass 'TERM stops adapter group and cleans scratch' From 7536643bbec4d98bd658ccb4040af8383082f0bf Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 15:49:11 -0400 Subject: [PATCH 12/12] Silence signal cleanup notices --- adapter-tests/v1/runner.sh | 1 + .../test/portable-adapter-contracts.test.sh | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index df7a279..0ffe8d7 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -60,6 +60,7 @@ signal_exit() { local status=$1 trap '' HUP INT TERM trap - EXIT + exec >/dev/null 2>&1 terminate_active_group cleanup exit "$status" diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index 0a196dd..e505c31 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -403,10 +403,33 @@ done signal_status=0 wait "$signal_pid" || signal_status=$? /bin/sleep 2.2 -[ "$signal_sent" -gt 0 ] && [ "$signal_status" -eq 143 ] && - [ ! -s "$signal_out" ] && [ ! -s "$signal_err" ] && - [ -z "$(find "$fixture/scratch" -mindepth 1 -print -quit)" ] || +signal_stdout_bytes=$(/usr/bin/wc -c < "$signal_out" | /usr/bin/tr -d ' ') +signal_stderr_bytes=$(/usr/bin/wc -c < "$signal_err" | /usr/bin/tr -d ' ') +signal_stdout_class=empty +signal_stderr_class=empty +[ "$signal_stdout_bytes" -eq 0 ] || signal_stdout_class=other +if [ "$signal_stderr_bytes" -ne 0 ]; then + if /usr/bin/grep -Eq '(^|[[:space:]])(Terminated|Killed|Hangup|Interrupt|Done)(:|[[:space:]]|$)' \ + "$signal_err"; then + signal_stderr_class=job-control + else + signal_stderr_class=other + fi +fi +signal_scratch_count=$(/usr/bin/find "$fixture/scratch" -mindepth 1 -print | /usr/bin/wc -l | /usr/bin/tr -d ' ') +signal_survivor=absent +if /usr/bin/find "$fixture/scratch" \( -name timeout-survived -o -name descendant-survived \) \ + -print -quit | /usr/bin/grep -q .; then + signal_survivor=present +fi +if [ "$signal_sent" -le 0 ] || [ "$signal_status" -ne 143 ] || + [ "$signal_stdout_bytes" -ne 0 ] || [ "$signal_stderr_bytes" -ne 0 ] || + [ "$signal_scratch_count" -ne 0 ] || [ "$signal_survivor" != absent ]; then + /usr/bin/printf 'signal-debug status=%s sent=%s stdout_bytes=%s stdout_class=%s stderr_bytes=%s stderr_class=%s scratch_entries=%s survivor=%s\n' \ + "$signal_status" "$signal_sent" "$signal_stdout_bytes" "$signal_stdout_class" \ + "$signal_stderr_bytes" "$signal_stderr_class" "$signal_scratch_count" "$signal_survivor" >&2 fail 'signal group cleanup' +fi pass 'TERM stops adapter group and cleans scratch' source_saved="$tmp/source.saved"