cd094b6dfb
Five findings from the second review round, and the first is this PR's own standard applied to its own P1. **The two fixes improvise-dj1l names had no discriminating test.** Reverting `has_key` to unification, or dropping `canon_key` from the duplicate gate, left kb-test at 27 passed. Case 11 was carried by `check_total` refusing with a DIFFERENT message, and case 10 by the other half of the fix. Both now assert which refusal fired (`said()`), plus two cases aimed at each half: a rm key with a variable in a non-final position must be refused as ABSENT rather than matching everything, and a keyed FACT whose key holds a variable must ADD without excluding the siblings it would unify with. Each proved by reverting exactly one fix and watching exactly that case fail. **Keying a rule on its whole clause made `replace` on an edited rule silently become `add`** -- both arms live, the stale one still firing, and `check_total` cannot see it because `NO + NF` is correct arithmetic for an add. Reachable on the 15 `kb_error/1` arms and the `kb_text/2` rules, so editing the check improvise-2f97 adds is the first thing to hit it. `check_rule_replace` refuses and names the route (rm, then add); the negative arm -- replace of a rule present verbatim -- is tested, or the refusal would just be "replace never works on rules". **The `decision(` arm was dead code.** `cited_name` took the first field, and the repo spells `decision(<ticket>, <name>)`, whose ticket id fails `name_shaped/1` on the hyphen -- so that marker could never fire for a real citation. A second `cited_name` clause takes the second field. All three markers are now exercised. Minor, from the same round: `_JS_TEST` no longer accepts `test.only`, which disables every other test in its file, with a case asserting a heading cannot be satisfied by one; and `node-test-test.sh`'s last case no longer duplicates an earlier one. Filed rather than fixed: improvise-w8ih, extracting `web/assemble.sh`'s gate so its own absence is visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
113 lines
4.1 KiB
Bash
Executable File
113 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# `node --test web/tests/*.test.js` is a gate that stops gating the moment the
|
|
# directory moves: bash leaves an unmatched glob as a literal, node 22+ treats
|
|
# the literal as a glob of its own, matches nothing, reports 0 tests and exits
|
|
# 0 (improvise-vz54). zsh aborts on an unmatched glob, so the failure is
|
|
# invisible from a developer's shell and visible only under CI's.
|
|
#
|
|
# So the glob is the SCRIPT's, not the shell's -- quote it at the call site.
|
|
# Three things are refused, and all three are "the gate asserted nothing"
|
|
# wearing an exit 0: a pattern that matches no file, a run in which nothing
|
|
# passed, and a matched file that registered no test.
|
|
#
|
|
# The last two are read off node's OWN report rather than scanned for lexically.
|
|
# A `grep` for `test(` is satisfied by a commented-out call, by a string
|
|
# mentioning one, by `test.skip`/`test.todo` and by an empty `test.describe` --
|
|
# every one of which executes no assertion (found by review on PR #69).
|
|
#
|
|
# Two measurements make the report usable, both on node 24.20, and both are
|
|
# taken PER FILE. `# pass` is 0 for a skip-only file, a todo-only file and an
|
|
# empty suite -- read over a whole run it would be masked by a passing
|
|
# neighbour. And a file that registered NO test is reported as one passing test
|
|
# WHOSE NAME IS THE FILE PATH, which is why `# tests` alone reads as the file
|
|
# count and a minimum-count assertion over it is satisfied by the very file it
|
|
# should reject. That naming convention is an assumption about node, so
|
|
# scripts/node-test-test.sh pins it: if a future node stops doing it, the
|
|
# self-test fails rather than the guard silently going quiet.
|
|
set -uo pipefail
|
|
|
|
usage() {
|
|
echo "usage: ${0##*/} '<glob>' ['<glob>'...]" >&2
|
|
echo " quote each glob: this script expands it, not the calling shell" >&2
|
|
exit 2
|
|
}
|
|
|
|
[ "$#" -gt 0 ] || usage
|
|
|
|
shopt -s nullglob
|
|
|
|
files=()
|
|
for pattern in "$@"; do
|
|
matched=($pattern)
|
|
if [ "${#matched[@]}" -eq 0 ]; then
|
|
echo "node-test: no file matches '$pattern'" >&2
|
|
echo "node-test: refusing to run, because node would report success here" >&2
|
|
exit 1
|
|
fi
|
|
files+=("${matched[@]}")
|
|
done
|
|
|
|
tap="$(mktemp)"
|
|
trap 'rm -f "$tap"' EXIT
|
|
|
|
# ONE FILE PER RUN. node's TAP does not attribute a passing test to the file it
|
|
# came from -- three files produce three flat `ok` lines named after the tests
|
|
# -- so a whole-run `# pass` count cannot tell a fully-skipped file from its
|
|
# passing neighbours, and a skip-only file rode in on a sibling (found by
|
|
# review on PR #69). node already forks a child per file, so this changes what
|
|
# is OBSERVABLE, not how the tests run.
|
|
status=0
|
|
total=0
|
|
silent=()
|
|
barren=()
|
|
for f in "${files[@]}"; do
|
|
node --test \
|
|
--test-reporter=spec --test-reporter-destination=stdout \
|
|
--test-reporter=tap --test-reporter-destination="$tap" \
|
|
"$f"
|
|
ran=$?
|
|
if [ "$ran" -ne 0 ]; then
|
|
status="$ran"
|
|
continue
|
|
fi
|
|
passed="$(sed -n 's/^# pass \([0-9][0-9]*\)$/\1/p' "$tap" | tail -1)"
|
|
if [ -z "$passed" ]; then
|
|
echo "node-test: could not read a pass count out of node's TAP output for $f" >&2
|
|
status=1
|
|
continue
|
|
fi
|
|
total=$((total + passed))
|
|
if [ "$passed" -eq 0 ]; then
|
|
silent+=("$f")
|
|
continue
|
|
fi
|
|
# A file that registered NO test is reported as one passing test whose name
|
|
# is the FILE PATH, which is why `# tests` reads as the file count.
|
|
awk -v want="$f" '
|
|
/^(ok|not ok) [0-9]+ - / {
|
|
desc = $0
|
|
sub(/^(ok|not ok) [0-9]+ - /, "", desc)
|
|
sub(/ # (SKIP|TODO).*$/, "", desc)
|
|
if (desc == want) { found = 1 }
|
|
}
|
|
END { exit found ? 0 : 1 }
|
|
' "$tap" && barren+=("$f")
|
|
done
|
|
|
|
if [ "${#silent[@]}" -gt 0 ]; then
|
|
echo "node-test: these files passed nothing (skipped, todo or an empty suite):" >&2
|
|
printf ' %s\n' "${silent[@]}" >&2
|
|
fi
|
|
if [ "${#barren[@]}" -gt 0 ]; then
|
|
echo "node-test: these files registered no test, so running them asserted nothing:" >&2
|
|
printf ' %s\n' "${barren[@]}" >&2
|
|
fi
|
|
if [ "$status" -ne 0 ]; then
|
|
exit "$status"
|
|
fi
|
|
if [ "${#silent[@]}" -gt 0 ] || [ "${#barren[@]}" -gt 0 ]; then
|
|
exit 1
|
|
fi
|
|
|
|
echo "node-test: $total test(s) passed over ${#files[@]} file(s)"
|