716 lines
29 KiB
Bash
Executable File
716 lines
29 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Regression tests for the fail-closed schema-v4-only absence gate.
|
|
set -euo pipefail
|
|
|
|
project_root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
gate="$project_root/scripts/verify-schema-v3-only.sh"
|
|
gate_bash="${BASH:-bash}"
|
|
sandbox="$(mktemp -d "${TMPDIR:-/tmp}/thoth-v3-gate-test.XXXXXX")"
|
|
fixture="$sandbox/fixture repository"
|
|
output="$sandbox/output"
|
|
real_git="$(command -v git)"
|
|
canonical_schema="$project_root/backend/dist/workspaces/schema.js"
|
|
canonical_server="$project_root/backend/dist/server.js"
|
|
canonical_schema_checksum="$(cksum <"$canonical_schema")"
|
|
canonical_server_checksum="$(cksum <"$canonical_server")"
|
|
cleanup() {
|
|
rm -rf "$sandbox"
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
fail() {
|
|
echo "FAIL: $*" >&2
|
|
[[ ! -f "$output" ]] || cat "$output" >&2
|
|
exit 1
|
|
}
|
|
|
|
write_fixture_descriptor() {
|
|
local path="$1" workspace_key="${2:-workspace:}" schema_key="${3:- schema_version: 4}"
|
|
printf '%s\n' "$workspace_key" "$schema_key" >"$path"
|
|
cat >>"$path" <<'YAML'
|
|
id: fixture-workspace
|
|
name: Fixture Workspace
|
|
language: en
|
|
dwh:
|
|
engine: postgres
|
|
database: warehouse
|
|
schema: public
|
|
supported_transports: [postgres_direct]
|
|
YAML
|
|
}
|
|
|
|
seed_fixture() {
|
|
rm -rf "$fixture"
|
|
mkdir -p \
|
|
"$fixture/backend/src/nested dir" \
|
|
"$fixture/backend/scripts" \
|
|
"$fixture/frontend/src/api" \
|
|
"$fixture/deploy/workspaces" \
|
|
"$fixture/scripts/fixtures"
|
|
"$real_git" -C "$fixture" init -q
|
|
"$real_git" -C "$fixture" config user.email fixture@example.invalid
|
|
"$real_git" -C "$fixture" config user.name Fixture
|
|
printf '%s\n' 'export const schemaVersion = 4;' >"$fixture/backend/src/server.ts"
|
|
printf '%s\n' 'export const spaced = true;' >"$fixture/backend/src/nested dir/file name.ts"
|
|
newline_path="$fixture/backend/src/line
|
|
break.ts"
|
|
printf '%s\n' 'export const newline = true;' >"$newline_path"
|
|
printf '%s\n' 'export const currentWorkspace = true;' >"$fixture/frontend/src/api/workspaces.ts"
|
|
printf '%s\n' 'export const productionCheck = true;' >"$fixture/backend/scripts/runtime-check.mjs"
|
|
write_fixture_descriptor "$fixture/deploy/workspaces/example.yaml"
|
|
write_fixture_descriptor "$fixture/deploy/workspaces/psd.yaml.example"
|
|
printf '%s\n' '#!/usr/bin/env bash' 'echo operator-smoke' >"$fixture/scripts/workspace-registry-smoke.sh"
|
|
write_fixture_descriptor "$fixture/scripts/fixtures/workspace-registry-smoke.yaml"
|
|
write_fixture_descriptor "$fixture/scripts/fixtures/workspace-registry-windows.yaml"
|
|
printf '%s\n' 'Write-Output "schema v4"' >"$fixture/scripts/test-windows-clone-contract.ps1"
|
|
"$real_git" -C "$fixture" add .
|
|
"$real_git" -C "$fixture" commit -qm seed
|
|
}
|
|
|
|
commit_fixture() {
|
|
"$real_git" -C "$fixture" add .
|
|
"$real_git" -C "$fixture" commit -qm "$1"
|
|
}
|
|
|
|
run_gate() {
|
|
set +e
|
|
"$gate_bash" "$gate" --root "$fixture" --runtime-only >"$output" 2>&1
|
|
gate_status=$?
|
|
set -e
|
|
}
|
|
|
|
expect_pass() {
|
|
local label="$1"
|
|
run_gate
|
|
[[ $gate_status -eq 0 ]] || fail "$label: expected pass, got status $gate_status"
|
|
}
|
|
|
|
expect_rejected() {
|
|
local label="$1" expected="$2"
|
|
run_gate
|
|
[[ $gate_status -eq 1 ]] || fail "$label: expected rejection status 1, got $gate_status"
|
|
grep -Fq -- "$expected" "$output" || fail "$label: rejection did not identify $expected"
|
|
}
|
|
|
|
# Clean tracked live paths, including spaces and an embedded newline, are NUL-safe.
|
|
seed_fixture
|
|
expect_pass "clean runtime fixture"
|
|
|
|
seed_fixture
|
|
mkfifo "$fixture/scripts/runtime-fifo"
|
|
expect_rejected "runtime FIFO" "scripts/runtime-fifo"
|
|
|
|
set +e
|
|
"$gate_bash" "$gate" --help >"$output" 2>&1
|
|
help_status=$?
|
|
set -e
|
|
[[ $help_status -eq 0 ]] || fail "gate help failed with status $help_status"
|
|
grep -Fq 'Node' "$output" || fail "gate help omits the runtime-only Node dependency"
|
|
grep -Fq 'backend/dist/workspaces/schema.js' "$output" \
|
|
|| fail "gate help omits the runtime-only compiled schema dependency"
|
|
grep -Fq 'scripts/verify-schema-v3-only-release.sh' "$output" \
|
|
|| fail "gate help omits the durable release entry point"
|
|
grep -Fq 'npm ci' "$output" || fail "gate help omits lockfile install order"
|
|
|
|
|
|
# Prescribed symbols and migration markers are case-insensitive substrings.
|
|
seed_fixture
|
|
printf '%s\n' 'export type WorkspaceV2Compat = unknown;' >"$fixture/backend/src/legacy.ts"
|
|
commit_fixture backend-symbol
|
|
expect_rejected "backend prescribed derivative symbol" "backend/src/legacy.ts"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'export type LegacyWorkspace = unknown;' >"$fixture/backend/src/legacy-workspace.ts"
|
|
commit_fixture legacy-workspace-symbol
|
|
expect_rejected "exact LegacyWorkspace symbol" "backend/src/legacy-workspace.ts"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'export const status = "MIGRATION_REQUIRED";' >"$fixture/backend/src/status.ts"
|
|
commit_fixture backend-case-insensitive-marker
|
|
expect_rejected "case-insensitive marker" "backend/src/status.ts"
|
|
|
|
# Every forbidden category is applied to every recursive policy root without extension filters.
|
|
policy_roots=(backend/src frontend/src backend/scripts scripts)
|
|
policy_extensions=(ts py js yaml.example)
|
|
policy_names=(WorkspaceV2 LegacyWorkspace migration_required 'revision.state')
|
|
for category_index in 0 1 2 3; do
|
|
for root_index in 0 1 2 3; do
|
|
seed_fixture
|
|
matrix_root="${policy_roots[$root_index]}"
|
|
matrix_extension="${policy_extensions[$root_index]}"
|
|
matrix_path="$matrix_root/matrix-$category_index.$matrix_extension"
|
|
mkdir -p "${fixture:?}/$matrix_root"
|
|
printf '%s\n' "${policy_names[$category_index]}" >"$fixture/$matrix_path"
|
|
commit_fixture "policy-matrix-$category_index-$root_index"
|
|
expect_rejected "policy category $category_index root $matrix_root" "$matrix_path"
|
|
done
|
|
done
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'export const status = "migration_required";' >"$fixture/frontend/src/api/workspaces.ts"
|
|
commit_fixture frontend-marker
|
|
expect_rejected "frontend migration status" "frontend/src/api/workspaces.ts"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'export const blocked = record.revision.state !== "operational";' >"$fixture/frontend/src/api/workspaces.ts"
|
|
commit_fixture frontend-revision-state
|
|
expect_rejected "frontend revision state gate" "frontend/src/api/workspaces.ts"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/scripts/nested/production"
|
|
printf '%s\n' 'const helper = "migrate-v2-qdrant.js";' >"$fixture/backend/scripts/nested/production/runtime.mjs"
|
|
commit_fixture nested-mjs
|
|
expect_rejected "nested backend production script" "backend/scripts/nested/production/runtime.mjs"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'const historical = entry.revision.state;' >"$fixture/backend/scripts/runtime-check.mjs"
|
|
commit_fixture backend-historical-state
|
|
expect_rejected "backend historical revision state" "backend/scripts/runtime-check.mjs"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'node backend/dist/workspaces/MIGRATE-LEGACY.js' >"$fixture/scripts/workspace-registry-smoke.sh"
|
|
commit_fixture operator-migrator
|
|
expect_rejected "operator smoke migrator" "scripts/workspace-registry-smoke.sh"
|
|
|
|
# Workspace YAML embedded in live non-test deployment scripts is validated structurally.
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
cat >"$fixture/scripts/operators/heredoc-smoke.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
cat <<'YAML'
|
|
workspace:
|
|
schema_version: 2
|
|
YAML
|
|
EOF
|
|
commit_fixture script-heredoc-v2
|
|
expect_rejected "deployment-script workspace schema" "scripts/operators/heredoc-smoke.sh"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
cat >"$fixture/scripts/operators/quoted-heredoc-smoke.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
cat <<'YAML'
|
|
"workspace" :
|
|
'schema_version' : 0x2
|
|
YAML
|
|
EOF
|
|
commit_fixture script-quoted-heredoc
|
|
expect_rejected "quoted deployment-script workspace schema" "scripts/operators/quoted-heredoc-smoke.sh"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
{
|
|
printf '%s\n' '#!/usr/bin/env bash' "cat <<'---'"
|
|
printf '%s \n' '---'
|
|
printf '%s\n' 'workspace:' ' schema_version: 2' '---'
|
|
} >"$fixture/scripts/operators/exact-close-smoke.sh"
|
|
"$gate_bash" -n "$fixture/scripts/operators/exact-close-smoke.sh"
|
|
commit_fixture script-exact-heredoc-close
|
|
expect_rejected "heredoc false close with trailing blanks" "scripts/operators/exact-close-smoke.sh"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
cat >"$fixture/scripts/operators/double-quoted-backslash-smoke.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
cat <<"\---"
|
|
---
|
|
workspace:
|
|
schema_version: 2
|
|
\---
|
|
EOF
|
|
"$gate_bash" -n "$fixture/scripts/operators/double-quoted-backslash-smoke.sh"
|
|
reviewer_output="$("$gate_bash" "$fixture/scripts/operators/double-quoted-backslash-smoke.sh")"
|
|
printf '%s\n' "$reviewer_output" | grep -F 'schema_version: 2' >/dev/null || fail "double-quoted backslash reviewer fixture did not execute with the Bash delimiter"
|
|
commit_fixture script-double-quoted-backslash
|
|
expect_rejected "double-quoted non-special backslash delimiter" "scripts/operators/double-quoted-backslash-smoke.sh"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
cat >"$fixture/scripts/operators/split-operator-smoke.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
cat <\
|
|
<'YAML'
|
|
workspace:
|
|
schema_version: 2
|
|
YAML
|
|
EOF
|
|
"$gate_bash" -n "$fixture/scripts/operators/split-operator-smoke.sh"
|
|
reviewer_output="$("$gate_bash" "$fixture/scripts/operators/split-operator-smoke.sh")"
|
|
printf '%s\n' "$reviewer_output" | grep -F 'schema_version: 2' >/dev/null || fail "split-operator reviewer fixture did not execute as a Bash heredoc"
|
|
commit_fixture script-split-heredoc-operator
|
|
expect_rejected "split heredoc operator continuation" "scripts/operators/split-operator-smoke.sh"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
cat >"$fixture/scripts/operators/evidence-bundle-smoke.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
cat <<'YAML'
|
|
evidence:
|
|
source: bundle
|
|
schema_version: 2
|
|
YAML
|
|
EOF
|
|
"$gate_bash" -n "$fixture/scripts/operators/evidence-bundle-smoke.sh"
|
|
commit_fixture script-evidence-bundle
|
|
expect_pass "evidence bundle without workspace mapping"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
cat >"$fixture/scripts/operators/powershell-comment-smoke.ps1" <<'EOF'
|
|
# harmless PowerShell comment \
|
|
$workspace = @'
|
|
workspace:
|
|
schema_version: 2
|
|
'@
|
|
EOF
|
|
commit_fixture powershell-comment-v2
|
|
expect_rejected "PowerShell comment backslash before v2 here-string" "scripts/operators/powershell-comment-smoke.ps1"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
cat >"$fixture/scripts/operators/powershell-valid-smoke.ps1" <<'EOF'
|
|
$workspace = @'
|
|
EOF
|
|
cat "$fixture/deploy/workspaces/example.yaml" >>"$fixture/scripts/operators/powershell-valid-smoke.ps1"
|
|
cat >>"$fixture/scripts/operators/powershell-valid-smoke.ps1" <<'EOF'
|
|
'@
|
|
$bundle = @'
|
|
evidence:
|
|
source: bundle
|
|
schema_version: 2
|
|
'@
|
|
EOF
|
|
commit_fixture powershell-v3-and-bundle
|
|
expect_rejected "PowerShell embedded v3 descriptor" "scripts/operators/powershell-valid-smoke.ps1"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/operators"
|
|
cat >"$fixture/scripts/operators/powershell-bundle-smoke.ps1" <<'EOF'
|
|
$bundle = @'
|
|
evidence:
|
|
source: bundle
|
|
schema_version: 2
|
|
'@
|
|
EOF
|
|
commit_fixture powershell-bundle
|
|
expect_pass "PowerShell non-workspace bundle"
|
|
|
|
# Quoted/space/indented YAML keys are real mappings; v4 passes and non-v4 fails.
|
|
seed_fixture
|
|
write_fixture_descriptor \
|
|
"$fixture/deploy/workspaces/example.yaml" \
|
|
'"workspace" :' \
|
|
" 'schema_version' : 4"
|
|
commit_fixture quoted-yaml-v4
|
|
expect_pass "quoted and indented workspace v4"
|
|
|
|
seed_fixture
|
|
cat >"$fixture/deploy/workspaces/example.yaml" <<'EOF'
|
|
'workspace' :
|
|
"schema_version" : 02
|
|
EOF
|
|
commit_fixture quoted-yaml-noncanonical
|
|
expect_rejected "quoted workspace noncanonical schema" "deploy/workspaces/example.yaml"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'workspace: { schema_version: 4 }' >"$fixture/deploy/workspaces/example.yaml"
|
|
commit_fixture inline-workspace
|
|
expect_rejected "inline workspace mapping" "deploy/workspaces/example.yaml"
|
|
|
|
# Deleted migrator basenames are rejected case-insensitively at any live nesting depth.
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/src/deep/nested"
|
|
printf '%s\n' 'export const otherwiseClean = true;' >"$fixture/backend/src/deep/nested/Migrate-Legacy.ts"
|
|
commit_fixture deleted-case-path
|
|
expect_rejected "case-insensitive deleted basename" "backend/src/deep/nested/Migrate-Legacy.ts"
|
|
|
|
# Live filesystem/index trust is fail-closed, including ignored and newline-bearing files.
|
|
seed_fixture
|
|
printf '%s\n' 'export const changed = true;' >"$fixture/backend/src/server.ts"
|
|
expect_rejected "modified tracked source" "backend/src/server.ts"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'export const staged = true;' >"$fixture/backend/src/server.ts"
|
|
"$real_git" -C "$fixture" add backend/src/server.ts
|
|
expect_rejected "staged tracked source" "backend/src/server.ts"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'export const untracked = true;' >"$fixture/backend/src/untracked.ts"
|
|
expect_rejected "untracked production source" "backend/src/untracked.ts"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'backend/src/ignored.ts' >"$fixture/.gitignore"
|
|
commit_fixture ignore-rule
|
|
printf '%s\n' 'export const ignored = true;' >"$fixture/backend/src/ignored.ts"
|
|
expect_rejected "ignored production source" "backend/src/ignored.ts"
|
|
|
|
seed_fixture
|
|
untracked_newline="$fixture/backend/src/untracked
|
|
production.ts"
|
|
printf '%s\n' 'export const untrackedNewline = true;' >"$untracked_newline"
|
|
expect_rejected "newline-bearing untracked source" "backend/src/untracked\nproduction.ts"
|
|
|
|
seed_fixture
|
|
ln -s server.ts "$fixture/backend/src/tracked-link.ts"
|
|
commit_fixture tracked-symlink
|
|
expect_rejected "tracked live symlink" "backend/src/tracked-link.ts"
|
|
|
|
# Generic non-workspace state/version formats remain allowed on live paths.
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/scripts/nested" "$fixture/scripts/operators"
|
|
printf '%s\n' \
|
|
'const first = entry.state;' \
|
|
'const second = lease.state === "operational";' \
|
|
'const third = job.state;' >"$fixture/backend/scripts/nested/generic-state.mjs"
|
|
printf '%s\n' '#!/usr/bin/env bash' 'bundle_schema_version=1' >"$fixture/scripts/operators/bundle-smoke.sh"
|
|
commit_fixture unrelated-state-version
|
|
expect_pass "unrelated entry lease job state and bundle version"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'const stale = selectedWorkspace?.state;' >"$fixture/backend/scripts/runtime-check.mjs"
|
|
commit_fixture workspace-state-gate
|
|
expect_rejected "selected workspace revision state" "backend/scripts/runtime-check.mjs"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'const revision = { revision: { id: "x", state: "operational" } };' >"$fixture/backend/scripts/runtime-check.mjs"
|
|
commit_fixture revision-object-state
|
|
expect_rejected "bounded revision object state" "backend/scripts/runtime-check.mjs"
|
|
|
|
# A top-level workspace descriptor has one exact schema_version: 4 key.
|
|
for malformed in schema-v1 schema-v2 schema-v3 leading-zero hexadecimal multiline duplicate; do
|
|
seed_fixture
|
|
case "$malformed" in
|
|
schema-v1)
|
|
printf '%s\n' 'workspace:' ' schema_version: 1' >"$fixture/deploy/workspaces/example.yaml"
|
|
;;
|
|
schema-v2)
|
|
printf '%s\n' 'workspace:' ' schema_version: 2' >"$fixture/deploy/workspaces/example.yaml"
|
|
;;
|
|
schema-v3)
|
|
printf '%s\n' 'workspace:' ' schema_version: 3' >"$fixture/deploy/workspaces/example.yaml"
|
|
;;
|
|
leading-zero)
|
|
printf '%s\n' 'workspace:' ' schema_version: 04' >"$fixture/deploy/workspaces/example.yaml"
|
|
;;
|
|
hexadecimal)
|
|
printf '%s\n' 'workspace:' ' schema_version: 0x4' >"$fixture/deploy/workspaces/example.yaml"
|
|
;;
|
|
multiline)
|
|
printf '%s\n' 'workspace:' ' schema_version: >' ' 4' >"$fixture/deploy/workspaces/example.yaml"
|
|
;;
|
|
duplicate)
|
|
printf '%s\n' 'workspace:' ' schema_version: 4' ' schema_version: 4' >"$fixture/deploy/workspaces/example.yaml"
|
|
;;
|
|
esac
|
|
commit_fixture "yaml-$malformed"
|
|
expect_rejected "malformed workspace schema $malformed" "deploy/workspaces/example.yaml"
|
|
done
|
|
|
|
# YAML that is not a top-level workspace descriptor is not a generic schema-version target.
|
|
seed_fixture
|
|
printf '%s\n' 'bundle_schema_version: 1' >"$fixture/deploy/maintenance-job.yaml"
|
|
commit_fixture unrelated-yaml-version
|
|
expect_pass "unrelated YAML schema version"
|
|
|
|
# Explicit deleted source paths and case-insensitive deleted basenames cannot hide as directories.
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/src/workspaces/migrate-legacy.ts"
|
|
expect_rejected "deleted source directory" "backend/src/workspaces/migrate-legacy.ts"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/src/deep/Migrate-V2-Qdrant.ts"
|
|
expect_rejected "case-insensitive deleted directory" "backend/src/deep/Migrate-V2-Qdrant.ts"
|
|
|
|
# Test and fixture naming never bypasses trust or content policy.
|
|
seed_fixture
|
|
mkdir -p "$fixture/frontend/src/test"
|
|
ln -s ../api/workspaces.ts "$fixture/frontend/src/test/negative.test.ts"
|
|
commit_fixture tracked-test-symlink
|
|
expect_rejected "tracked test symlink" "frontend/src/test/negative.test.ts"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/frontend/src/test"
|
|
printf '%s\n' 'export const clean = true;' >"$fixture/frontend/src/test/changed.test.ts"
|
|
commit_fixture tracked-test-dirty
|
|
printf '%s\n' 'export const changed = true;' >"$fixture/frontend/src/test/changed.test.ts"
|
|
expect_rejected "dirty test file" "frontend/src/test/changed.test.ts"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/frontend/src/test"
|
|
printf '%s\n' 'migration_required' >"$fixture/frontend/src/test/negative.test.ts"
|
|
commit_fixture tracked-test-forbidden
|
|
expect_rejected "forbidden marker in test path" "frontend/src/test/negative.test.ts"
|
|
|
|
# Explicitly live test-named Windows and workspace fixtures are not excluded.
|
|
seed_fixture
|
|
printf '%s\n' 'Write-Output "MIGRATE-LEGACY"' >"$fixture/scripts/test-windows-clone-contract.ps1"
|
|
commit_fixture live-windows-exception
|
|
expect_rejected "live Windows fixture exception" "scripts/test-windows-clone-contract.ps1"
|
|
|
|
seed_fixture
|
|
printf '%s\n' 'workspace:' ' schema_version: 2' >"$fixture/scripts/fixtures/workspace-registry-smoke.yaml"
|
|
commit_fixture live-workspace-fixture
|
|
expect_rejected "live workspace fixture exception" "scripts/fixtures/workspace-registry-smoke.yaml"
|
|
|
|
seed_fixture
|
|
write_fixture_descriptor \
|
|
"$fixture/scripts/fixtures/workspace-registry-future.yaml" \
|
|
'workspace:' \
|
|
' schema_version: 2'
|
|
commit_fixture future-workspace-family
|
|
expect_rejected "future workspace fixture family" "scripts/fixtures/workspace-registry-future.yaml"
|
|
|
|
# Only exact path+category policy literals are allowed.
|
|
seed_fixture
|
|
printf '%s\n' 'migration_required migrate-legacy WorkspaceV2 revision.state' >"$fixture/scripts/test-verify-schema-v3-only.sh"
|
|
commit_fixture exact-policy-allowlist
|
|
expect_pass "exact self-test policy allowlist"
|
|
|
|
# Diagnostic paths are shell-escaped so a newline cannot forge another log line.
|
|
seed_fixture
|
|
newline_spoof="$fixture/backend/src/spoof
|
|
forged.py"
|
|
printf '%s\n' harmless >"$newline_spoof"
|
|
run_gate
|
|
[[ $gate_status -eq 1 ]] || fail "newline untracked path was not rejected"
|
|
grep -Fq 'backend/src/spoof\nforged.py' "$output" \
|
|
|| fail "newline path diagnostic was not escaped on one line"
|
|
|
|
# Scanner operational errors are propagated, not converted into absence.
|
|
seed_fixture
|
|
fake_bin="$sandbox/fake-bin"
|
|
mkdir -p "$fake_bin"
|
|
cat >"$fake_bin/git" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
for argument in "$@"; do
|
|
if [[ "$argument" == grep ]]; then
|
|
echo "simulated git grep failure" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
exec "$REAL_GIT" "$@"
|
|
EOF
|
|
chmod +x "$fake_bin/git"
|
|
set +e
|
|
PATH="$fake_bin:$PATH" REAL_GIT="$real_git" "$gate_bash" "$gate" --root "$fixture" --runtime-only >"$output" 2>&1
|
|
gate_status=$?
|
|
set -e
|
|
[[ $gate_status -eq 2 ]] || fail "git grep status 2 was masked as $gate_status"
|
|
grep -Fq 'simulated git grep failure' "$output" || fail "git grep failure diagnostics were lost"
|
|
|
|
# Foreign roots are test-only and can never select fixture code for a full check.
|
|
set +e
|
|
"$gate_bash" "$gate" --root "$fixture" >"$output" 2>&1
|
|
gate_status=$?
|
|
set -e
|
|
[[ $gate_status -ne 0 ]] || fail "foreign-root full mode unexpectedly passed"
|
|
grep -Fq -- '--root is available only with --runtime-only or --bootstrap-trust-only' "$output" \
|
|
|| fail "foreign-root full rejection did not report the trust boundary"
|
|
|
|
# Prescribed symbols are forbidden as case-insensitive substrings, including derivatives.
|
|
derivative_names=(WorkspaceV2Compat wOrKsPaCeV2 deprecatedV2Descriptor WRITEMIGRATEDWORKSPACE LegacyWorkspaceAdapter migrateLegacyWorkspaceCompat)
|
|
for derivative in "${derivative_names[@]}"; do
|
|
for matrix_root in "${policy_roots[@]}"; do
|
|
seed_fixture
|
|
matrix_path="$matrix_root/derivative.ts"
|
|
printf '%s\n' "$derivative" >"$fixture/$matrix_path"
|
|
commit_fixture "derivative-$derivative-${matrix_root//\//-}"
|
|
expect_rejected "derivative $derivative in $matrix_root" "$matrix_path"
|
|
done
|
|
done
|
|
|
|
# Mixed/all-lower legacy spellings fail; the approved lower-camel identifier remains valid.
|
|
for spelling in legacyworkspace LeGaCyWoRkSpAcE; do
|
|
seed_fixture
|
|
printf '%s\n' "$spelling" >"$fixture/backend/src/legacy-variant.ts"
|
|
commit_fixture legacy-spelling
|
|
expect_rejected "legacy spelling $spelling" "backend/src/legacy-variant.ts"
|
|
done
|
|
seed_fixture
|
|
printf '%s\n' 'const legacyWorkspacePath = current;' >"$fixture/backend/src/legacy-allowed.ts"
|
|
commit_fixture lower-camel-legacy
|
|
expect_pass "approved lower-camel legacy identifier"
|
|
|
|
# Full-text revision scanning covers bracket access and newline-separated dot access.
|
|
for revision_source in \
|
|
'selectedWorkspace["state"]' \
|
|
$'workspaceRevision\n .state'; do
|
|
seed_fixture
|
|
printf '%s\n' "$revision_source" >"$fixture/frontend/src/revision-variant.ts"
|
|
commit_fixture revision-variant
|
|
expect_rejected "revision structural variant" "frontend/src/revision-variant.ts"
|
|
done
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/src/workspaces"
|
|
printf '%s\n' 'if (revision.state !== "operational") return;' >"$fixture/backend/src/workspaces/registry.ts"
|
|
commit_fixture historical-decoder
|
|
expect_pass "single exact historical decoder"
|
|
printf '%s\n' 'if (revision["state"] === "retired") return;' >>"$fixture/backend/src/workspaces/registry.ts"
|
|
commit_fixture extra-historical-branch
|
|
expect_rejected "extra registry revision branch" "backend/src/workspaces/registry.ts"
|
|
|
|
# Binary/NUL policy files are decoded and rejected rather than skipped by git grep -I.
|
|
seed_fixture
|
|
printf 'WorkspaceV2\0hidden\n' >"$fixture/backend/src/binary.ts"
|
|
commit_fixture nul-policy
|
|
expect_rejected "NUL policy file" "backend/src/binary.ts"
|
|
|
|
# Workspace fixture-family discovery is recursive by basename.
|
|
seed_fixture
|
|
mkdir -p "$fixture/scripts/fixtures/nested/deeper"
|
|
write_fixture_descriptor "$fixture/scripts/fixtures/nested/deeper/workspace-registry-nested.yaml" workspace: ' schema_version: 2'
|
|
commit_fixture nested-workspace-fixture
|
|
expect_rejected "nested workspace fixture" "scripts/fixtures/nested/deeper/workspace-registry-nested.yaml"
|
|
|
|
# Python bytecode is disabled before pre-gate docs, and release dry-run starts with bootstrap trust.
|
|
grep -Fq 'PYTHONDONTWRITEBYTECODE: "1"' "$project_root/.github/workflows/deployment.yml" \
|
|
|| fail "workflow does not disable Python bytecode"
|
|
grep -Fq 'python3 -m venv .venv' "$project_root/.github/workflows/deployment.yml" \
|
|
|| fail "workflow does not install the real harness CLI before backend integration tests"
|
|
grep -Fq '.venv/bin/python -m pip install -e .' "$project_root/.github/workflows/deployment.yml" \
|
|
|| fail "workflow does not install the harness package into its canonical virtual environment"
|
|
grep -Fq 'export PYTHONDONTWRITEBYTECODE=1' "$project_root/scripts/verify-schema-v3-only-release.sh" \
|
|
|| fail "release wrapper does not disable Python bytecode"
|
|
release_plan="$($gate_bash "$project_root/scripts/verify-schema-v3-only-release.sh" --dry-run)"
|
|
first_command="$(printf '%s\n' "$release_plan" | sed -n '1p')"
|
|
user_config_command="$(printf '%s\n' "$release_plan" | sed -n '2p')"
|
|
global_config_command="$(printf '%s\n' "$release_plan" | sed -n '3p')"
|
|
bootstrap_command="$(printf '%s\n' "$release_plan" | sed -n '4p')"
|
|
[[ "$first_command" == 'export PYTHONDONTWRITEBYTECODE=1' ]] \
|
|
|| fail "release dry-run does not print the Python bytecode export"
|
|
[[ "$user_config_command" == 'export NPM_CONFIG_USERCONFIG=<private-empty-user-config>' ]] \
|
|
|| fail "release dry-run does not isolate npm user configuration"
|
|
[[ "$global_config_command" == 'export NPM_CONFIG_GLOBALCONFIG=<private-empty-global-config>' ]] \
|
|
|| fail "release dry-run does not isolate npm global configuration"
|
|
[[ "$bootstrap_command" == '/bin/bash scripts/verify-schema-v3-only.sh --bootstrap-trust-only' ]] \
|
|
|| fail "release plan does not bootstrap trust before npm"
|
|
printf '%s\n' "$release_plan" | grep -Fq '(cd backend && npm ci --ignore-scripts)' \
|
|
|| fail "release plan does not disable npm lifecycle scripts"
|
|
py_fixture="$sandbox/python-bytecode"
|
|
mkdir -p "$py_fixture/scripts"
|
|
printf '%s\n' 'value = 3' >"$py_fixture/scripts/module.py"
|
|
PYTHONPATH="$py_fixture" PYTHONDONTWRITEBYTECODE=1 python3 -c 'import scripts.module'
|
|
[[ ! -e "$py_fixture/scripts/__pycache__" ]] || fail "pre-gate Python created ignored bytecode"
|
|
|
|
seed_bootstrap_fixture() {
|
|
seed_fixture
|
|
mkdir -p "$fixture/.github/workflows"
|
|
for required in \
|
|
backend/package.json backend/package-lock.json \
|
|
backend/scripts/verify-workspace-descriptor-files.mjs \
|
|
backend/scripts/verify-workspace-descriptor-files.test.mjs \
|
|
backend/scripts/revision-state-policy.mjs \
|
|
backend/scripts/revision-state-policy.test.mjs \
|
|
backend/scripts/bash-heredoc.mjs \
|
|
backend/scripts/revision_state_policy.py \
|
|
backend/scripts/test_revision_state_policy.py \
|
|
scripts/verify-schema-v3-only.sh scripts/test-verify-schema-v3-only.sh \
|
|
scripts/verify-schema-v3-only-release.sh scripts/workspace_descriptor_doc_contract.py \
|
|
.github/workflows/deployment.yml; do
|
|
mkdir -p "$fixture/${required%/*}"
|
|
cp "$project_root/$required" "$fixture/$required"
|
|
done
|
|
"$real_git" -C "$fixture" add .
|
|
"$real_git" -C "$fixture" commit -qm bootstrap-files
|
|
}
|
|
assert_release_stops_before_npm() {
|
|
local label="$1"
|
|
fake_lifecycle="$sandbox/fake-lifecycle"
|
|
mkdir -p "$fake_lifecycle"
|
|
cat >"$fake_lifecycle/npm" <<EOF
|
|
#!/usr/bin/env bash
|
|
echo invoked >>"$sandbox/npm-invoked"
|
|
exit 99
|
|
EOF
|
|
chmod +x "$fake_lifecycle/npm"
|
|
rm -f "$sandbox/npm-invoked"
|
|
set +e
|
|
PATH="$fake_lifecycle:$PATH" /bin/bash "$fixture/scripts/verify-schema-v3-only-release.sh" >"$output" 2>&1
|
|
release_status=$?
|
|
set -e
|
|
[[ $release_status -ne 0 ]] || fail "$label unexpectedly passed"
|
|
[[ ! -e "$sandbox/npm-invoked" ]] || fail "$label invoked npm before bootstrap trust"
|
|
}
|
|
|
|
seed_bootstrap_fixture
|
|
printf '%s\n' '# dirty' >>"$fixture/backend/package.json"
|
|
assert_release_stops_before_npm "dirty package bootstrap"
|
|
seed_bootstrap_fixture
|
|
printf '%s\n' '# dirty' >>"$fixture/backend/scripts/verify-workspace-descriptor-files.test.mjs"
|
|
assert_release_stops_before_npm "dirty checker test bootstrap"
|
|
seed_bootstrap_fixture
|
|
printf '%s\n' '// dirty' >>"$fixture/backend/scripts/revision-state-policy.mjs"
|
|
assert_release_stops_before_npm "dirty revision policy bootstrap"
|
|
seed_bootstrap_fixture
|
|
printf '%s\n' '# dirty' >>"$fixture/backend/scripts/revision_state_policy.py"
|
|
assert_release_stops_before_npm "dirty Python policy helper bootstrap"
|
|
seed_bootstrap_fixture
|
|
printf '%s\n' '# dirty' >>"$fixture/scripts/verify-schema-v3-only.sh"
|
|
assert_release_stops_before_npm "dirty gate bootstrap"
|
|
seed_bootstrap_fixture
|
|
rm "$fixture/backend/scripts/verify-workspace-descriptor-files.mjs"
|
|
ln -s /dev/null "$fixture/backend/scripts/verify-workspace-descriptor-files.mjs"
|
|
assert_release_stops_before_npm "symlink checker bootstrap"
|
|
seed_bootstrap_fixture
|
|
printf '%s\n' 'scripts/__pycache__/' >"$fixture/.gitignore"
|
|
"$real_git" -C "$fixture" add .gitignore
|
|
"$real_git" -C "$fixture" commit -qm ignore-rule
|
|
mkdir -p "$fixture/scripts/__pycache__"
|
|
printf x >"$fixture/scripts/__pycache__/ignored.pyc"
|
|
assert_release_stops_before_npm "ignored trusted artifact bootstrap"
|
|
seed_bootstrap_fixture
|
|
printf x >"$fixture/scripts/untracked-helper.sh"
|
|
assert_release_stops_before_npm "untracked helper bootstrap"
|
|
|
|
seed_bootstrap_fixture
|
|
mkfifo "$fixture/scripts/bootstrap-fifo"
|
|
assert_release_stops_before_npm "FIFO bootstrap"
|
|
seed_bootstrap_fixture
|
|
printf '%s\n' unsafe >"$fixture/backend/.npmrc"
|
|
assert_release_stops_before_npm "untracked backend npmrc bootstrap"
|
|
seed_bootstrap_fixture
|
|
global_ignore="$sandbox/global-ignore"
|
|
printf '%s\n' backend/.npmrc >"$global_ignore"
|
|
"$real_git" -C "$fixture" config core.excludesFile "$global_ignore"
|
|
printf '%s\n' unsafe >"$fixture/backend/.npmrc"
|
|
assert_release_stops_before_npm "globally ignored backend npmrc bootstrap"
|
|
|
|
# Dist failures are isolated to fixture roots; canonical backend/dist is never mutated.
|
|
run_gate_dist() {
|
|
set +e
|
|
"$gate_bash" "$gate" --root "$fixture" --runtime-only --check-dist >"$output" 2>&1
|
|
gate_status=$?
|
|
set -e
|
|
}
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/dist"
|
|
printf '%s\n' server >"$fixture/backend/dist/server.js"
|
|
run_gate_dist
|
|
[[ $gate_status -eq 1 ]] || fail "fixture missing schema module unexpectedly passed"
|
|
grep -Fq 'backend/dist/workspaces/schema.js' "$output" || fail "fixture missing schema path not reported"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/dist/workspaces"
|
|
printf '%s\n' schema >"$fixture/backend/dist/workspaces/schema.js"
|
|
run_gate_dist
|
|
[[ $gate_status -eq 1 ]] || fail "fixture missing server unexpectedly passed"
|
|
grep -Fq 'backend/dist/server.js' "$output" || fail "fixture missing server path not reported"
|
|
|
|
seed_fixture
|
|
mkdir -p "$fixture/backend/dist/workspaces"
|
|
printf '%s\n' schema >"$fixture/backend/dist/workspaces/schema.js"
|
|
printf '%s\n' server >"$fixture/backend/dist/server.js"
|
|
printf '%s\n' stale >"$fixture/backend/dist/workspaces/Migrate-Legacy.js"
|
|
run_gate_dist
|
|
[[ $gate_status -eq 1 ]] || fail "fixture stale migrator unexpectedly passed"
|
|
grep -Fq 'backend/dist/workspaces/Migrate-Legacy.js' "$output" || fail "fixture stale migrator path not reported"
|
|
|
|
[[ "$(cksum <"$canonical_schema")" == "$canonical_schema_checksum" ]] \
|
|
|| fail "shell regression mutated canonical compiled schema"
|
|
[[ "$(cksum <"$canonical_server")" == "$canonical_server_checksum" ]] \
|
|
|| fail "shell regression mutated canonical compiled server"
|
|
|
|
echo "schema-v4-only absence gate regression tests passed"
|