Testdata has no block-style branch:/path: fixture, so scoped-trigger parsing is unpinned #22

Closed
opened 2026-08-13 22:13:57 +03:00 by issuer-agent · 4 comments
Collaborator

Why: pipetree's parser correctly handles BLOCK-style trigger scoping —

when:
  - event: push
    branch:
      - main

verified empirically on 2026-08-13: pipetree -C ~/projects/gitea-mcp -all reads that config's trigger as branch=main event=push (correctly scoped), and reads ~/projects/pi-plugins/cluster as bare event=push OR event=pull_request. So the behavior is right today.

But nothing in the test suite pins it. Measured in the repo at that date:

  • 18 *.yaml files under internal/ testdata.
  • Every branch: occurrence across all of them is flow-style branch: [main] (4x) or scalar branch: main (2x), plus one unrelated commit-branch: "${CI_COMMIT_BRANCH}".
  • Zero block-style (branch: followed by an indented - item) fixtures. Same for path:.

Why it matters now specifically: commit 278a567 on oleks/emmett just deleted the hand-rolled check-woodpecker-redundant-triggers.xsh and cut the pre-push WPR slot over to pipetree lint. That retired check had a bug — oleks/emmett#385 — where block-style scoping was misparsed as an unscoped trigger, producing a hard-failing false positive on correctly-scoped configs. pipetree is now the SOLE guard for that class of defect, and the precise input style that broke its predecessor is untested in its own suite. A future parser refactor could regress it silently, and the regression would land straight in everyone's pre-push hook.

