-
Notifications
You must be signed in to change notification settings - Fork 48
fix(verify): require a baseline that pins the commit it names #647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1287,6 +1287,78 @@ def test_verify_dev_bundle_ancestor_baseline_passes(project): | |
| assert task.spec_file == str(sp) | ||
|
|
||
|
|
||
| def test_verify_dev_bundle_short_abbreviation_still_passes(project): | ||
| """The relaxation must survive a legitimately SHORT abbreviation: `--short` | ||
| output is shorter than git's 7-character default in a small repository, and | ||
| refusing it would discard finished bundle work over a spelling.""" | ||
| ancestor = verify.rev_parse_head(project.project) | ||
| (project.project / "story-work.txt").write_text("stories 1.1-1.3\n") | ||
| git(project.project, "add", "-A") | ||
| git(project.project, "commit", "-q", "-m", "story work") | ||
| task = make_bundle_task(project, dw_ids=("DW-1",)) | ||
| sp = project.implementation_artifacts / "spec-1-1-a.md" | ||
| write_spec(sp, "in-review", ancestor[:6]) # below same_commit's 7-char floor | ||
| (project.project / "src.txt").write_text("review fixes\n") | ||
| rj = {"workflow": "auto-dev", "spec_file": str(sp), "dw_ids": ["DW-1"]} | ||
| out = verify.verify_dev_bundle(task, project, rj) | ||
| assert out.ok | ||
|
|
||
|
|
||
| def test_verify_dev_bundle_hex_named_ref_is_refused(project): | ||
| """Hex is not a synonym for object id: git resolves `refs/heads/abcdef0` | ||
| before an abbreviated object of the same spelling, so a hex-LOOKING branch | ||
| name would reintroduce the very drift being screened out.""" | ||
| ancestor = verify.rev_parse_head(project.project) | ||
| git(project.project, "branch", "abcdef0", ancestor) | ||
| (project.project / "story-work.txt").write_text("stories 1.1-1.3\n") | ||
| git(project.project, "add", "-A") | ||
| git(project.project, "commit", "-q", "-m", "story work") | ||
| task = make_bundle_task(project, dw_ids=("DW-1",)) | ||
| sp = project.implementation_artifacts / "spec-1-1-a.md" | ||
| write_spec(sp, "in-review", "abcdef0") | ||
| (project.project / "src.txt").write_text("review fixes\n") | ||
| rj = {"workflow": "auto-dev", "spec_file": str(sp), "dw_ids": ["DW-1"]} | ||
| out = verify.verify_dev_bundle(task, project, rj) | ||
| assert not out.ok and "baseline" in out.reason | ||
|
|
||
|
|
||
| def test_verify_dev_bundle_single_char_ref_is_refused(project): | ||
| """A ref short enough to be a prefix of its own target defeats any | ||
| spelling-based test: a branch named for its target's first character DOES | ||
| begin with its own name. It is refused because it is a ref, not because of | ||
| how it is spelled.""" | ||
| ancestor = verify.rev_parse_head(project.project) | ||
| git(project.project, "branch", ancestor[0], ancestor) | ||
| (project.project / "story-work.txt").write_text("stories 1.1-1.3\n") | ||
| git(project.project, "add", "-A") | ||
| git(project.project, "commit", "-q", "-m", "story work") | ||
| task = make_bundle_task(project, dw_ids=("DW-1",)) | ||
| sp = project.implementation_artifacts / "spec-1-1-a.md" | ||
| write_spec(sp, "in-review", ancestor[0]) | ||
| (project.project / "src.txt").write_text("review fixes\n") | ||
| rj = {"workflow": "auto-dev", "spec_file": str(sp), "dw_ids": ["DW-1"]} | ||
| out = verify.verify_dev_bundle(task, project, rj) | ||
| assert not out.ok and "baseline" in out.reason | ||
|
|
||
|
|
||
| def test_verify_dev_bundle_symbolic_baseline_is_refused(project): | ||
| """A bundle spec naming a Git revision EXPRESSION rather than an object id is | ||
| resolved at verification time, not when the session stamped it, so the | ||
| ancestor relaxation applies to a claim that pins nothing.""" | ||
| ancestor = verify.rev_parse_head(project.project) | ||
| (project.project / "story-work.txt").write_text("stories 1.1-1.3\n") | ||
| git(project.project, "add", "-A") | ||
| git(project.project, "commit", "-q", "-m", "story work") | ||
| task = make_bundle_task(project, dw_ids=("DW-1",)) | ||
| sp = project.implementation_artifacts / "spec-1-1-a.md" | ||
| write_spec(sp, "in-review", "HEAD") | ||
| (project.project / "src.txt").write_text("review fixes\n") | ||
| rj = {"workflow": "auto-dev", "spec_file": str(sp), "dw_ids": ["DW-1"]} | ||
| out = verify.verify_dev_bundle(task, project, rj) | ||
| assert not out.ok and "baseline" in out.reason | ||
|
Comment on lines
+1307
to
+1358
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files 'tests/test_verify.py' '*verify*.py' | head -80
printf '%s\n' '--- test structure ---'
ast-grep outline tests/test_verify.py --match 'test_verify_dev_bundle' --view expanded | head -240
printf '%s\n' '--- relevant source references ---'
rg -n -C 8 'names_its_own_commit|verify_dev_bundle|baseline_revision|abcdef0|single_char_ref|symbolic_baseline|ablation' tests/test_verify.py . --glob '!tests/test_verify.py' --glob '!*.pyc' | head -320Repository: bmad-code-org/bmad-loop Length of output: 25474 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- production gate ---'
rg -n -C 18 'names_its_own_commit|same_commit|is_ancestor|ancestor' src/bmad_loop/verify.py
printf '%s\n' '--- test helpers and imports ---'
sed -n '1,90p' tests/test_verify.py
sed -n '1100,1325p' tests/test_verify.py
printf '%s\n' '--- ablation-documentation style near related tests ---'
sed -n '520,590p' tests/test_verify.py
sed -n '1848,1875p' tests/test_verify.pyRepository: bmad-code-org/bmad-loop Length of output: 33897 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
# Standalone model of the baseline branch in _verify_shared_gates.
# It does not import or execute repository code and does not modify files.
ancestor = "1234567890abcdef1234567890abcdef12345678"
unit_baseline = "abcdef0123456789abcdef0123456789abcdef01"
cases = {
"hex_named_ref": ("abcdef0", ancestor),
"single_char_ref": (ancestor[0], ancestor),
"symbolic_HEAD": ("HEAD", unit_baseline),
}
def same_commit(claimed, recorded):
return claimed == recorded if len(claimed) < 7 or len(recorded) < 7 else (
claimed.startswith(recorded) or recorded.startswith(claimed)
)
def mutated_baseline_gate(claimed, resolved_target):
# The requested mutation removes names_its_own_commit(...)
return same_commit(claimed, unit_baseline) or (
resolved_target == ancestor and unit_baseline != ancestor
)
for name, (claimed, resolved_target) in cases.items():
rejected_before_mutation = not (
same_commit(claimed, unit_baseline)
or (resolved_target == ancestor and claimed not in {"HEAD", "abcdef0", ancestor[0]})
)
accepted_after_mutation = mutated_baseline_gate(claimed, resolved_target)
print(f"{name}: mutated_gate_accepts={accepted_after_mutation}")
assert accepted_after_mutation, name
print("all three refusal cases become baseline-accepted under the mutation")
PYRepository: bmad-code-org/bmad-loop Length of output: 391 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
# Standalone model of the baseline branch in _verify_shared_gates.
# It does not import or execute repository code and does not modify files.
ancestor = "1234567890abcdef1234567890abcdef12345678"
unit_baseline = "abcdef0123456789abcdef0123456789abcdef01"
cases = {
"hex_named_ref": ("abcdef0", ancestor),
"single_char_ref": (ancestor[0], ancestor),
"symbolic_HEAD": ("HEAD", unit_baseline),
}
def same_commit(claimed, recorded):
return claimed == recorded if len(claimed) < 7 or len(recorded) < 7 else (
claimed.startswith(recorded) or recorded.startswith(claimed)
)
def is_ancestor(resolved_target, descendant):
# The test histories are linear: the original ancestor precedes the unit
# baseline, and HEAD resolves to the unit baseline.
return resolved_target in {ancestor, unit_baseline} and descendant == unit_baseline
for name, (claimed, resolved_target) in cases.items():
mutated_gate_accepts = same_commit(claimed, unit_baseline) or is_ancestor(
resolved_target, unit_baseline
)
print(f"{name}: mutated_gate_accepts={mutated_gate_accepts}")
assert mutated_gate_accepts, name
print("all three refusal cases become baseline-accepted under the mutation")
PYRepository: bmad-code-org/bmad-loop Length of output: 354 Document the Add a durable record stating that removing 🤖 Prompt for AI AgentsSources: Coding guidelines, Learnings |
||
| assert ancestor # the legitimate ancestor case is covered above | ||
|
|
||
|
|
||
| def test_verify_dev_bundle_foreign_baseline_still_fails(project): | ||
| """The bundle relaxation is ancestor-only: a baseline unknown to (or | ||
| diverged from) the unit's history still fails the gate.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a bundle claims a hex-named ref that matches the first 7+ characters of
task.baseline_commit(for example, branchabcdef0while the recorded baseline starts withabcdef0), the outersame_commit()check returns true without resolving the claim, so this new validation is never called; the ref can point to an older or unrelated commit and the post-session baseline gate still passes. Runnames_its_own_commit()before accepting either the equality or ancestor path so the advertised hex-ref refusal applies regardless of spelling.AGENTS.md reference: AGENTS.md:L25-L28
Useful? React with 👍 / 👎.