Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .ci/test-count-floors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"floors": {
"core": 199,
"opencode": 1894,
"pi": 114
},
"measurement": {
"head": "1b27511a3b59f9ac8f9640d5936cb0ee146925a8",
"dirtyPaths": 0
},
"evidence": {
"_note": "How each floor was established. Two runs agreeing proves only that they shared a subject — a contaminated tree reproduces perfectly. A derivation is a second instrument with a different failure mode, so it is independent of the run but NOT of the framework's counting convention: this repo has test.each sites where declaration-counting and runner-counting disagree. Neither is an oracle; agreement between the two is the evidence.",
"core": "measured twice, plus an independent git derivation of the expected delta against another ref",
"opencode": "measured twice, plus a per-file decomposition summing to the observed delta",
"pi": "measured twice only"
}
}
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
with:
fetch-depth: 0
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4
- run: bun install --frozen-lockfile
- 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>

- run: bun run --cwd packages/opencode smoke:tui
env:
TUI_SMOKE_SKIP_BUILD: "1"
Expand Down
281 changes: 281 additions & 0 deletions scripts/check-test-count-floors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import { afterEach, expect, test } from 'bun:test'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'

const script = resolve(import.meta.dir, 'check-test-count-floors.ts')
const workspaces: string[] = []

type Floors = Record<'core' | 'opencode' | 'pi', number>

type LoweringMarker = {
reason: string
lowering: Partial<
Record<'core' | 'opencode' | 'pi', { from: number; to: number }>
>
}

function runGit(cwd: string, args: string[]) {
const result = Bun.spawnSync(['git', ...args], {
cwd,
env: {
...process.env,
GIT_AUTHOR_NAME: 'Test Runner',
GIT_AUTHOR_EMAIL: 'test@example.com',
GIT_COMMITTER_NAME: 'Test Runner',
GIT_COMMITTER_EMAIL: 'test@example.com',
},
stdout: 'pipe',
stderr: 'pipe',
})
if (result.exitCode !== 0) {
throw new Error(new TextDecoder().decode(result.stderr))
}
return new TextDecoder().decode(result.stdout).trim()
}

async function writeFloors(
cwd: string,
floors: Floors,
measurement: { head: string; dirtyPaths: number },
) {
await mkdir(join(cwd, '.ci'), { recursive: true })
await writeFile(
join(cwd, '.ci', 'test-count-floors.json'),
`${JSON.stringify({ floors, measurement }, null, 2)}\n`,
)
}

async function makeStaleBranch(
baseFloors: Floors,
staleFloors: Floors,
marker?: LoweringMarker,
) {
const cwd = await mkdtemp(join(tmpdir(), 'test-count-floor-'))
workspaces.push(cwd)

runGit(cwd, ['init', '--initial-branch=main'])
runGit(cwd, ['commit', '--allow-empty', '-m', 'seed measurement subject'])
const staleMeasurementHead = runGit(cwd, ['rev-parse', 'HEAD'])
await writeFloors(cwd, staleFloors, {
head: staleMeasurementHead,
dirtyPaths: 0,
})
runGit(cwd, ['add', '.ci/test-count-floors.json'])
runGit(cwd, ['commit', '-m', 'record stale floors'])
runGit(cwd, ['branch', 'stale'])

if (JSON.stringify(baseFloors) !== JSON.stringify(staleFloors)) {
await writeFloors(cwd, baseFloors, {
head: runGit(cwd, ['rev-parse', 'HEAD']),
dirtyPaths: 0,
})
runGit(cwd, ['add', '.ci/test-count-floors.json'])
runGit(cwd, ['commit', '-m', 'raise main floor'])
}
runGit(cwd, ['checkout', 'stale'])

if (marker) {
await writeFile(
join(cwd, '.ci', 'allow-test-count-floor-lowering.json'),
`${JSON.stringify(marker, null, 2)}\n`,
)
runGit(cwd, ['add', '.ci/allow-test-count-floor-lowering.json'])
runGit(cwd, ['commit', '-m', 'authorize floor lowering'])
}

return cwd
}

function runGate(cwd: string, counts: Floors, baseRef = 'main') {
const result = Bun.spawnSync(
[
'bun',
script,
'--floor-file',
'.ci/test-count-floors.json',
'--base-ref',
baseRef,
'--counts',
JSON.stringify(counts),
],
{ cwd, stdout: 'pipe', stderr: 'pipe' },
)
return {
exitCode: result.exitCode,
output: `${new TextDecoder().decode(result.stdout)}${new TextDecoder().decode(result.stderr)}`,
}
}

function runGateWithCounts(cwd: string, counts: string, baseRef = 'main') {
const result = Bun.spawnSync(
[
'bun',
script,
'--floor-file',
'.ci/test-count-floors.json',
'--base-ref',
baseRef,
'--counts',
counts,
],
{ cwd, stdout: 'pipe', stderr: 'pipe' },
)
return {
exitCode: result.exitCode,
output: `${new TextDecoder().decode(result.stdout)}${new TextDecoder().decode(result.stderr)}`,
}
}

