test(12.1-05): complete the Phase 12.1 gate with the removal, coverage, app and evidence stages

- --go, --spa, --openapi, --dist, --docs and --hygiene on the pattern of the Phase 12.2 gate
- --app runs vet and the tests of every module of the application workspace, with the application's name masked in output
- --coverage refuses a package of pact, cabana or the user plugin below 80 percent
- --removal: 27 anchor-exact mutations, one per high or critical protection and one per fix; a dirty file is refused and every file is restored and compared with cmp
- --evidence ties every threat of the five plans to a review row, a test and a removal row
- --self-test plants an input for every detector
This commit is contained in:
Jakub Zych
2026-10-05 16:01:10 +02:00
parent aced6c7df3
commit 3190b1ce59

View File

@@ -4,15 +4,20 @@
# screens built on them). # screens built on them).
# #
# Every stage exits non-zero on a failing command, a go test run that fails, # Every stage exits non-zero on a failing command, a go test run that fails,
# skips, matches zero tests or does not build, and a named security test that # skips, matches zero tests or does not build, a named security test that is
# is missing, renamed or skipped. --self-test proves each detector fails # missing, renamed or skipped, OpenAPI or dist drift, a docs checker problem,
# closed on planted input. A stage that is not implemented refuses. # a consuming-application name in a framework file, a coverage number below
# the floor, and an evidence gap. --self-test proves each detector fails
# closed on planted input.
# #
# Framework commands run in this repository. The plugin's tests run inside # Framework commands run in this repository. The plugin's tests run inside
# the application workspace named by PHASE121_APP (default: the sibling # the application workspace named by PHASE121_APP (default: the sibling
# checkout next to this repository). Output about the application workspace # checkout next to this repository); --app runs every module of that
# has the application's name masked; set PHASE121_VERBOSE=1 to see it as it # workspace. Output about the application workspace has the application's
# is while debugging. # name masked; set PHASE121_VERBOSE=1 to see it as it is while debugging.
#
# --removal edits tracked source while it runs (and restores it byte for
# byte), so it is not part of --all.
# Run with FORCE_COLOR unset: bonfire's colour tests read it. # Run with FORCE_COLOR unset: bonfire's colour tests read it.
set -euo pipefail set -euo pipefail
@@ -21,18 +26,68 @@ APP="${PHASE121_APP:-$ROOT/../fonoteka.go}"
# The user plugin inside the application workspace. # The user plugin inside the application workspace.
PLUGIN="./plugins/golem15/user" PLUGIN="./plugins/golem15/user"
APP_NAMES='fonoteka|p[lł]ytarium' APP_NAMES='fonoteka|p[lł]ytarium'
PHASE_DIR="${PHASE121_PHASE_DIR:-$ROOT/.planning/phases/12.1-user-plugin-admin-screens}"
REVIEW="$PHASE_DIR/12.1-SECURITY-REVIEW.md"
VALIDATION="$PHASE_DIR/12.1-VALIDATION.md"
COVERAGE_FLOOR=80
# The framework tag the phase's contracts were released under.
RELEASE_TAG=v0.1.3
# The named tests of the security stage, by prefix. Each prefix must match # The named tests of the security stage, by prefix. Each prefix must match
# at least one top-level test that passes; any skip refuses. # at least one top-level test that passes; any skip refuses.
SECURITY_CABANA=(TestPhase121Threats TestBulkAction TestRecordAction TestRowState TestForbidden SECURITY_CABANA=(TestPhase121Threats TestBulkAction TestRecordAction TestRowState TestForbidden
TestSoftDeletedRecord TestPreview TestPasswordField TestVirtualFields TestFormRules TestSoftDeletedRecord TestPreview TestPasswordField TestVirtualFields TestFormRules
TestPermissionEditor TestRelationLock TestWritableForeignKey TestInvisibleColumn TestPermissionEditor TestRelationLock TestWritableForeignKey TestInvisibleColumn
TestFilterOptionsController) TestFilterOptionsController TestPhase121BootErrors)
SECURITY_PLUGIN=(TestPhase121Threats TestAdminPrivilegedGroups TestAdminPrivilegedMember SECURITY_PLUGIN=(TestPhase121Threats TestAdminPrivilegedGroups TestAdminPrivilegedMember
TestAdminUserGroupsField TestAdminUserActions TestAdminUserForceDelete TestAdminUserPassword TestAdminUserGroupsField TestAdminUserActions TestAdminUserForceDelete TestAdminUserPassword
TestAdminUserInvite TestAdminAvatarSharedWithAPI TestAdminGroups TestAdminOrganisations TestAdminUserInvite TestAdminAvatarSharedWithAPI TestAdminGroups TestAdminOrganisations
TestAdminOrganisationMembers TestLastSeen) TestAdminOrganisationMembers TestLastSeen TestAdminUsersEdge TestAdminUsersControllerGuards
SECURITY_PLUGIN_CLASSES=(TestMergedPermissions TestPermissionSetScan) TestAdminUsersGroupFilterValue TestAdminRegistration TestAdminNeedsSecret)
SECURITY_PLUGIN_CLASSES=(TestMergedPermissions TestPermissionSetScan TestIsPrivilegedMember
TestPrivilegedGroupCodes TestAdminActions)
# Framework files this phase added or changed; the hygiene stage refuses a
# consuming-application name in them, in the root README, in every module
# README, in docs and in the SPA sources and tests.
PHASE_FILES=(
modules/cabana/testdata/roster
modules/cabana/actions.go
modules/cabana/crud.go
modules/cabana/field_permission.go
modules/cabana/relation_field.go
modules/cabana/form_schema.go
modules/cabana/list_schema.go
modules/cabana/tx_context.go
modules/cabana/example_actions_test.go
modules/cabana/example_rowstate_test.go
modules/cabana/example_form_seams_test.go
modules/cabana/phase121_fixture_test.go
modules/cabana/phase121_actions_test.go
modules/cabana/phase121_form_test.go
modules/cabana/phase121_threats_test.go
modules/cabana/phase121_bulk_test.go
modules/cabana/phase121_record_test.go
modules/cabana/phase121_rowstate_test.go
modules/cabana/phase121_forbidden_test.go
modules/cabana/phase121_preview_test.go
modules/cabana/phase121_fields_test.go
modules/cabana/phase121_permission_test.go
modules/cabana/phase121_relation_lock_test.go
modules/cabana/phase121_list_test.go
modules/cabana/phase121_schema_boot_test.go
modules/pact/capabilities.go
modules/pact/capabilities_test.go
admin/tests
)
HYGIENE_DOCS=(README.md docs admin/src)
# Dependency manifests: the phase adds and changes no module or package.
MANIFESTS=(go.mod go.sum admin/package.json admin/package-lock.json)
# High or critical mitigated threats without a removal row. Each needs an
# "| NR-nn | <threat> | <reason> |" row in the review instead; any other
# high or critical mitigated threat needs an "| RC-nn | <threat> |" row.
NO_REMOVAL="T-12.1-17 T-12.1-SC"
STAGES=(self-test go security removal coverage spa openapi dist docs hygiene app evidence all) STAGES=(self-test go security removal coverage spa openapi dist docs hygiene app evidence all)
@@ -56,6 +111,7 @@ usage:
environment: environment:
PHASE121_APP the application workspace (default: the sibling checkout) PHASE121_APP the application workspace (default: the sibling checkout)
PHASE121_VERBOSE 1 shows application output without masking its name PHASE121_VERBOSE 1 shows application output without masking its name
PHASE121_RC removal rows to run, space separated (default: all)
EOF EOF
exit 2 exit 2
} }
@@ -72,21 +128,24 @@ mask() {
# where DIR names a directory in output: the application workspace is never # where DIR names a directory in output: the application workspace is never
# printed by its path. # printed by its path.
where() { where() {
if [[ "$1" == "$APP" ]]; then case "$1" in
echo "the application workspace" "$APP") echo "the application workspace" ;;
else "$APP"/*) echo "the application workspace (${1#"$APP"/})" ;;
echo "${1#"$ROOT"/}" *) echo "${1#"$ROOT"/}" ;;
fi esac
} }
# detect reads go test -json. Exit 1 fail or build failure, 2 skip, 3 zero # detect reads go test -json. Exit 1 fail or build failure, 2 skip, 3 zero
# tests or "no tests to run", 4 non-JSON, 5 a required prefix has no passing # tests or "no tests to run", 4 non-JSON, 5 a required prefix has no passing
# top-level test. REQUIRE_PREFIXES lists the prefixes. # top-level test. REQUIRE_PREFIXES lists the prefixes. ALLOW_EMPTY=1 accepts
# packages without tests (a whole-module run) but still refuses a run in
# which no test passed at all.
detect() { detect() {
python3 - "$1" <<'PY' python3 - "$1" <<'PY'
import json, os, sys import json, os, sys
path = sys.argv[1] path = sys.argv[1]
prefixes = os.environ.get("REQUIRE_PREFIXES", "").split() prefixes = os.environ.get("REQUIRE_PREFIXES", "").split()
allow_empty = os.environ.get("ALLOW_EMPTY", "") == "1"
passed = set() passed = set()
failed = [] failed = []
with open(path, encoding="utf-8", errors="replace") as fh: with open(path, encoding="utf-8", errors="replace") as fh:
@@ -105,7 +164,7 @@ with open(path, encoding="utf-8", errors="replace") as fh:
if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")): if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")):
print(f"refuse: build failed {pkg}", file=sys.stderr) print(f"refuse: build failed {pkg}", file=sys.stderr)
sys.exit(1) sys.exit(1)
if action == "output" and "no tests to run" in (ev.get("Output") or ""): if action == "output" and "no tests to run" in (ev.get("Output") or "") and not allow_empty:
print(f"refuse: no tests to run in {pkg}", file=sys.stderr) print(f"refuse: no tests to run in {pkg}", file=sys.stderr)
sys.exit(3) sys.exit(3)
if action == "skip" and test: if action == "skip" and test:
@@ -195,6 +254,559 @@ need_app() {
} }
} }
# workspace_modules prints the module directories of the application's
# go.work, relative to the workspace, one per line.
workspace_modules() {
python3 - "$1/go.work" <<'PY'
import re, sys
text = open(sys.argv[1]).read()
dirs = []
block = re.search(r"^use\s*\((.*?)^\)", text, re.S | re.M)
if block:
dirs += [line.split("//")[0].strip() for line in block.group(1).splitlines()]
dirs += re.findall(r"^use\s+([^\s(]+)\s*$", text, re.M)
dirs = [d.strip('"') for d in dirs if d]
if not dirs:
print("refuse: go.work names no module", file=sys.stderr)
sys.exit(1)
print("\n".join(dirs))
PY
}
run_go() {
(cd "$ROOT" && go vet ./...)
ALLOW_EMPTY=1 go_json "$ROOT" ./...
echo "phase12.1 go passed"
}
run_security() {
need_app
named "$ROOT" ./modules/cabana "${SECURITY_CABANA[@]}"
named "$APP" "$PLUGIN" "${SECURITY_PLUGIN[@]}"
named "$APP" "$PLUGIN/classes" "${SECURITY_PLUGIN_CLASSES[@]}"
echo "phase12.1 security passed"
}
# coverage_report FLOOR PROFILE... prints one line per package of the merged
# profiles (a block counts as covered when any profile covered it) and
# refuses any package below FLOOR percent.
coverage_report() {
python3 - "$@" <<'PY'
import collections, sys
floor = float(sys.argv[1])
blocks = {}
for path in sys.argv[2:]:
for line in open(path):
if line.startswith("mode:") or not line.strip():
continue
loc, n, c = line.rsplit(" ", 2)
n, c = int(n), int(c)
prev = blocks.get(loc, (n, 0))
blocks[loc] = (n, max(prev[1], c))
total, covered = collections.Counter(), collections.Counter()
for loc, (n, c) in blocks.items():
pkg = loc.split(":")[0].rsplit("/", 1)[0]
total[pkg] += n
if c:
covered[pkg] += n
if not total:
print("refuse: coverage profile is empty", file=sys.stderr)
sys.exit(1)
low = []
for pkg in sorted(total):
pct = 100.0 * covered[pkg] / total[pkg]
print(f"coverage {pkg} {pct:.1f}% ({covered[pkg]}/{total[pkg]} statements)")
if pct < floor:
low.append(f"{pkg} {pct:.1f}%")
if low:
print(f"refuse: below the {floor:.0f}% coverage floor: " + ", ".join(low), file=sys.stderr)
sys.exit(1)
PY
}
# cover_profile DIR OUT ARGS... writes a coverage profile of go test ARGS.
cover_profile() {
local dir="$1" out="$2"
shift 2
local log
log="$(mktemp)"
if ! (cd "$dir" && go test -count=1 -coverprofile="$out" "$@") >"$log" 2>&1; then
grep -E '^(--- FAIL|FAIL|panic:|\s+\S+_test\.go:[0-9]+)' "$log" | mask | tail -n 40 >&2
rm -f "$log"
echo "refuse: go test -coverprofile in $(where "$dir")" >&2
return 1
fi
rm -f "$log"
}
run_coverage() {
need_app
local dir
dir="$(mktemp -d)"
trap 'rm -rf "$dir"' RETURN
# Framework packages: each package's own tests.
cover_profile "$ROOT" "$dir/pact.out" ./modules/pact
cover_profile "$ROOT" "$dir/cabana.out" ./modules/cabana
coverage_report "$COVERAGE_FLOOR" "$dir/pact.out" "$dir/cabana.out"
# The plugin's packages: every test of the plugin that exercises them.
cover_profile "$APP" "$dir/plugin.out" "$PLUGIN/..." \
"-coverpkg=$PLUGIN,$PLUGIN/classes,$PLUGIN/controllers,$PLUGIN/models,$PLUGIN/updates"
coverage_report "$COVERAGE_FLOOR" "$dir/plugin.out" | mask
echo "phase12.1 coverage passed"
}
run_spa() {
npm --prefix "$ROOT/admin" run typecheck
local log rc=0
log="$(mktemp)"
npm --prefix "$ROOT/admin" test >"$log" 2>&1 || rc=$?
if [[ "$rc" -ne 0 ]] || grep -qE 'No test files found|Unhandled (Errors|Rejection)|FAIL ' "$log"; then
tail -n 60 "$log" >&2
rm -f "$log"
echo "refuse: admin Vitest run failed (exit $rc)" >&2
return 1
fi
grep -E 'Test Files|Tests ' "$log" || true
rm -f "$log"
# The five UI backstops of the UI-SPEC are named test cases.
local needle
for needle in \
'backstop: focus returns to the bulk menu trigger' \
'backstop: mapWinterUrl maps preview/:id' \
'backstop: a row state outside the fixed set' \
'backstop: radio mode emits 1 and -1' \
'backstop: a locked option cannot be chosen'; do
grep -rqF -- "$needle" "$ROOT/admin/tests" || {
echo "refuse: missing named test: no vitest case titled \"$needle\"" >&2
return 1
}
done
if grep -rnE "\b(it|describe|test)\.(skip|todo|only)\(" "$ROOT/admin/tests" >&2; then
echo "refuse: a skipped, todo or exclusive vitest case" >&2
return 1
fi
echo "phase12.1 spa passed"
}
run_openapi() {
"$ROOT/scripts/check-admin-openapi.sh" --check
named "$ROOT" ./modules/cabana TestPhase10OpenAPIConformance TestPhase09ContractInventory TestPhase09PermissionMatrix
echo "phase12.1 openapi passed"
}
run_dist() {
"$ROOT/scripts/check-admin-dist.sh"
echo "phase12.1 dist passed"
}
run_docs() {
go_json "$ROOT" ./cmd/summer -run '^TestDocsTree$'
local out
out="$(cd "$ROOT" && go run ./cmd/summer docs:build --check 2>&1)" || {
echo "$out" >&2
echo "refuse: docs:build --check failed" >&2
return 1
}
go_json "$ROOT" ./modules/phrasebook -run '^TestPhase10SPAKeysResolve$'
echo "phase12.1 docs passed"
}
# hygiene_scan ROOT PATH... prints every line that names a consuming
# application; the planning tree and this script are never scanned.
hygiene_scan() {
local root="$1"
shift
(cd "$root" && grep -rniIE "$APP_NAMES" "$@" 2>/dev/null || true)
}
run_hygiene() {
local bad=0 hits path
for path in "${PHASE_FILES[@]}"; do
[[ -e "$ROOT/$path" ]] || {
echo "refuse: hygiene: $path is listed and does not exist" >&2
bad=1
}
done
local readmes=()
mapfile -t readmes < <(cd "$ROOT" && ls modules/*/README.md)
[[ "${#readmes[@]}" -gt 0 ]] || {
echo "refuse: hygiene: no module README was found" >&2
bad=1
}
hits="$(hygiene_scan "$ROOT" "${HYGIENE_DOCS[@]}" "${readmes[@]}" "${PHASE_FILES[@]}")"
if [[ -n "$hits" ]]; then
echo "refuse: hygiene: consuming-application names in the framework:" >&2
cut -d: -f1,2 <<<"$hits" | sort -u | head -n 20 >&2
bad=1
fi
# The acme.roster fixture lives only in _test.go files and testdata.
hits="$(cd "$ROOT" && grep -rlnE 'acme\.roster|rosterPlugin' --include='*.go' modules cmd 2>/dev/null | grep -vE '_test\.go$' || true)"
if [[ -n "$hits" ]]; then
echo "refuse: hygiene: the test fixture plugin is referenced by production code: $hits" >&2
bad=1
fi
hits="$(cd "$ROOT" && grep -rnE 'v-html|innerHTML|outerHTML|insertAdjacentHTML' admin/src 2>/dev/null || true)"
if [[ -n "$hits" ]]; then
echo "refuse: hygiene: raw-HTML sink in admin/src: $hits" >&2
bad=1
fi
hits="$(cd "$ROOT" && gofmt -l modules/cabana modules/pact 2>/dev/null || true)"
if [[ -n "$hits" ]]; then
echo "refuse: hygiene: gofmt: $hits" >&2
bad=1
fi
# T-12.1-SC: no module and no package was added or changed since the tag.
if git -C "$ROOT" rev-parse -q --verify "refs/tags/$RELEASE_TAG" >/dev/null; then
hits="$(git -C "$ROOT" diff --name-only "$RELEASE_TAG" HEAD -- "${MANIFESTS[@]}")"
if [[ -n "$hits" || -n "$(git -C "$ROOT" status --porcelain -- "${MANIFESTS[@]}")" ]]; then
echo "refuse: hygiene: a dependency manifest changed since $RELEASE_TAG: $hits" >&2
bad=1
fi
else
echo "refuse: hygiene: the tag $RELEASE_TAG does not exist in this checkout" >&2
bad=1
fi
[[ "$bad" -eq 0 ]] || return 1
echo "phase12.1 hygiene passed"
}
run_app() {
need_app
local dir modules
modules="$(workspace_modules "$APP")"
(cd "$APP" && go build ./...) 2>&1 | mask >&2
while IFS= read -r dir; do
[[ -d "$APP/$dir" && -f "$APP/$dir/go.mod" ]] || {
echo "refuse: workspace module $(where "$APP/$dir") has no go.mod" >&2
return 1
}
if ! (cd "$APP/$dir" && go vet ./...) >/dev/null 2>"$APP_ERR"; then
mask <"$APP_ERR" | tail -n 40 >&2
echo "refuse: go vet in $(where "$APP/$dir")" >&2
return 1
fi
ALLOW_EMPTY=1 go_json "$APP/$dir" ./...
echo "app module $(where "$APP/$dir" | mask): vet and tests passed"
done <<<"$modules"
# The schema and the user API contract are named: they must have run.
REQUIRE_PREFIXES="TestSchemaMatchesPHPSnapshot TestUserAPINuxtFlows TestParityCorpus" \
go_json "$APP" ./parity -v -run '^(TestSchemaMatchesPHPSnapshot|TestUserAPINuxtFlows|TestParityCorpus)$'
local pointer head
if [[ -n "$(git -C "$APP" status --porcelain)" ]]; then
git -C "$APP" status --short | mask >&2
echo "refuse: the application workspace has uncommitted changes" >&2
return 1
fi
if [[ -n "$(git -C "$APP/$PLUGIN" status --porcelain)" ]]; then
git -C "$APP/$PLUGIN" status --short >&2
echo "refuse: the user plugin checkout has uncommitted changes" >&2
return 1
fi
pointer="$(git -C "$APP" rev-parse "HEAD:${PLUGIN#./}")"
head="$(git -C "$APP/$PLUGIN" rev-parse HEAD)"
if [[ "$pointer" != "$head" ]]; then
echo "refuse: the application records plugin commit $pointer, the plugin checkout is at $head" >&2
return 1
fi
echo "phase12.1 app passed"
}
# removal_table: one row per protection. Fields: id, threat, repo
# (root|plugin), file, anchor, replacement, test dir (root|app), package,
# test regex. An anchor must occur exactly once in its file.
removal_table() {
cat <<'EOF'
[
["RC-01", "T-12.1-01", "root", "modules/cabana/crud.go",
"if ext, ok := cc.Controller.(pact.ListExtendQuery); ok && ext != nil {\n\t\t\tif next := ext.ListExtendQuery(ctx, q); next != nil {\n\t\t\t\tq = next\n\t\t\t}\n\t\t}\n\t}\n\tcol := primaryColumn(proto)\n\tvals := make([]any, len(ids))",
"if ext, ok := cc.Controller.(pact.ListExtendQuery); ok && ext != nil && false {\n\t\t\tif next := ext.ListExtendQuery(ctx, q); next != nil {\n\t\t\t\tq = next\n\t\t\t}\n\t\t}\n\t}\n\tcol := primaryColumn(proto)\n\tvals := make([]any, len(ids))",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-01$"],
["RC-02", "T-12.1-01", "root", "modules/cabana/crud.go",
"\t\tif len(rows) != len(ids) {\n\t\t\treturn partialSelection{}\n\t\t}\n\t\tout, err := action.Run(ctx, pact.AdminBulkActionInput{Records: rows})",
"\t\tout, err := action.Run(ctx, pact.AdminBulkActionInput{Records: rows})",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-01$"],
["RC-03", "T-12.1-02", "root", "modules/cabana/actions.go",
"if Allows(principal, permissions) {\n\t\treturn true\n\t}\n\tvar adminID uint",
"if true || Allows(principal, permissions) {\n\t\treturn true\n\t}\n\tvar adminID uint",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-02$"],
["RC-04", "T-12.1-03", "root", "modules/cabana/http.go",
"requireAjax(s.bulkAction)", "s.bulkAction",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-03$"],
["RC-05", "T-12.1-03", "root", "modules/cabana/http.go",
"requireAjax(s.recordAction)", "s.recordAction",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-03$"],
["RC-06", "T-12.1-04", "root", "modules/cabana/crud.go",
"if err := loadRecord(ctx, tx, cc, target, pk); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif action.Applies != nil {",
"if err := loadRecord(ctx, tx, nil, target, pk); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif action.Applies != nil {",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-04$"],
["RC-07", "T-12.1-04", "root", "modules/cabana/crud.go",
"\t\t\tif !applies {\n\t\t\t\treturn actionConflict{}\n\t\t\t}\n\t\t}\n\t\tout, err := action.Run(ctx, pact.AdminRecordActionInput{",
"\t\t\tif false && !applies {\n\t\t\t\treturn actionConflict{}\n\t\t\t}\n\t\t}\n\t\tout, err := action.Run(ctx, pact.AdminRecordActionInput{",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-04$"],
["RC-08", "T-12.1-09", "root", "modules/cabana/crud.go",
"\t\tif !cc.virtual[field.Name] || !contextAllows(cc, field.Name, op) {\n\t\t\tcontinue\n\t\t}",
"\t\tif !cc.virtual[field.Name] {\n\t\t\tcontinue\n\t\t}",
"root", "./modules/cabana", "^(TestPhase121Threats$/^T-12.1-09|TestVirtualFields)"],
["RC-09", "T-12.1-09", "root", "modules/cabana/crud.go",
"|| protectedFillKey(field.Name) || cc.virtual[field.Name] {", "|| protectedFillKey(field.Name) {",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-09$"],
["RC-10", "T-12.1-10", "root", "modules/cabana/crud.go",
"|| protectedFillKey(field.Name) || cc.virtual[field.Name] {", "|| protectedFillKey(field.Name) {",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-10$"],
["RC-11", "T-12.1-11", "root", "modules/cabana/relation_field.go",
"out.ReadOnly = protectedFillKey(contract.ForeignKey) && !contract.WritableForeignKey", "out.ReadOnly = false",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-11$"],
["RC-12", "T-12.1-12", "root", "modules/cabana/crud.go",
"if err := checkRelationLocks(ctx, tx, cc, target, relations); err != nil {",
"if err := checkRelationLocks(ctx, tx, cc, target, nil); err != nil {",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-12$"],
["RC-13", "T-12.1-13", "root", "modules/cabana/field_permission.go",
"\t\t\tif _, known := offered[code]; !known {\n\t\t\t\treturn &ValidationError{",
"\t\t\tif _, known := offered[code]; false && !known {\n\t\t\t\treturn &ValidationError{",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-13$"],
["RC-14", "T-12.1-13", "root", "modules/cabana/field_permission.go",
"if option.Locked && stored[option.Code] != submitted[option.Code] {",
"if false && option.Locked && stored[option.Code] != submitted[option.Code] {",
"root", "./modules/cabana", "^TestPhase121Threats$/^T-12.1-13$"],
["RC-15", "T-12.1-18", "plugin", "controllers/users_admin_controller.go",
"func (usersAdminController) RequiredPermissions() []string {\n\treturn []string{PermissionAccessUsers}",
"func (usersAdminController) RequiredPermissions() []string {\n\treturn []string{}",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-18$"],
["RC-16", "T-12.1-19", "plugin", "models/user/columns.yaml",
" email:\n label: golem15.user::lang.user.email\n searchable: true\n",
" email:\n label: golem15.user::lang.user.email\n searchable: true\n password:\n label: golem15.user::lang.user.email\n",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-19$"],
["RC-17", "T-12.1-20", "plugin", "models/user/fields.yaml",
" email:\n label: golem15.user::lang.user.email\n type: text\n span: full\n tab: golem15.user::lang.user.account\n",
" email:\n label: golem15.user::lang.user.email\n type: text\n span: full\n tab: golem15.user::lang.user.account\n organisation_role:\n label: golem15.user::lang.user.email\n type: text\n tab: golem15.user::lang.user.account\n",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-20$"],
["RC-18", "T-12.1-24", "plugin", "models/user.go",
"LastSeen *time.Time `gorm:\"column:last_seen\" json:\"-\"`", "LastSeen *time.Time `gorm:\"column:last_seen\" json:\"last_seen\"`",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-24$"],
["RC-27", "T-12.1-24", "plugin", "models/user.go",
"Permissions PermissionSet `gorm:\"column:permissions\" json:\"-\"`", "Permissions PermissionSet `gorm:\"column:permissions\" json:\"permissions\"`",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-24$"],
["RC-19", "T-12.1-28", "plugin", "controllers/users_admin_controller.go",
"return cabana.RelationLock{IDs: ids, Message: usersLang + \"privileged_group_forbidden\"}, nil",
"return cabana.RelationLock{IDs: ids[:0], Message: usersLang + \"privileged_group_forbidden\"}, nil",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-28$"],
["RC-20", "T-12.1-29", "plugin", "controllers/usergroups_admin_controller.go",
"principal, _ := bouncer.User(ctx)\n\tif cabana.Allows(principal, []string{classes.PermissionManagePrivilegedGroups}) {\n\t\treturn nil\n\t}\n\tconst message = groupsLang",
"principal, _ := bouncer.User(ctx)\n\tif true || cabana.Allows(principal, []string{classes.PermissionManagePrivilegedGroups}) {\n\t\treturn nil\n\t}\n\tconst message = groupsLang",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-29$"],
["RC-21", "T-12.1-30", "plugin", "classes/admin_actions.go",
"\t\tuser.IsActivated = true\n\t\tuser.ActivatedAt = &now",
"\t\tfresh(ctx, db).Exec(\"DELETE FROM users_groups WHERE user_id = ?\", user.ID)\n\t\tuser.IsActivated = true\n\t\tuser.ActivatedAt = &now",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-30$"],
["RC-22", "T-12.1-34", "plugin", "models/user.go",
"joinReferences:user_group_id\" json:\"-\"`", "joinReferences:user_group_id\" json:\"groups\"`",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-34$"],
["RC-23", "T-12.1-38", "plugin", "controllers/users_admin_controller.go",
"\tif err := c.guardCredentials(ctx, user); err != nil {\n\t\treturn err\n\t}\n", "",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-38$"],
["RC-24", "T-12.1-39", "plugin", "controllers/users_admin_controller.go",
"return c.guardPrivilegedMember(ctx, user.ID, usersLang+\"privileged_member_delete_forbidden\", nil)", "return nil",
"app", "./plugins/golem15/user", "^TestPhase121Threats$/^T-12.1-39$"],
["RC-25", "T-12.1-38", "plugin", "classes/admin_actions.go",
"return db.Session(&gorm.Session{NewDB: true, Context: ctx}).Clauses().Session(&gorm.Session{NewDB: true})",
"return db.Session(&gorm.Session{NewDB: true}).WithContext(ctx)",
"app", "./plugins/golem15/user/classes", "^TestIsPrivilegedMember$"],
["RC-26", "D-23", "plugin", "models/user.go",
"\tid, ok := groupFilterID(value)\n\tif !ok {\n\t\treturn db.Where(\"1 = 0\")\n\t}\n\treturn db.Where(\"users.id IN (SELECT user_id FROM users_groups WHERE user_group_id = ?)\", id)",
"\t_, _ = groupFilterID(value)\n\treturn db.Where(\"users.id IN (SELECT user_id FROM users_groups WHERE user_group_id = ?)\", value)",
"app", "./plugins/golem15/user", "^TestAdminUsersGroupFilterValue$"]
]
EOF
}
# removal_harness TABLE_FILE: for each row, refuse a file with uncommitted
# changes, save it, apply the anchor-exact mutation, run the named test and
# require it to fail on an assertion (a build failure does not count), then
# restore the file and require cmp to match.
removal_harness() {
python3 - "$1" "$ROOT" "$APP" "$APP/$PLUGIN" <<'PY'
import json, os, shutil, subprocess, sys, tempfile
table = json.load(open(sys.argv[1]))
bases = {"root": sys.argv[2], "app": sys.argv[3], "plugin": sys.argv[4]}
only = set(os.environ.get("PHASE121_RC", "").split())
failures = 0
ran = 0
for rc, threat, repo, rel, anchor, repl, test_repo, pkg, run in table:
if only and rc not in only:
continue
ran += 1
path = os.path.join(bases[repo], rel)
where = os.path.dirname(path)
tracked = subprocess.run(["git", "-C", where, "rev-parse", "--is-inside-work-tree"], capture_output=True, text=True).returncode == 0
if tracked:
dirty = subprocess.run(["git", "-C", where, "status", "--porcelain", "--", os.path.basename(path)], capture_output=True, text=True).stdout.strip()
if dirty:
print(f"refuse: {rc}: {rel} is dirty; commit or restore it first", file=sys.stderr)
sys.exit(1)
original = open(path, "rb").read()
text = original.decode()
n = text.count(anchor)
if n != 1:
print(f"refuse: {rc} {threat}: anchor occurs {n} times in {rel}", file=sys.stderr)
sys.exit(1)
mutated = text.replace(anchor, repl, 1)
scratch = tempfile.mkdtemp(prefix="phase121-rc-")
saved = os.path.join(scratch, "saved")
shutil.copyfile(path, saved)
try:
with open(path, "w") as fh:
fh.write(mutated)
proc = subprocess.run(["go", "test", pkg, "-run", run, "-count=1"], cwd=bases[test_repo], capture_output=True, text=True, timeout=1200)
out = proc.stdout + proc.stderr
build = "[build failed]" in out or "[setup failed]" in out
ok = proc.returncode != 0 and "--- FAIL" in out and not build
fails = [l.strip() for l in out.splitlines() if l.strip().startswith("--- FAIL")]
names = [l.split()[2] for l in fails if len(l.split()) > 2]
evidence = ", ".join(names[:5]) + (f" (+{len(names) - 5} more)" if len(names) > 5 else "") if names else ("build failed" if build else "no failure")
finally:
with open(path, "wb") as fh:
fh.write(original)
same = subprocess.run(["cmp", "-s", saved, path]).returncode == 0
shutil.rmtree(scratch, ignore_errors=True)
if not same:
print(f"refuse: {rc}: {rel} was not restored byte for byte", file=sys.stderr)
sys.exit(1)
status = "fails as required" if ok else "SURVIVED"
print(f"{rc} {threat} {rel}: {status}: {evidence}; restored")
sys.stdout.flush()
if not ok:
failures += 1
if ran == 0:
print("refuse: no removal row was run", file=sys.stderr)
sys.exit(1)
if failures:
print(f"refuse: {failures} removal check(s) survived", file=sys.stderr)
sys.exit(1)
PY
}
run_removal() {
need_app
local table rc=0
table="$(mktemp)"
removal_table >"$table"
removal_harness "$table" || rc=$?
rm -f "$table"
[[ "$rc" -eq 0 ]] || return 1
# Nothing the harness touched may be left modified.
local dirty
dirty="$(git -C "$ROOT" status --porcelain -- modules; git -C "$APP/$PLUGIN" status --porcelain)"
if [[ -n "$dirty" ]]; then
echo "refuse: the removal stage left modified files: $dirty" >&2
return 1
fi
echo "phase12.1 removal passed"
}
# removal_harness_in ROOT TABLE runs the harness against another root.
removal_harness_in() {
local root="$1" table="$2"
(
ROOT="$root"
APP="$root"
PLUGIN="."
export GOWORK=off GOFLAGS=-mod=mod
removal_harness "$table"
)
}
# removal_ids prints the "RC-nn threat" pairs of the removal table.
removal_ids() {
removal_table | python3 -c 'import json,sys
for row in json.load(sys.stdin):
print(row[0], row[1])'
}
# evidence_check PHASE_DIR REVIEW VALIDATION NO_REMOVAL RC_PAIRS: every
# threat the plans declare has exactly one review row copying its severity
# and disposition; a mitigated threat names a test, a vitest file or a gate
# stage; a high or critical mitigated threat has a removal row (or, for the
# ids in NO_REMOVAL only, an NR row with a reason); every row of the removal
# table is in the review; the review reports zero open threats; the
# validation file is validated, Nyquist-compliant, keyed by real task ids
# and has no pending or TBD row.
evidence_check() {
python3 - "$@" <<'PY'
import glob, os, re, sys
phase_dir, review_path, validation_path = sys.argv[1], sys.argv[2], sys.argv[3]
no_removal = set(sys.argv[4].split())
rc_pairs = [line.split() for line in sys.argv[5].splitlines() if line.strip()]
for p in (review_path, validation_path):
if not os.path.isfile(p):
print(f"refuse: {os.path.basename(p)} is missing", file=sys.stderr)
sys.exit(1)
review = open(review_path).read()
validation = open(validation_path).read()
declared = {}
for plan in sorted(glob.glob(os.path.join(phase_dir, "*-0*-PLAN.md"))):
for line in open(plan):
m = re.match(r"^\| (T-12\.1-(?:\d\d|SC)) \|", line)
if m:
cells = [c.strip().lower() for c in line.strip().strip("|").split("|")]
declared.setdefault(m.group(1), cells)
if not declared:
print("refuse: no plan declares a T-12.1 threat", file=sys.stderr)
sys.exit(1)
lines = review.splitlines()
names_test = re.compile(r"Test[A-Z][A-Za-z0-9]+|[A-Za-z]+\.test\.ts|check-phase12\.1\.sh --[a-z-]+")
for tid, cells in sorted(declared.items()):
rows = [l for l in lines if l.startswith("| " + tid + " |")]
if len(rows) != 1:
print(f"refuse: review has {len(rows)} threat rows for {tid}, want 1", file=sys.stderr)
sys.exit(1)
row = [c.strip().lower() for c in rows[0].strip().strip("|").split("|")]
severity, disposition = cells[3], cells[4]
if severity not in row or disposition not in row:
print(f"refuse: review row {tid} does not copy severity {severity!r} and disposition {disposition!r}", file=sys.stderr)
sys.exit(1)
if disposition == "mitigate" and not names_test.search(rows[0]):
print(f"refuse: mitigated threat {tid} names no test or gate stage", file=sys.stderr)
sys.exit(1)
if severity in ("high", "critical") and disposition == "mitigate":
removal = any(re.match(r"^\| RC-\d+ \| " + re.escape(tid) + r" \|", l) for l in lines)
waived = [l for l in lines if re.match(r"^\| NR-\d+ \| " + re.escape(tid) + r" \|", l)]
reason = waived and len([c for c in waived[0].strip().strip("|").split("|") if c.strip()]) >= 3
if not removal and not (tid in no_removal and reason):
print(f"refuse: {severity} threat {tid} has no removal check row", file=sys.stderr)
sys.exit(1)
for rc, threat in rc_pairs:
if not any(re.match(r"^\| " + re.escape(rc) + r" \| " + re.escape(threat) + r" \|", l) for l in lines):
print(f"refuse: review has no row for removal check {rc} {threat}", file=sys.stderr)
sys.exit(1)
if not re.search(r"^threats_open: 0$", review, re.M):
print("refuse: the review does not report threats_open: 0", file=sys.stderr)
sys.exit(1)
if "T-12-18" not in review:
print("refuse: the review does not cite T-12-18", file=sys.stderr)
sys.exit(1)
if not re.search(r"^nyquist_compliant: true$", validation, re.M):
print("refuse: validation is not nyquist_compliant", file=sys.stderr)
sys.exit(1)
if not re.search(r"^status: validated$", validation, re.M):
print("refuse: validation status is not validated", file=sys.stderr)
sys.exit(1)
status_word = re.compile(r"(?<![A-Za-z])pending(?![A-Za-z])|⬜|\| TBD \|", re.I)
for line in validation.splitlines():
if line.startswith("|") and status_word.search(line):
print("refuse: validation row still pending: " + line, file=sys.stderr)
sys.exit(1)
if not any(re.match(r"^\| 12\.1-\d\d-T\d", l) for l in validation.splitlines()):
print("refuse: validation has no per-task verification rows", file=sys.stderr)
sys.exit(1)
print(f"evidence: {len(declared)} threats, {len(rc_pairs)} removal rows")
PY
}
run_evidence() {
evidence_check "$PHASE_DIR" "$REVIEW" "$VALIDATION" "$NO_REMOVAL" "$(removal_ids)"
echo "phase12.1 evidence passed"
}
run_self_test() { run_self_test() {
bash -n "${BASH_SOURCE[0]}" bash -n "${BASH_SOURCE[0]}"
expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestPhase121Threats"}' expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestPhase121Threats"}'
@@ -217,6 +829,14 @@ run_self_test() {
REQUIRE_PREFIXES="TestPhase121Threats TestAdminPrivilegedMember" expect_detect named 0 \ REQUIRE_PREFIXES="TestPhase121Threats TestAdminPrivilegedMember" expect_detect named 0 \
'{"Action":"pass","Package":"p","Test":"TestPhase121Threats"} '{"Action":"pass","Package":"p","Test":"TestPhase121Threats"}
{"Action":"pass","Package":"p","Test":"TestAdminPrivilegedMember"}' {"Action":"pass","Package":"p","Test":"TestAdminPrivilegedMember"}'
# A whole-module run may hold packages without tests, never zero tests,
# and a skip or a failure still refuses.
ALLOW_EMPTY=1 expect_detect module-run 0 '{"Action":"output","Package":"q","Output":"testing: warning: no tests to run\n"}
{"Action":"pass","Package":"p","Test":"TestA"}'
ALLOW_EMPTY=1 expect_detect module-zero 3 '{"Action":"output","Package":"q","Output":"testing: warning: no tests to run\n"}
{"Action":"pass","Package":"q"}'
ALLOW_EMPTY=1 expect_detect module-skip 2 '{"Action":"pass","Package":"p","Test":"TestA"}
{"Action":"skip","Package":"p","Test":"TestB"}'
local stage local stage
for stage in "${STAGES[@]}"; do for stage in "${STAGES[@]}"; do
grep -q -- "^ --$stage)" "${BASH_SOURCE[0]}" || { grep -q -- "^ --$stage)" "${BASH_SOURCE[0]}" || {
@@ -230,40 +850,223 @@ run_self_test() {
done done
# The mask hides the application's name unless asked not to. # The mask hides the application's name unless asked not to.
local masked local masked
masked="$(printf 'ok \tgit.example.test/x/fonoteka.go/parity\n' | PHASE121_VERBOSE= mask)" masked="$(printf 'ok \tgit.example.test/x/fonoteka.go/parity\n' | PHASE121_VERBOSE='' mask)"
if grep -qiE "$APP_NAMES" <<<"$masked"; then if grep -qiE "$APP_NAMES" <<<"$masked"; then
echo "refuse: self-test mask left the application's name: $masked" >&2 echo "refuse: self-test mask left the application's name: $masked" >&2
return 1 return 1
fi fi
local scratch out
scratch="$(mktemp -d)"
trap 'rm -rf "$scratch"' RETURN
# Coverage: 80% passes an 80% floor, a 50% package refuses, profiles
# merge, an empty profile refuses.
printf 'mode: set\nexample.test/acme/a.go:1.1,2.2 8 1\nexample.test/acme/a.go:3.1,4.2 2 0\n' >"$scratch/p1"
printf 'mode: set\nexample.test/low/b.go:1.1,2.2 5 1\nexample.test/low/b.go:3.1,4.2 5 0\n' >"$scratch/p2"
printf 'mode: set\nexample.test/low/b.go:3.1,4.2 5 1\n' >"$scratch/p3"
printf 'mode: set\n' >"$scratch/empty"
out="$(coverage_report 80 "$scratch/p1" 2>&1)" || {
echo "refuse: self-test coverage_report refused 80% at an 80% floor: $out" >&2
return 1
}
if out="$(coverage_report 80 "$scratch/p1" "$scratch/p2" 2>&1)"; then
echo "refuse: self-test coverage_report accepted a 50% package" >&2
return 1
fi
grep -q "below the 80% coverage floor: example.test/low 50.0%" <<<"$out" || {
echo "refuse: self-test coverage_report refused for the wrong reason: $out" >&2
return 1
}
coverage_report 80 "$scratch/p1" "$scratch/p2" "$scratch/p3" >/dev/null 2>&1 || {
echo "refuse: self-test coverage_report did not merge profiles" >&2
return 1
}
if coverage_report 80 "$scratch/empty" 2>/dev/null; then
echo "refuse: self-test coverage_report accepted an empty profile" >&2
return 1
fi
# Hygiene: a planted application name is found in a file and in a tree,
# in either spelling and any case; a clean tree gives nothing.
mkdir -p "$scratch/hyg/docs" "$scratch/hyg/modules/acme"
printf 'A neutral page about the acme plugin.\n' >"$scratch/hyg/docs/page.md"
printf 'package acme\n' >"$scratch/hyg/modules/acme/acme.go"
if [[ -n "$(hygiene_scan "$scratch/hyg" docs modules)" ]]; then
echo "refuse: self-test hygiene_scan found a name in a clean tree" >&2
return 1
fi
local plant
for plant in 'the Fonoteka app' 'Płytarium' 'plytarium' 'FONOTEKA.GO'; do
printf '// used by %s\n' "$plant" >"$scratch/hyg/modules/acme/acme.go"
if [[ -z "$(hygiene_scan "$scratch/hyg" docs modules)" ]]; then
echo "refuse: self-test hygiene_scan missed a planted application name" >&2
return 1
fi
done
# Evidence: a complete record passes; a missing threat row, a wrong
# disposition, a high threat without a removal row, a waiver for a
# threat that may not be waived, a removal row missing from the review,
# an open threat, a pending validation row and a draft validation file
# each refuse.
mkdir -p "$scratch/phase"
printf '| T-12.1-90 | Spoofing | x | high | mitigate | y |\n| T-12.1-91 | Tampering | x | low | accept | y |\n| T-12.1-92 | Tampering | x | high | mitigate | y |\n' >"$scratch/phase/12.1-01-PLAN.md"
cat >"$scratch/review.md" <<'EOR'
threats_open: 0
T-12-18 is revisited.
| T-12.1-90 | Spoofing | x | high | mitigate | y | TestAlpha | pass | none |
| T-12.1-91 | Tampering | x | low | accept | y | none (accepted) | accepted | none |
| T-12.1-92 | Tampering | x | high | mitigate | y | check-phase12.1.sh --hygiene | pass | none |
| RC-90 | T-12.1-90 | f | a | b | fails |
| NR-01 | T-12.1-92 | a process control with no code to remove |
EOR
cat >"$scratch/validation.md" <<'EOV'
status: validated
nyquist_compliant: true
| 12.1-01-T1 | D-09 | `go test -run '^TestAlpha$'` | ✅ green |
EOV
local pairs='RC-90 T-12.1-90'
out="$(evidence_check "$scratch/phase" "$scratch/review.md" "$scratch/validation.md" "T-12.1-92" "$pairs" 2>&1)" || {
echo "refuse: self-test evidence_check rejected a complete record: $out" >&2
return 1
}
local case waive
for case in missing-row disposition removal waiver table open cite pending draft tasks; do
cp "$scratch/review.md" "$scratch/review.case"
cp "$scratch/validation.md" "$scratch/validation.case"
waive="T-12.1-92"
case "$case" in
missing-row) sed -i '/^| T-12.1-91 /d' "$scratch/review.case" ;;
disposition) sed -i 's/| low | accept |/| low | mitigate |/' "$scratch/review.case" ;;
removal) sed -i '/^| RC-90 /d' "$scratch/review.case" ;;
waiver) waive="" ;;
table) pairs='RC-90 T-12.1-90
RC-91 T-12.1-92' ;;
open) sed -i 's/^threats_open: 0$/threats_open: 1/' "$scratch/review.case" ;;
cite) sed -i '/T-12-18/d' "$scratch/review.case" ;;
pending) printf '| 12.1-02-T1 | D-10 | x | ⬜ pending |\n' >>"$scratch/validation.case" ;;
draft) sed -i 's/^status: validated$/status: draft/' "$scratch/validation.case" ;;
tasks) sed -i 's/^| 12.1-01-T1 /| TBD-task /' "$scratch/validation.case" ;;
esac
if evidence_check "$scratch/phase" "$scratch/review.case" "$scratch/validation.case" "$waive" "$pairs" >/dev/null 2>&1; then
echo "refuse: self-test evidence_check accepted the $case plant" >&2
return 1
fi
pairs='RC-90 T-12.1-90'
done
# The removal harness reports a guarded mutation as failing, refuses a
# mutation whose test still passes, an anchor that is not unique and a
# dirty tracked file, and restores the file byte for byte.
local fake="$scratch/fake"
mkdir -p "$fake/modules/acme"
printf 'module example.test/acme\n\ngo 1.27\n' >"$fake/go.mod"
printf 'package acme\n\nfunc Guard(n int) bool {\n\tif n > 3 {\n\t\treturn false\n\t}\n\treturn true\n}\n' >"$fake/modules/acme/acme.go"
printf 'package acme\n\nimport "testing"\n\nfunc TestGuard(t *testing.T) {\n\tif Guard(4) {\n\t\tt.Fatal("guard removed")\n\t}\n}\n\nfunc TestOther(t *testing.T) {}\n' >"$fake/modules/acme/acme_test.go"
cp "$fake/modules/acme/acme.go" "$scratch/acme.go.saved"
local table="$scratch/table.json"
printf '[["RC-T1","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","root","./modules/acme","^TestGuard$"]]' >"$table"
out="$(removal_harness_in "$fake" "$table" 2>&1)" || {
echo "refuse: self-test removal harness did not catch a guarded mutation: $out" >&2
return 1
}
grep -q "RC-T1 T-X modules/acme/acme.go: fails as required" <<<"$out" || {
echo "refuse: self-test removal harness output: $out" >&2
return 1
}
cmp -s "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" || {
echo "refuse: self-test removal harness did not restore the file" >&2
return 1
}
printf '[["RC-T2","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","root","./modules/acme","^TestOther$"]]' >"$table"
if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then
echo "refuse: self-test removal harness accepted a mutation whose test passes: $out" >&2
return 1
fi
grep -q "RC-T2 T-X modules/acme/acme.go: SURVIVED" <<<"$out" || {
echo "refuse: self-test removal harness refused a surviving mutation for the wrong reason: $out" >&2
return 1
}
cmp -s "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" || {
echo "refuse: self-test removal harness did not restore the file after a surviving mutation" >&2
return 1
}
printf '[["RC-T5","T-X","root","modules/acme/acme.go","if n > 3 {","if n > 3 {{","root","./modules/acme","^TestGuard$"]]' >"$table"
if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then
echo "refuse: self-test removal harness counted a build failure as a failing test: $out" >&2
return 1
fi
printf '[["RC-T3","T-X","root","modules/acme/acme.go","return","x","root","./modules/acme","^TestGuard$"]]' >"$table"
if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then
echo "refuse: self-test removal harness accepted a non-unique anchor" >&2
return 1
fi
grep -q "anchor occurs 2 times" <<<"$out" || {
echo "refuse: self-test removal harness refused a non-unique anchor for the wrong reason: $out" >&2
return 1
}
(cd "$fake" && git init -q && git add -A && git -c user.email=gate@example.test -c user.name=gate commit -qm init) >/dev/null
printf '// local edit\n' >>"$fake/modules/acme/acme.go"
printf '[["RC-T4","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","root","./modules/acme","^TestGuard$"]]' >"$table"
if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then
echo "refuse: self-test removal harness mutated a dirty file" >&2
return 1
fi
grep -q "is dirty" <<<"$out" || {
echo "refuse: self-test removal harness refused a dirty file for the wrong reason: $out" >&2
return 1
}
# The workspace reader finds every module of a go.work and refuses an
# empty one.
mkdir -p "$scratch/ws"
printf 'go 1.27.0\n\nuse (\n\t.\n\t./plugins/acme/one\n\t./plugins/acme/two // comment\n)\n' >"$scratch/ws/go.work"
out="$(workspace_modules "$scratch/ws" | tr '\n' ' ')"
[[ "$out" == ". ./plugins/acme/one ./plugins/acme/two " ]] || {
echo "refuse: self-test workspace_modules read: $out" >&2
return 1
}
printf 'go 1.27.0\n' >"$scratch/ws/go.work"
if workspace_modules "$scratch/ws" >/dev/null 2>&1; then
echo "refuse: self-test workspace_modules accepted a go.work without modules" >&2
return 1
fi
echo "phase12.1 self-test passed" echo "phase12.1 self-test passed"
} }
run_security() { run_all() {
need_app local stage
named "$ROOT" ./modules/cabana "${SECURITY_CABANA[@]}" for stage in self-test go security coverage spa openapi dist docs hygiene app evidence; do
named "$APP" "$PLUGIN" "${SECURITY_PLUGIN[@]}" # Each stage runs in its own process, so errexit stays in force
named "$APP" "$PLUGIN/classes" "${SECURITY_PLUGIN_CLASSES[@]}" # inside it (bash ignores set -e in a function called from an if).
echo "phase12.1 security passed" local started=$SECONDS
if bash "${BASH_SOURCE[0]}" "--$stage"; then
echo "PASS $stage ($((SECONDS - started)) s)"
else
echo "FAIL $stage ($((SECONDS - started)) s)"
exit 1
fi
done
echo "phase12.1 all stages passed (removal is run separately)"
} }
not_implemented() { APP_ERR="$(mktemp)"
echo "refuse: stage --$1 is not implemented" >&2 trap 'rm -f "$APP_ERR"' EXIT
return 1
}
case "${1:-}" in case "${1:-}" in
--self-test) run_self_test ;; --self-test) run_self_test ;;
--go) not_implemented go ;; --go) run_go ;;
--security) run_security ;; --security) run_security ;;
--removal) not_implemented removal ;; --removal) run_removal ;;
--coverage) not_implemented coverage ;; --coverage) run_coverage ;;
--spa) not_implemented spa ;; --spa) run_spa ;;
--openapi) not_implemented openapi ;; --openapi) run_openapi ;;
--dist) not_implemented dist ;; --dist) run_dist ;;
--docs) not_implemented docs ;; --docs) run_docs ;;
--hygiene) not_implemented hygiene ;; --hygiene) run_hygiene ;;
--app) not_implemented app ;; --app) run_app ;;
--evidence) not_implemented evidence ;; --evidence) run_evidence ;;
--all) not_implemented all ;; --all) run_all ;;
*) usage ;; *) usage ;;
esac esac