AtlatestRepositoryfolio

folio / tree / testsabotage-m0.sh

1#!/usr/bin/env bash
2# M0 sabotage driver -- the FOUR SABOTAGES NAMED IN THE PLAN, not substitutes.
3#
4# S1 drop an indented task-body line
5# S2 conflate comma/tag metadata
6# S3 rewrite a duplicate heading link ambiguously
7# S4 omit one unrecognized construct from the report
8#
9# Each is shown RED SEPARATELY, with a NAMED SIGNATURE -- a non-zero exit alone is
10# equally true of a gate that never ran.
12# RESTORES ARE VERIFIED BY sha256, never by `git checkout --`, which wipes
13# uncommitted work silently with exit 0.
15# THESE ARE PRODUCT-LAYER SABOTAGES: they mutate `tools/m0_convert.py`, the loaded
16# product source, not the gate's assertions. A suite sabotaged only at the
17# assertion layer is consistent with a stale module, a fixture that reimplements the
18# behaviour, and a wrong load path -- all three invisible, all three seen here.
20# bash test/sabotage-m0.sh
21set -uo pipefail
23repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
24cd "$repo" || exit 1
26TARGET="$repo/tools/m0_convert.py"
27BACKUP="$(mktemp)"
28cp "$TARGET" "$BACKUP"
29ORIG_SHA="$(sha256sum "$TARGET" | cut -d' ' -f1)"
31export XDG_CACHE_HOME="$(mktemp -d)"
33legs=0
34red=0
36restore() {
37 cp "$BACKUP" "$TARGET"
38 local now
39 now="$(sha256sum "$TARGET" | cut -d' ' -f1)"
40 if [ "$now" != "$ORIG_SHA" ]; then
41 echo " RESTORE FAILED: $now != $ORIG_SHA"
42 exit 1
43 fi
44 echo " restore verified by sha256: $now"
47trap 'cp "$BACKUP" "$TARGET"; rm -f "$BACKUP"' EXIT
49# run_leg <name> <description> <signature-grep> <arm> <python-patch>
50run_leg() {
51 local name="$1" desc="$2" signature="$3" arm="$4" patch="$5"
52 legs=$((legs + 1))
53 echo
54 echo "=== $name: $desc ==="
55 cp "$BACKUP" "$TARGET"
56 if ! guix shell python -- python3 - "$TARGET" <<EOF
57import sys, pathlib
58p = pathlib.Path(sys.argv[1]); t = p.read_text()
59$patch
60p.write_text(t)
61EOF
62 then
63 echo " PATCH FAILED to apply -- the sabotage could not be planted"
64 restore
65 return
66 fi
67 # The patch must actually have CHANGED the file. A sabotage that silently
68 # applied nothing would produce a green run indistinguishable from a fixed bug.
69 local sabotaged_sha
70 sabotaged_sha="$(sha256sum "$TARGET" | cut -d' ' -f1)"
71 if [ "$sabotaged_sha" = "$ORIG_SHA" ]; then
72 echo " SABOTAGE DID NOT CHANGE THE FILE -- leg is vacuous"
73 restore
74 return
75 fi
77 local out status
78 out="$(M0_GATE_ONLY_ARM="$arm" bash test/gate-m0.sh 2>&1)"
79 status=$?
80 if [ "$status" -eq 0 ]; then
81 echo " NOT RED: arm $arm stayed green under $name"
82 echo "$out" | tail -3 | sed 's/^/ /'
83 elif printf '%s' "$out" | grep -qF "$signature"; then
84 echo " RED with the named signature: $signature"
85 red=$((red + 1))
86 else
87 echo " RED, but NOT with the expected signature '$signature'"
88 echo "$out" | grep -i fail | head -3 | sed 's/^/ /'
89 fi
90 restore
93echo "M0 sabotage: target $TARGET"
94echo " original sha256: $ORIG_SHA"
96# --------------------------------------------------------------------------
97# S1 -- DROP AN INDENTED TASK-BODY LINE.
99# The body line is silently lost from the manuscript. Arm 1 (determinism) cannot
100# see this -- a converter that always drops the line is perfectly deterministic --
101# which is exactly why the accounting arm has to exist.
102# --------------------------------------------------------------------------
103run_leg "S1" "drop an indented task-body line" \
104 "reached no manuscript" 4 \
105 'old = """ body_lines.append({"t": "para", "line": j + 1, "lines": [j + 1],
106 "text": lines[j].strip()})"""
107new = """ if "nested body line" not in lines[j] and "Continuation" not in lines[j]:
108 body_lines.append({"t": "para", "line": j + 1, "lines": [j + 1],
109 "text": lines[j].strip()})"""
110assert old in t, "S1 anchor not found"
111t = t.replace(old, new, 1)'
113# --------------------------------------------------------------------------
114# S2 -- CONFLATE COMMA/TAG METADATA.
116# `{id: t-x, tags: alpha beta}` holds TWO separators: `,` between fields and ` `
117# WITHIN the tags field. Conflating them yields one tag "alpha beta", which fails
118# `valid-tag?` at `make-task`. The converter's own grammar filter then DROPS it
119# with a warning, so the manuscript still parses -- the accounting arm is what sees
120# it, via a tag count taken by a net that shares no code with the converter.
121# --------------------------------------------------------------------------
122run_leg "S2" "conflate comma/tag metadata" \
123 "task tags emitted" 4 \
124 'old = """ for tag in meta["tags"].split():"""
125new = """ for tag in [meta["tags"]]:"""
126assert old in t, "S2 anchor not found"
127t = t.replace(old, new, 1)'
129# --------------------------------------------------------------------------
130# S3 -- REWRITE A DUPLICATE HEADING LINK AMBIGUOUSLY.
132# The ruling is that a migration must not rename what it can instead report. This
133# sabotage makes the converter silently disambiguate -- the behaviour that was
134# REVERSED -- so the faithful-conversion property and its warning both vanish.
135# Arm 9 pins the faithful emission, so it is the arm that must go red.
136# --------------------------------------------------------------------------
137run_leg "S3" "rewrite a duplicate heading link ambiguously" \
138 "faithful 'notes' slugs" 9 \
139 'old = """ if self.disambiguate_anchors:
140 sid = f"{base}-{len(occurrences)}\""""
141new = """ sid = f"{base}-{len(occurrences)}\""""
142assert old in t, "S3 anchor not found"
143t = t.replace(old, new, 1)'
145# --------------------------------------------------------------------------
146# S4 -- OMIT ONE UNRECOGNIZED CONSTRUCT FROM THE REPORT.
148# The default dispatch arm is what makes the denominator EXACT rather than
149# approximately right. Removing a class from the report is the failure mode where
150# "absent" reads as "fired zero times".
152# NOTE: this leg is written to prove arm 5 CAN fail. Before trusting it, the
153# unsabotaged arm 5 is confirmed green above -- otherwise a leg that reddens for an
154# unrelated reason would look like coverage.
155# --------------------------------------------------------------------------
156run_leg "S4" "omit one unrecognized construct from the report" \
157 "omits warning class" 5 \
158 'old = """ out = {code: 0 for code in Warn.ALL}"""
159new = """ out = {code: 0 for code in Warn.ALL if code != Warn.W6}"""
160assert old in t, "S4 anchor not found"
161t = t.replace(old, new, 1)'
163echo
164echo "final sha256: $(sha256sum "$TARGET" | cut -d' ' -f1)"
165if [ "$(sha256sum "$TARGET" | cut -d' ' -f1)" != "$ORIG_SHA" ]; then
166 echo "M0_SABOTAGE_RESTORE_FAILED"
167 exit 1
168fi
169echo "M0_SABOTAGE legs=$legs red=$red"
170[ "$legs" -eq "$red" ] || exit 1