Skip to content

ci: add test-count floor gate with merge-target ratchet - #235

Draft
iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:ci/test-count-floor
Draft

iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:ci/test-count-floor

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Stacked on #234 — merge that first, or this compares against a base with no floors and reports UNCHECKED.

What this closes

We have no test-count floor at all. The check gate is build && core && opencode && pi, so a suite can shrink to a single test and stay green.

That is not hypothetical. In August, 153 tests in packages/core were stranded — present, correct, and executed by no gate (#171). It was caught because a test total rose by fewer than the tests added and the arithmetic did not work. Nothing mechanical would catch a recurrence.

Three enforcements, kept distinguishable

COUNT:   measured >= branch floor          the suite did not shrink
RATCHET: branch floor >= merge target's    a branch may raise, never silently lower
LOWERING: allowed, with an explicit from/to marker and a reason

The ratchet is the part that is easy to get wrong. A bare number bumped by whoever measures is carryable backward by a stale branch: a branch forked before a bump merges green and lowers the bound, and nothing goes red, because a floor is a lower bound and lower bounds do not complain about being lowered. So the gate compares against the merge target's value rather than its own.

fetch-depth: 0 is load-bearing — a shallow clone cannot resolve the merge target's floor, and without it the UNCHECKED arm would be the only arm that ever fired.

Three verdict states, not two

FAIL       a floor was violated, or the invocation was noncompliant
UNCHECKED  the merge target could not be inspected  (exit 2)
PASS

Missing branch config is a noncompliant source contract → FAIL, rebase and adopt. Inability to inspect the merge target → UNCHECKED. Collapsing those would make UNCHECKED a catch-all that quietly absorbs a real violation.

One defect found by asking what github.event.pull_request.base.sha does on a non-PR event: if (!value || …) treated a quoted --base-ref "" as a missing flag and crashed as FAIL/1 rather than UNCHECKED/2. Every direct push would have hard-failed the gate — and failed as a violation verdict, asserting floors were breached when the comparison was never attempted. A gate that cries violation when it cannot run gets disabled. The suite was green with that defect present, because no arm passed an empty flag, only a missing one. Now pinned.

Verdicts carry their scope

VERDICT: PASS packages=core,opencode,pi (test counts and floor ratchet satisfied)
VERDICT: UNCHECKED packages=core,opencode,pi (merge target ref was not provided)
VERDICT: FAIL packages=none (test-count floor check rejected invalid input)

The package set is derived from the counts actually evaluated, not a hardcoded list. An empty scope renders packages=none and never as a pass. A green that does not state its scope can be read as covering more than it does — if a package were dropped from the checked set tomorrow, a scope-free verdict line would look identical.

Proof arms

a  counts equal to floors                 PASS      0
b  counts above floors                    PASS      0
c  a count below its floor                FAIL      1
d  branch floor below merge target        FAIL      1
e  deliberate lowering with marker        PASS      0
f  unresolvable base — missing, empty,
   and absent ref                         UNCHECKED 2
g  stale-branch replay                    FAIL      1
   branch has no floor config             FAIL      1

Arm (g) is the one that matters: floors bumped on main, a branch forked before the bump carrying the old numbers. Without it the other arms only prove the script runs.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Adds a test-count floor gate with a merge-target ratchet so a test suite can no longer shrink to a single test and stay green. It records initial floors for core, opencode, and pi and measures counts itself by running each package's tests.

Behavior

  • Checks each package's measured test count against its floor, and that branch floors are never lower than the merge target's floors.
  • Deliberate lowerings are allowed only with an explicit marker file naming the from/to values and a reason.
  • Returns PASS, FAIL, or UNCHECKED; missing branch floor config is FAIL, while an unresolvable merge target or a measurement stamp that isn't a clean ancestor of the checked ref is UNCHECKED (exit 2).
  • Verdicts include the package scope actually evaluated, so a green result can't be misinterpreted.
  • CI now uses fetch-depth: 0 so the merge target's floor can be resolved.

Written for commit fd004ad. Summary will update on new commits.

Review in cubic

@iceteaSA

Copy link
Copy Markdown
Contributor Author

CI on this PR is red, and it is red for the right reason. Recording it rather than working around it.

VERDICT: UNCHECKED packages=core,opencode,pi
  (could not resolve merge target floor f9c867d1…:.ci/test-count-floors.json)
exit 2

GitHub sets base.sha from the PR's base branch, not from its stacked parent — so this PR's merge target is main at f9c867d1, which does not yet contain .ci/test-count-floors.json (verified: 404 on that path at main). The gate cannot resolve a floor to compare against, so it reports UNCHECKED and exits 2.

That is the gate doing its job. It refuses to report PASS when the comparison was never attempted, which is the property the whole change exists to establish.

#234 merges first, then this goes green on a re-run. No code change is needed here and none should be made — the fix for "the gate cannot compare" is to give it something to compare against, not to teach it to pass anyway.

Worth stating plainly because it is the honest correction to my own PR description: I wrote that splitting the landing means "the base already has floors, so the ratchet works on its first run". That is true only after #234 lands. Until then this PR is correctly blocked, and the split still does what it was for — there is no bootstrap exemption anywhere in the code, and the condition resolves by merging in order rather than by an authorized bypass.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

5 issues found across 4 files

Confidence score: 3/5

  • In scripts/check-test-count-floors.ts, the COUNT path compares against the branch floor when it exceeds the merge-target floor, so counts that satisfy the intended merge-target gate can be rejected; compare COUNT results with targetFloors while retaining the separate ratchet and lowering checks.
  • In .github/workflows/ci.yml, exit code 2 (UNCHECKED) is treated like exit code 1 (FAIL) because the step lacks continue-on-error, so base-ref or stamp validation failures can fail the entire check job instead of being reported distinctly; handle the two verdicts separately in the workflow.
  • In scripts/check-test-count-floors.ts, zero-test measurements are rejected before COUNT evaluation, preventing valid non-negative counts from being checked; allow zero for measured counts while keeping floor-file values strictly positive, and add coverage for that path in scripts/check-test-count-floors.test.ts.
  • Early verdicts in scripts/check-test-count-floors.ts report packages=core,opencode,pi even when no counts were evaluated, and the related test case does not actually exercise an empty scope because parseFloors({}) fails first; derive pre-measurement scope from evaluated counts (none when empty) and rename the test to cover invalid input explicitly.

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/check-test-count-floors.test.ts">

<violation number="1" location="scripts/check-test-count-floors.test.ts:258">
P2: This input never exercises the empty-scope case: `parseFloors` rejects `{}` before evaluation, and the invalid-input handler happens to satisfy the same `FAIL packages=none` substring. Rename this as an invalid-input test and assert the rejection detail, or provide a valid way to produce an empty scope.</violation>
</file>

<file name="scripts/check-test-count-floors.ts">

<violation number="1" location="scripts/check-test-count-floors.ts:66">
P3: When a package has zero measured tests, `--counts` rejects the valid measurement before COUNT runs. Allow non-negative values for measured counts while keeping floor-file values strictly positive.</violation>

<violation number="2" location="scripts/check-test-count-floors.ts:309">
P2: When base-ref or stamp validation exits before measurement, UNCHECKED reports `packages=core,opencode,pi` even though no counts were evaluated. Derive every early verdict scope from evaluated counts, using `none` before measurement.</violation>

<violation number="3" location="scripts/check-test-count-floors.ts:373">
P2: When the branch floor exceeds the merge-target floor, this COUNT arm rejects counts that satisfy the merge-target gate. Compare counts with `targetFloors` as specified, while retaining the separate ratchet and lowering handling.</violation>
</file>

<file name=".github/workflows/ci.yml">

<violation number="1" location=".github/workflows/ci.yml:25">
P2: The gate distinguishes UNCHECKED (exit 2) from FAIL (exit 1), but this step has no `continue-on-error`, so both exit codes fail the check job identically. Whenever `${{ github.event.pull_request.base.sha }}` cannot be resolved locally (e.g., base branch force-pushed between the event and checkout, or the floor file not present at `base.sha`), the script prints VERDICT: UNCHECKED and exits 2, turning the whole CI job red and blocking the merge just like a real floor violation — with no distinguishing status.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant CI as CI Workflow
    participant Gate as check-test-count-floors.ts
    participant Git as Git Repository
    participant FloorFile as test-count-floors.json
    participant Marker as lowering marker file
    participant Tests as Package Test Suites

    Note over CI,Tests: Test-Count Floor Gate with Merge-Target Ratchet

    CI->>Git: fetch-depth: 0 (full clone)
    CI->>Gate: bun run check-test-count-floors.ts --base-ref ${{ github.event.pull_request.base.sha }}

    Gate->>FloorFile: Read branch floor file
    alt Branch floor file missing
        Gate-->>CI: FAIL packages=none (NONCOMPLIANT SOURCE, exit 1)
    else Branch floor file exists
        Gate->>Gate: Parse & validate floor file
        
        alt No --base-ref provided
            Gate-->>CI: UNCHECKED (merge target ref not provided, exit 2)
        else --base-ref provided
            Gate->>Git: git show {baseRef}:.ci/test-count-floors.json
            alt Merge target floor unresolvable
                Gate-->>CI: UNCHECKED (could not resolve merge target, exit 2)
            else Merge target floor resolved
                Gate->>Gate: Parse & validate merge target floors

                Gate->>Gate: Validate measurement stamp (ancestor check via git merge-base)

                alt Measurement head not an ancestor
                    Gate-->>CI: UNCHECKED (stale/foreign measurement, exit 2)
                else Measurement valid
                    Gate->>Tests: Measure test counts (core, opencode, pi)
                    Tests-->>Gate: Measured counts per package

                    alt Measured count < branch floor
                        Gate-->>CI: FAIL (count violation, exit 1)
                    else Counts satisfy branch floors
                        Gate->>Gate: Compare branch floor vs merge target floor
                        
                        alt Branch floor < merge target floor (ratchet violation)
                            Gate->>Marker: Check for lowering marker file
                            alt Marker missing or invalid
                                Gate-->>CI: FAIL (ratchet violation, exit 1)
                            else Marker valid with matching from/to
                                Gate-->>CI: PASS (deliberate lowering authorized, exit 0)
                            end
                        else Branch floor >= merge target floor
                            Gate-->>CI: PASS (test counts and floor ratchet satisfied, exit 0)
                        end
                    end
                end
            end
        end
    end

    Note over Gate,CI: Verdict includes evaluated package scope:
    Note over Gate,CI: PASS/FAIL/UNCHECKED packages=core,opencode,pi or packages=none
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

test('does not pass when no packages are evaluated', async () => {
const floors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(floors, floors)
const result = runGateWithCounts(cwd, '{}')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This input never exercises the empty-scope case: parseFloors rejects {} before evaluation, and the invalid-input handler happens to satisfy the same FAIL packages=none substring. Rename this as an invalid-input test and assert the rejection detail, or provide a valid way to produce an empty scope.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-test-count-floors.test.ts, line 258:

<comment>This input never exercises the empty-scope case: `parseFloors` rejects `{}` before evaluation, and the invalid-input handler happens to satisfy the same `FAIL packages=none` substring. Rename this as an invalid-input test and assert the rejection detail, or provide a valid way to produce an empty scope.</comment>

<file context>
@@ -0,0 +1,281 @@
+test('does not pass when no packages are evaluated', async () => {
+  const floors = { core: 10, opencode: 20, pi: 30 }
+  const cwd = await makeStaleBranch(floors, floors)
+  const result = runGateWithCounts(cwd, '{}')
+
+  expect(result.exitCode).toBe(1)
</file context>

console.error(
verdict(
'UNCHECKED',
packageScope(branchDocument.floors),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When base-ref or stamp validation exits before measurement, UNCHECKED reports packages=core,opencode,pi even though no counts were evaluated. Derive every early verdict scope from evaluated counts, using none before measurement.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-test-count-floors.ts, line 309:

<comment>When base-ref or stamp validation exits before measurement, UNCHECKED reports `packages=core,opencode,pi` even though no counts were evaluated. Derive every early verdict scope from evaluated counts, using `none` before measurement.</comment>

<file context>
@@ -0,0 +1,426 @@
+    console.error(
+      verdict(
+        'UNCHECKED',
+        packageScope(branchDocument.floors),
+        'merge target ref was not provided',
+      ),
</file context>

const targetFloors = targetDocument.floors
const failures: string[] = []
for (const packageName of packageNames) {
if (counts[packageName] < branchFloors[packageName]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When the branch floor exceeds the merge-target floor, this COUNT arm rejects counts that satisfy the merge-target gate. Compare counts with targetFloors as specified, while retaining the separate ratchet and lowering handling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-test-count-floors.ts, line 373:

<comment>When the branch floor exceeds the merge-target floor, this COUNT arm rejects counts that satisfy the merge-target gate. Compare counts with `targetFloors` as specified, while retaining the separate ratchet and lowering handling.</comment>

<file context>
@@ -0,0 +1,426 @@
+  const targetFloors = targetDocument.floors
+  const failures: string[] = []
+  for (const packageName of packageNames) {
+    if (counts[packageName] < branchFloors[packageName]) {
+      failures.push(
+        `COUNT: ${packageName} measured ${counts[packageName]} < branch floor ${branchFloors[packageName]}`,
</file context>

Comment thread .github/workflows/ci.yml
- run: bun run types
- run: bun run build
- run: bun test scripts/check-test-count-floors.test.ts
- run: bun scripts/check-test-count-floors.ts --base-ref "${{ github.event.pull_request.base.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.

P2: The gate distinguishes UNCHECKED (exit 2) from FAIL (exit 1), but this step has no continue-on-error, so both exit codes fail the check job identically. Whenever ${{ github.event.pull_request.base.sha }} cannot be resolved locally (e.g., base branch force-pushed between the event and checkout, or the floor file not present at base.sha), the script prints VERDICT: UNCHECKED and exits 2, turning the whole CI job red and blocking the merge just like a real floor violation — with no distinguishing status.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 25:

<comment>The gate distinguishes UNCHECKED (exit 2) from FAIL (exit 1), but this step has no `continue-on-error`, so both exit codes fail the check job identically. Whenever `${{ github.event.pull_request.base.sha }}` cannot be resolved locally (e.g., base branch force-pushed between the event and checkout, or the floor file not present at `base.sha`), the script prints VERDICT: UNCHECKED and exits 2, turning the whole CI job red and blocking the merge just like a real floor violation — with no distinguishing status.</comment>

<file context>
@@ -15,10 +15,14 @@ jobs:
       - run: bun run types
       - run: bun run build
+      - run: bun test scripts/check-test-count-floors.test.ts
+      - run: bun scripts/check-test-count-floors.ts --base-ref "${{ github.event.pull_request.base.sha }}"
       - run: bun run --cwd packages/opencode smoke:tui
         env:
</file context>

if (
typeof value !== 'number' ||
!Number.isSafeInteger(value) ||
value <= 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: When a package has zero measured tests, --counts rejects the valid measurement before COUNT runs. Allow non-negative values for measured counts while keeping floor-file values strictly positive.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-test-count-floors.ts, line 66:

<comment>When a package has zero measured tests, `--counts` rejects the valid measurement before COUNT runs. Allow non-negative values for measured counts while keeping floor-file values strictly positive.</comment>

<file context>
@@ -0,0 +1,426 @@
+    if (
+      typeof value !== 'number' ||
+      !Number.isSafeInteger(value) ||
+      value <= 0
+    ) {
+      fail(`${source}.${packageName} must be a positive integer`)
</file context>

@iceteaSA
iceteaSA marked this pull request as draft September 17, 2026 14:51
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Marked draft until #234 lands, because a red check here reads as "broken" when the accurate signal is "not mergeable yet".

The red is the gate refusing to report PASS on a comparison it could not make — base.sha is main, which has no .ci/test-count-floors.json until #234 merges. Nothing to fix here; it cannot go green before then, and it should not.

#234 is green and independent. Once it merges I will re-run this and un-draft.

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.

1 participant