afterEach(async () => {
await Promise.all(
workspaces.splice(0).map((cwd) => rm(cwd, { recursive: true })),
)
})

test('passes when counts equal the branch floors', async () => {
const floors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(floors, floors)
const result = runGate(cwd, floors)

expect(result.exitCode).toBe(0)
expect(result.output).toContain(
'VERDICT: PASS packages=core,opencode,pi (test counts and floor ratchet satisfied)',
)
})

test('passes when counts exceed the branch floors', async () => {
const floors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(floors, floors)
const result = runGate(cwd, { core: 11, opencode: 21, pi: 31 })

expect(result.exitCode).toBe(0)
expect(result.output).toContain('VERDICT: PASS')
})

test('fails when a measured count falls below its branch floor', async () => {
const floors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(floors, floors)
const result = runGate(cwd, { core: 9, opencode: 20, pi: 30 })

expect(result.exitCode).toBe(1)
expect(result.output).toContain('core measured 9 < branch floor 10')
expect(result.output).toContain('VERDICT: FAIL packages=core,opencode,pi')
})

test('fails when the branch floor is lower than the merge target floor', async () => {
const cwd = await makeStaleBranch(
{ core: 11, opencode: 20, pi: 30 },
{ core: 10, opencode: 20, pi: 30 },
)
const result = runGate(cwd, { core: 10, opencode: 20, pi: 30 })

expect(result.exitCode).toBe(1)
expect(result.output).toContain(
'core branch floor 10 < merge target floor 11',
)
})

test('passes an explicit lowering marker that names the target floor', async () => {
const cwd = await makeStaleBranch(
{ core: 11, opencode: 20, pi: 30 },
{ core: 10, opencode: 20, pi: 30 },
{
reason: 'The core suite intentionally removed obsolete coverage.',
lowering: { core: { from: 11, to: 10 } },
},
)
const result = runGate(cwd, { core: 10, opencode: 20, pi: 30 })

expect(result.exitCode).toBe(0)
expect(result.output).toContain('deliberate lowering authorized')
})

test('reports an unchecked non-zero verdict when the merge target is unavailable', async () => {
const floors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(floors, floors)
const result = runGate(cwd, floors, 'missing-target')

expect(result.exitCode).toBe(2)
expect(result.output).toContain(
'VERDICT: UNCHECKED packages=core,opencode,pi',
)
})

test('reports an unchecked verdict when CI supplies an empty merge target', async () => {
const floors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(floors, floors)
const result = runGate(cwd, floors, '')

expect(result.exitCode).toBe(2)
expect(result.output).toContain(
'VERDICT: UNCHECKED packages=core,opencode,pi',
)
})

test('reports an unchecked verdict for a floor stamped by an unrelated commit', async () => {
const staleFloors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(
{ core: 11, opencode: 20, pi: 30 },
staleFloors,
)
await writeFloors(cwd, staleFloors, {
head: runGit(cwd, ['rev-parse', 'main']),
dirtyPaths: 0,
})
const result = runGate(cwd, staleFloors)

expect(result.exitCode).toBe(2)
expect(result.output).toContain('VERDICT: UNCHECKED')
expect(result.output).toContain('not an ancestor')
})

test('allows the gate to run from a dirty working tree', async () => {
const floors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(floors, floors)
await writeFile(join(cwd, 'uncommitted-note'), 'local work is allowed\n')
const result = runGate(cwd, floors)

expect(result.exitCode).toBe(0)
expect(result.output).toContain('VERDICT: PASS')
})

test('fails as a noncompliant source when the branch floor file is absent', async () => {
const floors = { core: 10, opencode: 20, pi: 30 }
const cwd = await makeStaleBranch(floors, floors)
await rm(join(cwd, '.ci', 'test-count-floors.json'))
const result = runGate(cwd, floors)

expect(result.exitCode).toBe(1)
expect(result.output).toContain(
'VERDICT: FAIL packages=none (NONCOMPLIANT SOURCE',
)
})

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>


expect(result.exitCode).toBe(1)
expect(result.output).toContain('VERDICT: FAIL packages=none')
expect(result.output).not.toContain('VERDICT: PASS')
})

test('replays a stale branch after main raises the floor and rejects the replay', async () => {
const cwd = await makeStaleBranch(
{ core: 11, opencode: 20, pi: 30 },
{ core: 10, opencode: 20, pi: 30 },
)
const branch = Bun.spawnSync(['git', 'branch', '--show-current'], {
cwd,
stdout: 'pipe',
})
const result = runGate(cwd, { core: 10, opencode: 20, pi: 30 })

expect(new TextDecoder().decode(branch.stdout).trim()).toBe('stale')
expect(result.exitCode).toBe(1)
expect(result.output).toContain(
'core branch floor 10 < merge target floor 11',
)
})
Loading
Loading