Skip to content

test-integrity: P2-1 测试篡改检测门(ADR-0035 / .github #86) - #20

Merged
randypanding merged 42 commits into
mainfrom
feat/test-integrity-p2-1-r2
Aug 20, 2026
Merged

test-integrity: P2-1 测试篡改检测门(ADR-0035 / .github #86)#20
randypanding merged 42 commits into
mainfrom
feat/test-integrity-p2-1-r2

Conversation

@randypanding

Copy link
Copy Markdown
Contributor

摘要

落地 P2-1(.github #86,ADR-0035):新增 test-integrity 可复用工作流,四类测试篡改直接红——

规则 检测 严重度
TI-R1 测试文件删除 / 改名移出测试路径(内容不变的 rename 不算) red
TI-R2 断言计数净下降(全 PR 净额,文件间迁移不受影响) red
TI-R3 新增抑制标记(skip/xfail/only/t.Skip/mark.skip/@ignore…) red
TI-R4 期望值改写嫌疑(测试有删改行 + 零实现文件变更) require_adr
  • fail-closed:SHA 不可解析 / diff 失败 / policy 拉取失败 / ADR 清单读不到 = 红(TI-FC)
  • 逃生门:PR title/body 引用 \bADR-NNNN\b(agent-registry/decisions 存在性校验,防幽灵)→ 豁免但计数入账(TI-COUNT escape_hatch_waived + step summary)
  • 门禁三要素同源(同 ADR-0037 决策 6):workflow caller 钉 ref;执法工具经 github.workflow_ref 解析后从同 ref checkout(不取 caller 副本;实测 github.action_ref 在 reusable 上下文随步骤动作漂移,不可用);规则/阈值读 .github main policy(缺节用内置同值缺省,拉取失败即红)
  • T8 自检双重:本仓 ci.yml test-integrity-selftest job + 执法 workflow 每次运行前置 fixture 自检(15 case 预标注全比对)

本 PR 替代 #17(原分支与并行落地的 ADR-0037/#16、ADR-0039 产生文本冲突,基于含两者的 main 重建,内容等价 + 上述钉源/policy 解析改进)。

变更文件(41)

  • 新增 .github/workflows/test-integrity.yml(reusable workflow)
  • 新增 scripts/test-integrity.sh(检测器,bash,语言无关 regex 级)
  • 新增 scripts/parse-test-integrity-policy.py(policy 解析,fail-closed;独立 .py 而非内联 heredoc——CodeQL Analyze(python) 曾因内联片段 fatal)
  • 新增 .github/requirements-test-integrity.txt(PyYAML 版本+sha256 双锚定,同 diff-coverage 钉法)
  • 新增 scripts/test-integrity-fixtures/(T8:15 case + run.sh)
  • 修改 .github/workflows/ci.yml(selftest job + gate needs,ADR-0032 严格断言)
  • 修改 README.md(工作流表 + test-integrity 接入章节)

验证清单(全部已执行)

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@randypanding, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Limit details: You’ve used all 10 included reviews currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 72f3890a-5d92-437d-b48a-0246c894b4a3

📥 Commits

Reviewing files that changed from the base of the PR and between d1de902 and 363b14c.

📒 Files selected for processing (41)
  • .github/requirements-test-integrity.txt
  • .github/workflows/ci.yml
  • .github/workflows/test-integrity.yml
  • README.md
  • scripts/parse-test-integrity-policy.py
  • scripts/test-integrity-fixtures/cases/01-delete-test-file/diff.txt
  • scripts/test-integrity-fixtures/cases/01-delete-test-file/expected.txt
  • scripts/test-integrity-fixtures/cases/02-assert-net-decline/diff.txt
  • scripts/test-integrity-fixtures/cases/02-assert-net-decline/expected.txt
  • scripts/test-integrity-fixtures/cases/03-new-skip-marker/diff.txt
  • scripts/test-integrity-fixtures/cases/03-new-skip-marker/expected.txt
  • scripts/test-integrity-fixtures/cases/04-expectation-rewrite/diff.txt
  • scripts/test-integrity-fixtures/cases/04-expectation-rewrite/expected.txt
  • scripts/test-integrity-fixtures/cases/05-normal-evolution/diff.txt
  • scripts/test-integrity-fixtures/cases/05-normal-evolution/expected.txt
  • scripts/test-integrity-fixtures/cases/06-pure-refactor/diff.txt
  • scripts/test-integrity-fixtures/cases/06-pure-refactor/expected.txt
  • scripts/test-integrity-fixtures/cases/07-escape-hatch/adr-listing.txt
  • scripts/test-integrity-fixtures/cases/07-escape-hatch/diff.txt
  • scripts/test-integrity-fixtures/cases/07-escape-hatch/env.sh
  • scripts/test-integrity-fixtures/cases/07-escape-hatch/expected.txt
  • scripts/test-integrity-fixtures/cases/08-docs-only/diff.txt
  • scripts/test-integrity-fixtures/cases/08-docs-only/expected.txt
  • scripts/test-integrity-fixtures/cases/09-test-rename/diff.txt
  • scripts/test-integrity-fixtures/cases/09-test-rename/expected.txt
  • scripts/test-integrity-fixtures/cases/10-only-marker/diff.txt
  • scripts/test-integrity-fixtures/cases/10-only-marker/expected.txt
  • scripts/test-integrity-fixtures/cases/11-fail-closed-bad-sha/env.sh
  • scripts/test-integrity-fixtures/cases/11-fail-closed-bad-sha/expected.txt
  • scripts/test-integrity-fixtures/cases/12-ghost-adr/adr-listing.txt
  • scripts/test-integrity-fixtures/cases/12-ghost-adr/diff.txt
  • scripts/test-integrity-fixtures/cases/12-ghost-adr/env.sh
  • scripts/test-integrity-fixtures/cases/12-ghost-adr/expected.txt
  • scripts/test-integrity-fixtures/cases/13-skip-removed-boundary/diff.txt
  • scripts/test-integrity-fixtures/cases/13-skip-removed-boundary/expected.txt
  • scripts/test-integrity-fixtures/cases/14-test-move-between-files/diff.txt
  • scripts/test-integrity-fixtures/cases/14-test-move-between-files/expected.txt
  • scripts/test-integrity-fixtures/cases/15-pytest-mark-skip/diff.txt
  • scripts/test-integrity-fixtures/cases/15-pytest-mark-skip/expected.txt
  • scripts/test-integrity-fixtures/run.sh
  • scripts/test-integrity.sh

Comment @coderabbitai help to get the list of available commands.

Comment on lines +73 to +82
- name: Checkout 检测器(CI-Workflows 同 ref,不取 caller 仓内副本)
if: github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Cloudbird-Software/CI-Workflows
ref: ${{ steps.toolref.outputs.ref }}
path: ciw
persist-credentials: false

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add fail-closed reusable test-integrity gate

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds reusable, fail-closed CI detection for four test-tampering patterns.
• Supports policy-controlled enforcement and validated ADR-based waivers with audit counts.
• Adds 15 fixtures, pre-enforcement self-tests, CI gating, and integration documentation.
Diagram

sequenceDiagram
    participant Caller as Caller PR
    participant Gate as Integrity Workflow
    participant Source as Pinned Tools
    participant Fixtures as T8 Fixtures
    participant Policy as Policy Repo
    participant Parser as Policy Parser
    participant Detector as Diff Detector
    participant ADR as ADR Registry
    Caller->>Gate: Invoke pinned ref
    Gate->>Source: Checkout same ref
    Gate->>Fixtures: Run 15 cases
    Gate->>Policy: Fetch testing policy
    Gate->>Parser: Parse policy YAML
    Parser-->>Detector: Export rule settings
    Gate->>Detector: Analyze PR diff
    Detector->>ADR: Validate waiver refs
    Detector-->>Caller: Verdict and counts
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Language-specific AST analyzers
  • ➕ More accurately distinguishes assertions, suppression markers, and expectation rewrites.
  • ➕ Reduces regex false positives caused by comments, strings, or unusual syntax.
  • ➖ Requires separate maintained analyzers for every supported language and test framework.
  • ➖ Increases runtime, dependency surface, and rollout complexity for a reusable organization-wide gate.
  • ➖ Still needs diff, policy, waiver, and fail-closed orchestration implemented here.
2. Central GitHub App enforcement
  • ➕ Moves enforcement outside caller-controlled workflow execution.
  • ➕ Can centralize policy, metrics, and waiver decisions across repositories.
  • ➖ Introduces a deployed service, credentials, webhook reliability, and operational ownership.
  • ➖ Requires broader permissions and substantially more infrastructure than the current P2-1 scope.

Recommendation: Keep the reusable workflow and language-neutral regex detector for P2-1: pinned tool sourcing, fail-closed inputs, centralized policy, and fixture self-tests provide a practical organization-wide baseline without service operations. Consider AST adapters later for high-volume languages if production findings show material false-positive or false-negative rates.

Files changed (41) +768 / -3

Enhancement (2) +361 / -0
parse-test-integrity-policy.pyParse centralized integrity policy safely +63/-0

Parse centralized integrity policy safely

• Loads the test_integrity YAML section, validates TI-R4 severity, and exports configured patterns through GITHUB_ENV. Missing files, malformed YAML, unavailable PyYAML, and invalid severity fail closed.

scripts/parse-test-integrity-policy.py

test-integrity.shDetect and account for test tampering +298/-0

Detect and account for test tampering

• Implements language-neutral unified-diff analysis for deleted tests, net assertion loss, new suppression markers, and test-only expectation rewrites. It fails closed on unreliable inputs, emits audit counts and summaries, and permits validated ADR waivers unless policy makes TI-R4 unwaivable.

scripts/test-integrity.sh

Tests (35) +242 / -0
diff.txtFixture a deleted Go test file +13/-0

Fixture a deleted Go test file

• Provides a unified diff deleting a test file and its assertion for TI-R1 and TI-R2 coverage.

scripts/test-integrity-fixtures/cases/01-delete-test-file/diff.txt

expected.txtAssert deleted-test rejection +4/-0

Assert deleted-test rejection

• Expects failure with TI-R1, TI-R2, deleted-file evidence, and a negative assertion balance.

scripts/test-integrity-fixtures/cases/01-delete-test-file/expected.txt

diff.txtFixture a net assertion decline +12/-0

Fixture a net assertion decline

• Removes four assertion-like lines while adding one replacement assertion in a modified test.

scripts/test-integrity-fixtures/cases/02-assert-net-decline/diff.txt

expected.txtAssert assertion-decline detection +3/-0

Assert assertion-decline detection

• Expects TI-R2 and TI-R4 failure with a net assertion count of minus three.

scripts/test-integrity-fixtures/cases/02-assert-net-decline/expected.txt

diff.txtFixture a Go test skip marker +8/-0

Fixture a Go test skip marker

• Adds a new test containing t.Skip to exercise suppression-marker detection.

scripts/test-integrity-fixtures/cases/03-new-skip-marker/diff.txt

expected.txtAssert Go skip-marker rejection +3/-0

Assert Go skip-marker rejection

• Expects TI-R3 failure and t.Skip evidence.

scripts/test-integrity-fixtures/cases/03-new-skip-marker/expected.txt

diff.txtFixture an isolated expected-value rewrite +8/-0

Fixture an isolated expected-value rewrite

• Changes a test's expected value without any implementation-file modification.

scripts/test-integrity-fixtures/cases/04-expectation-rewrite/diff.txt

expected.txtAssert expectation-rewrite detection +3/-0

Assert expectation-rewrite detection

• Expects TI-R4 failure under the require_adr severity.

scripts/test-integrity-fixtures/cases/04-expectation-rewrite/expected.txt

diff.txtFixture normal implementation and test growth +24/-0

Fixture normal implementation and test growth

• Adds an implementation file and a corresponding asserted test to represent legitimate feature evolution.

scripts/test-integrity-fixtures/cases/05-normal-evolution/diff.txt

expected.txtAccept normal test evolution +2/-0

Accept normal test evolution

• Expects a successful TI-OK verdict.

scripts/test-integrity-fixtures/cases/05-normal-evolution/expected.txt

diff.txtFixture an implementation-only refactor +7/-0

Fixture an implementation-only refactor

• Changes implementation logic without touching tests or metadata.

scripts/test-integrity-fixtures/cases/06-pure-refactor/diff.txt

expected.txtAccept implementation-only refactoring +2/-0

Accept implementation-only refactoring

• Expects a successful TI-OK verdict.

scripts/test-integrity-fixtures/cases/06-pure-refactor/expected.txt

adr-listing.txtProvide valid ADR waiver records +2/-0

Provide valid ADR waiver records

• Lists ADR files used to validate the fixture's waiver reference.

scripts/test-integrity-fixtures/cases/07-escape-hatch/adr-listing.txt

diff.txtFixture waivable assertion reduction +12/-0

Fixture waivable assertion reduction

• Provides a test-only assertion reduction that triggers TI-R2 and TI-R4.

scripts/test-integrity-fixtures/cases/07-escape-hatch/diff.txt

env.shConfigure a valid ADR escape hatch +3/-0

Configure a valid ADR escape hatch

• Supplies PR metadata referencing ADR-0035 and points the detector to the local ADR listing.

scripts/test-integrity-fixtures/cases/07-escape-hatch/env.sh

expected.txtAssert audited waiver behavior +3/-0

Assert audited waiver behavior

• Expects success through TI-ESC while retaining TI-R2 and TI-R4 evidence and recording two waived hits.

scripts/test-integrity-fixtures/cases/07-escape-hatch/expected.txt

diff.txtFixture a documentation-only change +7/-0

Fixture a documentation-only change

• Changes only a README heading to verify metadata exclusions.

scripts/test-integrity-fixtures/cases/08-docs-only/diff.txt

expected.txtAccept documentation-only changes +2/-0

Accept documentation-only changes

• Expects a successful TI-OK verdict.

scripts/test-integrity-fixtures/cases/08-docs-only/expected.txt

diff.txtFixture a pure test-file rename +4/-0

Fixture a pure test-file rename

• Renames a test file with 100 percent similarity while keeping it inside a recognized test path.

scripts/test-integrity-fixtures/cases/09-test-rename/diff.txt

expected.txtAccept pure test-file renames +2/-0

Accept pure test-file renames

• Expects a successful TI-OK verdict without treating the rename as deletion.

scripts/test-integrity-fixtures/cases/09-test-rename/expected.txt

diff.txtFixture a focused JavaScript test +8/-0

Fixture a focused JavaScript test

• Adds an it.only test and assertion to exercise focused-test suppression detection.

scripts/test-integrity-fixtures/cases/10-only-marker/diff.txt

expected.txtAssert focused-test rejection +3/-0

Assert focused-test rejection

• Expects TI-R3 failure with .only evidence.

scripts/test-integrity-fixtures/cases/10-only-marker/expected.txt

env.shConfigure unresolvable Git SHAs +2/-0

Configure unresolvable Git SHAs

• Supplies nonexistent base and head commits to exercise fail-closed Git mode.

scripts/test-integrity-fixtures/cases/11-fail-closed-bad-sha/env.sh

expected.txtAssert bad-SHA fail-closed behavior +3/-0

Assert bad-SHA fail-closed behavior

• Expects TI-FC failure identifying the unresolvable base SHA.

scripts/test-integrity-fixtures/cases/11-fail-closed-bad-sha/expected.txt

adr-listing.txtProvide an ADR listing without the waiver +1/-0

Provide an ADR listing without the waiver

• Lists only ADR-0035 so the fixture's ADR-9999 reference cannot be validated.

scripts/test-integrity-fixtures/cases/12-ghost-adr/adr-listing.txt

diff.txtFixture a rule hit with a ghost waiver +12/-0

Fixture a rule hit with a ghost waiver

• Provides an assertion reduction that triggers TI-R2 and TI-R4 before waiver validation.

scripts/test-integrity-fixtures/cases/12-ghost-adr/diff.txt

env.shConfigure a nonexistent ADR reference +2/-0

Configure a nonexistent ADR reference

• Supplies PR metadata referencing ADR-9999 and a local authoritative listing that omits it.

scripts/test-integrity-fixtures/cases/12-ghost-adr/env.sh

expected.txtAssert ghost ADR rejection +4/-0

Assert ghost ADR rejection

• Expects failure with TI-R2 and TI-R4 plus explicit ghost-ADR evidence.

scripts/test-integrity-fixtures/cases/12-ghost-adr/expected.txt

diff.txtFixture removal of an existing skip +6/-0

Fixture removal of an existing skip

• Removes t.Skip from a test to verify that TI-R3 considers only newly added suppression markers.

scripts/test-integrity-fixtures/cases/13-skip-removed-boundary/diff.txt

expected.txtLimit skip-removal findings to TI-R4 +2/-0

Limit skip-removal findings to TI-R4

• Expects only TI-R4 because the test changed without implementation changes, while TI-R3 remains clear.

scripts/test-integrity-fixtures/cases/13-skip-removed-boundary/expected.txt

diff.txtFixture assertions moving between tests +15/-0

Fixture assertions moving between tests

• Moves an assertion between two test files and adds another, producing a positive PR-wide assertion balance.

scripts/test-integrity-fixtures/cases/14-test-move-between-files/diff.txt

expected.txtVerify PR-wide assertion accounting +3/-0

Verify PR-wide assertion accounting

• Expects no TI-R2 despite per-file removal, while TI-R4 still flags test-only deleted lines.

scripts/test-integrity-fixtures/cases/14-test-move-between-files/expected.txt

diff.txtFixture a pytest skip annotation +6/-0

Fixture a pytest skip annotation

• Adds pytest.mark.skip to exercise Python suppression-marker recognition.

scripts/test-integrity-fixtures/cases/15-pytest-mark-skip/diff.txt

expected.txtAssert pytest skip rejection +3/-0

Assert pytest skip rejection

• Expects TI-R3 failure with mark.skip evidence.

scripts/test-integrity-fixtures/cases/15-pytest-mark-skip/expected.txt

run.shRun and compare all integrity fixtures +48/-0

Run and compare all integrity fixtures

• Discovers each fixture, applies optional environment inputs, runs the detector, and compares exit codes, unique rule tags, and required output patterns. It reports aggregate pass and failure counts.

scripts/test-integrity-fixtures/run.sh

Documentation (1) +27 / -1
README.mdDocument test-integrity adoption and operation +27/-1

Document test-integrity adoption and operation

• Adds the workflow to the catalog and documents caller wiring, policy inputs, non-PR behavior, audited ADR waivers, pinned tool sourcing, and fixture self-testing.

README.md

Other (3) +138 / -2
requirements-test-integrity.txtPin the policy parser dependency +5/-0

Pin the policy parser dependency

• Pins PyYAML 6.0.3 with the Python 3.12 manylinux wheel hash for reproducible, verified installation.

.github/requirements-test-integrity.txt

ci.ymlGate CI on integrity fixture self-tests +17/-2

Gate CI on integrity fixture self-tests

• Adds a five-minute job that runs all test-integrity fixtures. The aggregate gate now strictly requires both hygiene and integrity self-tests to succeed.

.github/workflows/ci.yml

test-integrity.ymlIntroduce the reusable test-integrity workflow +116/-0

Introduce the reusable test-integrity workflow

• Adds a least-privilege reusable workflow that checks out enforcement tools from its own pinned ref, self-tests them, loads central policy, and evaluates PR diffs. Non-PR events return n/a-success while policy, source, and parsing failures fail closed.

.github/workflows/test-integrity.yml

@qodo-code-review

qodo-code-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Detector checkout uses caller ref ✓ Resolved 🐞 Bug ≡ Correctness
Description
github.workflow_ref identifies the caller workflow for jobs invoked through a reusable workflow,
so deriving a ref from it and using that ref to check out Cloudbird-Software/CI-Workflows can
request a caller-only branch or SHA or load a detector revision different from the pinned reusable
workflow. The checkout can therefore fail before policy, fixture, and enforcement steps run—breaking
every PR invocation and failing closed instead of performing the gate—or violate the workflow/tool
same-source guarantee.
Code

.github/workflows/test-integrity.yml[R61-65]

+          WORKFLOW_REF: ${{ github.workflow_ref }}
+        run: |
+          set -euo pipefail
+          # workflow_ref 形如 "owner/CI-Workflows/.github/workflows/test-integrity.yml@refs/tags/v1"
+          REF="${WORKFLOW_REF##*@}"
Evidence
The workflow parses github.workflow_ref and passes the resulting caller ref to a hard-coded
CI-Workflows checkout, after which all enforcement scripts are executed from that checkout.
GitHub’s reusable-workflow context documentation states that the github context in a called
workflow is associated with the caller, distinguishes job.workflow_ref as the reusable workflow
reference, and provides job.workflow_repository and job.workflow_sha for checking out files
co-located with the reusable workflow.

.github/workflows/test-integrity.yml[61-79]
.github/workflows/test-integrity.yml[82-116]
.github/workflows/test-integrity.yml[57-79]
.github/workflows/test-integrity.yml[89-116]
🌐 For jobs in reusable workflows, job.workflow_ref refers to the reusable workflow file; the documented example checks out its own source with job.workflow_repository and job.workflow_sha.
🌐 GitHub states that the called workflow's github context is associated with the caller workflow.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The reusable workflow derives the `CI-Workflows` detector checkout ref from `github.workflow_ref`, which belongs to the caller workflow in this context. This can request a caller-only ref that does not exist in `CI-Workflows`, fail before enforcement runs, or check out a detector revision different from the pinned reusable workflow.

## Issue Context
GitHub documents that the `github` context in a called workflow is associated with the caller, while `job.workflow_ref` identifies the reusable workflow file and `job.workflow_repository` and `job.workflow_sha` support checking out files co-located with it. Use the called job’s workflow metadata so the detector is checked out from the exact immutable revision that defines the reusable workflow, preserving the workflow/tool same-source guarantee.

## Fix Focus Areas
- .github/workflows/test-integrity.yml[57-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Caller can replace policy 🐞 Bug ⛨ Security
Description
A PR can modify its caller workflow to override the unrestricted policy-repo and policy-ref
inputs, redirecting the gate from the claimed immutable central source to attacker-controlled policy
regexes. Those patterns are exported to the detector and can be made permissive enough to classify
no files or assertions, bypassing R1–R3 and potentially R4 so test tampering passes.
Code

.github/workflows/test-integrity.yml[R23-28]

+      policy-repo:
+        type: string
+        default: "Cloudbird-Software/.github"   # 规则/阈值声明来源
+      policy-ref:
+        type: string
+        default: "main"                          # C1 保护的默认分支;调用方可钉 SHA 提高不可变性
Evidence
Both policy source coordinates are unrestricted workflow_call inputs that are interpolated
directly into the Contents API request, and each supplied nonempty pattern from the fetched policy
is written to GITHUB_ENV for the subsequent detector invocation without validation against
centrally intended values. Because the documented caller YAML resides in each consuming repository,
a PR can change that YAML and thereby control both the fetched policy and the regexes used for
enforcement.

.github/workflows/test-integrity.yml[21-28]
.github/workflows/test-integrity.yml[95-106]
scripts/parse-test-integrity-policy.py[39-62]
scripts/test-integrity.sh[26-44]
.github/workflows/test-integrity.yml[23-28]
.github/workflows/test-integrity.yml[95-116]
scripts/parse-test-integrity-policy.py[58-62]
README.md[77-89]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An audited PR can alter the reusable-workflow invocation to supply an attacker-controlled `policy-repo` or `policy-ref`, redirecting enforcement to untrusted policy regexes. The parser accepts and exports those regexes, allowing the detector's matching rules to be disabled and test tampering to pass.

## Issue Context
The policy is part of the gate's trust boundary and is intended to come from a location that the reviewed PR cannot modify. The current caller-selectable workflow inputs conflict with that boundary; hard-code or strictly allowlist the governance repository, and derive or validate an immutable trusted ref.

## Fix Focus Areas
- .github/workflows/test-integrity.yml[21-28]
- .github/workflows/test-integrity.yml[95-106]
- scripts/parse-test-integrity-policy.py[39-62]
- README.md[87-89]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Renames bypass rewrite detection 🐞 Bug ≡ Correctness
Description
An edited test-to-test rename is handled by the R branch, which records assertion and suppression
counts but never increments test_mod_removed for deleted lines; TI-R4 is raised only through the
ordinary modification path. Consequently, renaming a test while rewriting or removing an
expectation—with no net assertion decrease and no implementation change—can incorrectly exit TI-OK
and evade TI-R4.
Code

scripts/test-integrity.sh[R104-106]

+      [ "$old_test" = 1 ] && assert_rem=$((assert_rem + f_ar)) || true
+      [ "$new_test" = 1 ] && assert_add=$((assert_add + f_aa)) || true
+      [ "$new_test" = 1 ] && supp_add=$((supp_add + f_sa)) || true
Evidence
Deleted hunk lines increment f_r, but only the generic modified-file branch converts f_r > 0
into test_mod_removed; the rename branch accounts for assertions and suppressions while ignoring
f_r. Because the TI-R4 verdict later requires test_mod_removed > 0, an expectation rewrite
inside an edited test rename cannot trigger TI-R4.

scripts/test-integrity.sh[97-110]
scripts/test-integrity.sh[132-145]
scripts/test-integrity.sh[245-249]
scripts/test-integrity.sh[97-109]
scripts/test-integrity.sh[132-142]
scripts/test-integrity.sh[173-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A `git diff -M` rename with content edits is classified as `R`, but the rename handler excludes deleted test lines from TI-R4 activity accounting. As a result, expectation rewrites can evade TI-R4 merely by renaming the test file.

## Issue Context
`git diff -M` emits sufficiently similar edited renames with rename metadata and normal hunks, and the parser already counts removed diff lines in `f_r`. Only the `M` path currently consumes that value for TI-R4; apply equivalent `f_r`/R4 suspect handling when either side of a rename remains a test path, and add an edited-rename fixture.

## Fix Focus Areas
- scripts/test-integrity.sh[97-110]
- scripts/test-integrity.sh[132-145]
- scripts/test-integrity.sh[245-249]
- scripts/test-integrity-fixtures/run.sh[15-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (1)
4. Non-code changes suppress TI-R4 🐞 Bug ≡ Correctness
Description
Every changed path that is neither a test nor in the narrow metadata regex is counted as an
implementation file, including arbitrary configs, lockfiles, data, or junk files. Adding or touching
any such file alongside an expectation rewrite makes impl_changed nonzero and bypasses TI-R4
without an implementation change.
Code

scripts/test-integrity.sh[R143-145]

+      else
+        is_meta "$path" || impl_changed=$((impl_changed + 1)) || true
+      fi
Evidence
The default non-source regex covers only selected documentation and repository metadata. Every other
non-test add, delete, rename, or modification increments impl_changed, and TI-R4 is evaluated only
when that counter is exactly zero.

scripts/test-integrity.sh[27-30]
scripts/test-integrity.sh[107-145]
scripts/test-integrity.sh[245-249]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
TI-R4 treats all non-test, non-metadata files as implementation source, allowing unrelated file changes to suppress the rule.

## Issue Context
Use an explicit policy-controlled implementation/source classification, or conservatively exclude known config, generated, asset, and data paths without treating the complement of a small metadata list as source.

## Fix Focus Areas
- scripts/test-integrity.sh[27-30]
- scripts/test-integrity.sh[107-145]
- scripts/test-integrity.sh[245-249]
- scripts/parse-test-integrity-policy.py[58-62]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Self-test omits count validation 🐞 Bug ⚙ Maintainability
Description
The fixture runner compares exit status, emitted rule names, and a few optional regex fragments, but
it does not systematically compare the detector's assertion, suppression, deletion, or
implementation counts. Incorrect ledger accounting can therefore pass the required self-test even
though the job is documented as fully validating those counts.
Code

scripts/test-integrity-fixtures/run.sh[R28-36]

+  got_rules="$(printf '%s\n' "$out" | { grep -oE '\[TI-[A-Z0-9-]+\]' || true; } | tr -d '[]' | sort -u | paste -sd, -)"
+
+  ok=1
+  [ "$rc" = "$exp_exit" ] || ok=0
+  [ "$got_rules" = "$exp_rules" ] || ok=0
+  while IFS= read -r pat; do
+    [ -n "$pat" ] || continue
+    printf '%s\n' "$out" | grep -qE "$pat" || ok=0
+  done < <(sed -n 's/^contains=//p' "$d/expected.txt")
Evidence
The runner reads only exit and rules as structured expectations, while contains checks are
optional and many fixtures provide none. The detector emits five separate ledger lines, so count
regressions can leave the same rule set and still pass.

scripts/test-integrity-fixtures/run.sh[18-36]
scripts/test-integrity-fixtures/cases/05-normal-evolution/expected.txt[1-2]
scripts/test-integrity-fixtures/cases/06-pure-refactor/expected.txt[1-2]
scripts/test-integrity.sh[195-202]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fixture validation does not fully compare the detector's advertised count outputs with expected values.

## Issue Context
Give each fixture explicit expected `TI-COUNT` values and parse/compare every relevant counter, rather than relying on sparse `contains=` patterns.

## Fix Focus Areas
- scripts/test-integrity-fixtures/run.sh[18-36]
- scripts/test-integrity-fixtures/cases/01-delete-test-file/expected.txt[1-4]
- scripts/test-integrity-fixtures/cases/05-normal-evolution/expected.txt[1-2]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Binary deletions evade R1 🐞 Bug ≡ Correctness
Description
A binary test asset deletion has deleted file mode but no textual --- a/path header, and the
parser does not recover its old path from the preceding diff --git header, leaving old_test=0.
As a result, binary files under test directories or binary .snap assets are classified as changed
implementation files instead of being added to deleted_tests, allowing them to finish as TI-OK and
bypass TI-R1.
Code

scripts/test-integrity.sh[R155-157]

+      "diff --git "*)  finalize_file ;;
+      "deleted file mode "*) cur_status=D ;;
+      "new file mode "*)    cur_status=A ;;
Evidence
The parser sets cur_status=D when it encounters deleted file mode, but it populates cur_old
only from a later textual --- a/ line, which binary-deletion diffs do not contain. The deletion
branch records TI-R1 only when cur_old is present and matches the explicit test-path regex—which
includes test directories and .snap assets—so these binary deletions leave old_test unset and
are not rejected.

scripts/test-integrity.sh[90-120]
scripts/test-integrity.sh[151-170]
scripts/test-integrity.sh[221-228]
scripts/test-integrity.sh[27-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Binary file deletions set deletion status but do not provide the textual `---`/`+++` file headers currently used to recover the old path. Update the parser so it can recognize a deleted binary file under a test path and record TI-R1 rather than allowing it to finish as TI-OK.

## Issue Context
TI-R1 promises to reject test-file deletion regardless of file contents, and its test-path regex intentionally includes testdata and snapshots, which can contain binary test assets. Parse and validate old/new paths from `diff --git` headers or supplement parsing with a fail-closed `git diff --name-status -M` inventory.

## Fix Focus Areas
- scripts/test-integrity.sh[151-170]
- scripts/test-integrity.sh[90-120]
- scripts/test-integrity-fixtures/run.sh[15-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Web pages:
  +8 more
Review mode: 🧠 Deep: This is a security-sensitive CI gate with substantial new workflow, shell, policy parsing, ADR escape-hatch, diff-analysis logic, and many independent edge cases across 41 files, making redundant review materially valuable.

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread scripts/test-integrity.sh
Comment on lines +143 to +145
else
is_meta "$path" || impl_changed=$((impl_changed + 1)) || true
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Non-code changes suppress ti-r4 🐞 Bug ≡ Correctness

Every changed path that is neither a test nor in the narrow metadata regex is counted as an
implementation file, including arbitrary configs, lockfiles, data, or junk files. Adding or touching
any such file alongside an expectation rewrite makes impl_changed nonzero and bypasses TI-R4
without an implementation change.
Agent Prompt
## Issue description
TI-R4 treats all non-test, non-metadata files as implementation source, allowing unrelated file changes to suppress the rule.

## Issue Context
Use an explicit policy-controlled implementation/source classification, or conservatively exclude known config, generated, asset, and data paths without treating the complement of a small metadata list as source.

## Fix Focus Areas
- scripts/test-integrity.sh[27-30]
- scripts/test-integrity.sh[107-145]
- scripts/test-integrity.sh[245-249]
- scripts/parse-test-integrity-policy.py[58-62]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +28 to +36
got_rules="$(printf '%s\n' "$out" | { grep -oE '\[TI-[A-Z0-9-]+\]' || true; } | tr -d '[]' | sort -u | paste -sd, -)"

ok=1
[ "$rc" = "$exp_exit" ] || ok=0
[ "$got_rules" = "$exp_rules" ] || ok=0
while IFS= read -r pat; do
[ -n "$pat" ] || continue
printf '%s\n' "$out" | grep -qE "$pat" || ok=0
done < <(sed -n 's/^contains=//p' "$d/expected.txt")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Self-test omits count validation 🐞 Bug ⚙ Maintainability

The fixture runner compares exit status, emitted rule names, and a few optional regex fragments, but
it does not systematically compare the detector's assertion, suppression, deletion, or
implementation counts. Incorrect ledger accounting can therefore pass the required self-test even
though the job is documented as fully validating those counts.
Agent Prompt
## Issue description
Fixture validation does not fully compare the detector's advertised count outputs with expected values.

## Issue Context
Give each fixture explicit expected `TI-COUNT` values and parse/compare every relevant counter, rather than relying on sparse `contains=` patterns.

## Fix Focus Areas
- scripts/test-integrity-fixtures/run.sh[18-36]
- scripts/test-integrity-fixtures/cases/01-delete-test-file/expected.txt[1-4]
- scripts/test-integrity-fixtures/cases/05-normal-evolution/expected.txt[1-2]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread .github/workflows/test-integrity.yml Outdated
Comment on lines +23 to +28
policy-repo:
type: string
default: "Cloudbird-Software/.github" # 规则/阈值声明来源
policy-ref:
type: string
default: "main" # C1 保护的默认分支;调用方可钉 SHA 提高不可变性

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Caller can replace policy 🐞 Bug ⛨ Security

A PR can modify its caller workflow to override the unrestricted policy-repo and policy-ref
inputs, redirecting the gate from the claimed immutable central source to attacker-controlled policy
regexes. Those patterns are exported to the detector and can be made permissive enough to classify
no files or assertions, bypassing R1–R3 and potentially R4 so test tampering passes.
Agent Prompt
## Issue description
An audited PR can alter the reusable-workflow invocation to supply an attacker-controlled `policy-repo` or `policy-ref`, redirecting enforcement to untrusted policy regexes. The parser accepts and exports those regexes, allowing the detector's matching rules to be disabled and test tampering to pass.

## Issue Context
The policy is part of the gate's trust boundary and is intended to come from a location that the reviewed PR cannot modify. The current caller-selectable workflow inputs conflict with that boundary; hard-code or strictly allowlist the governance repository, and derive or validate an immutable trusted ref.

## Fix Focus Areas
- .github/workflows/test-integrity.yml[21-28]
- .github/workflows/test-integrity.yml[95-106]
- scripts/parse-test-integrity-policy.py[39-62]
- README.md[87-89]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread scripts/test-integrity.sh
Comment on lines +104 to +106
[ "$old_test" = 1 ] && assert_rem=$((assert_rem + f_ar)) || true
[ "$new_test" = 1 ] && assert_add=$((assert_add + f_aa)) || true
[ "$new_test" = 1 ] && supp_add=$((supp_add + f_sa)) || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Renames bypass rewrite detection 🐞 Bug ≡ Correctness

An edited test-to-test rename is handled by the R branch, which records assertion and suppression
counts but never increments test_mod_removed for deleted lines; TI-R4 is raised only through the
ordinary modification path. Consequently, renaming a test while rewriting or removing an
expectation—with no net assertion decrease and no implementation change—can incorrectly exit TI-OK
and evade TI-R4.
Agent Prompt
## Issue description
A `git diff -M` rename with content edits is classified as `R`, but the rename handler excludes deleted test lines from TI-R4 activity accounting. As a result, expectation rewrites can evade TI-R4 merely by renaming the test file.

## Issue Context
`git diff -M` emits sufficiently similar edited renames with rename metadata and normal hunks, and the parser already counts removed diff lines in `f_r`. Only the `M` path currently consumes that value for TI-R4; apply equivalent `f_r`/R4 suspect handling when either side of a rename remains a test path, and add an edited-rename fixture.

## Fix Focus Areas
- scripts/test-integrity.sh[97-110]
- scripts/test-integrity.sh[132-145]
- scripts/test-integrity.sh[245-249]
- scripts/test-integrity-fixtures/run.sh[15-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread scripts/test-integrity.sh
Comment on lines +155 to +157
"diff --git "*) finalize_file ;;
"deleted file mode "*) cur_status=D ;;
"new file mode "*) cur_status=A ;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Binary deletions evade r1 🐞 Bug ≡ Correctness

A binary test asset deletion has deleted file mode but no textual --- a/path header, and the
parser does not recover its old path from the preceding diff --git header, leaving old_test=0.
As a result, binary files under test directories or binary .snap assets are classified as changed
implementation files instead of being added to deleted_tests, allowing them to finish as TI-OK and
bypass TI-R1.
Agent Prompt
## Issue description
Binary file deletions set deletion status but do not provide the textual `---`/`+++` file headers currently used to recover the old path. Update the parser so it can recognize a deleted binary file under a test path and record TI-R1 rather than allowing it to finish as TI-OK.

## Issue Context
TI-R1 promises to reject test-file deletion regardless of file contents, and its test-path regex intentionally includes testdata and snapshots, which can contain binary test assets. Parse and validate old/new paths from `diff --git` headers or supplement parsing with a fail-closed `git diff --name-status -M` inventory.

## Fix Focus Areas
- scripts/test-integrity.sh[151-170]
- scripts/test-integrity.sh[90-120]
- scripts/test-integrity-fixtures/run.sh[15-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants