test(gate): establish the locale precondition instead of naming one
Review found the byte-versus-character witness asserting something
adjacent to its contract. It set `LC_ALL=C.UTF-8` and assumed the
locale took effect. Locale names beyond `C` and `POSIX` are
implementation-defined, so where that one is absent the shell falls
back to byte semantics --- and then the character-counting mutant
counts bytes too, agrees with the fix, and the row passes while
proving nothing. M-G-6 was killable here and unkillable elsewhere,
which is the same as not having it.
The locale is now chosen by BEHAVIOUR. Candidates come from `locale -a`
so the set reflects what is installed, and each is probed through the
same `/bin/sh` the gate runs under, asking `${#x}` on a two-byte
character and requiring `1`. No qualifying locale is a loud panic
naming what was tried, never a skip: a skip would be indistinguishable
from a pass, which is the failure mode this replaces.
M-G-8 proves the fix in three legs, because the hazard lives in the
environment rather than the code:
8a mutant gate, probed locale -> the row fails, and the
exact-boundary row still passes
8b SAME mutant gate, locale forced to `C` -> the row passes.
The defect reproduced rather than argued.
8c no candidate can qualify -> panic naming the candidates
Also marks framing revision 6 approved and records M-G-8 in the ledger.
Gates: all nine green under `env -u TMPDIR`, log 20260813T182020Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
8e1f03a4a0
commit
3a7e3790a1
|
|
@ -331,6 +331,26 @@ from #171 and #215.
|
|||
rejection-cleanup row. Propagation is observed in a **spawned
|
||||
child**, because the gate exporting a variable would only prove the
|
||||
gate can export a variable.
|
||||
- **`M-G-8` is the one that proves `M-G-6` is not vacuous**, and it
|
||||
needs three legs because the hazard is in the *environment*, not the
|
||||
code. The multibyte row originally set `LC_ALL=C.UTF-8` and assumed
|
||||
it took: locale names beyond `C` and `POSIX` are
|
||||
implementation-defined, so on a machine lacking that locale the shell
|
||||
falls back to **byte** semantics — and then the character-counting
|
||||
mutant counts bytes too, agrees with the fix, and the row passes
|
||||
while proving nothing.
|
||||
- **8a** — mutant gate, locale chosen by the probe → the row
|
||||
**fails**, and the exact-boundary row still passes.
|
||||
- **8b** — *same mutant gate*, locale forced to `C` → the row
|
||||
**passes**. This is the defect itself, reproduced rather than
|
||||
argued: the only difference between a real witness and a vacuous
|
||||
one is whether the shell counts characters.
|
||||
- **8c** — no candidate can qualify → the helper **panics** naming
|
||||
what it tried. A skip here would be indistinguishable from a pass.
|
||||
The locale is therefore selected by **behaviour**: candidates come
|
||||
from `locale -a`, and each is probed through the same `/bin/sh` the
|
||||
gate runs under, asking `${#x}` on a two-byte character and requiring
|
||||
`1`.
|
||||
- **Proved against the live hazard:** `/tmp/.git` is still present on
|
||||
this machine, and the tests it reddened now pass with **no override**.
|
||||
- **Gates:** `./scripts/gate --acceptance gate_script_acceptance`, run
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
# `scripts/gate` — per-worktree build isolation, and one gate suite
|
||||
|
||||
**Status: revision 6 — AWAITING APPROVAL.** Revision 6 extends the
|
||||
isolation contract to `TMPDIR` (§2a below) and is the only unapproved
|
||||
part of this document; everything else is as approved. It is a
|
||||
*widening of an existing responsibility*, not a new feature: §2 already
|
||||
owns "what the gate isolates", and `TMPDIR` was simply missing from
|
||||
that list — which is how a stray `/tmp/.git` came to redden a gate run
|
||||
on an unrelated lane.
|
||||
**Status: revision 6 — APPROVED and IMPLEMENTED.** Revision 6 extends
|
||||
the isolation contract to `TMPDIR` (§2a below). It is a *widening of an
|
||||
existing responsibility*, not a new feature: §2 already owns "what the
|
||||
gate isolates", and `TMPDIR` was simply missing from that list — which
|
||||
is how a stray `/tmp/.git` came to redden a gate run on an unrelated
|
||||
lane.
|
||||
|
||||
**Approved after three review rounds, all of which turned on evidence
|
||||
rather than design.** Revisions 6a–6c tightened the socket budget to
|
||||
the Darwin floor, replaced an existence-only ancestor check with one
|
||||
that honours marker types, made the traversal canonical, moved the
|
||||
guard rows onto the exact boundary, and — last — required the
|
||||
byte-versus-character row to *establish* its locale precondition
|
||||
instead of naming one. Each correction was a witness asserting
|
||||
something adjacent to the contract while appearing to assert the
|
||||
contract itself.
|
||||
|
||||
**Previously, revision 5. Approved at revision 4 and IMPLEMENTED; revision
|
||||
5 records two safety defects review found in the implementation.**
|
||||
|
|
@ -425,7 +434,7 @@ under real parallel load, direnv is the escalation.
|
|||
|
||||
---
|
||||
|
||||
## 2a. `TMPDIR` isolation (revision 6, AWAITING APPROVAL)
|
||||
## 2a. `TMPDIR` isolation (revision 6, APPROVED)
|
||||
|
||||
**The gap.** §2 lists what a gate run isolates: the target directory and
|
||||
five ambient roots. `TMPDIR` was not on that list, so
|
||||
|
|
|
|||
|
|
@ -834,13 +834,20 @@ fn the_socket_path_guard_counts_bytes_not_characters() {
|
|||
bytes (must fail), path {path}"
|
||||
);
|
||||
|
||||
// **Chosen by BEHAVIOUR, not by name.** Locale names beyond `C` and
|
||||
// `POSIX` are implementation-defined and an unrecognized value has
|
||||
// unspecified behaviour, so setting `LC_ALL=C.UTF-8` and hoping is
|
||||
// not a precondition — where that locale is absent the shell would
|
||||
// fall back to byte semantics and the character-counting mutant
|
||||
// would pass, silently.
|
||||
let locale = char_counting_locale();
|
||||
let out = std::process::Command::new(gate())
|
||||
.arg("--self-test")
|
||||
.current_dir(repo_root())
|
||||
.env("PMACS_GATE_TARGET_ROOT", root.path())
|
||||
.env("PMACS_GATE_ALLOW_ANCESTOR_MARKER", "1")
|
||||
.env("LC_ALL", "C.UTF-8")
|
||||
.env("LANG", "C.UTF-8")
|
||||
.env("LC_ALL", &locale)
|
||||
.env("LANG", &locale)
|
||||
.env_remove("TMPDIR")
|
||||
.output()
|
||||
.expect("run gate");
|
||||
|
|
@ -853,6 +860,56 @@ fn the_socket_path_guard_counts_bytes_not_characters() {
|
|||
);
|
||||
}
|
||||
|
||||
/// A locale under which **`/bin/sh` counts CHARACTERS** — verified by
|
||||
/// asking that very shell, not by trusting a name.
|
||||
///
|
||||
/// `${#x}` on a two-byte character answers `1` under a working UTF-8
|
||||
/// locale and `2` under `C`. That difference is the entire subject of
|
||||
/// the row this serves, so the row must establish it as a precondition
|
||||
/// rather than assume it: otherwise, on a machine without the named
|
||||
/// locale, the shell counts bytes, the mutant agrees with the fix, and
|
||||
/// the test passes while proving nothing.
|
||||
///
|
||||
/// Candidates come from `locale -a`, so this reflects what is actually
|
||||
/// installed. **Fails loudly when none qualifies** — a skip here would
|
||||
/// be indistinguishable from a pass.
|
||||
fn char_counting_locale() -> String {
|
||||
let installed = std::process::Command::new("locale")
|
||||
.arg("-a")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
let candidates: Vec<String> = ["C.UTF-8".to_owned(), "C.utf8".to_owned()]
|
||||
.into_iter()
|
||||
.chain(
|
||||
installed
|
||||
.lines()
|
||||
.filter(|l| l.to_ascii_lowercase().contains("utf"))
|
||||
.map(str::to_owned),
|
||||
)
|
||||
.collect();
|
||||
|
||||
for cand in &candidates {
|
||||
let probe = std::process::Command::new("/bin/sh")
|
||||
.arg("-c")
|
||||
.arg("x=é; echo ${#x}")
|
||||
.env("LC_ALL", cand)
|
||||
.env("LANG", cand)
|
||||
.output();
|
||||
if let Ok(out) = probe
|
||||
&& String::from_utf8_lossy(&out.stdout).trim() == "1"
|
||||
{
|
||||
return cand.clone();
|
||||
}
|
||||
}
|
||||
panic!(
|
||||
"no installed locale makes /bin/sh count characters, so the \
|
||||
byte-vs-character distinction this row exists to test cannot \
|
||||
be established here. Tried: {candidates:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A root whose full path is exactly `total` bytes.
|
||||
fn root_of_len(total: usize) -> tempfile::TempDir {
|
||||
let base = short_root_base();
|
||||
|
|
|
|||
Loading…
Reference in New Issue