Bugfix Issue 1805 Test Suite Gate Cannot See Lost Suite - sebiboga/testlink-upgraded GitHub Wiki
Bugfix โ Issue #1805: the mandated suite-file gate could not detect a lost suite (3rd occurrence of the class tracked in #1793)
- Issue: #1805
-
Branch:
fix/issue-1805 -
Commits:
96632e116(the gate),583854d4b(the three rulebooks),e6eec7c37(regression suite), plus the docs/wiki/CHANGELOG commit -
Area: test-suite maintenance / the
ai/*.mdrulebooks. No application code, no API endpoint, no template, no i18n bundle, no DB row was touched - Severity: minor for the product, major for the safety net โ the check every agent run executes to protect the shared QA evidence trail could not detect the loss it was written to prevent, and the loss had already happened three times.
The report says the whole ## Task โ Issue #1048: Implement SSO auto-login โฆ suite is absent from the default branch, and that the mandated gate passes on
a tree where a suite is missing.
Correction, measured. The data loss was already repaired: 966a7997d
(feat(#1286), 141 0 on the suite file) carried the #1048 block back to the
default branch. At dea6f4bea the file holds 55 suite headings and
grep -c "Issue #1048" returns 1. There was nothing left to restore.
What remained open is the part the report itself calls the silent part: the absence of any check that can see such a loss. That is what this fix adds.
Reproduction (/tmp/repro1805, a throwaway clone seeded with the tracked suite
file via git show HEAD:tmp/TLU_Test_Cases.md; 4 commands, no app or DB
involved โ the defect is in the verification procedure, not in the product):
BASE suites=55 #1048=1
AFTER suites=56 #1048=0 <-- a whole suite GONE
==== MANDATED GATE, rule 9 of ai/AGENTS.md ====
$ git diff --numstat HEAD~1 HEAD -- t.md
4 0 t.md
=> 0 deletions: GATE PASSES
==== what the gate is blind to ====
$ comm -23 <(grep -E "$H" base.md | sort) <(grep -E "$H" t.md | sort)
## Task โ Issue #1048: Implement SSO auto-login (SSO_enabled) + ssodisable bypass in login.html (gap vs legacy)
$ git diff --numstat <base> HEAD -- t.md
8 23 t.md <-- 23 deleted lines, invisible to the gate
| gate (all three were mandated) | why it cannot see the loss (measured) |
|---|---|
git diff --cached --numstat -- tmp/TLU_Test_Cases.md |
empty by construction once the file is staged with -f; the file is git-ignored (.gitignore:47), so git add refuses it without -f
|
git diff --numstat HEAD~1 HEAD -- tmp/TLU_Test_Cases.md |
a per-commit property. The loss enters in an earlier commit of the branch (the rebase resolution), so every later commit is append-only against it: measured 4 0 on a file that had lost 23 lines |
grep -cE "^## (Regression|Suite|Task|Modernize) " must not drop |
a COUNT, not a set. The same reproduction grows 55 โ 56 suites while deleting one, so this passes too |
The third row matters beyond this issue: it is the check the report's Expected section proposes to add, so the report's own remedy would not have caught its own bug. Only a set difference over suite headings can.
Both sides of a rebase append at EOF of a 4.5k-line file, so git reports a
conflict whose hunk starts a few lines after the
## Task โ Issue #1048 heading. Everything outside the marked region is
auto-resolved โ here git took upstream for the heading line โ so the heading
survives while the suite body, absent from the agent's stale copy, is never
restored. The agent then resolves the visible markers; scanning only the
markers shows nothing wrong, the commit is append-only relative to its rebased
parent, and every per-commit gate reports 0 deletions.
The invariant that actually matters is a merge-base property โ "every suite
heading that existed before this run still exists" โ and nothing in the
repository computed it. docs/Bugfix-Issue-1793-โฆ had already reached that
conclusion for its own deletion gate ("compares against the merge-base with the
default branch, not HEAD~1") but never turned it into a runnable check.
A tracked, read-only gate next to the rulebooks that mandate it. It compares the
candidate suite file against the merge-base with origin/<default>
(origin/HEAD as a second try; the root commit is not used as a baseline โ
see "an unrunnable check is a FAIL") and checks seven invariants:
| # | invariant | catches |
|---|---|---|
| 1 | candidate is readable and non-empty | an unreadable/truncated candidate must stop the run, not be scored on empty output |
| 2 | candidate still contains suite headings | truncated / empty file |
| 3 | markdown code fences balanced (count > 0 and even) | a block cut mid-block |
| 4 | no suite heading left without a body (body == 0) | the #1805 auto-resolution signature: heading kept, body never restored |
| 5 |
no suite lost vs the merge-base โ set difference over the ^## suite headings |
the reported defect, and delete-one/append-two clobbers a count misses |
| 6 | no line removed vs the merge-base | a loss introduced in an earlier commit of the branch (invisible to HEAD~1) |
| 7 | the run's own suite heading is present (TLU_REQUIRE_SUITE="Issue #<n>") |
the agent forgetting its own suite |
Two pattern details that a first draft got wrong and code review caught:
-
H='^## ', notH='^## (Regression|Suite|Task|Modernize) '. All 60##headings in the tracked file are suites, but four do not use those prefixes (## T1637 โ Regression: โฆ,## 1783 โ Regression: โฆ,## 1785 โ Modernize โฆ,## 1787 โ Modernize โฆ), so the narrow pattern silently ignored them โ four suites whose loss check 6 would still catch, but not check 5. Matching the heading level is the safer default here: the consequence of a false positive is a conservative FAIL, the consequence of a false negative is a lost suite. -
TLU_REQUIRE_SUITEis matched against headings, case-insensitively. A whole-filegrep -Fis a proven false negative โ the issue number also occurs in prose, so the gate reportedown suite presenton a candidate from which that very suite had been deleted โ and three real headings spell it lowercase (## Suite 1644 โ โฆ (issue #1644)), a proven false positive.
Usage โ bash ai/verify_test_suites.sh [--allow-skip] [<candidate-file>].
Two deliberate design points:
-
An unrunnable check is a FAIL, never a silent PASS. Outside a clone, checks
5โ7 report FAIL and exit 1; only an explicit
--allow-skipdowngrades them to a visibleSKIPwith exit 0. The same applies to a weak baseline: when neitherorigin/<default>nororigin/HEADis reachable, the root commit cannot support the claim "nothing that existed before this run is missing" (it only contains the suites from repository start), so it is reported as unverifiable rather than silently accepted โ a first draft used it as a silent fallback and a review reproduced three healthy suites being declared LOST off it. For the same reason each command substitution is status-checked: an empty result from a failedgrep/diff/commis not evidence of zero. -
It lives in
ai/, nottmp/. The 22 sibling harnesses (tmp/verify_*.shโ 28 of them) sit under the git-ignoredtmp/and survive only because they were force-added โ the very ignore rule that makes this bug possible would also make the fix vanish.ai/verify_test_suites.shis tracked by an ordinarygit addand cannot be lost that way.
The script never writes, moves or deletes the suite file or anything in the
repository; it only creates and removes scratch directories under $TMPDIR
(cleaned up by an EXIT/INT/TERM trap) while resolving the baseline.

ai/AGENTS.md rule 9, ai/FIX-ISSUE.md ยง5 and ai/IMPLEMENT-TASK.md ยง5 now
require TLU_REQUIRE_SUITE="Issue #<n>" bash ai/verify_test_suites.sh, keep the
two numstat checks as extra evidence only, and state โ with the measured
numbers above โ why the per-commit gate and the count cannot work. All three were
edited in one commit so they cannot drift. Hardening only ai/AGENTS.md would
have left the hole open in the two rulebooks whose runs actually produce the
clobber (the conclusion #1793 reached).
Review note: the first version of the ai/FIX-ISSUE.md edit added the new
bullet while leaving the old "verify git diff --cached --numstat shows 0
deletions" sentence in place immediately above it, so that rulebook alone still
mandated the blind gate and contradicted itself. The old sentence was replaced,
as in the other two files.
$ bash -n ai/verify_test_suites.sh # syntax OK, no output
$ TLU_REQUIRE_SUITE="Issue #1048" bash ai/verify_test_suites.sh
suite file: tmp/TLU_Test_Cases.md
PASS candidate is readable and non-empty
PASS candidate contains suite headings
PASS markdown code fences balanced (= 72)
PASS no suite heading left without a body (= 0)
baseline: dea6f4bea (59) -> candidate: 60 suites
PASS no suite lost vs merge-base with origin/sebiboga (= 0)
PASS no line removed from the suite file vs merge-base with origin/sebiboga (= 0)
PASS own suite heading present (Issue #1048)
G1805 result: 7 PASS / 0 FAIL / 0 SKIP # exit 0Negative controls โ a gate that only ever passes proves nothing:
All commands below run bash ai/verify_test_suites.sh [<candidate>]; the counts
are the verbatim final lines of each run.
| control | doctored candidate | result |
|---|---|---|
NC1 โ the reported defect, the whole #1048 block removed, TLU_REQUIRE_SUITE="Issue #1048"
|
awk '/^## Task โ Issue #1048: โฆ/{s=1} s&&/^## /&&!/1048/{s=0} !s' |
4 PASS / 3 FAIL, exit 1 โ LOST: ## Task โ Issue #1048: โฆ, no line removed โฆ (= 23), own suite heading present FAIL, recovery hint |
| NC2 โ a different suite removed while another is appended (with a body), heading count 60 โ 60 |
#1286 block removed, ## Regression โ Issue #8888 appended, TLU_REQUIRE_SUITE="Issue #8888"
|
5 PASS / 2 FAIL, exit 1 โ LOST: ## Task โ Issue #1286: โฆ, no line removed โฆ (= 113), own-suite PASS โ the count gate reports "fine" on this file |
NC3 โ normal flow: append one well-formed suite, TLU_REQUIRE_SUITE="Issue #8888"
|
60 suites |
7 PASS / 0 FAIL, exit 0 |
| NC4 โ run outside a git clone | cd /tmp && bash โฆ/verify_test_suites.sh t.md |
4 PASS / 1 FAIL, exit 1; with --allow-skip โ 4 PASS / 0 FAIL / 1 SKIP, exit 0 |
NC5 โ clone whose root commit holds the file but with no origin/* refs |
git init + one commit |
4 PASS / 1 FAIL, exit 1 โ baseline fell back to the root commit โฆ so 'no suite lost' cannot be verified
|
NC6 โ a suite heading appended with no body (the #1805 signature), TLU_REQUIRE_SUITE="Issue #7777"
|
## Task โ Issue #7777: header survived, body lost |
6 PASS / 1 FAIL, exit 1 โ EMPTY: ## Task โ Issue #7777: โฆ
|
NC7 โ unreadable candidate (chmod 000) |
โ |
0 PASS / 1 FAIL, exit 1 โ a grep that cannot read the file returns an empty count, which must not be scored as "zero problems" |
| NC8 โ tracked file absent from the worktree (sparse checkout) |
git rm --cached + delete |
4 PASS / 1 FAIL, exit 1 โ the index fallback resolves, the weak baseline is reported; a first draft wrote the scratch copy into a non-existent directory and then "verified" a file it never wrote |
NC9 โ unknown option --bogus
|
โ |
unknown option: --bogus, exit 1 โ a typo must not be read as a candidate path |
NC10 โ invoked from ai/ instead of the repo root |
โ | 6 PASS / 0 FAIL / 1 SKIP, exit 0 โ the suite path is resolved against the repository toplevel |
Event Viewer: SELECT COUNT(*) FROM events; โ 0 rows, unchanged. Nothing in
this change reaches PHP at all.
## Regression โ Issue #1805: ai/verify_test_suites.sh sees the suite loss that the mandated gates cannot in tmp/TLU_Test_Cases.md โ 10 steps covering the syntax
gate, the clean tree, the original loss, the count-is-not-a-set control, the
append-only false-positive control, the three unrunnable-gate controls, the
header-only signature, the post-append re-run and the events table, plus the
four controls that code review added (unreadable candidate, index fallback, unknown
option, invocation from ai/). 10/10 PASS. Appended with >> โ 97
insertions / 0 deletions against dea6f4bea, verified with
git diff --numstat dea6f4bea -- tmp/TLU_Test_Cases.md and, after committing,
with git diff --numstat HEAD~1 HEAD.
-
Re-restore the
#1048suite โ already restored by966a7997d; measured 1/1 present atdea6f4bea. Nothing to restore. - Add the count check the report proposes โ measured blind (row 3 of the blind-gates table). Rejected on evidence, not on preference.
-
A sixth CI workflow running the gate โ rejected on rule 18 (do not
fight the five existing workflows), and noted as the natural follow-up: the
suite file is tracked (
git ls-tree origin/sebiboga tmp/TLU_Test_Cases.mdโ a blob), and.gitignoredoes not affect path filters on tracked paths, so an unconditionalon: pushstep runningbash ai/verify_test_suites.shwould work today and none of the five workflows does (they only mention the file in their agent prompts). A first draft of this page rejected the option on the claim that a git-ignored path cannot drive apaths:filter; that premise was wrong and is corrected here. Server-side enforcement is the real answer to a bug that has now recurred three times, but it is a workflow change, not a bug fix. -
Move the suite file out of
tmp/to a tracked path โ removes the root ignore problem, but breaks the reference contract of every existingRefs #<n>suite and every in-flight agent. Recorded as a follow-up (already noted in #1793); out of scope for a bug fix. The gate script is placed inai/instead so at least the check is unconditionally tracked. - Prose-only guidance for rebase conflicts โ cannot help: the loss is produced by git's auto-resolution outside the conflict markers, i.e. invisible to any review that reads the markers.
| File | Change |
|---|---|
ai/verify_test_suites.sh |
new, executable โ the merge-base gate (+281/-0 vs dea6f4bea, after the review fixes) |
ai/AGENTS.md |
rule 9: the ineffective checks replaced by the script, with the measured reason each is blind |
ai/FIX-ISSUE.md |
ยง5: same |
ai/IMPLEMENT-TASK.md |
ยง5: same |
tmp/TLU_Test_Cases.md |
append-only: the ## Regression โ Issue #1805 suite (+97/-0 vs dea6f4bea) |
CHANGELOG |
one [TESTING] line under 2.0.1 |
docs/Bugfix-Issue-1805-โฆmd + the wiki page of the same name |
this document |
No application code, API endpoint, template, locale bundle or DB schema was
touched, so no Event Viewer / events entry can be produced by this change
(verified: SELECT COUNT(*) FROM events; โ 0).
Update โ the blind spot was still open, and cost 87 suites (#1849)
The merge-base gate described above proves "nothing was lost since the merge-base".
It says nothing about a loss an ancestor commit already made: once
136fc4426 ("testcases: append suite for #1089") landed as a +12 / -6332
replacement of tmp/TLU_Test_Cases.md, the merge-base carried the damage and
the gate reported 6 PASS / 0 FAIL / EXIT=0 on a file that had lost 87 of its
88 suites.
ai/verify_test_suites.sh therefore gained a check 3 โ "no suite lost vs the
pre-image of the newest destructive commit in history" โ which anchors the
baseline to a commit that replaced the file instead of to a baseline that
moves with the default branch. The same input that used to pass now fails with the
87 lost headings named and the git show <sha>^:tmp/TLU_Test_Cases.md >> โฆ
recovery command. The 87 suites were also restored.
Full write-up: Bugfix-Issue-1849-Test-Suite-Clobber-87-Suites.