test(14-06): check-phase14.sh runs the named tests, the coverage floors and the removal harness
- --named runs every test the validation map and the security review name, by package, the fonoteka, discogs, golem and feedback suites with -race - --coverage enforces the 80% floor on the framework packages, classes/ discogs, console and every package of both shared plugins, and on each of the 132 Phase 14 functions of the fonoteka root, classes and controllers/api (two exemptions with their reasons) - --removal applies 43 anchor-exact mutations, one or more per mitigated threat, requires the named test to fail on an assertion and restores each file byte for byte; the self-test plants each refusal
This commit is contained in:
@@ -8,6 +8,12 @@
|
||||
# coverage floor missed, a corpus secret or an evidence gap. --self-test
|
||||
# proves each detector fails closed on planted inputs.
|
||||
#
|
||||
# --removal is the anchor-exact mutation harness behind the RC rows of
|
||||
# 14-SECURITY-REVIEW.md: it removes one protection at a time, requires its
|
||||
# named test to fail on an assertion, and restores the file byte for byte
|
||||
# (checked with cmp). It refuses a file with uncommitted changes and edits
|
||||
# tracked source while it runs, so it is not part of --all.
|
||||
#
|
||||
# Framework commands run in summercms.go; application commands run in the
|
||||
# sibling repository named by PHASE14_APP (default ../fonoteka.go), whose
|
||||
# workspace also holds the shared plugins sm-user-plugin, sm-golem-plugin
|
||||
@@ -40,6 +46,9 @@ usage:
|
||||
check-phase14.sh --self-test
|
||||
check-phase14.sh --go
|
||||
check-phase14.sh --parity
|
||||
check-phase14.sh --named
|
||||
check-phase14.sh --removal
|
||||
check-phase14.sh --coverage
|
||||
check-phase14.sh --all
|
||||
EOF
|
||||
exit 2
|
||||
@@ -323,6 +332,396 @@ run_parity() {
|
||||
echo "phase14 parity passed ($EXPECTED_ROUTES recorded, $EXPECTED_PORTED ported and passing, 0 failing, $EXPECTED_PENDING pending)"
|
||||
}
|
||||
|
||||
# The named tests: every test 14-VALIDATION.md and 14-SECURITY-REVIEW.md
|
||||
# name, by package. The evidence stage refuses a validation or review row
|
||||
# naming a test missing here.
|
||||
NAMED_ROOT_FETCHGUARD="TestClientModes TestClientSchemeGuard TestClientMultipart TestClientBodyCap TestClientPutJSONHeaderOrder TestTransportSeamIsCodeOnly TestClientPostJSONThroughUpstreamFake TestClientNeverFollowsRedirects TestIsPrivateAddr"
|
||||
NAMED_ROOT_TIDE="TestUpstreamFakeRejectsMismatchedRequest TestUpstreamFakeHashesBase64Bodies TestWriteUpstreamRefusesUnmaskedCredential TestUpstreamProxyScriptMode TestUpstreamProxyRefusesNonLoopback TestEnsureParityCA TestUpstreamProxyMultipartAndForwardGuard TestUpstreamSidecarRefusals"
|
||||
NAMED_ROOT_SUNSCREEN="TestRedactHandler TestScrub TestInstallDefault"
|
||||
NAMED_ROOT_SURF="TestRecoverHidesPanicDetails TestCORSOptionsMatchesLaravel"
|
||||
NAMED_ROOT_BEACHCOMBER="TestDropIndex"
|
||||
NAMED_ROOT_TYPESENSE="TestEngineDropIndex TestEngineEnsureIndex"
|
||||
NAMED_ROOT_BUILD="TestGenerateMainInstallsRedactingLogger"
|
||||
NAMED_ROOT_SUMMER="TestParityCommandContract TestToolCommandNames"
|
||||
NAMED_APP_FONOTEKA="TestCsvMatchJob TestCsvImportJob TestCsvWR02 TestCsvRowPickSeam TestCsvRowPickResolves TestWishlistDigestJob TestWishlistDigestWorkerRegistered TestFonotekaSchedulePrune TestDiscogsMatchRoute TestDiscogsInboundLimits TestApplyReleaseModes TestDiscogsImportRoute TestCoverPriceRoute TestDiscogsCredentialTestRoute TestAICredentialTestRoute TestAdminVisionTier TestRecognizeRoutes TestResolveAIConfigPrecedence TestRouteTablePhase14 TestRouteTablePhase13 TestRouteTablePhase12 TestPhase14Threats TestPhase14Edges TestPhase14Jobs TestPhase14HandlersFailClosed FuzzWriteEndpoints"
|
||||
NAMED_APP_CLASSES="TestRecognizeAlbums TestPhase14Classes"
|
||||
NAMED_APP_DISCOGS="TestPHPTruthDiscogs TestClientGetRelease TestBucketID TestDiscogsClientStatuses TestRateLimiterBudget TestRateLimiterSyncFromHeaders TestRegisterRetryAfter TestDiscogsDomainVectors TestRateLimiterPostgresAcquire TestDiscogsRateWindowConcurrent TestCoverFetcherHostLock TestNumberFormat4"
|
||||
NAMED_APP_CONSOLE="TestPruneNotifications TestReindexCommand"
|
||||
NAMED_APP_API="TestPhase14ControllerHelpers"
|
||||
NAMED_APP_GOLEM="TestGolemPluginBoot TestImportSettings TestAdminModelsForm TestGolemAdminSchemas TestGolemImportCommand"
|
||||
NAMED_APP_GOLEM_PROVIDERS="TestOpenAIAdapterPayload TestAnthropicAdapterPayload"
|
||||
NAMED_APP_GOLEM_SERVICES="TestAIServiceFailures TestAIServiceStream TestDefaultModelQuirk TestAdminModelTrusted"
|
||||
NAMED_APP_GOLEM_FACTORIES="TestPromptFactory"
|
||||
NAMED_APP_GOLEM_SECURITY="TestSSRFGuard"
|
||||
NAMED_APP_FEEDBACK="TestFeedbackPluginBoot TestFeedbackConfig TestKeyMatches TestURLHost TestAllowedOriginHosts TestFeedbackSubmit TestMeHidden TestFeedbackApiArrayHook TestFeedbackOptionsPreflight TestEmbedJSServed TestFeedbackImportSettings TestFeedbackAdminSchemas TestFeedbackEdges"
|
||||
NAMED_APP_FEEDBACK_CLASSES="TestSyncG15Office TestTaskText TestImageGuardCopy"
|
||||
NAMED_APP_PARITY="TestParityCorpus TestCheckCorpusPortedCaseStatus TestUpstreamSidecarsAreReplayed TestFeedbackJobSidecarMatchesPlugin TestFonotekaNuxtFlows TestCheckCorpusUpstreamCredential TestParityContract TestSchemaMatchesPHPSnapshot"
|
||||
|
||||
all_named() {
|
||||
echo "$NAMED_ROOT_FETCHGUARD $NAMED_ROOT_TIDE $NAMED_ROOT_SUNSCREEN $NAMED_ROOT_SURF $NAMED_ROOT_BEACHCOMBER $NAMED_ROOT_TYPESENSE $NAMED_ROOT_BUILD $NAMED_ROOT_SUMMER $NAMED_APP_FONOTEKA $NAMED_APP_CLASSES $NAMED_APP_DISCOGS $NAMED_APP_CONSOLE $NAMED_APP_API $NAMED_APP_GOLEM $NAMED_APP_GOLEM_PROVIDERS $NAMED_APP_GOLEM_SERVICES $NAMED_APP_GOLEM_FACTORIES $NAMED_APP_GOLEM_SECURITY $NAMED_APP_FEEDBACK $NAMED_APP_FEEDBACK_CLASSES $NAMED_APP_PARITY"
|
||||
}
|
||||
|
||||
run_named() {
|
||||
phase14_tests "$ROOT" ./modules/fetchguard $NAMED_ROOT_FETCHGUARD
|
||||
phase14_tests "$ROOT" ./modules/tide $NAMED_ROOT_TIDE
|
||||
phase14_tests "$ROOT" ./modules/sunscreen $NAMED_ROOT_SUNSCREEN
|
||||
phase14_tests "$ROOT" ./modules/surf $NAMED_ROOT_SURF
|
||||
phase14_tests "$ROOT" ./modules/beachcomber $NAMED_ROOT_BEACHCOMBER
|
||||
phase14_tests "$ROOT" ./modules/beachcomber/typesense $NAMED_ROOT_TYPESENSE
|
||||
phase14_tests "$ROOT" ./internal/build $NAMED_ROOT_BUILD
|
||||
phase14_tests "$ROOT" ./cmd/summer $NAMED_ROOT_SUMMER
|
||||
phase14_tests "$APP" ./plugins/golem15/fonoteka -race $NAMED_APP_FONOTEKA
|
||||
phase14_tests "$APP" ./plugins/golem15/fonoteka/classes $NAMED_APP_CLASSES
|
||||
phase14_tests "$APP" ./plugins/golem15/fonoteka/classes/discogs -race $NAMED_APP_DISCOGS
|
||||
phase14_tests "$APP" ./plugins/golem15/fonoteka/console $NAMED_APP_CONSOLE
|
||||
phase14_tests "$APP" ./plugins/golem15/fonoteka/controllers/api $NAMED_APP_API
|
||||
phase14_tests "$APP" ./plugins/golem15/golem -race $NAMED_APP_GOLEM
|
||||
phase14_tests "$APP" ./plugins/golem15/golem/classes/providers $NAMED_APP_GOLEM_PROVIDERS
|
||||
phase14_tests "$APP" ./plugins/golem15/golem/classes/services -race $NAMED_APP_GOLEM_SERVICES
|
||||
phase14_tests "$APP" ./plugins/golem15/golem/classes/factories $NAMED_APP_GOLEM_FACTORIES
|
||||
phase14_tests "$APP" ./plugins/golem15/golem/classes/security $NAMED_APP_GOLEM_SECURITY
|
||||
phase14_tests "$APP" ./plugins/golem15/feedback -race $NAMED_APP_FEEDBACK
|
||||
phase14_tests "$APP" ./plugins/golem15/feedback/classes $NAMED_APP_FEEDBACK_CLASSES
|
||||
phase14_tests "$APP" ./parity $NAMED_APP_PARITY
|
||||
echo "phase14 named 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_ONLY limits the report
|
||||
# to packages whose import path ends with one of its words; COVERAGE_SKIP
|
||||
# drops packages whose import path ends with one of its words (each a
|
||||
# test-only helper named, with its reason, in COVERAGE_EXEMPT_PACKAGES).
|
||||
coverage_report() {
|
||||
python3 - "$@" <<'PY'
|
||||
import collections, os, sys
|
||||
floor = float(sys.argv[1])
|
||||
only = os.environ.get("COVERAGE_ONLY", "").split()
|
||||
skip = os.environ.get("COVERAGE_SKIP", "").split()
|
||||
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]
|
||||
if only and not any(pkg.endswith(o) for o in only):
|
||||
continue
|
||||
if any(pkg.endswith(s) for s in skip):
|
||||
continue
|
||||
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}%")
|
||||
if pct < floor:
|
||||
low.append(f"{pkg} {pct:.1f}%")
|
||||
if low:
|
||||
print(f"refuse: below the {floor:.1f}% coverage floor: " + ", ".join(low), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
# PHASE14_FUNCS: every function Phase 14 added or changed in the fonoteka
|
||||
# plugin's root, classes and controllers/api packages (a function whose
|
||||
# lines the 14-02..14-05 diff touched), by file. Each must reach the
|
||||
# function floor by go tool cover -func over every test of the plugin.
|
||||
PHASE14_FUNCS='classes/ai_config_resolver.go SetAdminVisionModel AdminVisionModel ResolveAIConfig UserAIConfig ModelConfig orgAIConfig aiConfigFrom
|
||||
classes/album_recognition.go Error RecognizeAlbums finishReason stripFences decodeRecognition normalizeRecognition sortedJSONKeys coalesce castString nullableString recognitionYear recognitionFormat recognitionTracklist collectionGenreNames recognitionLanguage RecognitionPrompt
|
||||
classes/album_write_service.go SetAlbumField SaveAlbumRow albumFillValue isEmptyFill withTracklistText albumPivotCount FillEmptyFromCsv CreateCsvAlbum ImportCsvCovers csvInputOf ApplyCsvFill ApplyCsvOverwrite CreateCanonicalCsvAlbum ResolveCsvStyleIDs ResolveGenreID SyncCsvRating
|
||||
classes/cover_importer.go NewGuardedCoverImporter allowHosts fetch
|
||||
classes/csv_canonical_matcher.go MatchCanonicalCsvAlbum
|
||||
classes/csv_import_service.go UpdateCsvMapping FetchRelease UpdateCsvRow saveCsvRowLocked lockCsvImport CancelCsvImport
|
||||
classes/gates.go AIAllowed
|
||||
classes/php_values.go PHPIsNumeric PHPFloatString PHPFloatOf
|
||||
controllers/api/album_cover_fetch_controller.go AlbumCoverPriceFetch
|
||||
controllers/api/credentials_controller.go DiscogsCredentialTest AICredentialTest aiTestConfig
|
||||
controllers/api/discogs_import_controller.go DiscogsImport
|
||||
controllers/api/inbound_limits.go NewInboundLimits DiscogsMissing DiscogsImport Recognize
|
||||
controllers/api/recognize_controller.go AlbumRecognize
|
||||
controllers/api/release_match_controller.go discogsLang appConfig writeDiscogsDisabled discogsAllowed discogsGateAndLimit validateDiscogsInput writeDiscogsCallError discogsErrorClass discogsClientFor AlbumReleaseMatch searchCandidates mapValue phpListValues filled yearWithin mediumMatches AlbumReleaseMatchDraft draftAlbum AlbumApplyRelease applyRelease appliedAlbumDTO discogsCoverImporter nonNil
|
||||
controllers/api/wishlist_release_match_controller.go WishlistReleaseMatch WishlistApplyRelease
|
||||
csv_import_job.go importCsv deliverCsvImport setStatus importerCanWrite stopUnauthorized fail writeOptions writeRow markRowWritten isCanonicalCsvRow inputForRow csvFieldsMap rowCellList decodePHPObject
|
||||
csv_match_job.go matchCsv deliverCsvMatch loadUser lostImport fail pause matchRow searchResults saveRow unwrapResults decodeRowCells rowCell nullableString orEmptyMap emptyCandidates
|
||||
discogs_wiring.go FetchRelease discogsErrorClass
|
||||
golem_wiring.go golemVisionModel installGolemWiring
|
||||
jobs.go Jobs
|
||||
mail.go MailTemplates
|
||||
plugin.go inboundLimits Requires Register Boot Commands
|
||||
routes.go Routes
|
||||
wishlist_digest_job.go sendWishlistDigest deliverWishlistDigest mailWishlistDigest'
|
||||
|
||||
# PHASE14_FUNC_EXEMPT: functions held below the floor, with the reason.
|
||||
# Each still has to appear in the profile.
|
||||
PHASE14_FUNC_EXEMPT='classes/cover_importer.go fetch the one-shot fetchguard.Fetch path ignores the WithTransport seam, so its 2xx and status branches run only against the live Discogs image hosts (deferred-items.md, 14-03)
|
||||
controllers/api/release_match_controller.go validateDiscogsInput lagoon.ValidateRequest fails only on a malformed rule table or a database rule, and the Discogs tables have neither'
|
||||
|
||||
# COVERAGE_EXEMPT_PACKAGES: packages outside the floor, with the reason.
|
||||
COVERAGE_EXEMPT_PACKAGES='internal/pgtest test-only helper of each shared plugin: its uncovered lines are the container start failures'
|
||||
|
||||
# funcs_floor FLOOR SPEC EXEMPT reads go tool cover -func output on stdin
|
||||
# and refuses a SPEC function below FLOOR (unless EXEMPT names it with a
|
||||
# reason) or missing from the profile.
|
||||
funcs_floor() {
|
||||
python3 -c '
|
||||
import sys
|
||||
floor, spec, exempt = float(sys.argv[1]), sys.argv[2], sys.argv[3]
|
||||
want = {}
|
||||
for line in spec.strip().splitlines():
|
||||
parts = line.split()
|
||||
for name in parts[1:]:
|
||||
want.setdefault((parts[0], name), [])
|
||||
skip = {}
|
||||
for line in exempt.strip().splitlines():
|
||||
parts = line.split(None, 2)
|
||||
if len(parts) < 3 or not parts[2].strip():
|
||||
print(f"refuse: exemption without a reason: {line}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
skip[(parts[0], parts[1])] = parts[2]
|
||||
for line in sys.stdin:
|
||||
parts = line.split()
|
||||
if len(parts) < 3 or parts[0] == "total:":
|
||||
continue
|
||||
path = parts[0].rsplit(":", 2)[0]
|
||||
for (f, name) in want:
|
||||
if parts[1] == name and (path == f or path.endswith("/" + f)):
|
||||
want[(f, name)].append(float(parts[-1].rstrip("%")))
|
||||
low, missing = [], []
|
||||
for (f, name), pcts in sorted(want.items()):
|
||||
if not pcts:
|
||||
missing.append(f + " " + name)
|
||||
continue
|
||||
pct = min(pcts)
|
||||
note = ""
|
||||
if (f, name) in skip:
|
||||
note = " (exempt: " + skip[(f, name)] + ")"
|
||||
elif pct < floor:
|
||||
low.append(f"{f} {name} {pct:.1f}%")
|
||||
print(f"coverage {f} {name} {pct:.1f}%{note}")
|
||||
if missing:
|
||||
print("refuse: Phase 14 functions missing from the profile: " + ", ".join(missing), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if low:
|
||||
print(f"refuse: below the {floor:.0f}% function floor: " + ", ".join(low), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
' "$@"
|
||||
}
|
||||
|
||||
# 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
|
||||
tail -n 40 "$log" >&2
|
||||
rm -f "$log"
|
||||
echo "refuse: go test -coverprofile $* in $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$log"
|
||||
}
|
||||
|
||||
run_coverage() {
|
||||
local dir
|
||||
dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$dir"' RETURN
|
||||
local pkg i=0
|
||||
# Framework packages changed in Phase 14: each package's own tests.
|
||||
for pkg in ./modules/fetchguard ./modules/tide ./modules/sunscreen ./modules/beachcomber ./modules/beachcomber/typesense; do
|
||||
i=$((i + 1))
|
||||
cover_profile "$ROOT" "$dir/root$i.out" "$pkg"
|
||||
done
|
||||
coverage_report "$COVERAGE_FLOOR" "$dir"/root*.out
|
||||
# The application: classes/discogs and console at the package floor,
|
||||
# every Phase 14 function of the root, classes and controllers/api
|
||||
# packages at the function floor.
|
||||
local f=./plugins/golem15/fonoteka
|
||||
cover_profile "$APP" "$dir/app.out" "$f/..." \
|
||||
-coverpkg="$f,$f/classes,$f/classes/discogs,$f/console,$f/controllers/api"
|
||||
COVERAGE_ONLY="/classes/discogs /console" coverage_report "$COVERAGE_FLOOR" "$dir/app.out"
|
||||
(cd "$APP" && go tool cover -func="$dir/app.out") | funcs_floor "$COVERAGE_FLOOR" "$PHASE14_FUNCS" "$PHASE14_FUNC_EXEMPT"
|
||||
# The two shared plugins: every package at the floor over all the
|
||||
# plugin's tests, the test-only helper excepted.
|
||||
local skip
|
||||
skip="$(awk '{print $1}' <<<"$COVERAGE_EXEMPT_PACKAGES" | tr '\n' ' ')"
|
||||
cover_profile "$APP" "$dir/golem.out" ./plugins/golem15/golem/... -coverpkg=./plugins/golem15/golem/...
|
||||
COVERAGE_SKIP="$skip" coverage_report "$COVERAGE_FLOOR" "$dir/golem.out"
|
||||
cover_profile "$APP" "$dir/feedback.out" ./plugins/golem15/feedback/... -coverpkg=./plugins/golem15/feedback/...
|
||||
COVERAGE_SKIP="$skip" coverage_report "$COVERAGE_FLOOR" "$dir/feedback.out"
|
||||
echo "phase14 coverage passed"
|
||||
}
|
||||
|
||||
# removal_table: the RC rows of 14-SECURITY-REVIEW.md. Fields: id, threat,
|
||||
# repo (root|app|script), file, anchor, replacement, package, test regex.
|
||||
# Anchors must occur exactly once. app files include the shared plugins'
|
||||
# submodule checkouts; a dirty file there is refused like any other.
|
||||
removal_table() {
|
||||
cat <<'EOF'
|
||||
[
|
||||
["RC-01", "T-14-01", "root", "modules/fetchguard/client.go", "\tif scheme != \"https\" {\n\t\treturn &Error{Reason: ReasonScheme}\n\t}\n\tif c.policy.Mode == AllowHostsMode", "\tif false && scheme != \"https\" {\n\t\treturn &Error{Reason: ReasonScheme}\n\t}\n\tif c.policy.Mode == AllowHostsMode", "./modules/fetchguard", "^TestClientSchemeGuard$"],
|
||||
["RC-02", "T-14-01", "root", "modules/fetchguard/fetch.go", "\t\t\treturn http.ErrUseLastResponse\n", "\t\t\treturn nil\n", "./modules/fetchguard", "^TestClientNeverFollowsRedirects$"],
|
||||
["RC-03", "T-14-02", "root", "modules/fetchguard/client.go", "type Client struct {\n", "type Client struct {\n\tTransport http.RoundTripper\n", "./modules/fetchguard", "^TestTransportSeamIsCodeOnly$"],
|
||||
["RC-04", "T-14-03", "root", "modules/sunscreen/sunscreen.go", "\t\"authorization\",\n", "", "./modules/sunscreen", "^TestRedactHandler$"],
|
||||
["RC-05", "T-14-04", "root", "modules/tide/upstream.go", "if isCredentialHeader(name) && !fullyMasked(v) {", "if false && isCredentialHeader(name) && !fullyMasked(v) {", "./modules/tide", "^TestWriteUpstreamRefusesUnmaskedCredential$"],
|
||||
["RC-06", "T-14-05", "root", "modules/tide/upstream_proxy.go", "\tif err := requireLoopbackAddr(cfg.Listen); err != nil {", "\tif err := error(nil); err != nil {", "./modules/tide", "^TestUpstreamProxyRefusesNonLoopback$"],
|
||||
["RC-07", "T-14-06", "root", "modules/fetchguard/client.go", "resp.Body = &cappedBody{rc: resp.Body, max: c.maxBytes}", "resp.Body = &cappedBody{rc: resp.Body, max: 1 << 62}", "./modules/fetchguard", "^TestClientBodyCap$"],
|
||||
["RC-08", "T-14-07", "root", "modules/beachcomber/typesense/engine.go", "\tcase code == http.StatusNotFound:\n\t\treturn false, nil", "\tcase code == http.StatusNotFound:\n\t\treturn true, nil", "./modules/beachcomber/typesense", "^TestEngineDropIndex$"],
|
||||
["RC-09", "T-14-08", "app", "plugins/golem15/fonoteka/classes/discogs/rate_limiter.go", "\treturn hex.EncodeToString(m.Sum(nil))[:32]", "\t_ = hex.EncodeToString(m.Sum(nil))[:32]\n\treturn token", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-08$"],
|
||||
["RC-10", "T-14-09", "app", "plugins/golem15/fonoteka/classes/discogs/rate_store.go", "OR w.hits < ?\nRETURNING", "OR w.hits < ? OR TRUE\nRETURNING", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-09$"],
|
||||
["RC-11", "T-14-10", "app", "plugins/golem15/fonoteka/classes/csv_import_service.go", "\t\tif !csvBeforeCommit(cur.Status) {\n\t\t\treturn ErrCsvAlreadyCommitted\n\t\t}\n\t\tvar rows []models.CsvImportRow", "\t\tvar rows []models.CsvImportRow", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-10$"],
|
||||
["RC-12", "T-14-11", "app", "plugins/golem15/fonoteka/classes/csv_import_service.go", "func csvAllowedCandidate(row *models.CsvImportRow, selected string) bool {\n\tif !row.CandidatesJSON.Valid {", "func csvAllowedCandidate(row *models.CsvImportRow, selected string) bool {\n\tif selected != \"\" {\n\t\treturn true\n\t}\n\tif !row.CandidatesJSON.Valid {", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-11$"],
|
||||
["RC-13", "T-14-12", "app", "plugins/golem15/fonoteka/csv_match_job.go", "\treturn m.d.Jobs.FailJob(ctx, jobID, map[string]any{\"error\": msg})\n}\n\n// pause", "\t_ = m.d.Jobs.FailJob(ctx, jobID, map[string]any{\"error\": msg})\n\treturn cause\n}\n\n// pause", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-12$"],
|
||||
["RC-14", "T-14-13", "app", "plugins/golem15/fonoteka/csv_import_job.go", "\treturn n > 0, err\n}\n\nfunc (w *csvWriter) stopUnauthorized", "\treturn true, err\n}\n\nfunc (w *csvWriter) stopUnauthorized", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-13$"],
|
||||
["RC-15", "T-14-14", "app", "plugins/golem15/fonoteka/wishlist_digest_job.go", "wishlists[0].Kind != \"wishlist\" || ", "", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-14$"],
|
||||
["RC-16", "T-14-15", "app", "plugins/golem15/fonoteka/console/reindex.go", "\t\t\tif tenantless > 0 {\n\t\t\t\treturn fail(reindexTenantless)\n\t\t\t}\n", "", "./plugins/golem15/fonoteka/console", "^TestReindexCommand$"],
|
||||
["RC-17", "T-14-16", "app", "plugins/golem15/fonoteka/controllers/api/release_match_controller.go", "func AlbumReleaseMatch(app *backpack.App, limits *InboundLimits) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\ts, ok := resolveAlbumScope(w, r, app)\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t\talbum, ok := s.findAlbum(w, r)", "func AlbumReleaseMatch(app *backpack.App, limits *InboundLimits) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\ts, ok := resolveAlbumScope(w, r, app)\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t\talbum, ok := &models.Album{ID: pathID(r, \"id\")}, true", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-16$"],
|
||||
["RC-18", "T-14-17", "app", "plugins/golem15/fonoteka/controllers/api/inbound_limits.go", "\tdiscogsMissingMax = 60\n", "\tdiscogsMissingMax = 6000\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-17$"],
|
||||
["RC-19", "T-14-18", "app", "plugins/golem15/fonoteka/classes/cover_importer.go", "\treturn host == \"discogs.com\" || (ci.HostSuffix != \"\" && strings.HasSuffix(host, strings.ToLower(ci.HostSuffix)))", "\treturn host != \"\"", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-18$"],
|
||||
["RC-20", "T-14-19", "app", "plugins/golem15/fonoteka/classes/discogs/applicator.go", "\t\t\tif !overwriteAll && !isBlankValue(current) {\n\t\t\t\tcontinue\n\t\t\t}\n", "", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-19$"],
|
||||
["RC-21", "T-14-19", "app", "plugins/golem15/fonoteka/classes/discogs/applicator.go", "\t\t\tif dryRun && slices.Contains(applicatorDraftFields, field) {", "\t\t\tif false && dryRun && slices.Contains(applicatorDraftFields, field) {", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-19$"],
|
||||
["RC-22", "T-14-20", "app", "plugins/golem15/fonoteka/routes.go", "api.AlbumCoverPriceFetch(p.app), \"inv.scope:write\", \"throttle:12,1\")", "api.AlbumCoverPriceFetch(p.app), \"inv.scope:read\", \"throttle:12,1\")", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-20$"],
|
||||
["RC-23", "T-14-20", "app", "plugins/golem15/fonoteka/routes.go", "api.AlbumCoverPriceFetch(p.app), \"inv.scope:write\", \"throttle:12,1\")", "api.AlbumCoverPriceFetch(p.app), \"inv.scope:read\", \"throttle:12,1\")", "./plugins/golem15/fonoteka", "^TestRouteTablePhase14$"],
|
||||
["RC-24", "T-14-21", "app", "plugins/golem15/fonoteka/controllers/api/credentials_controller.go", "\t\t\twriteJSON(w, http.StatusOK, discogsTestResult{Error: discogsLang(r, app, \"unavailable\", nil)})", "\t\t\twriteJSON(w, http.StatusOK, discogsTestResult{Error: err.Error()})", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-21$"],
|
||||
["RC-25", "T-14-22", "app", "plugins/golem15/fonoteka/classes/ai_config_resolver.go", "\t\tif err := security.AssertSafeURL(ctx, *baseURL, security.AllowedHosts(cfg)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n", "\t\t_ = security.AllowedHosts(cfg)\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-22$"],
|
||||
["RC-26", "T-14-23", "app", "plugins/golem15/fonoteka/classes/ai_config_resolver.go", "\tc := &AIConfig{Adapter: \"openai\", APIKey: key, BaseURL: openAIBaseURL}", "\tc := &AIConfig{Adapter: \"openai\", APIKey: key, BaseURL: openAIBaseURL, Trusted: true}", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-23$"],
|
||||
["RC-27", "T-14-24", "app", "plugins/golem15/golem/models/ai_model.go", "gorm:\"column:api_key\" json:\"-\"", "gorm:\"column:api_key\" json:\"api_key\"", "./plugins/golem15/golem", "^TestGolemAdminSchemas$"],
|
||||
["RC-28", "T-14-24", "app", "plugins/golem15/golem/models/ai_model.go", "gorm:\"column:api_key\" json:\"-\"", "gorm:\"column:api_key\" json:\"api_key\"", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-24$"],
|
||||
["RC-29", "T-14-25", "app", "plugins/golem15/fonoteka/classes/album_recognition.go", "\tRecognitionMaxAlbums = 30\n", "\tRecognitionMaxAlbums = 3000\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-25$"],
|
||||
["RC-30", "T-14-26", "app", "plugins/golem15/fonoteka/controllers/api/inbound_limits.go", "\trecognizeMax = 10\n", "\trecognizeMax = 1000\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-26$"],
|
||||
["RC-31", "T-14-27", "app", "plugins/golem15/fonoteka/controllers/api/recognize_controller.go", "\t\tif !classes.IsAllowedImage(data) {", "\t\tif false && !classes.IsAllowedImage(data) {", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-27$"],
|
||||
["RC-32", "T-14-29", "app", "plugins/golem15/golem/README.md", "# golem15.golem\n\n", "# golem15.golem\n\nsk-PLANTEDsecretPLANTEDsecretPLANTED\n\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-29$"],
|
||||
["RC-33", "T-14-30", "app", "plugins/golem15/feedback/controllers/api/feedback_api_controller.go", "\treturn subtle.ConstantTimeCompare([]byte(settings.WidgetKey), []byte(key)) == 1", "\t_ = subtle.ConstantTimeCompare([]byte(settings.WidgetKey), []byte(key))\n\treturn true", "./plugins/golem15/feedback", "^TestFeedbackConfig$"],
|
||||
["RC-43", "T-14-30", "app", "plugins/golem15/feedback/controllers/api/feedback_api_controller.go", "\t\tif a == host {", "\t\tif a == host || a != host {", "./plugins/golem15/feedback", "^TestFeedbackConfig$"],
|
||||
["RC-34", "T-14-31", "app", "plugins/golem15/feedback/plugin.go", "BucketSubmit: {Max: 10, Decay: time.Minute, Key: byIP},", "BucketSubmit: {Max: 1000, Decay: time.Minute, Key: byIP},", "./plugins/golem15/feedback", "^TestFeedbackEdges$"],
|
||||
["RC-35", "T-14-32", "app", "plugins/golem15/feedback/controllers/api/feedback_api_controller.go", "\t\t\tif !classes.IsAllowedImage(data) {", "\t\t\tif false && !classes.IsAllowedImage(data) {", "./plugins/golem15/feedback", "^TestFeedbackSubmit$"],
|
||||
["RC-36", "T-14-33", "app", "plugins/golem15/feedback/classes/g15office_client.go", "\"Authorization\": {fetchguard.Bearer(c.token)},", "\"Authorization\": {fetchguard.Bearer(\"\")},", "./plugins/golem15/feedback/classes", "^TestSyncG15Office$"],
|
||||
["RC-37", "T-14-34", "app", "plugins/golem15/feedback/classes/g15office_client.go", "\t\tMode: fetchguard.AllowHostsMode,\n\t\tAllowHosts: []string{u.Hostname()},", "\t\tMode: fetchguard.TrustedMode,\n\t\tAllowHosts: []string{u.Hostname()},", "./plugins/golem15/feedback/classes", "^TestSyncG15Office$"],
|
||||
["RC-38", "T-14-35", "app", "plugins/golem15/feedback/controllers/api/me_hidden_controller.go", "models.SetWidgetHidden(ctx, gdb, user.ID, hidden)", "models.SetWidgetHidden(ctx, gdb, user.ID+1, hidden)", "./plugins/golem15/feedback", "^TestMeHidden$"],
|
||||
["RC-39", "T-14-36", "app", "plugins/golem15/feedback/models/submission/columns.yaml", " message:\n label: golem15.feedback::lang.submissions.message\n type: text", " message:\n label: golem15.feedback::lang.submissions.message\n type: partial", "./plugins/golem15/feedback", "^TestFeedbackAdminSchemas$"],
|
||||
["RC-40", "T-14-37", "script", "scripts/check-phase14.sh", "if total != routes or recorded != total or passing != ported or failing != 0 or unrecorded != 0 or pend != pending or passing + pend != total:\n print(", "if False:\n print(", "", "--self-test"],
|
||||
["RC-41", "T-14-37", "script", "scripts/check-phase14.sh", " if action == \"skip\" and test:", " if False:", "", "--self-test"],
|
||||
["RC-42", "T-14-38", "script", "scripts/check-phase14.sh", " if dirty:\n", " if False:\n", "", "--self-test"]
|
||||
]
|
||||
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 (or,
|
||||
# for the gate script, its --self-test on a mutated copy) and require it to
|
||||
# fail on an assertion, then restore the file and require cmp to match.
|
||||
# PHASE14_RC limits the run to the listed row ids.
|
||||
removal_harness() {
|
||||
python3 - "$1" "$ROOT" "$APP" <<'PY'
|
||||
import json, os, shutil, signal, subprocess, sys, tempfile
|
||||
table = json.load(open(sys.argv[1]))
|
||||
root, app = sys.argv[2], sys.argv[3]
|
||||
only = set(os.environ.get("PHASE14_RC", "").split())
|
||||
current = {}
|
||||
|
||||
def restore(*_):
|
||||
# A signal mid-run still puts the file back.
|
||||
if current:
|
||||
with open(current["path"], "wb") as fh:
|
||||
fh.write(current["original"])
|
||||
sys.exit(1)
|
||||
|
||||
signal.signal(signal.SIGINT, restore)
|
||||
signal.signal(signal.SIGTERM, restore)
|
||||
failures = 0
|
||||
for rc, threat, repo, rel, anchor, repl, pkg, run in table:
|
||||
if only and rc not in only:
|
||||
continue
|
||||
base = {"root": root, "app": app, "script": root}[repo]
|
||||
path = os.path.join(base, rel)
|
||||
tracked = subprocess.run(["git", "-C", os.path.dirname(path), "rev-parse", "--is-inside-work-tree"], capture_output=True, text=True).returncode == 0
|
||||
if tracked and repo != "script":
|
||||
dirty = subprocess.run(["git", "-C", os.path.dirname(path), "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="phase14-rc-")
|
||||
saved = os.path.join(scratch, "saved")
|
||||
shutil.copyfile(path, saved)
|
||||
try:
|
||||
if repo == "script":
|
||||
copy = os.path.join(scratch, os.path.basename(rel))
|
||||
open(copy, "w").write(mutated)
|
||||
env = dict(os.environ, PHASE14_ROOT=root, PHASE14_APP=app)
|
||||
proc = subprocess.run(["bash", copy, run], cwd=root, env=env, capture_output=True, text=True, timeout=900)
|
||||
out = proc.stdout + proc.stderr
|
||||
ok = proc.returncode != 0 and "refuse:" in out
|
||||
evidence = next((l for l in out.splitlines() if l.startswith("refuse:")), "")
|
||||
else:
|
||||
current.update(path=path, original=original)
|
||||
with open(path, "w") as fh:
|
||||
fh.write(mutated)
|
||||
proc = subprocess.run(["go", "test", pkg, "-run", run, "-count=1"], cwd=base, capture_output=True, text=True, timeout=1800)
|
||||
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)
|
||||
current.clear()
|
||||
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}", flush=True)
|
||||
if not ok:
|
||||
failures += 1
|
||||
if failures:
|
||||
print(f"refuse: {failures} removal check(s) survived", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
run_removal() {
|
||||
local table
|
||||
table="$(mktemp)"
|
||||
removal_table >"$table"
|
||||
if ! removal_harness "$table"; then
|
||||
rm -f "$table"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$table"
|
||||
echo "phase14 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"
|
||||
export GOWORK=off GOFLAGS=-mod=mod
|
||||
removal_harness "$table"
|
||||
)
|
||||
}
|
||||
|
||||
run_self_test() {
|
||||
bash -n "${BASH_SOURCE[0]}"
|
||||
expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestPhase14Threats"}'
|
||||
@@ -466,6 +865,148 @@ run_self_test() {
|
||||
fi
|
||||
PHASE14_REQUIRE="TestFlows $subs" expect_detect flow-missing 5 '{"Action":"pass","Package":"p","Test":"TestFlows"}
|
||||
{"Action":"pass","Package":"p","Test":"TestFlows/nuxt-a"}'
|
||||
|
||||
# The coverage report refuses a package under the floor and accepts one
|
||||
# over it; a block covered by any profile counts once; COVERAGE_ONLY
|
||||
# narrows the report and COVERAGE_SKIP drops an exempt package.
|
||||
printf 'mode: set\nexample.test/a/x.go:1.1,2.2 8 1\nexample.test/a/x.go:3.1,4.2 2 0\n' >"$scratch/p1"
|
||||
printf 'mode: set\nexample.test/a/x.go:3.1,4.2 2 1\nexample.test/b/y.go:1.1,2.2 5 0\nexample.test/b/y.go:3.1,4.2 5 1\n' >"$scratch/p2"
|
||||
local out
|
||||
out="$(coverage_report 80 "$scratch/p1" 2>&1)" || {
|
||||
echo "refuse: self-test coverage_report refused 80% at an 80% floor: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
if out="$(coverage_report 80 "$scratch/p1" "$scratch/p2" 2>&1)"; then
|
||||
echo "refuse: self-test coverage_report accepted a 50% package" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q "coverage example.test/a 100.0%" <<<"$out" || {
|
||||
echo "refuse: self-test coverage_report did not merge profiles: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
COVERAGE_ONLY="/a" coverage_report 80 "$scratch/p1" "$scratch/p2" >/dev/null 2>&1 || {
|
||||
echo "refuse: self-test COVERAGE_ONLY did not narrow the report" >&2
|
||||
exit 1
|
||||
}
|
||||
COVERAGE_SKIP="/b" coverage_report 80 "$scratch/p1" "$scratch/p2" >/dev/null 2>&1 || {
|
||||
echo "refuse: self-test COVERAGE_SKIP did not drop the exempt package" >&2
|
||||
exit 1
|
||||
}
|
||||
printf 'mode: set\n' >"$scratch/empty"
|
||||
if coverage_report 80 "$scratch/empty" 2>/dev/null; then
|
||||
echo "refuse: self-test coverage_report accepted an empty profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The function floor refuses a listed function below it or missing from
|
||||
# the profile, and an exemption without a reason; an exempt function
|
||||
# with a reason passes.
|
||||
printf 'example.test/p/csv_job.go:10:\tdeliver\t84.4%%\nexample.test/p/csv_job.go:40:\thelper\t79.9%%\nexample.test/p/other.go:5:\tdeliver\t10.0%%\ntotal:\t(statements)\t90.0%%\n' >"$scratch/func"
|
||||
funcs_floor 80 "csv_job.go deliver" "" <"$scratch/func" >/dev/null 2>&1 || {
|
||||
echo "refuse: self-test funcs_floor rejected a function over the floor" >&2
|
||||
exit 1
|
||||
}
|
||||
if funcs_floor 80 "csv_job.go deliver helper" "" <"$scratch/func" >/dev/null 2>&1; then
|
||||
echo "refuse: self-test funcs_floor accepted a 79.9% function" >&2
|
||||
exit 1
|
||||
fi
|
||||
if funcs_floor 80 "csv_job.go deliver gone" "" <"$scratch/func" >/dev/null 2>&1; then
|
||||
echo "refuse: self-test funcs_floor accepted a function missing from the profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
funcs_floor 80 "csv_job.go deliver helper" "csv_job.go helper a recorded reason" <"$scratch/func" >/dev/null 2>&1 || {
|
||||
echo "refuse: self-test funcs_floor refused an exempt function" >&2
|
||||
exit 1
|
||||
}
|
||||
if funcs_floor 80 "csv_job.go deliver helper" "csv_job.go helper" <"$scratch/func" >/dev/null 2>&1; then
|
||||
echo "refuse: self-test funcs_floor accepted an exemption without a reason" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The removal harness refuses an anchor that is not unique and a dirty
|
||||
# tracked file, restores the file byte for byte, and reports a mutation
|
||||
# whose test passes or does not build.
|
||||
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 {","./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
|
||||
exit 1
|
||||
}
|
||||
grep -q "RC-T1 T-X modules/acme/acme.go: fails as required" <<<"$out" || {
|
||||
echo "refuse: self-test removal harness output: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
cmp -s "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" || {
|
||||
echo "refuse: self-test removal harness did not restore the file" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '[["RC-T2","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","./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
|
||||
exit 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
|
||||
exit 1
|
||||
}
|
||||
printf '[["RC-T3","T-X","root","modules/acme/acme.go","return","x","./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
|
||||
exit 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
|
||||
exit 1
|
||||
}
|
||||
printf '[["RC-T5","T-X","root","modules/acme/acme.go","if n > 3 {","if n > 3 {\\n\\tundefinedCall()","./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" >&2
|
||||
exit 1
|
||||
fi
|
||||
cmp -s "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" || {
|
||||
echo "refuse: self-test removal harness did not restore after a build failure" >&2
|
||||
exit 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 {","./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
|
||||
exit 1
|
||||
fi
|
||||
grep -q "is dirty" <<<"$out" || {
|
||||
echo "refuse: self-test removal harness refused a dirty file for the wrong reason: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Every row of the real removal table names a unique anchor in the
|
||||
# current tree (the --removal stage would refuse it otherwise).
|
||||
removal_table >"$table"
|
||||
python3 - "$table" "$ROOT" "$APP" <<'PY' || exit 1
|
||||
import json, os, sys
|
||||
table, root, app = json.load(open(sys.argv[1])), sys.argv[2], sys.argv[3]
|
||||
ids = set()
|
||||
for rc, threat, repo, rel, anchor, repl, pkg, run in table:
|
||||
if rc in ids:
|
||||
print(f"refuse: self-test duplicate removal row {rc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
ids.add(rc)
|
||||
base = {"root": root, "app": app, "script": root}[repo]
|
||||
path = os.path.join(base, rel)
|
||||
if not os.path.isfile(path):
|
||||
print(f"refuse: self-test {rc}: {rel} is missing", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
n = open(path).read().count(anchor)
|
||||
if n != 1:
|
||||
print(f"refuse: self-test {rc}: anchor occurs {n} times in {rel}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
echo "phase14 self-test passed"
|
||||
}
|
||||
|
||||
@@ -473,10 +1014,16 @@ case "${1:-}" in
|
||||
--self-test) run_self_test ;;
|
||||
--go) run_go ;;
|
||||
--parity) run_parity ;;
|
||||
--named) run_named ;;
|
||||
--removal) run_removal ;;
|
||||
--coverage) run_coverage ;;
|
||||
--all)
|
||||
# --removal edits tracked source while it runs, so it runs on its own.
|
||||
run_self_test
|
||||
run_go
|
||||
run_parity
|
||||
run_named
|
||||
run_coverage
|
||||
echo "phase14 all passed"
|
||||
;;
|
||||
*) usage ;;
|
||||
|
||||
Reference in New Issue
Block a user