Note a fix for the xonsh script (oleks/emmett PR #390) did add four fixtures covering block-style branch, block-style path, flow-style, and the genuinely-redundant bare pair — but that PR was closed unmerged as superseded by the cutover, so those fixtures were deleted with it. This is the request to recreate equivalent coverage in the linter that actually survived.

Acceptance:

  • Add testdata fixture(s) with block-style branch: (indented - item list) and assert the trigger parses as SCOPED, not bare.
  • Add testdata fixture(s) with block-style path: and assert the same.
  • Assert the redundant-trigger rule stays quiet on these correctly-scoped block-style cases, matching existing coverage for flow-style branch: [main].

Links: oleks/emmett#385, oleks/emmett#380, oleks/pipetree#20, oleks/emmett PR #390 (closed unmerged, superseded)

**Why**: `pipetree`'s parser correctly handles BLOCK-style trigger scoping — ```yaml when: - event: push branch: - main ``` verified empirically on 2026-08-13: `pipetree -C ~/projects/gitea-mcp -all` reads that config's trigger as `branch=main event=push` (correctly scoped), and reads `~/projects/pi-plugins/cluster` as bare `event=push OR event=pull_request`. So the behavior is right today. But nothing in the test suite pins it. Measured in the repo at that date: - 18 `*.yaml` files under `internal/` testdata. - Every `branch:` occurrence across all of them is flow-style `branch: [main]` (4x) or scalar `branch: main` (2x), plus one unrelated `commit-branch: "${CI_COMMIT_BRANCH}"`. - **Zero** block-style (`branch:` followed by an indented `- item`) fixtures. Same for `path:`. Why it matters now specifically: commit `278a567` on oleks/emmett just **deleted** the hand-rolled `check-woodpecker-redundant-triggers.xsh` and cut the pre-push `WPR` slot over to `pipetree lint`. That retired check had a bug — oleks/emmett#385 — where block-style scoping was misparsed as an unscoped trigger, producing a hard-failing false positive on correctly-scoped configs. pipetree is now the SOLE guard for that class of defect, and the precise input style that broke its predecessor is untested in its own suite. A future parser refactor could regress it silently, and the regression would land straight in everyone's pre-push hook. Note a fix for the xonsh script (oleks/emmett PR #390) did add four fixtures covering block-style branch, block-style path, flow-style, and the genuinely-redundant bare pair — but that PR was closed unmerged as superseded by the cutover, so those fixtures were deleted with it. This is the request to recreate equivalent coverage in the linter that actually survived. **Acceptance**: - [ ] Add testdata fixture(s) with block-style `branch:` (indented `- item` list) and assert the trigger parses as SCOPED, not bare. - [ ] Add testdata fixture(s) with block-style `path:` and assert the same. - [ ] Assert the redundant-trigger rule stays quiet on these correctly-scoped block-style cases, matching existing coverage for flow-style `branch: [main]`. **Links**: oleks/emmett#385, oleks/emmett#380, oleks/pipetree#20, oleks/emmett PR #390 (closed unmerged, superseded)
oleks added the agent/wip label 2026-08-13 22:22:47 +03:00
oleks added this to the pipetree-board project 2026-08-13 22:22:48 +03:00
Owner

Done in d6a0d37 → PR oleks/pipetree#23 (test-only; no parser or rule code touched). Auto-closes on merge — unlike the emmett-side issues, this needs no deploy gate, since the change adds coverage without altering behavior.

Acceptance:

  • Block-style branch: fixture asserting the trigger parses as SCOPED — wp001_blockbranch_clean.yaml, modelled on ~/projects/gitea-mcp, the real config that surfaced oleks/emmett#385
  • Block-style path: fixture asserting the same — wp001_blockpath_clean.yaml, with two entries so a first-item-wins parse can't pass by accident
  • WP001 stays quiet on both — wired into TestWP001_CleanCases alongside the existing flow-style coverage

One finding worth recording, because it changes what this issue was asking for

The third criterion — "assert the redundant-trigger rule stays quiet" — turns out to be insufficient on its own, and satisfying only it would have produced a test that looks like protection but isn't.

A parser that loses the block-style value still leaves the branch key in the condition. WP001's bareness check only asks whether a key other than event is present, so it stays quiet whether or not the scoping actually survived. Rule silence cannot distinguish "scoping understood" from "scoping lost" — the two failure modes are indistinguishable at the verdict level.

So I added TestWP001_BlockStyleScopingParsesAsScoped, which asserts the constraint actually lands in Condition.Fields. Verified by mutation rather than by argument — making scalarOrList reject non-flow sequences (simulating oleks/emmett#385) gives:

--- PASS: TestWP001_Fires
--- PASS: TestWP001_EventListFires
--- PASS: TestWP001_CleanCases          <-- including BOTH new fixtures
--- FAIL: TestWP001_BlockStyleScopingParsesAsScoped
--- PASS: TestWP001_RemediationCarriesClass2Caveat

Only the new parse test catches it. The two fixtures this issue asked for, wired in exactly as specified, pass straight through the regression. The mutation was reverted and no parser code is in the branch.

Incidental

Adding fixtures tripped the oracle's corpus-drift guard (corpus has 26 fixtures but golden records 24) — working as designed, and a good guard. Regenerated verdicts.json with the real woodpecker-cli via -update rather than hand-editing it, so the new entries are genuine captured ground truth: Woodpecker's own parser reports both fixtures valid: true, has_when: true, steps: 1. No existing entry changed.

go test ./... green; go vet clean on the touched packages.

Done in `d6a0d37` → PR oleks/pipetree#23 (test-only; no parser or rule code touched). Auto-closes on merge — unlike the emmett-side issues, this needs no deploy gate, since the change adds coverage without altering behavior. **Acceptance:** - [x] Block-style `branch:` fixture asserting the trigger parses as SCOPED — `wp001_blockbranch_clean.yaml`, modelled on `~/projects/gitea-mcp`, the real config that surfaced oleks/emmett#385 - [x] Block-style `path:` fixture asserting the same — `wp001_blockpath_clean.yaml`, with **two** entries so a first-item-wins parse can't pass by accident - [x] WP001 stays quiet on both — wired into `TestWP001_CleanCases` alongside the existing flow-style coverage ## One finding worth recording, because it changes what this issue was asking for The third criterion — "assert the redundant-trigger rule stays quiet" — turns out to be **insufficient on its own**, and satisfying only it would have produced a test that looks like protection but isn't. A parser that loses the block-style value still leaves the `branch` **key** in the condition. WP001's bareness check only asks whether a key other than `event` is present, so it stays quiet whether or not the scoping actually survived. Rule silence cannot distinguish "scoping understood" from "scoping lost" — the two failure modes are indistinguishable at the verdict level. So I added `TestWP001_BlockStyleScopingParsesAsScoped`, which asserts the constraint actually lands in `Condition.Fields`. Verified by mutation rather than by argument — making `scalarOrList` reject non-flow sequences (simulating oleks/emmett#385) gives: ``` --- PASS: TestWP001_Fires --- PASS: TestWP001_EventListFires --- PASS: TestWP001_CleanCases <-- including BOTH new fixtures --- FAIL: TestWP001_BlockStyleScopingParsesAsScoped --- PASS: TestWP001_RemediationCarriesClass2Caveat ``` Only the new parse test catches it. The two fixtures this issue asked for, wired in exactly as specified, pass straight through the regression. The mutation was reverted and no parser code is in the branch. ## Incidental Adding fixtures tripped the oracle's corpus-drift guard (`corpus has 26 fixtures but golden records 24`) — working as designed, and a good guard. Regenerated `verdicts.json` with the **real** `woodpecker-cli` via `-update` rather than hand-editing it, so the new entries are genuine captured ground truth: Woodpecker's own parser reports both fixtures `valid: true`, `has_when: true`, `steps: 1`. No existing entry changed. `go test ./...` green; `go vet` clean on the touched packages.
oleks added spent time 6 minutes 2026-08-13 22:28:23 +03:00
Owner

⏱ worked | 13-08-26 | session oleks/e17a29e9 | +6min | session-total 6min (lane-release) | elapsed from timeline

⏱ worked | 13-08-26 | session oleks/e17a29e9 | +6min | session-total 6min (lane-release) | elapsed from timeline
oleks added agent/worked:oleks/e17a29e9agent/worked:oleks/e17a29e9:6min and removed agent/wip labels 2026-08-13 22:28:25 +03:00
oleks moved this to Review in pipetree-board on 2026-08-13 22:28:27 +03:00
oleks added the agent/wip label 2026-08-13 22:50:36 +03:00
oleks moved this to Doing in pipetree-board on 2026-08-13 22:50:38 +03:00
Owner

Done in d078f88.

Three fixtures and four assertions covering block-style branch:, path:
and event: — an indented list rather than a same-line scalar. Confirmed
the gap first: every pre-existing fixture used flow style (branch: [main])
or a scalar, so block style had zero coverage.

Mutation-checked rather than assumed. A passing test proves nothing
about whether it would catch the regression, so I reintroduced the
predecessor's bug (ignore block sequences in conditionFromMap) and
confirmed all four assertions fail — the branch/path ones with a diagnostic
that names oleks/emmett#385 outright:

block-style "branch" was dropped during parsing — the push entry would
read as unscoped, which is exactly oleks/emmett#385. Fields: map[event:push]

Then reverted; parse.go is unchanged in the commit.

Two design points worth recording:

The tests assert the parsed CONDITION, not just the verdict. A rule
staying quiet is weak evidence: if branch: alone were dropped the entry
would have one key and WP001 would fire, but if BOTH keys were dropped the
rule would also stay silent while the config went effectively unanalyzed.
Checking the fields distinguishes those two.

The clean/fires pair is load-bearing, not tidiness. Block style means
SCOPED in two fixtures and TWO UNSCOPED EVENTS in the third, so a change
that mishandled block sequences could otherwise satisfy the scoping tests
by dropping list contents entirely — passing for exactly the wrong reason.
The path fixture doubles as WP022 cover, since hasPathScoping reads the
same parsed field for a different purpose.

woodpecker-cli accepts all three, so the oracle golden moves 24 → 27.

Done in `d078f88`. Three fixtures and four assertions covering block-style `branch:`, `path:` and `event:` — an indented list rather than a same-line scalar. Confirmed the gap first: every pre-existing fixture used flow style (`branch: [main]`) or a scalar, so block style had zero coverage. **Mutation-checked rather than assumed.** A passing test proves nothing about whether it would catch the regression, so I reintroduced the predecessor's bug (ignore block sequences in `conditionFromMap`) and confirmed all four assertions fail — the branch/path ones with a diagnostic that names oleks/emmett#385 outright: block-style "branch" was dropped during parsing — the push entry would read as unscoped, which is exactly oleks/emmett#385. Fields: map[event:push] Then reverted; `parse.go` is unchanged in the commit. Two design points worth recording: **The tests assert the parsed CONDITION, not just the verdict.** A rule staying quiet is weak evidence: if `branch:` alone were dropped the entry would have one key and WP001 would fire, but if BOTH keys were dropped the rule would also stay silent while the config went effectively unanalyzed. Checking the fields distinguishes those two. **The clean/fires pair is load-bearing, not tidiness.** Block style means SCOPED in two fixtures and TWO UNSCOPED EVENTS in the third, so a change that mishandled block sequences could otherwise satisfy the scoping tests by dropping list contents entirely — passing for exactly the wrong reason. The path fixture doubles as WP022 cover, since `hasPathScoping` reads the same parsed field for a different purpose. woodpecker-cli accepts all three, so the oracle golden moves 24 → 27.
oleks closed this issue 2026-08-13 23:01:37 +03:00
oleks added spent time 12 minutes 2026-08-13 23:01:56 +03:00
Owner

⏱ worked | 13-08-26 | session oleks/32d802d6 | +12min | session-total 12min (lane-release) | elapsed from timeline

⏱ worked | 13-08-26 | session oleks/32d802d6 | +12min | session-total 12min (lane-release) | elapsed from timeline
oleks added agent/worked:oleks/32d802d6agent/worked:oleks/32d802d6:12min and removed agent/wip labels 2026-08-13 23:01:58 +03:00
Sign in to join this conversation.