Skip to content

test(engines): binary-gated smoke/contract tier across every engine adapter (#915) - #1014

Merged
frankbria merged 4 commits into
mainfrom
feat/915-engine-smoke-tier
Aug 1, 2026
Merged

test(engines): binary-gated smoke/contract tier across every engine adapter (#915)#1014
frankbria merged 4 commits into
mainfrom
feat/915-engine-smoke-tier

Conversation

@frankbria

Copy link
Copy Markdown
Owner

Closes #915.

Why this matters

Every external-engine adapter test patches shutil.which and subprocess.Popen and replays the output the adapter's author expected. That is not a hygiene problem — it is the mechanism by which three of the four shipped engines went out with invocations that could not do any work, each behind a fully green suite:

issue what shipped what actually happened
#913 opencode --non-interactive no such flag — it started the TUI
#914 an invented app-server handshake the real server rejected initialize
#1012 kilo run <prompt> no such subcommand — it started the TUI and hung

All three were found by pointing the real binary at the adapter's own build_command. This tier makes that the standing check rather than a thing someone happens to try.

Two legs, gated differently on purpose

Contract leg — the CLI exists, responds, and still documents the entry point the adapter depends on. Asserted against the CLI's own --help, not against our code, so an upstream rename fails here instead of silently producing no-op runs. No credentials, no model calls, ~2s. Cheap enough to gate PRs.

Task leg — one trivial task per engine driven through adapter.run() to a terminal state with a real file written. Never "exit 0" and never "reached a terminal state" alone: every bug above produced a clean-looking exit having done nothing, so the file on disk is the only evidence that survives. Needs credentials, costs money, takes minutes — additionally gated on CODEFRAME_ENGINE_SMOKE=1 (the opt-in established in #1012) and run on a schedule.

Demo — all four real CLIs, locally

Contract leg, no credentials:

test_the_cli_is_installed_and_responds[claude-code|codex|opencode|kilocode]              PASSED
test_the_cli_still_documents_the_adapters_entry_point[claude-code|codex|opencode|kilocode] PASSED
test_the_adapter_constructs_against_the_real_binary[claude-code|codex|opencode|kilocode] PASSED
12 passed, 4 skipped in 2.43s

Task leg, CODEFRAME_ENGINE_SMOKE=1, real model calls:

test_..._drives_a_trivial_task_to_a_terminal_state[claude-code]  PASSED
test_..._drives_a_trivial_task_to_a_terminal_state[codex]        PASSED
test_..._drives_a_trivial_task_to_a_terminal_state[opencode]     SKIPPED
test_..._drives_a_trivial_task_to_a_terminal_state[kilocode]     PASSED
3 passed, 1 skipped in 43.22s

Three engines drove a real task to completion and wrote real files. opencode's skip is honest, not convenient: its backend is returning UnknownError / "Unexpected server error" with a server-side ref id, reproducible from a bare opencode run outside the adapter, on both the positional and stdin transports. The adapter is fine; the provider is down.

That distinction is the one place this tier could quietly rot, so it is deliberately narrow: a small list of the engines' own server-error envelopes, and the skip reports NO COVERAGE — upstream provider fault rather than passing quietly. A broad "if it failed, skip it" would hide exactly what the tier exists to catch.

Acceptance criteria

AC Evidence
binary-gated tier runs each CLI's --help/version and drives one trivial task to a terminal state per engine test_engine_smoke_tier.py, demoed above against all four installed CLIs
codex outbound validated against a checked-in generate-json-schema fixture ✅ landed in #914fixtures/codex_app_server/ + TestCodexSchemaContract
codex timeout tests exercise a real pipe, not a fileno()-less fake ✅ landed in #914_PipeStdout over os.pipe(); the selectors path is gone entirely (reader thread + queue)
runs in a workflow with the binaries installed, required before an adapter change merges .github/workflows/engine-smoke.yml — contract leg on PRs, full tier weekly

The workflow

The contract job always runs and filters internally rather than using paths:. A path-filtered required check reports "skipped", which GitHub treats as never-satisfied — it would block every unrelated PR forever.

Review

codex review --base main (cross-family, pre-PR) raised one [P2], and it was the sharpest possible critique: my npm install … || echo "unavailable" meant a failed install left the binary absent, the tier skipped that engine, and pytest still exited 0 — a required check green while covering nothing. That is the exact failure mode this tier exists to end, reintroduced in its own workflow.

Fixed in d03a81f: installs now fail the step, and an explicit verification step fails the job naming any missing CLI. Applied to the scheduled job too — a canary that silently stops canarying is worse than no canary.

Tests

tests/core/adapters/ — 214 passed, 13 skipped (default, no credentials). ruff check clean. Workflow YAML and every run: block parse-checked.

Known limitations

  • Making the contract check required is a repo-settings change I cannot make from a PR: main's protection currently requires only Code Quality and Backend Unit Tests. Adding Engine CLI Contract to the required contexts is a one-line settings change and is the last step of AC4.
  • The npm package names are verified against the registry (@anthropic-ai/claude-code, @openai/codex, opencode-ai, @kilocode/cli), but CI installs latest while my local kilo is @kilocode/cli@0.22.0 and the registry is at 7.4.17. If that major jump changed the CLI surface, the contract leg is what will say so — which is the point.
  • opencode's task-leg coverage is unproven until its backend recovers; the contract leg covers it today.

…dapter (#915)

Every external-engine adapter test patches shutil.which and Popen and replays
the output the adapter's author expected. That is the mechanism by which three
engines shipped with invocations that could not do any work, each behind a
green suite: #913 (opencode's non-existent --non-interactive), #914 (codex's
invented app-server handshake), #1012 (kilocode's non-existent `run`).

This tier never mocks. Two legs, gated differently:

- contract: the CLI exists, responds, and still documents the entry point the
  adapter depends on, asserted against the CLI's own --help rather than our
  code. No credentials, seconds to run, so it can gate PRs.
- task: one trivial task per engine driven through adapter.run() to a terminal
  state with a real file written — never "exit 0", since every bug above
  produced a clean-looking exit having done nothing. Needs credentials and
  costs money, so it additionally requires CODEFRAME_ENGINE_SMOKE=1.

An upstream provider fault is distinguished from an adapter defect by a
deliberately narrow marker list, and reports "NO COVERAGE" rather than a quiet
pass — a broad skip here would hide exactly what the tier exists to catch.

Workflow runs the contract leg on PRs touching adapter paths (the job always
runs and filters internally: a path-filtered required check reports "skipped"
and blocks the merge forever) and the full tier weekly with the staging
credentials.

Verified locally against all four real CLIs: contract leg 12 passed in 2.4s;
task leg drove claude-code, codex and kilocode to completion with real files
written, opencode skipped on its backend's UnknownError outage.
codex review [P2]: the best-effort `npm install ... || echo` left a failed
install as an absent binary, the tier skipped that engine, and pytest still
exited 0 — a required check green while covering nothing. That is precisely
the failure mode this tier exists to end, reintroduced in its own workflow.

Installs now fail the step, and an explicit verification step fails the job
naming any missing CLI. Applied to the scheduled task job too: a canary that
silently stops canarying is worse than no canary.
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 5m 41s —— View job


GLM bug-hunting review — done

Scope: concrete defects only (logic errors, null/error paths, security, data loss, contract breaks, resource leaks). Style/coverage/architecture are covered by the general review and intentionally excluded.

  • Gather PR diff + full file context
  • Read both changed files in full
  • Inspect surrounding adapters/fixtures the tests drive (agent_adapter.py, subprocess_adapter.py, claude_code.py, opencode.py, kilocode.py, codex.py)
  • Analyze for confirmed defects
  • Post inline finding (1 minor)
  • Post summary table

Verified clean (no defect): AgentResult.{status,error,modified_files} all exist and are populated by every adapter path; constructor signatures match (ClaudeCodeAdapter(), CodexAdapter(turn_timeout_ms=…), OpenCodeAdapter(timeout_s=…), KilocodeAdapter(*, timeout_s=…), the last a keyword-only arg the test passes correctly); SubprocessAdapter resolves _binary_path via shutil.which, so cmd[0] == binary holds for claude-code/opencode/kilocode, and codex correctly skips the build_command branch via hasattr; codex's EnvironmentError-on-missing-binary can't fire because _require_binary skips first; repo fixture's baseline commit makes _git_head non-None so require_file_changes actually fires; workflow security is sound — the secrets-bearing task job is gated if: github.event_name != 'pull_request' (no fork-PR secret exposure), secrets are mapped via env: not interpolated, and base_ref/event_name aren't attacker-controlled so the inline ${{ }} use isn't an injection vector; bash arrays/set -e/pipefail handling is correct.

severity file:line finding
minor tests/core/adapters/test_engine_smoke_tier.py:49-50 _TIMEOUT_PREFIXES omits CodexAdapter's timeout strings (Turn timeout/Stall timeout/Codex app-server timed out), so a codex run that exceeds 240s fails red instead of skipping as "ran out of time locally" like the other 3 engines. Safe direction (loud, not a false green) — see inline comment with a suggestion.

Net: 1 minor finding, no major/critical. The tier's core guarantee (no false-green) is intact — the gap only makes codex over-report failures on transient provider slowness.

Comment thread tests/core/adapters/test_engine_smoke_tier.py
…g on it (#915, #1015)

The tier caught real upstream drift on its own first CI run: @kilocode/cli went
0.22.0 (2026-01, what the adapter targets) -> 7.4.17 (2026-07, what CI installs).
7.x reinstated a `run` subcommand and renamed --workspace to --dir, so the
adapter is stale against a current install. Filed as #1015.

That is a true positive, so it is not silenced — but it must not block every
future adapter PR on an unrelated migration either. The kilocode contract
expectation now carries a `known_drift` reason and xfails (non-strict) when the
installed CLI lacks the flags, naming #1015 in the report. It passes normally
against 0.22.0, and will flip to a pass by itself once the adapter is migrated,
at which point the marker must be removed.

Verified both directions: 12 passed against local 0.22.0; xfail with the drift
reason against a 7.4.17 install.
@frankbria

Copy link
Copy Markdown
Owner Author

The contract leg caught real drift on its own first CI run

Engine CLI Contract failed here — and it was a true positive, which is the best possible evidence that the tier works:

FAILED test_the_cli_still_documents_the_adapters_entry_point[kilocode]
  AssertionError: kilocode: ['--auto', '--workspace'] absent from
  `/usr/local/bin/kilo --help` — the adapter's invocation may no longer be valid

I flagged the version gap as a risk in the PR body; it turned out to be an actual break. Investigated against a real 7.4.17 install rather than guessing:

@kilocode/cli@0.22.0 (2026-01-15, my local) @kilocode/cli@7.4.17 (2026-07-29, what CI installs)
usage kilocode [options] [command] [prompt] kilo run [message..]
workspace flag --workspace <path> --dir <path>--workspace is gone
run subcommand absent present

So the adapter targets a CLI six months out of date. Filed as #1015 (P0.30) with the full comparison.

On #1012, plainly

#1012 was not wrong — kilo <prompt> --auto --workspace is correct for 0.22.0, and the TUI hang it fixed was real and reproduced in both directions. Both things are true at once: that fix was right for the installed CLI, and the adapter is stale against current kilocode. #1015 is the second half, and I would rather say that directly than let the merge imply kilocode is fully sorted.

How this PR handles it (f1b98af)

Not silenced, not blocking. The kilocode contract expectation carries a known_drift reason and xfails non-strictly when the installed CLI lacks the flags, naming #1015 in the report — the same pin pattern this repo already uses for #1007. A required check that goes red on an unrelated migration would block every future adapter PR; one that stays quiet would be the failure mode this whole tier exists to end. The xfail flips to a pass on its own once the adapter is migrated, at which point the marker must be removed (it is in #1015's acceptance criteria).

Verified both directions:

local @kilocode/cli 0.22.0 : 12 passed, 4 skipped
KILOCODE_PATH=<7.4.17>     : 3 passed, 1 xfailed
                             XFAIL ... known drift — adapter targets 0.22.0; 7.x
                             renamed --workspace to --dir and reinstated `run` (#1015)

The scheduled Engine Task Smoke job is deliberately left to fail loudly for kilocode against 7.x — it is not a required check, and a red canary on a genuinely broken engine is the correct signal until #1015 lands.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 2m 12s —— View job


GLM bug-hunting re-review — synchronize (f1b98af)

Triggered by the new commit f1b98af (kilocode known_drift xfail). Scope: concrete defects in the changed code only; style/coverage/architecture are covered by the general review and intentionally excluded.

  • Diff the new commit vs d03a81f — test file only, +23/−3; the workflow is untouched this commit
  • Read test_engine_smoke_tier.py in full + the kilocode adapter (kilocode.py) it drives
  • Check existing comments — the one prior finding (_TIMEOUT_PREFIXES, codex timeout strings) was posted on the initial push and is not re-posted
  • Analyze the new known_drift / pytest.xfail() path for defects
  • Post summary

What f1b98af adds: a known_drift: str | None field on the Engine dataclass (test_engine_smoke_tier.py:93-97), the kilocode entry carrying it (:146-155), and a pytest.xfail(...) branch in test_the_cli_still_documents_the_adapters_entry_point (:218-226).

Verified the xfail logic across every state:

  • CLI lacks the flags (drift present, e.g. 7.x) → missing non-empty + known_drift set → pytest.xfail() fires → XFAILED with the [P0.30] Kilocode adapter targets a 6-month-stale CLI — kilocode 7.x renamed --workspace to --dir and reinstated run #1015 reason. ✓
  • Adapter migrated / flags present again → missing empty → if missing and engine.known_drift short-circuits → assert not missing passes → PASSED (the "flips to a pass on its own" behavior). ✓
  • Stale known_drift left after migration is harmless — missing empty means the branch never fires, so the suite stays green. ✓
  • The other three engines (known_drift=None) with real drift → if is False → falls through to assert not missing → still fails red. The narrow pin weakens the tier for nobody but kilocode. ✓
  • _require_binary runs first and skips on absent binary, so the xfail only applies when a real install produced help output lacking the flags. ✓

The imperative pytest.xfail() (not the @pytest.mark.xfail marker) is the correct call here: it is conditional on missing, so there is no XPASS/strictness hazard. The comment's strict=False wording describes the equivalent observable behavior, not a marker the code actually uses — imprecise but not a defect. pytest is imported (:36); the field has a default so the frozen dataclass and the other 4-positional-arg Engine(...) calls stay valid.

severity file:line finding
no new findings this commit

Net: no new defects in f1b98af. The tier's no-false-green guarantee is intact.

Still open from the earlier review (f1b98af did not touch it, so not re-posted as an inline): minortests/core/adapters/test_engine_smoke_tier.py:48-51 _TIMEOUT_PREFIXES omits CodexAdapter's Turn timeout / Stall timeout / Codex app-server timed out strings (codex.py:229/345/351), so a codex run exceeding the 240s turn_timeout_ms fails red instead of skipping as "ran out of time locally". Safe direction (loud, not a false green); the suggestion block is in the prior inline comment.

…al (#915)

GLM review [minor]: _TIMEOUT_PREFIXES only knew SubprocessAdapter's wording, so
a codex run exceeding turn_timeout_ms failed red instead of reporting NO
COVERAGE. Safe direction, but it made the tier's environmental-vs-defect
distinction inconsistent for exactly one engine.

Added codex.py's three distinct timeout paths (:229 handshake, :345 stall,
:351 turn). Enumerated rather than pattern-matched on the word 'timeout' — a
loose match would start swallowing the real defects this tier exists to catch.
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 2m 31s —— View job


GLM bug-hunting re-review — synchronize (d02f98f)

Triggered by new commit d02f98f ("test(engines): recognise codex's three timeout strings as environmental"). Scope: concrete defects in the changed code only; style/coverage/architecture are covered by the general review and intentionally excluded.

  • Diff the new commit (d02f98f) vs f1b98af — test file only, +7/−1; the workflow is untouched
  • Read test_engine_smoke_tier.py in full + the codex adapter (codex.py) it drives
  • Verify the three added strings exactly match what codex.py emits (verbatim prefix)
  • Trace the _environmental_reason path end-to-end: timeout string → startswith → skip (no false-green)
  • Check existing comments — the prior _TIMEOUT_PREFIXES finding was the one this commit addresses, so not re-posted
  • Post summary

What d02f98f adds: three entries to _TIMEOUT_PREFIXES (test_engine_smoke_tier.py:55-57) — "Codex app-server timed out", "Stall timeout:", "Turn timeout:". This is precisely the minor finding raised on the initial push, now implemented (with trailing colons on the stall/turn prefixes, which is tighter than the suggested colon-less form — it cannot match a hypothetical "Turn timeout disabled").

Verified all three prefixes resolve correctly against codex.py:

prefix emitted at path into result.error startswith
"Codex app-server timed out" codex.py:229 f"Codex app-server timed out responding to '{method}'" raises _ProtocolError → caught codex.py:196AgentResult(error=str(exc))
"Stall timeout:" codex.py:345 f"Stall timeout: no events for {…}ms" direct AgentResult(status="failed", error=…) return from _stream_turn
"Turn timeout:" codex.py:351 f"Turn timeout: exceeded {…}ms" direct AgentResult(status="failed", error=…) return from _stream_turn

Cross-checked the surrounding logic for new defects:

The comment's cited line numbers (codex.py:229/345/351) are accurate.

severity file:line finding
no new findings this commit

Net: no new defects in d02f98f. The change correctly closes the prior minor finding, and the tier's no-false-green guarantee is intact.

@frankbria
frankbria merged commit 5d653f0 into main Aug 1, 2026
15 of 16 checks passed
@frankbria
frankbria deleted the feat/915-engine-smoke-tier branch August 1, 2026 05:46
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.

[P0.21] Run every shipped engine adapter against its real CLI in a binary-gated smoke/contract tier

1 participant